uart_fpl) API ReferenceThe 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.
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 | 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. |
frame.rs)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.FrameTypeIdentifies the semantics of a frame:
#[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.
ProtocolIdIdentifies the framing and transport protocol negotiated across Channel 0:
#[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.FrameIn-memory representation of a validated, parsed protocol frame:
#[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).encode_framepub 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.
parser.rs)FrameParserA streaming push-parser that accepts arbitrary byte slices from a serial or socket reader and emits validated frames.
pub struct FrameParser { /* private fields */ }
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.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.next_frame(&mut self) -> Option<Frame>: Extracts the next complete, validated frame.None if more bytes are needed to complete a frame.take_unconsumed(&mut self) -> Vec<u8>: Drains and returns all unparsed bytes currently residing in the buffer, resetting parser cursor to zero.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.handshake.rs)uart_fpl coordinates transport negotiation over Channel 0 before general data streams begin.
HostHandshakeCoordinates protocol negotiation from the host side:
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.TargetHandshakeCoordinates protocol negotiation from the target side:
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.(Some(selected_protocol), response_payload) on success.(None, response_payload) with HandshakeStatus::NoCommonProtocol if no common protocol exists.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.
resend_sp.rs)The resend_sp module provides the Go-Back-N sliding-window state machines for reliable, sequenced packet transport.
ResendSenderMaintains the transmission sliding window, sequence allocation, in-flight buffering, Karn's RTT measurement, and timeout retransmissions.
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>:can_send() is false, returns Err(FrameError::WindowFull { .. }).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:[base, next_seq).base through ack_seq, advances base = ack_seq + 1, resets consecutive retransmission attempts to 0, and returns AckOutcome::Advanced.AckOutcome::OutOfWindow.handle_timeout(&mut self) -> Result<Vec<Vec<u8>>, RetransmissionLimitExceeded>:max_retransmission_attempts, returns Err(RetransmissionLimitExceeded).[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#[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, }, }
ResendReceiverMaintains the receiving sequence state machine and cumulative ACK progression.
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:FrameStatus::InOrder { seq } if seq == expected_seq.FrameStatus::OutOfOrder { seq, expected_seq, ack_seq } if out-of-order or duplicate.advance_in_order(&mut self, seq: u8) -> Result<u8, UnexpectedSeqError>:seq == expected_seq. If mismatched, returns Err(UnexpectedSeqError).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: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.inspect() followed by advance_in_order() instead.current_ack_seq(&self) -> Option<u8>: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#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FrameStatus { InOrder { seq: u8 }, OutOfOrder { seq: u8, expected_seq: u8, ack_seq: Option<u8>, }, }
window.rs)AckTrackerAckTracker provides thread-safe, lock-free cumulative ACK coordination between asynchronous receiver and writer tasks.
#[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.error.rs)All errors implement std::error::Error, thiserror::Error, and are Clone, Copy, PartialEq, Eq.
FrameErrorErrors 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.HandshakeErrorErrors 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.RetransmissionLimitExceededFatal error returned by ResendSender::handle_timeout when consecutive retransmission attempts reach max_retransmission_attempts:
#[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, }
UnexpectedSeqErrorError returned by ResendReceiver::advance_in_order when the sequence number being committed does not match expected_seq:
#[derive(Debug, Error, PartialEq, Eq, Clone, Copy)] #[error("Unexpected sequence number: expected {expected}, received {received}")] pub struct UnexpectedSeqError { pub expected: u8, pub received: u8, }
This section documents the architectural choices and design trade-offs embodied in the uart_fpl API.
A primary architectural decision in uart_fpl is that ResendReceiver does not buffer, reorder, or queue out-of-order frames.
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:
fdomain-uart-driver) runs on minimal embedded targets with tight heap limits.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
inspect(seq). If the frame is out of order, it is dropped immediately and the current cumulative ACK is returned.mpsc::Sender::try_send or Sink::poll_ready).receiver.advance_in_order(seq) (which verifies seq == expected_seq, advances expected_seq, and updates AckTracker).advance_in_order(seq).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.This guarantees true end-to-end backpressure without a single byte of receiver-side reassembly buffering.
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:
ffx log) or serves package files over UART, the link is completely monopolized.ffx target echo, ffx component list) would be blocked until the stream terminates.By embedding a 16-bit channel_id into the framing API:
ffx-uart-driver).channel_id to an independent FIDL channel.FrameType::Close without disrupting concurrent connections.CONTROL_CHANNEL_ID = 0)CONTROL_CHANNEL_ID: u16 = 0 is permanently reserved for transport signaling and handshake negotiation:
1..=65535) handle client application data. Channel 0 manages transport lifecycle.NegotiateReq (carrying a fresh session_id) directly into Channel 0. The parser processes this reset cleanly without desynchronizing data channels.Hardware UART FIFOs, USB bridge chips (e.g. FTDI FT4232H), and kernel TTY buffers frequently retain unread bytes across host restarts or target reboots:
session_id allows immediate rejection of stale frames and instantaneous detection of peer reboots.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:
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.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:
retransmitted = true.AckOutcome::Advanced returns rtt: None.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);
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 } }
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); } } }