Skip to main content

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}