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}