Skip to main content

pamoja_radios/sx127x/
status.rs

1//! What an SX127x reports back in LoRa mode: the signal levels of a packet and the modem's
2//! state.
3//!
4//! The levels follow the RSSI and SNR in LoRa Mode section of the SX1276/77/78/79
5//! datasheet (Rev 7) and its descriptions of RegPktSnrValue, RegPktRssiValue, and
6//! RegRssiValue. The chip reports power in whole decibels above an offset that depends on
7//! which of its two RF ports is in use, and SNR in quarters of a decibel, so every level
8//! comes out as a [`Decibels`] with nothing rounded away.
9
10use pamoja_lora::budget::Decibels;
11
12/// The frequency, in hertz, above which a radio uses its high frequency port. It is the
13/// threshold Semtech's LoRaMac-node applies to the SX1276's RSSI offsets and errata.
14pub const MID_BAND_HZ: u32 = 525_000_000;
15
16/// The RF port a frequency is received on, which sets the RSSI offset.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
18pub enum Port {
19    /// The high frequency port, RFI_HF, for bands above [`MID_BAND_HZ`] such as 868 and
20    /// 915 MHz.
21    High,
22    /// The low frequency port, RFI_LF, for 433 and 169 MHz.
23    Low,
24}
25
26impl Port {
27    /// Returns the port that receives a frequency.
28    ///
29    /// # Arguments
30    ///
31    /// * `frequency_hz` - the carrier frequency in hertz.
32    ///
33    /// # Returns
34    ///
35    /// [`Port::High`] above [`MID_BAND_HZ`], else [`Port::Low`].
36    pub const fn for_frequency(frequency_hz: u32) -> Port {
37        if frequency_hz > MID_BAND_HZ {
38            Port::High
39        } else {
40            Port::Low
41        }
42    }
43
44    /// Returns the constant the RSSI registers are added to.
45    ///
46    /// # Returns
47    ///
48    /// -157 dBm on the high frequency port and -164 dBm on the low one.
49    pub const fn rssi_offset_dbm(self) -> i32 {
50        match self {
51            Port::High => -157,
52            Port::Low => -164,
53        }
54    }
55}
56
57/// Decodes RegRssiValue, the signal power the receiver hears right now.
58///
59/// # Arguments
60///
61/// * `byte` - the RegRssiValue byte.
62/// * `port` - the port the radio listens on.
63///
64/// # Returns
65///
66/// The RSSI in dBm: the port's offset plus the register value.
67///
68/// # Examples
69///
70/// ```
71/// use pamoja_radios::sx127x::status::{rssi_dbm, Port};
72///
73/// assert_eq!(rssi_dbm(0x30, Port::High).to_string(), "-109.00");
74/// assert_eq!(rssi_dbm(0x30, Port::Low).to_string(), "-116.00");
75/// ```
76pub const fn rssi_dbm(byte: u8, port: Port) -> Decibels {
77    Decibels::from_hundredths((port.rssi_offset_dbm() + byte as i32) * 100)
78}
79
80/// The signal levels of the last LoRa packet received.
81///
82/// # Examples
83///
84/// ```
85/// use pamoja_radios::sx127x::status::{PacketStatus, Port};
86///
87/// // RegPktSnrValue 0xF6 and RegPktRssiValue 0x30, heard at 868.1 MHz.
88/// let packet = PacketStatus::from_bytes([0xF6, 0x30], Port::High);
89/// assert_eq!(packet.snr_db.to_string(), "-2.50");
90/// assert_eq!(packet.rssi_dbm.to_string(), "-109.00");
91/// assert_eq!(packet.signal_rssi_dbm.to_string(), "-111.50");
92/// ```
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94pub struct PacketStatus {
95    /// The RSSI averaged over the packet, in dBm: the port's offset plus PacketRssi.
96    pub rssi_dbm: Decibels,
97    /// The estimated signal-to-noise ratio, in dB: PacketSnr, a two's complement byte,
98    /// divided by 4.
99    pub snr_db: Decibels,
100    /// The strength of the packet itself, in dBm: the RSSI, lowered by the SNR when the
101    /// packet arrived below the noise floor, as the datasheet computes it.
102    pub signal_rssi_dbm: Decibels,
103}
104
105impl PacketStatus {
106    /// Decodes RegPktSnrValue and RegPktRssiValue.
107    ///
108    /// # Arguments
109    ///
110    /// * `bytes` - PacketSnr and PacketRssi, the registers at 0x19 and 0x1A in that order.
111    /// * `port` - the port the packet was received on.
112    ///
113    /// # Returns
114    ///
115    /// The three levels, exact to a hundredth of a decibel.
116    pub const fn from_bytes(bytes: [u8; 2], port: Port) -> PacketStatus {
117        let snr_quarters = bytes[0] as i8 as i32;
118        let rssi = (port.rssi_offset_dbm() + bytes[1] as i32) * 100;
119        let signal = if snr_quarters < 0 {
120            rssi + snr_quarters * 25
121        } else {
122            rssi
123        };
124        PacketStatus {
125            rssi_dbm: Decibels::from_hundredths(rssi),
126            snr_db: Decibels::from_hundredths(snr_quarters * 25),
127            signal_rssi_dbm: Decibels::from_hundredths(signal),
128        }
129    }
130}
131
132/// The live state of the LoRa modem, from RegModemStat.
133///
134/// # Examples
135///
136/// ```
137/// use pamoja_radios::sx127x::status::ModemStatus;
138///
139/// // 0x0F: a signal detected and synchronized, a reception under way with a valid header.
140/// let modem = ModemStatus::from_byte(0x0F);
141/// assert!(modem.signal_detected && modem.header_valid && modem.rx_ongoing);
142/// assert!(!modem.clear);
143/// ```
144#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
145pub struct ModemStatus {
146    /// The coding rate the last header announced, as the denominator of 4/5 to 4/8, or
147    /// `None` for a reserved value.
148    pub coding_rate_denominator: Option<u8>,
149    /// Bit 4: the modem is clear.
150    pub clear: bool,
151    /// Bit 3: the header of the packet under way is valid.
152    pub header_valid: bool,
153    /// Bit 2: a reception is under way.
154    pub rx_ongoing: bool,
155    /// Bit 1: the modem has synchronized on the end of the preamble.
156    pub signal_synchronized: bool,
157    /// Bit 0: a LoRa preamble has been detected.
158    pub signal_detected: bool,
159}
160
161impl ModemStatus {
162    /// Decodes RegModemStat.
163    ///
164    /// # Arguments
165    ///
166    /// * `byte` - the register value.
167    ///
168    /// # Returns
169    ///
170    /// The modem's state.
171    pub const fn from_byte(byte: u8) -> ModemStatus {
172        let coding_rate = byte >> 5;
173        ModemStatus {
174            coding_rate_denominator: if coding_rate >= 1 && coding_rate <= 4 {
175                Some(coding_rate + 4)
176            } else {
177                None
178            },
179            clear: byte & 0x10 != 0,
180            header_valid: byte & 0x08 != 0,
181            rx_ongoing: byte & 0x04 != 0,
182            signal_synchronized: byte & 0x02 != 0,
183            signal_detected: byte & 0x01 != 0,
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn the_port_changes_at_the_mid_band_threshold() {
194        assert_eq!(Port::for_frequency(433_175_000), Port::Low);
195        assert_eq!(Port::for_frequency(MID_BAND_HZ), Port::Low);
196        assert_eq!(Port::for_frequency(MID_BAND_HZ + 1), Port::High);
197        assert_eq!(Port::for_frequency(915_000_000), Port::High);
198    }
199
200    #[test]
201    fn a_packet_above_the_noise_floor_is_as_strong_as_its_rssi() {
202        let packet = PacketStatus::from_bytes([0x1C, 0x7D], Port::High);
203        assert_eq!(packet.snr_db, Decibels::from_hundredths(700));
204        assert_eq!(packet.rssi_dbm, Decibels::from_db(-32));
205        assert_eq!(packet.signal_rssi_dbm, packet.rssi_dbm);
206    }
207
208    #[test]
209    fn a_packet_below_the_noise_floor_is_weaker_than_its_rssi_by_its_snr() {
210        let packet = PacketStatus::from_bytes([0x80, 0x20], Port::Low);
211        assert_eq!(packet.snr_db, Decibels::from_db(-32));
212        assert_eq!(packet.rssi_dbm, Decibels::from_db(-132));
213        assert_eq!(packet.signal_rssi_dbm, Decibels::from_db(-164));
214    }
215
216    #[test]
217    fn the_modem_status_reads_its_coding_rate_from_the_top_bits() {
218        assert_eq!(
219            ModemStatus::from_byte(0x30).coding_rate_denominator,
220            Some(5)
221        );
222        assert_eq!(
223            ModemStatus::from_byte(0x90).coding_rate_denominator,
224            Some(8)
225        );
226        assert_eq!(ModemStatus::from_byte(0x10).coding_rate_denominator, None);
227        assert!(ModemStatus::from_byte(0x10).clear);
228    }
229}