1use 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
35pub const PAMOJA_LORA_RADIO_SX126X: u8 = 0;
37
38pub const PAMOJA_LORA_RADIO_SX127X: u8 = 1;
40
41pub const PAMOJA_LORA_RADIO_FRAME: u8 = 0;
43
44pub const PAMOJA_LORA_RADIO_TIMEOUT: u8 = 1;
46
47pub const PAMOJA_LORA_RADIO_CORRUPT: u8 = 2;
49
50pub const PAMOJA_LORA_RADIO_NOTHING: u8 = 3;
52
53pub const PAMOJA_LORA_RADIO_SYNC_WORD_PUBLIC: u8 = 0x34;
55
56pub const PAMOJA_LORA_RADIO_SYNC_WORD_PRIVATE: u8 = 0x12;
58
59pub 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
66pub struct PamojaLoraRadio {
69 radio: Held,
70}
71
72enum 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
87macro_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
97pub struct PamojaLoraSimChip {
99 chip: Chip,
100}
101
102#[repr(C)]
104#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
105pub struct PamojaLoraTuning {
106 pub frequency_hz: u32,
109 pub link: PamojaLoraLink,
111 pub output_dbm: i8,
113 pub sync_word: u8,
115}
116
117#[repr(C)]
122#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
123pub struct PamojaSx126xBoard {
124 pub high_power: bool,
127 pub tcxo: bool,
129 pub tcxo_voltage: u8,
132 pub dio2_rf_switch: bool,
134 pub dc_dc: bool,
136 pub llcc68: bool,
138 pub tcxo_settle_us: u32,
140}
141
142#[repr(C)]
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub struct PamojaLoraRadioConfig {
146 pub frequency_hz: u32,
148 pub band_low_hz: u32,
151 pub band_high_hz: u32,
153 pub link: PamojaLoraLink,
155 pub output_dbm: i8,
157 pub sync_word: u8,
160 pub invert_iq_transmit: bool,
162 pub invert_iq_receive: bool,
164}
165
166#[repr(C)]
168#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
169pub struct PamojaLoraRadioReception {
170 pub outcome: u8,
173 pub len: usize,
175 pub rssi_centi_dbm: i32,
177 pub snr_centi_db: i32,
179 pub signal_rssi_centi_dbm: i32,
181}
182
183#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
1098fn 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}
1107unsafe 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
1117unsafe 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
1134unsafe 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
1161struct Refusal {
1163 message: String,
1164 status: PamojaStatus,
1165}
1166
1167fn refusal<E: fmt::Debug>(error: RadioError<E>) -> Refusal {
1169 Refusal {
1170 message: error.to_string(),
1171 status: radio_status(&error),
1172 }
1173}
1174
1175unsafe 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
1195fn status(result: Result<(), PamojaStatus>) -> PamojaStatus {
1197 result.err().unwrap_or(PamojaStatus::Ok)
1198}
1199
1200fn 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
1224unsafe 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
1236fn 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
1259fn 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
1274fn 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
1290fn 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
1315fn 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}