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}