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}