1use std::ptr;
19
20use pamoja_lora::LinkSettings;
21use pamoja_lorawan::device::{
22 Battery, DeviceError, EndDevice, Heard, Next, ReceiveWindow, RelayExchange, RelayStatus, Saved,
23 Settings, StateError, Transmission, Window, WorNext, SAVED_LEN,
24};
25use pamoja_lorawan::relay::{RelayActivation, RelaySync, WOR_UPLINK_LEN};
26use pamoja_lorawan::LorawanError;
27
28use crate::lora::PamojaLoraLink;
29use crate::lora_region::PamojaLoraPlan;
30use crate::lorawan::{failed, PamojaLorawanDevice, PamojaLorawanSession};
31use crate::lorawan_link::{PAMOJA_LORAWAN_VERSION_1_0_3, PAMOJA_LORAWAN_VERSION_1_0_4};
32use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
33
34pub const PAMOJA_LORAWAN_SAVED_LEN: usize = 1678;
36
37pub const PAMOJA_LORAWAN_BATTERY_EXTERNAL: u8 = 0;
39pub const PAMOJA_LORAWAN_BATTERY_LEVEL: u8 = 1;
41pub const PAMOJA_LORAWAN_BATTERY_UNKNOWN: u8 = 2;
43
44pub const PAMOJA_LORAWAN_HEARD_JOINED: u8 = 0;
46pub const PAMOJA_LORAWAN_HEARD_DATA: u8 = 1;
48
49pub const PAMOJA_LORAWAN_WINDOW_RX1: u8 = 1;
51pub const PAMOJA_LORAWAN_WINDOW_RX2: u8 = 2;
53pub const PAMOJA_LORAWAN_WINDOW_RXR: u8 = 3;
55
56pub const PAMOJA_LORAWAN_RELAY_DISABLED: u8 = 0;
58pub const PAMOJA_LORAWAN_RELAY_ENABLED: u8 = 1;
60pub const PAMOJA_LORAWAN_RELAY_DYNAMIC: u8 = 2;
62pub const PAMOJA_LORAWAN_RELAY_DEVICE_CONTROLLED: u8 = 3;
64
65pub const PAMOJA_LORAWAN_RELAY_INITIALIZED: u8 = 0;
67pub const PAMOJA_LORAWAN_RELAY_UNSYNCHRONIZED: u8 = 1;
69pub const PAMOJA_LORAWAN_RELAY_SYNCHRONIZED: u8 = 2;
71
72pub const PAMOJA_LORAWAN_WOR_NEXT_UPLINK: u8 = 0;
74pub const PAMOJA_LORAWAN_WOR_NEXT_WAKE_UP: u8 = 1;
76
77pub const PAMOJA_LORAWAN_NEXT_REPEAT: u8 = 0;
79pub const PAMOJA_LORAWAN_NEXT_DONE: u8 = 1;
81pub const PAMOJA_LORAWAN_NEXT_UNACKNOWLEDGED: u8 = 2;
83pub const PAMOJA_LORAWAN_NEXT_JOIN_AGAIN: u8 = 3;
85
86pub const PAMOJA_LORAWAN_DEVICE_OK: u8 = 0;
88pub const PAMOJA_LORAWAN_DEVICE_TOO_MANY_CHANNELS: u8 = 1;
90pub const PAMOJA_LORAWAN_DEVICE_NO_CREDENTIALS: u8 = 2;
92pub const PAMOJA_LORAWAN_DEVICE_NOT_JOINED: u8 = 3;
94pub const PAMOJA_LORAWAN_DEVICE_BUSY: u8 = 4;
96pub const PAMOJA_LORAWAN_DEVICE_NOTHING_PENDING: u8 = 5;
98pub const PAMOJA_LORAWAN_DEVICE_WAIT: u8 = 6;
100pub const PAMOJA_LORAWAN_DEVICE_NO_CHANNEL: u8 = 7;
102pub const PAMOJA_LORAWAN_DEVICE_DATA_RATE: u8 = 8;
104pub const PAMOJA_LORAWAN_DEVICE_PAYLOAD_TOO_LONG: u8 = 9;
106pub const PAMOJA_LORAWAN_DEVICE_COUNTER_EXHAUSTED: u8 = 10;
108pub const PAMOJA_LORAWAN_DEVICE_FRAME: u8 = 11;
110pub const PAMOJA_LORAWAN_DEVICE_FOREIGN: u8 = 12;
112pub const PAMOJA_LORAWAN_DEVICE_REPLAYED: u8 = 13;
114pub const PAMOJA_LORAWAN_DEVICE_COUNTER_GAP: u8 = 14;
116pub const PAMOJA_LORAWAN_DEVICE_REFUSED: u8 = 15;
118pub const PAMOJA_LORAWAN_DEVICE_STATE: u8 = 16;
120
121pub const PAMOJA_LORAWAN_STATE_LENGTH: u8 = 1;
123pub const PAMOJA_LORAWAN_STATE_CORRUPT: u8 = 2;
125pub const PAMOJA_LORAWAN_STATE_FORMAT: u8 = 3;
127pub const PAMOJA_LORAWAN_STATE_PLAN: u8 = 4;
129
130#[repr(C)]
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub struct PamojaLorawanDeviceSettings {
136 pub lowest_hz: u32,
138 pub highest_hz: u32,
140 pub seed: u32,
143 pub version: u8,
146 pub adr: u8,
148 pub min_output_dbm: i8,
150 pub max_output_dbm: i8,
152 pub antenna_gain_db: i8,
154 pub regional_duty_cycle: u8,
156 pub behind_repeater: u8,
158}
159
160#[repr(C)]
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub struct PamojaLorawanWindow {
164 pub delay_us: u32,
166 pub frequency_hz: u32,
168 pub link: PamojaLoraLink,
171 pub data_rate: u8,
173}
174
175#[repr(C)]
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct PamojaLorawanRelayExchange {
180 pub wake_up_start_us: u64,
182 pub wake_up_airtime_us: u64,
184 pub ack_start_us: u64,
186 pub ack_airtime_us: u64,
188 pub uplink_start_us: u64,
191 pub wake_up_frame: [u8; 15],
194 pub wake_up_len: u8,
196 pub wake_up_frequency_hz: u32,
198 pub wake_up_data_rate: u8,
200 pub wake_up_link: PamojaLoraLink,
202 pub wake_up_output_dbm: i8,
204 pub has_ack: u8,
206 pub ack_frequency_hz: u32,
208 pub ack_data_rate: u8,
210 pub ack_link: PamojaLoraLink,
212 pub rxr: PamojaLorawanWindow,
214}
215
216#[repr(C)]
218#[derive(Clone, Copy, Debug, PartialEq, Eq)]
219pub struct PamojaLorawanRelayStatus {
220 pub cad_periodicity: u8,
222 pub xtal_accuracy: u8,
224 pub cad_to_rx: u8,
226 pub relay_data_rate: u8,
228 pub forward: u8,
230}
231
232#[repr(C)]
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237pub struct PamojaLorawanTransmission {
238 pub airtime_us: u64,
240 pub frequency_hz: u32,
242 pub rx1: PamojaLorawanWindow,
244 pub rx2: PamojaLorawanWindow,
246 pub link: PamojaLoraLink,
249 pub data_rate: u8,
251 pub output_dbm: i8,
253 pub carries_payload: u8,
256 pub has_relay: u8,
258 pub relay: PamojaLorawanRelayExchange,
260}
261
262#[repr(C)]
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub struct PamojaLorawanHeard {
268 pub dev_addr: u32,
270 pub gps_seconds: u32,
272 pub kind: u8,
274 pub has_port: u8,
276 pub port: u8,
278 pub acknowledged: u8,
280 pub confirmed: u8,
282 pub more_pending: u8,
284 pub has_link_check: u8,
286 pub margin_db: u8,
288 pub gateways: u8,
290 pub has_device_time: u8,
292 pub fraction: u8,
294}
295
296#[repr(C)]
298#[derive(Clone, Copy, Debug, PartialEq, Eq)]
299pub struct PamojaLorawanNext {
300 pub not_before_us: u64,
302 pub kind: u8,
304}
305
306#[repr(C)]
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
309pub struct PamojaLorawanDeviceError {
310 pub until_us: u64,
312 pub max: u32,
315 pub kind: u8,
317 pub data_rate: u8,
319 pub state: u8,
321 pub format: u8,
323}
324
325#[repr(C)]
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328pub struct PamojaLorawanEndDeviceStatus {
329 pub dev_addr: u32,
331 pub fcnt_up: u32,
333 pub fcnt_down: u32,
335 pub rx2_frequency_hz: u32,
337 pub receive_delay_us: u32,
339 pub lowest_hz: u32,
341 pub highest_hz: u32,
343 pub channel_count: u16,
345 pub joined: u8,
347 pub data_rate: u8,
349 pub has_fcnt_down: u8,
351 pub transmissions: u8,
353 pub rx2_data_rate: u8,
355}
356
357#[repr(C)]
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
360pub struct PamojaLorawanChannel {
361 pub uplink_hz: u32,
363 pub downlink_hz: u32,
365 pub min_data_rate: u8,
367 pub max_data_rate: u8,
369}
370
371pub struct PamojaLorawanEndDevice {
375 device: EndDevice<'static>,
376 error: Option<DeviceError>,
377}
378
379pub(crate) fn status_out(device: &EndDevice<'static>) -> PamojaLorawanEndDeviceStatus {
381 let (rx2_frequency_hz, rx2_data_rate) = device.rx2();
382 let (lowest_hz, highest_hz) = device.frequency_span();
383 PamojaLorawanEndDeviceStatus {
384 dev_addr: device.dev_addr().unwrap_or(0),
385 fcnt_up: device.fcnt_up(),
386 fcnt_down: device.fcnt_down().unwrap_or(0),
387 rx2_frequency_hz,
388 receive_delay_us: device.receive_delay_us(),
389 lowest_hz,
390 highest_hz,
391 channel_count: device.channels().count() as u16,
392 joined: u8::from(device.is_joined()),
393 data_rate: device.data_rate(),
394 has_fcnt_down: u8::from(device.fcnt_down().is_some()),
395 transmissions: device.transmissions(),
396 rx2_data_rate,
397 }
398}
399
400#[no_mangle]
424pub unsafe extern "C" fn pamoja_lorawan_device_settings(
425 min_output_dbm: i8,
426 max_output_dbm: i8,
427 out_settings: *mut PamojaLorawanDeviceSettings,
428) -> PamojaStatus {
429 if out_settings.is_null() {
430 set_last_error("out_settings must not be null".to_owned());
431 return PamojaStatus::InvalidArgument;
432 }
433 *out_settings = PamojaLorawanDeviceSettings {
434 lowest_hz: 137_000_000,
435 highest_hz: 1_020_000_000,
436 seed: 0,
437 version: PAMOJA_LORAWAN_VERSION_1_0_4,
438 adr: 1,
439 min_output_dbm,
440 max_output_dbm,
441 antenna_gain_db: 0,
442 regional_duty_cycle: 1,
443 behind_repeater: 0,
444 };
445 PamojaStatus::Ok
446}
447
448fn settings(crossed: &PamojaLorawanDeviceSettings) -> Result<Settings, PamojaStatus> {
450 let version = match crossed.version {
451 PAMOJA_LORAWAN_VERSION_1_0_3 => pamoja_lorawan::Version::V1_0_3,
452 PAMOJA_LORAWAN_VERSION_1_0_4 => pamoja_lorawan::Version::V1_0_4,
453 other => {
454 set_last_error(format!("{other} is not a LoRaWAN version"));
455 return Err(PamojaStatus::InvalidArgument);
456 }
457 };
458 if crossed.min_output_dbm > crossed.max_output_dbm || crossed.lowest_hz > crossed.highest_hz {
459 set_last_error("the output power and tuning ranges must run from low to high".to_owned());
460 return Err(PamojaStatus::InvalidArgument);
461 }
462 let mut settings = Settings::new(crossed.min_output_dbm, crossed.max_output_dbm)
463 .with_version(version)
464 .with_adr(crossed.adr != 0)
465 .with_antenna_gain(crossed.antenna_gain_db)
466 .with_tuning_range(crossed.lowest_hz, crossed.highest_hz)
467 .with_seed(crossed.seed);
468 if crossed.regional_duty_cycle == 0 {
469 settings = settings.without_regional_duty_cycle();
470 }
471 if crossed.behind_repeater != 0 {
472 settings = settings.behind_repeater();
473 }
474 Ok(settings)
475}
476
477pub(crate) unsafe fn made_from(
484 plan: *const PamojaLoraPlan,
485 settings: *const PamojaLorawanDeviceSettings,
486) -> Result<(&'static pamoja_lora::region::ChannelPlan<'static>, Settings), PamojaStatus> {
487 let (Some(plan), Some(crossed)) = (plan.as_ref(), settings.as_ref()) else {
488 set_last_error("plan and settings must not be null".to_owned());
489 return Err(PamojaStatus::InvalidArgument);
490 };
491 let Some(published) = plan.published() else {
492 set_last_error(
493 "a device runs on a published plan, from pamoja_lora_plan_for_region or pamoja_lora_plan_for_cn470"
494 .to_owned(),
495 );
496 return Err(PamojaStatus::InvalidArgument);
497 };
498 Ok((published, self::settings(crossed)?))
499}
500
501unsafe fn made(
507 result: Result<EndDevice<'static>, DeviceError>,
508 fcnt_up: u32,
509 has_fcnt_down: u8,
510 fcnt_down: u32,
511 slot: &mut *mut PamojaLorawanEndDevice,
512) -> PamojaStatus {
513 match result {
514 Ok(device) => {
515 let down = (has_fcnt_down != 0).then_some(fcnt_down);
516 *slot = Box::into_raw(Box::new(PamojaLorawanEndDevice {
517 device: device.with_frame_counters(fcnt_up, down),
518 error: None,
519 }));
520 PamojaStatus::Ok
521 }
522 Err(error) => {
523 set_last_error(error.to_string());
524 status_of(error)
525 }
526 }
527}
528
529#[no_mangle]
558pub unsafe extern "C" fn pamoja_lorawan_end_device_new(
559 plan: *const PamojaLoraPlan,
560 credentials: *const PamojaLorawanDevice,
561 settings: *const PamojaLorawanDeviceSettings,
562 fcnt_up: u32,
563 has_fcnt_down: u8,
564 fcnt_down: u32,
565 out_device: *mut *mut PamojaLorawanEndDevice,
566) -> PamojaStatus {
567 if out_device.is_null() {
568 set_last_error("out_device must not be null".to_owned());
569 return PamojaStatus::InvalidArgument;
570 }
571 let slot = &mut *out_device;
572 *slot = ptr::null_mut();
573 let Some(credentials) = credentials.as_ref() else {
574 set_last_error("credentials must not be null".to_owned());
575 return PamojaStatus::InvalidArgument;
576 };
577 let (plan, settings) = match made_from(plan, settings) {
578 Ok(pair) => pair,
579 Err(status) => return status,
580 };
581 made(
582 EndDevice::new(plan, credentials.device.clone(), settings),
583 fcnt_up,
584 has_fcnt_down,
585 fcnt_down,
586 slot,
587 )
588}
589
590#[no_mangle]
620pub unsafe extern "C" fn pamoja_lorawan_end_device_personalized(
621 plan: *const PamojaLoraPlan,
622 session: *const PamojaLorawanSession,
623 settings: *const PamojaLorawanDeviceSettings,
624 fcnt_up: u32,
625 has_fcnt_down: u8,
626 fcnt_down: u32,
627 out_device: *mut *mut PamojaLorawanEndDevice,
628) -> PamojaStatus {
629 if out_device.is_null() {
630 set_last_error("out_device must not be null".to_owned());
631 return PamojaStatus::InvalidArgument;
632 }
633 let slot = &mut *out_device;
634 *slot = ptr::null_mut();
635 let Some(session) = session.as_ref() else {
636 set_last_error("session must not be null".to_owned());
637 return PamojaStatus::InvalidArgument;
638 };
639 let (plan, settings) = match made_from(plan, settings) {
640 Ok(pair) => pair,
641 Err(status) => return status,
642 };
643 made(
644 EndDevice::personalized(plan, session.session, settings),
645 fcnt_up,
646 has_fcnt_down,
647 fcnt_down,
648 slot,
649 )
650}
651
652#[no_mangle]
661pub unsafe extern "C" fn pamoja_lorawan_end_device_free(device: *mut PamojaLorawanEndDevice) {
662 if !device.is_null() {
663 drop(Box::from_raw(device));
664 }
665}
666
667#[no_mangle]
688pub unsafe extern "C" fn pamoja_lorawan_end_device_error(
689 device: *const PamojaLorawanEndDevice,
690 out_error: *mut PamojaLorawanDeviceError,
691) -> PamojaStatus {
692 let (Some(device), false) = (device.as_ref(), out_error.is_null()) else {
693 set_last_error("device and out_error must not be null".to_owned());
694 return PamojaStatus::InvalidArgument;
695 };
696 *out_error = error_out(device.error);
697 PamojaStatus::Ok
698}
699
700fn error_out(error: Option<DeviceError>) -> PamojaLorawanDeviceError {
702 let mut out = PamojaLorawanDeviceError {
703 until_us: 0,
704 max: 0,
705 kind: PAMOJA_LORAWAN_DEVICE_OK,
706 data_rate: 0,
707 state: 0,
708 format: 0,
709 };
710 let Some(error) = error else {
711 return out;
712 };
713 out.kind = match error {
714 DeviceError::TooManyChannels { max } => {
715 out.max = max as u32;
716 PAMOJA_LORAWAN_DEVICE_TOO_MANY_CHANNELS
717 }
718 DeviceError::NoCredentials => PAMOJA_LORAWAN_DEVICE_NO_CREDENTIALS,
719 DeviceError::NotJoined => PAMOJA_LORAWAN_DEVICE_NOT_JOINED,
720 DeviceError::Busy => PAMOJA_LORAWAN_DEVICE_BUSY,
721 DeviceError::NothingPending => PAMOJA_LORAWAN_DEVICE_NOTHING_PENDING,
722 DeviceError::Wait { until_us } => {
723 out.until_us = until_us;
724 PAMOJA_LORAWAN_DEVICE_WAIT
725 }
726 DeviceError::NoChannel => PAMOJA_LORAWAN_DEVICE_NO_CHANNEL,
727 DeviceError::DataRate(rate) => {
728 out.data_rate = rate;
729 PAMOJA_LORAWAN_DEVICE_DATA_RATE
730 }
731 DeviceError::PayloadTooLong { max } => {
732 out.max = max as u32;
733 PAMOJA_LORAWAN_DEVICE_PAYLOAD_TOO_LONG
734 }
735 DeviceError::CounterExhausted => PAMOJA_LORAWAN_DEVICE_COUNTER_EXHAUSTED,
736 DeviceError::Frame(_) => PAMOJA_LORAWAN_DEVICE_FRAME,
737 DeviceError::Foreign => PAMOJA_LORAWAN_DEVICE_FOREIGN,
738 DeviceError::Replayed => PAMOJA_LORAWAN_DEVICE_REPLAYED,
739 DeviceError::CounterGap => PAMOJA_LORAWAN_DEVICE_COUNTER_GAP,
740 DeviceError::Refused => PAMOJA_LORAWAN_DEVICE_REFUSED,
741 DeviceError::State(state) => {
742 out.state = match state {
743 StateError::Length => PAMOJA_LORAWAN_STATE_LENGTH,
744 StateError::Corrupt => PAMOJA_LORAWAN_STATE_CORRUPT,
745 StateError::Format(format) => {
746 out.format = format;
747 PAMOJA_LORAWAN_STATE_FORMAT
748 }
749 StateError::Plan => PAMOJA_LORAWAN_STATE_PLAN,
750 };
751 PAMOJA_LORAWAN_DEVICE_STATE
752 }
753 };
754 out
755}
756
757fn status_of(error: DeviceError) -> PamojaStatus {
759 match error {
760 DeviceError::TooManyChannels { .. }
761 | DeviceError::NoCredentials
762 | DeviceError::DataRate(_)
763 | DeviceError::PayloadTooLong { .. }
764 | DeviceError::State(StateError::Length) => PamojaStatus::InvalidArgument,
765 DeviceError::Frame(frame) => failed(frame),
766 DeviceError::Replayed | DeviceError::CounterGap => PamojaStatus::Auth,
767 DeviceError::Refused | DeviceError::State(_) => PamojaStatus::Codec,
768 DeviceError::CounterExhausted => PamojaStatus::Closed,
769 DeviceError::NotJoined
770 | DeviceError::Busy
771 | DeviceError::NothingPending
772 | DeviceError::Wait { .. }
773 | DeviceError::NoChannel
774 | DeviceError::Foreign => PamojaStatus::Other,
775 }
776}
777
778impl PamojaLorawanEndDevice {
779 fn settle<T>(&mut self, result: Result<T, DeviceError>) -> Result<T, PamojaStatus> {
781 match result {
782 Ok(value) => {
783 self.error = None;
784 Ok(value)
785 }
786 Err(error) => {
787 self.error = Some(error);
788 set_last_error(error.to_string());
789 Err(status_of(error))
790 }
791 }
792 }
793}
794
795pub(crate) fn link_out(link: pamoja_lora::LinkSettings) -> PamojaLoraLink {
797 PamojaLoraLink {
798 bandwidth_hz: link.bandwidth_hz(),
799 preamble_symbols: link.preamble_symbols(),
800 spreading_factor: link.spreading_factor(),
801 coding_rate_denominator: link.coding_rate_denominator(),
802 explicit_header: u8::from(link.explicit_header()),
803 crc: u8::from(link.crc()),
804 }
805}
806
807fn window_out(window: Window) -> PamojaLorawanWindow {
809 PamojaLorawanWindow {
810 delay_us: window.delay_us,
811 frequency_hz: window.frequency_hz,
812 link: link_out(window.link),
813 data_rate: window.data_rate,
814 }
815}
816
817pub(crate) unsafe fn transmitted(
823 transmission: Transmission,
824 out_frame: *mut *mut PamojaBuffer,
825 out_transmission: *mut PamojaLorawanTransmission,
826) {
827 *out_frame = PamojaBuffer::into_raw(transmission.frame.as_bytes().to_vec());
828 *out_transmission = PamojaLorawanTransmission {
829 airtime_us: transmission.airtime_us,
830 frequency_hz: transmission.frequency_hz,
831 rx1: window_out(transmission.rx1),
832 rx2: window_out(transmission.rx2),
833 link: link_out(transmission.link),
834 data_rate: transmission.data_rate,
835 output_dbm: transmission.output_dbm,
836 carries_payload: u8::from(transmission.carries_payload),
837 has_relay: u8::from(transmission.relay.is_some()),
838 relay: transmission.relay.map_or_else(empty_exchange, exchange_out),
839 };
840}
841
842fn exchange_out(exchange: RelayExchange) -> PamojaLorawanRelayExchange {
844 let mut wake_up_frame = [0u8; WOR_UPLINK_LEN];
845 let frame = exchange.wake_up.frame();
846 wake_up_frame[..frame.len()].copy_from_slice(frame);
847 PamojaLorawanRelayExchange {
848 wake_up_start_us: exchange.wake_up.start_us,
849 wake_up_airtime_us: exchange.wake_up.airtime_us,
850 ack_start_us: exchange.ack.map_or(0, |ack| ack.start_us),
851 ack_airtime_us: exchange.ack.map_or(0, |ack| ack.airtime_us),
852 uplink_start_us: exchange.uplink_start_us,
853 wake_up_frame,
854 wake_up_len: frame.len() as u8,
855 wake_up_frequency_hz: exchange.wake_up.carrier.frequency_hz,
856 wake_up_data_rate: exchange.wake_up.carrier.data_rate,
857 wake_up_link: link_out(exchange.wake_up.link),
858 wake_up_output_dbm: exchange.wake_up.output_dbm,
859 has_ack: u8::from(exchange.ack.is_some()),
860 ack_frequency_hz: exchange.ack.map_or(0, |ack| ack.carrier.frequency_hz),
861 ack_data_rate: exchange.ack.map_or(0, |ack| ack.carrier.data_rate),
862 ack_link: exchange
863 .ack
864 .map_or_else(|| link_out(exchange.wake_up.link), |ack| link_out(ack.link)),
865 rxr: window_out(exchange.rxr),
866 }
867}
868
869fn empty_exchange() -> PamojaLorawanRelayExchange {
871 PamojaLorawanRelayExchange {
872 wake_up_start_us: 0,
873 wake_up_airtime_us: 0,
874 ack_start_us: 0,
875 ack_airtime_us: 0,
876 uplink_start_us: 0,
877 wake_up_frame: [0; WOR_UPLINK_LEN],
878 wake_up_len: 0,
879 wake_up_frequency_hz: 0,
880 wake_up_data_rate: 0,
881 wake_up_link: link_out(LinkSettings::new(7, 125_000)),
882 wake_up_output_dbm: 0,
883 has_ack: 0,
884 ack_frequency_hz: 0,
885 ack_data_rate: 0,
886 ack_link: link_out(LinkSettings::new(7, 125_000)),
887 rxr: PamojaLorawanWindow {
888 delay_us: 0,
889 frequency_hz: 0,
890 link: link_out(LinkSettings::new(7, 125_000)),
891 data_rate: 0,
892 },
893 }
894}
895
896unsafe fn transmit(
902 device: *mut PamojaLorawanEndDevice,
903 out_frame: *mut *mut PamojaBuffer,
904 out_transmission: *mut PamojaLorawanTransmission,
905 call: impl FnOnce(&mut EndDevice<'static>) -> Result<Transmission, DeviceError>,
906) -> PamojaStatus {
907 let (Some(device), false, false) = (
908 device.as_mut(),
909 out_frame.is_null(),
910 out_transmission.is_null(),
911 ) else {
912 set_last_error("device, out_frame and out_transmission must not be null".to_owned());
913 return PamojaStatus::InvalidArgument;
914 };
915 *out_frame = ptr::null_mut();
916 let result = call(&mut device.device);
917 match device.settle(result) {
918 Ok(transmission) => {
919 transmitted(transmission, out_frame, out_transmission);
920 PamojaStatus::Ok
921 }
922 Err(status) => status,
923 }
924}
925
926#[no_mangle]
950pub unsafe extern "C" fn pamoja_lorawan_end_device_join(
951 device: *mut PamojaLorawanEndDevice,
952 dev_nonce: u16,
953 now_us: u64,
954 out_frame: *mut *mut PamojaBuffer,
955 out_transmission: *mut PamojaLorawanTransmission,
956) -> PamojaStatus {
957 transmit(device, out_frame, out_transmission, |device| {
958 device.join(dev_nonce, now_us)
959 })
960}
961
962#[no_mangle]
991#[allow(clippy::too_many_arguments)]
992pub unsafe extern "C" fn pamoja_lorawan_end_device_send(
993 device: *mut PamojaLorawanEndDevice,
994 port: u8,
995 payload: *const u8,
996 payload_len: usize,
997 confirmed: u8,
998 now_us: u64,
999 out_frame: *mut *mut PamojaBuffer,
1000 out_transmission: *mut PamojaLorawanTransmission,
1001) -> PamojaStatus {
1002 let payload = match read_bytes(payload, payload_len) {
1003 Ok(bytes) => bytes,
1004 Err(status) => return status,
1005 };
1006 if port == 0 {
1007 if let Some(device) = device.as_mut() {
1008 device.error = Some(DeviceError::Frame(LorawanError::MalformedFrame));
1009 }
1010 set_last_error("port 0 carries MAC commands, not an application payload".to_owned());
1011 return PamojaStatus::InvalidArgument;
1012 }
1013 transmit(device, out_frame, out_transmission, |device| {
1014 device.send(port, &payload, confirmed != 0, now_us)
1015 })
1016}
1017
1018#[no_mangle]
1040pub unsafe extern "C" fn pamoja_lorawan_end_device_send_empty(
1041 device: *mut PamojaLorawanEndDevice,
1042 now_us: u64,
1043 out_frame: *mut *mut PamojaBuffer,
1044 out_transmission: *mut PamojaLorawanTransmission,
1045) -> PamojaStatus {
1046 transmit(device, out_frame, out_transmission, |device| {
1047 device.send_empty(now_us)
1048 })
1049}
1050
1051#[no_mangle]
1073pub unsafe extern "C" fn pamoja_lorawan_end_device_repeat(
1074 device: *mut PamojaLorawanEndDevice,
1075 now_us: u64,
1076 out_frame: *mut *mut PamojaBuffer,
1077 out_transmission: *mut PamojaLorawanTransmission,
1078) -> PamojaStatus {
1079 transmit(device, out_frame, out_transmission, |device| {
1080 device.repeat(now_us)
1081 })
1082}
1083
1084#[no_mangle]
1115pub unsafe extern "C" fn pamoja_lorawan_end_device_heard(
1116 device: *mut PamojaLorawanEndDevice,
1117 frame: *const u8,
1118 frame_len: usize,
1119 snr_db: i8,
1120 out_heard: *mut PamojaLorawanHeard,
1121 out_payload: *mut *mut PamojaBuffer,
1122) -> PamojaStatus {
1123 let (Some(device), false, false) =
1124 (device.as_mut(), out_heard.is_null(), out_payload.is_null())
1125 else {
1126 set_last_error("device, out_heard and out_payload must not be null".to_owned());
1127 return PamojaStatus::InvalidArgument;
1128 };
1129 heard_frame(
1130 device,
1131 None,
1132 frame,
1133 frame_len,
1134 snr_db,
1135 out_heard,
1136 out_payload,
1137 )
1138}
1139
1140#[no_mangle]
1173pub unsafe extern "C" fn pamoja_lorawan_end_device_heard_in(
1174 device: *mut PamojaLorawanEndDevice,
1175 window: u8,
1176 frame: *const u8,
1177 frame_len: usize,
1178 snr_db: i8,
1179 out_heard: *mut PamojaLorawanHeard,
1180 out_payload: *mut *mut PamojaBuffer,
1181) -> PamojaStatus {
1182 let (Some(device), false, false) =
1183 (device.as_mut(), out_heard.is_null(), out_payload.is_null())
1184 else {
1185 set_last_error("device, out_heard and out_payload must not be null".to_owned());
1186 return PamojaStatus::InvalidArgument;
1187 };
1188 let Some(window) = window_in(window) else {
1189 set_last_error(format!("{window} is not a receive window"));
1190 return PamojaStatus::InvalidArgument;
1191 };
1192 heard_frame(
1193 device,
1194 Some(window),
1195 frame,
1196 frame_len,
1197 snr_db,
1198 out_heard,
1199 out_payload,
1200 )
1201}
1202
1203unsafe fn heard_frame(
1205 device: &mut PamojaLorawanEndDevice,
1206 window: Option<ReceiveWindow>,
1207 frame: *const u8,
1208 frame_len: usize,
1209 snr_db: i8,
1210 out_heard: *mut PamojaLorawanHeard,
1211 out_payload: *mut *mut PamojaBuffer,
1212) -> PamojaStatus {
1213 *out_payload = ptr::null_mut();
1214 let frame = match read_bytes(frame, frame_len) {
1215 Ok(bytes) => bytes,
1216 Err(status) => return status,
1217 };
1218 let result = match window {
1219 Some(window) => device.device.heard_in(window, &frame, snr_db),
1220 None => device.device.heard(&frame, snr_db),
1221 };
1222 let heard = match device.settle(result) {
1223 Ok(heard) => heard,
1224 Err(status) => return status,
1225 };
1226 *out_heard = heard_out(heard, device.device.dev_addr().unwrap_or(0), out_payload);
1227 PamojaStatus::Ok
1228}
1229
1230pub(crate) unsafe fn heard_out(
1236 heard: Heard,
1237 dev_addr: u32,
1238 out_payload: *mut *mut PamojaBuffer,
1239) -> PamojaLorawanHeard {
1240 let mut out = PamojaLorawanHeard {
1241 dev_addr: 0,
1242 gps_seconds: 0,
1243 kind: PAMOJA_LORAWAN_HEARD_JOINED,
1244 has_port: 0,
1245 port: 0,
1246 acknowledged: 0,
1247 confirmed: 0,
1248 more_pending: 0,
1249 has_link_check: 0,
1250 margin_db: 0,
1251 gateways: 0,
1252 has_device_time: 0,
1253 fraction: 0,
1254 };
1255 match heard {
1256 Heard::Joined { dev_addr } => out.dev_addr = dev_addr,
1257 Heard::Data(delivery) => {
1258 out.kind = PAMOJA_LORAWAN_HEARD_DATA;
1259 out.dev_addr = dev_addr;
1260 out.has_port = u8::from(delivery.port().is_some());
1261 out.port = delivery.port().unwrap_or(0);
1262 out.acknowledged = u8::from(delivery.acknowledged());
1263 out.confirmed = u8::from(delivery.confirmed());
1264 out.more_pending = u8::from(delivery.more_pending());
1265 if let Some(check) = delivery.link_check() {
1266 out.has_link_check = 1;
1267 out.margin_db = check.margin_db;
1268 out.gateways = check.gateways;
1269 }
1270 if let Some(time) = delivery.device_time() {
1271 out.has_device_time = 1;
1272 out.gps_seconds = time.gps_seconds;
1273 out.fraction = time.fraction;
1274 }
1275 *out_payload = PamojaBuffer::into_raw(delivery.payload().to_vec());
1276 }
1277 }
1278 out
1279}
1280
1281pub(crate) const fn window_in(window: u8) -> Option<ReceiveWindow> {
1283 match window {
1284 PAMOJA_LORAWAN_WINDOW_RX1 => Some(ReceiveWindow::Rx1),
1285 PAMOJA_LORAWAN_WINDOW_RX2 => Some(ReceiveWindow::Rx2),
1286 PAMOJA_LORAWAN_WINDOW_RXR => Some(ReceiveWindow::Rxr),
1287 _ => None,
1288 }
1289}
1290
1291pub(crate) const fn next_out(next: Next) -> (u8, u64) {
1293 match next {
1294 Next::Repeat { not_before_us } => (PAMOJA_LORAWAN_NEXT_REPEAT, not_before_us),
1295 Next::Done => (PAMOJA_LORAWAN_NEXT_DONE, 0),
1296 Next::Unacknowledged => (PAMOJA_LORAWAN_NEXT_UNACKNOWLEDGED, 0),
1297 Next::JoinAgain { not_before_us } => (PAMOJA_LORAWAN_NEXT_JOIN_AGAIN, not_before_us),
1298 }
1299}
1300
1301#[no_mangle]
1322pub unsafe extern "C" fn pamoja_lorawan_end_device_nothing_heard(
1323 device: *mut PamojaLorawanEndDevice,
1324 now_us: u64,
1325 out_next: *mut PamojaLorawanNext,
1326) -> PamojaStatus {
1327 let (Some(device), false) = (device.as_mut(), out_next.is_null()) else {
1328 set_last_error("device and out_next must not be null".to_owned());
1329 return PamojaStatus::InvalidArgument;
1330 };
1331 let result = device.device.nothing_heard(now_us);
1332 let next = match device.settle(result) {
1333 Ok(next) => next,
1334 Err(status) => return status,
1335 };
1336 let (kind, not_before_us) = next_out(next);
1337 *out_next = PamojaLorawanNext {
1338 not_before_us,
1339 kind,
1340 };
1341 PamojaStatus::Ok
1342}
1343
1344#[no_mangle]
1365pub unsafe extern "C" fn pamoja_lorawan_end_device_set_battery(
1366 device: *mut PamojaLorawanEndDevice,
1367 kind: u8,
1368 level: u8,
1369) -> PamojaStatus {
1370 let Some(device) = device.as_mut() else {
1371 set_last_error("device must not be null".to_owned());
1372 return PamojaStatus::InvalidArgument;
1373 };
1374 let battery = match kind {
1375 PAMOJA_LORAWAN_BATTERY_EXTERNAL => Battery::External,
1376 PAMOJA_LORAWAN_BATTERY_LEVEL => Battery::Level(level),
1377 PAMOJA_LORAWAN_BATTERY_UNKNOWN => Battery::Unknown,
1378 other => {
1379 set_last_error(format!("{other} is not a battery kind"));
1380 return PamojaStatus::InvalidArgument;
1381 }
1382 };
1383 device.device.set_battery(battery);
1384 PamojaStatus::Ok
1385}
1386
1387#[no_mangle]
1405pub unsafe extern "C" fn pamoja_lorawan_end_device_request_link_check(
1406 device: *mut PamojaLorawanEndDevice,
1407) -> PamojaStatus {
1408 let Some(device) = device.as_mut() else {
1409 set_last_error("device must not be null".to_owned());
1410 return PamojaStatus::InvalidArgument;
1411 };
1412 device.device.request_link_check();
1413 PamojaStatus::Ok
1414}
1415
1416#[no_mangle]
1434pub unsafe extern "C" fn pamoja_lorawan_end_device_request_device_time(
1435 device: *mut PamojaLorawanEndDevice,
1436) -> PamojaStatus {
1437 let Some(device) = device.as_mut() else {
1438 set_last_error("device must not be null".to_owned());
1439 return PamojaStatus::InvalidArgument;
1440 };
1441 device.device.request_device_time();
1442 PamojaStatus::Ok
1443}
1444
1445#[no_mangle]
1464pub unsafe extern "C" fn pamoja_lorawan_end_device_status(
1465 device: *const PamojaLorawanEndDevice,
1466 out_status: *mut PamojaLorawanEndDeviceStatus,
1467) -> PamojaStatus {
1468 let (Some(device), false) = (device.as_ref(), out_status.is_null()) else {
1469 set_last_error("device and out_status must not be null".to_owned());
1470 return PamojaStatus::InvalidArgument;
1471 };
1472 *out_status = status_out(&device.device);
1473 PamojaStatus::Ok
1474}
1475
1476#[no_mangle]
1499pub unsafe extern "C" fn pamoja_lorawan_end_device_channel(
1500 device: *const PamojaLorawanEndDevice,
1501 position: u16,
1502 out_index: *mut u16,
1503 out_channel: *mut PamojaLorawanChannel,
1504) -> PamojaStatus {
1505 let (Some(device), false, false) =
1506 (device.as_ref(), out_index.is_null(), out_channel.is_null())
1507 else {
1508 set_last_error("device, out_index and out_channel must not be null".to_owned());
1509 return PamojaStatus::InvalidArgument;
1510 };
1511 let Some((index, channel)) = device.device.channels().nth(usize::from(position)) else {
1512 set_last_error(format!(
1513 "the device has no enabled channel at position {position}"
1514 ));
1515 return PamojaStatus::InvalidArgument;
1516 };
1517 *out_index = index as u16;
1518 *out_channel = PamojaLorawanChannel {
1519 uplink_hz: channel.uplink_hz,
1520 downlink_hz: channel.downlink_hz,
1521 min_data_rate: channel.min_data_rate,
1522 max_data_rate: channel.max_data_rate,
1523 };
1524 PamojaStatus::Ok
1525}
1526
1527#[no_mangle]
1552pub unsafe extern "C" fn pamoja_lorawan_end_device_save(
1553 device: *mut PamojaLorawanEndDevice,
1554 now_us: u64,
1555 out_saved: *mut u8,
1556 len: usize,
1557) -> PamojaStatus {
1558 let (Some(device), false) = (device.as_mut(), out_saved.is_null()) else {
1559 set_last_error("device and out_saved must not be null".to_owned());
1560 return PamojaStatus::InvalidArgument;
1561 };
1562 if len != SAVED_LEN {
1563 set_last_error(format!("a saved state takes {SAVED_LEN} bytes, not {len}"));
1564 return PamojaStatus::InvalidArgument;
1565 }
1566 let result = device.device.save(now_us);
1567 match device.settle(result) {
1568 Ok(saved) => {
1569 ptr::copy_nonoverlapping(saved.as_bytes().as_ptr(), out_saved, SAVED_LEN);
1570 PamojaStatus::Ok
1571 }
1572 Err(status) => status,
1573 }
1574}
1575
1576#[no_mangle]
1600pub unsafe extern "C" fn pamoja_lorawan_end_device_resume(
1601 device: *mut PamojaLorawanEndDevice,
1602 saved: *const u8,
1603 saved_len: usize,
1604 now_us: u64,
1605) -> PamojaStatus {
1606 let Some(device) = device.as_mut() else {
1607 set_last_error("device must not be null".to_owned());
1608 return PamojaStatus::InvalidArgument;
1609 };
1610 let bytes = match read_bytes(saved, saved_len) {
1611 Ok(bytes) => bytes,
1612 Err(status) => return status,
1613 };
1614 let result = Saved::from_bytes(&bytes)
1615 .map_err(DeviceError::State)
1616 .and_then(|saved| device.device.resume(&saved, now_us));
1617 match device.settle(result) {
1618 Ok(()) => PamojaStatus::Ok,
1619 Err(status) => status,
1620 }
1621}
1622
1623#[no_mangle]
1647pub unsafe extern "C" fn pamoja_lorawan_end_device_use_relay(
1648 device: *mut PamojaLorawanEndDevice,
1649 on: u8,
1650 out_taken: *mut u8,
1651) -> PamojaStatus {
1652 let (Some(device), false) = (device.as_mut(), out_taken.is_null()) else {
1653 set_last_error("device and out_taken must not be null".to_owned());
1654 return PamojaStatus::InvalidArgument;
1655 };
1656 *out_taken = u8::from(device.device.use_relay(on != 0));
1657 PamojaStatus::Ok
1658}
1659
1660#[no_mangle]
1686pub unsafe extern "C" fn pamoja_lorawan_end_device_relay_mode(
1687 device: *mut PamojaLorawanEndDevice,
1688 out_relaying: *mut u8,
1689 out_activation: *mut u8,
1690 out_sync: *mut u8,
1691 out_wor_counter: *mut u32,
1692) -> PamojaStatus {
1693 let Some(device) = device.as_mut() else {
1694 set_last_error("device must not be null".to_owned());
1695 return PamojaStatus::InvalidArgument;
1696 };
1697 if !out_relaying.is_null() {
1698 *out_relaying = u8::from(device.device.relaying());
1699 }
1700 if !out_activation.is_null() {
1701 *out_activation = match device.device.relay_activation() {
1702 RelayActivation::Disabled => PAMOJA_LORAWAN_RELAY_DISABLED,
1703 RelayActivation::Enabled => PAMOJA_LORAWAN_RELAY_ENABLED,
1704 RelayActivation::Dynamic => PAMOJA_LORAWAN_RELAY_DYNAMIC,
1705 RelayActivation::DeviceControlled => PAMOJA_LORAWAN_RELAY_DEVICE_CONTROLLED,
1706 };
1707 }
1708 if !out_sync.is_null() {
1709 *out_sync = match device.device.relay_sync() {
1710 RelaySync::Initialized => PAMOJA_LORAWAN_RELAY_INITIALIZED,
1711 RelaySync::Unsynchronized => PAMOJA_LORAWAN_RELAY_UNSYNCHRONIZED,
1712 RelaySync::Synchronized => PAMOJA_LORAWAN_RELAY_SYNCHRONIZED,
1713 };
1714 }
1715 if !out_wor_counter.is_null() {
1716 *out_wor_counter = device.device.wor_counter();
1717 }
1718 PamojaStatus::Ok
1719}
1720
1721#[no_mangle]
1741pub unsafe extern "C" fn pamoja_lorawan_end_device_relay_status(
1742 device: *mut PamojaLorawanEndDevice,
1743 out_status: *mut PamojaLorawanRelayStatus,
1744) -> PamojaStatus {
1745 let (Some(device), false) = (device.as_mut(), out_status.is_null()) else {
1746 set_last_error("device and out_status must not be null".to_owned());
1747 return PamojaStatus::InvalidArgument;
1748 };
1749 match device.device.relay_status() {
1750 Some(status) => {
1751 *out_status = relay_status_out(status);
1752 PamojaStatus::Ok
1753 }
1754 None => {
1755 set_last_error("no acknowledgment has arrived from a relay".to_owned());
1756 PamojaStatus::Other
1757 }
1758 }
1759}
1760
1761#[no_mangle]
1784pub unsafe extern "C" fn pamoja_lorawan_end_device_heard_wor_ack(
1785 device: *mut PamojaLorawanEndDevice,
1786 frame: *const u8,
1787 frame_len: usize,
1788 out_status: *mut PamojaLorawanRelayStatus,
1789) -> PamojaStatus {
1790 let (Some(device), false) = (device.as_mut(), out_status.is_null()) else {
1791 set_last_error("device and out_status must not be null".to_owned());
1792 return PamojaStatus::InvalidArgument;
1793 };
1794 let frame = match read_bytes(frame, frame_len) {
1795 Ok(bytes) => bytes,
1796 Err(status) => return status,
1797 };
1798 let heard = device.device.heard_wor_ack(&frame);
1799 match device.settle(heard) {
1800 Ok(status) => {
1801 *out_status = relay_status_out(status);
1802 PamojaStatus::Ok
1803 }
1804 Err(status) => status,
1805 }
1806}
1807
1808#[no_mangle]
1832pub unsafe extern "C" fn pamoja_lorawan_end_device_no_wor_ack(
1833 device: *mut PamojaLorawanEndDevice,
1834 now_us: u64,
1835 out_next: *mut u8,
1836 out_exchange: *mut PamojaLorawanRelayExchange,
1837) -> PamojaStatus {
1838 let (Some(device), false, false) =
1839 (device.as_mut(), out_next.is_null(), out_exchange.is_null())
1840 else {
1841 set_last_error("device, out_next and out_exchange must not be null".to_owned());
1842 return PamojaStatus::InvalidArgument;
1843 };
1844 let next = device.device.no_wor_ack(now_us);
1845 match device.settle(next) {
1846 Ok(WorNext::Uplink) => {
1847 *out_next = PAMOJA_LORAWAN_WOR_NEXT_UPLINK;
1848 *out_exchange = empty_exchange();
1849 PamojaStatus::Ok
1850 }
1851 Ok(WorNext::WakeUp(exchange)) => {
1852 *out_next = PAMOJA_LORAWAN_WOR_NEXT_WAKE_UP;
1853 *out_exchange = exchange_out(exchange);
1854 PamojaStatus::Ok
1855 }
1856 Err(status) => status,
1857 }
1858}
1859
1860pub(crate) fn relay_status_out(status: RelayStatus) -> PamojaLorawanRelayStatus {
1862 PamojaLorawanRelayStatus {
1863 cad_periodicity: status.cad_periodicity.code(),
1864 xtal_accuracy: status.xtal_accuracy.code(),
1865 cad_to_rx: status.cad_to_rx.code(),
1866 relay_data_rate: status.relay_data_rate,
1867 forward: status.forward.code(),
1868 }
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873 use super::*;
1874
1875 use crate::lora_region::{
1876 pamoja_lora_plan_for_region, pamoja_lora_plan_free, PAMOJA_LORA_REGION_EU868,
1877 };
1878 use crate::lorawan::{pamoja_lorawan_device_free, pamoja_lorawan_device_new};
1879 use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1880 use pamoja_lorawan::{Downlink, JoinGrant};
1881
1882 const APP_KEY: [u8; 16] = [0x2B; 16];
1883
1884 unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
1886 let bytes =
1887 std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
1888 .to_vec();
1889 pamoja_buffer_free(buffer);
1890 bytes
1891 }
1892
1893 #[test]
1894 fn the_saved_length_is_the_crate_value() {
1895 assert_eq!(PAMOJA_LORAWAN_SAVED_LEN, SAVED_LEN);
1896 }
1897
1898 #[test]
1899 #[cfg(feature = "eu868")]
1900 fn a_device_joins_sends_hears_saves_and_resumes_across_the_boundary() {
1901 unsafe {
1902 let mut plan = ptr::null_mut();
1903 assert_eq!(
1904 pamoja_lora_plan_for_region(PAMOJA_LORA_REGION_EU868, &mut plan),
1905 PamojaStatus::Ok
1906 );
1907 let mut credentials = ptr::null_mut();
1908 assert_eq!(
1909 pamoja_lorawan_device_new(
1910 [0x11; 8].as_ptr(),
1911 8,
1912 [0x22; 8].as_ptr(),
1913 8,
1914 APP_KEY.as_ptr(),
1915 16,
1916 &mut credentials
1917 ),
1918 PamojaStatus::Ok
1919 );
1920 let mut settings = std::mem::zeroed();
1921 assert_eq!(
1922 pamoja_lorawan_device_settings(2, 14, &mut settings),
1923 PamojaStatus::Ok
1924 );
1925 settings.seed = 7;
1926 let mut device = ptr::null_mut();
1927 assert_eq!(
1928 pamoja_lorawan_end_device_new(plan, credentials, &settings, 0, 0, 0, &mut device),
1929 PamojaStatus::Ok
1930 );
1931
1932 let mut frame = ptr::null_mut();
1933 let mut transmission = std::mem::zeroed::<PamojaLorawanTransmission>();
1934 assert_eq!(
1935 pamoja_lorawan_end_device_send(
1936 device,
1937 2,
1938 b"21.5".as_ptr(),
1939 4,
1940 0,
1941 0,
1942 &mut frame,
1943 &mut transmission
1944 ),
1945 PamojaStatus::Other
1946 );
1947 let mut error = std::mem::zeroed::<PamojaLorawanDeviceError>();
1948 pamoja_lorawan_end_device_error(device, &mut error);
1949 assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_NOT_JOINED);
1950
1951 assert_eq!(
1952 pamoja_lorawan_end_device_join(device, 1, 0, &mut frame, &mut transmission),
1953 PamojaStatus::Ok
1954 );
1955 pamoja_lorawan_end_device_error(device, &mut error);
1956 assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_OK);
1957 let request = take(frame);
1958 assert_eq!(request.len(), 23);
1959 assert_eq!(transmission.rx1.delay_us, 5_000_000);
1960 assert_eq!(transmission.rx2.frequency_hz, 869_525_000);
1961 assert_eq!(transmission.rx2.link.crc, 0);
1962
1963 assert_eq!(
1964 pamoja_lorawan_end_device_join(device, 2, 0, &mut frame, &mut transmission),
1965 PamojaStatus::Other
1966 );
1967 pamoja_lorawan_end_device_error(device, &mut error);
1968 assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_BUSY);
1969
1970 let accept = JoinGrant::new(0x01, 0x13, 0x2601_2E43).accept(&APP_KEY, 1);
1971 let mut heard = std::mem::zeroed::<PamojaLorawanHeard>();
1972 let mut payload = ptr::null_mut();
1973 assert_eq!(
1974 pamoja_lorawan_end_device_heard(
1975 device,
1976 accept.as_bytes().as_ptr(),
1977 accept.as_bytes().len(),
1978 7,
1979 &mut heard,
1980 &mut payload
1981 ),
1982 PamojaStatus::Ok
1983 );
1984 assert_eq!(
1985 (heard.kind, heard.dev_addr),
1986 (PAMOJA_LORAWAN_HEARD_JOINED, 0x2601_2E43)
1987 );
1988 assert!(payload.is_null());
1989
1990 let mut status = std::mem::zeroed::<PamojaLorawanEndDeviceStatus>();
1991 pamoja_lorawan_end_device_status(device, &mut status);
1992 assert_eq!(
1993 (status.joined, status.dev_addr, status.channel_count),
1994 (1, 0x2601_2E43, 3)
1995 );
1996 let mut index = 0;
1997 let mut channel = std::mem::zeroed::<PamojaLorawanChannel>();
1998 assert_eq!(
1999 pamoja_lorawan_end_device_channel(device, 0, &mut index, &mut channel),
2000 PamojaStatus::Ok
2001 );
2002 assert_eq!((index, channel.uplink_hz), (0, 868_100_000));
2003 assert_eq!(
2004 pamoja_lorawan_end_device_channel(device, 3, &mut index, &mut channel),
2005 PamojaStatus::InvalidArgument
2006 );
2007
2008 assert_eq!(
2009 pamoja_lorawan_end_device_send(
2010 device,
2011 0,
2012 ptr::null(),
2013 0,
2014 0,
2015 7_000_000,
2016 &mut frame,
2017 &mut transmission
2018 ),
2019 PamojaStatus::InvalidArgument
2020 );
2021 assert_eq!(
2022 pamoja_lorawan_end_device_send(
2023 device,
2024 2,
2025 b"21.5".as_ptr(),
2026 4,
2027 1,
2028 7_000_000,
2029 &mut frame,
2030 &mut transmission
2031 ),
2032 PamojaStatus::Ok
2033 );
2034 let uplink = take(frame);
2035 assert_eq!(transmission.rx1.delay_us, 1_000_000);
2036 assert_eq!(transmission.carries_payload, 1);
2037
2038 let down = JoinGrant::new(0x01, 0x13, 0x2601_2E43)
2040 .session(&APP_KEY, 1)
2041 .encode_downlink(&Downlink::new(0, 3, b"ok").with_ack())
2042 .expect("a downlink");
2043 assert_eq!(
2044 pamoja_lorawan_end_device_heard(
2045 device,
2046 down.as_bytes().as_ptr(),
2047 down.as_bytes().len(),
2048 7,
2049 &mut heard,
2050 &mut payload
2051 ),
2052 PamojaStatus::Ok
2053 );
2054 assert_eq!(heard.kind, PAMOJA_LORAWAN_HEARD_DATA);
2055 assert_eq!((heard.has_port, heard.port, heard.acknowledged), (1, 3, 1));
2056 assert_eq!(take(payload), b"ok");
2057
2058 let mut next = std::mem::zeroed::<PamojaLorawanNext>();
2059 assert_eq!(
2060 pamoja_lorawan_end_device_nothing_heard(device, 9_000_000, &mut next),
2061 PamojaStatus::Other
2062 );
2063 pamoja_lorawan_end_device_error(device, &mut error);
2064 assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_NOTHING_PENDING);
2065
2066 let mut saved = vec![0u8; PAMOJA_LORAWAN_SAVED_LEN];
2067 assert_eq!(
2068 pamoja_lorawan_end_device_save(device, 9_000_000, saved.as_mut_ptr(), saved.len()),
2069 PamojaStatus::Ok
2070 );
2071 let mut next_uplink = ptr::null_mut();
2072 let mut next_transmission = std::mem::zeroed::<PamojaLorawanTransmission>();
2073 assert_eq!(
2074 pamoja_lorawan_end_device_send(
2075 device,
2076 2,
2077 b"22.0".as_ptr(),
2078 4,
2079 0,
2080 400_000_000,
2081 &mut next_uplink,
2082 &mut next_transmission
2083 ),
2084 PamojaStatus::Ok
2085 );
2086 let original = take(next_uplink);
2087
2088 let mut woken = ptr::null_mut();
2089 assert_eq!(
2090 pamoja_lorawan_end_device_new(plan, credentials, &settings, 0, 0, 0, &mut woken),
2091 PamojaStatus::Ok
2092 );
2093 assert_eq!(
2094 pamoja_lorawan_end_device_resume(woken, saved.as_ptr(), saved.len(), 9_000_000),
2095 PamojaStatus::Ok
2096 );
2097 assert_eq!(
2098 pamoja_lorawan_end_device_send(
2099 woken,
2100 2,
2101 b"22.0".as_ptr(),
2102 4,
2103 0,
2104 400_000_000,
2105 &mut next_uplink,
2106 &mut next_transmission
2107 ),
2108 PamojaStatus::Ok
2109 );
2110 assert_eq!(
2111 take(next_uplink),
2112 original,
2113 "the woken device sends the same frame"
2114 );
2115 assert_ne!(uplink, original);
2116
2117 let long = JoinGrant::new(0x01, 0x13, 0x2601_2E43)
2120 .session(&APP_KEY, 1)
2121 .encode_downlink(&Downlink::new(1, 3, &[0x5A; 52]))
2122 .expect("a downlink");
2123 let heard_long = |window| {
2124 let mut heard = std::mem::zeroed::<PamojaLorawanHeard>();
2125 let mut payload = ptr::null_mut();
2126 let status = pamoja_lorawan_end_device_heard_in(
2127 woken,
2128 window,
2129 long.as_bytes().as_ptr(),
2130 long.as_bytes().len(),
2131 7,
2132 &mut heard,
2133 &mut payload,
2134 );
2135 (status, payload)
2136 };
2137 assert_eq!(
2138 heard_long(9).0,
2139 PamojaStatus::InvalidArgument,
2140 "a code that names no window"
2141 );
2142 assert_eq!(
2143 heard_long(PAMOJA_LORAWAN_WINDOW_RX2).0,
2144 PamojaStatus::InvalidArgument
2145 );
2146 pamoja_lorawan_end_device_error(woken, &mut error);
2147 assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_FRAME);
2148 let (status, payload) = heard_long(PAMOJA_LORAWAN_WINDOW_RX1);
2149 assert_eq!(status, PamojaStatus::Ok);
2150 assert_eq!(take(payload), [0x5A; 52]);
2151
2152 let mut blank = ptr::null_mut();
2153 assert_eq!(
2154 pamoja_lorawan_end_device_new(plan, credentials, &settings, 0, 0, 0, &mut blank),
2155 PamojaStatus::Ok
2156 );
2157 assert_eq!(
2158 pamoja_lorawan_end_device_resume(blank, saved.as_ptr(), 10, 0),
2159 PamojaStatus::InvalidArgument
2160 );
2161 pamoja_lorawan_end_device_error(blank, &mut error);
2162 assert_eq!(
2163 (error.kind, error.state),
2164 (PAMOJA_LORAWAN_DEVICE_STATE, PAMOJA_LORAWAN_STATE_LENGTH)
2165 );
2166 saved[20] ^= 0xFF;
2167 assert_eq!(
2168 pamoja_lorawan_end_device_resume(blank, saved.as_ptr(), saved.len(), 0),
2169 PamojaStatus::Codec
2170 );
2171 pamoja_lorawan_end_device_error(blank, &mut error);
2172 assert_eq!(error.state, PAMOJA_LORAWAN_STATE_CORRUPT);
2173
2174 pamoja_lorawan_end_device_free(blank);
2175 pamoja_lorawan_end_device_free(woken);
2176 pamoja_lorawan_end_device_free(device);
2177 pamoja_lorawan_device_free(credentials);
2178 pamoja_lora_plan_free(plan);
2179 }
2180 }
2181}