Skip to main content

rm_frame/
frame.rs

1//! Frame Encoding and Decoding.
2
3use crate::PackError;
4use crate::{DjiValidator, UnPackError, Validator};
5use crate::{ImplMarshal, ImplUnMarshal, MarshalerError};
6use core::marker::PhantomData;
7
8/// Start of Frame Byte
9const SOF: u8 = 0xA5;
10
11/// Size of the frame header (SOF + length + sequence + CRC8).
12const HEAD_SIZE: usize = 5;
13/// Size of the command ID field.
14const CMDID_SIZE: usize = size_of::<u16>();
15/// Size of the tail CRC field.
16const TAIL_SIZE: usize = size_of::<u16>();
17
18/// Frame encoder and decoder.
19///
20/// [`Messager`] packs typed messages into binary frames and unpacks
21/// validated frames from raw bytes. It performs no I/O and allocates
22/// no memory.
23///
24/// Generic over [`Validator`] to allow custom CRC implementations.
25/// The default validator is [`DjiValidator`].
26///
27/// The internal sequence counter starts at the value passed to [`new`](Self::new)
28/// and increments by one after each successful [`pack`](Self::pack) call,
29/// wrapping on overflow.
30///
31/// # Frame Layout
32///
33/// ```text
34/// +--------+--------+--------+--------+--------+---------+--------+
35/// |  SOF   |  LEN   |  SEQ   |  CRC8  | CMD_ID |  DATA   | CRC16  |
36/// +--------+--------+--------+--------+--------+---------+--------+
37/// | 1 byte | 2 byte | 1 byte | 1 byte | 2 byte | N bytes | 2 byte |
38/// +--------+--------+--------+--------+--------+---------+--------+
39/// ```
40///
41/// - `SOF`: start-of-frame marker (`0xA5`)
42/// - `LEN`: payload length, little-endian `u16`
43/// - `SEQ`: sequence number, `u8`
44/// - `CRC8`: checksum over `[SOF, LEN, SEQ]`
45/// - `CMD_ID`: command identifier, little-endian `u16`
46/// - `DATA`: payload bytes (N = [`crate::Marshaler::PAYLOAD_SIZE`])
47/// - `CRC16`: checksum over the entire frame preceding this field
48#[derive(Debug)]
49#[cfg_attr(feature = "defmt", derive(defmt::Format))]
50#[doc(alias("Msg", "Msger", "MessageParser", "FrameParser"))]
51pub struct Messager<V: Validator = DjiValidator> {
52    /// Current frame sequence number.
53    sequence: u8,
54    /// Marker for the validator type.
55    _marker: PhantomData<V>,
56}
57
58impl<V: Validator> Messager<V> {
59    /// Creates a new [`Messager`] with the given initial sequence number.
60    ///
61    /// The sequence number is embedded in each packed frame header and
62    /// increments automatically on each successful [`pack`](Self::pack) call.
63    pub const fn new(seq: u8) -> Self {
64        Self {
65            sequence: seq,
66            _marker: PhantomData,
67        }
68    }
69
70    /// Encode a message into a binary frame.
71    ///
72    /// Serializes `msg` and writes the complete frame — header, command ID,
73    /// payload, and CRC — into `dst`. The payload is written directly into
74    /// `dst` with no intermediate buffer.
75    ///
76    /// On success, returns the total number of bytes written.
77    ///
78    /// The sequence counter increments by one after each successful call.
79    ///
80    /// # Errors
81    ///
82    /// - [`PackError::BufferTooSmall`] — `dst` is smaller than the full frame.
83    ///   `need` is the minimum required size in bytes.
84    /// - [`PackError::InvalidPayloadSize`] — the marshaler returned a byte
85    ///   count that does not equal [`crate::Marshaler::PAYLOAD_SIZE`].
86    /// - [`PackError::MarshalerError`] — the marshaler itself failed.
87    pub fn pack<M: ImplMarshal>(&mut self, msg: &M, dst: &mut [u8]) -> Result<usize, PackError> {
88        let mut cursor: usize = 0;
89
90        let payload_offset = HEAD_SIZE + CMDID_SIZE;
91        let payload_size = M::PAYLOAD_SIZE as usize;
92
93        // Ensure space for the entire frame before writing anything.
94        let total = payload_offset + payload_size + TAIL_SIZE;
95        if dst.len() < total {
96            return Err(PackError::BufferTooSmall { need: total });
97        }
98
99        // Serialize payload directly into the destination buffer.
100        let size = msg.marshal(&mut dst[payload_offset..payload_offset + payload_size])?;
101
102        // Validate payload length.
103        if size != payload_size {
104            return Err(PackError::InvalidPayloadSize {
105                expected: M::PAYLOAD_SIZE as usize,
106                found: size,
107            });
108        }
109
110        // Prepare Header
111        let cmd_id = M::CMD_ID;
112        let sequence = self.sequence;
113
114        // Build frame header.
115        let header: [u8; HEAD_SIZE] = {
116            let mut temp = [0; _];
117            let size_bytes = (payload_size as u16).to_le_bytes();
118            temp[0] = SOF;
119            temp[1] = size_bytes[0];
120            temp[2] = size_bytes[1];
121            temp[3] = sequence;
122            temp[4] = V::calculate_crc8(&temp[..4]);
123            temp
124        };
125
126        // Write header.
127        dst[cursor..cursor + HEAD_SIZE].copy_from_slice(&header);
128        cursor += HEAD_SIZE;
129
130        // Write command ID.
131        dst[cursor..cursor + CMDID_SIZE].copy_from_slice(&cmd_id.to_le_bytes());
132        cursor += CMDID_SIZE;
133
134        // Skip over payload (already written).
135        cursor += payload_size;
136
137        // Write frame CRC.
138        let crc: u16 = V::calculate_crc16(&dst[..cursor]);
139        dst[cursor..cursor + TAIL_SIZE].copy_from_slice(&crc.to_le_bytes());
140        cursor += TAIL_SIZE;
141
142        // Advance sequence number.
143        self.sequence = self.sequence.wrapping_add(1);
144
145        Ok(cursor)
146    }
147
148    /// Parse and validate one frame from raw bytes.
149    ///
150    /// Reads from the start of `src` and performs these checks in order:
151    ///
152    /// 1. Start-of-frame marker (`0xA5`)
153    /// 2. Header CRC8
154    /// 3. Frame CRC16
155    ///
156    /// On success, returns a [`RawFrame`] whose payload borrows from `src`,
157    /// and the number of bytes consumed.
158    ///
159    /// The payload in the returned [`RawFrame`] is untyped. Use
160    /// [`RawFrame::unmarshal`] to decode it, or call [`unmarshal`](Self::unmarshal)
161    /// instead of this method to do both steps at once.
162    ///
163    /// # Errors
164    ///
165    /// - [`UnPackError::ReSync`] — `src` does not start with SOF.
166    ///   `skip` is the offset of the next SOF byte; discard that many bytes
167    ///   and retry.
168    /// - [`UnPackError::MissingHeader`] — no SOF byte found anywhere in `src`.
169    ///   `skip` equals `src.len()`; discard the entire buffer and wait for
170    ///   more data.
171    /// - [`UnPackError::UnexpectedEnd`] — the frame is truncated.
172    ///   Keep the existing bytes and append more data before retrying.
173    /// - [`UnPackError::InvalidChecksum`] — CRC validation failed.
174    ///   Call [`UnPackError::skip`] to determine how many bytes to discard.
175    pub fn unpack<'t>(&self, src: &'t [u8]) -> Result<(RawFrame<'t>, usize), UnPackError> {
176        let mut cursor = 0;
177
178        // Locate start-of-frame.
179        if !src.starts_with(&[SOF]) {
180            if let Some(start) = src.iter().position(|&x| SOF == x) {
181                return Err(UnPackError::ReSync { skip: start });
182            } else {
183                return Err(UnPackError::MissingHeader { skip: src.len() });
184            }
185        }
186
187        // Read header.
188        let Some(header) = src.get(cursor..cursor + HEAD_SIZE) else {
189            return Err(UnPackError::UnexpectedEnd { read: src.len() });
190        };
191        cursor += HEAD_SIZE;
192
193        // Validate header and extract metadata.
194        let (length, sequence) = {
195            let (header_bytes, crc) = (&header[..4], header[4]);
196            if V::calculate_crc8(header_bytes) != crc {
197                return Err(UnPackError::InvalidChecksum { at: cursor });
198            }
199
200            // `header_bytes[0]` is the SOF byte, which we have already validated.
201            let length = u16::from_le_bytes([header_bytes[1], header_bytes[2]]);
202            let sequence = header_bytes[3];
203            (length as usize, sequence)
204        };
205
206        // Read command ID.
207        let Some(cmd) = src.get(cursor..cursor + CMDID_SIZE) else {
208            return Err(UnPackError::UnexpectedEnd { read: src.len() });
209        };
210        cursor += CMDID_SIZE;
211
212        // Read payload.
213        let Some(payload) = src.get(cursor..cursor + length) else {
214            return Err(UnPackError::UnexpectedEnd { read: src.len() });
215        };
216        cursor += length;
217
218        // Get the raw data for CRC calculation
219        // Note: `cursor` is within bounds due to previous checks
220        let frame_bytes = &src[..cursor];
221
222        // Read and validate tail CRC.
223        let Some(tail) = src.get(cursor..cursor + TAIL_SIZE) else {
224            return Err(UnPackError::UnexpectedEnd { read: src.len() });
225        };
226        cursor += TAIL_SIZE;
227
228        // Note: `tail` has a Fixed Length of 2
229        let crc = u16::from_le_bytes([tail[0], tail[1]]);
230
231        // Validate CRC
232        if V::calculate_crc16(frame_bytes) != crc {
233            return Err(UnPackError::InvalidChecksum { at: cursor });
234        }
235
236        // Parse Cmd ID
237        let cmd_id = u16::from_le_bytes([cmd[0], cmd[1]]);
238
239        // Construct Payload
240        Ok((
241            RawFrame {
242                cmd_id,
243                sequence,
244                payload,
245            },
246            cursor,
247        ))
248    }
249
250    /// Parse a frame and decode its payload into a typed message.
251    ///
252    /// Combines [`unpack`](Self::unpack) and [`RawFrame::unmarshal`] in one call.
253    ///
254    /// On success, returns the decoded message and the number of bytes consumed.
255    ///
256    /// # Errors
257    ///
258    /// - Returns [`UnPackError`] for any framing or CRC failure (see [`unpack`](Self::unpack)).
259    /// - Returns [`UnPackError::MarshalerError`] if command ID or payload size
260    ///   does not match `M`, or if [`M::unmarshal`](crate::Marshaler::unmarshal) fails.
261    pub fn unmarshal<M: ImplUnMarshal>(&self, src: &[u8]) -> Result<(M, usize), UnPackError> {
262        let (frame, cursor) = self.unpack(src)?;
263        Ok((frame.unmarshal::<M>()?, cursor))
264    }
265}
266
267/// A validated, undecoded frame.
268///
269/// Produced by [`Messager::unpack`]. The frame has passed all structural
270/// and CRC checks, but the payload is still raw bytes.
271///
272/// Use [`unmarshal`](RawFrame::unmarshal) to decode the payload into a
273/// concrete type, or inspect the fields directly for routing purposes.
274///
275/// # Lifetime
276///
277/// `'t` is tied to the input buffer passed to [`Messager::unpack`].
278/// The payload slice borrows from that buffer with no copying.
279#[derive(Debug)]
280#[cfg_attr(feature = "defmt", derive(defmt::Format))]
281#[doc(alias("Frame", "RawMessage", "RawMsg"))]
282pub struct RawFrame<'t> {
283    /// Command ID of the frame.
284    pub(crate) cmd_id: u16,
285    /// Sequence number of the frame.
286    pub(crate) sequence: u8,
287    /// Raw payload bytes.
288    pub(crate) payload: &'t [u8],
289}
290
291impl<'t> RawFrame<'t> {
292    /// Returns the command ID from the frame header.
293    pub fn cmd_id(&self) -> u16 {
294        self.cmd_id
295    }
296
297    /// Returns the sequence number from the frame header.
298    pub fn sequence(&self) -> u8 {
299        self.sequence
300    }
301
302    /// Decode the payload into a typed message.
303    ///
304    /// Verifies that the frame's command ID matches [`crate::Marshaler::CMD_ID`] and that the
305    /// payload length matches `M::PAYLOAD_SIZE`, then delegates to `M::unmarshal`.
306    ///
307    /// # Errors
308    ///
309    /// - [`MarshalerError::InvalidCmdID`] — the frame command ID does not match `M`.
310    /// - [`MarshalerError::InvalidDataLength`] — payload length does not match
311    ///   `M::PAYLOAD_SIZE`.
312    /// - Any error returned by [`M::unmarshal`](crate::Marshaler::unmarshal).
313    pub fn unmarshal<M: ImplUnMarshal>(&self) -> Result<M, MarshalerError> {
314        if M::CMD_ID != self.cmd_id {
315            return Err(MarshalerError::InvalidCmdID {
316                expected: M::CMD_ID,
317                found: self.cmd_id,
318            });
319        }
320
321        if self.payload.len() != M::PAYLOAD_SIZE as usize {
322            return Err(MarshalerError::InvalidDataLength {
323                expected: M::PAYLOAD_SIZE as usize,
324                found: self.payload.len(),
325            });
326        }
327
328        M::unmarshal(self.payload)
329    }
330
331    /// Returns the raw payload bytes.
332    ///
333    /// The slice borrows directly from the input buffer passed to
334    /// [`Messager::unpack`]. It contains only the payload data —
335    /// no header, command ID, or CRC.
336    pub fn payload(&self) -> &'t [u8] {
337        self.payload
338    }
339}