Skip to main content

pamoja_mesh/
frame.rs

1//! The mesh frame: the addressed, hop-limited, checksummed packet on the wire.
2
3use crate::crc::Crc16;
4use crate::error::MeshError;
5
6/// The destination that addresses a frame to every node, for flooding the whole mesh.
7pub const BROADCAST: u32 = 0xFFFF_FFFF;
8
9/// An addressed mesh packet.
10///
11/// A frame names where it came from and where it is going, carries a sequence number its
12/// origin assigns, counts down a hop limit as it is relayed, and ends with a checksum.
13/// The byte layout is fixed and big-endian:
14///
15/// ```text
16/// 0       version
17/// 1..=4   source node       (u32)
18/// 5..=8   destination node  (u32, BROADCAST for every node)
19/// 9..=10  sequence id       (u16)
20/// 11      hop limit
21/// 12..    payload
22/// last 2  checksum          (u16)
23/// ```
24///
25/// The checksum covers every byte except the hop limit, which changes at each relay. So
26/// the check is end to end: a node can confirm a flooded packet's payload is intact no
27/// matter how many relays forwarded it, and a relay spends a hop without recomputing it.
28/// The whole frame lives in a fixed buffer, so neither building nor parsing allocates.
29///
30/// # Examples
31///
32/// ```
33/// use pamoja_mesh::Frame;
34///
35/// let frame = Frame::new(0x0A, 0x0B, 7, b"hello").unwrap();
36/// assert_eq!(frame.src(), 0x0A);
37/// assert_eq!(frame.dst(), 0x0B);
38/// assert_eq!(frame.id(), 7);
39/// assert_eq!(frame.payload(), b"hello");
40///
41/// let received = Frame::parse(frame.as_bytes()).unwrap();
42/// assert_eq!(received.payload(), b"hello");
43/// ```
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct Frame {
46    bytes: [u8; Frame::MAX_LEN],
47    len: usize,
48}
49
50impl Frame {
51    /// The largest a mesh frame may be, in bytes, sized to the payload of a connectionless
52    /// ESP-NOW frame.
53    pub const MAX_LEN: usize = 250;
54
55    /// The fixed header length in bytes: version, source, destination, sequence id, and
56    /// hop limit.
57    pub const HEADER_LEN: usize = 12;
58
59    /// The non-payload bytes of a frame: the header plus the trailing checksum.
60    pub const OVERHEAD: usize = Self::HEADER_LEN + 2;
61
62    /// The largest payload a single frame can carry.
63    pub const MAX_PAYLOAD: usize = Self::MAX_LEN - Self::OVERHEAD;
64
65    /// The protocol version this build writes and accepts.
66    pub const VERSION: u8 = 1;
67
68    /// The hop limit a newly built frame starts with, enough for a small local mesh.
69    pub const DEFAULT_HOP_LIMIT: u8 = 3;
70
71    // The offset of the hop-limit byte, the one byte the checksum does not cover.
72    const HOP_LIMIT: usize = 11;
73
74    /// Builds a frame from a source, a destination, a sequence id, and a payload, starting
75    /// at [`DEFAULT_HOP_LIMIT`](Frame::DEFAULT_HOP_LIMIT).
76    ///
77    /// # Arguments
78    ///
79    /// * `src` - the origin node's address.
80    /// * `dst` - the destination node's address, or [`BROADCAST`] for every node.
81    /// * `id` - the sequence number the origin assigns, increasing per message; with the
82    ///   source it identifies a packet as it floods, for [`dedup_key`](Frame::dedup_key).
83    /// * `payload` - the bytes to carry.
84    ///
85    /// # Returns
86    ///
87    /// The frame, ready to send.
88    ///
89    /// # Errors
90    ///
91    /// Returns [`MeshError::PayloadTooLong`] if `payload` is longer than
92    /// [`MAX_PAYLOAD`](Frame::MAX_PAYLOAD).
93    pub fn new(src: u32, dst: u32, id: u16, payload: &[u8]) -> Result<Frame, MeshError> {
94        if payload.len() > Self::MAX_PAYLOAD {
95            return Err(MeshError::PayloadTooLong);
96        }
97        let len = Self::OVERHEAD + payload.len();
98        let mut bytes = [0u8; Self::MAX_LEN];
99        bytes[0] = Self::VERSION;
100        bytes[1..5].copy_from_slice(&src.to_be_bytes());
101        bytes[5..9].copy_from_slice(&dst.to_be_bytes());
102        bytes[9..11].copy_from_slice(&id.to_be_bytes());
103        bytes[Self::HOP_LIMIT] = Self::DEFAULT_HOP_LIMIT;
104        bytes[12..12 + payload.len()].copy_from_slice(payload);
105        let crc = Self::checksum(&bytes, len);
106        bytes[len - 2..len].copy_from_slice(&crc.to_be_bytes());
107        Ok(Frame { bytes, len })
108    }
109
110    /// Builds a frame addressed to every node, for flooding the whole mesh.
111    ///
112    /// # Arguments
113    ///
114    /// * `src` - the origin node's address.
115    /// * `id` - the sequence number the origin assigns.
116    /// * `payload` - the bytes to carry.
117    ///
118    /// # Returns
119    ///
120    /// The broadcast frame, ready to send.
121    ///
122    /// # Errors
123    ///
124    /// Returns [`MeshError::PayloadTooLong`] if `payload` is longer than
125    /// [`MAX_PAYLOAD`](Frame::MAX_PAYLOAD).
126    pub fn broadcast(src: u32, id: u16, payload: &[u8]) -> Result<Frame, MeshError> {
127        Self::new(src, BROADCAST, id, payload)
128    }
129
130    /// Sets the hop limit, the number of further relays the frame is allowed.
131    ///
132    /// The checksum does not cover the hop limit, so this needs no recomputation and
133    /// leaves a parsed frame still valid.
134    ///
135    /// # Arguments
136    ///
137    /// * `hop_limit` - the new hop limit. `0` means no node should relay the frame further.
138    ///
139    /// # Returns
140    ///
141    /// The frame with the hop limit set, for chaining.
142    pub fn with_hop_limit(mut self, hop_limit: u8) -> Frame {
143        self.bytes[Self::HOP_LIMIT] = hop_limit;
144        self
145    }
146
147    /// Parses a received frame, verifying its version and checksum.
148    ///
149    /// # Arguments
150    ///
151    /// * `bytes` - the raw frame as it came off the radio.
152    ///
153    /// # Returns
154    ///
155    /// The validated frame.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`MeshError::FrameTooShort`] or [`MeshError::FrameTooLong`] if the length is
160    /// outside a frame's bounds, [`MeshError::UnsupportedVersion`] if the version byte is
161    /// not [`VERSION`](Frame::VERSION), or [`MeshError::CrcMismatch`] if the checksum does
162    /// not match the contents.
163    pub fn parse(bytes: &[u8]) -> Result<Frame, MeshError> {
164        if bytes.len() < Self::OVERHEAD {
165            return Err(MeshError::FrameTooShort);
166        }
167        if bytes.len() > Self::MAX_LEN {
168            return Err(MeshError::FrameTooLong);
169        }
170        if bytes[0] != Self::VERSION {
171            return Err(MeshError::UnsupportedVersion(bytes[0]));
172        }
173        let len = bytes.len();
174        let expected = Self::checksum(bytes, len);
175        let found = u16::from_be_bytes([bytes[len - 2], bytes[len - 1]]);
176        if expected != found {
177            return Err(MeshError::CrcMismatch { expected, found });
178        }
179        let mut buffer = [0u8; Self::MAX_LEN];
180        buffer[..len].copy_from_slice(bytes);
181        Ok(Frame { bytes: buffer, len })
182    }
183
184    // The checksum over every byte except the mutable hop limit and the checksum field:
185    // the leading header bytes, then the payload.
186    fn checksum(bytes: &[u8], len: usize) -> u16 {
187        let mut crc = Crc16::new();
188        crc.update(&bytes[..Self::HOP_LIMIT]);
189        crc.update(&bytes[12..len - 2]);
190        crc.finish()
191    }
192
193    /// Returns the protocol version.
194    ///
195    /// # Returns
196    ///
197    /// The version byte.
198    pub fn version(&self) -> u8 {
199        self.bytes[0]
200    }
201
202    /// Returns the source node's address.
203    ///
204    /// # Returns
205    ///
206    /// The address of the node that originated the frame.
207    pub fn src(&self) -> u32 {
208        u32::from_be_bytes([self.bytes[1], self.bytes[2], self.bytes[3], self.bytes[4]])
209    }
210
211    /// Returns the destination node's address.
212    ///
213    /// # Returns
214    ///
215    /// The address of the destination node, or [`BROADCAST`] for every node.
216    pub fn dst(&self) -> u32 {
217        u32::from_be_bytes([self.bytes[5], self.bytes[6], self.bytes[7], self.bytes[8]])
218    }
219
220    /// Returns the sequence id the origin assigned.
221    ///
222    /// # Returns
223    ///
224    /// The sequence number.
225    pub fn id(&self) -> u16 {
226        u16::from_be_bytes([self.bytes[9], self.bytes[10]])
227    }
228
229    /// Returns the remaining hop limit.
230    ///
231    /// # Returns
232    ///
233    /// The number of further relays the frame is allowed.
234    pub fn hop_limit(&self) -> u8 {
235        self.bytes[Self::HOP_LIMIT]
236    }
237
238    /// Returns the payload.
239    ///
240    /// # Returns
241    ///
242    /// The carried bytes, without the header or checksum.
243    pub fn payload(&self) -> &[u8] {
244        &self.bytes[12..self.len - 2]
245    }
246
247    /// Returns the whole frame, checksum included, ready for the radio.
248    ///
249    /// # Returns
250    ///
251    /// The frame as a byte slice.
252    pub fn as_bytes(&self) -> &[u8] {
253        &self.bytes[..self.len]
254    }
255
256    /// Reports whether the frame is addressed to every node.
257    ///
258    /// # Returns
259    ///
260    /// `true` if the destination is [`BROADCAST`].
261    pub fn is_broadcast(&self) -> bool {
262        self.dst() == BROADCAST
263    }
264
265    /// Returns the key that identifies this packet as it floods: its source and sequence
266    /// id.
267    ///
268    /// # Returns
269    ///
270    /// The `(source, id)` pair, for a [`SeenCache`](crate::SeenCache).
271    pub fn dedup_key(&self) -> (u32, u16) {
272        (self.src(), self.id())
273    }
274
275    /// Returns the frame to forward one hop further, with a hop spent.
276    ///
277    /// # Returns
278    ///
279    /// The same frame with its hop limit reduced by one, or [`None`] if the hop limit is
280    /// already `0` and the frame must not be relayed further.
281    pub fn relayed(&self) -> Option<Frame> {
282        let hop_limit = self.hop_limit();
283        if hop_limit == 0 {
284            return None;
285        }
286        let mut forwarded = *self;
287        forwarded.bytes[Self::HOP_LIMIT] = hop_limit - 1;
288        Some(forwarded)
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn new_then_parse_round_trips() {
298        let frame = Frame::new(0x0102_0304, 0x0506_0708, 0x090A, b"payload").unwrap();
299        let parsed = Frame::parse(frame.as_bytes()).unwrap();
300        assert_eq!(parsed.version(), Frame::VERSION);
301        assert_eq!(parsed.src(), 0x0102_0304);
302        assert_eq!(parsed.dst(), 0x0506_0708);
303        assert_eq!(parsed.id(), 0x090A);
304        assert_eq!(parsed.hop_limit(), Frame::DEFAULT_HOP_LIMIT);
305        assert_eq!(parsed.payload(), b"payload");
306    }
307
308    #[test]
309    fn an_empty_payload_round_trips() {
310        let frame = Frame::new(1, 2, 3, b"").unwrap();
311        assert_eq!(frame.as_bytes().len(), Frame::OVERHEAD);
312        let parsed = Frame::parse(frame.as_bytes()).unwrap();
313        assert_eq!(parsed.payload(), b"");
314    }
315
316    #[test]
317    fn broadcast_is_addressed_to_every_node() {
318        let frame = Frame::broadcast(0x42, 1, b"hi").unwrap();
319        assert_eq!(frame.dst(), BROADCAST);
320        assert!(frame.is_broadcast());
321        assert!(!Frame::new(0x42, 0x43, 1, b"hi").unwrap().is_broadcast());
322    }
323
324    #[test]
325    fn the_largest_payload_fits_and_a_larger_one_does_not() {
326        let big = [0u8; Frame::MAX_PAYLOAD];
327        let frame = Frame::new(1, 2, 3, &big).unwrap();
328        assert_eq!(frame.as_bytes().len(), Frame::MAX_LEN);
329
330        let too_big = [0u8; Frame::MAX_PAYLOAD + 1];
331        assert_eq!(
332            Frame::new(1, 2, 3, &too_big),
333            Err(MeshError::PayloadTooLong)
334        );
335    }
336
337    #[test]
338    fn parse_rejects_a_short_frame() {
339        let short = [0u8; Frame::OVERHEAD - 1];
340        assert_eq!(Frame::parse(&short), Err(MeshError::FrameTooShort));
341    }
342
343    #[test]
344    fn parse_rejects_an_oversized_frame() {
345        let big = [0u8; Frame::MAX_LEN + 1];
346        assert_eq!(Frame::parse(&big), Err(MeshError::FrameTooLong));
347    }
348
349    #[test]
350    fn parse_rejects_an_unknown_version() {
351        let mut bytes = Frame::new(1, 2, 3, b"x").unwrap().as_bytes().to_vec();
352        bytes[0] = 0xFF;
353        assert_eq!(
354            Frame::parse(&bytes),
355            Err(MeshError::UnsupportedVersion(0xFF))
356        );
357    }
358
359    #[test]
360    fn parse_rejects_a_corrupt_payload() {
361        let mut bytes = Frame::new(1, 2, 3, b"data").unwrap().as_bytes().to_vec();
362        bytes[12] ^= 0xFF; // flip a payload byte
363        assert!(matches!(
364            Frame::parse(&bytes),
365            Err(MeshError::CrcMismatch { .. })
366        ));
367    }
368
369    #[test]
370    fn the_checksum_ignores_the_hop_limit() {
371        // Changing only the hop-limit byte must not break the end-to-end checksum, which
372        // is what lets a relay spend a hop without recomputing it.
373        let frame = Frame::new(1, 2, 3, b"data").unwrap();
374        let mut bytes = frame.as_bytes().to_vec();
375        bytes[11] = 99;
376        let parsed = Frame::parse(&bytes).unwrap();
377        assert_eq!(parsed.hop_limit(), 99);
378    }
379
380    #[test]
381    fn with_hop_limit_leaves_the_frame_valid() {
382        let frame = Frame::new(1, 2, 3, b"data").unwrap().with_hop_limit(7);
383        assert_eq!(frame.hop_limit(), 7);
384        assert_eq!(Frame::parse(frame.as_bytes()).unwrap().hop_limit(), 7);
385    }
386
387    #[test]
388    fn relaying_spends_a_hop_and_keeps_everything_else() {
389        let frame = Frame::new(0xAA, 0xBB, 5, b"flood")
390            .unwrap()
391            .with_hop_limit(2);
392        let forwarded = frame.relayed().unwrap();
393        assert_eq!(forwarded.hop_limit(), 1);
394        assert_eq!(forwarded.src(), frame.src());
395        assert_eq!(forwarded.dst(), frame.dst());
396        assert_eq!(forwarded.id(), frame.id());
397        assert_eq!(forwarded.payload(), frame.payload());
398        // The forwarded frame is still valid on the wire.
399        assert!(Frame::parse(forwarded.as_bytes()).is_ok());
400    }
401
402    #[test]
403    fn a_frame_out_of_hops_is_not_relayed() {
404        let frame = Frame::new(1, 2, 3, b"x").unwrap().with_hop_limit(0);
405        assert_eq!(frame.relayed(), None);
406    }
407
408    #[test]
409    fn dedup_key_is_source_and_id() {
410        let frame = Frame::new(0xDEAD_BEEF, 2, 0x1234, b"x").unwrap();
411        assert_eq!(frame.dedup_key(), (0xDEAD_BEEF, 0x1234));
412    }
413
414    #[test]
415    fn the_largest_payload_round_trips() {
416        let payload = [0xCD; Frame::MAX_PAYLOAD];
417        let frame = Frame::new(1, 2, 3, &payload).unwrap();
418        assert_eq!(frame.as_bytes().len(), Frame::MAX_LEN);
419        let parsed = Frame::parse(frame.as_bytes()).unwrap();
420        assert_eq!(parsed.payload(), &payload[..]);
421    }
422}