Skip to main content

pamoja_radios/sx1302/
rx.rs

1//! Reading packets out of the concentrator.
2//!
3//! Received packets arrive in one buffer, back to back, each wrapped in metadata the chip
4//! writes around it: a sync word and nine bytes saying how long the payload is and how it was
5//! demodulated, then the payload, then fourteen more bytes of what the receiver made of it.
6//!
7//! The metadata is in two halves and they are addressed differently. Everything before the
8//! payload is counted from the start of the packet; everything after it is counted from the
9//! start plus the payload length, which is why a reader has to know how long the payload is
10//! before it can find the signal levels or the timestamp.
11//!
12//! A packet can also carry fine timestamp metrics after all of that, two bytes each, and the
13//! count lives in the tail. A reader that ignores them finds the next packet in the wrong
14//! place, so [`packet_len`] takes them into account and the walk in [`packets`] stays aligned.
15//!
16//! Nothing here reads a register. A buffer is bytes, and this turns those bytes into packets,
17//! so a capture from a working gateway can be replayed and checked without hardware.
18
19use super::spi;
20
21/// The first byte of the word that marks the start of a packet.
22pub const SYNC_BYTE_0: u8 = 0xa5;
23
24/// The second byte of that word.
25pub const SYNC_BYTE_1: u8 = 0xc0;
26
27/// How many bytes of metadata come before the payload, the sync word included.
28pub const HEAD_METADATA: usize = 9;
29
30/// How many bytes of metadata come after it, before any timestamp metrics.
31pub const TAIL_METADATA: usize = 14;
32
33/// How many bytes each fine timestamp metric takes.
34pub const METRIC_LEN: usize = 2;
35
36/// How many bytes the receive buffer holds in all.
37pub const BUFFER_LEN: usize = 4096;
38
39/// The largest payload a packet carries.
40pub const MAX_PAYLOAD: usize = 255;
41
42/// The highest identifier belonging to a multi-spreading-factor LoRa demodulator.
43pub const LORA_MULTI_MODEM_MAX: u8 = 15;
44
45/// The identifier of the single-spreading-factor LoRa demodulator.
46pub const LORA_STD_MODEM: u8 = 16;
47
48/// The identifier of the frequency shift keying demodulator.
49pub const FSK_MODEM: u8 = 17;
50
51/// Which demodulator heard a packet.
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum Modem {
54    /// One of the demodulators that take any spreading factor, by its number.
55    LoraMulti(u8),
56    /// The demodulator fixed to one spreading factor and bandwidth.
57    LoraStandard,
58    /// The frequency shift keying demodulator.
59    Fsk,
60    /// An identifier this build does not know, carried rather than dropped.
61    Unknown(u8),
62}
63
64impl Modem {
65    /// Reads the demodulator identifier the chip wrote.
66    ///
67    /// # Arguments
68    ///
69    /// * `id` - the identifier from the packet metadata.
70    ///
71    /// # Returns
72    ///
73    /// Which demodulator it names.
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// use pamoja_radios::sx1302::rx::Modem;
79    ///
80    /// assert_eq!(Modem::of(3), Modem::LoraMulti(3));
81    /// assert_eq!(Modem::of(16), Modem::LoraStandard);
82    /// assert_eq!(Modem::of(17), Modem::Fsk);
83    /// ```
84    #[must_use]
85    pub const fn of(id: u8) -> Modem {
86        match id {
87            0..=LORA_MULTI_MODEM_MAX => Modem::LoraMulti(id),
88            LORA_STD_MODEM => Modem::LoraStandard,
89            FSK_MODEM => Modem::Fsk,
90            other => Modem::Unknown(other),
91        }
92    }
93
94    /// Whether this demodulator carries LoRa rather than frequency shift keying.
95    ///
96    /// # Returns
97    ///
98    /// Whether the spreading factor and coding rate in the metadata mean anything.
99    #[must_use]
100    pub const fn is_lora(&self) -> bool {
101        matches!(self, Modem::LoraMulti(_) | Modem::LoraStandard)
102    }
103}
104
105/// What the receiver made of a packet it heard.
106#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
107pub struct Quality {
108    /// Whether the payload failed its own check.
109    pub crc_error: bool,
110    /// Whether the preamble did not line up.
111    pub sync_error: bool,
112    /// Whether the header did not check out.
113    pub header_error: bool,
114    /// Whether the timestamp is one the chip stands behind.
115    pub timing_set: bool,
116}
117
118impl Quality {
119    /// Whether the packet arrived intact.
120    ///
121    /// # Returns
122    ///
123    /// Whether none of the three errors is set. A gateway forwards a packet that failed its
124    /// check only when it is told to, so this is what decides.
125    #[must_use]
126    pub const fn is_clean(&self) -> bool {
127        !self.crc_error && !self.sync_error && !self.header_error
128    }
129}
130
131/// A packet as the concentrator wrote it into the buffer.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct Packet<'a> {
134    /// The payload, exactly as it arrived.
135    pub payload: &'a [u8],
136    /// Which of the concentrator channels heard it.
137    pub channel: u8,
138    /// Which demodulator heard it.
139    pub modem: Modem,
140    /// The spreading factor, for a LoRa packet.
141    pub datarate: u8,
142    /// The coding rate, for a LoRa packet.
143    pub coding_rate: u8,
144    /// Whether the sender asked for a payload check at all.
145    pub crc_enabled: bool,
146    /// What the receiver made of it.
147    pub quality: Quality,
148    /// How far off the carrier was, signed, in the units the chip counts.
149    pub frequency_offset: i32,
150    /// The average signal-to-noise ratio, in dB, which is negative for a weak signal.
151    pub snr_average: i8,
152    /// The average channel power, as the chip reports it.
153    pub rssi_channel: u8,
154    /// The average signal power, as the chip reports it.
155    pub rssi_signal: u8,
156    /// The concentrator counter the packet is stamped against.
157    pub timestamp: u32,
158    /// The check the sender put over the payload.
159    pub payload_crc: u16,
160    /// How many fine timestamp metrics follow the packet.
161    pub timing_metrics: u8,
162    /// Whether the chip's own sum over the packet matches what the bus delivered.
163    pub checksum_ok: bool,
164}
165
166/// Why a buffer did not read as a packet.
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum RxError {
169    /// The bytes at this point do not start with the sync word.
170    NotAPacket,
171    /// The buffer ends before the packet it announced does.
172    Truncated {
173        /// How many bytes the packet needs.
174        needs: usize,
175        /// How many the buffer has left.
176        has: usize,
177    },
178}
179
180impl core::fmt::Display for RxError {
181    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
182        match self {
183            RxError::NotAPacket => f.write_str("these bytes do not begin a packet"),
184            RxError::Truncated { needs, has } => write!(
185                f,
186                "the packet needs {needs} bytes and the buffer has {has} left"
187            ),
188        }
189    }
190}
191
192/// How many bytes a packet takes in the buffer.
193///
194/// # Arguments
195///
196/// * `payload_len` - the length the head metadata announced.
197/// * `metrics` - how many fine timestamp metrics the tail announced.
198///
199/// # Returns
200///
201/// The whole span, which is where the next packet starts. The metrics are counted because a
202/// reader that leaves them out finds the next packet in the wrong place.
203///
204/// # Examples
205///
206/// ```
207/// use pamoja_radios::sx1302::rx::packet_len;
208///
209/// // Four bytes of payload and no metrics: nine of head and fourteen of tail around it.
210/// assert_eq!(packet_len(4, 0), 27);
211///
212/// // Each metric adds two bytes after the tail.
213/// assert_eq!(packet_len(4, 3), 33);
214/// ```
215#[must_use]
216pub const fn packet_len(payload_len: usize, metrics: u8) -> usize {
217    HEAD_METADATA + payload_len + TAIL_METADATA + (METRIC_LEN * metrics as usize)
218}
219
220/// Reads the one packet that starts at the front of these bytes.
221///
222/// # Arguments
223///
224/// * `buffer` - the bytes read out of the receive buffer.
225///
226/// # Returns
227///
228/// The packet, and how many bytes of the buffer it took.
229///
230/// # Errors
231///
232/// Returns [`RxError::NotAPacket`] when the sync word is missing, and [`RxError::Truncated`]
233/// when the buffer ends before the packet does.
234///
235/// # Examples
236///
237/// ```
238/// use pamoja_radios::sx1302::rx::{parse, Modem};
239///
240/// // Head metadata, payload, then the tail the chip writes after it.
241/// let mut buffer = vec![0xa5, 0xc0, 4, 2, 0b0111_0001, 3, 0, 0, 0];
242/// buffer.extend_from_slice(b"21.5");
243/// buffer.extend_from_slice(&[0u8; 14]);
244///
245/// let (packet, took) = parse(&buffer).expect("it begins with the sync word");
246/// assert_eq!(packet.payload, b"21.5");
247/// assert_eq!(packet.channel, 2);
248/// assert_eq!(packet.modem, Modem::LoraMulti(3));
249/// assert_eq!(packet.datarate, 7);
250/// assert!(packet.crc_enabled);
251/// assert_eq!(took, buffer.len());
252/// ```
253pub fn parse(buffer: &[u8]) -> Result<(Packet<'_>, usize), RxError> {
254    if buffer.len() < 2 || buffer[0] != SYNC_BYTE_0 || buffer[1] != SYNC_BYTE_1 {
255        return Err(RxError::NotAPacket);
256    }
257    if buffer.len() < HEAD_METADATA {
258        return Err(RxError::Truncated {
259            needs: HEAD_METADATA,
260            has: buffer.len(),
261        });
262    }
263
264    // Everything after the payload is addressed from here, so the length comes first.
265    let payload_len = buffer[2] as usize;
266    let tail_at = payload_len;
267
268    // The metric count lives in the tail, and the packet length depends on it.
269    let counted = tail_at + 21;
270    if buffer.len() <= counted {
271        return Err(RxError::Truncated {
272            needs: counted + 1,
273            has: buffer.len(),
274        });
275    }
276    let metrics = buffer[counted];
277
278    let needs = packet_len(payload_len, metrics);
279    if buffer.len() < needs {
280        return Err(RxError::Truncated {
281            needs,
282            has: buffer.len(),
283        });
284    }
285
286    let head = buffer[4];
287    let status = buffer[tail_at + 9];
288
289    // Twenty bits across three bytes, and the top of that range means a negative offset.
290    let offset = i32::from(buffer[6])
291        | (i32::from(buffer[7]) << 8)
292        | (i32::from(spi::field(buffer[8], 0, 4)) << 16);
293    let frequency_offset = if offset >= (1 << 19) {
294        offset - (1 << 20)
295    } else {
296        offset
297    };
298
299    let timestamp = u32::from(buffer[tail_at + 15])
300        | (u32::from(buffer[tail_at + 16]) << 8)
301        | (u32::from(buffer[tail_at + 17]) << 16)
302        | (u32::from(buffer[tail_at + 18]) << 24);
303
304    let payload_crc = u16::from(buffer[tail_at + 19]) | (u16::from(buffer[tail_at + 20]) << 8);
305
306    // The chip sums every byte of the packet but the last, and writes the sum into it.
307    let written = buffer[needs - 1];
308    let summed = buffer[..needs - 1]
309        .iter()
310        .fold(0u8, |sum, byte| sum.wrapping_add(*byte));
311
312    let packet = Packet {
313        payload: &buffer[HEAD_METADATA..HEAD_METADATA + payload_len],
314        channel: buffer[3],
315        modem: Modem::of(buffer[5]),
316        datarate: spi::field(head, 4, 4),
317        coding_rate: spi::field(head, 1, 3),
318        crc_enabled: spi::field(head, 0, 1) == 1,
319        quality: Quality {
320            crc_error: spi::field(status, 0, 1) == 1,
321            sync_error: spi::field(status, 2, 1) == 1,
322            header_error: spi::field(status, 3, 1) == 1,
323            timing_set: spi::field(status, 4, 1) == 1,
324        },
325        frequency_offset,
326        snr_average: buffer[tail_at + 10] as i8,
327        rssi_channel: buffer[tail_at + 11],
328        rssi_signal: buffer[tail_at + 12],
329        timestamp,
330        payload_crc,
331        timing_metrics: metrics,
332        checksum_ok: written == summed,
333    };
334
335    Ok((packet, needs))
336}
337
338/// Walks every packet the buffer holds, in the order the chip wrote them.
339///
340/// # Arguments
341///
342/// * `buffer` - the bytes read out of the receive buffer.
343///
344/// # Returns
345///
346/// Each packet in turn. The walk stops at the first thing that does not read as one, which is
347/// how the end of the written part of the buffer announces itself.
348///
349/// # Examples
350///
351/// ```
352/// use pamoja_radios::sx1302::rx::packets;
353///
354/// // Two packets back to back, then the unwritten rest of the buffer.
355/// let mut buffer = Vec::new();
356/// for payload in [&b"one"[..], &b"two"[..]] {
357///     buffer.extend_from_slice(&[0xa5, 0xc0, payload.len() as u8, 0, 0, 0, 0, 0, 0]);
358///     buffer.extend_from_slice(payload);
359///     buffer.extend_from_slice(&[0u8; 14]);
360/// }
361/// buffer.extend_from_slice(&[0u8; 32]);
362///
363/// let read: Vec<_> = packets(&buffer).map(|packet| packet.payload.to_vec()).collect();
364/// assert_eq!(read, [b"one".to_vec(), b"two".to_vec()]);
365/// ```
366pub fn packets(buffer: &[u8]) -> impl Iterator<Item = Packet<'_>> {
367    let mut at = 0usize;
368    core::iter::from_fn(move || match parse(buffer.get(at..)?) {
369        Ok((packet, took)) => {
370            at += took;
371            Some(packet)
372        }
373        Err(_) => None,
374    })
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    /// Lays out a packet the way the chip writes one, so a test can read it back.
382    fn written(
383        payload: &[u8],
384        channel: u8,
385        modem: u8,
386        head: u8,
387        status: u8,
388        metrics: u8,
389    ) -> Vec<u8> {
390        let mut buffer = vec![
391            SYNC_BYTE_0,
392            SYNC_BYTE_1,
393            payload.len() as u8,
394            channel,
395            head,
396            modem,
397            0,
398            0,
399            0,
400        ];
401        buffer.extend_from_slice(payload);
402        buffer.extend_from_slice(&[0u8; TAIL_METADATA]);
403        buffer.extend_from_slice(&vec![0u8; METRIC_LEN * metrics as usize]);
404
405        // The tail is addressed from the payload length, not from the packet start.
406        let tail_at = payload.len();
407        buffer[tail_at + 9] = status;
408        buffer[tail_at + 21] = metrics;
409
410        // The chip sums every byte but the last, and writes the sum into it.
411        let last = buffer.len() - 1;
412        let sum = buffer[..last]
413            .iter()
414            .fold(0u8, |sum, byte| sum.wrapping_add(*byte));
415        buffer[last] = sum;
416        buffer
417    }
418
419    #[test]
420    fn a_packet_reads_back_the_way_the_chip_wrote_it() {
421        let buffer = written(b"21.5", 2, 3, 0b0111_0001, 0, 0);
422        let (packet, took) = parse(&buffer).expect("it begins with the sync word");
423
424        assert_eq!(packet.payload, b"21.5");
425        assert_eq!(packet.channel, 2);
426        assert_eq!(packet.modem, Modem::LoraMulti(3));
427        assert_eq!(packet.datarate, 7);
428        assert_eq!(packet.coding_rate, 0);
429        assert!(packet.crc_enabled);
430        assert!(packet.quality.is_clean());
431        assert!(packet.checksum_ok);
432        assert_eq!(took, packet_len(4, 0));
433    }
434
435    #[test]
436    fn the_modem_and_the_frequency_offset_are_different_bytes() {
437        // These two sit next to each other, and reading one for the other is silent.
438        let mut buffer = written(b"x", 0, 7, 0, 0, 0);
439        buffer[6] = 0x34;
440        buffer[7] = 0x12;
441        buffer[8] = 0x00;
442        let last = buffer.len() - 1;
443        buffer[last] = buffer[..last]
444            .iter()
445            .fold(0u8, |sum, byte| sum.wrapping_add(*byte));
446
447        let (packet, _) = parse(&buffer).expect("a packet");
448        assert_eq!(packet.modem, Modem::LoraMulti(7), "the modem is byte five");
449        assert_eq!(
450            packet.frequency_offset, 0x1234,
451            "the offset starts at byte six"
452        );
453    }
454
455    #[test]
456    fn a_carrier_below_the_channel_reads_as_a_negative_offset() {
457        // The offset is twenty bits and signed, so the top of the range wraps.
458        let mut buffer = written(b"x", 0, 0, 0, 0, 0);
459        buffer[6] = 0xff;
460        buffer[7] = 0xff;
461        buffer[8] = 0x0f;
462        let last = buffer.len() - 1;
463        buffer[last] = buffer[..last]
464            .iter()
465            .fold(0u8, |sum, byte| sum.wrapping_add(*byte));
466
467        let (packet, _) = parse(&buffer).expect("a packet");
468        assert_eq!(packet.frequency_offset, -1);
469    }
470
471    #[test]
472    fn the_tail_is_addressed_from_the_payload_length() {
473        // A longer payload moves every tail field, which is what makes this easy to get wrong.
474        for payload in [&b"x"[..], &b"a much longer payload than the first one"[..]] {
475            let mut buffer = written(payload, 0, 0, 0, 0, 0);
476            let tail_at = payload.len();
477            buffer[tail_at + 10] = 0xf6; // ten below zero, as a signed byte
478            buffer[tail_at + 11] = 200;
479            buffer[tail_at + 12] = 180;
480            buffer[tail_at + 15] = 0x78;
481            buffer[tail_at + 16] = 0x56;
482            buffer[tail_at + 17] = 0x34;
483            buffer[tail_at + 18] = 0x12;
484            let last = buffer.len() - 1;
485            buffer[last] = buffer[..last]
486                .iter()
487                .fold(0u8, |sum, byte| sum.wrapping_add(*byte));
488
489            let (packet, _) = parse(&buffer).expect("a packet");
490            assert_eq!(packet.snr_average, -10);
491            assert_eq!(packet.rssi_channel, 200);
492            assert_eq!(packet.rssi_signal, 180);
493            assert_eq!(packet.timestamp, 0x1234_5678);
494        }
495    }
496
497    #[test]
498    fn timestamp_metrics_move_where_the_next_packet_starts() {
499        // A packet carrying metrics is longer than its head and tail suggest. A reader that
500        // misses them lands in the middle of the next packet and finds nothing.
501        let mut buffer = written(b"one", 0, 0, 0, 0, 4);
502        let first_len = buffer.len();
503        assert_eq!(first_len, packet_len(3, 4));
504
505        buffer.extend_from_slice(&written(b"two", 0, 0, 0, 0, 0));
506
507        let read: Vec<_> = packets(&buffer)
508            .map(|packet| packet.payload.to_vec())
509            .collect();
510        assert_eq!(read, [b"one".to_vec(), b"two".to_vec()]);
511
512        let (first, took) = parse(&buffer).expect("a packet");
513        assert_eq!(first.timing_metrics, 4);
514        assert_eq!(took, first_len);
515    }
516
517    #[test]
518    fn what_does_not_begin_with_the_sync_word_is_not_a_packet() {
519        assert_eq!(parse(&[]), Err(RxError::NotAPacket));
520        assert_eq!(parse(&[0u8; 64]), Err(RxError::NotAPacket));
521        assert_eq!(parse(&[0xa5, 0x00, 0, 0]), Err(RxError::NotAPacket));
522    }
523
524    #[test]
525    fn a_packet_cut_short_says_how_much_it_needed() {
526        let buffer = written(b"a longer payload than the buffer holds", 0, 0, 0, 0, 0);
527        let cut = &buffer[..20];
528        let Err(RxError::Truncated { has, .. }) = parse(cut) else {
529            panic!("a cut packet is truncated");
530        };
531        assert_eq!(has, 20);
532
533        // The head alone does not say how long the packet is.
534        assert_eq!(
535            parse(&[SYNC_BYTE_0, SYNC_BYTE_1, 4]),
536            Err(RxError::Truncated {
537                needs: HEAD_METADATA,
538                has: 3
539            })
540        );
541    }
542
543    #[test]
544    fn the_error_flags_are_read_apart() {
545        // Each flag has a bit of its own, so one failure is never read as another.
546        let buffer = written(b"x", 0, 0, 0, 0b0000_0001, 0);
547        let crc = parse(&buffer).expect("a packet").0.quality;
548        assert!(crc.crc_error);
549        assert!(!crc.sync_error);
550        assert!(!crc.is_clean());
551
552        let buffer = written(b"x", 0, 0, 0, 0b0000_0100, 0);
553        let sync = parse(&buffer).expect("a packet").0.quality;
554        assert!(sync.sync_error);
555        assert!(!sync.crc_error);
556
557        let buffer = written(b"x", 0, 0, 0, 0b0000_1000, 0);
558        let header = parse(&buffer).expect("a packet").0.quality;
559        assert!(header.header_error);
560
561        let buffer = written(b"x", 0, 0, 0, 0b0001_0000, 0);
562        let timed = parse(&buffer).expect("a packet").0.quality;
563        assert!(timed.timing_set);
564        assert!(timed.is_clean(), "a timestamp is not a failure");
565    }
566
567    #[test]
568    fn a_packet_the_bus_corrupted_is_told_apart_from_one_the_air_did() {
569        let good = written(b"21.5", 1, 0, 0b0111_0001, 0, 0);
570        let (packet, _) = parse(&good).expect("a packet");
571        assert!(packet.checksum_ok, "the chip and the bus agree");
572        assert!(packet.quality.is_clean(), "and the air was clean");
573
574        // One byte flipped on the way out of the chip: the air was fine, the bus was not.
575        let mut corrupted = good.clone();
576        corrupted[10] ^= 0x01;
577        let (packet, _) = parse(&corrupted).expect("a packet");
578        assert!(!packet.checksum_ok);
579        assert!(
580            packet.quality.is_clean(),
581            "the receiver still reports a clean reception"
582        );
583    }
584
585    #[test]
586    fn the_demodulators_are_told_apart() {
587        for id in 0..=LORA_MULTI_MODEM_MAX {
588            assert_eq!(Modem::of(id), Modem::LoraMulti(id));
589            assert!(Modem::of(id).is_lora());
590        }
591        assert_eq!(Modem::of(LORA_STD_MODEM), Modem::LoraStandard);
592        assert!(Modem::of(LORA_STD_MODEM).is_lora());
593
594        assert_eq!(Modem::of(FSK_MODEM), Modem::Fsk);
595        assert!(!Modem::of(FSK_MODEM).is_lora());
596
597        assert_eq!(Modem::of(200), Modem::Unknown(200));
598        assert!(!Modem::of(200).is_lora());
599    }
600
601    #[test]
602    fn the_buffer_is_walked_until_it_stops_being_packets() {
603        let mut buffer = Vec::new();
604        for payload in [&b"one"[..], &b"two"[..], &b"three"[..]] {
605            buffer.extend_from_slice(&written(payload, 0, 0, 0, 0, 0));
606        }
607        buffer.extend_from_slice(&[0u8; 64]);
608
609        let read: Vec<_> = packets(&buffer)
610            .map(|packet| packet.payload.to_vec())
611            .collect();
612        assert_eq!(read, [b"one".to_vec(), b"two".to_vec(), b"three".to_vec()]);
613
614        assert_eq!(packets(&[]).count(), 0);
615    }
616
617    #[test]
618    fn a_packet_takes_its_payload_plus_its_metadata() {
619        assert_eq!(packet_len(0, 0), HEAD_METADATA + TAIL_METADATA);
620        assert_eq!(packet_len(255, 0), HEAD_METADATA + 255 + TAIL_METADATA);
621        assert_eq!(packet_len(4, 3), HEAD_METADATA + 4 + TAIL_METADATA + 6);
622        assert!(packet_len(MAX_PAYLOAD, 0) < BUFFER_LEN);
623    }
624}