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}