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/// // RegRssiValue reads 48: -109 dBm on the high band's port, whose offset is -157, and
74/// // -116 dBm on the low band's, whose offset is -164.
75/// assert_eq!(rssi_dbm(48, Port::High).to_string(), "-109.00");
76/// assert_eq!(rssi_dbm(48, Port::Low).to_string(), "-116.00");
77/// ```
78pub const fn rssi_dbm(byte: u8, port: Port) -> Decibels {
79    Decibels::from_hundredths((port.rssi_offset_dbm() + byte as i32) * 100)
80}
81
82/// The signal levels of the last LoRa packet received.
83///
84/// # Examples
85///
86/// ```
87/// use pamoja_radios::sx127x::status::{PacketStatus, Port};
88///
89/// // RegPktSnrValue 0xF6 and RegPktRssiValue 0x30, heard at 868.1 MHz.
90/// let packet = PacketStatus::from_bytes([0xF6, 0x30], Port::High);
91/// assert_eq!(packet.snr_db.to_string(), "-2.50");
92/// assert_eq!(packet.rssi_dbm.to_string(), "-109.00");
93/// assert_eq!(packet.signal_rssi_dbm.to_string(), "-111.50");
94/// ```
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
96pub struct PacketStatus {
97    /// The RSSI averaged over the packet, in dBm: the port's offset plus PacketRssi.
98    pub rssi_dbm: Decibels,
99    /// The estimated signal-to-noise ratio, in dB: PacketSnr, a two's complement byte,
100    /// divided by 4.
101    pub snr_db: Decibels,
102    /// The strength of the packet itself, in dBm: the RSSI, lowered by the SNR when the
103    /// packet arrived below the noise floor, as the datasheet computes it.
104    pub signal_rssi_dbm: Decibels,
105}
106
107impl PacketStatus {
108    /// Decodes RegPktSnrValue and RegPktRssiValue.
109    ///
110    /// # Arguments
111    ///
112    /// * `bytes` - PacketSnr and PacketRssi, the registers at 0x19 and 0x1A in that order.
113    /// * `port` - the port the packet was received on.
114    ///
115    /// # Returns
116    ///
117    /// The three levels, exact to a hundredth of a decibel.
118    pub const fn from_bytes(bytes: [u8; 2], port: Port) -> PacketStatus {
119        let snr_quarters = bytes[0] as i8 as i32;
120        let rssi = (port.rssi_offset_dbm() + bytes[1] as i32) * 100;
121        let signal = if snr_quarters < 0 {
122            rssi + snr_quarters * 25
123        } else {
124            rssi
125        };
126        PacketStatus {
127            rssi_dbm: Decibels::from_hundredths(rssi),
128            snr_db: Decibels::from_hundredths(snr_quarters * 25),
129            signal_rssi_dbm: Decibels::from_hundredths(signal),
130        }
131    }
132}
133
134/// The live state of the LoRa modem, from RegModemStat.
135///
136/// # Examples
137///
138/// ```
139/// use pamoja_radios::sx127x::status::ModemStatus;
140///
141/// // 0x0F: a signal detected and synchronized, a reception under way with a valid header.
142/// let modem = ModemStatus::from_byte(0x0F);
143/// assert!(modem.signal_detected && modem.header_valid && modem.rx_ongoing);
144/// assert!(!modem.clear);
145/// ```
146#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
147pub struct ModemStatus {
148    /// The coding rate the last header announced, as the denominator of 4/5 to 4/8, or
149    /// `None` for a reserved value.
150    pub coding_rate_denominator: Option<u8>,
151    /// Bit 4: the modem is clear.
152    pub clear: bool,
153    /// Bit 3: the header of the packet under way is valid.
154    pub header_valid: bool,
155    /// Bit 2: a reception is under way.
156    pub rx_ongoing: bool,
157    /// Bit 1: the modem has synchronized on the end of the preamble.
158    pub signal_synchronized: bool,
159    /// Bit 0: a LoRa preamble has been detected.
160    pub signal_detected: bool,
161}
162
163impl ModemStatus {
164    /// Decodes RegModemStat.
165    ///
166    /// # Arguments
167    ///
168    /// * `byte` - the register value.
169    ///
170    /// # Returns
171    ///
172    /// The modem's state.
173    pub const fn from_byte(byte: u8) -> ModemStatus {
174        let coding_rate = byte >> 5;
175        ModemStatus {
176            coding_rate_denominator: if coding_rate >= 1 && coding_rate <= 4 {
177                Some(coding_rate + 4)
178            } else {
179                None
180            },
181            clear: byte & 0x10 != 0,
182            header_valid: byte & 0x08 != 0,
183            rx_ongoing: byte & 0x04 != 0,
184            signal_synchronized: byte & 0x02 != 0,
185            signal_detected: byte & 0x01 != 0,
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn the_port_changes_at_the_mid_band_threshold() {
196        assert_eq!(Port::for_frequency(433_175_000), Port::Low);
197        assert_eq!(Port::for_frequency(MID_BAND_HZ), Port::Low);
198        assert_eq!(Port::for_frequency(MID_BAND_HZ + 1), Port::High);
199        assert_eq!(Port::for_frequency(915_000_000), Port::High);
200    }
201
202    #[test]
203    fn a_packet_above_the_noise_floor_is_as_strong_as_its_rssi() {
204        let packet = PacketStatus::from_bytes([0x1C, 0x7D], Port::High);
205        assert_eq!(packet.snr_db, Decibels::from_hundredths(700));
206        assert_eq!(packet.rssi_dbm, Decibels::from_db(-32));
207        assert_eq!(packet.signal_rssi_dbm, packet.rssi_dbm);
208    }
209
210    #[test]
211    fn a_packet_below_the_noise_floor_is_weaker_than_its_rssi_by_its_snr() {
212        let packet = PacketStatus::from_bytes([0x80, 0x20], Port::Low);
213        assert_eq!(packet.snr_db, Decibels::from_db(-32));
214        assert_eq!(packet.rssi_dbm, Decibels::from_db(-132));
215        assert_eq!(packet.signal_rssi_dbm, Decibels::from_db(-164));
216    }
217
218    #[test]
219    fn the_modem_status_reads_its_coding_rate_from_the_top_bits() {
220        assert_eq!(
221            ModemStatus::from_byte(0x30).coding_rate_denominator,
222            Some(5)
223        );
224        assert_eq!(
225            ModemStatus::from_byte(0x90).coding_rate_denominator,
226            Some(8)
227        );
228        assert_eq!(ModemStatus::from_byte(0x10).coding_rate_denominator, None);
229        assert!(ModemStatus::from_byte(0x10).clear);
230    }
231}