Skip to main content

pamoja_ffi/
lora_radio.rs

1//! The C ABI for a LoRa radio on a Linux board, or on a simulated chip anywhere.
2//!
3//! These functions open an SX126x or SX127x module through the kernel's spidev and GPIO
4//! character devices, as [`pamoja_radios::linux`] does, and drive either family with the same
5//! calls: configure, transmit, receive, listen, standby, sleep, and a register read and write.
6//! A radio holds its device files open between calls, so it crosses the boundary as a handle
7//! the caller releases with [`pamoja_lora_radio_free`].
8//!
9//! Only Linux has those devices. On every other platform the header and the functions are the
10//! same, and opening a radio returns [`PamojaStatus::Unsupported`] with a message saying why.
11//!
12//! A simulated chip, [`PamojaLoraSimChip`], stands in for a module on any platform. The radio
13//! [`pamoja_lora_sim_chip_radio`] gives drives it with the same calls, while the caller says
14//! what arrives on the air and reads back what the chip was tuned to and what it sent.
15//!
16//! Each call blocks until the chip answers: a transmission for its airtime, a reception for
17//! its timeout. A caller with an event loop makes these calls from a worker thread, and a
18//! handle is used by one thread at a time.
19
20use std::fmt;
21use std::os::raw::c_char;
22use std::panic::{catch_unwind, AssertUnwindSafe};
23
24use pamoja_lora::budget::Decibels;
25use pamoja_radios::linux::{self, LinuxRadio, OpenError, Wiring};
26use pamoja_radios::radio::{Family, RadioConfig, RadioError, Reception, SyncWord};
27use pamoja_radios::sim::{Chip, SimRadio, Tuning};
28use pamoja_radios::sx126x::config::{PowerAmplifier, TcxoVoltage};
29use pamoja_radios::sx127x::config::PaOutput;
30use pamoja_radios::{sx126x, sx127x};
31
32use crate::lora::{settings, PamojaLoraLink};
33use crate::{read_str, set_last_error, PamojaStatus};
34
35/// The family of an SX1261, SX1262, SX1268, or LLCC68.
36pub const PAMOJA_LORA_RADIO_SX126X: u8 = 0;
37
38/// The family of an SX1276, SX1277, SX1278, or SX1279.
39pub const PAMOJA_LORA_RADIO_SX127X: u8 = 1;
40
41/// A reception outcome: a frame arrived and checked.
42pub const PAMOJA_LORA_RADIO_FRAME: u8 = 0;
43
44/// A reception outcome: no frame arrived before the timeout.
45pub const PAMOJA_LORA_RADIO_TIMEOUT: u8 = 1;
46
47/// A reception outcome: a frame arrived whose header or CRC failed its check, and was dropped.
48pub const PAMOJA_LORA_RADIO_CORRUPT: u8 = 2;
49
50/// A reception outcome: nothing has arrived since the last frame was taken.
51pub const PAMOJA_LORA_RADIO_NOTHING: u8 = 3;
52
53/// The sync word of a public network such as LoRaWAN.
54pub const PAMOJA_LORA_RADIO_SYNC_WORD_PUBLIC: u8 = 0x34;
55
56/// The sync word of a private network, and both families' reset value.
57pub const PAMOJA_LORA_RADIO_SYNC_WORD_PRIVATE: u8 = 0x12;
58
59/// The SPI clock a radio opens at when the call passes 0, in hertz.
60pub const PAMOJA_LORA_RADIO_DEFAULT_SPI_HZ: u32 = 2_000_000;
61
62const _: () = assert!(PAMOJA_LORA_RADIO_DEFAULT_SPI_HZ == linux::DEFAULT_SPI_HZ);
63const _: () = assert!(PAMOJA_LORA_RADIO_SYNC_WORD_PUBLIC == SyncWord::Public.to_byte());
64const _: () = assert!(PAMOJA_LORA_RADIO_SYNC_WORD_PRIVATE == SyncWord::Private.to_byte());
65
66/// A LoRa radio opened on a Linux board or wired to a simulated chip, released with
67/// [`pamoja_lora_radio_free`].
68pub struct PamojaLoraRadio {
69    radio: Held,
70}
71
72/// The radio a handle drives: a module on a Linux board, or a simulated chip.
73enum Held {
74    Linux(LinuxRadio),
75    Simulated(SimRadio),
76}
77
78impl Held {
79    fn family(&self) -> Family {
80        match self {
81            Held::Linux(radio) => radio.family(),
82            Held::Simulated(radio) => radio.family(),
83        }
84    }
85}
86
87/// Runs the same call on whichever radio a handle holds, turning its error into a refusal.
88macro_rules! on_radio {
89    ($held:expr, $radio:ident => $call:expr) => {
90        match $held {
91            Held::Linux($radio) => $call.map_err(refusal),
92            Held::Simulated($radio) => $call.map_err(refusal),
93        }
94    };
95}
96
97/// A simulated SX126x or SX127x, released with [`pamoja_lora_sim_chip_free`].
98pub struct PamojaLoraSimChip {
99    chip: Chip,
100}
101
102/// What a simulated chip is tuned to.
103#[repr(C)]
104#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
105pub struct PamojaLoraTuning {
106    /// The carrier frequency in hertz, as the chip's synthesizer steps it: within a hertz of
107    /// the one asked for on an SX126x, and within 61 Hz on an SX127x.
108    pub frequency_hz: u32,
109    /// The spreading factor, bandwidth, coding rate, preamble, header, and CRC.
110    pub link: PamojaLoraLink,
111    /// The output power the amplifier was asked for, in dBm.
112    pub output_dbm: i8,
113    /// The sync word byte.
114    pub sync_word: u8,
115}
116
117/// How a module wires its SX126x.
118///
119/// The SPI interface cannot see the parts around the chip, so these come from the module's
120/// schematic or its maker's example code.
121#[repr(C)]
122#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
123pub struct PamojaSx126xBoard {
124    /// `true` for the high power amplifier of the SX1262, SX1268, and LLCC68, `false` for the
125    /// SX1261's low power amplifier.
126    pub high_power: bool,
127    /// `true` when a TCXO powered from DIO3 clocks the chip instead of a crystal.
128    pub tcxo: bool,
129    /// The TCXO supply voltage as SetDIO3AsTCXOCtrl takes it: 0 for 1.6 V, 1 for 1.7 V, 2 for
130    /// 1.8 V, 3 for 2.2 V, 4 for 2.4 V, 5 for 2.7 V, 6 for 3.0 V, and 7 for 3.3 V.
131    pub tcxo_voltage: u8,
132    /// `true` when DIO2 drives the antenna switch.
133    pub dio2_rf_switch: bool,
134    /// `true` when the module fits the inductor the DC-DC regulator needs.
135    pub dc_dc: bool,
136    /// `true` for an LLCC68, which is held to the rates it supports.
137    pub llcc68: bool,
138    /// How long the TCXO takes to settle, in microseconds.
139    pub tcxo_settle_us: u32,
140}
141
142/// What a radio of either family sends and listens with.
143#[repr(C)]
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub struct PamojaLoraRadioConfig {
146    /// The carrier frequency in hertz.
147    pub frequency_hz: u32,
148    /// The lower edge of the band an SX126x calibrates its receiver for, in hertz, or 0 with
149    /// `band_high_hz` 0 to calibrate for the carrier alone.
150    pub band_low_hz: u32,
151    /// The upper edge of that band in hertz.
152    pub band_high_hz: u32,
153    /// The spreading factor, bandwidth, coding rate, preamble, header, and CRC.
154    pub link: PamojaLoraLink,
155    /// The output power asked of the amplifier, in dBm, clamped to its range.
156    pub output_dbm: i8,
157    /// The sync word byte: [`PAMOJA_LORA_RADIO_SYNC_WORD_PUBLIC`],
158    /// [`PAMOJA_LORA_RADIO_SYNC_WORD_PRIVATE`], or another value.
159    pub sync_word: u8,
160    /// `true` to send frames with inverted IQ, as a LoRaWAN gateway sends downlinks.
161    pub invert_iq_transmit: bool,
162    /// `true` to expect frames with inverted IQ, as a LoRaWAN device hears downlinks.
163    pub invert_iq_receive: bool,
164}
165
166/// How a reception ended, with the frame's signal levels when one arrived.
167#[repr(C)]
168#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
169pub struct PamojaLoraRadioReception {
170    /// [`PAMOJA_LORA_RADIO_FRAME`], [`PAMOJA_LORA_RADIO_TIMEOUT`],
171    /// [`PAMOJA_LORA_RADIO_CORRUPT`], or [`PAMOJA_LORA_RADIO_NOTHING`].
172    pub outcome: u8,
173    /// The payload length, at the start of the buffer, for a frame; 0 otherwise.
174    pub len: usize,
175    /// The received signal strength averaged over the frame, in hundredths of a dBm.
176    pub rssi_centi_dbm: i32,
177    /// The estimated signal-to-noise ratio, in hundredths of a dB.
178    pub snr_centi_db: i32,
179    /// The estimated strength of the LoRa signal itself, in hundredths of a dBm.
180    pub signal_rssi_centi_dbm: i32,
181}
182
183/// Returns a configuration with a private sync word, standard IQ both ways, and an SX126x
184/// calibrating for the carrier alone.
185///
186/// # Arguments
187///
188/// * `frequency_hz` - the carrier frequency in hertz.
189/// * `link` - the LoRa link settings.
190/// * `output_dbm` - the output power asked of the amplifier, in dBm.
191///
192/// # Returns
193///
194/// The configuration.
195#[no_mangle]
196pub extern "C" fn pamoja_lora_radio_config_default(
197    frequency_hz: u32,
198    link: PamojaLoraLink,
199    output_dbm: i8,
200) -> PamojaLoraRadioConfig {
201    PamojaLoraRadioConfig {
202        frequency_hz,
203        band_low_hz: 0,
204        band_high_hz: 0,
205        link,
206        output_dbm,
207        sync_word: PAMOJA_LORA_RADIO_SYNC_WORD_PRIVATE,
208        invert_iq_transmit: false,
209        invert_iq_receive: false,
210    }
211}
212
213/// Opens an SX1261, SX1262, SX1268, or LLCC68 module on a Linux board and resets it.
214///
215/// # Arguments
216///
217/// * `spi` - the SPI device file, such as `/dev/spidev0.0`.
218/// * `spi_hz` - the SPI clock in hertz, or 0 for [`PAMOJA_LORA_RADIO_DEFAULT_SPI_HZ`].
219/// * `gpio_chip` - the GPIO chip the lines are on, such as `/dev/gpiochip0`.
220/// * `busy_line` - the line the BUSY pin is on.
221/// * `reset_line` - the line the reset pin is on.
222/// * `board` - how the module wires the chip.
223/// * `out_radio` - receives the radio, or null when opening fails.
224///
225/// # Returns
226///
227/// [`PamojaStatus::Ok`] with the radio in `out_radio`; [`PamojaStatus::Unsupported`] on any
228/// platform but Linux; [`PamojaStatus::InvalidArgument`] for a null or non-UTF-8 argument or a
229/// TCXO voltage past 7; or [`PamojaStatus::Io`] when a device cannot be opened or no chip
230/// answers. The last error message says which.
231///
232/// # Safety
233///
234/// `spi` and `gpio_chip` must be null-terminated strings or null, and `out_radio` a writable
235/// pointer or null.
236#[no_mangle]
237pub unsafe extern "C" fn pamoja_lora_radio_open_sx126x(
238    spi: *const c_char,
239    spi_hz: u32,
240    gpio_chip: *const c_char,
241    busy_line: u32,
242    reset_line: u32,
243    board: PamojaSx126xBoard,
244    out_radio: *mut *mut PamojaLoraRadio,
245) -> PamojaStatus {
246    if !clear(out_radio) {
247        return PamojaStatus::InvalidArgument;
248    }
249    let Some(chip) = sx126x_board(board) else {
250        set_last_error(format!(
251            "TCXO voltage code {} is not one of 0 to 7",
252            board.tcxo_voltage
253        ));
254        return PamojaStatus::InvalidArgument;
255    };
256    let Some(wiring) = wiring(spi, spi_hz, gpio_chip, reset_line) else {
257        return PamojaStatus::InvalidArgument;
258    };
259    open(out_radio, || {
260        linux::open_sx126x(&wiring.with_busy_line(busy_line), chip)
261    })
262}
263
264/// Opens an SX1276, SX1277, SX1278, or SX1279 module on a Linux board, such as an RFM95W,
265/// and resets it into LoRa mode.
266///
267/// # Arguments
268///
269/// * `spi` - the SPI device file, such as `/dev/spidev0.0`.
270/// * `spi_hz` - the SPI clock in hertz, or 0 for [`PAMOJA_LORA_RADIO_DEFAULT_SPI_HZ`].
271/// * `gpio_chip` - the GPIO chip the reset line is on, such as `/dev/gpiochip0`.
272/// * `reset_line` - the line the reset pin is on.
273/// * `pa_boost` - `true` when the antenna is on the PA_BOOST output, as on the RFM95W, and
274///   `false` for RFO.
275/// * `tcxo` - `true` when a TCXO drives the XTA pin instead of a crystal.
276/// * `out_radio` - receives the radio, or null when opening fails.
277///
278/// # Returns
279///
280/// [`PamojaStatus::Ok`] with the radio in `out_radio`; [`PamojaStatus::Unsupported`] on any
281/// platform but Linux; [`PamojaStatus::InvalidArgument`] for a null or non-UTF-8 argument; or
282/// [`PamojaStatus::Io`] when a device cannot be opened or no chip answers. The last error
283/// message says which.
284///
285/// # Safety
286///
287/// `spi` and `gpio_chip` must be null-terminated strings or null, and `out_radio` a writable
288/// pointer or null.
289#[no_mangle]
290pub unsafe extern "C" fn pamoja_lora_radio_open_sx127x(
291    spi: *const c_char,
292    spi_hz: u32,
293    gpio_chip: *const c_char,
294    reset_line: u32,
295    pa_boost: bool,
296    tcxo: bool,
297    out_radio: *mut *mut PamojaLoraRadio,
298) -> PamojaStatus {
299    if !clear(out_radio) {
300        return PamojaStatus::InvalidArgument;
301    }
302    let Some(wiring) = wiring(spi, spi_hz, gpio_chip, reset_line) else {
303        return PamojaStatus::InvalidArgument;
304    };
305    let output = if pa_boost {
306        PaOutput::PaBoost
307    } else {
308        PaOutput::Rfo
309    };
310    let mut board = sx127x::Board::new(output);
311    if tcxo {
312        board = board.with_tcxo();
313    }
314    open(out_radio, || linux::open_sx127x(&wiring, board))
315}
316
317/// Returns the family of a radio's chip.
318///
319/// # Arguments
320///
321/// * `radio` - the radio.
322///
323/// # Returns
324///
325/// [`PAMOJA_LORA_RADIO_SX126X`] or [`PAMOJA_LORA_RADIO_SX127X`], or 255 if `radio` is null.
326///
327/// # Safety
328///
329/// `radio` must be a live handle from one of the open functions, or null.
330#[no_mangle]
331pub unsafe extern "C" fn pamoja_lora_radio_family(radio: *const PamojaLoraRadio) -> u8 {
332    if radio.is_null() {
333        return u8::MAX;
334    }
335    match (*radio).radio.family() {
336        Family::Sx126x => PAMOJA_LORA_RADIO_SX126X,
337        Family::Sx127x => PAMOJA_LORA_RADIO_SX127X,
338    }
339}
340
341/// Tunes a radio to a configuration.
342///
343/// # Arguments
344///
345/// * `radio` - the radio.
346/// * `config` - the configuration, such as one from [`pamoja_lora_radio_config_default`].
347///
348/// # Returns
349///
350/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null radio or settings the
351/// chip cannot use, such as a bandwidth it lacks; or [`PamojaStatus::Io`] when the chip does
352/// not answer.
353///
354/// # Safety
355///
356/// `radio` must be a live handle from one of the open functions, or null.
357#[no_mangle]
358pub unsafe extern "C" fn pamoja_lora_radio_configure(
359    radio: *mut PamojaLoraRadio,
360    config: PamojaLoraRadioConfig,
361) -> PamojaStatus {
362    status(drive(
363        radio,
364        |held| on_radio!(held, radio => radio.configure(config_of(config))),
365    ))
366}
367
368/// Sends one frame and waits for it to leave.
369///
370/// # Arguments
371///
372/// * `radio` - the radio.
373/// * `payload` - the payload bytes, 1 to 255 of them.
374/// * `len` - the payload length.
375/// * `out_airtime_us` - receives the frame's airtime in microseconds, or null.
376///
377/// # Returns
378///
379/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null radio, a null payload
380/// with a length, a payload the chip cannot send, or a radio not yet configured; or
381/// [`PamojaStatus::Io`] when the chip does not report the frame sent.
382///
383/// # Safety
384///
385/// `radio` must be a live handle or null, `payload` must point at `len` readable bytes or be
386/// null, and `out_airtime_us` must be writable or null.
387#[no_mangle]
388pub unsafe extern "C" fn pamoja_lora_radio_transmit(
389    radio: *mut PamojaLoraRadio,
390    payload: *const u8,
391    len: usize,
392    out_airtime_us: *mut u64,
393) -> PamojaStatus {
394    let payload = if len == 0 {
395        &[][..]
396    } else if payload.is_null() {
397        set_last_error("payload must not be null when its length is non-zero".to_owned());
398        return PamojaStatus::InvalidArgument;
399    } else {
400        std::slice::from_raw_parts(payload, len)
401    };
402    match drive(
403        radio,
404        |held| on_radio!(held, radio => radio.transmit(payload)),
405    ) {
406        Ok(airtime_us) => {
407            if !out_airtime_us.is_null() {
408                *out_airtime_us = airtime_us;
409            }
410            PamojaStatus::Ok
411        }
412        Err(status) => status,
413    }
414}
415
416/// Listens for one frame.
417///
418/// # Arguments
419///
420/// * `radio` - the radio.
421/// * `buffer` - where the payload goes.
422/// * `capacity` - the buffer's length; up to 255 bytes are accepted.
423/// * `timeout_us` - how long to listen for a frame to start, in microseconds.
424/// * `out_reception` - receives how the reception ended.
425///
426/// # Returns
427///
428/// [`PamojaStatus::Ok`] with the outcome in `out_reception`, a timeout and a corrupt frame
429/// included; [`PamojaStatus::InvalidArgument`] for a null argument, a payload longer than
430/// `capacity`, or a radio not yet configured; or [`PamojaStatus::Io`] when the chip does not
431/// answer.
432///
433/// # Safety
434///
435/// `radio` must be a live handle or null, `buffer` must point at `capacity` writable bytes or
436/// be null, and `out_reception` must be writable or null.
437#[no_mangle]
438pub unsafe extern "C" fn pamoja_lora_radio_receive(
439    radio: *mut PamojaLoraRadio,
440    buffer: *mut u8,
441    capacity: usize,
442    timeout_us: u64,
443    out_reception: *mut PamojaLoraRadioReception,
444) -> PamojaStatus {
445    let Some(buffer) = buffer_of(buffer, capacity) else {
446        return PamojaStatus::InvalidArgument;
447    };
448    if out_reception.is_null() {
449        set_last_error("out_reception must not be null".to_owned());
450        return PamojaStatus::InvalidArgument;
451    }
452    match drive(
453        radio,
454        |held| on_radio!(held, radio => radio.receive(buffer, timeout_us)),
455    ) {
456        Ok(reception) => {
457            *out_reception = reception_of(Some(reception));
458            PamojaStatus::Ok
459        }
460        Err(status) => status,
461    }
462}
463
464/// Starts listening, frame after frame, until another call changes the mode.
465///
466/// # Arguments
467///
468/// * `radio` - the radio.
469///
470/// # Returns
471///
472/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null radio or one not yet
473/// configured; or [`PamojaStatus::Io`] when the chip does not answer.
474///
475/// # Safety
476///
477/// `radio` must be a live handle from one of the open functions, or null.
478#[no_mangle]
479pub unsafe extern "C" fn pamoja_lora_radio_listen(radio: *mut PamojaLoraRadio) -> PamojaStatus {
480    status(drive(
481        radio,
482        |held| on_radio!(held, radio => radio.listen()),
483    ))
484}
485
486/// Takes the frame a listening radio has received, if one has arrived.
487///
488/// # Arguments
489///
490/// * `radio` - the radio.
491/// * `buffer` - where the payload goes.
492/// * `capacity` - the buffer's length.
493/// * `out_reception` - receives the frame, a corrupt frame, or
494///   [`PAMOJA_LORA_RADIO_NOTHING`].
495///
496/// # Returns
497///
498/// [`PamojaStatus::Ok`] with the outcome in `out_reception`; [`PamojaStatus::InvalidArgument`]
499/// for a null argument or a payload longer than `capacity`; or [`PamojaStatus::Io`] when the
500/// chip does not answer.
501///
502/// # Safety
503///
504/// `radio` must be a live handle or null, `buffer` must point at `capacity` writable bytes or
505/// be null, and `out_reception` must be writable or null.
506#[no_mangle]
507pub unsafe extern "C" fn pamoja_lora_radio_take_frame(
508    radio: *mut PamojaLoraRadio,
509    buffer: *mut u8,
510    capacity: usize,
511    out_reception: *mut PamojaLoraRadioReception,
512) -> PamojaStatus {
513    let Some(buffer) = buffer_of(buffer, capacity) else {
514        return PamojaStatus::InvalidArgument;
515    };
516    if out_reception.is_null() {
517        set_last_error("out_reception must not be null".to_owned());
518        return PamojaStatus::InvalidArgument;
519    }
520    match drive(
521        radio,
522        |held| on_radio!(held, radio => radio.take_frame(buffer)),
523    ) {
524        Ok(taken) => {
525            *out_reception = reception_of(taken);
526            PamojaStatus::Ok
527        }
528        Err(status) => status,
529    }
530}
531
532/// Listens a few symbols for a LoRa preamble and reports whether one is there, as a relay's
533/// scan does, leaving the chip in standby.
534///
535/// # Arguments
536///
537/// * `radio` - the radio.
538/// * `symbols` - how many symbols an SX126x listens over: 1, 2, 4, 8, or 16, rounded down to
539///   one of them. An SX127x listens over one.
540/// * `out_detected` - receives whether a preamble was there.
541///
542/// # Returns
543///
544/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null argument or a radio not
545/// yet configured; or [`PamojaStatus::Io`] when the chip does not answer.
546///
547/// # Safety
548///
549/// `radio` must be a live handle or null, and `out_detected` writable or null.
550#[no_mangle]
551pub unsafe extern "C" fn pamoja_lora_radio_detect(
552    radio: *mut PamojaLoraRadio,
553    symbols: u8,
554    out_detected: *mut bool,
555) -> PamojaStatus {
556    if out_detected.is_null() {
557        set_last_error("out_detected must not be null".to_owned());
558        return PamojaStatus::InvalidArgument;
559    }
560    match drive(
561        radio,
562        |held| on_radio!(held, radio => radio.detect(symbols)),
563    ) {
564        Ok(detected) => {
565            *out_detected = detected;
566            PamojaStatus::Ok
567        }
568        Err(status) => status,
569    }
570}
571/// Puts a radio in standby, which stops a transmission or a reception.
572///
573/// # Arguments
574///
575/// * `radio` - the radio.
576///
577/// # Returns
578///
579/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null radio; or
580/// [`PamojaStatus::Io`] when the chip does not answer.
581///
582/// # Safety
583///
584/// `radio` must be a live handle from one of the open functions, or null.
585#[no_mangle]
586pub unsafe extern "C" fn pamoja_lora_radio_standby(radio: *mut PamojaLoraRadio) -> PamojaStatus {
587    status(drive(
588        radio,
589        |held| on_radio!(held, radio => radio.standby()),
590    ))
591}
592
593/// Puts a radio to sleep until the next call wakes it. An SX126x is configured again before
594/// its next frame; an SX127x keeps its registers.
595///
596/// # Arguments
597///
598/// * `radio` - the radio.
599///
600/// # Returns
601///
602/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null radio; or
603/// [`PamojaStatus::Io`] when the chip does not answer.
604///
605/// # Safety
606///
607/// `radio` must be a live handle from one of the open functions, or null.
608#[no_mangle]
609pub unsafe extern "C" fn pamoja_lora_radio_sleep(radio: *mut PamojaLoraRadio) -> PamojaStatus {
610    status(drive(radio, |held| on_radio!(held, radio => radio.sleep())))
611}
612
613/// Draws a random number from the noise a radio's receiver hears, leaving the chip in standby.
614///
615/// Semtech's own drivers draw it the same way, and LoRaWAN 1.0.3 suggests this source for a
616/// join nonce on a device that has no other.
617///
618/// # Arguments
619///
620/// * `radio` - the radio.
621/// * `out_value` - receives thirty-two bits of noise.
622///
623/// # Returns
624///
625/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null argument; or
626/// [`PamojaStatus::Io`] when the chip does not answer.
627///
628/// # Safety
629///
630/// `radio` must be a live handle or null, and `out_value` writable or null.
631#[no_mangle]
632pub unsafe extern "C" fn pamoja_lora_radio_random(
633    radio: *mut PamojaLoraRadio,
634    out_value: *mut u32,
635) -> PamojaStatus {
636    if out_value.is_null() {
637        set_last_error("out_value must not be null".to_owned());
638        return PamojaStatus::InvalidArgument;
639    }
640    match drive(radio, |held| on_radio!(held, radio => radio.random())) {
641        Ok(value) => {
642            *out_value = value;
643            PamojaStatus::Ok
644        }
645        Err(status) => status,
646    }
647}
648
649/// Reads one register of a radio's chip.
650///
651/// # Arguments
652///
653/// * `radio` - the radio.
654/// * `address` - the register address: 16 bits on the SX126x, 0x00 to 0x7F on the SX127x.
655/// * `out_value` - receives the value.
656///
657/// # Returns
658///
659/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null argument or an SX127x
660/// address past 0x7F; or [`PamojaStatus::Io`] when the chip does not answer.
661///
662/// # Safety
663///
664/// `radio` must be a live handle or null, and `out_value` writable or null.
665#[no_mangle]
666pub unsafe extern "C" fn pamoja_lora_radio_read_register(
667    radio: *mut PamojaLoraRadio,
668    address: u16,
669    out_value: *mut u8,
670) -> PamojaStatus {
671    if out_value.is_null() {
672        set_last_error("out_value must not be null".to_owned());
673        return PamojaStatus::InvalidArgument;
674    }
675    match drive(
676        radio,
677        |held| on_radio!(held, radio => radio.read_register(address)),
678    ) {
679        Ok(value) => {
680            *out_value = value;
681            PamojaStatus::Ok
682        }
683        Err(status) => status,
684    }
685}
686
687/// Writes one register of a radio's chip.
688///
689/// # Arguments
690///
691/// * `radio` - the radio.
692/// * `address` - the register address: 16 bits on the SX126x, 0x00 to 0x7F on the SX127x.
693/// * `value` - the value to write.
694///
695/// # Returns
696///
697/// [`PamojaStatus::Ok`]; [`PamojaStatus::InvalidArgument`] for a null radio or an SX127x
698/// address past 0x7F; or [`PamojaStatus::Io`] when the chip does not answer.
699///
700/// # Safety
701///
702/// `radio` must be a live handle from one of the open functions, or null.
703#[no_mangle]
704pub unsafe extern "C" fn pamoja_lora_radio_write_register(
705    radio: *mut PamojaLoraRadio,
706    address: u16,
707    value: u8,
708) -> PamojaStatus {
709    status(drive(
710        radio,
711        |held| on_radio!(held, radio => radio.write_register(address, value)),
712    ))
713}
714
715/// Closes a radio's device files and releases it.
716///
717/// # Arguments
718///
719/// * `radio` - the radio, which must not be used again.
720///
721/// # Safety
722///
723/// `radio` must be a live handle from one of the open functions, or null.
724#[no_mangle]
725pub unsafe extern "C" fn pamoja_lora_radio_free(radio: *mut PamojaLoraRadio) {
726    if !radio.is_null() {
727        drop(Box::from_raw(radio));
728    }
729}
730
731/// Creates a simulated SX1261, SX1262, SX1268, or LLCC68, out of reset.
732///
733/// # Arguments
734///
735/// * `board` - how the module wires the chip, which the radio
736///   [`pamoja_lora_sim_chip_radio`] gives is told.
737/// * `out_chip` - receives the chip.
738///
739/// # Returns
740///
741/// [`PamojaStatus::Ok`] with the chip in `out_chip`, or [`PamojaStatus::InvalidArgument`] for
742/// a null `out_chip` or a TCXO voltage past 7.
743///
744/// # Safety
745///
746/// `out_chip` must be a writable pointer or null.
747#[no_mangle]
748pub unsafe extern "C" fn pamoja_lora_sim_chip_sx126x(
749    board: PamojaSx126xBoard,
750    out_chip: *mut *mut PamojaLoraSimChip,
751) -> PamojaStatus {
752    if out_chip.is_null() {
753        set_last_error("out_chip must not be null".to_owned());
754        return PamojaStatus::InvalidArgument;
755    }
756    *out_chip = std::ptr::null_mut();
757    let Some(chip) = sx126x_board(board) else {
758        set_last_error(format!(
759            "TCXO voltage code {} is not one of 0 to 7",
760            board.tcxo_voltage
761        ));
762        return PamojaStatus::InvalidArgument;
763    };
764    *out_chip = Box::into_raw(Box::new(PamojaLoraSimChip {
765        chip: Chip::sx126x(chip),
766    }));
767    PamojaStatus::Ok
768}
769
770/// Creates a simulated SX1276, SX1277, SX1278, or SX1279, out of reset.
771///
772/// # Arguments
773///
774/// * `pa_boost` - `true` when the antenna is on the PA_BOOST output, as on the RFM95W, and
775///   `false` for RFO.
776/// * `tcxo` - `true` when a TCXO drives the XTA pin instead of a crystal.
777/// * `out_chip` - receives the chip.
778///
779/// # Returns
780///
781/// [`PamojaStatus::Ok`] with the chip in `out_chip`, or [`PamojaStatus::InvalidArgument`] for
782/// a null `out_chip`.
783///
784/// # Safety
785///
786/// `out_chip` must be a writable pointer or null.
787#[no_mangle]
788pub unsafe extern "C" fn pamoja_lora_sim_chip_sx127x(
789    pa_boost: bool,
790    tcxo: bool,
791    out_chip: *mut *mut PamojaLoraSimChip,
792) -> PamojaStatus {
793    if out_chip.is_null() {
794        set_last_error("out_chip must not be null".to_owned());
795        return PamojaStatus::InvalidArgument;
796    }
797    let output = if pa_boost {
798        PaOutput::PaBoost
799    } else {
800        PaOutput::Rfo
801    };
802    let mut board = sx127x::Board::new(output);
803    if tcxo {
804        board = board.with_tcxo();
805    }
806    *out_chip = Box::into_raw(Box::new(PamojaLoraSimChip {
807        chip: Chip::sx127x(board),
808    }));
809    PamojaStatus::Ok
810}
811
812/// Returns the family of a simulated chip.
813///
814/// # Arguments
815///
816/// * `chip` - the chip.
817///
818/// # Returns
819///
820/// [`PAMOJA_LORA_RADIO_SX126X`] or [`PAMOJA_LORA_RADIO_SX127X`], or 255 if `chip` is null.
821///
822/// # Safety
823///
824/// `chip` must be a live handle or null.
825#[no_mangle]
826pub unsafe extern "C" fn pamoja_lora_sim_chip_family(chip: *const PamojaLoraSimChip) -> u8 {
827    match chip.as_ref().map(|chip| chip.chip.family()) {
828        Some(Family::Sx126x) => PAMOJA_LORA_RADIO_SX126X,
829        Some(Family::Sx127x) => PAMOJA_LORA_RADIO_SX127X,
830        None => u8::MAX,
831    }
832}
833
834/// Wires a radio to a simulated chip and resets it, as opening a module does.
835///
836/// # Arguments
837///
838/// * `chip` - the chip.
839/// * `out_radio` - receives the radio, released with [`pamoja_lora_radio_free`].
840///
841/// # Returns
842///
843/// [`PamojaStatus::Ok`] with the radio in `out_radio`, or [`PamojaStatus::InvalidArgument`]
844/// for a null argument.
845///
846/// # Safety
847///
848/// `chip` must be a live handle or null, and `out_radio` a writable pointer or null.
849#[no_mangle]
850pub unsafe extern "C" fn pamoja_lora_sim_chip_radio(
851    chip: *const PamojaLoraSimChip,
852    out_radio: *mut *mut PamojaLoraRadio,
853) -> PamojaStatus {
854    if !clear(out_radio) {
855        return PamojaStatus::InvalidArgument;
856    }
857    let Some(chip) = chip.as_ref() else {
858        set_last_error("chip must not be null".to_owned());
859        return PamojaStatus::InvalidArgument;
860    };
861    let mut radio = chip.chip.radio();
862    if let Err(error) = radio.init() {
863        let refused = refusal(error);
864        set_last_error(refused.message);
865        return refused.status;
866    }
867    *out_radio = Box::into_raw(Box::new(PamojaLoraRadio {
868        radio: Held::Simulated(radio),
869    }));
870    PamojaStatus::Ok
871}
872
873/// Puts a frame on the air for a simulated chip to receive the next time it listens.
874///
875/// # Arguments
876///
877/// * `chip` - the chip.
878/// * `payload` - the frame's payload; past 255 bytes it is cut to 255.
879/// * `len` - the payload length.
880/// * `rssi_centi_dbm` - the strength the chip hears the frame at, in hundredths of a dBm.
881/// * `snr_centi_db` - the signal-to-noise ratio it hears it with, in hundredths of a dB.
882///
883/// # Returns
884///
885/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null chip or a null
886/// payload with a length.
887///
888/// # Safety
889///
890/// `chip` must be a live handle or null, and `payload` must point at `len` readable bytes or
891/// be null.
892#[no_mangle]
893pub unsafe extern "C" fn pamoja_lora_sim_chip_hear(
894    chip: *const PamojaLoraSimChip,
895    payload: *const u8,
896    len: usize,
897    rssi_centi_dbm: i32,
898    snr_centi_db: i32,
899) -> PamojaStatus {
900    let Some(chip) = chip.as_ref() else {
901        set_last_error("chip must not be null".to_owned());
902        return PamojaStatus::InvalidArgument;
903    };
904    let payload = if len == 0 {
905        &[][..]
906    } else if payload.is_null() {
907        set_last_error("payload must not be null when its length is non-zero".to_owned());
908        return PamojaStatus::InvalidArgument;
909    } else {
910        std::slice::from_raw_parts(payload, len)
911    };
912    chip.chip.hear(
913        payload,
914        Decibels::from_hundredths(rssi_centi_dbm),
915        Decibels::from_hundredths(snr_centi_db),
916    );
917    PamojaStatus::Ok
918}
919
920/// Puts a frame on the air whose CRC fails, which the chip reports and drops.
921///
922/// # Arguments
923///
924/// * `chip` - the chip.
925/// * `rssi_centi_dbm` - the strength the chip hears the frame at, in hundredths of a dBm.
926/// * `snr_centi_db` - the signal-to-noise ratio it hears it with, in hundredths of a dB.
927///
928/// # Returns
929///
930/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null chip.
931///
932/// # Safety
933///
934/// `chip` must be a live handle or null.
935#[no_mangle]
936pub unsafe extern "C" fn pamoja_lora_sim_chip_hear_corrupt(
937    chip: *const PamojaLoraSimChip,
938    rssi_centi_dbm: i32,
939    snr_centi_db: i32,
940) -> PamojaStatus {
941    let Some(chip) = chip.as_ref() else {
942        set_last_error("chip must not be null".to_owned());
943        return PamojaStatus::InvalidArgument;
944    };
945    chip.chip.hear_corrupt(
946        Decibels::from_hundredths(rssi_centi_dbm),
947        Decibels::from_hundredths(snr_centi_db),
948    );
949    PamojaStatus::Ok
950}
951
952/// Returns how many frames wait on the air for a simulated chip to receive.
953///
954/// # Arguments
955///
956/// * `chip` - the chip.
957///
958/// # Returns
959///
960/// The number of frames heard and not yet received, or 0 if `chip` is null.
961///
962/// # Safety
963///
964/// `chip` must be a live handle or null.
965#[no_mangle]
966pub unsafe extern "C" fn pamoja_lora_sim_chip_waiting(chip: *const PamojaLoraSimChip) -> usize {
967    chip.as_ref().map_or(0, |chip| chip.chip.waiting())
968}
969
970/// Reads what a simulated chip is tuned to now.
971///
972/// # Arguments
973///
974/// * `chip` - the chip.
975/// * `out_tuning` - receives the tuning.
976///
977/// # Returns
978///
979/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
980///
981/// # Safety
982///
983/// `chip` must be a live handle or null, and `out_tuning` writable or null.
984#[no_mangle]
985pub unsafe extern "C" fn pamoja_lora_sim_chip_tuning(
986    chip: *const PamojaLoraSimChip,
987    out_tuning: *mut PamojaLoraTuning,
988) -> PamojaStatus {
989    let (Some(chip), false) = (chip.as_ref(), out_tuning.is_null()) else {
990        set_last_error("chip and out_tuning must not be null".to_owned());
991        return PamojaStatus::InvalidArgument;
992    };
993    *out_tuning = tuning_of(chip.chip.tuning());
994    PamojaStatus::Ok
995}
996
997/// Returns how many frames a simulated chip has put on the air.
998///
999/// # Arguments
1000///
1001/// * `chip` - the chip.
1002///
1003/// # Returns
1004///
1005/// The number of frames sent, or 0 if `chip` is null.
1006///
1007/// # Safety
1008///
1009/// `chip` must be a live handle or null.
1010#[no_mangle]
1011pub unsafe extern "C" fn pamoja_lora_sim_chip_sent_count(chip: *const PamojaLoraSimChip) -> usize {
1012    chip.as_ref().map_or(0, |chip| chip.chip.sent().len())
1013}
1014
1015/// Reads what a simulated chip was tuned to when it sent a frame.
1016///
1017/// # Arguments
1018///
1019/// * `chip` - the chip.
1020/// * `index` - which frame, the oldest first.
1021/// * `out_tuning` - receives the tuning.
1022///
1023/// # Returns
1024///
1025/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument or an index
1026/// past the frames sent.
1027///
1028/// # Safety
1029///
1030/// `chip` must be a live handle or null, and `out_tuning` writable or null.
1031#[no_mangle]
1032pub unsafe extern "C" fn pamoja_lora_sim_chip_sent_tuning(
1033    chip: *const PamojaLoraSimChip,
1034    index: usize,
1035    out_tuning: *mut PamojaLoraTuning,
1036) -> PamojaStatus {
1037    let (Some(chip), false) = (chip.as_ref(), out_tuning.is_null()) else {
1038        set_last_error("chip and out_tuning must not be null".to_owned());
1039        return PamojaStatus::InvalidArgument;
1040    };
1041    match chip.chip.sent().get(index) {
1042        Some(sent) => {
1043            *out_tuning = tuning_of(sent.tuning);
1044            PamojaStatus::Ok
1045        }
1046        None => {
1047            set_last_error(format!("the chip has sent no frame {index}"));
1048            PamojaStatus::InvalidArgument
1049        }
1050    }
1051}
1052
1053/// Copies the payload of a frame a simulated chip sent.
1054///
1055/// # Arguments
1056///
1057/// * `chip` - the chip.
1058/// * `index` - which frame, the oldest first.
1059///
1060/// # Returns
1061///
1062/// The payload, released with [`pamoja_buffer_free`](crate::pamoja_buffer_free), or null for
1063/// a null chip or an index past the frames sent.
1064///
1065/// # Safety
1066///
1067/// `chip` must be a live handle or null.
1068#[no_mangle]
1069pub unsafe extern "C" fn pamoja_lora_sim_chip_sent_payload(
1070    chip: *const PamojaLoraSimChip,
1071    index: usize,
1072) -> *mut crate::PamojaBuffer {
1073    match chip
1074        .as_ref()
1075        .and_then(|chip| chip.chip.sent().get(index).cloned())
1076    {
1077        Some(sent) => crate::PamojaBuffer::into_raw(sent.payload),
1078        None => std::ptr::null_mut(),
1079    }
1080}
1081
1082/// Releases a simulated chip. A radio wired to it keeps working until it is released too.
1083///
1084/// # Arguments
1085///
1086/// * `chip` - the chip, which must not be used again.
1087///
1088/// # Safety
1089///
1090/// `chip` must be a live handle or null.
1091#[no_mangle]
1092pub unsafe extern "C" fn pamoja_lora_sim_chip_free(chip: *mut PamojaLoraSimChip) {
1093    if !chip.is_null() {
1094        drop(Box::from_raw(chip));
1095    }
1096}
1097
1098/// Flattens a tuning for the boundary.
1099fn tuning_of(tuning: Tuning) -> PamojaLoraTuning {
1100    PamojaLoraTuning {
1101        frequency_hz: tuning.frequency_hz,
1102        link: crate::lora::link_of(tuning.link),
1103        output_dbm: tuning.output_dbm,
1104        sync_word: tuning.sync_word.to_byte(),
1105    }
1106}
1107/// Nulls an out pointer before a radio is opened into it, refusing a null pointer.
1108unsafe fn clear(out_radio: *mut *mut PamojaLoraRadio) -> bool {
1109    if out_radio.is_null() {
1110        set_last_error("out_radio must not be null".to_owned());
1111        return false;
1112    }
1113    *out_radio = std::ptr::null_mut();
1114    true
1115}
1116
1117/// Reads the wiring the open functions share, recording an error for a bad string.
1118unsafe fn wiring(
1119    spi: *const c_char,
1120    spi_hz: u32,
1121    gpio_chip: *const c_char,
1122    reset_line: u32,
1123) -> Option<Wiring> {
1124    let spi = read_str(spi, "spi")?;
1125    let gpio_chip = read_str(gpio_chip, "gpio_chip")?;
1126    let wiring = Wiring::new(spi, gpio_chip, reset_line);
1127    Some(if spi_hz == 0 {
1128        wiring
1129    } else {
1130        wiring.with_spi_hz(spi_hz)
1131    })
1132}
1133
1134/// Opens a radio into an out pointer that [`clear`] has checked.
1135unsafe fn open(
1136    out_radio: *mut *mut PamojaLoraRadio,
1137    opening: impl FnOnce() -> Result<LinuxRadio, OpenError>,
1138) -> PamojaStatus {
1139    match catch_unwind(AssertUnwindSafe(opening)) {
1140        Ok(Ok(radio)) => {
1141            *out_radio = Box::into_raw(Box::new(PamojaLoraRadio {
1142                radio: Held::Linux(radio),
1143            }));
1144            PamojaStatus::Ok
1145        }
1146        Ok(Err(error)) => {
1147            set_last_error(error.to_string());
1148            match error {
1149                OpenError::Unsupported => PamojaStatus::Unsupported,
1150                OpenError::NoBusyLine => PamojaStatus::InvalidArgument,
1151                OpenError::Bus { .. } | OpenError::ResetLine { .. } | OpenError::Bridge { .. } => {
1152                    PamojaStatus::Io
1153                }
1154                OpenError::Radio(error) => radio_status(&error),
1155            }
1156        }
1157        Err(_) => panicked(),
1158    }
1159}
1160
1161/// Why a radio refused a call: its message, and the status that classifies it.
1162struct Refusal {
1163    message: String,
1164    status: PamojaStatus,
1165}
1166
1167/// Turns a radio's error into a refusal.
1168fn refusal<E: fmt::Debug>(error: RadioError<E>) -> Refusal {
1169    Refusal {
1170        message: error.to_string(),
1171        status: radio_status(&error),
1172    }
1173}
1174
1175/// Runs a call on a live radio, turning a refusal or a panic into a status.
1176unsafe fn drive<T>(
1177    radio: *mut PamojaLoraRadio,
1178    call: impl FnOnce(&mut Held) -> Result<T, Refusal>,
1179) -> Result<T, PamojaStatus> {
1180    if radio.is_null() {
1181        set_last_error("radio must not be null".to_owned());
1182        return Err(PamojaStatus::InvalidArgument);
1183    }
1184    let held = &mut (*radio).radio;
1185    match catch_unwind(AssertUnwindSafe(|| call(held))) {
1186        Ok(Ok(value)) => Ok(value),
1187        Ok(Err(refused)) => {
1188            set_last_error(refused.message);
1189            Err(refused.status)
1190        }
1191        Err(_) => Err(panicked()),
1192    }
1193}
1194
1195/// Folds a call that returns nothing into its status.
1196fn status(result: Result<(), PamojaStatus>) -> PamojaStatus {
1197    result.err().unwrap_or(PamojaStatus::Ok)
1198}
1199
1200/// Classifies a radio error: settings or a call the chip cannot take are the caller's to fix,
1201/// and everything else is the bus or the chip.
1202fn radio_status<E>(error: &RadioError<E>) -> PamojaStatus {
1203    match error {
1204        RadioError::Address(_)
1205        | RadioError::Sx126x(
1206            sx126x::RadioError::Bandwidth(_)
1207            | sx126x::RadioError::Llcc68 { .. }
1208            | sx126x::RadioError::Amplifier
1209            | sx126x::RadioError::PayloadTooLong(_)
1210            | sx126x::RadioError::BufferTooSmall(_)
1211            | sx126x::RadioError::NotConfigured,
1212        )
1213        | RadioError::Sx127x(
1214            sx127x::RadioError::Modulation(_)
1215            | sx127x::RadioError::Output
1216            | sx127x::RadioError::PayloadLength(_)
1217            | sx127x::RadioError::BufferTooSmall(_)
1218            | sx127x::RadioError::NotConfigured,
1219        ) => PamojaStatus::InvalidArgument,
1220        _ => PamojaStatus::Io,
1221    }
1222}
1223
1224/// Borrows a caller's buffer, recording an error for a null one with a capacity.
1225unsafe fn buffer_of<'a>(buffer: *mut u8, capacity: usize) -> Option<&'a mut [u8]> {
1226    if capacity == 0 {
1227        return Some(&mut []);
1228    }
1229    if buffer.is_null() {
1230        set_last_error("buffer must not be null when its capacity is non-zero".to_owned());
1231        return None;
1232    }
1233    Some(std::slice::from_raw_parts_mut(buffer, capacity))
1234}
1235
1236/// Reads a board description, or `None` for a TCXO voltage code past 7.
1237fn sx126x_board(board: PamojaSx126xBoard) -> Option<sx126x::Board> {
1238    let amplifier = if board.high_power {
1239        PowerAmplifier::HighPower
1240    } else {
1241        PowerAmplifier::LowPower
1242    };
1243    let mut chip = sx126x::Board::new(amplifier);
1244    if board.tcxo {
1245        chip = chip.with_tcxo(tcxo_voltage(board.tcxo_voltage)?, board.tcxo_settle_us);
1246    }
1247    if board.dio2_rf_switch {
1248        chip = chip.with_dio2_rf_switch();
1249    }
1250    if board.dc_dc {
1251        chip = chip.with_dc_dc();
1252    }
1253    if board.llcc68 {
1254        chip = chip.with_llcc68();
1255    }
1256    Some(chip)
1257}
1258
1259/// Names the TCXO voltage a SetDIO3AsTCXOCtrl code selects.
1260fn tcxo_voltage(code: u8) -> Option<TcxoVoltage> {
1261    Some(match code {
1262        0 => TcxoVoltage::V1_6,
1263        1 => TcxoVoltage::V1_7,
1264        2 => TcxoVoltage::V1_8,
1265        3 => TcxoVoltage::V2_2,
1266        4 => TcxoVoltage::V2_4,
1267        5 => TcxoVoltage::V2_7,
1268        6 => TcxoVoltage::V3_0,
1269        7 => TcxoVoltage::V3_3,
1270        _ => return None,
1271    })
1272}
1273
1274/// Reads a configuration from the boundary.
1275fn config_of(config: PamojaLoraRadioConfig) -> RadioConfig {
1276    let radio_config = RadioConfig::new(
1277        config.frequency_hz,
1278        settings(config.link),
1279        config.output_dbm,
1280    )
1281    .with_sync_word(SyncWord::from_byte(config.sync_word))
1282    .with_inverted_iq(config.invert_iq_transmit, config.invert_iq_receive);
1283    if config.band_low_hz == 0 && config.band_high_hz == 0 {
1284        radio_config
1285    } else {
1286        radio_config.with_band(config.band_low_hz, config.band_high_hz)
1287    }
1288}
1289
1290/// Flattens how a reception ended for the boundary.
1291fn reception_of(reception: Option<Reception>) -> PamojaLoraRadioReception {
1292    match reception {
1293        Some(Reception::Frame { len, levels }) => PamojaLoraRadioReception {
1294            outcome: PAMOJA_LORA_RADIO_FRAME,
1295            len,
1296            rssi_centi_dbm: levels.rssi_dbm.hundredths(),
1297            snr_centi_db: levels.snr_db.hundredths(),
1298            signal_rssi_centi_dbm: levels.signal_rssi_dbm.hundredths(),
1299        },
1300        Some(Reception::Timeout) => PamojaLoraRadioReception {
1301            outcome: PAMOJA_LORA_RADIO_TIMEOUT,
1302            ..PamojaLoraRadioReception::default()
1303        },
1304        Some(Reception::Corrupt) => PamojaLoraRadioReception {
1305            outcome: PAMOJA_LORA_RADIO_CORRUPT,
1306            ..PamojaLoraRadioReception::default()
1307        },
1308        None => PamojaLoraRadioReception {
1309            outcome: PAMOJA_LORA_RADIO_NOTHING,
1310            ..PamojaLoraRadioReception::default()
1311        },
1312    }
1313}
1314
1315/// Records a caught panic and reports it as [`PamojaStatus::Panic`].
1316fn panicked() -> PamojaStatus {
1317    set_last_error("panic at the FFI boundary".to_owned());
1318    PamojaStatus::Panic
1319}
1320
1321#[cfg(test)]
1322mod tests {
1323    use std::ffi::{CStr, CString};
1324    use std::ptr;
1325
1326    use pamoja_lora::budget::Decibels;
1327    use pamoja_radios::radio::SignalLevels;
1328
1329    use super::*;
1330    use crate::lora::pamoja_lora_link_default;
1331    use crate::pamoja_last_error_message;
1332
1333    fn last_error() -> String {
1334        unsafe { CStr::from_ptr(pamoja_last_error_message()) }
1335            .to_string_lossy()
1336            .into_owned()
1337    }
1338
1339    #[test]
1340    fn a_default_config_is_private_and_calibrates_for_the_carrier() {
1341        let link = pamoja_lora_link_default(9, 125_000);
1342        let config = pamoja_lora_radio_config_default(868_100_000, link, 14);
1343        assert_eq!(config.sync_word, PAMOJA_LORA_RADIO_SYNC_WORD_PRIVATE);
1344
1345        let plain = config_of(config);
1346        assert_eq!(plain.band_hz, (868_100_000, 868_100_000));
1347        assert_eq!(plain.sync_word, SyncWord::Private);
1348        assert_eq!(plain.output_dbm, 14);
1349
1350        let banded = config_of(PamojaLoraRadioConfig {
1351            band_low_hz: 863_000_000,
1352            band_high_hz: 870_000_000,
1353            sync_word: 0x2B,
1354            invert_iq_receive: true,
1355            ..config
1356        });
1357        assert_eq!(banded.band_hz, (863_000_000, 870_000_000));
1358        assert_eq!(banded.sync_word, SyncWord::Custom(0x2B));
1359        assert!(banded.invert_iq_receive && !banded.invert_iq_transmit);
1360    }
1361
1362    #[test]
1363    fn a_board_names_its_tcxo_voltage_by_code() {
1364        let board = PamojaSx126xBoard {
1365            high_power: true,
1366            tcxo: true,
1367            tcxo_voltage: 1,
1368            dio2_rf_switch: true,
1369            dc_dc: true,
1370            llcc68: false,
1371            tcxo_settle_us: 5_000,
1372        };
1373        let chip = sx126x_board(board).expect("1.7 V is a TCXO voltage");
1374        assert_eq!(chip.tcxo, Some((TcxoVoltage::V1_7, 5_000)));
1375        assert!(chip.dio2_rf_switch && !chip.llcc68);
1376        assert_eq!(chip.amplifier, PowerAmplifier::HighPower);
1377        assert!(sx126x_board(PamojaSx126xBoard {
1378            tcxo_voltage: 8,
1379            ..board
1380        })
1381        .is_none());
1382    }
1383
1384    #[test]
1385    fn a_reception_crosses_with_its_levels_in_hundredths() {
1386        let frame = reception_of(Some(Reception::Frame {
1387            len: 10,
1388            levels: SignalLevels {
1389                rssi_dbm: Decibels::from_db(-109),
1390                snr_db: Decibels::from_tenths(-25),
1391                signal_rssi_dbm: Decibels::from_tenths(-1115),
1392            },
1393        }));
1394        assert_eq!(frame.outcome, PAMOJA_LORA_RADIO_FRAME);
1395        assert_eq!(frame.len, 10);
1396        assert_eq!(
1397            (
1398                frame.rssi_centi_dbm,
1399                frame.snr_centi_db,
1400                frame.signal_rssi_centi_dbm
1401            ),
1402            (-10_900, -250, -11_150)
1403        );
1404        assert_eq!(
1405            reception_of(Some(Reception::Timeout)).outcome,
1406            PAMOJA_LORA_RADIO_TIMEOUT
1407        );
1408        assert_eq!(reception_of(None).outcome, PAMOJA_LORA_RADIO_NOTHING);
1409    }
1410
1411    #[test]
1412    fn a_null_radio_or_argument_is_refused() {
1413        unsafe {
1414            assert_eq!(
1415                pamoja_lora_radio_standby(ptr::null_mut()),
1416                PamojaStatus::InvalidArgument
1417            );
1418            let mut noise = 0;
1419            assert_eq!(
1420                pamoja_lora_radio_random(ptr::null_mut(), &mut noise),
1421                PamojaStatus::InvalidArgument
1422            );
1423            assert_eq!(
1424                pamoja_lora_radio_random(ptr::null_mut(), ptr::null_mut()),
1425                PamojaStatus::InvalidArgument
1426            );
1427            assert_eq!(pamoja_lora_radio_family(ptr::null()), u8::MAX);
1428            let spi = CString::new("/dev/spidev0.0").expect("no interior null");
1429            assert_eq!(
1430                pamoja_lora_radio_open_sx127x(
1431                    spi.as_ptr(),
1432                    0,
1433                    ptr::null(),
1434                    25,
1435                    true,
1436                    false,
1437                    &mut ptr::null_mut()
1438                ),
1439                PamojaStatus::InvalidArgument
1440            );
1441            assert!(last_error().contains("gpio_chip"), "{}", last_error());
1442            pamoja_lora_radio_free(ptr::null_mut());
1443        }
1444    }
1445
1446    #[cfg(target_os = "linux")]
1447    #[test]
1448    fn opening_a_missing_device_is_an_io_error_that_names_it() {
1449        let spi = CString::new("/dev/spidev-pamoja-absent").expect("no interior null");
1450        let chip = CString::new("/dev/gpiochip0").expect("no interior null");
1451        let mut radio = ptr::null_mut();
1452        let opened = unsafe {
1453            pamoja_lora_radio_open_sx127x(
1454                spi.as_ptr(),
1455                0,
1456                chip.as_ptr(),
1457                25,
1458                true,
1459                false,
1460                &mut radio,
1461            )
1462        };
1463        assert_eq!(opened, PamojaStatus::Io);
1464        assert!(radio.is_null());
1465        assert!(
1466            last_error().starts_with("/dev/spidev-pamoja-absent: "),
1467            "{}",
1468            last_error()
1469        );
1470    }
1471
1472    #[cfg(not(target_os = "linux"))]
1473    #[test]
1474    fn opening_off_linux_is_unsupported() {
1475        let spi = CString::new("/dev/spidev0.0").expect("no interior null");
1476        let chip = CString::new("/dev/gpiochip0").expect("no interior null");
1477        let board = PamojaSx126xBoard {
1478            high_power: true,
1479            ..PamojaSx126xBoard::default()
1480        };
1481        let mut radio = ptr::null_mut();
1482        let opened = unsafe {
1483            pamoja_lora_radio_open_sx126x(spi.as_ptr(), 0, chip.as_ptr(), 24, 25, board, &mut radio)
1484        };
1485        assert_eq!(opened, PamojaStatus::Unsupported);
1486        assert!(radio.is_null());
1487        assert!(last_error().contains("only Linux"), "{}", last_error());
1488    }
1489
1490    unsafe fn bytes<'a>(buffer: *mut crate::PamojaBuffer) -> &'a [u8] {
1491        std::slice::from_raw_parts(
1492            crate::pamoja_buffer_data(buffer),
1493            crate::pamoja_buffer_len(buffer),
1494        )
1495    }
1496
1497    #[test]
1498    fn a_simulated_chip_puts_one_frame_on_the_air_and_hands_another_over() {
1499        unsafe {
1500            let board = PamojaSx126xBoard {
1501                high_power: true,
1502                ..PamojaSx126xBoard::default()
1503            };
1504            let mut chip = ptr::null_mut();
1505            assert_eq!(
1506                pamoja_lora_sim_chip_sx126x(board, &mut chip),
1507                PamojaStatus::Ok
1508            );
1509            assert_eq!(pamoja_lora_sim_chip_family(chip), PAMOJA_LORA_RADIO_SX126X);
1510            let mut radio = ptr::null_mut();
1511            assert_eq!(
1512                pamoja_lora_sim_chip_radio(chip, &mut radio),
1513                PamojaStatus::Ok
1514            );
1515            let link = pamoja_lora_link_default(9, 125_000);
1516            let config = pamoja_lora_radio_config_default(868_100_000, link, 14);
1517            assert_eq!(pamoja_lora_radio_configure(radio, config), PamojaStatus::Ok);
1518
1519            let reading = b"21.5";
1520            let mut airtime = 0;
1521            assert_eq!(
1522                pamoja_lora_radio_transmit(radio, reading.as_ptr(), reading.len(), &mut airtime),
1523                PamojaStatus::Ok
1524            );
1525            assert_eq!(pamoja_lora_sim_chip_sent_count(chip), 1);
1526            let mut tuning = PamojaLoraTuning::default();
1527            assert_eq!(
1528                pamoja_lora_sim_chip_sent_tuning(chip, 0, &mut tuning),
1529                PamojaStatus::Ok
1530            );
1531            assert_eq!(tuning.frequency_hz, 868_100_000);
1532            assert_eq!(tuning.link.spreading_factor, 9);
1533            assert_eq!(tuning.output_dbm, 14);
1534            assert_eq!(tuning.sync_word, PAMOJA_LORA_RADIO_SYNC_WORD_PRIVATE);
1535            let sent = pamoja_lora_sim_chip_sent_payload(chip, 0);
1536            assert_eq!(bytes(sent), reading);
1537            crate::pamoja_buffer_free(sent);
1538            assert!(pamoja_lora_sim_chip_sent_payload(chip, 1).is_null());
1539            assert_eq!(
1540                pamoja_lora_sim_chip_sent_tuning(chip, 1, &mut tuning),
1541                PamojaStatus::InvalidArgument
1542            );
1543
1544            let answer = b"ok";
1545            assert_eq!(
1546                pamoja_lora_sim_chip_hear(chip, answer.as_ptr(), answer.len(), -10_900, -250),
1547                PamojaStatus::Ok
1548            );
1549            assert_eq!(pamoja_lora_sim_chip_waiting(chip), 1);
1550            let mut detected = false;
1551            assert_eq!(
1552                pamoja_lora_radio_detect(radio, 4, &mut detected),
1553                PamojaStatus::Ok
1554            );
1555            assert!(detected, "a frame on the air is activity");
1556            let mut buffer = [0u8; 255];
1557            let mut reception = PamojaLoraRadioReception::default();
1558            assert_eq!(
1559                pamoja_lora_radio_receive(
1560                    radio,
1561                    buffer.as_mut_ptr(),
1562                    buffer.len(),
1563                    1_000_000,
1564                    &mut reception
1565                ),
1566                PamojaStatus::Ok
1567            );
1568            assert_eq!(reception.outcome, PAMOJA_LORA_RADIO_FRAME);
1569            assert_eq!(&buffer[..reception.len], answer);
1570            assert_eq!(reception.rssi_centi_dbm, -10_900);
1571            assert_eq!(reception.snr_centi_db, -250);
1572            assert_eq!(reception.signal_rssi_centi_dbm, -11_150);
1573
1574            assert_eq!(
1575                pamoja_lora_sim_chip_hear_corrupt(chip, -12_000, -1_000),
1576                PamojaStatus::Ok
1577            );
1578            assert_eq!(
1579                pamoja_lora_radio_receive(
1580                    radio,
1581                    buffer.as_mut_ptr(),
1582                    buffer.len(),
1583                    1_000_000,
1584                    &mut reception
1585                ),
1586                PamojaStatus::Ok
1587            );
1588            assert_eq!(reception.outcome, PAMOJA_LORA_RADIO_CORRUPT);
1589            pamoja_lora_radio_free(radio);
1590            pamoja_lora_sim_chip_free(chip);
1591        }
1592    }
1593
1594    #[test]
1595    fn a_simulated_rfm95w_is_tuned_through_its_registers() {
1596        unsafe {
1597            let mut chip = ptr::null_mut();
1598            assert_eq!(
1599                pamoja_lora_sim_chip_sx127x(true, false, &mut chip),
1600                PamojaStatus::Ok
1601            );
1602            assert_eq!(pamoja_lora_sim_chip_family(chip), PAMOJA_LORA_RADIO_SX127X);
1603            let mut radio = ptr::null_mut();
1604            assert_eq!(
1605                pamoja_lora_sim_chip_radio(chip, &mut radio),
1606                PamojaStatus::Ok
1607            );
1608            assert_eq!(pamoja_lora_radio_family(radio), PAMOJA_LORA_RADIO_SX127X);
1609            let link = pamoja_lora_link_default(9, 125_000);
1610            let mut config = pamoja_lora_radio_config_default(868_100_000, link, 14);
1611            config.sync_word = PAMOJA_LORA_RADIO_SYNC_WORD_PUBLIC;
1612            assert_eq!(pamoja_lora_radio_configure(radio, config), PamojaStatus::Ok);
1613            let mut tuning = PamojaLoraTuning::default();
1614            assert_eq!(
1615                pamoja_lora_sim_chip_tuning(chip, &mut tuning),
1616                PamojaStatus::Ok
1617            );
1618            assert!(tuning.frequency_hz.abs_diff(868_100_000) <= 61);
1619            assert_eq!(tuning.output_dbm, 14);
1620            assert_eq!(tuning.sync_word, PAMOJA_LORA_RADIO_SYNC_WORD_PUBLIC);
1621            pamoja_lora_radio_free(radio);
1622            pamoja_lora_sim_chip_free(chip);
1623        }
1624    }
1625
1626    #[test]
1627    fn a_null_chip_is_refused() {
1628        unsafe {
1629            let mut radio = ptr::null_mut();
1630            assert_eq!(
1631                pamoja_lora_sim_chip_radio(ptr::null(), &mut radio),
1632                PamojaStatus::InvalidArgument
1633            );
1634            assert!(radio.is_null());
1635            assert_eq!(
1636                pamoja_lora_sim_chip_hear(ptr::null(), ptr::null(), 0, 0, 0),
1637                PamojaStatus::InvalidArgument
1638            );
1639            assert_eq!(pamoja_lora_sim_chip_family(ptr::null()), u8::MAX);
1640            assert_eq!(pamoja_lora_sim_chip_waiting(ptr::null()), 0);
1641            assert!(pamoja_lora_sim_chip_sent_payload(ptr::null(), 0).is_null());
1642            assert_eq!(
1643                pamoja_lora_sim_chip_sx126x(PamojaSx126xBoard::default(), ptr::null_mut()),
1644                PamojaStatus::InvalidArgument
1645            );
1646        }
1647    }
1648}