Skip to main content

uart_fpl/
frame.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Binary frame definitions, wire constants, serialization, and checksums.
6
7use zerocopy::byteorder::big_endian::{U16 as BeU16, U32 as BeU32};
8use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
9
10use crate::error::FrameError;
11
12/// Frame type indicator for unsequenced or sequenced data payloads.
13const TYPE_DATA: u8 = 0x01;
14/// Frame type indicator for cumulative acknowledgments.
15const TYPE_ACK: u8 = 0x02;
16/// Frame type indicator for channel termination.
17const TYPE_CLOSE: u8 = 0x03;
18/// Frame type indicator for transport link reset.
19const TYPE_RESET: u8 = 0x04;
20/// Frame type indicator for Channel 0 handshake negotiation request.
21const TYPE_NEGOTIATE_REQ: u8 = 0x10;
22/// Frame type indicator for Channel 0 handshake negotiation response.
23const TYPE_NEGOTIATE_RESP: u8 = 0x11;
24
25/// Reserved logical channel identifier for handshake negotiation and link control.
26pub const CONTROL_CHANNEL_ID: u16 = 0;
27
28/// Length of the framing preamble sync word in bytes.
29pub(crate) const SYNC_WORD_LEN: usize = 2;
30/// Length of the IEEE CRC-32 checksum field in bytes.
31pub(crate) const CHECKSUM_LEN: usize = 4;
32
33/// Canonical sync preamble bytes for UART protocol frames.
34pub(crate) const SYNC_WORD: [u8; SYNC_WORD_LEN] = [0xAA, 0x55];
35
36/// Packed wire format of the 13-byte UART protocol frame header.
37#[derive(FromBytes, IntoBytes, KnownLayout, Immutable, Debug, Clone, Copy, PartialEq, Eq)]
38#[repr(C, packed)]
39pub(crate) struct WireHeader {
40    /// Preamble synchronization bytes (`[0xAA, 0x55]`).
41    pub sync: [u8; SYNC_WORD_LEN],
42    /// 32-bit session identifier negotiated during Channel 0 handshake.
43    pub session_id: BeU32,
44    /// 16-bit logical channel identifier (0 = control).
45    pub channel_id: BeU16,
46    /// 8-bit Go-Back-N frame sequence number.
47    pub seq: u8,
48    /// Protocol frame type indicator.
49    pub frame_type: u8,
50    /// 16-bit payload length in bytes.
51    pub payload_len: BeU16,
52    /// 8-bit CRC header checksum over preceding header fields.
53    pub header_checksum: u8,
54}
55
56/// Total wire length of an un-encoded frame header in bytes (13 bytes: sync + header + checksum).
57pub(crate) const HEADER_LEN: usize = std::mem::size_of::<WireHeader>();
58
59/// Maximum payload size in bytes accepted by framing encoders and parsers.
60pub const MAX_PAYLOAD_SIZE: usize = 1024;
61
62/// Maximum total wire size in bytes of an encoded frame (header + payload + CRC-32).
63pub const MAX_FRAME_SIZE: usize = HEADER_LEN + MAX_PAYLOAD_SIZE + CHECKSUM_LEN;
64
65#[cfg(test)]
66pub(crate) const PAYLOAD_LEN_OFFSET: usize = std::mem::offset_of!(WireHeader, payload_len);
67pub(crate) const HEADER_CHECKSUM_OFFSET: usize = std::mem::offset_of!(WireHeader, header_checksum);
68
69/// Protocol frame type indicator.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub enum FrameType {
72    /// Unreliable or reliable payload data frame.
73    Data,
74    /// Cumulative sequence acknowledgment frame.
75    Ack,
76    /// Channel termination frame.
77    Close,
78    /// Transport link reset frame.
79    Reset,
80    /// Dynamic protocol negotiation request (Channel 0 only).
81    NegotiateReq,
82    /// Dynamic protocol negotiation response (Channel 0 only).
83    NegotiateResp,
84    /// Unrecognized frame type received over the wire.
85    Unknown(u8),
86}
87
88impl From<u8> for FrameType {
89    fn from(val: u8) -> Self {
90        match val {
91            TYPE_DATA => FrameType::Data,
92            TYPE_ACK => FrameType::Ack,
93            TYPE_CLOSE => FrameType::Close,
94            TYPE_RESET => FrameType::Reset,
95            TYPE_NEGOTIATE_REQ => FrameType::NegotiateReq,
96            TYPE_NEGOTIATE_RESP => FrameType::NegotiateResp,
97            other => FrameType::Unknown(other),
98        }
99    }
100}
101
102impl From<FrameType> for u8 {
103    fn from(ft: FrameType) -> Self {
104        match ft {
105            FrameType::Data => TYPE_DATA,
106            FrameType::Ack => TYPE_ACK,
107            FrameType::Close => TYPE_CLOSE,
108            FrameType::Reset => TYPE_RESET,
109            FrameType::NegotiateReq => TYPE_NEGOTIATE_REQ,
110            FrameType::NegotiateResp => TYPE_NEGOTIATE_RESP,
111            FrameType::Unknown(val) => val,
112        }
113    }
114}
115
116impl std::fmt::Display for FrameType {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        write!(f, "{self:?}")
119    }
120}
121
122/// Protocol version identifier negotiated during handshake.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124pub enum ProtocolId {
125    /// ResendSP v1 (Sliding-window Go-Back-N ARQ with CRC-32).
126    ResendSP,
127    /// Forward-compatible representation of an unknown protocol ID proposed by a future peer.
128    Unknown(u32),
129}
130
131impl ProtocolId {
132    /// Returns the 32-bit wire integer representation of this protocol ID.
133    pub fn wire_id(self) -> u32 {
134        match self {
135            ProtocolId::ResendSP => 1,
136            ProtocolId::Unknown(val) => val,
137        }
138    }
139}
140
141impl From<u32> for ProtocolId {
142    fn from(val: u32) -> Self {
143        match val {
144            1 => ProtocolId::ResendSP,
145            other => ProtocolId::Unknown(other),
146        }
147    }
148}
149
150impl From<ProtocolId> for u32 {
151    fn from(proto: ProtocolId) -> Self {
152        proto.wire_id()
153    }
154}
155
156impl std::fmt::Display for ProtocolId {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            ProtocolId::ResendSP => write!(f, "ResendSP"),
160            ProtocolId::Unknown(id) => write!(f, "Unknown({id})"),
161        }
162    }
163}
164
165impl Default for ProtocolId {
166    fn default() -> Self {
167        ProtocolId::ResendSP
168    }
169}
170
171/// Wire representation of a parsed UART protocol frame emitted by [`crate::FrameParser`].
172///
173/// While `Frame` represents an in-memory parsed frame, [`encode_frame`] is the primary
174/// zero-allocation serializer for transmitting byte slices over the wire.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct Frame {
177    /// 32-bit session identifier negotiated during handshake.
178    pub session_id: u32,
179    /// Logical channel identifier (Channel 0 is reserved for control).
180    pub channel_id: u16,
181    /// 8-bit Go-Back-N sequence number.
182    pub seq: u8,
183    /// Protocol frame type indicator.
184    pub frame_type: FrameType,
185    /// Raw un-encoded payload bytes.
186    pub payload: Vec<u8>,
187}
188
189impl Frame {
190    /// Creates a new validated protocol frame.
191    ///
192    /// # Errors
193    /// Returns [`FrameError::PayloadTooLarge`] if `payload.len() > MAX_PAYLOAD_SIZE`.
194    pub fn new(
195        session_id: u32,
196        channel_id: u16,
197        seq: u8,
198        frame_type: FrameType,
199        payload: Vec<u8>,
200    ) -> Result<Self, FrameError> {
201        if payload.len() > MAX_PAYLOAD_SIZE {
202            return Err(FrameError::PayloadTooLarge(payload.len()));
203        }
204        Ok(Self { session_id, channel_id, seq, frame_type, payload })
205    }
206
207    /// Serializes and encodes this frame with header, header checksum, and trailing CRC-32.
208    ///
209    /// # Errors
210    /// Returns [`FrameError::PayloadTooLarge`] if `self.payload.len() > MAX_PAYLOAD_SIZE`.
211    pub fn encode(&self) -> Result<Vec<u8>, FrameError> {
212        encode_frame(self.session_id, self.channel_id, self.seq, self.frame_type, &self.payload)
213    }
214
215    /// Returns the total wire length of this frame in bytes when encoded.
216    pub fn wire_len(&self) -> usize {
217        HEADER_LEN + self.payload.len() + CHECKSUM_LEN
218    }
219}
220
221/// Computes the IEEE 802.3 CRC-32 checksum of a byte slice.
222pub(crate) fn crc32(data: &[u8]) -> u32 {
223    crc32fast::hash(data)
224}
225
226/// Computes an 8-bit CRC over data using polynomial 0x07 (matching Pigweed pw_ulink).
227pub(crate) fn crc8(data: &[u8]) -> u8 {
228    let mut crc: u8 = 0x00;
229    for &byte in data {
230        crc ^= byte;
231        for _ in 0..8 {
232            if (crc & 0x80) != 0 {
233                crc = (crc << 1) ^ 0x07;
234            } else {
235                crc <<= 1;
236            }
237        }
238    }
239    crc
240}
241
242/// Serializes and encodes a protocol frame with the 13-byte binary header,
243/// 1-byte header checksum (`HdrChk`), and trailing CRC-32 checksum.
244///
245/// Layout: `[Sync (2B)] + [Session (4B)] + [Channel (2B)] + [Seq (1B)] + [Type (1B)] + [Len (2B)] + [HdrChk (1B)] + [Payload (L B)] + [CRC-32 (4B)]`
246///
247/// # Errors
248/// Returns [`FrameError::PayloadTooLarge`] if `payload.len() > MAX_PAYLOAD_SIZE`.
249pub fn encode_frame(
250    session_id: u32,
251    channel_id: u16,
252    seq: u8,
253    frame_type: FrameType,
254    payload: &[u8],
255) -> Result<Vec<u8>, FrameError> {
256    if payload.len() > MAX_PAYLOAD_SIZE {
257        return Err(FrameError::PayloadTooLarge(payload.len()));
258    }
259
260    let mut header = WireHeader {
261        sync: SYNC_WORD,
262        session_id: BeU32::new(session_id),
263        channel_id: BeU16::new(channel_id),
264        seq,
265        frame_type: frame_type.into(),
266        payload_len: BeU16::new(payload.len() as u16),
267        header_checksum: 0,
268    };
269    let header_bytes = header.as_bytes();
270    header.header_checksum = crc8(&header_bytes[SYNC_WORD_LEN..HEADER_CHECKSUM_OFFSET]);
271
272    let mut frame = Vec::with_capacity(HEADER_LEN + payload.len() + CHECKSUM_LEN);
273    frame.extend_from_slice(header.as_bytes());
274    frame.extend_from_slice(payload);
275
276    let cksum = crc32(&frame[SYNC_WORD_LEN..]);
277    frame.extend_from_slice(&cksum.to_be_bytes());
278
279    Ok(frame)
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[fuchsia::test]
287    fn test_crc32() {
288        assert_eq!(crc32(b""), 0);
289        assert_eq!(crc32(b"123456789"), 0xCBF43926);
290    }
291
292    #[fuchsia::test]
293    fn test_crc8() {
294        assert_eq!(crc8(b""), 0);
295        assert_eq!(crc8(b"123456789"), 0xF4);
296    }
297
298    #[fuchsia::test]
299    fn test_encode_frame() {
300        let payload = b"hello";
301        let encoded = encode_frame(0x12345678, 1, 0, FrameType::Data, payload)
302            .expect("payload length is within MAX_PAYLOAD_SIZE");
303        assert_eq!(encoded[0], 0xAA);
304        assert_eq!(encoded[1], 0x55);
305        assert_eq!(encoded.len(), HEADER_LEN + payload.len() + CHECKSUM_LEN);
306    }
307
308    #[fuchsia::test]
309    fn test_encode_frame_payload_exceeding_max_payload_size() {
310        let oversized = vec![0u8; MAX_PAYLOAD_SIZE + 1];
311        let res = encode_frame(1, 1, 1, FrameType::Data, &oversized);
312        assert_eq!(res, Err(FrameError::PayloadTooLarge(MAX_PAYLOAD_SIZE + 1)));
313    }
314
315    #[fuchsia::test]
316    fn test_encode_frame_max_payload_boundary() {
317        let max_payload = vec![0x42u8; MAX_PAYLOAD_SIZE];
318        let encoded = encode_frame(1, 1, 1, FrameType::Data, &max_payload)
319            .expect("MAX_PAYLOAD_SIZE boundary must encode successfully");
320        assert_eq!(encoded.len(), HEADER_LEN + MAX_PAYLOAD_SIZE + CHECKSUM_LEN);
321    }
322
323    #[fuchsia::test]
324    fn test_frame_type_conversions() {
325        assert_eq!(FrameType::from(0x01), FrameType::Data);
326        assert_eq!(FrameType::from(0x02), FrameType::Ack);
327        assert_eq!(FrameType::from(0x03), FrameType::Close);
328        assert_eq!(FrameType::from(0x04), FrameType::Reset);
329        assert_eq!(FrameType::from(0x10), FrameType::NegotiateReq);
330        assert_eq!(FrameType::from(0x11), FrameType::NegotiateResp);
331        assert_eq!(FrameType::from(0x99), FrameType::Unknown(0x99));
332
333        assert_eq!(u8::from(FrameType::Data), 0x01);
334        assert_eq!(u8::from(FrameType::Ack), 0x02);
335        assert_eq!(u8::from(FrameType::Close), 0x03);
336        assert_eq!(u8::from(FrameType::Reset), 0x04);
337        assert_eq!(u8::from(FrameType::NegotiateReq), 0x10);
338        assert_eq!(u8::from(FrameType::NegotiateResp), 0x11);
339        assert_eq!(u8::from(FrameType::Unknown(0x99)), 0x99);
340
341        assert_eq!(FrameType::Data.to_string(), "Data");
342        assert_eq!(FrameType::Ack.to_string(), "Ack");
343        assert_eq!(FrameType::Close.to_string(), "Close");
344        assert_eq!(FrameType::Reset.to_string(), "Reset");
345        assert_eq!(FrameType::NegotiateReq.to_string(), "NegotiateReq");
346        assert_eq!(FrameType::NegotiateResp.to_string(), "NegotiateResp");
347        assert_eq!(FrameType::Unknown(0x99).to_string(), "Unknown(153)");
348    }
349
350    #[fuchsia::test]
351    fn test_protocol_id_conversions() {
352        assert_eq!(ProtocolId::from(1), ProtocolId::ResendSP);
353        assert_eq!(ProtocolId::from(42), ProtocolId::Unknown(42));
354
355        assert_eq!(ProtocolId::default(), ProtocolId::ResendSP);
356
357        assert_eq!(ProtocolId::ResendSP.wire_id(), 1);
358        assert_eq!(ProtocolId::Unknown(42).wire_id(), 42);
359
360        assert_eq!(ProtocolId::ResendSP.to_string(), "ResendSP");
361        assert_eq!(ProtocolId::Unknown(42).to_string(), "Unknown(42)");
362    }
363
364    #[fuchsia::test]
365    fn test_frame_inherent_methods() {
366        let payload = b"payload".to_vec();
367        let frame = Frame::new(42, CONTROL_CHANNEL_ID, 7, FrameType::Data, payload.clone())
368            .expect("valid frame creation");
369        assert_eq!(frame.session_id, 42);
370        assert_eq!(frame.channel_id, CONTROL_CHANNEL_ID);
371        assert_eq!(frame.seq, 7);
372        assert_eq!(frame.frame_type, FrameType::Data);
373        assert_eq!(frame.payload, payload);
374        assert_eq!(frame.wire_len(), HEADER_LEN + payload.len() + CHECKSUM_LEN);
375
376        let encoded = frame.encode().expect("encode succeeds");
377        assert_eq!(encoded.len(), frame.wire_len());
378
379        let oversized = vec![0u8; MAX_PAYLOAD_SIZE + 1];
380        let err = Frame::new(42, 1, 0, FrameType::Data, oversized).unwrap_err();
381        assert_eq!(err, FrameError::PayloadTooLarge(MAX_PAYLOAD_SIZE + 1));
382    }
383}