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