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