pamoja_radios/radio.rs
1//! One LoRa radio of either family, behind one set of calls.
2//!
3//! The SX126x takes commands and the SX127x takes registers, but a program that sends a
4//! reading does not care which: it tunes to a carrier, sends a frame, and listens for one.
5//! [`Radio`] holds either driver and gives both the same calls, configured from one
6//! [`RadioConfig`] of a carrier, a LoRa link, an output power, a sync word, and the IQ
7//! polarity of each direction. A program that reads which module it has from a file keeps
8//! one code path, and one that needs a chip's own features matches on the enum and reaches
9//! its driver.
10//!
11//! # Examples
12//!
13//! An RFM95W behind the shared calls, answering a register read over a scripted bus:
14//!
15//! ```
16//! use pamoja_hal::script::{DelayLog, PinScript, SpiScript, SpiStep};
17//! use pamoja_radios::radio::{Family, Radio, RadioError};
18//! use pamoja_radios::sx127x::config::PaOutput;
19//! use pamoja_radios::sx127x::{register, Board, Sx127x};
20//!
21//! // The chip answers a read of RegVersion with the value every SX1276 holds there.
22//! let spi = SpiScript::new([
23//! SpiStep::write([register::VERSION]),
24//! SpiStep::read([register::VERSION_SX1276]),
25//! ]);
26//! let board = Board::new(PaOutput::PaBoost);
27//! let chip = Sx127x::new(spi, PinScript::new([]), DelayLog::new(), board);
28//! let mut radio: Radio<_, PinScript, _, _> = Radio::from(chip);
29//!
30//! assert_eq!(radio.family(), Family::Sx127x);
31//! let version = radio.read_register(register::VERSION.into());
32//! assert_eq!(version, Ok(register::VERSION_SX1276));
33//!
34//! // The SX127x address byte carries seven bits, so a register past 0x7F, such as the
35//! // SX126x sync word, is out of its reach.
36//! const SX126X_SYNC_WORD: u16 = 0x0740;
37//! assert_eq!(
38//! radio.read_register(SX126X_SYNC_WORD),
39//! Err(RadioError::Address(SX126X_SYNC_WORD))
40//! );
41//! ```
42
43use core::fmt;
44
45use embedded_hal::delay::DelayNs;
46use embedded_hal::digital::{InputPin, OutputPin};
47use embedded_hal::spi::SpiDevice;
48use pamoja_lora::budget::Decibels;
49use pamoja_lora::LinkSettings;
50
51use crate::sx126x::{self, Sx126x};
52use crate::sx127x::{self, Sx127x};
53
54/// The last address an SX127x register read or write can reach: the SPI address byte
55/// carries seven bits after the write flag.
56const SX127X_LAST_REGISTER: u16 = 0x7F;
57
58/// Which family a radio's chip belongs to.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
60pub enum Family {
61 /// The SX1261, SX1262, SX1268, and LLCC68, which take commands.
62 Sx126x,
63 /// The SX1276, SX1277, SX1278, and SX1279, which take registers.
64 Sx127x,
65}
66
67/// The LoRa sync word, which keeps one network's frames apart from another's.
68///
69/// Both families carry it as one byte. The SX127x writes the byte to RegSyncWord. The SX126x
70/// spreads its two nibbles over its two sync word registers with a 4 after each, so 0x34 is
71/// written as 0x3444 and 0x12 as 0x1424, which are the chip's own public and private values,
72/// and any other byte takes the same layout, the one RadioLib's `setSyncWord` writes.
73///
74/// # Examples
75///
76/// ```
77/// use pamoja_radios::radio::SyncWord;
78/// use pamoja_radios::sx126x::config::SyncWord as Sx126xSyncWord;
79///
80/// assert_eq!(SyncWord::from_byte(0x34), SyncWord::Public);
81/// assert_eq!(SyncWord::Custom(0x2B).sx126x(), Sx126xSyncWord::Custom(0x24B4));
82/// ```
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
84pub enum SyncWord {
85 /// 0x34, for a public network such as LoRaWAN.
86 Public,
87 /// 0x12, for a private network, and both families' reset value.
88 Private,
89 /// Another byte.
90 Custom(u8),
91}
92
93impl SyncWord {
94 /// Returns the byte.
95 ///
96 /// # Returns
97 ///
98 /// 0x34 for a public network, 0x12 for a private one, or the custom byte.
99 pub const fn to_byte(self) -> u8 {
100 match self {
101 SyncWord::Public => 0x34,
102 SyncWord::Private => 0x12,
103 SyncWord::Custom(byte) => byte,
104 }
105 }
106
107 /// Names a byte.
108 ///
109 /// # Arguments
110 ///
111 /// * `byte` - the sync word byte.
112 ///
113 /// # Returns
114 ///
115 /// [`SyncWord::Public`] for 0x34, [`SyncWord::Private`] for 0x12, and a custom word for
116 /// any other byte.
117 pub const fn from_byte(byte: u8) -> SyncWord {
118 match byte {
119 0x34 => SyncWord::Public,
120 0x12 => SyncWord::Private,
121 other => SyncWord::Custom(other),
122 }
123 }
124
125 /// Returns the word as the SX126x takes it.
126 ///
127 /// # Returns
128 ///
129 /// The two-byte word, each nibble of the byte followed by a 4.
130 pub const fn sx126x(self) -> sx126x::config::SyncWord {
131 match self {
132 SyncWord::Public => sx126x::config::SyncWord::Public,
133 SyncWord::Private => sx126x::config::SyncWord::Private,
134 SyncWord::Custom(byte) => {
135 let high = (byte >> 4) as u16;
136 let low = (byte & 0x0F) as u16;
137 sx126x::config::SyncWord::Custom((high << 12) | 0x0400 | (low << 4) | 0x0004)
138 }
139 }
140 }
141
142 /// Returns the word as the SX127x takes it.
143 ///
144 /// # Returns
145 ///
146 /// The RegSyncWord value.
147 pub const fn sx127x(self) -> sx127x::config::SyncWord {
148 match self {
149 SyncWord::Public => sx127x::config::SyncWord::Public,
150 SyncWord::Private => sx127x::config::SyncWord::Private,
151 SyncWord::Custom(byte) => sx127x::config::SyncWord::Custom(byte),
152 }
153 }
154}
155
156/// What a radio of either family sends and listens with.
157///
158/// The output power is what the amplifier is asked for, in whole dBm. Each driver clamps it
159/// to the range of the amplifier its board names, so a regional ceiling is kept by choosing
160/// the power first, with
161/// [`LinkBudget::max_transmit_power_dbm`](pamoja_lora::budget::LinkBudget::max_transmit_power_dbm)
162/// rounded down.
163///
164/// # Examples
165///
166/// ```
167/// use pamoja_lora::LinkSettings;
168/// use pamoja_radios::radio::{RadioConfig, SyncWord};
169///
170/// // A LoRaWAN device on 868.1 MHz at SF9 with 14 dBm, calibrated for the whole band.
171/// let config = RadioConfig::new(868_100_000, LinkSettings::new(9, 125_000), 14)
172/// .with_band(863_000_000, 870_000_000)
173/// .lorawan_device();
174/// assert_eq!(config.sync_word, SyncWord::Public);
175/// assert!(config.invert_iq_receive && !config.invert_iq_transmit);
176/// ```
177#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178pub struct RadioConfig {
179 /// The carrier frequency in hertz.
180 pub frequency_hz: u32,
181 /// The band an SX126x calibrates its receiver image for, as its lower and upper edges in
182 /// hertz. The SX127x calibrates at the carrier instead, once per RF port.
183 pub band_hz: (u32, u32),
184 /// The spreading factor, bandwidth, coding rate, preamble, header, and CRC.
185 pub link: LinkSettings,
186 /// The output power asked of the amplifier, in dBm.
187 pub output_dbm: i8,
188 /// The sync word.
189 pub sync_word: SyncWord,
190 /// Whether frames go out with inverted IQ, as a LoRaWAN gateway sends downlinks.
191 pub invert_iq_transmit: bool,
192 /// Whether the receiver expects inverted IQ, as a LoRaWAN device hears downlinks.
193 pub invert_iq_receive: bool,
194}
195
196impl RadioConfig {
197 /// Builds a configuration with a private sync word and standard IQ both ways.
198 ///
199 /// # Arguments
200 ///
201 /// * `frequency_hz` - the carrier frequency in hertz.
202 /// * `link` - the LoRa link settings.
203 /// * `output_dbm` - the output power asked of the amplifier, in dBm.
204 ///
205 /// # Returns
206 ///
207 /// The configuration, with the SX126x calibration band covering just the carrier.
208 pub const fn new(frequency_hz: u32, link: LinkSettings, output_dbm: i8) -> RadioConfig {
209 RadioConfig {
210 frequency_hz,
211 band_hz: (frequency_hz, frequency_hz),
212 link,
213 output_dbm,
214 sync_word: SyncWord::Private,
215 invert_iq_transmit: false,
216 invert_iq_receive: false,
217 }
218 }
219
220 /// Returns the configuration with an SX126x calibrating for a whole band, so moving
221 /// between channels inside it needs no new calibration.
222 ///
223 /// # Arguments
224 ///
225 /// * `low_hz` - the lower edge of the band in hertz.
226 /// * `high_hz` - the upper edge of the band in hertz.
227 ///
228 /// # Returns
229 ///
230 /// The configuration.
231 pub const fn with_band(mut self, low_hz: u32, high_hz: u32) -> RadioConfig {
232 self.band_hz = (low_hz, high_hz);
233 self
234 }
235
236 /// Returns the configuration with another sync word.
237 ///
238 /// # Arguments
239 ///
240 /// * `sync_word` - the sync word.
241 ///
242 /// # Returns
243 ///
244 /// The configuration.
245 pub const fn with_sync_word(mut self, sync_word: SyncWord) -> RadioConfig {
246 self.sync_word = sync_word;
247 self
248 }
249
250 /// Returns the configuration with the IQ polarity set for each direction.
251 ///
252 /// # Arguments
253 ///
254 /// * `transmit` - `true` to send with inverted IQ.
255 /// * `receive` - `true` to listen for inverted IQ.
256 ///
257 /// # Returns
258 ///
259 /// The configuration.
260 pub const fn with_inverted_iq(mut self, transmit: bool, receive: bool) -> RadioConfig {
261 self.invert_iq_transmit = transmit;
262 self.invert_iq_receive = receive;
263 self
264 }
265
266 /// Returns the configuration a LoRaWAN end device uses: the public sync word, uplinks
267 /// with standard IQ, and downlinks heard with inverted IQ.
268 ///
269 /// # Returns
270 ///
271 /// The configuration.
272 pub const fn lorawan_device(self) -> RadioConfig {
273 self.with_sync_word(SyncWord::Public)
274 .with_inverted_iq(false, true)
275 }
276}
277
278/// The signal levels a frame arrived with.
279#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
280pub struct SignalLevels {
281 /// The received signal strength averaged over the frame, in dBm.
282 pub rssi_dbm: Decibels,
283 /// The estimated signal-to-noise ratio, in dB, negative below the noise floor.
284 pub snr_db: Decibels,
285 /// The estimated strength of the LoRa signal itself, in dBm.
286 pub signal_rssi_dbm: Decibels,
287}
288
289impl From<sx126x::status::PacketStatus> for SignalLevels {
290 fn from(status: sx126x::status::PacketStatus) -> SignalLevels {
291 SignalLevels {
292 rssi_dbm: status.rssi_dbm,
293 snr_db: status.snr_db,
294 signal_rssi_dbm: status.signal_rssi_dbm,
295 }
296 }
297}
298
299impl From<sx127x::status::PacketStatus> for SignalLevels {
300 fn from(status: sx127x::status::PacketStatus) -> SignalLevels {
301 SignalLevels {
302 rssi_dbm: status.rssi_dbm,
303 snr_db: status.snr_db,
304 signal_rssi_dbm: status.signal_rssi_dbm,
305 }
306 }
307}
308
309/// How a reception ended.
310#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
311pub enum Reception {
312 /// A frame that checked.
313 Frame {
314 /// The payload length; the payload is at the start of the buffer.
315 len: usize,
316 /// The signal levels the frame arrived with.
317 levels: SignalLevels,
318 },
319 /// No frame arrived before the timeout.
320 Timeout,
321 /// A frame arrived whose header or CRC failed its check, and was dropped.
322 Corrupt,
323}
324
325impl From<sx126x::Reception> for Reception {
326 fn from(reception: sx126x::Reception) -> Reception {
327 match reception {
328 sx126x::Reception::Frame { len, status } => Reception::Frame {
329 len,
330 levels: status.into(),
331 },
332 sx126x::Reception::Timeout => Reception::Timeout,
333 sx126x::Reception::Corrupt => Reception::Corrupt,
334 }
335 }
336}
337
338impl From<sx127x::Reception> for Reception {
339 fn from(reception: sx127x::Reception) -> Reception {
340 match reception {
341 sx127x::Reception::Frame { len, status } => Reception::Frame {
342 len,
343 levels: status.into(),
344 },
345 sx127x::Reception::Timeout => Reception::Timeout,
346 sx127x::Reception::Corrupt => Reception::Corrupt,
347 }
348 }
349}
350
351/// What can go wrong driving a radio of either family.
352#[derive(Clone, Copy, Debug, PartialEq, Eq)]
353pub enum RadioError<E> {
354 /// The SX126x driver failed.
355 Sx126x(sx126x::RadioError<E>),
356 /// The SX127x driver failed.
357 Sx127x(sx127x::RadioError<E>),
358 /// A register address past the 0x7F an SX127x register map ends at.
359 Address(u16),
360}
361
362impl<E: fmt::Debug> fmt::Display for RadioError<E> {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 match self {
365 RadioError::Sx126x(error) => error.fmt(f),
366 RadioError::Sx127x(error) => error.fmt(f),
367 RadioError::Address(address) => write!(
368 f,
369 "the SX127x has no register at {address:#06x}; its map ends at 0x7F"
370 ),
371 }
372 }
373}
374
375impl<E: fmt::Debug> core::error::Error for RadioError<E> {}
376
377/// A LoRa radio of either family: an SX126x with its BUSY line, or an SX127x.
378///
379/// Build one from a driver with [`From`], or open one on a Linux board through
380/// [`linux`](crate::linux) with the `linux` feature. [`init`](Radio::init) resets the chip,
381/// [`configure`](Radio::configure) tunes it, and the calls after that are the same for both
382/// families. The SX127x has no BUSY line, so its radio names a BUSY type it never uses.
383pub enum Radio<SPI, BUSY, RESET, D> {
384 /// An SX1261, SX1262, SX1268, or LLCC68.
385 Sx126x(Sx126x<SPI, BUSY, RESET, D>),
386 /// An SX1276, SX1277, SX1278, or SX1279.
387 Sx127x(Sx127x<SPI, RESET, D>),
388}
389
390impl<SPI, BUSY, RESET, D> From<Sx126x<SPI, BUSY, RESET, D>> for Radio<SPI, BUSY, RESET, D> {
391 fn from(radio: Sx126x<SPI, BUSY, RESET, D>) -> Self {
392 Radio::Sx126x(radio)
393 }
394}
395
396impl<SPI, BUSY, RESET, D> From<Sx127x<SPI, RESET, D>> for Radio<SPI, BUSY, RESET, D> {
397 fn from(radio: Sx127x<SPI, RESET, D>) -> Self {
398 Radio::Sx127x(radio)
399 }
400}
401
402impl<SPI, BUSY, RESET, D> Radio<SPI, BUSY, RESET, D> {
403 /// Returns the family of the chip.
404 ///
405 /// # Returns
406 ///
407 /// [`Family::Sx126x`] or [`Family::Sx127x`].
408 pub fn family(&self) -> Family {
409 match self {
410 Radio::Sx126x(_) => Family::Sx126x,
411 Radio::Sx127x(_) => Family::Sx127x,
412 }
413 }
414
415 /// Returns the link settings the radio was last configured with.
416 ///
417 /// # Returns
418 ///
419 /// The settings, or `None` before [`configure`](Radio::configure) and after an SX126x
420 /// has slept.
421 pub fn link(&self) -> Option<LinkSettings> {
422 match self {
423 Radio::Sx126x(radio) => radio.config().map(|config| config.link),
424 Radio::Sx127x(radio) => radio.config().map(|config| config.link),
425 }
426 }
427}
428
429impl<SPI, BUSY, RESET, D> Radio<SPI, BUSY, RESET, D>
430where
431 SPI: SpiDevice,
432 BUSY: InputPin,
433 RESET: OutputPin,
434 D: DelayNs,
435{
436 /// Resets the chip and sets up what its board wires around it.
437 ///
438 /// # Errors
439 ///
440 /// Returns the errors of [`Sx126x::init`] or [`Sx127x::init`]: most often that no chip
441 /// of the family answered, which is a wiring or a power problem.
442 pub fn init(&mut self) -> Result<(), RadioError<SPI::Error>> {
443 match self {
444 Radio::Sx126x(radio) => radio.init().map_err(RadioError::Sx126x),
445 Radio::Sx127x(radio) => radio.init().map_err(RadioError::Sx127x),
446 }
447 }
448
449 /// Tunes the chip to a configuration.
450 ///
451 /// The output power becomes the settings of the amplifier the board names, clamped to
452 /// its range, and the sync word takes the family's own layout.
453 ///
454 /// # Arguments
455 ///
456 /// * `config` - the configuration.
457 ///
458 /// # Errors
459 ///
460 /// Returns the errors of [`Sx126x::configure`] or [`Sx127x::configure`], such as a
461 /// bandwidth the chip does not have or a link an LLCC68 cannot carry.
462 pub fn configure(&mut self, config: RadioConfig) -> Result<(), RadioError<SPI::Error>> {
463 match self {
464 Radio::Sx126x(radio) => {
465 let power = radio.tx_power(config.output_dbm);
466 let (low_hz, high_hz) = config.band_hz;
467 let settings = sx126x::RadioConfig::new(config.frequency_hz, config.link, power)
468 .with_band(low_hz, high_hz)
469 .with_sync_word(config.sync_word.sx126x())
470 .with_inverted_iq(config.invert_iq_transmit, config.invert_iq_receive);
471 radio.configure(settings).map_err(RadioError::Sx126x)
472 }
473 Radio::Sx127x(radio) => {
474 let power = radio.tx_power(config.output_dbm);
475 let settings = sx127x::RadioConfig::new(config.frequency_hz, config.link, power)
476 .with_sync_word(config.sync_word.sx127x())
477 .with_inverted_iq(config.invert_iq_transmit, config.invert_iq_receive);
478 radio.configure(settings).map_err(RadioError::Sx127x)
479 }
480 }
481 }
482
483 /// Sends one frame and waits for it to leave.
484 ///
485 /// # Arguments
486 ///
487 /// * `payload` - the frame's payload, 1 to 255 bytes.
488 ///
489 /// # Returns
490 ///
491 /// The frame's airtime in microseconds, for a [`DutyCycle`](crate::duty::DutyCycle) to
492 /// count.
493 ///
494 /// # Errors
495 ///
496 /// Returns the errors of [`Sx126x::transmit`] or [`Sx127x::transmit`].
497 pub fn transmit(&mut self, payload: &[u8]) -> Result<u64, RadioError<SPI::Error>> {
498 match self {
499 Radio::Sx126x(radio) => radio.transmit(payload).map_err(RadioError::Sx126x),
500 Radio::Sx127x(radio) => radio.transmit(payload).map_err(RadioError::Sx127x),
501 }
502 }
503
504 /// Starts sending one frame and returns once the chip is transmitting.
505 ///
506 /// # Arguments
507 ///
508 /// * `payload` - the frame's payload, 1 to 255 bytes.
509 ///
510 /// # Returns
511 ///
512 /// The frame's airtime in microseconds, after which
513 /// [`finish_transmit`](Radio::finish_transmit) reports it sent.
514 ///
515 /// # Errors
516 ///
517 /// Returns the errors of [`Sx126x::start_transmit`] or [`Sx127x::start_transmit`].
518 pub fn start_transmit(&mut self, payload: &[u8]) -> Result<u64, RadioError<SPI::Error>> {
519 match self {
520 Radio::Sx126x(radio) => radio.start_transmit(payload).map_err(RadioError::Sx126x),
521 Radio::Sx127x(radio) => radio.start_transmit(payload).map_err(RadioError::Sx127x),
522 }
523 }
524
525 /// Reports whether the frame [`start_transmit`](Radio::start_transmit) began has left.
526 ///
527 /// # Returns
528 ///
529 /// `true` once it has been sent.
530 ///
531 /// # Errors
532 ///
533 /// Returns the errors of [`Sx126x::finish_transmit`] or [`Sx127x::finish_transmit`].
534 pub fn finish_transmit(&mut self) -> Result<bool, RadioError<SPI::Error>> {
535 match self {
536 Radio::Sx126x(radio) => radio.finish_transmit().map_err(RadioError::Sx126x),
537 Radio::Sx127x(radio) => radio.finish_transmit().map_err(RadioError::Sx127x),
538 }
539 }
540
541 /// Listens for one frame.
542 ///
543 /// # Arguments
544 ///
545 /// * `buffer` - where the payload goes; up to 255 bytes are accepted.
546 /// * `timeout_us` - how long to listen for a frame to start, in microseconds. The SX127x
547 /// counts it in symbols, from 4 to 1023.
548 ///
549 /// # Returns
550 ///
551 /// The frame's length and signal levels, or that the timeout passed or the frame was
552 /// corrupt.
553 ///
554 /// # Errors
555 ///
556 /// Returns the errors of [`Sx126x::receive`] or [`Sx127x::receive`].
557 pub fn receive(
558 &mut self,
559 buffer: &mut [u8],
560 timeout_us: u64,
561 ) -> Result<Reception, RadioError<SPI::Error>> {
562 match self {
563 Radio::Sx126x(radio) => radio
564 .receive(buffer, timeout_us)
565 .map(Reception::from)
566 .map_err(RadioError::Sx126x),
567 Radio::Sx127x(radio) => radio
568 .receive(buffer, timeout_us)
569 .map(Reception::from)
570 .map_err(RadioError::Sx127x),
571 }
572 }
573
574 /// Listens for a preamble over a few symbols and reports whether one is there.
575 ///
576 /// An SX126x listens over the symbols asked for, with the thresholds its spreading
577 /// factor and bandwidth call for; an SX127x listens over the one symbol its detection
578 /// takes and ignores the count.
579 ///
580 /// # Arguments
581 ///
582 /// * `symbols` - how many symbols to listen over.
583 ///
584 /// # Returns
585 ///
586 /// `true` when the chip detected activity.
587 ///
588 /// # Errors
589 ///
590 /// Returns the errors of [`Sx126x::detect`] or [`Sx127x::detect`].
591 pub fn detect(&mut self, symbols: u8) -> Result<bool, RadioError<SPI::Error>> {
592 match self {
593 Radio::Sx126x(radio) => radio.detect(symbols).map_err(RadioError::Sx126x),
594 Radio::Sx127x(radio) => radio.detect().map_err(RadioError::Sx127x),
595 }
596 }
597
598 /// Starts listening, frame after frame, until another mode is set.
599 ///
600 /// # Errors
601 ///
602 /// Returns the errors of [`Sx126x::listen`] or [`Sx127x::listen`].
603 pub fn listen(&mut self) -> Result<(), RadioError<SPI::Error>> {
604 match self {
605 Radio::Sx126x(radio) => radio.listen().map_err(RadioError::Sx126x),
606 Radio::Sx127x(radio) => radio.listen().map_err(RadioError::Sx127x),
607 }
608 }
609
610 /// Takes the frame a [`listen`](Radio::listen) has received, if one has arrived.
611 ///
612 /// # Arguments
613 ///
614 /// * `buffer` - where the payload goes.
615 ///
616 /// # Returns
617 ///
618 /// The frame, a corrupt frame, or `None` when nothing has arrived.
619 ///
620 /// # Errors
621 ///
622 /// Returns the errors of [`Sx126x::take_frame`] or [`Sx127x::take_frame`].
623 pub fn take_frame(
624 &mut self,
625 buffer: &mut [u8],
626 ) -> Result<Option<Reception>, RadioError<SPI::Error>> {
627 match self {
628 Radio::Sx126x(radio) => radio
629 .take_frame(buffer)
630 .map(|taken| taken.map(Reception::from))
631 .map_err(RadioError::Sx126x),
632 Radio::Sx127x(radio) => radio
633 .take_frame(buffer)
634 .map(|taken| taken.map(Reception::from))
635 .map_err(RadioError::Sx127x),
636 }
637 }
638
639 /// Draws a random number from the noise the receiver hears, leaving the chip in standby.
640 ///
641 /// # Returns
642 ///
643 /// Thirty-two bits of noise.
644 ///
645 /// # Errors
646 ///
647 /// Returns the errors of [`Sx126x::random`] or [`Sx127x::random`].
648 pub fn random(&mut self) -> Result<u32, RadioError<SPI::Error>> {
649 match self {
650 Radio::Sx126x(radio) => radio.random().map_err(RadioError::Sx126x),
651 Radio::Sx127x(radio) => radio.random().map_err(RadioError::Sx127x),
652 }
653 }
654
655 /// Puts the chip in standby, which stops a transmission or a reception.
656 ///
657 /// # Errors
658 ///
659 /// Returns the bus errors of either driver.
660 pub fn standby(&mut self) -> Result<(), RadioError<SPI::Error>> {
661 match self {
662 Radio::Sx126x(radio) => radio.standby().map_err(RadioError::Sx126x),
663 Radio::Sx127x(radio) => radio.standby().map_err(RadioError::Sx127x),
664 }
665 }
666
667 /// Puts the chip to sleep until the next call wakes it.
668 ///
669 /// An SX126x takes a warm start, keeping its settings in retention, and is configured
670 /// again before its next frame. An SX127x keeps its registers and wakes for the next
671 /// frame as it is.
672 ///
673 /// # Errors
674 ///
675 /// Returns the bus errors of either driver.
676 pub fn sleep(&mut self) -> Result<(), RadioError<SPI::Error>> {
677 match self {
678 Radio::Sx126x(radio) => radio.sleep(true).map_err(RadioError::Sx126x),
679 Radio::Sx127x(radio) => radio.sleep().map_err(RadioError::Sx127x),
680 }
681 }
682
683 /// Reads one register.
684 ///
685 /// # Arguments
686 ///
687 /// * `address` - the register address: 16 bits on the SX126x, 0x00 to 0x7F on the
688 /// SX127x.
689 ///
690 /// # Returns
691 ///
692 /// The register's value.
693 ///
694 /// # Errors
695 ///
696 /// Returns [`RadioError::Address`] for an SX127x address past 0x7F, and the bus errors
697 /// of either driver.
698 pub fn read_register(&mut self, address: u16) -> Result<u8, RadioError<SPI::Error>> {
699 match self {
700 Radio::Sx126x(radio) => {
701 let mut value = [0u8; 1];
702 radio
703 .read_register(address, &mut value)
704 .map_err(RadioError::Sx126x)?;
705 Ok(value[0])
706 }
707 Radio::Sx127x(radio) => radio
708 .read_register(sx127x_address(address)?)
709 .map_err(RadioError::Sx127x),
710 }
711 }
712
713 /// Writes one register.
714 ///
715 /// # Arguments
716 ///
717 /// * `address` - the register address: 16 bits on the SX126x, 0x00 to 0x7F on the
718 /// SX127x.
719 /// * `value` - the value to write.
720 ///
721 /// # Errors
722 ///
723 /// Returns [`RadioError::Address`] for an SX127x address past 0x7F, and the bus errors
724 /// of either driver.
725 pub fn write_register(
726 &mut self,
727 address: u16,
728 value: u8,
729 ) -> Result<(), RadioError<SPI::Error>> {
730 match self {
731 Radio::Sx126x(radio) => radio
732 .write_register(address, &[value])
733 .map_err(RadioError::Sx126x),
734 Radio::Sx127x(radio) => radio
735 .write_register(sx127x_address(address)?, value)
736 .map_err(RadioError::Sx127x),
737 }
738 }
739}
740
741fn sx127x_address<E>(address: u16) -> Result<u8, RadioError<E>> {
742 if address > SX127X_LAST_REGISTER {
743 return Err(RadioError::Address(address));
744 }
745 Ok(address as u8)
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751 use pamoja_hal::script::{DelayLog, PinScript, SpiScript, SpiStep};
752
753 type ScriptedRadio = Radio<SpiScript, PinScript, PinScript, DelayLog>;
754
755 fn sx126x_radio(steps: Vec<SpiStep>, board: sx126x::Board) -> ScriptedRadio {
756 let mut busy = PinScript::new([]);
757 busy.set_low().expect("a scripted line takes any level");
758 Radio::from(Sx126x::new(
759 SpiScript::new(steps),
760 busy,
761 PinScript::new([]),
762 DelayLog::new(),
763 board,
764 ))
765 }
766
767 fn sx127x_radio(steps: Vec<SpiStep>) -> ScriptedRadio {
768 Radio::from(Sx127x::new(
769 SpiScript::new(steps),
770 PinScript::new([]),
771 DelayLog::new(),
772 sx127x::Board::new(sx127x::config::PaOutput::PaBoost),
773 ))
774 }
775
776 fn done(radio: ScriptedRadio) -> bool {
777 match radio {
778 Radio::Sx126x(radio) => radio.release().0.done(),
779 Radio::Sx127x(radio) => radio.release().0.done(),
780 }
781 }
782
783 fn wrote(address: u8, values: &[u8]) -> [SpiStep; 2] {
784 [
785 SpiStep::write([address | 0x80]),
786 SpiStep::write(values.to_vec()),
787 ]
788 }
789
790 fn read(address: u8, values: &[u8]) -> [SpiStep; 2] {
791 [SpiStep::write([address]), SpiStep::read(values.to_vec())]
792 }
793
794 #[test]
795 fn a_sync_word_byte_takes_each_familys_layout() {
796 assert_eq!(
797 SyncWord::Public.sx126x().to_bytes(),
798 SyncWord::Custom(0x34).sx126x().to_bytes()
799 );
800 assert_eq!(
801 SyncWord::Private.sx126x().to_bytes(),
802 SyncWord::Custom(0x12).sx126x().to_bytes()
803 );
804 assert_eq!(SyncWord::Custom(0x2B).sx126x().to_bytes(), [0x24, 0xB4]);
805 assert_eq!(SyncWord::Custom(0x2B).sx127x().to_byte(), 0x2B);
806 for byte in [0x00, 0x12, 0x2B, 0x34, 0xFF] {
807 assert_eq!(SyncWord::from_byte(byte).to_byte(), byte);
808 }
809 }
810
811 #[test]
812 fn an_sx127x_config_reaches_the_registers_with_the_boards_output() {
813 let mut steps = Vec::new();
814 steps.extend(wrote(0x01, &[0x88]));
815 steps.extend(wrote(0x01, &[0x08]));
816 steps.extend(wrote(0x01, &[0x09]));
817 steps.extend(wrote(0x09, &[0x00]));
818 steps.extend(wrote(0x06, &[0xD9, 0x06, 0x66]));
819 steps.extend(read(0x3B, &[0x82]));
820 steps.extend(wrote(0x3B, &[0x42]));
821 steps.extend(read(0x3B, &[0x22]));
822 steps.extend(read(0x3B, &[0x02]));
823 steps.extend(wrote(0x01, &[0x08]));
824 steps.extend(wrote(0x01, &[0x88]));
825 steps.extend(wrote(0x01, &[0x89]));
826 steps.extend(wrote(0x09, &[0xFC]));
827 steps.extend(wrote(0x4D, &[0x84]));
828 steps.extend(wrote(0x0B, &[0x2B]));
829 steps.extend(wrote(0x1D, &[0x72]));
830 steps.extend(wrote(0x1E, &[0x74]));
831 steps.extend(wrote(0x26, &[0x04]));
832 steps.extend(wrote(0x20, &[0x00, 0x08]));
833 steps.extend(read(0x31, &[0xC3]));
834 steps.extend(wrote(0x31, &[0xC3]));
835 steps.extend(wrote(0x37, &[0x0A]));
836 steps.extend(wrote(0x36, &[0x03]));
837 steps.extend(wrote(0x23, &[0xFF]));
838 steps.extend(wrote(0x39, &[0x34]));
839 let mut radio = sx127x_radio(steps);
840
841 let config =
842 RadioConfig::new(868_100_000, LinkSettings::new(7, 125_000), 14).lorawan_device();
843 radio.configure(config).expect("configures");
844
845 assert_eq!(radio.link(), Some(LinkSettings::new(7, 125_000)));
846 assert!(done(radio));
847 }
848
849 #[test]
850 fn an_sx126x_config_sends_the_commands_and_the_spread_sync_word() {
851 use sx126x::command;
852 use sx126x::config::{self as chip, LoraModulation, PacketType, RampTime, StandbyMode};
853
854 let link = LinkSettings::new(7, 125_000);
855 let board = sx126x::Board::new(chip::PowerAmplifier::HighPower);
856 let power = chip::TxPower::for_output(chip::PowerAmplifier::HighPower, 14);
857 let modulation = LoraModulation::from_link(&link).expect("125 kHz is an SX126x bandwidth");
858 let image = chip::image_calibration(863_000_000, 870_000_000);
859 let steps = vec![
860 SpiStep::write(command::set_standby(StandbyMode::Rc).as_bytes().to_vec()),
861 SpiStep::write(
862 command::set_packet_type(PacketType::Lora)
863 .as_bytes()
864 .to_vec(),
865 ),
866 SpiStep::write(command::calibrate_image(image).as_bytes().to_vec()),
867 SpiStep::write(
868 command::set_rf_frequency(chip::frequency_word(868_100_000))
869 .as_bytes()
870 .to_vec(),
871 ),
872 SpiStep::write(command::set_pa_config(power.pa).as_bytes().to_vec()),
873 SpiStep::write(
874 command::set_tx_params(power.setting_dbm, RampTime::Us40)
875 .as_bytes()
876 .to_vec(),
877 ),
878 SpiStep::write(
879 command::set_lora_modulation_params(modulation)
880 .as_bytes()
881 .to_vec(),
882 ),
883 SpiStep::write([0x0D, 0x07, 0x40]),
884 SpiStep::write([0x24, 0xB4]),
885 ];
886 let mut radio = sx126x_radio(steps, board);
887
888 let config = RadioConfig::new(868_100_000, link, 14)
889 .with_band(863_000_000, 870_000_000)
890 .with_sync_word(SyncWord::Custom(0x2B));
891 radio.configure(config).expect("configures");
892
893 assert_eq!(radio.family(), Family::Sx126x);
894 assert!(done(radio));
895 }
896
897 #[test]
898 fn a_chip_refusal_arrives_wrapped_in_its_family() {
899 let board = sx126x::Board::new(sx126x::config::PowerAmplifier::HighPower).with_llcc68();
900 let mut radio = sx126x_radio(Vec::new(), board);
901 let config = RadioConfig::new(868_100_000, LinkSettings::new(10, 125_000), 14);
902
903 let refused = radio.configure(config);
904
905 assert_eq!(
906 refused,
907 Err(RadioError::Sx126x(sx126x::RadioError::Llcc68 {
908 spreading_factor: 10,
909 bandwidth_hz: 125_000,
910 }))
911 );
912 assert!(refused.expect_err("refused").to_string().contains("LLCC68"));
913 }
914
915 #[test]
916 fn nothing_is_sent_before_a_configuration() {
917 let mut radio = sx127x_radio(Vec::new());
918 assert_eq!(
919 radio.transmit(b"21.5"),
920 Err(RadioError::Sx127x(sx127x::RadioError::NotConfigured))
921 );
922 assert_eq!(radio.link(), None);
923 }
924
925 #[test]
926 fn registers_reach_either_family_and_the_sx127x_map_ends_at_0x7f() {
927 let mut steps = Vec::new();
928 steps.extend(wrote(0x39, &[0x2B]));
929 steps.extend(read(0x39, &[0x2B]));
930 let mut radio = sx127x_radio(steps);
931 radio.write_register(0x39, 0x2B).expect("writes");
932 assert_eq!(radio.read_register(0x39), Ok(0x2B));
933 assert_eq!(
934 radio.write_register(0x80, 0),
935 Err(RadioError::Address(0x80))
936 );
937 assert!(done(radio));
938
939 let board = sx126x::Board::new(sx126x::config::PowerAmplifier::HighPower);
940 let steps = vec![
941 SpiStep::write([0x1D, 0x07, 0x40, 0x00]),
942 SpiStep::read([0x14]),
943 ];
944 let mut radio = sx126x_radio(steps, board);
945 assert_eq!(radio.read_register(0x0740), Ok(0x14));
946 assert!(done(radio));
947 }
948}