| <!-- 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); |
| } |
| } |
| } |
| ``` |