Skip to main content

pamoja_radios/
linux.rs

1//! Opening a LoRa radio on a Linux board.
2//!
3//! On a Raspberry Pi or any Linux board a radio module's SPI bus is a file such as
4//! `/dev/spidev0.0`, and its reset and BUSY pins are lines on a GPIO chip such as
5//! `/dev/gpiochip0`. [`open_sx126x`] and [`open_sx127x`] open them through the Linux
6//! backends of `pamoja-hal`, reset the chip, and hand it back as a [`LinuxRadio`], so a
7//! gateway, a Python script, or a C# service drives a radio with no bus code of its own.
8//! The kernel's SPI interface has to be turned on first; on a Raspberry Pi that is
9//! `dtparam=spi=on` in `config.txt`.
10//!
11//! Only Linux has spidev and the GPIO character device. Everywhere else this module still
12//! builds and [`LinuxRadio`] still names a type, but opening a radio returns
13//! [`OpenError::Unsupported`], so a program and the language bindings compile on any
14//! platform and say plainly where no radio can be reached.
15//!
16//! # Examples
17//!
18//! ```no_run
19//! use pamoja_lora::LinkSettings;
20//! use pamoja_radios::linux::{self, Wiring};
21//! use pamoja_radios::radio::RadioConfig;
22//! use pamoja_radios::sx127x::config::PaOutput;
23//! use pamoja_radios::sx127x::Board;
24//!
25//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
26//! // An RFM95W on the Raspberry Pi's SPI0, chip select CE0, with its reset pin on GPIO25.
27//! let wiring = Wiring::new("/dev/spidev0.0", "/dev/gpiochip0", 25);
28//! let mut radio = linux::open_sx127x(&wiring, Board::new(PaOutput::PaBoost))?;
29//! radio.configure(RadioConfig::new(868_100_000, LinkSettings::new(9, 125_000), 14))?;
30//! let airtime_us = radio.transmit(b"21.5")?;
31//! println!("sent in {airtime_us} us");
32//! # Ok(())
33//! # }
34//! ```
35
36use std::fmt;
37use std::path::{Path, PathBuf};
38
39use embedded_hal::delay::DelayNs;
40use embedded_hal::digital::{self, InputPin, OutputPin};
41use embedded_hal::spi::{self, Operation, SpiDevice};
42
43use crate::radio::{Radio, RadioError};
44use crate::sx1302::bridge::Identity;
45use crate::sx1302::{usb, Sx1261, Sx1302};
46use crate::{sx126x, sx127x};
47
48/// The SPI mode both families take: CPOL 0 and CPHA 0, the clock idling low and data sampled
49/// on its rising edge, as section 8.2 of the SX126x family's datasheets gives it and as
50/// RadioLib opens both families.
51pub const SPI_MODE: u8 = 0;
52
53/// The SPI clock a radio is opened at unless its wiring names another, in hertz.
54///
55/// RadioLib opens both families at 2 MHz by default. A slower clock tolerates longer leads
56/// between the board and the module, and [`Wiring::with_spi_hz`] sets another.
57pub const DEFAULT_SPI_HZ: u32 = 2_000_000;
58
59/// The name the kernel shows as holding a radio's GPIO lines, which `gpioinfo` prints.
60pub const CONSUMER: &str = "pamoja-radio";
61
62/// The SPI device a radio is opened on: the kernel's spidev device.
63#[cfg(target_os = "linux")]
64pub type Spi = pamoja_hal::linux::SpidevDevice;
65
66/// The SPI device a radio is opened on, which only Linux has.
67#[cfg(not(target_os = "linux"))]
68pub type Spi = Unavailable;
69
70/// A GPIO line a radio's reset or BUSY pin is on: a line of the GPIO character device.
71#[cfg(target_os = "linux")]
72pub type Line = pamoja_hal::linux::CdevPin;
73
74/// A GPIO line a radio's reset or BUSY pin is on, which only Linux opens.
75#[cfg(not(target_os = "linux"))]
76pub type Line = Unavailable;
77
78/// The delay a radio's driver waits with: the process sleeping.
79#[cfg(target_os = "linux")]
80pub type Delay = pamoja_hal::linux::Delay;
81
82/// The delay a radio's driver waits with, which no opened radio needs off Linux.
83#[cfg(not(target_os = "linux"))]
84pub type Delay = Unavailable;
85
86/// What the SPI device reports when a transfer fails.
87#[cfg(target_os = "linux")]
88pub type SpiError = pamoja_hal::linux::SPIError;
89
90/// What the SPI device reports when a transfer fails.
91#[cfg(not(target_os = "linux"))]
92pub type SpiError = spi::ErrorKind;
93
94/// Why the SPI device or a GPIO line could not be opened.
95#[cfg(target_os = "linux")]
96pub type BusError = pamoja_hal::linux::OpenError;
97
98/// Why the SPI device or a GPIO line could not be opened, which never happens off Linux
99/// because nothing is opened there.
100#[cfg(not(target_os = "linux"))]
101pub type BusError = Unavailable;
102
103/// A radio opened on a Linux board.
104pub type LinuxRadio = Radio<Spi, Line, Line, Delay>;
105
106/// A concentrator opened on a Linux board.
107///
108/// A gateway part rather than a node one: it listens on many channels at once and is driven
109/// as a register map, so it is a handle of its own rather than a [`LinuxRadio`].
110pub type LinuxConcentrator = Sx1302<Spi, Line, Delay>;
111
112/// The port a USB card's bridge answers on.
113#[cfg(target_os = "linux")]
114pub type Port = std::fs::File;
115
116/// On any other platform there is no port to open, and this stands in for one.
117#[cfg(not(target_os = "linux"))]
118pub type Port = Unavailable;
119
120/// A concentrator on a USB card: the same driver as [`LinuxConcentrator`], reached through
121/// the card's bridge rather than a bus of the host's own.
122pub type UsbConcentrator = Sx1302<usb::BridgeSpi<Port>, usb::BridgePin<Port>, Delay>;
123
124/// The SX1261 beside a concentrator card on the host's SPI, which checks a channel before the
125/// gateway talks on it.
126pub type LinuxListener = Sx1261<Spi, Line, Delay>;
127
128/// The SX1261 on a USB card, reached through the card's bridge.
129pub type UsbListener = Sx1261<usb::BridgeSpi<Port>, usb::BridgePin<Port>, Delay>;
130
131/// A USB card, opened: its concentrator, and the bridge everything on the card shares.
132pub struct UsbCard {
133    /// The concentrator, reset and answering.
134    pub concentrator: UsbConcentrator,
135    /// The SX1261 on the card, for a card that carries one, not yet brought up.
136    ///
137    /// It shares the bridge with the concentrator under its own target and reset pin, so it
138    /// is driven while the concentrator runs. [`Sx1261::bring_up`] readies it; a card without
139    /// one simply never answers it.
140    pub listener: UsbListener,
141    /// The bridge, for the radio beside the concentrator and for the card's own status.
142    pub bridge: usb::Shared<Port>,
143    /// Who the bridge said it was, which a caller compares against the firmware this crate
144    /// was written for with [`Identity::matches_firmware`].
145    pub identity: Identity,
146}
147
148/// The bus, line, and delay of a platform with no spidev or GPIO character device.
149///
150/// No value of it exists, so a [`LinuxRadio`] can be named on any platform and opened only on
151/// Linux.
152#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
153pub enum Unavailable {}
154
155impl fmt::Display for Unavailable {
156    fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
157        match *self {}
158    }
159}
160
161impl std::error::Error for Unavailable {}
162
163impl spi::ErrorType for Unavailable {
164    type Error = spi::ErrorKind;
165}
166
167impl SpiDevice for Unavailable {
168    fn transaction(&mut self, _: &mut [Operation<'_, u8>]) -> Result<(), spi::ErrorKind> {
169        match *self {}
170    }
171}
172
173impl digital::ErrorType for Unavailable {
174    type Error = digital::ErrorKind;
175}
176
177impl InputPin for Unavailable {
178    fn is_high(&mut self) -> Result<bool, digital::ErrorKind> {
179        match *self {}
180    }
181
182    fn is_low(&mut self) -> Result<bool, digital::ErrorKind> {
183        match *self {}
184    }
185}
186
187impl OutputPin for Unavailable {
188    fn set_low(&mut self) -> Result<(), digital::ErrorKind> {
189        match *self {}
190    }
191
192    fn set_high(&mut self) -> Result<(), digital::ErrorKind> {
193        match *self {}
194    }
195}
196
197impl DelayNs for Unavailable {
198    fn delay_ns(&mut self, _: u32) {
199        match *self {}
200    }
201}
202
203impl std::io::Read for Unavailable {
204    fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
205        match *self {}
206    }
207}
208
209impl std::io::Write for Unavailable {
210    fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
211        match *self {}
212    }
213
214    fn flush(&mut self) -> std::io::Result<()> {
215        match *self {}
216    }
217}
218
219/// Where a radio module is wired on a Linux board.
220///
221/// # Examples
222///
223/// ```
224/// use pamoja_radios::linux::{Wiring, DEFAULT_SPI_HZ};
225///
226/// // An SX1262 board on SPI0's second chip select, BUSY on GPIO24 and reset on GPIO25.
227/// let wiring = Wiring::new("/dev/spidev0.1", "/dev/gpiochip0", 25).with_busy_line(24);
228/// assert_eq!(wiring.busy_line, Some(24));
229/// assert_eq!(wiring.spi_hz, DEFAULT_SPI_HZ);
230/// ```
231#[derive(Clone, Debug, PartialEq, Eq, Hash)]
232pub struct Wiring {
233    /// The SPI device file, one per chip select, such as `/dev/spidev0.0`.
234    pub spi: PathBuf,
235    /// The SPI clock in hertz.
236    pub spi_hz: u32,
237    /// The GPIO chip the lines are on, `/dev/gpiochip0` for a Raspberry Pi's header.
238    pub gpio_chip: PathBuf,
239    /// The line the module's reset pin is on: its offset on the chip, which on a Raspberry Pi
240    /// is the BCM GPIO number.
241    pub reset_line: u32,
242    /// The line an SX126x's BUSY pin is on. The SX127x has no BUSY pin.
243    pub busy_line: Option<u32>,
244    /// The line that switches a concentrator's supply on, for a board that gates it.
245    ///
246    /// A card wired this way answers nothing at all until the line is raised, and it has to
247    /// stay raised for as long as the card is used.
248    pub power_enable_line: Option<u32>,
249}
250
251impl Wiring {
252    /// Describes a module on an SPI device with its reset pin on a GPIO line, clocked at
253    /// [`DEFAULT_SPI_HZ`] and with no BUSY line.
254    ///
255    /// # Arguments
256    ///
257    /// * `spi` - the SPI device file.
258    /// * `gpio_chip` - the GPIO chip the lines are on.
259    /// * `reset_line` - the line the reset pin is on.
260    ///
261    /// # Returns
262    ///
263    /// The wiring.
264    pub fn new(spi: impl Into<PathBuf>, gpio_chip: impl Into<PathBuf>, reset_line: u32) -> Wiring {
265        Wiring {
266            spi: spi.into(),
267            spi_hz: DEFAULT_SPI_HZ,
268            gpio_chip: gpio_chip.into(),
269            reset_line,
270            busy_line: None,
271            power_enable_line: None,
272        }
273    }
274
275    /// Returns the wiring with a concentrator's supply gated behind a line.
276    ///
277    /// # Arguments
278    ///
279    /// * `line` - the line that switches the supply on.
280    ///
281    /// # Returns
282    ///
283    /// The wiring.
284    pub fn with_power_enable_line(mut self, line: u32) -> Wiring {
285        self.power_enable_line = Some(line);
286        self
287    }
288
289    /// Returns the wiring with an SX126x's BUSY pin on a line.
290    ///
291    /// # Arguments
292    ///
293    /// * `line` - the line the BUSY pin is on.
294    ///
295    /// # Returns
296    ///
297    /// The wiring.
298    pub fn with_busy_line(mut self, line: u32) -> Wiring {
299        self.busy_line = Some(line);
300        self
301    }
302
303    /// Returns the wiring with another SPI clock.
304    ///
305    /// # Arguments
306    ///
307    /// * `hz` - the clock in hertz.
308    ///
309    /// # Returns
310    ///
311    /// The wiring.
312    pub fn with_spi_hz(mut self, hz: u32) -> Wiring {
313        self.spi_hz = hz;
314        self
315    }
316}
317
318/// Why a radio could not be opened.
319#[derive(Debug)]
320pub enum OpenError {
321    /// The platform has no spidev or GPIO character device, which is any platform but Linux.
322    Unsupported,
323    /// An SX126x was opened on wiring that names no BUSY line.
324    NoBusyLine,
325    /// A device file could not be opened: the SPI device or the GPIO chip.
326    Bus {
327        /// The file that could not be opened.
328        device: PathBuf,
329        /// Why, which is most often a kernel interface left off or a missing group.
330        error: BusError,
331    },
332    /// The chip did not come up after its reset, which is most often a wiring or a power
333    /// problem.
334    Radio(RadioError<SpiError>),
335    /// The reset line was opened but could not be driven, so the chip was never reset.
336    ResetLine {
337        /// The GPIO chip the line is on.
338        device: PathBuf,
339    },
340    /// The bridge on a USB card did not answer, or refused, while the card was brought up.
341    Bridge {
342        /// The port the card is on.
343        device: PathBuf,
344        /// What went wrong.
345        error: usb::UsbError,
346    },
347}
348
349impl fmt::Display for OpenError {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        match self {
352            OpenError::Unsupported => f.write_str(
353                "a LoRa radio or concentrator is opened through spidev and the GPIO character device, which only Linux has",
354            ),
355            OpenError::ResetLine { device } => write!(
356                f,
357                "{}: the reset line opened but could not be driven",
358                device.display()
359            ),
360            OpenError::Bridge { device, error } => {
361                write!(f, "{}: the bridge on the card: {error}", device.display())
362            }
363            OpenError::NoBusyLine => {
364                f.write_str("an SX126x needs its BUSY line, and the wiring names none")
365            }
366            OpenError::Bus { device, error } => write!(f, "{}: {error}", device.display()),
367            OpenError::Radio(error) => error.fmt(f),
368        }
369    }
370}
371
372impl std::error::Error for OpenError {
373    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
374        match self {
375            OpenError::Bridge { error, .. } => Some(error),
376            OpenError::Bus { error, .. } => Some(error),
377            OpenError::Radio(error) => Some(error),
378            OpenError::Unsupported | OpenError::NoBusyLine | OpenError::ResetLine { .. } => None,
379        }
380    }
381}
382
383/// Opens an SX1261, SX1262, SX1268, or LLCC68 module and resets it.
384///
385/// # Arguments
386///
387/// * `wiring` - the SPI device and the lines, which must name the BUSY line.
388/// * `board` - how the module wires the chip: its amplifier, clock, antenna switch, and
389///   regulator.
390///
391/// # Returns
392///
393/// The radio, reset and in standby, ready for [`Radio::configure`].
394///
395/// # Errors
396///
397/// Returns [`OpenError::NoBusyLine`] if the wiring names no BUSY line,
398/// [`OpenError::Unsupported`] on any platform but Linux, [`OpenError::Bus`] if the SPI device
399/// or a line cannot be opened, and [`OpenError::Radio`] if no SX126x answers.
400pub fn open_sx126x(wiring: &Wiring, board: sx126x::Board) -> Result<LinuxRadio, OpenError> {
401    let busy_line = wiring.busy_line.ok_or(OpenError::NoBusyLine)?;
402    platform::open_sx126x(wiring, busy_line, board)
403}
404
405/// Opens an SX1276, SX1277, SX1278, or SX1279 module, such as an RFM95W, and resets it into
406/// LoRa mode.
407///
408/// # Arguments
409///
410/// * `wiring` - the SPI device and the reset line.
411/// * `board` - how the module wires the chip: its amplifier output and clock.
412///
413/// # Returns
414///
415/// The radio, reset and in standby, ready for [`Radio::configure`].
416///
417/// # Errors
418///
419/// Returns [`OpenError::Unsupported`] on any platform but Linux, [`OpenError::Bus`] if the
420/// SPI device or the reset line cannot be opened, and [`OpenError::Radio`] if no SX127x
421/// answers.
422pub fn open_sx127x(wiring: &Wiring, board: sx127x::Board) -> Result<LinuxRadio, OpenError> {
423    platform::open_sx127x(wiring, board)
424}
425
426/// Opens an SX1302 or SX1303 concentrator and pulses its reset line.
427///
428/// Unlike a transceiver, a concentrator is not ready when this returns. It answers on the bus,
429/// which is what the reset buys, but it hears nothing until its two microcontrollers are given
430/// firmware, and those images belong to the caller. So this hands back a handle to check with
431/// [`Sx1302::check`] and load with [`Sx1302::load_firmware`], rather than pretending to an
432/// initialization it cannot finish.
433///
434/// # Arguments
435///
436/// * `wiring` - the SPI device and the reset line. The concentrator has no BUSY pin, so
437///   [`busy_line`](Wiring::busy_line) is ignored.
438///
439/// # Returns
440///
441/// The concentrator, reset and answering, and the supply line for a board that gates one.
442///
443/// That second value has to be kept for as long as the concentrator is used. A GPIO line is
444/// released when the handle holding it is dropped, so letting it go switches the card off
445/// again, and the card then answers nothing while every other part of the configuration
446/// looks right. Bind it to a name rather than to `_`.
447///
448/// # Errors
449///
450/// Returns [`OpenError::Unsupported`] on any platform but Linux, and [`OpenError::Bus`] if
451/// the SPI device, the reset line, or the supply line cannot be opened.
452pub fn open_sx1302(wiring: &Wiring) -> Result<(LinuxConcentrator, Option<Line>), OpenError> {
453    platform::open_sx1302(wiring)
454}
455
456/// Opens the SX1261 beside a concentrator card on SPI.
457///
458/// The radio has its own chip select, `/dev/spidev0.1` on Semtech's reference card, and its
459/// own reset line, GPIO 22 in the reference's `reset_lgw.sh`, and no BUSY line. Its reset line
460/// is left high, which is the radio running; [`Sx1261::bring_up`] pulses it low.
461///
462/// # Arguments
463///
464/// * `wiring` - the radio's SPI device and reset line. The BUSY and supply lines are ignored.
465///
466/// # Returns
467///
468/// The radio, not yet brought up.
469///
470/// # Errors
471///
472/// Returns [`OpenError::Unsupported`] on any platform but Linux, and [`OpenError::Bus`] if
473/// the SPI device or the reset line cannot be opened.
474pub fn open_sx1261(wiring: &Wiring) -> Result<LinuxListener, OpenError> {
475    platform::open_sx1261(wiring)
476}
477
478/// Opens a concentrator on a USB card, and brings the card up.
479///
480/// The card enumerates as a serial device, `/dev/ttyACM0` on a Raspberry Pi with nothing
481/// else plugged in. The port is opened raw, the bridge is asked who it is and how it is, the
482/// supply is switched on and both resets are pulsed in the order the reference does them,
483/// and then the concentrator is reset once more through the driver, so it is in the same
484/// state a concentrator on SPI is left in by [`open_sx1302`].
485///
486/// A bridge running a different firmware than this crate was written for is not refused;
487/// the reference warns and carries on, and the caller has [`UsbCard::identity`] to do the
488/// same.
489///
490/// # Arguments
491///
492/// * `port` - the serial device the card is on.
493///
494/// # Returns
495///
496/// The card, with its concentrator answering.
497///
498/// # Errors
499///
500/// [`OpenError::Unsupported`] on any platform but Linux, [`OpenError::Bus`] when the port
501/// cannot be opened, [`OpenError::Bridge`] when the bridge does not answer or refuses a pin,
502/// and [`OpenError::ResetLine`] when the concentrator cannot be reset through it.
503pub fn open_usb_sx1302(port: impl AsRef<Path>) -> Result<UsbCard, OpenError> {
504    platform::open_usb_sx1302(port.as_ref())
505}
506
507#[cfg(target_os = "linux")]
508mod platform {
509    use std::path::Path;
510
511    use embedded_hal::digital::PinState;
512    use pamoja_hal::linux;
513
514    use super::{
515        Line, LinuxConcentrator, LinuxRadio, OpenError, UsbCard, Wiring, CONSUMER, SPI_MODE,
516    };
517    use crate::radio::Radio;
518    use crate::sx1302::{bridge, usb, Sx1261, Sx1302};
519    use crate::{sx126x, sx127x};
520
521    pub(super) fn open_sx126x(
522        wiring: &Wiring,
523        busy_line: u32,
524        board: sx126x::Board,
525    ) -> Result<LinuxRadio, OpenError> {
526        let spi = spi(wiring)?;
527        let busy = linux::input(&wiring.gpio_chip, busy_line, CONSUMER)
528            .map_err(|error| bus(&wiring.gpio_chip, error))?;
529        let reset = reset(wiring)?;
530        start(Radio::Sx126x(sx126x::Sx126x::new(
531            spi,
532            busy,
533            reset,
534            linux::delay(),
535            board,
536        )))
537    }
538
539    pub(super) fn open_sx127x(
540        wiring: &Wiring,
541        board: sx127x::Board,
542    ) -> Result<LinuxRadio, OpenError> {
543        let spi = spi(wiring)?;
544        let reset = reset(wiring)?;
545        start(Radio::Sx127x(sx127x::Sx127x::new(
546            spi,
547            reset,
548            linux::delay(),
549            board,
550        )))
551    }
552
553    pub(super) fn open_sx1302(
554        wiring: &Wiring,
555    ) -> Result<(LinuxConcentrator, Option<Line>), OpenError> {
556        // The supply comes first, before the bus and before the reset, which is the order the
557        // reference platform script uses. A board that gates its concentrator answers nothing
558        // until this is raised.
559        let power = match wiring.power_enable_line {
560            None => None,
561            Some(line) => Some(
562                linux::output(&wiring.gpio_chip, line, CONSUMER, PinState::High)
563                    .map_err(|error| bus(&wiring.gpio_chip, error))?,
564            ),
565        };
566
567        let spi = spi(wiring)?;
568        let reset = reset(wiring)?;
569        let mut concentrator = Sx1302::new(spi, reset, linux::delay());
570
571        // The chip does not reset itself, and nothing it answers means anything until it has.
572        concentrator.reset().map_err(|_| OpenError::ResetLine {
573            device: wiring.gpio_chip.clone(),
574        })?;
575        Ok((concentrator, power))
576    }
577
578    pub(super) fn open_usb_sx1302(port: &Path) -> Result<UsbCard, OpenError> {
579        let file = linux::serial(port, bridge::BAUD).map_err(|error| bus(port, error))?;
580        let mut over = usb::Bridge::new(file);
581        let failed = |error: usb::UsbError| OpenError::Bridge {
582            device: port.to_path_buf(),
583            error,
584        };
585
586        // The reference asks who the bridge is and how it is before touching a pin; an
587        // answer to both is the proof that a bridge, and not something else, is on the port.
588        let identity = over.ping().map_err(failed)?;
589        over.status().map_err(failed)?;
590        over.power_up().map_err(failed)?;
591
592        let shared = usb::Shared::new(over);
593        let mut concentrator = Sx1302::new(
594            shared.concentrator(),
595            shared.pin(bridge::PIN_RESET),
596            linux::delay(),
597        );
598        concentrator.reset().map_err(|_| OpenError::ResetLine {
599            device: port.to_path_buf(),
600        })?;
601        let listener = Sx1261::new(
602            shared.listener(),
603            shared.pin(bridge::PIN_RADIO_RESET),
604            linux::delay(),
605        );
606        Ok(UsbCard {
607            concentrator,
608            listener,
609            bridge: shared,
610            identity,
611        })
612    }
613
614    pub(super) fn open_sx1261(wiring: &Wiring) -> Result<super::LinuxListener, OpenError> {
615        Ok(Sx1261::new(spi(wiring)?, reset(wiring)?, linux::delay()))
616    }
617
618    fn spi(wiring: &Wiring) -> Result<linux::SpidevDevice, OpenError> {
619        linux::spi(&wiring.spi, SPI_MODE, wiring.spi_hz).map_err(|error| bus(&wiring.spi, error))
620    }
621
622    fn reset(wiring: &Wiring) -> Result<linux::CdevPin, OpenError> {
623        linux::output(
624            &wiring.gpio_chip,
625            wiring.reset_line,
626            CONSUMER,
627            PinState::High,
628        )
629        .map_err(|error| bus(&wiring.gpio_chip, error))
630    }
631
632    fn start(mut radio: LinuxRadio) -> Result<LinuxRadio, OpenError> {
633        radio.init().map_err(OpenError::Radio)?;
634        Ok(radio)
635    }
636
637    fn bus(device: &Path, error: linux::OpenError) -> OpenError {
638        OpenError::Bus {
639            device: device.to_path_buf(),
640            error,
641        }
642    }
643}
644
645#[cfg(not(target_os = "linux"))]
646mod platform {
647    use super::{LinuxConcentrator, LinuxRadio, OpenError, Wiring};
648    use crate::{sx126x, sx127x};
649
650    pub(super) fn open_sx126x(
651        _: &Wiring,
652        _: u32,
653        _: sx126x::Board,
654    ) -> Result<LinuxRadio, OpenError> {
655        Err(OpenError::Unsupported)
656    }
657
658    pub(super) fn open_sx127x(_: &Wiring, _: sx127x::Board) -> Result<LinuxRadio, OpenError> {
659        Err(OpenError::Unsupported)
660    }
661
662    pub(super) fn open_sx1302(
663        _: &Wiring,
664    ) -> Result<(LinuxConcentrator, Option<super::Line>), OpenError> {
665        Err(OpenError::Unsupported)
666    }
667
668    pub(super) fn open_usb_sx1302(_: &std::path::Path) -> Result<super::UsbCard, OpenError> {
669        Err(OpenError::Unsupported)
670    }
671
672    pub(super) fn open_sx1261(_: &Wiring) -> Result<super::LinuxListener, OpenError> {
673        Err(OpenError::Unsupported)
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680    use crate::sx126x::config::PowerAmplifier;
681    use crate::sx127x::config::PaOutput;
682
683    #[test]
684    fn wiring_starts_at_the_default_clock_with_no_busy_line() {
685        let wiring = Wiring::new("/dev/spidev0.0", "/dev/gpiochip0", 25);
686        assert_eq!(wiring.spi_hz, 2_000_000);
687        assert_eq!(wiring.busy_line, None);
688        let wiring = wiring.with_busy_line(24).with_spi_hz(8_000_000);
689        assert_eq!((wiring.busy_line, wiring.spi_hz), (Some(24), 8_000_000));
690    }
691
692    #[test]
693    fn an_sx126x_is_not_opened_without_its_busy_line() {
694        let wiring = Wiring::new("/dev/spidev0.0", "/dev/gpiochip0", 25);
695        let refused = open_sx126x(&wiring, sx126x::Board::new(PowerAmplifier::HighPower)).err();
696        assert!(matches!(refused, Some(OpenError::NoBusyLine)));
697    }
698
699    #[cfg(target_os = "linux")]
700    #[test]
701    fn a_missing_spi_device_is_named_in_the_error() {
702        let wiring = Wiring::new("/dev/spidev-pamoja-absent", "/dev/gpiochip0", 25);
703        let refused = open_sx127x(&wiring, sx127x::Board::new(PaOutput::PaBoost))
704            .err()
705            .expect("no such device exists");
706        assert!(matches!(refused, OpenError::Bus { .. }));
707        assert!(
708            refused
709                .to_string()
710                .starts_with("/dev/spidev-pamoja-absent: "),
711            "{refused}"
712        );
713    }
714
715    #[cfg(target_os = "linux")]
716    #[test]
717    fn a_concentrator_names_the_device_it_could_not_open() {
718        let wiring = Wiring::new("/dev/spidev-pamoja-absent", "/dev/gpiochip0", 23);
719        let refused = open_sx1302(&wiring).err().expect("no such device exists");
720        assert!(matches!(refused, OpenError::Bus { .. }));
721        assert!(
722            refused
723                .to_string()
724                .starts_with("/dev/spidev-pamoja-absent: "),
725            "{refused}"
726        );
727    }
728
729    #[cfg(not(target_os = "linux"))]
730    #[test]
731    fn only_linux_opens_a_concentrator() {
732        // A concentrator needs no BUSY line, so the wiring that opens one names none.
733        let wiring = Wiring::new("/dev/spidev0.0", "/dev/gpiochip0", 23);
734        let refused = open_sx1302(&wiring).err();
735        assert!(matches!(refused, Some(OpenError::Unsupported)));
736    }
737
738    #[cfg(not(target_os = "linux"))]
739    #[test]
740    fn only_linux_opens_a_radio() {
741        let wiring = Wiring::new("/dev/spidev0.0", "/dev/gpiochip0", 25);
742        let refused = open_sx127x(&wiring, sx127x::Board::new(PaOutput::PaBoost)).err();
743        assert!(matches!(refused, Some(OpenError::Unsupported)));
744        assert!(OpenError::Unsupported.to_string().contains("only Linux"));
745    }
746}