1use std::ptr;
11
12use pamoja_lorawan::adr::{Backoff, Standing};
13use pamoja_lorawan::{
14 CfList, CfListKind, Version, CFLIST_FREQUENCIES, CFLIST_LEN, CFLIST_MASK_GROUPS,
15};
16
17use crate::{read_bytes, set_last_error, PamojaStatus};
18
19pub const PAMOJA_LORAWAN_VERSION_1_0_3: u8 = 3;
21pub const PAMOJA_LORAWAN_VERSION_1_0_4: u8 = 4;
23
24pub const PAMOJA_LORAWAN_RECEIVE_DELAY1_US: u32 = 1_000_000;
26pub const PAMOJA_LORAWAN_RECEIVE_DELAY2_US: u32 = 2_000_000;
28pub const PAMOJA_LORAWAN_JOIN_ACCEPT_DELAY1_US: u32 = 5_000_000;
30pub const PAMOJA_LORAWAN_JOIN_ACCEPT_DELAY2_US: u32 = 6_000_000;
32pub const PAMOJA_LORAWAN_RECEIVE_WINDOW_TOLERANCE_US: u32 = 20;
34pub const PAMOJA_LORAWAN_MAX_FCNT_GAP: u32 = 16_384;
36pub const PAMOJA_LORAWAN_ADR_ACK_LIMIT: u32 = 64;
38pub const PAMOJA_LORAWAN_ADR_ACK_DELAY: u32 = 32;
40pub const PAMOJA_LORAWAN_RETRANSMIT_TIMEOUT_MIN_US: u32 = 1_000_000;
42pub const PAMOJA_LORAWAN_RETRANSMIT_TIMEOUT_MAX_US: u32 = 3_000_000;
44
45pub const PAMOJA_LORAWAN_CFLIST_LEN: usize = 16;
47pub const PAMOJA_LORAWAN_CFLIST_FREQUENCIES: usize = 5;
49pub const PAMOJA_LORAWAN_CFLIST_MASK_GROUPS: usize = 6;
51pub const PAMOJA_LORAWAN_CFLIST_TYPE_FREQUENCIES: u8 = 0;
53pub const PAMOJA_LORAWAN_CFLIST_TYPE_CHANNEL_MASKS: u8 = 1;
55
56#[repr(C)]
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub struct PamojaLorawanBackoffStep {
62 pub request_ack: u8,
64 pub restore_power: u8,
67 pub lower_data_rate: u8,
69 pub restore_channels: u8,
72}
73
74pub struct PamojaLorawanBackoff {
78 backoff: Backoff,
79}
80
81pub(crate) fn version(code: u8) -> Result<Version, PamojaStatus> {
83 match code {
84 PAMOJA_LORAWAN_VERSION_1_0_3 => Ok(Version::V1_0_3),
85 PAMOJA_LORAWAN_VERSION_1_0_4 => Ok(Version::V1_0_4),
86 other => {
87 set_last_error(format!(
88 "{other} is not a LoRaWAN version; expected {PAMOJA_LORAWAN_VERSION_1_0_3} or {PAMOJA_LORAWAN_VERSION_1_0_4}"
89 ));
90 Err(PamojaStatus::InvalidArgument)
91 }
92 }
93}
94
95#[no_mangle]
121pub unsafe extern "C" fn pamoja_lorawan_backoff_new(
122 version: u8,
123 limit: u32,
124 delay: u32,
125 out_backoff: *mut *mut PamojaLorawanBackoff,
126) -> PamojaStatus {
127 if out_backoff.is_null() {
128 set_last_error("out_backoff must not be null".to_owned());
129 return PamojaStatus::InvalidArgument;
130 }
131 let slot = &mut *out_backoff;
132 *slot = ptr::null_mut();
133 let version = match self::version(version) {
134 Ok(version) => version,
135 Err(status) => return status,
136 };
137 *slot = Box::into_raw(Box::new(PamojaLorawanBackoff {
138 backoff: Backoff::new(version, limit, delay),
139 }));
140 PamojaStatus::Ok
141}
142
143#[no_mangle]
168pub unsafe extern "C" fn pamoja_lorawan_backoff_uplink(
169 backoff: *mut PamojaLorawanBackoff,
170 default_data_rate: u8,
171 out_step: *mut PamojaLorawanBackoffStep,
172) -> PamojaStatus {
173 let (Some(backoff), false) = (backoff.as_mut(), out_step.is_null()) else {
174 set_last_error("backoff and out_step must not be null".to_owned());
175 return PamojaStatus::InvalidArgument;
176 };
177 let step = backoff.backoff.uplink(Standing {
178 default_data_rate: default_data_rate != 0,
179 });
180 *out_step = PamojaLorawanBackoffStep {
181 request_ack: u8::from(step.request_ack),
182 restore_power: u8::from(step.restore_power),
183 lower_data_rate: u8::from(step.lower_data_rate),
184 restore_channels: u8::from(step.restore_channels),
185 };
186 PamojaStatus::Ok
187}
188
189#[no_mangle]
208pub unsafe extern "C" fn pamoja_lorawan_backoff_downlink(
209 backoff: *mut PamojaLorawanBackoff,
210) -> PamojaStatus {
211 let Some(backoff) = backoff.as_mut() else {
212 set_last_error("backoff must not be null".to_owned());
213 return PamojaStatus::InvalidArgument;
214 };
215 backoff.backoff.downlink();
216 PamojaStatus::Ok
217}
218
219#[no_mangle]
229pub unsafe extern "C" fn pamoja_lorawan_backoff_counter(
230 backoff: *const PamojaLorawanBackoff,
231) -> u32 {
232 backoff
233 .as_ref()
234 .map_or(0, |backoff| backoff.backoff.counter())
235}
236
237#[no_mangle]
248pub unsafe extern "C" fn pamoja_lorawan_backoff_version(
249 backoff: *const PamojaLorawanBackoff,
250) -> u8 {
251 backoff
252 .as_ref()
253 .map_or(0, |backoff| match backoff.backoff.version() {
254 Version::V1_0_3 => PAMOJA_LORAWAN_VERSION_1_0_3,
255 Version::V1_0_4 => PAMOJA_LORAWAN_VERSION_1_0_4,
256 })
257}
258
259#[no_mangle]
268pub unsafe extern "C" fn pamoja_lorawan_backoff_free(backoff: *mut PamojaLorawanBackoff) {
269 if !backoff.is_null() {
270 drop(Box::from_raw(backoff));
271 }
272}
273
274unsafe fn cflist(cflist: *const u8, cflist_len: usize) -> Result<CfList, PamojaStatus> {
281 let bytes = read_bytes(cflist, cflist_len)?;
282 let Ok(bytes) = <[u8; CFLIST_LEN]>::try_from(&bytes[..]) else {
283 set_last_error(format!(
284 "a channel list is exactly {CFLIST_LEN} bytes, not {cflist_len}"
285 ));
286 return Err(PamojaStatus::InvalidArgument);
287 };
288 Ok(CfList::from_bytes(bytes))
289}
290
291unsafe fn write_cflist(list: CfList, out_cflist: *mut u8) {
297 let bytes = list.to_bytes();
298 ptr::copy_nonoverlapping(bytes.as_ptr(), out_cflist, bytes.len());
299}
300
301#[no_mangle]
325pub unsafe extern "C" fn pamoja_lorawan_cflist_from_frequencies(
326 frequencies_hz: *const u32,
327 len: usize,
328 out_cflist: *mut u8,
329) -> PamojaStatus {
330 if frequencies_hz.is_null() || out_cflist.is_null() || len != CFLIST_FREQUENCIES {
331 set_last_error(format!(
332 "a channel list takes {CFLIST_FREQUENCIES} frequencies and room for {CFLIST_LEN} bytes"
333 ));
334 return PamojaStatus::InvalidArgument;
335 }
336 let mut slots = [0u32; CFLIST_FREQUENCIES];
337 slots.copy_from_slice(std::slice::from_raw_parts(frequencies_hz, len));
338 match CfList::frequencies(slots) {
339 Ok(list) => {
340 write_cflist(list, out_cflist);
341 PamojaStatus::Ok
342 }
343 Err(error) => {
344 set_last_error(format!(
345 "{error}: a channel list frequency is a whole number of hundreds of hertz from 100 MHz to just under 1.678 GHz"
346 ));
347 PamojaStatus::Codec
348 }
349 }
350}
351
352#[no_mangle]
374pub unsafe extern "C" fn pamoja_lorawan_cflist_from_channel_masks(
375 masks: *const u16,
376 len: usize,
377 out_cflist: *mut u8,
378) -> PamojaStatus {
379 if masks.is_null() || out_cflist.is_null() || len != CFLIST_MASK_GROUPS {
380 set_last_error(format!(
381 "a channel list takes {CFLIST_MASK_GROUPS} mask groups and room for {CFLIST_LEN} bytes"
382 ));
383 return PamojaStatus::InvalidArgument;
384 }
385 let mut groups = [0u16; CFLIST_MASK_GROUPS];
386 groups.copy_from_slice(std::slice::from_raw_parts(masks, len));
387 write_cflist(CfList::channel_masks(groups), out_cflist);
388 PamojaStatus::Ok
389}
390
391#[no_mangle]
414pub unsafe extern "C" fn pamoja_lorawan_cflist_type(
415 cflist: *const u8,
416 cflist_len: usize,
417 out_type: *mut u8,
418) -> PamojaStatus {
419 if out_type.is_null() {
420 set_last_error("out_type must not be null".to_owned());
421 return PamojaStatus::InvalidArgument;
422 }
423 match self::cflist(cflist, cflist_len) {
424 Ok(list) => {
425 *out_type = list.kind().to_byte();
426 PamojaStatus::Ok
427 }
428 Err(status) => status,
429 }
430}
431
432#[no_mangle]
456pub unsafe extern "C" fn pamoja_lorawan_cflist_frequencies(
457 cflist: *const u8,
458 cflist_len: usize,
459 out_frequencies_hz: *mut u32,
460 len: usize,
461) -> PamojaStatus {
462 if out_frequencies_hz.is_null() || len != CFLIST_FREQUENCIES {
463 set_last_error(format!(
464 "out_frequencies_hz must have room for {CFLIST_FREQUENCIES} frequencies"
465 ));
466 return PamojaStatus::InvalidArgument;
467 }
468 let list = match self::cflist(cflist, cflist_len) {
469 Ok(list) => list,
470 Err(status) => return status,
471 };
472 let Some(frequencies) = list.frequencies_hz() else {
473 set_last_error(format!(
474 "the channel list is type {}, not a list of frequencies",
475 list.kind().to_byte()
476 ));
477 return PamojaStatus::InvalidArgument;
478 };
479 ptr::copy_nonoverlapping(frequencies.as_ptr(), out_frequencies_hz, len);
480 PamojaStatus::Ok
481}
482
483#[no_mangle]
506pub unsafe extern "C" fn pamoja_lorawan_cflist_channel_masks(
507 cflist: *const u8,
508 cflist_len: usize,
509 out_masks: *mut u16,
510 len: usize,
511) -> PamojaStatus {
512 if out_masks.is_null() || len != CFLIST_MASK_GROUPS {
513 set_last_error(format!(
514 "out_masks must have room for {CFLIST_MASK_GROUPS} mask groups"
515 ));
516 return PamojaStatus::InvalidArgument;
517 }
518 let list = match self::cflist(cflist, cflist_len) {
519 Ok(list) => list,
520 Err(status) => return status,
521 };
522 let Some(groups) = list.channel_mask_groups() else {
523 set_last_error(format!(
524 "the channel list is type {}, not a list of channel masks",
525 list.kind().to_byte()
526 ));
527 return PamojaStatus::InvalidArgument;
528 };
529 ptr::copy_nonoverlapping(groups.as_ptr(), out_masks, len);
530 PamojaStatus::Ok
531}
532
533#[no_mangle]
555pub unsafe extern "C" fn pamoja_lorawan_cflist_enables(
556 cflist: *const u8,
557 cflist_len: usize,
558 channel: u8,
559 out_enabled: *mut u8,
560) -> PamojaStatus {
561 if out_enabled.is_null() {
562 set_last_error("out_enabled must not be null".to_owned());
563 return PamojaStatus::InvalidArgument;
564 }
565 let list = match self::cflist(cflist, cflist_len) {
566 Ok(list) => list,
567 Err(status) => return status,
568 };
569 match list.enables(channel) {
570 Some(enabled) => {
571 *out_enabled = u8::from(enabled);
572 PamojaStatus::Ok
573 }
574 None if matches!(list.kind(), CfListKind::ChannelMasks) => {
575 set_last_error(format!(
576 "channel {channel} is past the {} a channel list covers",
577 CFLIST_MASK_GROUPS * 16
578 ));
579 PamojaStatus::InvalidArgument
580 }
581 None => {
582 set_last_error(format!(
583 "the channel list is type {}, not a list of channel masks",
584 list.kind().to_byte()
585 ));
586 PamojaStatus::InvalidArgument
587 }
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 use pamoja_lorawan::defaults;
596
597 #[test]
598 fn the_constants_are_the_crate_values() {
599 assert_eq!(
600 PAMOJA_LORAWAN_RECEIVE_DELAY1_US,
601 defaults::RECEIVE_DELAY1_US
602 );
603 assert_eq!(
604 PAMOJA_LORAWAN_RECEIVE_DELAY2_US,
605 defaults::RECEIVE_DELAY2_US
606 );
607 assert_eq!(
608 PAMOJA_LORAWAN_JOIN_ACCEPT_DELAY1_US,
609 defaults::JOIN_ACCEPT_DELAY1_US
610 );
611 assert_eq!(
612 PAMOJA_LORAWAN_JOIN_ACCEPT_DELAY2_US,
613 defaults::JOIN_ACCEPT_DELAY2_US
614 );
615 assert_eq!(
616 PAMOJA_LORAWAN_RECEIVE_WINDOW_TOLERANCE_US,
617 defaults::RECEIVE_WINDOW_TOLERANCE_US
618 );
619 assert_eq!(PAMOJA_LORAWAN_MAX_FCNT_GAP, defaults::MAX_FCNT_GAP);
620 assert_eq!(PAMOJA_LORAWAN_ADR_ACK_LIMIT, defaults::ADR_ACK_LIMIT);
621 assert_eq!(PAMOJA_LORAWAN_ADR_ACK_DELAY, defaults::ADR_ACK_DELAY);
622 assert_eq!(
623 PAMOJA_LORAWAN_RETRANSMIT_TIMEOUT_MIN_US,
624 defaults::RETRANSMIT_TIMEOUT_MIN_US
625 );
626 assert_eq!(
627 PAMOJA_LORAWAN_RETRANSMIT_TIMEOUT_MAX_US,
628 defaults::RETRANSMIT_TIMEOUT_MAX_US
629 );
630 assert_eq!(PAMOJA_LORAWAN_CFLIST_LEN, CFLIST_LEN);
631 assert_eq!(PAMOJA_LORAWAN_CFLIST_FREQUENCIES, CFLIST_FREQUENCIES);
632 assert_eq!(PAMOJA_LORAWAN_CFLIST_MASK_GROUPS, CFLIST_MASK_GROUPS);
633 }
634
635 #[test]
636 fn a_back_off_follows_table_9_across_the_boundary() {
637 unsafe {
638 let mut backoff = ptr::null_mut();
639 assert_eq!(
640 pamoja_lorawan_backoff_new(PAMOJA_LORAWAN_VERSION_1_0_4, 64, 32, &mut backoff),
641 PamojaStatus::Ok
642 );
643 assert_eq!(
644 pamoja_lorawan_backoff_version(backoff),
645 PAMOJA_LORAWAN_VERSION_1_0_4
646 );
647
648 let mut data_rate = 2;
651 let mut steps = Vec::new();
652 let mut step = PamojaLorawanBackoffStep {
653 request_ack: 0,
654 restore_power: 0,
655 lower_data_rate: 0,
656 restore_channels: 0,
657 };
658 for sent in 1..=200 {
659 assert_eq!(
660 pamoja_lorawan_backoff_uplink(backoff, u8::from(data_rate == 0), &mut step),
661 PamojaStatus::Ok
662 );
663 if sent == 64 {
664 assert_eq!(step.request_ack, 1, "the sixty-fourth asks");
665 }
666 if step.restore_power == 1 {
667 steps.push((sent, "power"));
668 }
669 if step.lower_data_rate == 1 {
670 data_rate -= 1;
671 steps.push((sent, "rate"));
672 }
673 if step.restore_channels == 1 {
674 steps.push((sent, "channels"));
675 }
676 }
677 assert_eq!(
678 steps,
679 [
680 (96, "power"),
681 (128, "rate"),
682 (160, "rate"),
683 (192, "channels")
684 ]
685 );
686 assert_eq!(pamoja_lorawan_backoff_counter(backoff), 200);
687
688 assert_eq!(pamoja_lorawan_backoff_downlink(backoff), PamojaStatus::Ok);
689 assert_eq!(pamoja_lorawan_backoff_counter(backoff), 0);
690 pamoja_lorawan_backoff_free(backoff);
691
692 assert_eq!(
693 pamoja_lorawan_backoff_new(2, 64, 32, &mut backoff),
694 PamojaStatus::InvalidArgument
695 );
696 assert!(backoff.is_null());
697 assert_eq!(
698 pamoja_lorawan_backoff_uplink(ptr::null_mut(), 0, &mut step),
699 PamojaStatus::InvalidArgument
700 );
701 }
702 }
703
704 #[test]
705 fn a_channel_list_crosses_in_both_forms() {
706 unsafe {
707 let frequencies = [
709 867_100_000,
710 867_300_000,
711 867_500_000,
712 867_700_000,
713 867_900_000,
714 ];
715 let mut bytes = [0xAAu8; 16];
716 assert_eq!(
717 pamoja_lorawan_cflist_from_frequencies(frequencies.as_ptr(), 5, bytes.as_mut_ptr()),
718 PamojaStatus::Ok
719 );
720 assert_eq!(
721 bytes,
722 [
723 0x18, 0x4F, 0x84, 0xE8, 0x56, 0x84, 0xB8, 0x5E, 0x84, 0x88, 0x66, 0x84, 0x58,
724 0x6E, 0x84, 0x00
725 ]
726 );
727 let mut kind = 9;
728 assert_eq!(
729 pamoja_lorawan_cflist_type(bytes.as_ptr(), 16, &mut kind),
730 PamojaStatus::Ok
731 );
732 assert_eq!(kind, PAMOJA_LORAWAN_CFLIST_TYPE_FREQUENCIES);
733 let mut read = [0u32; 5];
734 assert_eq!(
735 pamoja_lorawan_cflist_frequencies(bytes.as_ptr(), 16, read.as_mut_ptr(), 5),
736 PamojaStatus::Ok
737 );
738 assert_eq!(read, frequencies);
739 let mut masks = [0u16; 6];
740 assert_eq!(
741 pamoja_lorawan_cflist_channel_masks(bytes.as_ptr(), 16, masks.as_mut_ptr(), 6),
742 PamojaStatus::InvalidArgument
743 );
744 let mut enabled = 9;
745 assert_eq!(
746 pamoja_lorawan_cflist_enables(bytes.as_ptr(), 16, 0, &mut enabled),
747 PamojaStatus::InvalidArgument
748 );
749
750 for refused in [867_100_050, 99_999_900, 1_677_721_600] {
751 let one = [refused, 0, 0, 0, 0];
752 assert_eq!(
753 pamoja_lorawan_cflist_from_frequencies(one.as_ptr(), 5, bytes.as_mut_ptr()),
754 PamojaStatus::Codec,
755 "{refused} Hz"
756 );
757 }
758
759 let groups = [0xFF00, 0, 0, 0, 0x0002, 0];
761 assert_eq!(
762 pamoja_lorawan_cflist_from_channel_masks(groups.as_ptr(), 6, bytes.as_mut_ptr()),
763 PamojaStatus::Ok
764 );
765 assert_eq!(
766 bytes,
767 [0x00, 0xFF, 0, 0, 0, 0, 0, 0, 0x02, 0x00, 0, 0, 0, 0, 0, 0x01]
768 );
769 assert_eq!(
770 pamoja_lorawan_cflist_channel_masks(bytes.as_ptr(), 16, masks.as_mut_ptr(), 6),
771 PamojaStatus::Ok
772 );
773 assert_eq!(masks, groups);
774 for (channel, want) in [(7, 0), (8, 1), (15, 1), (64, 0), (65, 1)] {
775 assert_eq!(
776 pamoja_lorawan_cflist_enables(bytes.as_ptr(), 16, channel, &mut enabled),
777 PamojaStatus::Ok
778 );
779 assert_eq!(enabled, want, "channel {channel}");
780 }
781 assert_eq!(
782 pamoja_lorawan_cflist_enables(bytes.as_ptr(), 16, 96, &mut enabled),
783 PamojaStatus::InvalidArgument
784 );
785 assert_eq!(
786 pamoja_lorawan_cflist_type(bytes.as_ptr(), 15, &mut kind),
787 PamojaStatus::InvalidArgument
788 );
789 }
790 }
791}