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}