| <!-- Copyright 2026 The Fuchsia Authors. All rights reserved. | |
| Use of this source code is governed by a BSD-style license that can be | |
| found in the LICENSE file. --> | |
| # UART Framing Protocol Library (`uart_fpl`) API Reference | |
| The `uart_fpl` crate (`src/developer/lib/uart_fpl`) provides binary packet framing, streaming parsing, dynamic protocol negotiation, and sliding-window flow control primitives for transporting multiplexed packet streams over raw serial (UART) links. | |
| This document serves as the high-level API reference for developers integrating with or maintaining `uart_fpl`. It focuses on library types, state machines, method contracts, lifecycle management, and architectural justifications (such as end-to-end backpressure), deliberately omitting low-level wire formats and bit-level representations. | |
| --- | |
| ## 1. Architectural Overview & Component Topology | |
| `uart_fpl` is structured into five modular layers: | |
| ``` | |
| +-------------------------------------------------------------------------+ | |
| | Client Layer | | |
| | (Host CLI/Daemon: ffx-uart-driver, Target Runner: fdomain) | | |
| +------------------------------------+------------------------------------+ | |
| | | |
| +---------------------+--------------------+ | |
| | | | |
| v v | |
| +-----------------------------+ +-----------------------------+ | |
| | Dynamic Negotiation | | Reliable Flow Control | | |
| | (handshake::HostHandshake,| | (resend_sp::ResendSender, | | |
| | handshake::TargetHandshake) | resend_sp::ResendReceiver)| | |
| +--------------+--------------+ +--------------+--------------+ | |
| | | | |
| +---------------------+--------------------+ | |
| | | |
| v | |
| +------------------------------------------+ | |
| | Framing & Checksum Engine | | |
| | (frame::Frame, encode_frame, | | |
| | parser::FrameParser) | | |
| +---------------------+--------------------+ | |
| | | |
| v | |
| +-------------------------------------------------------------------------+ | |
| | Physical / Virtual | | |
| | UART Byte Stream | | |
| +-------------------------------------------------------------------------+ | |
| ``` | |
| ### Module Breakdown | |
| | Module | Primary Public Types | Responsibility | | |
| | :--- | :--- | :--- | | |
| | `frame` | `Frame`, `FrameType`, `ProtocolId`, `encode_frame` | In-memory frame representations, type enums, and zero-allocation frame serialization. | | |
| | `parser` | `FrameParser` | Streaming parser with sync preamble recovery, two-tier header verification, and bounded memory buffers. | | |
| | `handshake` | `HostHandshake`, `TargetHandshake`, `HandshakeResponse`, `HandshakeStatus` | Channel 0 dynamic protocol negotiation state machines and idempotent retransmission handling. | | |
| | `resend_sp` | `ResendSender`, `ResendReceiver`, `FrameStatus`, `AckOutcome` | Sliding-window Go-Back-N transmission, sequence wrapping math, Karn's RTT tracking, and cumulative ACK validation. | | |
| | `window` | `AckTracker` | Thread-safe, lock-free cumulative ACK coalescing and async task notification. | | |
| | `error` | `FrameError`, `HandshakeError`, `RetransmissionLimitExceeded`, `UnexpectedSeqError` | Strongly typed errors across framing, negotiation, and transport. | | |
| --- | |
| ## 2. Framing & Serialization API (`frame.rs`) | |
| ### Constants | |
| - **`CONTROL_CHANNEL_ID: u16 = 0`**: Reserved logical channel identifier used exclusively for link negotiation, session management, and link-level control signaling. | |
| - **`MAX_PAYLOAD_SIZE: usize = 1024`**: Maximum permissible payload size in bytes accepted by encoders and parsers for a single frame. | |
| - **`MAX_FRAME_SIZE: usize = 1041`**: Total maximum wire length of an encoded frame (13-byte header + 1024-byte payload + 4-byte CRC-32). | |
| - **`DEFAULT_WINDOW_SIZE: u8 = 64`**: Default Go-Back-N sliding-window size for `ResendSender`. | |
| - **`DEFAULT_MAX_RETRANSMISSION_ATTEMPTS: usize = 60`**: Default consecutive timeout retransmission limit before declaring a link failure. | |
| - **`DEFAULT_RETRANSMISSION_TIMEOUT: Duration = Duration::from_millis(1000)`**: Default base timeout interval for retransmissions. | |
| ### Types & Enums | |
| #### `FrameType` | |
| Identifies the semantics of a frame: | |
| ```rust | |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | |
| pub enum FrameType { | |
| Data, // Unreliable or sequenced payload data | |
| Ack, // Cumulative sequence acknowledgment | |
| Close, // Logical channel teardown | |
| Reset, // Transport link reset | |
| NegotiateReq, // Protocol negotiation request (Channel 0 only) | |
| NegotiateResp, // Protocol negotiation response (Channel 0 only) | |
| Unknown(u8), // Forward-compatible unmapped frame type | |
| } | |
| ``` | |
| Conversions: Implements `From<u8>`, `Into<u8>`, and `Display`. | |
| #### `ProtocolId` | |
| Identifies the framing and transport protocol negotiated across Channel 0: | |
| ```rust | |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | |
| pub enum ProtocolId { | |
| ResendSP, // Sliding-window Go-Back-N with CRC-32 (wire ID 1) | |
| Unknown(u32), // Forward-compatible representation for future protocol proposals | |
| } | |
| ``` | |
| Methods: | |
| - `wire_id(self) -> u32`: Returns the 32-bit wire integer representation. | |
| #### `Frame` | |
| In-memory representation of a validated, parsed protocol frame: | |
| ```rust | |
| #[derive(Debug, Clone, PartialEq, Eq)] | |
| pub struct Frame { | |
| pub session_id: u32, | |
| pub channel_id: u16, | |
| pub seq: u8, | |
| pub frame_type: FrameType, | |
| pub payload: Vec<u8>, | |
| } | |
| ``` | |
| Constructors & Methods: | |
| - `Frame::new(session_id: u32, channel_id: u16, seq: u8, frame_type: FrameType, payload: Vec<u8>) -> Result<Self, FrameError>`: Constructs a frame, returning `FrameError::PayloadTooLarge` if `payload.len() > MAX_PAYLOAD_SIZE`. | |
| - `encode(&self) -> Result<Vec<u8>, FrameError>`: Serializes the frame into wire bytes. | |
| - `wire_len(&self) -> usize`: Returns the total wire length in bytes (header + payload + checksums). | |
| ### Serialization Function | |
| #### `encode_frame` | |
| ```rust | |
| pub fn encode_frame( | |
| session_id: u32, | |
| channel_id: u16, | |
| seq: u8, | |
| frame_type: FrameType, | |
| payload: &[u8], | |
| ) -> Result<Vec<u8>, FrameError> | |
| ``` | |
| Zero-allocation wire serialization helper for transmitting byte slices directly without allocating an intermediate `Frame` struct. Encodes the binary header, header checksum (`HdrChk`), and trailing CRC-32. Returns `FrameError::PayloadTooLarge` if payload exceeds 1,024 bytes. | |
| --- | |
| ## 3. Streaming Frame Parser API (`parser.rs`) | |
| ### `FrameParser` | |
| A streaming push-parser that accepts arbitrary byte slices from a serial or socket reader and emits validated frames. | |
| ```rust | |
| pub struct FrameParser { /* private fields */ } | |
| ``` | |
| #### Lifecycle & Operations | |
| - **`FrameParser::new() -> Self`**: Creates a new parser with an empty buffer and zeroed metrics. (Also implements `Default`). | |
| - **`feed(&mut self, data: &[u8])`**: Ingests an incoming slice of bytes into the internal streaming buffer. | |
| - *Memory Guarantee*: Memory consumption is strictly bounded to 64 KiB (`MAX_BUFFER_CAPACITY`). If continuous incoming noise or garbage causes unconsumed bytes to exceed 64 KiB, the oldest unparsed bytes are drained automatically to prevent memory exhaustion. | |
| - *Compaction*: Consumed bytes are automatically reclaimed when the internal cursor passes a compaction threshold (4 KiB or 50% buffer capacity). | |
| - **`next_frame(&mut self) -> Option<Frame>`**: Extracts the next complete, validated frame. | |
| - Returns `None` if more bytes are needed to complete a frame. | |
| - Automatically handles preamble resynchronization: slides past framing noise to align on valid sync preambles. | |
| - Enforces two-tier validation: verifies the header checksum before evaluating payload length, preventing corrupted lengths from causing buffer stalls. | |
| - **`take_unconsumed(&mut self) -> Vec<u8>`**: Drains and returns all unparsed bytes currently residing in the buffer, resetting parser cursor to zero. | |
| - *Crucial Transition Contract*: When transitioning an active link between phases (e.g. from the Channel 0 handshake phase to the continuous data bridging loop), callers **must** call `take_unconsumed()` to extract any ingress data bytes received in the same read buffer as the handshake response, feeding them into the subsequent reader. | |
| - **`checksum_errors(&self) -> u64`**: Returns the cumulative count of corrupted frames (header checksum or CRC-32 mismatches) encountered, useful for link-quality telemetry (`ffx uart status`). | |
| - **`reset(&mut self)`**: Clears internal buffers, cursors, and error statistics. | |
| - **`is_empty(&self) -> bool`**, **`unconsumed(&self) -> &[u8]`**, **`unconsumed_len(&self) -> usize`**: Inspection methods for buffer status and unparsed byte slices. | |
| --- | |
| ## 4. Channel 0 Dynamic Protocol Negotiation API (`handshake.rs`) | |
| `uart_fpl` coordinates transport negotiation over Channel 0 before general data streams begin. | |
| ### `HostHandshake` | |
| Coordinates protocol negotiation from the host side: | |
| ```rust | |
| pub struct HostHandshake { /* private fields */ } | |
| ``` | |
| - **`new(proposed: Vec<ProtocolId>) -> Self`**: Initializes the host handshake with an ordered preference list of protocols (e.g., `vec![ProtocolId::ResendSP]`). | |
| - **`start(&self) -> Result<(FrameType, Vec<u8>), HandshakeError>`**: Generates the initial negotiation request: `(FrameType::NegotiateReq, wire_payload)`. The caller encodes this payload into a frame on `CONTROL_CHANNEL_ID` and transmits it. | |
| - **`handle_response(&self, payload: &[u8]) -> Result<ProtocolId, HandshakeError>`**: Parses and validates the target's `NegotiateResp` payload. Confirms the target selected a mutually agreeable protocol from the host's proposed list and returns the agreed `ProtocolId`. | |
| ### `TargetHandshake` | |
| Coordinates protocol negotiation from the target side: | |
| ```rust | |
| pub struct TargetHandshake { /* private fields */ } | |
| ``` | |
| - **`new(supported: Vec<ProtocolId>) -> Self`**: Initializes the target handler with the set of protocols supported by the target. | |
| - **`handle_request(&self, payload: &[u8]) -> Result<(Option<ProtocolId>, Vec<u8>), HandshakeError>`**: Parses an incoming `NegotiateReq` payload, finds the highest-preference protocol supported by both sides, and serializes the `NegotiateResp` payload. | |
| - Returns `(Some(selected_protocol), response_payload)` on success. | |
| - Returns `(None, response_payload)` with `HandshakeStatus::NoCommonProtocol` if no common protocol exists. | |
| #### Idempotent Handshake Retransmission Contract | |
| Over noisy or lossy serial lines, the target's `NegotiateResp` may be dropped in transit. When this happens, the host's retry loop will retransmit the original `NegotiateReq` with the identical `session_id`. | |
| **Contract**: The target implementation (e.g., in `fdomain-uart-driver`) **must** treat repeated `NegotiateReq` frames containing the active `session_id` as idempotent retransmissions: it must re-emit the cached `NegotiateResp` frame rather than rejecting the packet as out-of-order or triggering a transport reset. If a `NegotiateReq` arrives with a *new* session ID, it indicates a host restart, requiring a clean state reset. | |
| --- | |
| ## 5. Sliding-Window Flow Control API (`resend_sp.rs`) | |
| The `resend_sp` module provides the Go-Back-N sliding-window state machines for reliable, sequenced packet transport. | |
| ### `ResendSender` | |
| Maintains the transmission sliding window, sequence allocation, in-flight buffering, Karn's RTT measurement, and timeout retransmissions. | |
| ```rust | |
| pub struct ResendSender { /* private fields */ } | |
| ``` | |
| - **`new(window_size: u8, max_retransmission_attempts: usize) -> Self`**: Creates a sender. Panics if `window_size == 0 || window_size > 128` or `max_retransmission_attempts == 0`. Default is window size 64, 60 maximum attempts. | |
| - **`can_send(&self) -> bool`**: Returns `true` if `in_flight() < window_size`. | |
| - **`in_flight(&self) -> u8`**: Returns the number of frames currently outstanding without acknowledgment (`next_seq - base`). | |
| - **`is_idle(&self) -> bool`**: Returns `true` when all transmitted frames have been acknowledged (`base == next_seq`). | |
| - **`enqueue_frame(&mut self, session_id: u32, channel_id: u16, frame_type: FrameType, payload: &[u8]) -> Result<(u8, Vec<u8>), FrameError>`**: | |
| - If `can_send()` is `false`, returns `Err(FrameError::WindowFull { .. })`. | |
| - Otherwise, assigns `next_seq`, serializes the frame, buffers it in an internal slot with an `Instant::now()` timestamp, increments `next_seq` (wrapping modulo 256), and returns `Ok((assigned_seq, wire_bytes))`. | |
| - **`handle_ack(&mut self, ack_seq: u8) -> AckOutcome`**: | |
| - Evaluates an incoming cumulative ACK against the active window `[base, next_seq)`. | |
| - If valid: frees all buffer slots from `base` through `ack_seq`, advances `base = ack_seq + 1`, resets consecutive retransmission attempts to 0, and returns `AckOutcome::Advanced`. | |
| - If out-of-window (e.g., stale or duplicate ACK): returns `AckOutcome::OutOfWindow`. | |
| - **`handle_timeout(&mut self) -> Result<Vec<Vec<u8>>, RetransmissionLimitExceeded>`**: | |
| - Invoked when the retransmission timer fires (default 1,000 ms). | |
| - If idle, returns an empty vector. | |
| - Increments consecutive retransmission attempts. If attempts reach `max_retransmission_attempts`, returns `Err(RetransmissionLimitExceeded)`. | |
| - Otherwise, returns clones of all unacknowledged frames `[base, next_seq)` to be retransmitted over the wire, updates their timestamps, and marks them as retransmitted. | |
| - **`reset(&mut self)`**: Resets `base`, `next_seq`, and clears all buffered slots. | |
| #### `AckOutcome` | |
| ```rust | |
| #[derive(Debug, Clone, PartialEq, Eq)] | |
| pub enum AckOutcome { | |
| Advanced { | |
| new_base: u8, | |
| rtt: Option<Duration>, // None if acknowledged frame was retransmitted (Karn's algorithm) | |
| all_acked: bool, | |
| }, | |
| OutOfWindow { | |
| base: u8, | |
| next_seq: u8, | |
| }, | |
| } | |
| ``` | |
| ### `ResendReceiver` | |
| Maintains the receiving sequence state machine and cumulative ACK progression. | |
| ```rust | |
| pub struct ResendReceiver { /* private fields */ } | |
| ``` | |
| - **`new() -> Self`**: Creates a receiver expecting sequence 0. | |
| - **`expected_seq(&self) -> u8`**: Returns the sequence number currently expected. | |
| - **`inspect(&self, seq: u8) -> FrameStatus`**: | |
| - Evaluates an incoming frame sequence *without mutating receiver state*. | |
| - Returns `FrameStatus::InOrder { seq }` if `seq == expected_seq`. | |
| - Returns `FrameStatus::OutOfOrder { seq, expected_seq, ack_seq }` if out-of-order or duplicate. | |
| - **`advance_in_order(&mut self, seq: u8) -> Result<u8, UnexpectedSeqError>`**: | |
| - Verifies that `seq == expected_seq`. If mismatched, returns `Err(UnexpectedSeqError)`. | |
| - Advances `expected_seq` by 1 (modulo 256), records `seq` as the latest acknowledged frame, and returns `Ok(seq)` (the cumulative ACK sequence to be transmitted back to the sender). | |
| - **`accept_seq(&mut self, seq: u8) -> FrameStatus`**: | |
| - Convenience single-step handler: inspects `seq`, and if in-order, automatically advances sequence progression via `advance_in_order(seq)` and returns `FrameStatus::InOrder`. If out-of-order, returns `FrameStatus::OutOfOrder` without mutating state. | |
| - Callers implementing two-phase flow control (e.g. attempting non-blocking writes to downstream sinks before committing sequence advancement) should use `inspect()` followed by `advance_in_order()` instead. | |
| - **`current_ack_seq(&self) -> Option<u8>`**: | |
| - Returns the latest cumulative ACK sequence. | |
| - Returns `None` if sequence 0 has not yet been received. This prevents spurious emission of `ACK 255` on startup packet loss. | |
| - **`reset(&mut self)`**: Resets `expected_seq` to 0 and clears acknowledgment history. | |
| #### `FrameStatus` | |
| ```rust | |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| pub enum FrameStatus { | |
| InOrder { seq: u8 }, | |
| OutOfOrder { | |
| seq: u8, | |
| expected_seq: u8, | |
| ack_seq: Option<u8>, | |
| }, | |
| } | |
| ``` | |
| --- | |
| ## 6. Asynchronous Cumulative ACK Tracking API (`window.rs`) | |
| ### `AckTracker` | |
| `AckTracker` provides thread-safe, lock-free cumulative ACK coordination between asynchronous receiver and writer tasks. | |
| ```rust | |
| #[derive(Debug, Clone, Default)] | |
| pub struct AckTracker { /* private Arc-wrapped state */ } | |
| ``` | |
| In Go-Back-N, acknowledgments are cumulative: acknowledging sequence $N$ implicitly acknowledges all sequences prior to $N$. When a receiver processes multiple packets in rapid succession, queueing individual ACK frames creates reverse-path traffic storms. `AckTracker` coalesces these updates into a single atomic word. | |
| - **`set_ack(&self, session_id: u32, seq: u8)`**: Records the latest cumulative ACK and wakes any task waiting on `wait_ack()`. Both `session_id` and `seq` are updated atomically in a single `AtomicU64` to prevent torn reads. | |
| - **`take_ack(&self) -> Option<(u32, u8)>`**: Non-blocking extraction of the pending cumulative ACK. Clears the pending flag and returns `Some((session_id, seq))` if an ACK was pending, or `None` if already consumed. | |
| - **`wait_ack(&self) -> (u32, u8)`**: Async future that suspends until a new cumulative ACK is recorded via `set_ack()`, returning the latest `(session_id, seq)`. | |
| - **`register_waker(&self, cx: &mut Context<'_>)`**: Registers a task waker for manual polling contexts. | |
| --- | |
| ## 7. Error Handling API (`error.rs`) | |
| All errors implement `std::error::Error`, `thiserror::Error`, and are `Clone`, `Copy`, `PartialEq`, `Eq`. | |
| ### `FrameError` | |
| Errors encountered during frame construction, serialization, or window queueing: | |
| - **`PayloadTooLarge(usize)`**: Payload length exceeds `MAX_PAYLOAD_SIZE` (1,024 bytes). | |
| - **`WindowFull { window_size: u8, in_flight: u8 }`**: Transmission queue cannot accept another frame because the sliding window is full. | |
| ### `HandshakeError` | |
| Errors encountered during Channel 0 protocol negotiation: | |
| - **`MalformedPayload`**: Byte payload was truncated or contained invalid counts/fields. | |
| - **`ProtocolNotProposed`**: Target selected a protocol that the host did not propose. | |
| - **`NoCommonProtocol`**: Target and host do not share any supported protocol. | |
| - **`UnsupportedStatus(u8)`**: Peer responded with an unrecognized status code. | |
| ### `RetransmissionLimitExceeded` | |
| Fatal error returned by `ResendSender::handle_timeout` when consecutive retransmission attempts reach `max_retransmission_attempts`: | |
| ```rust | |
| #[derive(Debug, Error, PartialEq, Eq, Clone, Copy)] | |
| #[error("Too many retransmission failures, base={base}, next_seq={next_seq}, attempts={attempts}")] | |
| pub struct RetransmissionLimitExceeded { | |
| pub base: u8, | |
| pub next_seq: u8, | |
| pub attempts: usize, | |
| } | |
| ``` | |
| ### `UnexpectedSeqError` | |
| Error returned by `ResendReceiver::advance_in_order` when the sequence number being committed does not match `expected_seq`: | |
| ```rust | |
| #[derive(Debug, Error, PartialEq, Eq, Clone, Copy)] | |
| #[error("Unexpected sequence number: expected {expected}, received {received}")] | |
| pub struct UnexpectedSeqError { | |
| pub expected: u8, | |
| pub received: u8, | |
| } | |
| ``` | |
| --- | |
| ## 8. High-Level Architectural Justifications | |
| This section documents the architectural choices and design trade-offs embodied in the `uart_fpl` API. | |
| ### 8.1 End-to-End Backpressure Without Local Receiver Queueing | |
| A primary architectural decision in `uart_fpl` is that **`ResendReceiver` does not buffer, reorder, or queue out-of-order frames**. | |
| #### The Problem | |
| In standard networking stacks (like TCP), receivers maintain out-of-order reassembly queues to absorb misordered packets. However, serial connections (physical UARTs or USB-to-serial bridges) operate under severe memory and CPU constraints: | |
| 1. The target runner (`fdomain-uart-driver`) runs on minimal embedded targets with tight heap limits. | |
| 2. If downstream consumers (such as FIDL channels or local sockets) become backpressured or blocked on I/O, a buffering receiver would accumulate frames in memory, risking OOM crashes. | |
| #### The Solution: Two-Phase Check-and-Commit | |
| `uart_fpl` enforces backpressure at the link level by decoupling sequence inspection from sequence advancement: | |
| ``` | |
| Incoming Frame (seq) | |
| │ | |
| ▼ | |
| ResendReceiver::inspect(seq) | |
| │ | |
| [In-Order?] ─── No ───► Drop frame, emit current cumulative ACK | |
| │ | |
| Yes | |
| │ | |
| ▼ | |
| Downstream Sink::try_send(payload) | |
| │ | |
| [Capacity?] ─── No (Full) ──► Drop frame! DO NOT advance receiver! | |
| │ | |
| Yes (Accepted) | |
| │ | |
| ▼ | |
| ResendReceiver::advance_in_order(seq) | |
| │ | |
| ▼ | |
| AckTracker::set_ack(seq) ──► Emit Cumulative ACK to Sender | |
| ``` | |
| 1. **Step 1 (Inspect)**: The receiver checks `inspect(seq)`. If the frame is out of order, it is dropped immediately and the current cumulative ACK is returned. | |
| 2. **Step 2 (Non-Blocking Dispatch)**: If the frame is in-order, the caller does **not** advance the receiver immediately. Instead, it attempts to dispatch the payload downstream via a non-blocking operation (e.g. `mpsc::Sender::try_send` or `Sink::poll_ready`). | |
| 3. **Step 3 (Conditional Commit)**: | |
| - **If downstream accepts the data**: The caller commits by calling `receiver.advance_in_order(seq)` (which verifies `seq == expected_seq`, advances `expected_seq`, and updates `AckTracker`). | |
| - **If downstream is full / saturated**: The caller **drops the frame and does NOT call `advance_in_order(seq)`**. | |
| 4. **Natural Sender Throttling**: Because `advance_in_order()` was not called, the cumulative ACK does not advance. The remote sender's sliding window fills up until `can_send()` returns `false`, stalling further transmissions at the source. | |
| 5. **Recovery**: Once the downstream bottleneck drains, the remote sender's retransmission timer expires, re-sending the unacknowledged frames into now-available receiver buffers. | |
| This guarantees true end-to-end backpressure without a single byte of receiver-side reassembly buffering. | |
| --- | |
| ### 8.2 Logical Channel Multiplexing (`channel_id`) | |
| A physical serial port is inherently a single byte stream. Without packet-level channel multiplexing, access to the serial link is an exclusive, single-client bottleneck: | |
| - If a developer runs a continuous log streaming session (`ffx log`) or serves package files over UART, the link is completely monopolized. | |
| - Concurrent control queries (`ffx target echo`, `ffx component list`) would be blocked until the stream terminates. | |
| By embedding a 16-bit `channel_id` into the framing API: | |
| - Multiple concurrent host client connections are multiplexed over a single serial daemon (`ffx-uart-driver`). | |
| - Target-side demux maps each `channel_id` to an independent FIDL channel. | |
| - Channels are torn down cleanly with `FrameType::Close` without disrupting concurrent connections. | |
| --- | |
| ### 8.3 Dedicated Control Plane (`CONTROL_CHANNEL_ID = 0`) | |
| `CONTROL_CHANNEL_ID: u16 = 0` is permanently reserved for transport signaling and handshake negotiation: | |
| 1. **Separation of Concerns**: User channels (`1..=65535`) handle client application data. Channel 0 manages transport lifecycle. | |
| 2. **In-Band Reboot & Session Reset**: Serial links lack out-of-band hardware disconnect events. When an endpoint reboots, it emits a `NegotiateReq` (carrying a fresh `session_id`) directly into Channel 0. The parser processes this reset cleanly without desynchronizing data channels. | |
| 3. **Queue Independence**: Control frames on Channel 0 operate outside client Go-Back-N data window queues, preventing handshakes from being blocked behind backpressured data streams. | |
| --- | |
| ### 8.4 32-Bit Session IDs | |
| Hardware UART FIFOs, USB bridge chips (e.g. FTDI FT4232H), and kernel TTY buffers frequently retain unread bytes across host restarts or target reboots: | |
| - In a sliding-window protocol where sequence numbers always start at 0, stale in-flight packets or delayed ACKs from a previous session could be accepted as valid packets in a new session. | |
| - Tagging every frame with a random 32-bit `session_id` allows immediate rejection of stale frames and instantaneous detection of peer reboots. | |
| --- | |
| ### 8.5 Cumulative ACK Coalescing (`AckTracker`) | |
| In asymmetric or half-duplex links (and even full-duplex UARTs), emitting an individual ACK packet for every incoming data packet consumes substantial reverse bandwidth: | |
| - Under rapid bursts (e.g. 64 KB FIDL transfers split into seventy 1 KB frames), generating 70 individual ACK packets causes reverse FIFO contention. | |
| - `AckTracker` coalesces incoming acknowledgments in memory: the writer task only emits the latest cumulative ACK sequence. If packets 0 through 15 arrive in a single burst, only a single cumulative ACK for packet 15 needs to cross the wire. | |
| --- | |
| ### 8.6 Karn's Algorithm for Accurate RTT Measurement | |
| Estimating round-trip time (RTT) is necessary to dynamically calibrate retransmission timeouts. However, naive RTT estimation suffers from ambiguity when packets are retransmitted: when an ACK arrives for a retransmitted packet, it is impossible to determine whether the ACK corresponds to the original transmission or the retransmission. | |
| `ResendSender` implements **Karn's Algorithm**: | |
| - Frames that have undergone retransmission have `retransmitted = true`. | |
| - When an ACK arrives for a retransmitted frame, `AckOutcome::Advanced` returns `rtt: None`. | |
| - RTT samples are recorded *exclusively* for frames acknowledged on their first transmission, preventing retransmission bursts from poisoning RTT estimates and inflating timeouts. | |
| --- | |
| ## 9. Usage Examples | |
| ### 9.1 Handshake Negotiation Flow | |
| ```rust | |
| use uart_fpl::{ | |
| FrameParser, FrameType, HostHandshake, ProtocolId, TargetHandshake, encode_frame, | |
| }; | |
| // 1. Host initiates negotiation proposing ResendSP | |
| let host = HostHandshake::new(vec![ProtocolId::ResendSP]); | |
| let (req_type, req_payload) = host.start().expect("Host handshake start"); | |
| let host_req_wire = encode_frame( | |
| /*session_id=*/ 0x12345678, | |
| /*channel_id=*/ 0, | |
| /*seq=*/ 0, | |
| req_type, | |
| &req_payload, | |
| ).expect("Encode negotiate req"); | |
| // 2. Target receives and evaluates request | |
| let target = TargetHandshake::new(vec![ProtocolId::ResendSP]); | |
| let mut target_parser = FrameParser::new(); | |
| target_parser.feed(&host_req_wire); | |
| let parsed_req = target_parser.next_frame().expect("Target parses req"); | |
| let (selected, resp_payload) = target | |
| .handle_request(&parsed_req.payload) | |
| .expect("Target handles req"); | |
| assert_eq!(selected, Some(ProtocolId::ResendSP)); | |
| let target_resp_wire = encode_frame( | |
| /*session_id=*/ 0x12345678, | |
| /*channel_id=*/ 0, | |
| /*seq=*/ 0, | |
| FrameType::NegotiateResp, | |
| &resp_payload, | |
| ).expect("Encode negotiate resp"); | |
| // 3. Host completes negotiation | |
| let mut host_parser = FrameParser::new(); | |
| host_parser.feed(&target_resp_wire); | |
| let parsed_resp = host_parser.next_frame().expect("Host parses resp"); | |
| let negotiated_proto = host | |
| .handle_response(&parsed_resp.payload) | |
| .expect("Host handles resp"); | |
| assert_eq!(negotiated_proto, ProtocolId::ResendSP); | |
| ``` | |
| ### 9.2 Reliable Sender Loop | |
| ```rust | |
| use uart_fpl::{AckOutcome, FrameType, ResendSender}; | |
| let mut sender = ResendSender::new(/*window_size=*/ 64, /*max_attempts=*/ 60); | |
| // Check window capacity before sending | |
| if sender.can_send() { | |
| let (seq, wire_bytes) = sender | |
| .enqueue_frame( | |
| /*session_id=*/ 0x12345678, | |
| /*channel_id=*/ 1, | |
| FrameType::Data, | |
| b"Hello Fuchsia UART", | |
| ) | |
| .expect("Enqueue frame"); | |
| // Transmit wire_bytes over serial port... | |
| } | |
| // When an incoming ACK is parsed: | |
| match sender.handle_ack(/*ack_seq=*/ 0) { | |
| AckOutcome::Advanced { new_base, rtt, all_acked } => { | |
| if let Some(measured_rtt) = rtt { | |
| // Update RTT estimator with measured sample | |
| } | |
| if all_acked { | |
| // All outstanding data has been acknowledged | |
| } | |
| } | |
| AckOutcome::OutOfWindow { base, next_seq } => { | |
| // Stale or duplicate ACK, safely ignore | |
| } | |
| } | |
| ``` | |
| ### 9.3 Reliable Receiver Loop with Two-Phase Backpressure | |
| ```rust | |
| use uart_fpl::{AckTracker, FrameStatus, FrameType, ResendReceiver}; | |
| let mut receiver = ResendReceiver::new(); | |
| let ack_tracker = AckTracker::new(); | |
| // In the packet reception handler: | |
| let frame_status = receiver.inspect(parsed_frame.seq); | |
| match frame_status { | |
| FrameStatus::InOrder { seq } => { | |
| // Step 1: Attempt non-blocking dispatch downstream | |
| match downstream_channel.try_send(parsed_frame.payload) { | |
| Ok(()) => { | |
| // Step 2: Downstream accepted payload; advance receiver | |
| let ack_seq = receiver | |
| .advance_in_order(seq) | |
| .expect("In-order sequence verified by inspect"); | |
| ack_tracker.set_ack(parsed_frame.session_id, ack_seq); | |
| } | |
| Err(_full_error) => { | |
| // Downstream is full! Drop packet and DO NOT advance receiver. | |
| // Sender will stall when window fills, naturally backpressuring. | |
| } | |
| } | |
| } | |
| FrameStatus::OutOfOrder { seq: _, expected_seq: _, ack_seq } => { | |
| // Duplicate or gap packet: drop payload, re-emit latest cumulative ACK | |
| if let Some(ack) = ack_seq { | |
| ack_tracker.set_ack(parsed_frame.session_id, ack); | |
| } | |
| } | |
| } | |
| ``` |