pamoja_radios/sx1302/lbt.rs
1//! Listening before talking, on the SX1261 beside the concentrator.
2//!
3//! Some bands forbid transmitting into a channel somebody else is already using. The
4//! concentrator cannot check for itself while it is busy receiving, so a gateway that needs
5//! this carries a second radio, an SX1261, wired to the same bus behind the concentrator. It
6//! samples the channel, compares what it hears against a threshold, and the concentrator asks
7//! it for permission before a trigger is armed.
8//!
9//! That radio takes commands rather than register writes: an opcode byte, then its payload.
10//! This module builds those frames and reads the answers back. It opens no bus.
11//!
12//! Two conversions here are worth stating plainly, because neither is what the numbers look
13//! like. A threshold in dBm is written as a doubled negative, so -80 dBm goes out as 160. And
14//! a scan time is not a duration the radio counts: it is a number of samples, and only two
15//! durations have one, so anything else is refused rather than rounded.
16
17use super::sx1261::Bandwidth;
18use super::tx::Chain;
19
20/// The command that starts a carrier check.
21pub const OP_LBT_START: u8 = 0x9a;
22
23/// The command that starts a spectral scan.
24pub const OP_SPECTRAL_SCAN: u8 = 0x9b;
25
26/// The command that writes one of the radio's own registers.
27pub const OP_WRITE_REGISTER: u8 = 0x0d;
28
29/// The command that puts the radio in frequency synthesis mode, which ends a check.
30pub const OP_SET_FS: u8 = 0xc1;
31
32pub use super::sx1261::REG_RSSI_WINDOW;
33
34/// How long the radio waits between the samples it takes, as the radio counts it.
35///
36/// Eleven is about 8.2 microseconds. Ten and twelve are the neighboring settings the
37/// reference mentions, at roughly 7.68 and 8.68.
38pub const SAMPLE_INTERVAL: u8 = 11;
39
40/// Which of the radio's pins reports the answer back to the concentrator.
41pub const GPIO_ID: u8 = 1;
42
43/// How many results a spectral scan returns.
44pub const SCAN_RESULTS: usize = 33;
45
46/// How long the radio listens for before it answers.
47///
48/// The radio counts samples rather than time, and only these two durations have a count, so
49/// they are the only ones it takes.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum ScanTime {
52 /// A short check, long enough for the channel to be plainly busy or plainly clear.
53 Short,
54 /// A long check, for a band whose rules ask for one.
55 Long,
56}
57
58impl ScanTime {
59 /// How long this check listens, in microseconds.
60 ///
61 /// # Returns
62 ///
63 /// The duration, which is also how long a caller waits before asking for the answer.
64 ///
65 /// # Examples
66 ///
67 /// ```
68 /// use pamoja_radios::sx1302::lbt::ScanTime;
69 ///
70 /// assert_eq!(ScanTime::Short.micros(), 128);
71 /// assert_eq!(ScanTime::Long.micros(), 5000);
72 /// ```
73 #[must_use]
74 pub const fn micros(&self) -> u16 {
75 match self {
76 ScanTime::Short => 128,
77 ScanTime::Long => 5000,
78 }
79 }
80
81 /// How many samples the radio takes to fill that duration.
82 ///
83 /// # Returns
84 ///
85 /// The count the radio is programmed with. It is not the duration divided by anything a
86 /// caller can work out, so it is carried rather than computed.
87 ///
88 /// # Examples
89 ///
90 /// ```
91 /// use pamoja_radios::sx1302::lbt::ScanTime;
92 ///
93 /// assert_eq!(ScanTime::Short.samples(), 24);
94 /// assert_eq!(ScanTime::Long.samples(), 715);
95 /// ```
96 #[must_use]
97 pub const fn samples(&self) -> u16 {
98 match self {
99 ScanTime::Short => 24,
100 ScanTime::Long => 715,
101 }
102 }
103
104 /// Reads a duration a caller asked for.
105 ///
106 /// # Arguments
107 ///
108 /// * `micros` - the duration in microseconds.
109 ///
110 /// # Returns
111 ///
112 /// The check that takes that long, or `None` for a duration the radio has no count for.
113 /// A near miss is refused rather than rounded, because a check shorter than the rules ask
114 /// for is worse than no check at all.
115 ///
116 /// # Examples
117 ///
118 /// ```
119 /// use pamoja_radios::sx1302::lbt::ScanTime;
120 ///
121 /// assert_eq!(ScanTime::of_micros(128), Some(ScanTime::Short));
122 /// assert_eq!(ScanTime::of_micros(5000), Some(ScanTime::Long));
123 /// assert_eq!(ScanTime::of_micros(4999), None, "a near miss is not rounded");
124 /// ```
125 #[must_use]
126 pub const fn of_micros(micros: u16) -> Option<ScanTime> {
127 match micros {
128 128 => Some(ScanTime::Short),
129 5000 => Some(ScanTime::Long),
130 _ => None,
131 }
132 }
133}
134
135/// Writes a threshold the way the radio takes it.
136///
137/// The radio counts in half decibels below zero, so a threshold in dBm goes out doubled and
138/// with its sign dropped: -80 dBm becomes 160.
139///
140/// # Arguments
141///
142/// * `dbm` - the threshold, which is negative for any real one.
143///
144/// # Returns
145///
146/// The byte the radio is programmed with.
147///
148/// # Examples
149///
150/// ```
151/// use pamoja_radios::sx1302::lbt::threshold_byte;
152///
153/// assert_eq!(threshold_byte(-80), 160);
154/// assert_eq!(threshold_byte(-60), 120);
155/// ```
156#[must_use]
157pub const fn threshold_byte(dbm: i8) -> u8 {
158 (dbm as i16 * -2) as u8
159}
160
161/// Reads a threshold byte back as decibels.
162///
163/// # Arguments
164///
165/// * `byte` - what the radio was programmed with.
166///
167/// # Returns
168///
169/// The threshold in dBm.
170///
171/// # Examples
172///
173/// ```
174/// use pamoja_radios::sx1302::lbt::{threshold_byte, threshold_dbm};
175///
176/// // Every threshold a caller would set survives the round trip.
177/// for dbm in -120..0 {
178/// assert_eq!(threshold_dbm(threshold_byte(dbm)), dbm);
179/// }
180/// ```
181#[must_use]
182pub const fn threshold_dbm(byte: u8) -> i8 {
183 -((byte / 2) as i8)
184}
185
186/// Builds the command that starts a carrier check.
187///
188/// # Arguments
189///
190/// * `scan_time` - how long to listen for.
191/// * `threshold_dbm` - the level above which the channel counts as busy.
192///
193/// # Returns
194///
195/// The opcode and its payload. A caller sends it, waits
196/// [`micros`](ScanTime::micros), and only then asks the concentrator for a trigger.
197///
198/// # Examples
199///
200/// ```
201/// use pamoja_radios::sx1302::lbt::{start, ScanTime, OP_LBT_START};
202///
203/// let (opcode, payload) = start(ScanTime::Short, -80);
204/// assert_eq!(opcode, OP_LBT_START);
205/// assert_eq!(payload, [11, 0, 24, 160, 1]);
206/// ```
207#[must_use]
208pub const fn start(scan_time: ScanTime, threshold_dbm: i8) -> (u8, [u8; 5]) {
209 let samples = scan_time.samples();
210 (
211 OP_LBT_START,
212 [
213 SAMPLE_INTERVAL,
214 (samples >> 8) as u8,
215 (samples & 0xff) as u8,
216 threshold_byte(threshold_dbm),
217 GPIO_ID,
218 ],
219 )
220}
221
222/// Builds the two commands that end a carrier check.
223///
224/// # Returns
225///
226/// The register write that releases the radio, then the command that parks it. Both go out in
227/// this order, and the second takes no payload.
228///
229/// # Examples
230///
231/// ```
232/// use pamoja_radios::sx1302::lbt::{stop, OP_SET_FS, OP_WRITE_REGISTER};
233///
234/// let ((release_op, release), (park_op, park)) = stop();
235/// assert_eq!(release_op, OP_WRITE_REGISTER);
236/// assert_eq!(release, [0x08, 0x9b, 0x00], "clear the register that holds the check");
237/// assert_eq!(park_op, OP_SET_FS);
238/// assert!(park.is_empty());
239/// ```
240#[must_use]
241pub const fn stop() -> ((u8, [u8; 3]), (u8, [u8; 0])) {
242 (
243 (
244 OP_WRITE_REGISTER,
245 [
246 (REG_RSSI_WINDOW >> 8) as u8,
247 (REG_RSSI_WINDOW & 0xff) as u8,
248 0x00,
249 ],
250 ),
251 (OP_SET_FS, []),
252 )
253}
254
255/// Builds the command that starts a spectral scan.
256///
257/// # Arguments
258///
259/// * `scans` - how many sweeps to take.
260///
261/// # Returns
262///
263/// The opcode and its payload.
264///
265/// # Examples
266///
267/// ```
268/// use pamoja_radios::sx1302::lbt::{spectral_scan, OP_SPECTRAL_SCAN};
269///
270/// let (opcode, payload) = spectral_scan(1000);
271/// assert_eq!(opcode, OP_SPECTRAL_SCAN);
272/// assert_eq!(payload, [0x03, 0xe8, 11]);
273/// ```
274#[must_use]
275pub const fn spectral_scan(scans: u16) -> (u8, [u8; 3]) {
276 // The reference fills three bytes here and then asks its bus layer to send nine, which
277 // reads past the end of its own buffer. Three is what it actually fills, and what the
278 // radio is documented to take, so three is what goes out.
279 (
280 OP_SPECTRAL_SCAN,
281 [(scans >> 8) as u8, (scans & 0xff) as u8, SAMPLE_INTERVAL],
282 )
283}
284
285/// How many one-millisecond polls a checked transmission is given to start, and then its
286/// status to clear: half a second each, as Semtech's `lgw_lbt_tx_status` waits.
287pub const OUTCOME_POLLS: u32 = 500;
288
289/// What the host writes to the gain control's first mailbox to clear a transmit status.
290pub const CLEAR_TRANSMIT_STATUS: u8 = 0xff;
291
292/// What it writes once the status reads clear, to acknowledge it.
293pub const ACKNOWLEDGE_CLEARED: u8 = 0x00;
294
295/// How long before a packet leaves the concentrator reads the check's answer, in
296/// microseconds, which a channel's transmit time has to leave room for.
297pub const SENSE_LEAD_US: u32 = 1_500;
298
299/// How far a transmission's carrier may be from a check channel's and still be that channel,
300/// in hertz. The reference allows ten kilohertz, since carriers pass through floating point on
301/// their way from a configuration file.
302pub const FREQUENCY_TOLERANCE_HZ: u32 = 10_000;
303
304/// Whether the gain control reports that a transmission has started on a chain.
305///
306/// # Arguments
307///
308/// * `status` - the gain control's status register.
309/// * `chain` - the chain the packet was armed on.
310///
311/// # Returns
312///
313/// Bit 0 for chain A and bit 1 for chain B.
314///
315/// # Examples
316///
317/// ```
318/// use pamoja_radios::sx1302::lbt::started;
319/// use pamoja_radios::sx1302::tx::Chain;
320///
321/// assert!(started(0b0000_0001, Chain::A));
322/// assert!(!started(0b0000_0001, Chain::B));
323/// ```
324#[must_use]
325pub const fn started(status: u8, chain: Chain) -> bool {
326 status & (1 << chain_bit(chain)) != 0
327}
328
329/// Whether the carrier check kept a started transmission off the air.
330///
331/// # Arguments
332///
333/// * `status` - the gain control's status register, read once the transmission started.
334/// * `chain` - the chain the packet was armed on.
335///
336/// # Returns
337///
338/// Bit 6 for chain A and bit 7 for chain B.
339///
340/// # Examples
341///
342/// ```
343/// use pamoja_radios::sx1302::lbt::refused;
344/// use pamoja_radios::sx1302::tx::Chain;
345///
346/// assert!(refused(0b0100_0001, Chain::A), "started, and held back by the check");
347/// assert!(!refused(0b1000_0001, Chain::A), "bit 7 is chain B's");
348/// ```
349#[must_use]
350pub const fn refused(status: u8, chain: Chain) -> bool {
351 status & (1 << (6 + chain_bit(chain))) != 0
352}
353
354const fn chain_bit(chain: Chain) -> u8 {
355 match chain {
356 Chain::A => 0,
357 Chain::B => 1,
358 }
359}
360
361/// A channel a gateway checks before it transmits on it.
362#[derive(Clone, Copy, Debug, PartialEq, Eq)]
363pub struct Channel {
364 /// The channel's carrier, in hertz.
365 pub frequency_hz: u32,
366 /// Its width.
367 pub bandwidth: Bandwidth,
368 /// How long a check listens.
369 pub scan_time: ScanTime,
370 /// How long one transmission may hold the channel, in milliseconds.
371 pub transmit_time_ms: u16,
372}
373
374impl Channel {
375 /// Whether this is the channel a transmission goes out on.
376 ///
377 /// # Arguments
378 ///
379 /// * `frequency_hz` - the transmission's carrier.
380 /// * `bandwidth_hz` - its bandwidth.
381 ///
382 /// # Returns
383 ///
384 /// Whether the widths match and the carriers are within [`FREQUENCY_TOLERANCE_HZ`].
385 #[must_use]
386 pub const fn covers(&self, frequency_hz: u32, bandwidth_hz: u32) -> bool {
387 let width = match self.bandwidth {
388 Bandwidth::Khz125 => 125_000,
389 Bandwidth::Khz250 => 250_000,
390 };
391 width == bandwidth_hz && self.frequency_hz.abs_diff(frequency_hz) <= FREQUENCY_TOLERANCE_HZ
392 }
393
394 /// Whether a transmission fits the time the channel allows.
395 ///
396 /// The concentrator reads the check's answer [`SENSE_LEAD_US`] before the packet leaves,
397 /// so that much of the transmit time is spent before the packet starts.
398 ///
399 /// # Arguments
400 ///
401 /// * `airtime_us` - how long the transmission holds the air.
402 ///
403 /// # Returns
404 ///
405 /// Whether it ends inside the channel's transmit time.
406 ///
407 /// # Examples
408 ///
409 /// ```
410 /// use pamoja_radios::sx1302::lbt::{Channel, ScanTime};
411 /// use pamoja_radios::sx1302::sx1261::Bandwidth;
412 ///
413 /// let channel = Channel {
414 /// frequency_hz: 922_100_000,
415 /// bandwidth: Bandwidth::Khz125,
416 /// scan_time: ScanTime::Long,
417 /// transmit_time_ms: 400,
418 /// };
419 /// assert!(channel.fits(398_500));
420 /// assert!(!channel.fits(398_501));
421 /// ```
422 #[must_use]
423 pub const fn fits(&self, airtime_us: u64) -> bool {
424 let allowed = (self.transmit_time_ms as u64 * 1_000).saturating_sub(SENSE_LEAD_US as u64);
425 airtime_us <= allowed
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn only_two_durations_have_a_sample_count() {
435 assert_eq!(ScanTime::of_micros(128), Some(ScanTime::Short));
436 assert_eq!(ScanTime::of_micros(5000), Some(ScanTime::Long));
437
438 // Anything else is refused. A check shorter than the rules ask for would pass while
439 // meaning nothing, which is worse than refusing to run one.
440 for micros in [0, 127, 129, 1000, 4999, 5001, u16::MAX] {
441 assert_eq!(ScanTime::of_micros(micros), None, "{micros}");
442 }
443 }
444
445 #[test]
446 fn a_duration_and_its_count_belong_together() {
447 assert_eq!(ScanTime::Short.micros(), 128);
448 assert_eq!(ScanTime::Short.samples(), 24);
449 assert_eq!(ScanTime::Long.micros(), 5000);
450 assert_eq!(ScanTime::Long.samples(), 715);
451
452 // Every duration reads back as the check it names.
453 for scan in [ScanTime::Short, ScanTime::Long] {
454 assert_eq!(ScanTime::of_micros(scan.micros()), Some(scan));
455 }
456 }
457
458 #[test]
459 fn a_threshold_goes_out_doubled_and_positive() {
460 // The radio counts half decibels below zero, so the sign is dropped on the way out.
461 assert_eq!(threshold_byte(-80), 160);
462 assert_eq!(threshold_byte(-60), 120);
463 assert_eq!(threshold_byte(-1), 2);
464 assert_eq!(threshold_byte(0), 0);
465
466 // And every threshold a caller would set survives the round trip.
467 for dbm in -127..=0 {
468 assert_eq!(threshold_dbm(threshold_byte(dbm)), dbm, "{dbm} dBm");
469 }
470 }
471
472 #[test]
473 fn the_start_command_carries_the_count_not_the_duration() {
474 let (opcode, payload) = start(ScanTime::Short, -80);
475 assert_eq!(opcode, OP_LBT_START);
476 assert_eq!(payload, [SAMPLE_INTERVAL, 0, 24, 160, GPIO_ID]);
477
478 // The long check needs two bytes for its count, which is why it is not one byte wide.
479 let (_, payload) = start(ScanTime::Long, -80);
480 assert_eq!(payload[1], (715 >> 8) as u8);
481 assert_eq!(payload[2], (715 & 0xff) as u8);
482 assert_eq!(u16::from_be_bytes([payload[1], payload[2]]), 715);
483 }
484
485 #[test]
486 fn the_threshold_reaches_the_command_unchanged() {
487 for dbm in [-40i8, -70, -80, -90, -110] {
488 let (_, payload) = start(ScanTime::Long, dbm);
489 assert_eq!(payload[3], threshold_byte(dbm));
490 assert_eq!(threshold_dbm(payload[3]), dbm);
491 }
492 }
493
494 #[test]
495 fn stopping_clears_the_register_then_parks_the_radio() {
496 let ((release_op, release), (park_op, park)) = stop();
497
498 // The register that holds the check is cleared by address, most significant first.
499 assert_eq!(release_op, OP_WRITE_REGISTER);
500 assert_eq!(release[0], (REG_RSSI_WINDOW >> 8) as u8);
501 assert_eq!(release[1], (REG_RSSI_WINDOW & 0xff) as u8);
502 assert_eq!(release[2], 0x00, "cleared, not set");
503 assert_eq!(
504 u16::from_be_bytes([release[0], release[1]]),
505 REG_RSSI_WINDOW
506 );
507
508 // Parking takes no payload at all.
509 assert_eq!(park_op, OP_SET_FS);
510 assert!(park.is_empty());
511 }
512
513 #[test]
514 fn a_spectral_scan_carries_its_sweep_count() {
515 let (opcode, payload) = spectral_scan(1000);
516 assert_eq!(opcode, OP_SPECTRAL_SCAN);
517 assert_eq!(u16::from_be_bytes([payload[0], payload[1]]), 1000);
518 assert_eq!(payload[2], SAMPLE_INTERVAL);
519
520 // The count spans both bytes, so a sweep past 255 is not truncated.
521 let (_, payload) = spectral_scan(u16::MAX);
522 assert_eq!(u16::from_be_bytes([payload[0], payload[1]]), u16::MAX);
523 }
524
525 #[test]
526 fn the_commands_do_not_collide() {
527 // Four opcodes, all different, none of them zero.
528 let opcodes = [OP_LBT_START, OP_SPECTRAL_SCAN, OP_WRITE_REGISTER, OP_SET_FS];
529 for (at, opcode) in opcodes.iter().enumerate() {
530 assert_ne!(*opcode, 0);
531 for other in &opcodes[at + 1..] {
532 assert_ne!(opcode, other);
533 }
534 }
535 }
536}