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}