diff options
Diffstat (limited to 'src/net/link.zig')
| -rw-r--r-- | src/net/link.zig | 552 |
1 files changed, 552 insertions, 0 deletions
diff --git a/src/net/link.zig b/src/net/link.zig new file mode 100644 index 0000000..4277595 --- /dev/null +++ b/src/net/link.zig @@ -0,0 +1,552 @@ +//! The seam: ESP-Hosted's station data channel, bridged to `src/net/ip.zig`. +//! +//! Everything below this file is proven - the SDIO host driver, the runtime, the port table, the +//! RPC layer, the association. Everything above it is proven too: `ip.zig` has 117 host tests and a +//! mutation sweep. This file is the twenty lines of pointer handling in between, and it is the one +//! part of the path that no host test can check, because both of its neighbours are C. +//! +//! So every decision here is cited rather than inferred. +//! +//! ------------------------------------------------------------------------------------------ +//! 1. Where the received frame starts: at `buffer`, offset zero. +//! +//! This is the single most expensive thing to get wrong. A frame shifted by the 12-byte +//! `esp_payload_header` parses as garbage - the ethertype lands in the middle of a MAC address - +//! and every one of ip.zig's tests would still pass. The RX convention is established by the +//! producer and confirmed by the vendor's own consumer: +//! +//! * sdio_drv.c:830 rejects any packet whose header `offset` field is not +//! `sizeof(struct esp_payload_header)`, so the payload always begins exactly one header in. +//! * sdio_drv.c:887 `buf_handle.payload = rxbuff + offset` - `payload` already points past the +//! header. `priv_buffer_handle` (:882) is what still points at the header. +//! * sdio_drv.c:1396-1400 allocates `copy_payload = _h_malloc(buf_handle->payload_len)` and +//! memcpy's `payload_len` bytes from `buf_handle->payload` into it, then frees the original +//! buffer at :1401. So the copy is exactly the payload, nothing more. +//! * sdio_drv.c:1407-1408 `rx(api_chan, copy_payload, copy_payload, payload_len)` - `buffer` +//! and `buff_to_free` are the same pointer, and it is the start of the frame. +//! * The vendor's own consumer agrees: esp_wifi_remote_net2.c:40-51 passes `buffer` straight to +//! the netif receive function as the frame and `buff_to_free` only as the free handle. +//! +//! `H_ESP_PAYLOAD_HEADER_OFFSET` appears on the *transmit* side only (transport_drv.c:381), where +//! ESP-Hosted is *building* a buffer and has to leave room for the header it is about to write. +//! Adding it on receive would be applying the same correction twice, in the wrong direction. +//! +//! ------------------------------------------------------------------------------------------ +//! 2. Who frees, and with what. +//! +//! `copy_payload` came from `_h_malloc` (sdio_drv.c:1396), so it is freed with `_h_free` - which +//! is exactly what `HOSTED_FREE` expands to (port_esp_hosted_host_os.h:139) and what +//! `transport_sta_free_cb` reaches through `MEMPOOL_FREE` with the pool disabled +//! (transport_util.h:29-31). `onRxFrame` below frees it through `g_h.funcs->_h_free`, once, on +//! every path including the error paths, and always returns `ESP_OK`. +//! +//! Returning `ESP_OK` unconditionally is not laziness, it is the only value that is safe under +//! both of sdio_drv.c's ownership rules. With `ESP_WIFI_REMOTE_VERSION` >= 1.3.1 the callee always +//! owns the buffer and the caller never frees (:1418). Below that, and when the macro is undefined, +//! the caller frees the buffer *if the callee returned non-zero* (:1411-1416). A non-zero return +//! from a callback that has already freed is therefore a double free under one rule and a leak +//! under neither - so this file frees and returns zero, which is one free under both. +//! +//! ------------------------------------------------------------------------------------------ +//! 3. `api_chan` must not be null. +//! +//! `transport_drv_sta_tx` opens with `assert(h && h == chan_arr[ESP_STA_IF]->api_chan)` +//! (transport_drv.c:369), and the vendor's reference RX callback opens with `assert(h)` +//! (esp_wifi_remote_net2.c:41). ESP-Hosted's own registration honours that: it allocates a cookie +//! and passes it in (esp_hosted_api.c:200-203). This build compiles the C at -O2 with `-DNDEBUG` +//! (Zig adds it for every non-Debug optimize mode), so those asserts are compiled out today and a +//! null cookie would merely be an unchecked contract violation rather than a crash - which is a +//! worse outcome, not a better one. `channel_cookie` below is that non-null cookie, and it is +//! handed back to `tx` on every transmit so the identity check holds. +//! +//! ------------------------------------------------------------------------------------------ +//! 4. The transmitted frame need not outlive the call. +//! +//! `transport_drv_sta_tx` allocates its own buffer and copies into it before queueing: +//! `mempool_alloc(..., MAX_TRANSPORT_BUFFER_SIZE, true)` at transport_drv.c:372 - with the pool +//! disabled that is `_h_malloc_align(1536, 64)` (transport_util.h:21-27) - then +//! `_h_memcpy(copy_buff + H_ESP_PAYLOAD_HEADER_OFFSET, buffer, len)` at :381, and only then +//! `esp_hosted_tx(..., copy_buff, ...)` at :383. Nothing retains `buffer`. That is what makes +//! `ip.Stack`'s "the slice is borrowed for the duration of the call" contract satisfiable, and it +//! is why `sendFrame` may hand over a pointer into the stack's single transmit staging buffer. +//! +//! ------------------------------------------------------------------------------------------ +//! 5. Why there is a re-entrancy guard. +//! +//! This is the one hazard the task description does not mention and it is real. +//! +//! `ip.Stack` is a single-threaded state machine: `onFrame` may send (an ARP reply, an ICMP echo +//! reply, a TCP ACK) before it returns, and `tick` and `httpGet` may too. Sending ends in +//! `esp_hosted_tx`, whose last act is +//! `_h_queue_item(to_slave_queue[prio], &buf_handle, HOSTED_BLOCK_MAX)` (sdio_drv.c:1607). That +//! queue holds four items (`CONFIG_ESP_HOSTED_SDIO_TX_Q_SIZE 4`, src/net/hosted/sdkconfig.h:41) +//! and `_h_queue_item` with `HOSTED_BLOCK_MAX` is a *blocking* send: port.zig:734-740 forwards it +//! to `os.Queue.send`, which suspends the calling task until there is room. +//! +//! So a full transmit queue suspends whoever is inside the stack. `onRxFrame` runs on ESP-Hosted's +//! `sdio_process_rx_task`; `tick` and `httpGet` run on the application's task. Without a guard, +//! either one can be suspended mid-mutation and the other walk straight into the same `Stack`. +//! On a cooperative scheduler that is not a torn read, it is two interleaved state machines +//! sharing one transmit buffer, one TCP sequence space and one `http.out` slice. +//! +//! The guard makes that impossible, and every way it can fire has a correct answer already: +//! +//! * a frame arriving while the stack is busy is dropped, which is what a real NIC does when its +//! transmit queue is full. DHCP, ARP and TCP all retransmit. +//! * a `tick` skipped is a `tick` deferred: `ip.zig`'s timers are absolute deadlines compared +//! against `now_ms` (`dhcpTick`, `tcpTick`), not increments, so nothing is lost. +//! * `httpGet` returns `error.WouldBlock`, which is precisely the answer its protocol already +//! requires the caller to handle by calling again with identical arguments. +//! +//! Each of those is counted, so a log can say which one happened rather than leaving a stall +//! unexplained. + +const std = @import("std"); + +const ip = @import("ip.zig"); +const port = @import("port.zig"); + +// ================================================================= ESP-Hosted's C surface + +/// `esp_hosted_if_type_t`, common/esp_hosted_interface.h:14-24. +/// +/// Note the value. The enumeration opens with `ESP_INVALID_IF`, so the station interface is **1**, +/// not 0. Registering channel 0 would fall through `transport_drv_add_channel`'s switch to +/// `default:` (transport_drv.c:481-484), which logs "Not yet supported" and returns NULL after +/// having already installed a half-built channel - and `chan_arr[ESP_STA_IF]` would stay NULL, so +/// sdio_drv.c:1394 would go on discarding every station frame in silence. +const esp_sta_if: c_uint = 1; + +/// `transport_channel_tx_fn_t`, transport_drv.h:118. Returns `esp_err_t`; 0 is `ESP_OK`. +const TxFn = *const fn (h: ?*anyopaque, buffer: ?*anyopaque, len: usize) callconv(.c) c_int; + +/// `transport_channel_rx_fn_t`, transport_drv.h:119. +const RxFn = *const fn ( + h: ?*anyopaque, + buffer: ?*anyopaque, + buff_to_free: ?*anyopaque, + len: usize, +) callconv(.c) c_int; + +/// transport_drv.h:134-136. `tx` is an out-parameter: the transport writes the interface's own +/// transmit function into it (transport_drv.c:469-471) and that is the only way to obtain it. +/// +/// This is compiled in - `transport_drv.c` is on build.zig's source list - but nothing calls it, +/// because the file that normally does (`esp_hosted_api.c`'s `add_esp_wifi_remote_channels`) is +/// not compiled: this project calls `setup_transport`, `rpc_init` and `transport_drv_reconfigure` +/// directly from `src/net/all.zig`. Registering the station channel is therefore ours to do. +extern fn transport_drv_add_channel( + api_chan: ?*anyopaque, + if_type: c_uint, + secure: u8, + tx: *?TxFn, + rx: RxFn, +) ?*anyopaque; + +/// The station's MAC, through the C shim (src/net/hosted/wifi_shim.c:96). It belongs to the C6's +/// radio, not to this chip, and ARP and Ethernet framing are built on it. Valid only after +/// `hosted_wifi_sta_start`, because that is what brings the radio up on the coprocessor. +extern fn hosted_wifi_get_mac(out: *[6]u8) c_int; + +// ============================================================================== module state + +/// The one IPv4 stack. A module-level variable rather than something the caller owns, because +/// `ip.Stack.send` is `*const fn ([]const u8) void` with no context pointer: the transmit callback +/// has to reach the transport some other way, and a file-scope binding is the honest version of +/// "some other way". 3,576 bytes of .bss - see `footprint`. +var sta: ip.Stack = undefined; + +/// The `api_chan` cookie. Its address is what ESP-Hosted stores and compares; its contents are +/// never read by anyone. See note 3 in the header for why it may not be null. +var channel_cookie: u32 = 0x5354_4100; // 'STA\0', so a memory dump names it + +/// The transport's station transmit function, from `transport_drv_add_channel`'s out-parameter. +var tx_fn: ?TxFn = null; + +/// Set once the channel is registered and the stack is live. +var opened: bool = false; + +/// The re-entrancy guard. See note 5 in the header. +var in_stack: bool = false; + +pub const Stats = struct { + /// Frames handed to us by sdio_drv.c, before any filtering. + rx_frames: u32 = 0, + /// Frames whose `h` was not our cookie. Non-zero means another channel's traffic reached this + /// callback, which would be an ESP-Hosted bug and not something to paper over. + rx_wrong_channel: u32 = 0, + /// `buffer` was null, or `len` was zero or larger than an Ethernet frame. + rx_bad: u32 = 0, + /// Frames dropped because the stack was already entered. See note 5. + rx_reentrant: u32 = 0, + /// Frames actually delivered to `ip.Stack.onFrame`. + rx_delivered: u32 = 0, + /// `tick` calls that found the stack entered and did nothing. + tick_skipped: u32 = 0, + /// `httpGet`/`httpGetHost` calls answered `WouldBlock` by the guard rather than by the stack. + http_deferred: u32 = 0, + /// `resolve` calls answered `WouldBlock` by the guard rather than by the stack. The query's + /// own timer runs in `tick`, so these cost a poll and never a retransmission. + dns_deferred: u32 = 0, + /// Frames handed to the transport. + tx_frames: u32 = 0, + /// Transmits the transport rejected: not ready, throttled, or out of buffers. + tx_failed: u32 = 0, + /// Transmits attempted before the channel existed. Should be zero. + tx_no_channel: u32 = 0, + /// Frames the stack asked to send, accepted into the deferred ring. The difference between this + /// and `tx_frames` is what is still waiting for the next `tick`. + tx_queued: u32 = 0, + /// Frames dropped because the deferred ring was full when the stack tried to send. Non-zero + /// means `tick` is not keeping up with the offered load; every protocol above this retransmits, + /// so it costs latency rather than correctness. + tx_ring_full: u32 = 0, + /// Frames the stack offered with an impossible length. Should be zero; a non-zero value points + /// at ip.zig rather than at the transport. + tx_bad: u32 = 0, +}; + +var counters: Stats = .{}; + +/// Everything this file adds to .bss, so the number in a report cannot rot. The stack dominates it. +pub const footprint: usize = + @sizeOf(@TypeOf(sta)) + + @sizeOf(@TypeOf(channel_cookie)) + + @sizeOf(@TypeOf(tx_fn)) + + @sizeOf(@TypeOf(opened)) + + @sizeOf(@TypeOf(in_stack)) + + @sizeOf(@TypeOf(counters)) + + @sizeOf(@TypeOf(tx_ring)); + +// ================================================================================= transmit + +/// `ip.Stack.send`. The slice is borrowed for the duration of this call only, which is exactly what +/// the transport needs - see note 4 in the header. +fn sendFrame(frame: []const u8) void { + if (tx_fn == null) { + counters.tx_no_channel += 1; + return; + } + if (frame.len == 0 or frame.len > ip.frame_max) { + counters.tx_bad += 1; + return; + } + // Queued, never transmitted from here. See `flushTx`. + const next = (tx_ring.head + 1) % tx_ring_slots; + if (next == tx_ring.tail) { + counters.tx_ring_full += 1; + return; + } + @memcpy(tx_ring.slot[tx_ring.head][0..frame.len], frame); + tx_ring.len[tx_ring.head] = @intCast(frame.len); + tx_ring.head = next; + counters.tx_queued += 1; +} + +/// Hand every queued frame to ESP-Hosted. MUST be called only from a task that may block. +/// +/// This indirection is the fix for a deadlock the board demonstrated, and it is worth stating +/// exactly because the shape of it is not obvious. +/// +/// `ip.Stack.onFrame` answers things: an ARP request gets a reply, an ICMP echo gets an echo, a TCP +/// segment gets an ACK. So a received frame turns into a transmitted frame inside `onFrame`. But +/// `onFrame` runs on ESP-Hosted's `sdio_process_rx_task`, and transmitting ends in +/// `_h_queue_item(to_slave_queue, HOSTED_BLOCK_MAX)` (sdio_drv.c:1607), which SUSPENDS the caller +/// when the queue is full. Suspend the RX task and it stops draining the receive queue; the receive +/// queue fills; ESP-Hosted logs "task still writing Rx data to queue!" and stops delivering. +/// Everything then looks like a dead IP stack. +/// +/// Measured on the board before this change: frames received froze at 17 and never advanced again, +/// no ping was ever answered, and the HTTP GET failed with HostUnreachable because the ARP reply it +/// needed was never sent. Raising the SDIO queue depth from 4 to 16 only moved the number. +/// +/// So the receive path now only ever copies into this ring, which cannot block, and the application +/// task drains it from `tick`. The cost is one copy and `tx_ring_slots * frame_max` of .bss. +fn flushTx() void { + const tx = tx_fn orelse return; + while (tx_ring.tail != tx_ring.head) { + const i = tx_ring.tail; + const n = tx_ring.len[i]; + counters.tx_frames += 1; + // The const cast is sound and it is load-bearing that it is: `transport_drv_sta_tx` reads + // `buffer` exactly once, as the source of a memcpy into its own aligned buffer + // (transport_drv.c:381), and neither writes through it nor retains it. ESP-Hosted's + // signature is simply not const-correct. + const rc = tx(@ptrCast(&channel_cookie), @ptrCast(&tx_ring.slot[i]), n); + if (rc != 0) counters.tx_failed += 1; + // Advance only after the call returns, so a frame is never handed out twice. + tx_ring.tail = (i + 1) % tx_ring_slots; + } +} + +/// Outgoing frames waiting for a task that may block. +/// +/// Four slots, at `ip.frame_max` each. Enough that the replies one pass of received frames can +/// generate - an ARP answer, an ICMP echo, a TCP ACK - all fit, since the whole ring is drained on +/// the very next `tick`. A full ring drops the newest frame and counts it, which is what a real +/// network interface does under load, and every protocol above this retransmits. +/// +/// Deliberately small: this is .bss competing with the heap ESP-Hosted allocates every received +/// frame from, and eight slots cost 12 KB that the transport needs more than this ring does. +const tx_ring_slots = 4; + +var tx_ring: struct { + slot: [tx_ring_slots][ip.frame_max]u8 = undefined, + len: [tx_ring_slots]u16 = @splat(0), + head: usize = 0, + tail: usize = 0, +} = .{}; + +// ================================================================================== receive + +/// `transport_channel_rx_fn_t`. Called from ESP-Hosted's `sdio_process_rx_task` +/// (sdio_drv.c:1407), which is one of the tasks `port.zig` spawned on this project's own runtime. +/// +/// The buffer is ours the moment this is entered, and it is freed on every path. See notes 1 and 2. +fn onRxFrame( + h: ?*anyopaque, + buffer: ?*anyopaque, + buff_to_free: ?*anyopaque, + len: usize, +) callconv(.c) c_int { + // `HOSTED_FREE(buff)` is `g_h.funcs->_h_free(buff)` (port_esp_hosted_host_os.h:139), and this + // is that call. First statement in the function so that no early return can miss it: the + // failure mode of a missed free here is not a leak that shows up in a heap report, it is the + // 32 KiB heap exhausted in a few seconds of the AP's broadcast traffic. + defer port.g_h.funcs.free(buff_to_free); + + counters.rx_frames += 1; + + if (h != @as(?*anyopaque, @ptrCast(&channel_cookie))) { + counters.rx_wrong_channel += 1; + return 0; + } + const bytes: [*]const u8 = @ptrCast(buffer orelse { + counters.rx_bad += 1; + return 0; + }); + if (!opened or len == 0 or len > ip.frame_max) { + counters.rx_bad += 1; + return 0; + } + if (in_stack) { + counters.rx_reentrant += 1; + return 0; + } + + in_stack = true; + defer in_stack = false; + counters.rx_delivered += 1; + sta.onFrame(bytes[0..len]); + return 0; +} + +// ================================================================================ lifecycle + +pub const Error = error{ + /// `hosted_wifi_get_mac` failed, or answered with the all-zero MAC that means "no radio yet". + /// The usual cause is calling this before `hosted_wifi_sta_start`. + MacUnavailable, + /// `transport_drv_add_channel` refused, or accepted without filling in the transmit function. + ChannelRegisterFailed, + AlreadyOpen, +}; + +/// Register the station channel and bring the IP stack up behind it. +/// +/// Call after `net.init` and after `hosted_wifi_sta_start`; association may follow or may already +/// have happened, it makes no difference to this. Registering *before* associating is the tidier +/// order, because `chan_arr[ESP_STA_IF]` becoming non-null is the moment sdio_drv.c stops +/// discarding station frames, and until then a live association fills ESP-Hosted's receive queue +/// and logs "task still writing Rx data to queue!". +/// +/// The order inside matters: the stack is constructed *before* the channel is registered. The +/// instant `transport_drv_add_channel` returns, `sdio_process_rx_task` may call `onRxFrame`, and +/// that must not find `sta` uninitialised. +pub fn open() Error!void { + if (opened) return error.AlreadyOpen; + + var mac_bytes: [6]u8 = @splat(0); + if (hosted_wifi_get_mac(&mac_bytes) != 0) return error.MacUnavailable; + // An all-zero MAC is not a MAC. It is what the shim hands back if the coprocessor answered + // without having a station interface, and building an ARP cache on it would produce a stack + // that transmits frames no switch will ever route back. + if (std.mem.allEqual(u8, &mac_bytes, 0)) return error.MacUnavailable; + + sta = .init(mac_bytes, &sendFrame); + + var tx: ?TxFn = null; + const channel = transport_drv_add_channel( + @ptrCast(&channel_cookie), + esp_sta_if, + 0, // secure=0: plain text, as ESP-Hosted itself uses for the two Wi-Fi interfaces + // (esp_hosted_api.c:105-107). The secure path is the RPC channel's, and RPC has + // its own already. + &tx, + &onRxFrame, + ); + if (channel == null) return error.ChannelRegisterFailed; + // Belt and braces: the switch at transport_drv.c:467-485 is the only writer of `*tx`, and the + // one branch that leaves it untouched also returns NULL. Checking both means a future + // ESP-Hosted that separates those cannot leave us with a live channel and no way to transmit. + tx_fn = tx orelse return error.ChannelRegisterFailed; + + opened = true; +} + +/// True once `open` has succeeded. +pub fn isOpen() bool { + return opened; +} + +// ============================================================ the guarded entry points +// +// Every function that can mutate the stack goes through `in_stack`. Every function that only reads +// it does not, because a read cannot suspend and the worst it can observe is a value one frame out +// of date. + +/// Advance the stack's clock. Returns false if the stack was busy and the tick was skipped, which +/// is harmless - see note 5 - but worth being able to see. +pub fn tick(now_ms: u64) bool { + if (in_stack) { + counters.tick_skipped += 1; + return false; + } + in_stack = true; + sta.tick(now_ms); + in_stack = false; + // Outside the guard, and last: draining may block, and `in_stack` must not be held across a + // suspension or the receive path would drop every frame that arrived while we waited. + flushTx(); + return true; +} + +/// Begin DHCP. Call `tick` at least once first: `dhcpStart` stamps the acquisition's start time +/// from the stack's idea of now, which only `tick` sets. Returns false if the stack was busy. +pub fn dhcpStart() bool { + if (in_stack) return false; + in_stack = true; + defer in_stack = false; + sta.dhcpStart(); + return true; +} + +/// Configure statically instead of asking a server. +pub fn setStatic(addr: [4]u8, mask: [4]u8, gw: [4]u8) bool { + if (in_stack) return false; + in_stack = true; + defer in_stack = false; + sta.setStatic(addr, mask, gw); + return true; +} + +/// Override the resolver `resolve` asks. Not needed on a network whose DHCP server offers one - +/// `dhcpBind` stores option 6 and `resolve` uses it with no configuration at all. Returns false if +/// the stack was busy. +pub fn setDnsServer(addr: [4]u8) bool { + if (in_stack) return false; + in_stack = true; + defer in_stack = false; + sta.setDnsServer(addr); + return true; +} + +/// One HTTP GET, with the address literal as the `Host:` header. `ip.Stack.httpGet`'s protocol, +/// unchanged: this returns `error.WouldBlock` until the body is complete, and the caller must keep +/// calling with *identical* arguments while driving `tick`. `out` is borrowed until a length comes +/// back. +pub fn httpGet(host: [4]u8, remote_port: u16, path: []const u8, out: []u8) ip.HttpError!usize { + return httpGetHost(host, null, remote_port, path, out); +} + +/// The same, with an explicit `Host:` name for a name-based virtual host. See +/// `ip.Stack.httpGetHost`; `name` is part of the request's identity, so it must not change between +/// calls any more than `path` may. +pub fn httpGetHost( + host: [4]u8, + name: ?[]const u8, + remote_port: u16, + path: []const u8, + out: []u8, +) ip.HttpError!usize { + if (in_stack) { + // Answering the caller's own protocol back at it. The alternative - waiting - would be a + // second place in this file that can block, and the guard exists to have exactly none. + counters.http_deferred += 1; + return error.WouldBlock; + } + in_stack = true; + defer in_stack = false; + return sta.httpGetHost(host, name, remote_port, path, out); +} + +/// Resolve a name to an address. `ip.Stack.resolve`'s protocol, which is `httpGet`'s: this returns +/// `error.WouldBlock` until an address or a real error comes back, and the caller keeps calling +/// with the same name while driving `tick`. +/// +/// The guard's answer is the same `error.WouldBlock`, for the same reason it is in `httpGetHost`: +/// the query's own retransmissions run in `sta.tick`, so a deferred poll costs nothing and the 7 s +/// bound still holds. Frames the query sends go through `sendFrame` into the deferred ring like +/// every other frame here - nothing on this path touches the transport's tx function directly. +pub fn resolve(name: []const u8) ip.DnsError!ip.Ip4 { + if (in_stack) { + counters.dns_deferred += 1; + return error.WouldBlock; + } + in_stack = true; + defer in_stack = false; + return sta.resolve(name); +} + +// ==================================================================== read-only accessors + +/// The station MAC the stack was built on. +pub fn mac() [6]u8 { + return sta.mac; +} + +/// The configured address, or null if there is none yet. +pub fn address() ?[4]u8 { + return sta.addr; +} + +pub fn netmask() [4]u8 { + return sta.mask; +} + +pub fn gateway() [4]u8 { + return sta.gw; +} + +pub fn dnsServer() ?[4]u8 { + return sta.dns; +} + +pub fn dhcpState() ip.DhcpState { + return sta.dhcp.state; +} + +pub fn tcpState() ip.TcpState { + return sta.tcp.state; +} + +pub fn httpStatus() u16 { + return sta.http.status; +} + +/// The IP stack's own counters: frames in, frames dropped, echoes answered, checksums rejected. +pub fn ipCounters() ip.Counters { + return sta.counters; +} + +/// This file's counters: the transport boundary, and every way the guard fired. +pub fn stats() Stats { + return counters; +} + +// There are no tests here, and that is an answer rather than an omission. Two of the three things +// this file does are calls into ESP-Hosted's C - `transport_drv_add_channel` and the transmit +// function it hands back - and the third is a callback that C invokes. A host test could only +// exercise it against a mock of the very code whose conventions are the thing in doubt, and it +// would pass just as happily against a mock that put the frame one header too late. The evidence +// that matters is the citations in this file's header and a board that answers a ping. |
