1use std::fmt;
37use std::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::{sx126x, sx127x};
45
46pub const SPI_MODE: u8 = 0;
50
51pub const DEFAULT_SPI_HZ: u32 = 2_000_000;
56
57pub const CONSUMER: &str = "pamoja-radio";
59
60#[cfg(target_os = "linux")]
62pub type Spi = pamoja_hal::linux::SpidevDevice;
63
64#[cfg(not(target_os = "linux"))]
66pub type Spi = Unavailable;
67
68#[cfg(target_os = "linux")]
70pub type Line = pamoja_hal::linux::CdevPin;
71
72#[cfg(not(target_os = "linux"))]
74pub type Line = Unavailable;
75
76#[cfg(target_os = "linux")]
78pub type Delay = pamoja_hal::linux::Delay;
79
80#[cfg(not(target_os = "linux"))]
82pub type Delay = Unavailable;
83
84#[cfg(target_os = "linux")]
86pub type SpiError = pamoja_hal::linux::SPIError;
87
88#[cfg(not(target_os = "linux"))]
90pub type SpiError = spi::ErrorKind;
91
92#[cfg(target_os = "linux")]
94pub type BusError = pamoja_hal::linux::OpenError;
95
96#[cfg(not(target_os = "linux"))]
99pub type BusError = Unavailable;
100
101pub type LinuxRadio = Radio<Spi, Line, Line, Delay>;
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
109pub enum Unavailable {}
110
111impl fmt::Display for Unavailable {
112 fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match *self {}
114 }
115}
116
117impl std::error::Error for Unavailable {}
118
119impl spi::ErrorType for Unavailable {
120 type Error = spi::ErrorKind;
121}
122
123impl SpiDevice for Unavailable {
124 fn transaction(&mut self, _: &mut [Operation<'_, u8>]) -> Result<(), spi::ErrorKind> {
125 match *self {}
126 }
127}
128
129impl digital::ErrorType for Unavailable {
130 type Error = digital::ErrorKind;
131}
132
133impl InputPin for Unavailable {
134 fn is_high(&mut self) -> Result<bool, digital::ErrorKind> {
135 match *self {}
136 }
137
138 fn is_low(&mut self) -> Result<bool, digital::ErrorKind> {
139 match *self {}
140 }
141}
142
143impl OutputPin for Unavailable {
144 fn set_low(&mut self) -> Result<(), digital::ErrorKind> {
145 match *self {}
146 }
147
148 fn set_high(&mut self) -> Result<(), digital::ErrorKind> {
149 match *self {}
150 }
151}
152
153impl DelayNs for Unavailable {
154 fn delay_ns(&mut self, _: u32) {
155 match *self {}
156 }
157}
158
159#[derive(Clone, Debug, PartialEq, Eq, Hash)]
172pub struct Wiring {
173 pub spi: PathBuf,
175 pub spi_hz: u32,
177 pub gpio_chip: PathBuf,
179 pub reset_line: u32,
182 pub busy_line: Option<u32>,
184}
185
186impl Wiring {
187 pub fn new(spi: impl Into<PathBuf>, gpio_chip: impl Into<PathBuf>, reset_line: u32) -> Wiring {
200 Wiring {
201 spi: spi.into(),
202 spi_hz: DEFAULT_SPI_HZ,
203 gpio_chip: gpio_chip.into(),
204 reset_line,
205 busy_line: None,
206 }
207 }
208
209 pub fn with_busy_line(mut self, line: u32) -> Wiring {
219 self.busy_line = Some(line);
220 self
221 }
222
223 pub fn with_spi_hz(mut self, hz: u32) -> Wiring {
233 self.spi_hz = hz;
234 self
235 }
236}
237
238#[derive(Debug)]
240pub enum OpenError {
241 Unsupported,
243 NoBusyLine,
245 Bus {
247 device: PathBuf,
249 error: BusError,
251 },
252 Radio(RadioError<SpiError>),
255}
256
257impl fmt::Display for OpenError {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 match self {
260 OpenError::Unsupported => f.write_str(
261 "a LoRa radio is opened through spidev and the GPIO character device, which only Linux has",
262 ),
263 OpenError::NoBusyLine => {
264 f.write_str("an SX126x needs its BUSY line, and the wiring names none")
265 }
266 OpenError::Bus { device, error } => write!(f, "{}: {error}", device.display()),
267 OpenError::Radio(error) => error.fmt(f),
268 }
269 }
270}
271
272impl std::error::Error for OpenError {
273 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
274 match self {
275 OpenError::Bus { error, .. } => Some(error),
276 OpenError::Radio(error) => Some(error),
277 OpenError::Unsupported | OpenError::NoBusyLine => None,
278 }
279 }
280}
281
282pub fn open_sx126x(wiring: &Wiring, board: sx126x::Board) -> Result<LinuxRadio, OpenError> {
300 let busy_line = wiring.busy_line.ok_or(OpenError::NoBusyLine)?;
301 platform::open_sx126x(wiring, busy_line, board)
302}
303
304pub fn open_sx127x(wiring: &Wiring, board: sx127x::Board) -> Result<LinuxRadio, OpenError> {
322 platform::open_sx127x(wiring, board)
323}
324
325#[cfg(target_os = "linux")]
326mod platform {
327 use std::path::Path;
328
329 use embedded_hal::digital::PinState;
330 use pamoja_hal::linux;
331
332 use super::{LinuxRadio, OpenError, Wiring, CONSUMER, SPI_MODE};
333 use crate::radio::Radio;
334 use crate::{sx126x, sx127x};
335
336 pub(super) fn open_sx126x(
337 wiring: &Wiring,
338 busy_line: u32,
339 board: sx126x::Board,
340 ) -> Result<LinuxRadio, OpenError> {
341 let spi = spi(wiring)?;
342 let busy = linux::input(&wiring.gpio_chip, busy_line, CONSUMER)
343 .map_err(|error| bus(&wiring.gpio_chip, error))?;
344 let reset = reset(wiring)?;
345 start(Radio::Sx126x(sx126x::Sx126x::new(
346 spi,
347 busy,
348 reset,
349 linux::delay(),
350 board,
351 )))
352 }
353
354 pub(super) fn open_sx127x(
355 wiring: &Wiring,
356 board: sx127x::Board,
357 ) -> Result<LinuxRadio, OpenError> {
358 let spi = spi(wiring)?;
359 let reset = reset(wiring)?;
360 start(Radio::Sx127x(sx127x::Sx127x::new(
361 spi,
362 reset,
363 linux::delay(),
364 board,
365 )))
366 }
367
368 fn spi(wiring: &Wiring) -> Result<linux::SpidevDevice, OpenError> {
369 linux::spi(&wiring.spi, SPI_MODE, wiring.spi_hz).map_err(|error| bus(&wiring.spi, error))
370 }
371
372 fn reset(wiring: &Wiring) -> Result<linux::CdevPin, OpenError> {
373 linux::output(
374 &wiring.gpio_chip,
375 wiring.reset_line,
376 CONSUMER,
377 PinState::High,
378 )
379 .map_err(|error| bus(&wiring.gpio_chip, error))
380 }
381
382 fn start(mut radio: LinuxRadio) -> Result<LinuxRadio, OpenError> {
383 radio.init().map_err(OpenError::Radio)?;
384 Ok(radio)
385 }
386
387 fn bus(device: &Path, error: linux::OpenError) -> OpenError {
388 OpenError::Bus {
389 device: device.to_path_buf(),
390 error,
391 }
392 }
393}
394
395#[cfg(not(target_os = "linux"))]
396mod platform {
397 use super::{LinuxRadio, OpenError, Wiring};
398 use crate::{sx126x, sx127x};
399
400 pub(super) fn open_sx126x(
401 _: &Wiring,
402 _: u32,
403 _: sx126x::Board,
404 ) -> Result<LinuxRadio, OpenError> {
405 Err(OpenError::Unsupported)
406 }
407
408 pub(super) fn open_sx127x(_: &Wiring, _: sx127x::Board) -> Result<LinuxRadio, OpenError> {
409 Err(OpenError::Unsupported)
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::sx126x::config::PowerAmplifier;
417 use crate::sx127x::config::PaOutput;
418
419 #[test]
420 fn wiring_starts_at_the_default_clock_with_no_busy_line() {
421 let wiring = Wiring::new("/dev/spidev0.0", "/dev/gpiochip0", 25);
422 assert_eq!(wiring.spi_hz, 2_000_000);
423 assert_eq!(wiring.busy_line, None);
424 let wiring = wiring.with_busy_line(24).with_spi_hz(8_000_000);
425 assert_eq!((wiring.busy_line, wiring.spi_hz), (Some(24), 8_000_000));
426 }
427
428 #[test]
429 fn an_sx126x_is_not_opened_without_its_busy_line() {
430 let wiring = Wiring::new("/dev/spidev0.0", "/dev/gpiochip0", 25);
431 let refused = open_sx126x(&wiring, sx126x::Board::new(PowerAmplifier::HighPower)).err();
432 assert!(matches!(refused, Some(OpenError::NoBusyLine)));
433 }
434
435 #[cfg(target_os = "linux")]
436 #[test]
437 fn a_missing_spi_device_is_named_in_the_error() {
438 let wiring = Wiring::new("/dev/spidev-pamoja-absent", "/dev/gpiochip0", 25);
439 let refused = open_sx127x(&wiring, sx127x::Board::new(PaOutput::PaBoost))
440 .err()
441 .expect("no such device exists");
442 assert!(matches!(refused, OpenError::Bus { .. }));
443 assert!(
444 refused
445 .to_string()
446 .starts_with("/dev/spidev-pamoja-absent: "),
447 "{refused}"
448 );
449 }
450
451 #[cfg(not(target_os = "linux"))]
452 #[test]
453 fn only_linux_opens_a_radio() {
454 let wiring = Wiring::new("/dev/spidev0.0", "/dev/gpiochip0", 25);
455 let refused = open_sx127x(&wiring, sx127x::Board::new(PaOutput::PaBoost)).err();
456 assert!(matches!(refused, Some(OpenError::Unsupported)));
457 assert!(OpenError::Unsupported.to_string().contains("only Linux"));
458 }
459}