1use std::ptr;
15
16use crate::lora::PamojaLoraLink;
17use crate::lorawan_device::{
18 heard_out, link_out, made_from, next_out, status_out, transmitted, window_in,
19 PamojaLorawanDeviceSettings, PamojaLorawanEndDeviceStatus, PamojaLorawanHeard,
20 PamojaLorawanTransmission,
21};
22use crate::lorawan_relay::PAMOJA_LORAWAN_WOR_ACK_LEN;
23use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
24use pamoja_lora::region::RelayChannel;
25use pamoja_lorawan::device::{DeviceError, Heard, Transmission};
26use pamoja_lorawan::relay::{
27 Acknowledgment, CadPeriodicity, CadToRx, Carrier, Listen, Relay, RelayConfig, RelayError,
28 RelayHeard, RelaySettings, RxrDownlink, Scan, Wake, WorChannel, XtalAccuracy,
29};
30
31pub const PAMOJA_LORAWAN_WAKE_JOIN_REQUEST: u8 = 0;
33pub const PAMOJA_LORAWAN_WAKE_UPLINK: u8 = 1;
35pub const PAMOJA_LORAWAN_WAKE_NOTIFIED: u8 = 2;
37
38pub const PAMOJA_LORAWAN_RELAY_HEARD_DEVICE: u8 = 0;
40pub const PAMOJA_LORAWAN_RELAY_HEARD_DOWNLINK: u8 = 1;
42pub const PAMOJA_LORAWAN_RELAY_HEARD_UNDELIVERABLE: u8 = 2;
44
45pub struct PamojaLorawanRelay {
49 relay: Relay<'static>,
50 error: Option<RelayError>,
51}
52
53#[repr(C)]
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct PamojaLorawanScan {
57 pub start_us: u64,
59 pub frequency_hz: u32,
61 pub data_rate: u8,
63 pub channel: u8,
65 pub link: PamojaLoraLink,
67 pub preamble_symbols: u16,
69}
70
71#[repr(C)]
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub struct PamojaLorawanWake {
75 pub ack_start_us: u64,
77 pub ack_airtime_us: u64,
79 pub listen_start_us: u64,
81 pub kind: u8,
84 pub dev_addr: u32,
86 pub wfcnt: u32,
88 pub forward: u8,
90 pub has_ack: u8,
92 pub ack_frame: [u8; PAMOJA_LORAWAN_WOR_ACK_LEN],
94 pub ack_frequency_hz: u32,
96 pub ack_data_rate: u8,
98 pub ack_link: PamojaLoraLink,
100 pub ack_output_dbm: i8,
102 pub has_listen: u8,
104 pub listen_frequency_hz: u32,
106 pub listen_data_rate: u8,
108 pub listen_link: PamojaLoraLink,
110 pub listen_max_len: usize,
112}
113
114#[repr(C)]
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct PamojaLorawanRxr {
120 pub start_us: u64,
122 pub airtime_us: u64,
124 pub frequency_hz: u32,
126 pub data_rate: u8,
128 pub link: PamojaLoraLink,
130 pub output_dbm: i8,
132}
133
134#[no_mangle]
161pub unsafe extern "C" fn pamoja_lorawan_relay_personalized(
162 plan: *const crate::lora_region::PamojaLoraPlan,
163 session: *const crate::lorawan::PamojaLorawanSession,
164 settings: *const PamojaLorawanDeviceSettings,
165 xtal_accuracy: u8,
166 cad_to_rx: u8,
167 out_relay: *mut *mut PamojaLorawanRelay,
168) -> PamojaStatus {
169 let (Some(session), false) = (session.as_ref(), out_relay.is_null()) else {
170 set_last_error("session and out_relay must not be null".to_owned());
171 return PamojaStatus::InvalidArgument;
172 };
173 let (plan, settings) = match made_from(plan, settings) {
174 Ok(parts) => parts,
175 Err(status) => return status,
176 };
177 match pamoja_lorawan::device::EndDevice::personalized(plan, session.session, settings) {
178 Ok(device) => {
179 *out_relay = Box::into_raw(Box::new(PamojaLorawanRelay {
180 relay: Relay::new(device, relay_settings(xtal_accuracy, cad_to_rx)),
181 error: None,
182 }));
183 PamojaStatus::Ok
184 }
185 Err(error) => {
186 set_last_error(error.to_string());
187 PamojaStatus::InvalidArgument
188 }
189 }
190}
191
192#[no_mangle]
216pub unsafe extern "C" fn pamoja_lorawan_relay_new(
217 plan: *const crate::lora_region::PamojaLoraPlan,
218 credentials: *const crate::lorawan::PamojaLorawanDevice,
219 settings: *const PamojaLorawanDeviceSettings,
220 xtal_accuracy: u8,
221 cad_to_rx: u8,
222 out_relay: *mut *mut PamojaLorawanRelay,
223) -> PamojaStatus {
224 let (Some(credentials), false) = (credentials.as_ref(), out_relay.is_null()) else {
225 set_last_error("credentials and out_relay must not be null".to_owned());
226 return PamojaStatus::InvalidArgument;
227 };
228 let (plan, settings) = match made_from(plan, settings) {
229 Ok(parts) => parts,
230 Err(status) => return status,
231 };
232 match pamoja_lorawan::device::EndDevice::new(plan, credentials.device.clone(), settings) {
233 Ok(device) => {
234 *out_relay = Box::into_raw(Box::new(PamojaLorawanRelay {
235 relay: Relay::new(device, relay_settings(xtal_accuracy, cad_to_rx)),
236 error: None,
237 }));
238 PamojaStatus::Ok
239 }
240 Err(error) => {
241 set_last_error(error.to_string());
242 PamojaStatus::InvalidArgument
243 }
244 }
245}
246
247fn relay_settings(xtal_accuracy: u8, cad_to_rx: u8) -> RelaySettings {
249 RelaySettings::new(
250 XtalAccuracy::from_code(xtal_accuracy),
251 CadToRx::from_code(cad_to_rx),
252 )
253}
254
255#[no_mangle]
266pub unsafe extern "C" fn pamoja_lorawan_relay_free(relay: *mut PamojaLorawanRelay) {
267 if !relay.is_null() {
268 drop(Box::from_raw(relay));
269 }
270}
271
272#[no_mangle]
295pub unsafe extern "C" fn pamoja_lorawan_relay_join(
296 relay: *mut PamojaLorawanRelay,
297 dev_nonce: u16,
298 now_us: u64,
299 out_frame: *mut *mut PamojaBuffer,
300 out_transmission: *mut PamojaLorawanTransmission,
301) -> PamojaStatus {
302 relay_transmit(relay, out_frame, out_transmission, |device| {
303 device.join(dev_nonce, now_us)
304 })
305}
306
307#[no_mangle]
334#[allow(clippy::too_many_arguments)]
335pub unsafe extern "C" fn pamoja_lorawan_relay_send(
336 relay: *mut PamojaLorawanRelay,
337 port: u8,
338 payload: *const u8,
339 payload_len: usize,
340 confirmed: u8,
341 now_us: u64,
342 out_frame: *mut *mut PamojaBuffer,
343 out_transmission: *mut PamojaLorawanTransmission,
344) -> PamojaStatus {
345 let payload = match read_bytes(payload, payload_len) {
346 Ok(bytes) => bytes,
347 Err(status) => return status,
348 };
349 relay_transmit(relay, out_frame, out_transmission, |device| {
350 if port == 0 {
351 device.send_empty(now_us)
352 } else {
353 device.send(port, &payload, confirmed != 0, now_us)
354 }
355 })
356}
357
358#[no_mangle]
377pub unsafe extern "C" fn pamoja_lorawan_relay_status(
378 relay: *const PamojaLorawanRelay,
379 out_status: *mut PamojaLorawanEndDeviceStatus,
380) -> PamojaStatus {
381 let (Some(relay), false) = (relay.as_ref(), out_status.is_null()) else {
382 set_last_error("relay and out_status must not be null".to_owned());
383 return PamojaStatus::InvalidArgument;
384 };
385 *out_status = status_out(relay.relay.device());
386 PamojaStatus::Ok
387}
388
389unsafe fn relay_transmit(
395 relay: *mut PamojaLorawanRelay,
396 out_frame: *mut *mut PamojaBuffer,
397 out_transmission: *mut PamojaLorawanTransmission,
398 call: impl FnOnce(
399 &mut pamoja_lorawan::device::EndDevice<'static>,
400 ) -> Result<Transmission, DeviceError>,
401) -> PamojaStatus {
402 let (Some(relay), false, false) = (
403 relay.as_mut(),
404 out_frame.is_null(),
405 out_transmission.is_null(),
406 ) else {
407 set_last_error("relay, out_frame and out_transmission must not be null".to_owned());
408 return PamojaStatus::InvalidArgument;
409 };
410 *out_frame = ptr::null_mut();
411 let result = call(relay.relay.device_mut()).map_err(RelayError::Device);
412 match relay.settle(result) {
413 Ok(transmission) => {
414 transmitted(transmission, out_frame, out_transmission);
415 PamojaStatus::Ok
416 }
417 Err(status) => status,
418 }
419}
420
421#[no_mangle]
436pub unsafe extern "C" fn pamoja_lorawan_relay_error(
437 relay: *mut PamojaLorawanRelay,
438) -> *mut crate::PamojaString {
439 match relay.as_ref().and_then(|relay| relay.error) {
440 Some(error) => crate::PamojaString::into_raw(error.to_string()),
441 None => ptr::null_mut(),
442 }
443}
444
445#[no_mangle]
472pub unsafe extern "C" fn pamoja_lorawan_relay_start(
473 relay: *mut PamojaLorawanRelay,
474 cad_periodicity: u8,
475 default_channel_index: u8,
476 has_second_channel: u8,
477 second_wor_frequency_hz: u32,
478 second_ack_frequency_hz: u32,
479 second_data_rate: u8,
480) -> PamojaStatus {
481 let Some(relay) = relay.as_mut() else {
482 set_last_error("relay must not be null".to_owned());
483 return PamojaStatus::InvalidArgument;
484 };
485 let Some(periodicity) = CadPeriodicity::from_code(cad_periodicity) else {
486 set_last_error("the scan period is reserved".to_owned());
487 return PamojaStatus::InvalidArgument;
488 };
489 let Some(default_channel) = relay.relay.region_channel(default_channel_index) else {
490 set_last_error("the region does not define that wake-on-radio channel".to_owned());
491 return PamojaStatus::InvalidArgument;
492 };
493 let mut config = RelayConfig::new(periodicity, default_channel);
494 if has_second_channel != 0 {
495 config = config.with_second_channel(RelayChannel::new(
496 second_wor_frequency_hz,
497 second_ack_frequency_hz,
498 second_data_rate,
499 ));
500 }
501 let started = relay.relay.start(config);
502 relay.settle(started).err().unwrap_or(PamojaStatus::Ok)
503}
504
505#[no_mangle]
515pub unsafe extern "C" fn pamoja_lorawan_relay_stop(relay: *mut PamojaLorawanRelay) {
516 if let Some(relay) = relay.as_mut() {
517 relay.relay.stop();
518 }
519}
520
521#[no_mangle]
535pub unsafe extern "C" fn pamoja_lorawan_relay_running(relay: *const PamojaLorawanRelay) -> u8 {
536 match relay.as_ref() {
537 Some(relay) => u8::from(relay.relay.config().is_some()),
538 None => 0,
539 }
540}
541
542#[no_mangle]
566pub unsafe extern "C" fn pamoja_lorawan_relay_trust(
567 relay: *mut PamojaLorawanRelay,
568 index: u8,
569 dev_addr: u32,
570 root_wor_s_key: *const u8,
571 next_wfcnt: u32,
572 reload_rate: u8,
573 bucket_size: u8,
574) -> PamojaStatus {
575 let (Some(relay), false) = (relay.as_mut(), root_wor_s_key.is_null()) else {
576 set_last_error("relay and root_wor_s_key must not be null".to_owned());
577 return PamojaStatus::InvalidArgument;
578 };
579 let mut key = [0u8; 16];
580 key.copy_from_slice(std::slice::from_raw_parts(root_wor_s_key, 16));
581 if relay
582 .relay
583 .trust(index, dev_addr, &key, next_wfcnt, reload_rate, bucket_size)
584 {
585 PamojaStatus::Ok
586 } else {
587 set_last_error("a relay trusts sixteen devices, at indexes 0 to 15".to_owned());
588 PamojaStatus::InvalidArgument
589 }
590}
591
592#[no_mangle]
613pub unsafe extern "C" fn pamoja_lorawan_relay_next_scan(
614 relay: *mut PamojaLorawanRelay,
615 now_us: u64,
616 out_scan: *mut PamojaLorawanScan,
617) -> PamojaStatus {
618 let (Some(relay), false) = (relay.as_mut(), out_scan.is_null()) else {
619 set_last_error("relay and out_scan must not be null".to_owned());
620 return PamojaStatus::InvalidArgument;
621 };
622 match relay.relay.next_scan(now_us) {
623 Some(scan) => {
624 *out_scan = scan_out(&scan);
625 relay.error = None;
626 PamojaStatus::Ok
627 }
628 None => {
629 relay.error = Some(RelayError::Stopped);
630 set_last_error(RelayError::Stopped.to_string());
631 PamojaStatus::Other
632 }
633 }
634}
635
636#[no_mangle]
664#[allow(clippy::too_many_arguments)]
665pub unsafe extern "C" fn pamoja_lorawan_relay_heard_wor(
666 relay: *mut PamojaLorawanRelay,
667 scan: *const PamojaLorawanScan,
668 frame: *const u8,
669 frame_len: usize,
670 rssi_dbm: i16,
671 snr_db: i8,
672 ended_us: u64,
673 out_wake: *mut PamojaLorawanWake,
674) -> PamojaStatus {
675 let (Some(relay), Some(scan), false) = (relay.as_mut(), scan.as_ref(), out_wake.is_null())
676 else {
677 set_last_error("relay, scan and out_wake must not be null".to_owned());
678 return PamojaStatus::InvalidArgument;
679 };
680 let frame = match read_bytes(frame, frame_len) {
681 Ok(bytes) => bytes,
682 Err(status) => return status,
683 };
684 let scan = match scan_in(scan) {
685 Some(scan) => scan,
686 None => {
687 set_last_error("the scan names a data rate the region does not define".to_owned());
688 return PamojaStatus::InvalidArgument;
689 }
690 };
691 let heard = relay
692 .relay
693 .heard_wor(&scan, &frame, rssi_dbm, snr_db, ended_us);
694 match relay.settle(heard) {
695 Ok(wake) => {
696 *out_wake = wake_out(&wake, &scan);
697 PamojaStatus::Ok
698 }
699 Err(status) => status,
700 }
701}
702
703#[no_mangle]
730pub unsafe extern "C" fn pamoja_lorawan_relay_heard_uplink(
731 relay: *mut PamojaLorawanRelay,
732 frame: *const u8,
733 frame_len: usize,
734 rssi_dbm: i16,
735 snr_db: i8,
736 ended_us: u64,
737 out_due_us: *mut u64,
738) -> PamojaStatus {
739 let (Some(relay), false) = (relay.as_mut(), out_due_us.is_null()) else {
740 set_last_error("relay and out_due_us must not be null".to_owned());
741 return PamojaStatus::InvalidArgument;
742 };
743 let frame = match read_bytes(frame, frame_len) {
744 Ok(bytes) => bytes,
745 Err(status) => return status,
746 };
747 let held = relay.relay.heard_uplink(&frame, rssi_dbm, snr_db, ended_us);
748 match relay.settle(held) {
749 Ok(due_us) => {
750 *out_due_us = due_us;
751 PamojaStatus::Ok
752 }
753 Err(status) => status,
754 }
755}
756
757#[no_mangle]
767pub unsafe extern "C" fn pamoja_lorawan_relay_uplink_missed(relay: *mut PamojaLorawanRelay) {
768 if let Some(relay) = relay.as_mut() {
769 relay.relay.uplink_missed();
770 }
771}
772
773#[no_mangle]
796pub unsafe extern "C" fn pamoja_lorawan_relay_forward(
797 relay: *mut PamojaLorawanRelay,
798 now_us: u64,
799 out_frame: *mut *mut PamojaBuffer,
800 out_transmission: *mut PamojaLorawanTransmission,
801) -> PamojaStatus {
802 let (Some(relay), false, false) = (
803 relay.as_mut(),
804 out_frame.is_null(),
805 out_transmission.is_null(),
806 ) else {
807 set_last_error("relay, out_frame and out_transmission must not be null".to_owned());
808 return PamojaStatus::InvalidArgument;
809 };
810 *out_frame = ptr::null_mut();
811 let sent = relay.relay.forward(now_us);
812 match relay.settle(sent) {
813 Ok(transmission) => {
814 transmitted(transmission, out_frame, out_transmission);
815 PamojaStatus::Ok
816 }
817 Err(status) => status,
818 }
819}
820
821#[no_mangle]
836pub unsafe extern "C" fn pamoja_lorawan_relay_forward_due(
837 relay: *mut PamojaLorawanRelay,
838 out_due_us: *mut u64,
839) -> u8 {
840 let (Some(relay), false) = (relay.as_ref(), out_due_us.is_null()) else {
841 return 0;
842 };
843 match relay.relay.forward_due() {
844 Some(due_us) => {
845 *out_due_us = due_us;
846 1
847 }
848 None => 0,
849 }
850}
851
852#[no_mangle]
888#[allow(clippy::too_many_arguments)]
889pub unsafe extern "C" fn pamoja_lorawan_relay_heard_in(
890 relay: *mut PamojaLorawanRelay,
891 window: u8,
892 frame: *const u8,
893 frame_len: usize,
894 snr_db: i8,
895 out_kind: *mut u8,
896 out_heard: *mut PamojaLorawanHeard,
897 out_payload: *mut *mut PamojaBuffer,
898 out_frame: *mut *mut PamojaBuffer,
899 out_downlink: *mut PamojaLorawanRxr,
900) -> PamojaStatus {
901 let (Some(relay), false, false, false, false, false) = (
902 relay.as_mut(),
903 out_kind.is_null(),
904 out_heard.is_null(),
905 out_payload.is_null(),
906 out_frame.is_null(),
907 out_downlink.is_null(),
908 ) else {
909 set_last_error("relay and every out pointer must not be null".to_owned());
910 return PamojaStatus::InvalidArgument;
911 };
912 let bytes = match read_bytes(frame, frame_len) {
913 Ok(bytes) => bytes,
914 Err(status) => return status,
915 };
916 let dev_addr = relay.relay.device().dev_addr().unwrap_or(0);
917 let Some(window) = window_in(window) else {
918 set_last_error("the window must be RX1, RX2 or RXR".to_owned());
919 return PamojaStatus::InvalidArgument;
920 };
921 *out_payload = ptr::null_mut();
922 *out_frame = ptr::null_mut();
923
924 let heard = relay.relay.heard_in(window, &bytes, snr_db);
925 match relay.settle(heard) {
926 Ok(RelayHeard::Device(heard)) => {
927 *out_kind = PAMOJA_LORAWAN_RELAY_HEARD_DEVICE;
928 *out_heard = heard_out(heard, dev_addr, out_payload);
929 *out_downlink = empty_rxr();
930 PamojaStatus::Ok
931 }
932 Ok(RelayHeard::Downlink { delivery, downlink }) => {
933 *out_kind = PAMOJA_LORAWAN_RELAY_HEARD_DOWNLINK;
934 *out_heard = heard_out(Heard::Data(delivery), dev_addr, out_payload);
935 *out_frame = PamojaBuffer::into_raw(downlink.frame().to_vec());
936 *out_downlink = rxr_out(&downlink);
937 PamojaStatus::Ok
938 }
939 Ok(RelayHeard::Undeliverable { delivery, reason }) => {
940 *out_kind = PAMOJA_LORAWAN_RELAY_HEARD_UNDELIVERABLE;
941 *out_heard = heard_out(Heard::Data(delivery), dev_addr, out_payload);
942 *out_downlink = empty_rxr();
943 relay.error = Some(reason);
944 set_last_error(reason.to_string());
945 PamojaStatus::Ok
946 }
947 Err(status) => status,
948 }
949}
950
951#[no_mangle]
975pub unsafe extern "C" fn pamoja_lorawan_relay_nothing_heard(
976 relay: *mut PamojaLorawanRelay,
977 now_us: u64,
978 out_next: *mut u8,
979 out_not_before_us: *mut u64,
980) -> PamojaStatus {
981 let (Some(relay), false, false) = (
982 relay.as_mut(),
983 out_next.is_null(),
984 out_not_before_us.is_null(),
985 ) else {
986 set_last_error("relay, out_next and out_not_before_us must not be null".to_owned());
987 return PamojaStatus::InvalidArgument;
988 };
989 let next = relay.relay.nothing_heard(now_us);
990 match relay.settle(next) {
991 Ok(next) => {
992 let (code, not_before_us) = next_out(next);
993 *out_next = code;
994 *out_not_before_us = not_before_us;
995 PamojaStatus::Ok
996 }
997 Err(status) => status,
998 }
999}
1000
1001impl PamojaLorawanRelay {
1002 fn settle<T>(&mut self, result: Result<T, RelayError>) -> Result<T, PamojaStatus> {
1004 match result {
1005 Ok(value) => {
1006 self.error = None;
1007 Ok(value)
1008 }
1009 Err(error) => {
1010 self.error = Some(error);
1011 set_last_error(error.to_string());
1012 Err(PamojaStatus::Other)
1013 }
1014 }
1015 }
1016}
1017
1018fn scan_out(scan: &Scan) -> PamojaLorawanScan {
1020 PamojaLorawanScan {
1021 start_us: scan.start_us,
1022 frequency_hz: scan.carrier.frequency_hz,
1023 data_rate: scan.carrier.data_rate,
1024 channel: match scan.channel {
1025 WorChannel::Default => 0,
1026 WorChannel::Second => 1,
1027 },
1028 link: link_out(scan.link),
1029 preamble_symbols: scan.preamble_symbols,
1030 }
1031}
1032
1033#[allow(clippy::unnecessary_wraps)]
1035fn scan_in(scan: &PamojaLorawanScan) -> Option<Scan> {
1036 Some(Scan {
1037 start_us: scan.start_us,
1038 channel: match scan.channel {
1039 0 => WorChannel::Default,
1040 1 => WorChannel::Second,
1041 _ => return None,
1042 },
1043 carrier: Carrier::new(scan.frequency_hz, scan.data_rate),
1044 link: crate::lora::settings(scan.link),
1045 preamble_symbols: scan.preamble_symbols,
1046 })
1047}
1048
1049fn wake_out(wake: &Wake, scan: &Scan) -> PamojaLorawanWake {
1051 let empty_link = link_out(scan.link);
1052 let mut out = PamojaLorawanWake {
1053 ack_start_us: 0,
1054 ack_airtime_us: 0,
1055 listen_start_us: 0,
1056 kind: PAMOJA_LORAWAN_WAKE_JOIN_REQUEST,
1057 dev_addr: 0,
1058 wfcnt: 0,
1059 forward: 0,
1060 has_ack: 0,
1061 ack_frame: [0; PAMOJA_LORAWAN_WOR_ACK_LEN],
1062 ack_frequency_hz: 0,
1063 ack_data_rate: 0,
1064 ack_link: empty_link,
1065 ack_output_dbm: 0,
1066 has_listen: 0,
1067 listen_frequency_hz: 0,
1068 listen_data_rate: 0,
1069 listen_link: empty_link,
1070 listen_max_len: 0,
1071 };
1072 match wake {
1073 Wake::JoinRequest { listen } => {
1074 out.kind = PAMOJA_LORAWAN_WAKE_JOIN_REQUEST;
1075 listen_out(&mut out, listen);
1076 }
1077 Wake::Uplink {
1078 dev_addr,
1079 wfcnt,
1080 forward,
1081 acknowledgment,
1082 listen,
1083 } => {
1084 out.kind = PAMOJA_LORAWAN_WAKE_UPLINK;
1085 out.dev_addr = *dev_addr;
1086 out.wfcnt = *wfcnt;
1087 out.forward = forward.code();
1088 if let Some(acknowledgment) = acknowledgment {
1089 ack_out(&mut out, acknowledgment);
1090 }
1091 if let Some(listen) = listen {
1092 listen_out(&mut out, listen);
1093 }
1094 }
1095 Wake::Notified { dev_addr } => {
1096 out.kind = PAMOJA_LORAWAN_WAKE_NOTIFIED;
1097 out.dev_addr = *dev_addr;
1098 }
1099 }
1100 out
1101}
1102
1103fn ack_out(out: &mut PamojaLorawanWake, acknowledgment: &Acknowledgment) {
1105 out.has_ack = 1;
1106 out.ack_frame = acknowledgment.frame;
1107 out.ack_start_us = acknowledgment.start_us;
1108 out.ack_airtime_us = acknowledgment.airtime_us;
1109 out.ack_frequency_hz = acknowledgment.carrier.frequency_hz;
1110 out.ack_data_rate = acknowledgment.carrier.data_rate;
1111 out.ack_link = link_out(acknowledgment.link);
1112 out.ack_output_dbm = acknowledgment.output_dbm;
1113}
1114
1115fn listen_out(out: &mut PamojaLorawanWake, listen: &Listen) {
1117 out.has_listen = 1;
1118 out.listen_start_us = listen.start_us;
1119 out.listen_frequency_hz = listen.carrier.frequency_hz;
1120 out.listen_data_rate = listen.carrier.data_rate;
1121 out.listen_link = link_out(listen.link);
1122 out.listen_max_len = listen.max_len;
1123}
1124
1125fn rxr_out(downlink: &RxrDownlink) -> PamojaLorawanRxr {
1127 PamojaLorawanRxr {
1128 start_us: downlink.start_us,
1129 airtime_us: downlink.airtime_us,
1130 frequency_hz: downlink.carrier.frequency_hz,
1131 data_rate: downlink.carrier.data_rate,
1132 link: link_out(downlink.link),
1133 output_dbm: downlink.output_dbm,
1134 }
1135}
1136
1137fn empty_rxr() -> PamojaLorawanRxr {
1139 PamojaLorawanRxr {
1140 start_us: 0,
1141 airtime_us: 0,
1142 frequency_hz: 0,
1143 data_rate: 0,
1144 link: link_out(pamoja_lora::LinkSettings::new(7, 125_000)),
1145 output_dbm: 0,
1146 }
1147}
1148
1149#[cfg(test)]
1150mod tests {
1151 use super::*;
1152
1153 use pamoja_lorawan::relay::{root_wor_s_key, wor_uplink, WorKeys, LA_FPORT_RELAY};
1154 use pamoja_lorawan::{Downlink, Session, Uplink};
1155
1156 use crate::lora_region::{
1157 pamoja_lora_plan_for_region, PamojaLoraPlan, PAMOJA_LORA_REGION_EU868,
1158 };
1159 use crate::lorawan::{pamoja_lorawan_session_new, PamojaLorawanSession};
1160 use crate::lorawan_device::{pamoja_lorawan_device_settings, PAMOJA_LORAWAN_WINDOW_RX1};
1161
1162 unsafe fn buffer_bytes(buffer: *const PamojaBuffer) -> Vec<u8> {
1164 let len = crate::pamoja_buffer_len(buffer);
1165 std::slice::from_raw_parts(crate::pamoja_buffer_data(buffer), len).to_vec()
1166 }
1167
1168 const RELAY_ADDR: u32 = 0x2601_0001;
1169 const SENSOR_ADDR: u32 = 0x2601_1BDA;
1170 const SENSOR_NWK_S_KEY: [u8; 16] = [0x2B; 16];
1171
1172 unsafe fn relay() -> (
1174 *mut PamojaLoraPlan,
1175 *mut PamojaLorawanSession,
1176 *mut PamojaLorawanRelay,
1177 ) {
1178 let mut plan: *mut PamojaLoraPlan = ptr::null_mut();
1179 assert_eq!(
1180 pamoja_lora_plan_for_region(PAMOJA_LORA_REGION_EU868, &mut plan),
1181 PamojaStatus::Ok
1182 );
1183 let mut session: *mut PamojaLorawanSession = ptr::null_mut();
1184 assert_eq!(
1185 pamoja_lorawan_session_new(
1186 RELAY_ADDR,
1187 [0x11; 16].as_ptr(),
1188 16,
1189 [0x22; 16].as_ptr(),
1190 16,
1191 &mut session
1192 ),
1193 PamojaStatus::Ok
1194 );
1195 let mut settings = core::mem::zeroed::<PamojaLorawanDeviceSettings>();
1196 assert_eq!(
1197 pamoja_lorawan_device_settings(2, 14, &mut settings),
1198 PamojaStatus::Ok
1199 );
1200 settings.lowest_hz = 863_000_000;
1201 settings.highest_hz = 870_000_000;
1202 let mut relay: *mut PamojaLorawanRelay = ptr::null_mut();
1203 assert_eq!(
1204 pamoja_lorawan_relay_personalized(plan, session, &settings, 0, 0, &mut relay),
1205 PamojaStatus::Ok
1206 );
1207 assert_eq!(
1208 pamoja_lorawan_relay_start(relay, 0, 0, 0, 0, 0, 0),
1209 PamojaStatus::Ok
1210 );
1211 let root = root_wor_s_key(&SENSOR_NWK_S_KEY);
1212 assert_eq!(
1213 pamoja_lorawan_relay_trust(relay, 0, SENSOR_ADDR, root.as_ptr(), 0, 63, 0),
1214 PamojaStatus::Ok
1215 );
1216 (plan, session, relay)
1217 }
1218
1219 #[test]
1220 fn a_relay_scans_answers_and_forwards_across_the_boundary() {
1221 unsafe {
1222 let (plan, session, relay) = relay();
1223
1224 let mut scan = PamojaLorawanScan {
1226 start_us: 0,
1227 frequency_hz: 0,
1228 data_rate: 0,
1229 channel: 0,
1230 link: crate::lorawan_device::link_out(pamoja_lora::LinkSettings::new(7, 125_000)),
1231 preamble_symbols: 0,
1232 };
1233 assert_eq!(
1234 pamoja_lorawan_relay_next_scan(relay, 0, &mut scan),
1235 PamojaStatus::Ok
1236 );
1237 assert_eq!(scan.frequency_hz, 865_100_000);
1238 assert_eq!(scan.data_rate, 3);
1239
1240 let sensor = Session::new(SENSOR_ADDR, SENSOR_NWK_S_KEY, [0x99; 16]);
1242 let keys = WorKeys::derive(&root_wor_s_key(&SENSOR_NWK_S_KEY), SENSOR_ADDR);
1243 let uplink = pamoja_lorawan::relay::Carrier::new(868_100_000, 5);
1244 let wor = wor_uplink(
1245 &keys,
1246 SENSOR_ADDR,
1247 0,
1248 uplink,
1249 pamoja_lorawan::relay::Carrier::new(scan.frequency_hz, scan.data_rate),
1250 )
1251 .expect("a wake-on-radio frame");
1252
1253 let mut wake = core::mem::zeroed::<PamojaLorawanWake>();
1254 assert_eq!(
1255 pamoja_lorawan_relay_heard_wor(
1256 relay,
1257 &scan,
1258 wor.as_ptr(),
1259 wor.len(),
1260 -90,
1261 4,
1262 scan.start_us + 900_000,
1263 &mut wake,
1264 ),
1265 PamojaStatus::Ok
1266 );
1267 assert_eq!(wake.kind, PAMOJA_LORAWAN_WAKE_UPLINK);
1268 assert_eq!(wake.dev_addr, SENSOR_ADDR);
1269 assert_eq!(wake.has_ack, 1);
1270 assert_eq!(wake.ack_frequency_hz, 865_300_000);
1271 assert_eq!(wake.has_listen, 1);
1272 assert_eq!(wake.listen_frequency_hz, 868_100_000);
1273
1274 let frame = sensor
1276 .encode_uplink(&Uplink::new(0, 1, b"21.5"))
1277 .expect("an uplink");
1278 let mut due_us = 0u64;
1279 assert_eq!(
1280 pamoja_lorawan_relay_heard_uplink(
1281 relay,
1282 frame.as_bytes().as_ptr(),
1283 frame.as_bytes().len(),
1284 -88,
1285 6,
1286 wake.listen_start_us + 60_000,
1287 &mut due_us,
1288 ),
1289 PamojaStatus::Ok
1290 );
1291 let mut waiting = 0u64;
1292 assert_eq!(pamoja_lorawan_relay_forward_due(relay, &mut waiting), 1);
1293 assert_eq!(waiting, due_us);
1294
1295 let mut forwarded: *mut PamojaBuffer = ptr::null_mut();
1296 let mut transmission = core::mem::zeroed::<PamojaLorawanTransmission>();
1297 assert_eq!(
1298 pamoja_lorawan_relay_forward(relay, due_us, &mut forwarded, &mut transmission),
1299 PamojaStatus::Ok
1300 );
1301 let sent = buffer_bytes(forwarded);
1302 crate::pamoja_buffer_free(forwarded);
1303 let relay_session = Session::new(RELAY_ADDR, [0x11; 16], [0x22; 16]);
1304 let read = relay_session.decode(&sent, 0).expect("it decodes");
1305 assert_eq!(read.fport(), Some(LA_FPORT_RELAY));
1306
1307 let answer = sensor
1309 .encode_downlink(&Downlink::new(0, 1, b"ok"))
1310 .expect("a downlink");
1311 let reply = relay_session
1312 .encode_downlink(&Downlink::new(0, LA_FPORT_RELAY, answer.as_bytes()))
1313 .expect("a downlink");
1314 let mut kind = 0u8;
1315 let mut heard = core::mem::zeroed::<PamojaLorawanHeard>();
1316 let mut payload: *mut PamojaBuffer = ptr::null_mut();
1317 let mut out_frame: *mut PamojaBuffer = ptr::null_mut();
1318 let mut downlink = core::mem::zeroed::<PamojaLorawanRxr>();
1319 assert_eq!(
1320 pamoja_lorawan_relay_heard_in(
1321 relay,
1322 PAMOJA_LORAWAN_WINDOW_RX1,
1323 reply.as_bytes().as_ptr(),
1324 reply.as_bytes().len(),
1325 7,
1326 &mut kind,
1327 &mut heard,
1328 &mut payload,
1329 &mut out_frame,
1330 &mut downlink,
1331 ),
1332 PamojaStatus::Ok
1333 );
1334 assert_eq!(kind, PAMOJA_LORAWAN_RELAY_HEARD_DOWNLINK);
1335 assert_eq!(downlink.frequency_hz, 865_100_000);
1336 assert_eq!(buffer_bytes(out_frame), answer.as_bytes());
1337 crate::pamoja_buffer_free(payload);
1338 crate::pamoja_buffer_free(out_frame);
1339
1340 pamoja_lorawan_relay_free(relay);
1341 crate::lorawan::pamoja_lorawan_session_free(session);
1342 crate::lora_region::pamoja_lora_plan_free(plan);
1343 }
1344 }
1345
1346 #[test]
1347 fn a_stopped_relay_says_so_and_a_bad_channel_is_refused() {
1348 unsafe {
1349 let (plan, session, relay) = relay();
1350 pamoja_lorawan_relay_stop(relay);
1351 let mut scan = core::mem::zeroed::<PamojaLorawanScan>();
1352 assert_eq!(
1353 pamoja_lorawan_relay_next_scan(relay, 0, &mut scan),
1354 PamojaStatus::Other
1355 );
1356 let reason = pamoja_lorawan_relay_error(relay);
1357 assert!(!reason.is_null());
1358 crate::pamoja_string_free(reason);
1359
1360 assert_eq!(
1361 pamoja_lorawan_relay_start(relay, 7, 0, 0, 0, 0, 0),
1362 PamojaStatus::InvalidArgument,
1363 "a reserved scan period",
1364 );
1365 assert_eq!(
1366 pamoja_lorawan_relay_start(relay, 0, 9, 0, 0, 0, 0),
1367 PamojaStatus::InvalidArgument,
1368 "a channel the region does not define",
1369 );
1370
1371 pamoja_lorawan_relay_free(relay);
1372 crate::lorawan::pamoja_lorawan_session_free(session);
1373 crate::lora_region::pamoja_lora_plan_free(plan);
1374 }
1375 }
1376}