1#[cfg(feature = "cn470")]
22use pamoja_lora::region::Cn470Plan;
23use pamoja_lora::region::{
24 Beacon, ChannelBlock, ChannelPlan, ChannelPlanBuilder, DataRate, FixedChannelList,
25 JoinSequence, MaskControl, MaxPayload, Modulation, OwnedChannelPlan, PayloadTable, PlanKind,
26 PowerReference, RelayChannel, SubBand,
27};
28#[cfg(any(
31 feature = "eu868",
32 feature = "us915",
33 feature = "eu433",
34 feature = "au915",
35 feature = "cn470",
36 feature = "as923",
37 feature = "kr920",
38 feature = "in865",
39 feature = "ru864"
40))]
41use pamoja_lora::region::Region;
42
43use crate::lora::PamojaLoraLink;
44use crate::{set_last_error, PamojaStatus, PamojaString};
45
46pub const PAMOJA_LORA_REGION_EU868: u32 = 1;
48pub const PAMOJA_LORA_REGION_US915: u32 = 2;
50pub const PAMOJA_LORA_REGION_EU433: u32 = 3;
52pub const PAMOJA_LORA_REGION_AU915: u32 = 4;
54pub const PAMOJA_LORA_REGION_CN470: u32 = 5;
56pub const PAMOJA_LORA_REGION_AS923: u32 = 6;
58pub const PAMOJA_LORA_REGION_KR920: u32 = 7;
60pub const PAMOJA_LORA_REGION_IN865: u32 = 8;
62pub const PAMOJA_LORA_REGION_RU864: u32 = 9;
64
65pub const PAMOJA_LORA_MODULATION_LORA: u8 = 0;
67pub const PAMOJA_LORA_MODULATION_FSK: u8 = 1;
69pub const PAMOJA_LORA_MODULATION_LR_FHSS: u8 = 2;
71pub const PAMOJA_LORA_MODULATION_RESERVED: u8 = 3;
73
74pub const PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_REPEATER: u32 = 0;
76pub const PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_DIRECT: u32 = 1;
78pub const PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_REPEATER: u32 = 2;
80pub const PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_DIRECT: u32 = 3;
82pub const PAMOJA_LORA_PAYLOAD_TABLE_DWELL_LIMITED: u32 = 4;
84
85pub const PAMOJA_LORA_CHANNELS_JOIN: u32 = 0;
87pub const PAMOJA_LORA_CHANNELS_DEFAULT: u32 = 1;
89pub const PAMOJA_LORA_CHANNELS_DOWNLINK: u32 = 2;
91
92pub const PAMOJA_LORA_PLAN_KIND_DYNAMIC: u8 = 0;
94pub const PAMOJA_LORA_PLAN_KIND_FIXED: u8 = 1;
96
97pub const PAMOJA_LORA_CHANNEL_LIST_NONE: u8 = 0;
99pub const PAMOJA_LORA_CHANNEL_LIST_MHZ800: u8 = 1;
101pub const PAMOJA_LORA_CHANNEL_LIST_MHZ900: u8 = 2;
103
104pub const PAMOJA_LORA_JOIN_RANDOM: u8 = 0;
106pub const PAMOJA_LORA_JOIN_OCTET_PASSES: u8 = 1;
109
110pub const PAMOJA_LORA_POWER_EIRP: u8 = 0;
112pub const PAMOJA_LORA_POWER_CONDUCTED: u8 = 1;
114
115pub const PAMOJA_LORA_MASK_GROUP: u8 = 0;
117pub const PAMOJA_LORA_MASK_BANKS: u8 = 1;
119pub const PAMOJA_LORA_MASK_PAIRED_BANKS: u8 = 2;
122pub const PAMOJA_LORA_MASK_ALL: u8 = 3;
124pub const PAMOJA_LORA_MASK_RESERVED: u8 = 4;
126
127pub const PAMOJA_LORA_CN470_NONE: u32 = 0;
129pub const PAMOJA_LORA_CN470_ANTENNA_20MHZ_A: u32 = 1;
131pub const PAMOJA_LORA_CN470_ANTENNA_20MHZ_B: u32 = 2;
133pub const PAMOJA_LORA_CN470_ANTENNA_26MHZ_A: u32 = 3;
135pub const PAMOJA_LORA_CN470_ANTENNA_26MHZ_B: u32 = 4;
137pub const PAMOJA_LORA_CN470_CHANNELS_96: u32 = 5;
139
140pub const PAMOJA_LORA_DIRECTION_UPLINK: u32 = 0;
142pub const PAMOJA_LORA_DIRECTION_DOWNLINK: u32 = 1;
144
145#[repr(C)]
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub struct PamojaLoraDataRate {
154 pub bitrate_bps: u32,
156 pub bandwidth_hz: u32,
158 pub kind: u8,
160 pub spreading_factor: u8,
162 pub coding_rate_numerator: u8,
164 pub coding_rate_denominator: u8,
166}
167
168#[repr(C)]
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub struct PamojaLoraMaxPayload {
172 pub mac_payload: u16,
174 pub application: u16,
176}
177
178#[repr(C)]
180#[derive(Clone, Copy, Debug, PartialEq, Eq)]
181pub struct PamojaLoraChannelBlock {
182 pub start_hz: u32,
184 pub step_hz: u32,
186 pub count: u16,
188 pub min_data_rate: u8,
190 pub max_data_rate: u8,
192}
193
194#[repr(C)]
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub struct PamojaLoraSubBand {
198 pub start_hz: u32,
200 pub end_hz: u32,
202 pub duty_cycle_permille: u32,
205 pub max_eirp_dbm: i8,
207}
208
209#[repr(C)]
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub struct PamojaLoraBeacon {
213 pub frequency_hz: u32,
215 pub ping_slot_frequency_hz: u32,
217 pub data_rate: u8,
219}
220
221#[repr(C)]
223#[derive(Clone, Copy, Debug, PartialEq, Eq)]
224pub struct PamojaLoraRelayChannel {
225 pub wor_frequency_hz: u32,
227 pub ack_frequency_hz: u32,
229 pub data_rate: u8,
231}
232
233#[repr(C)]
235#[derive(Clone, Copy, Debug, PartialEq, Eq)]
236pub struct PamojaLoraPlanInfo {
237 pub rx2_frequency_hz: u32,
239 pub uplink_data_rate_count: u16,
241 pub downlink_data_rate_count: u16,
243 pub default_channel_count: u16,
245 pub join_channel_block_count: u16,
247 pub default_channel_block_count: u16,
249 pub sub_band_count: u16,
251 pub beacon: PamojaLoraBeacon,
253 pub rx2_data_rate: u8,
255 pub default_max_eirp_dbm: i8,
257 pub tx_power_step_db: u8,
259 pub max_tx_power_index: u8,
261 pub max_rx1_data_rate_offset: u8,
263 pub has_dwell_time_limit: u8,
265 pub has_dwell_limited_payloads: u8,
267 pub has_dwell_limited_rx1: u8,
270}
271
272#[repr(C)]
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub struct PamojaLoraPlanRules {
276 pub kind: u8,
278 pub channel_list: u8,
280 pub tx_param_setup: u8,
282 pub join_sequence: u8,
284 pub power_reference: u8,
286 pub gain_allowance_db: u8,
288 pub downlink_channel_block_count: u16,
290 pub join_plan_count: u16,
293}
294
295#[repr(C)]
297#[derive(Clone, Copy, Debug, PartialEq, Eq)]
298pub struct PamojaLoraMaskControl {
299 pub kind: u8,
301 pub group: u8,
303 pub on: u8,
305 pub has_then_group: u8,
307 pub then_group: u8,
309}
310
311#[repr(C)]
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub struct PamojaLoraJoinPlan {
315 pub channels: PamojaLoraChannelBlock,
317 pub accept_start_hz: u32,
319 pub accept_step_hz: u32,
321 pub rx2_start_hz: u32,
323 pub rx2_step_hz: u32,
325 pub cn470_plan: u32,
328}
329
330pub struct PamojaLoraPlan {
339 plan: OwnedChannelPlan,
340 published: Option<&'static ChannelPlan<'static>>,
341}
342
343impl PamojaLoraPlan {
344 fn into_handle(plan: OwnedChannelPlan) -> *mut Self {
354 Box::into_raw(Box::new(Self {
355 plan,
356 published: None,
357 }))
358 }
359
360 #[cfg_attr(
370 not(any(
371 feature = "eu868",
372 feature = "us915",
373 feature = "eu433",
374 feature = "au915",
375 feature = "cn470",
376 feature = "as923",
377 feature = "kr920",
378 feature = "in865",
379 feature = "ru864"
380 )),
381 allow(dead_code)
382 )]
383 fn published_handle(plan: &'static ChannelPlan<'static>) -> *mut Self {
384 Box::into_raw(Box::new(Self {
385 plan: OwnedChannelPlan::from_plan(plan),
386 published: Some(plan),
387 }))
388 }
389
390 pub(crate) fn published(&self) -> Option<&'static ChannelPlan<'static>> {
397 self.published
398 }
399
400 pub(crate) fn with<R>(&self, f: impl FnOnce(&ChannelPlan<'_>) -> R) -> R {
410 self.plan.with_plan(f)
411 }
412}
413
414fn data_rate_out(rate: Option<DataRate>) -> PamojaLoraDataRate {
424 let Some(rate) = rate else {
425 return PamojaLoraDataRate {
426 bitrate_bps: 0,
427 bandwidth_hz: 0,
428 kind: PAMOJA_LORA_MODULATION_RESERVED,
429 spreading_factor: 0,
430 coding_rate_numerator: 0,
431 coding_rate_denominator: 0,
432 };
433 };
434 let mut out = PamojaLoraDataRate {
435 bitrate_bps: rate.bitrate_bps,
436 bandwidth_hz: 0,
437 kind: PAMOJA_LORA_MODULATION_FSK,
438 spreading_factor: 0,
439 coding_rate_numerator: 0,
440 coding_rate_denominator: 0,
441 };
442 match rate.modulation {
443 Modulation::LoRa {
444 spreading_factor,
445 bandwidth_hz,
446 } => {
447 out.kind = PAMOJA_LORA_MODULATION_LORA;
448 out.spreading_factor = spreading_factor;
449 out.bandwidth_hz = bandwidth_hz;
450 }
451 Modulation::Fsk { .. } => {}
452 Modulation::LrFhss {
453 coding_rate_numerator,
454 coding_rate_denominator,
455 bandwidth_hz,
456 } => {
457 out.kind = PAMOJA_LORA_MODULATION_LR_FHSS;
458 out.coding_rate_numerator = coding_rate_numerator;
459 out.coding_rate_denominator = coding_rate_denominator;
460 out.bandwidth_hz = bandwidth_hz;
461 }
462 }
463 out
464}
465
466fn data_rate_in(rate: &PamojaLoraDataRate) -> Result<Option<DataRate>, PamojaStatus> {
477 match rate.kind {
478 PAMOJA_LORA_MODULATION_LORA => Ok(Some(DataRate::lora(
479 rate.spreading_factor,
480 rate.bandwidth_hz,
481 rate.bitrate_bps,
482 ))),
483 PAMOJA_LORA_MODULATION_FSK => Ok(Some(DataRate::fsk(rate.bitrate_bps))),
484 PAMOJA_LORA_MODULATION_LR_FHSS => Ok(Some(DataRate::lr_fhss(
485 rate.coding_rate_numerator,
486 rate.coding_rate_denominator,
487 rate.bandwidth_hz,
488 rate.bitrate_bps,
489 ))),
490 PAMOJA_LORA_MODULATION_RESERVED => Ok(None),
491 other => {
492 set_last_error(format!("{other} is not a modulation this build defines"));
493 Err(PamojaStatus::InvalidArgument)
494 }
495 }
496}
497
498fn is_region_code(region: u32) -> bool {
510 (PAMOJA_LORA_REGION_EU868..=PAMOJA_LORA_REGION_RU864).contains(®ion)
511}
512
513fn published(region: u32) -> Result<&'static ChannelPlan<'static>, PamojaStatus> {
528 let plan: Option<&'static ChannelPlan<'static>> = match region {
529 #[cfg(feature = "eu868")]
530 PAMOJA_LORA_REGION_EU868 => Some(Region::Eu868.plan()),
531 #[cfg(feature = "us915")]
532 PAMOJA_LORA_REGION_US915 => Some(Region::Us915.plan()),
533 #[cfg(feature = "eu433")]
534 PAMOJA_LORA_REGION_EU433 => Some(Region::Eu433.plan()),
535 #[cfg(feature = "au915")]
536 PAMOJA_LORA_REGION_AU915 => Some(Region::Au915.plan()),
537 #[cfg(feature = "cn470")]
538 PAMOJA_LORA_REGION_CN470 => Some(Region::Cn470.plan()),
539 #[cfg(feature = "as923")]
540 PAMOJA_LORA_REGION_AS923 => Some(Region::As923.plan()),
541 #[cfg(feature = "kr920")]
542 PAMOJA_LORA_REGION_KR920 => Some(Region::Kr920.plan()),
543 #[cfg(feature = "in865")]
544 PAMOJA_LORA_REGION_IN865 => Some(Region::In865.plan()),
545 #[cfg(feature = "ru864")]
546 PAMOJA_LORA_REGION_RU864 => Some(Region::Ru864.plan()),
547 _ => None,
548 };
549 match plan {
550 Some(plan) => Ok(plan),
551 None if is_region_code(region) => {
552 set_last_error(format!(
553 "region {region} is not compiled into this build of pamoja-lora"
554 ));
555 Err(PamojaStatus::Unsupported)
556 }
557 None => {
558 set_last_error(format!("{region} is not a region code"));
559 Err(PamojaStatus::InvalidArgument)
560 }
561 }
562}
563
564#[no_mangle]
585pub unsafe extern "C" fn pamoja_lora_plan_for_region(
586 region: u32,
587 out_plan: *mut *mut PamojaLoraPlan,
588) -> PamojaStatus {
589 if out_plan.is_null() {
590 set_last_error("out_plan must not be null".to_owned());
591 return PamojaStatus::InvalidArgument;
592 }
593 let slot = &mut *out_plan;
594 *slot = std::ptr::null_mut();
595
596 match published(region) {
597 Ok(plan) => {
598 *slot = PamojaLoraPlan::published_handle(plan);
599 PamojaStatus::Ok
600 }
601 Err(status) => status,
602 }
603}
604
605#[no_mangle]
626pub unsafe extern "C" fn pamoja_lora_plan_for_cn470(
627 which: u32,
628 out_plan: *mut *mut PamojaLoraPlan,
629) -> PamojaStatus {
630 if out_plan.is_null() {
631 set_last_error("out_plan must not be null".to_owned());
632 return PamojaStatus::InvalidArgument;
633 }
634 let slot = &mut *out_plan;
635 *slot = std::ptr::null_mut();
636 if !(PAMOJA_LORA_CN470_ANTENNA_20MHZ_A..=PAMOJA_LORA_CN470_CHANNELS_96).contains(&which) {
637 set_last_error(format!("{which} is not a CN470 plan"));
638 return PamojaStatus::InvalidArgument;
639 }
640
641 #[cfg(feature = "cn470")]
642 {
643 let plan = Cn470Plan::all()[(which - 1) as usize].plan();
644 *slot = PamojaLoraPlan::published_handle(plan);
645 PamojaStatus::Ok
646 }
647 #[cfg(not(feature = "cn470"))]
648 {
649 set_last_error("CN470-510 is not compiled into this build of pamoja-lora".to_owned());
650 PamojaStatus::Unsupported
651 }
652}
653
654fn cn470_code(target: &ChannelPlan<'_>) -> u32 {
656 #[cfg(feature = "cn470")]
657 {
658 if let Some(position) = Cn470Plan::all()
659 .iter()
660 .position(|plan| std::ptr::eq(plan.plan(), target))
661 {
662 return position as u32 + 1;
663 }
664 }
665 let _ = target;
666 PAMOJA_LORA_CN470_NONE
667}
668
669#[no_mangle]
689pub unsafe extern "C" fn pamoja_lora_plan_rules(
690 plan: *const PamojaLoraPlan,
691 out_rules: *mut PamojaLoraPlanRules,
692) -> PamojaStatus {
693 let (Some(handle), false) = (plan.as_ref(), out_rules.is_null()) else {
694 set_last_error("plan and out_rules must not be null".to_owned());
695 return PamojaStatus::InvalidArgument;
696 };
697 let join_plan_count = handle
698 .published
699 .map_or(0, |plan| plan.join_plans.len() as u16);
700 *out_rules = handle.with(|plan| {
701 let (kind, channel_list) = match plan.kind {
702 PlanKind::Dynamic { channel_list } => (
703 PAMOJA_LORA_PLAN_KIND_DYNAMIC,
704 match channel_list {
705 None => PAMOJA_LORA_CHANNEL_LIST_NONE,
706 Some(FixedChannelList::Mhz800) => PAMOJA_LORA_CHANNEL_LIST_MHZ800,
707 Some(FixedChannelList::Mhz900) => PAMOJA_LORA_CHANNEL_LIST_MHZ900,
708 },
709 ),
710 PlanKind::Fixed => (PAMOJA_LORA_PLAN_KIND_FIXED, PAMOJA_LORA_CHANNEL_LIST_NONE),
711 };
712 let (power_reference, gain_allowance_db) = match plan.power_reference {
713 PowerReference::Eirp => (PAMOJA_LORA_POWER_EIRP, 0),
714 PowerReference::Conducted { gain_allowance_db } => {
715 (PAMOJA_LORA_POWER_CONDUCTED, gain_allowance_db)
716 }
717 };
718 PamojaLoraPlanRules {
719 kind,
720 channel_list,
721 tx_param_setup: u8::from(plan.tx_param_setup),
722 join_sequence: match plan.join_sequence {
723 JoinSequence::Random => PAMOJA_LORA_JOIN_RANDOM,
724 JoinSequence::OctetPasses => PAMOJA_LORA_JOIN_OCTET_PASSES,
725 },
726 power_reference,
727 gain_allowance_db,
728 downlink_channel_block_count: plan.downlink_channels.len() as u16,
729 join_plan_count,
730 }
731 });
732 PamojaStatus::Ok
733}
734
735fn mask_control_out(control: MaskControl) -> PamojaLoraMaskControl {
737 let mut out = PamojaLoraMaskControl {
738 kind: PAMOJA_LORA_MASK_RESERVED,
739 group: 0,
740 on: 0,
741 has_then_group: 0,
742 then_group: 0,
743 };
744 match control {
745 MaskControl::Group(group) => {
746 out.kind = PAMOJA_LORA_MASK_GROUP;
747 out.group = group;
748 }
749 MaskControl::Banks => out.kind = PAMOJA_LORA_MASK_BANKS,
750 MaskControl::PairedBanks => out.kind = PAMOJA_LORA_MASK_PAIRED_BANKS,
751 MaskControl::All { on, then_group } => {
752 out.kind = PAMOJA_LORA_MASK_ALL;
753 out.on = u8::from(on);
754 out.has_then_group = u8::from(then_group.is_some());
755 out.then_group = then_group.unwrap_or(0);
756 }
757 MaskControl::Reserved => {}
758 }
759 out
760}
761
762fn mask_control_in(control: &PamojaLoraMaskControl) -> Result<MaskControl, PamojaStatus> {
764 match control.kind {
765 PAMOJA_LORA_MASK_GROUP => Ok(MaskControl::Group(control.group)),
766 PAMOJA_LORA_MASK_BANKS => Ok(MaskControl::Banks),
767 PAMOJA_LORA_MASK_PAIRED_BANKS => Ok(MaskControl::PairedBanks),
768 PAMOJA_LORA_MASK_ALL => Ok(MaskControl::All {
769 on: control.on != 0,
770 then_group: (control.has_then_group != 0).then_some(control.then_group),
771 }),
772 PAMOJA_LORA_MASK_RESERVED => Ok(MaskControl::Reserved),
773 other => {
774 set_last_error(format!("{other} is not a channel mask control"));
775 Err(PamojaStatus::InvalidArgument)
776 }
777 }
778}
779
780#[no_mangle]
801pub unsafe extern "C" fn pamoja_lora_plan_mask_control(
802 plan: *const PamojaLoraPlan,
803 value: u8,
804 out_control: *mut PamojaLoraMaskControl,
805) -> PamojaStatus {
806 let (Some(plan), false) = (plan.as_ref(), out_control.is_null()) else {
807 set_last_error("plan and out_control must not be null".to_owned());
808 return PamojaStatus::InvalidArgument;
809 };
810 if value > 7 {
811 set_last_error(format!(
812 "{value} is not a ChMaskCntl value, which is three bits"
813 ));
814 return PamojaStatus::InvalidArgument;
815 }
816 *out_control = plan.with(|plan| mask_control_out(plan.mask_controls[usize::from(value)]));
817 PamojaStatus::Ok
818}
819
820#[no_mangle]
844pub unsafe extern "C" fn pamoja_lora_plan_rx1_frequency_hz(
845 plan: *const PamojaLoraPlan,
846 uplink_channel: u16,
847 uplink_hz: u32,
848 out_frequency_hz: *mut u32,
849) -> PamojaStatus {
850 let (Some(plan), false) = (plan.as_ref(), out_frequency_hz.is_null()) else {
851 set_last_error("plan and out_frequency_hz must not be null".to_owned());
852 return PamojaStatus::InvalidArgument;
853 };
854 let Some(frequency) = plan.with(|plan| plan.rx1_frequency_hz(uplink_channel, uplink_hz)) else {
855 set_last_error(format!(
856 "this plan names no downlink for uplink channel {uplink_channel}"
857 ));
858 return PamojaStatus::InvalidArgument;
859 };
860 *out_frequency_hz = frequency;
861 PamojaStatus::Ok
862}
863
864#[no_mangle]
886pub unsafe extern "C" fn pamoja_lora_plan_downlink_channel_frequency_hz(
887 plan: *const PamojaLoraPlan,
888 channel: u16,
889 out_frequency_hz: *mut u32,
890) -> PamojaStatus {
891 let (Some(plan), false) = (plan.as_ref(), out_frequency_hz.is_null()) else {
892 set_last_error("plan and out_frequency_hz must not be null".to_owned());
893 return PamojaStatus::InvalidArgument;
894 };
895 let Some(frequency) = plan.with(|plan| plan.downlink_channel_frequency_hz(channel)) else {
896 set_last_error(format!("this plan has no downlink channel {channel}"));
897 return PamojaStatus::InvalidArgument;
898 };
899 *out_frequency_hz = frequency;
900 PamojaStatus::Ok
901}
902
903#[no_mangle]
925pub unsafe extern "C" fn pamoja_lora_plan_join_plan(
926 plan: *const PamojaLoraPlan,
927 index: u16,
928 out_join_plan: *mut PamojaLoraJoinPlan,
929) -> PamojaStatus {
930 let (Some(handle), false) = (plan.as_ref(), out_join_plan.is_null()) else {
931 set_last_error("plan and out_join_plan must not be null".to_owned());
932 return PamojaStatus::InvalidArgument;
933 };
934 let Some(run) = handle
935 .published
936 .and_then(|plan| plan.join_plans.get(usize::from(index)))
937 else {
938 set_last_error(format!("this plan has no join plan {index}"));
939 return PamojaStatus::InvalidArgument;
940 };
941 *out_join_plan = PamojaLoraJoinPlan {
942 channels: PamojaLoraChannelBlock {
943 start_hz: run.channels.start_hz,
944 step_hz: run.channels.step_hz,
945 count: run.channels.count,
946 min_data_rate: run.channels.min_data_rate,
947 max_data_rate: run.channels.max_data_rate,
948 },
949 accept_start_hz: run.accept_start_hz,
950 accept_step_hz: run.accept_step_hz,
951 rx2_start_hz: run.rx2_start_hz,
952 rx2_step_hz: run.rx2_step_hz,
953 cn470_plan: cn470_code(run.plan),
954 };
955 PamojaStatus::Ok
956}
957
958#[no_mangle]
983pub unsafe extern "C" fn pamoja_lora_plan_join_plan_for_channel(
984 plan: *const PamojaLoraPlan,
985 join_channel: u16,
986 out_index: *mut u16,
987 out_offset: *mut u16,
988) -> PamojaStatus {
989 let (Some(handle), false, false) = (plan.as_ref(), out_index.is_null(), out_offset.is_null())
990 else {
991 set_last_error("plan, out_index and out_offset must not be null".to_owned());
992 return PamojaStatus::InvalidArgument;
993 };
994 let mut remaining = join_channel;
995 for (index, run) in handle
996 .published
997 .map_or(&[][..], |plan| plan.join_plans)
998 .iter()
999 .enumerate()
1000 {
1001 if remaining < run.channels.count {
1002 *out_index = index as u16;
1003 *out_offset = remaining;
1004 return PamojaStatus::Ok;
1005 }
1006 remaining -= run.channels.count;
1007 }
1008 set_last_error(format!("no join plan holds join channel {join_channel}"));
1009 PamojaStatus::InvalidArgument
1010}
1011
1012#[no_mangle]
1026pub extern "C" fn pamoja_lora_region_is_available(region: u32) -> u8 {
1027 u8::from(published(region).is_ok())
1028}
1029
1030#[no_mangle]
1041pub unsafe extern "C" fn pamoja_lora_plan_free(plan: *mut PamojaLoraPlan) {
1042 if !plan.is_null() {
1043 drop(Box::from_raw(plan));
1044 }
1045}
1046
1047#[no_mangle]
1062pub unsafe extern "C" fn pamoja_lora_plan_name(plan: *const PamojaLoraPlan) -> *mut PamojaString {
1063 let Some(plan) = plan.as_ref() else {
1064 set_last_error("plan must not be null".to_owned());
1065 return std::ptr::null_mut();
1066 };
1067 plan.with(|plan| PamojaString::into_raw(plan.name.to_owned()))
1068}
1069
1070#[no_mangle]
1090pub unsafe extern "C" fn pamoja_lora_plan_info(
1091 plan: *const PamojaLoraPlan,
1092 out_info: *mut PamojaLoraPlanInfo,
1093) -> PamojaStatus {
1094 let (Some(plan), false) = (plan.as_ref(), out_info.is_null()) else {
1095 set_last_error("plan and out_info must not be null".to_owned());
1096 return PamojaStatus::InvalidArgument;
1097 };
1098 *out_info = plan.with(|plan| PamojaLoraPlanInfo {
1099 rx2_frequency_hz: plan.rx2_frequency_hz,
1100 uplink_data_rate_count: plan.uplink_data_rates.len() as u16,
1101 downlink_data_rate_count: plan.downlink_data_rates.len() as u16,
1102 default_channel_count: plan.default_channel_count(),
1103 join_channel_block_count: plan.join_channels.len() as u16,
1104 default_channel_block_count: plan.default_channels.len() as u16,
1105 sub_band_count: plan.sub_bands.len() as u16,
1106 beacon: PamojaLoraBeacon {
1107 frequency_hz: plan.beacon.frequency_hz,
1108 ping_slot_frequency_hz: plan.beacon.ping_slot_frequency_hz,
1109 data_rate: plan.beacon.data_rate,
1110 },
1111 rx2_data_rate: plan.rx2_data_rate,
1112 default_max_eirp_dbm: plan.default_max_eirp_dbm,
1113 tx_power_step_db: plan.tx_power_step_db,
1114 max_tx_power_index: plan.max_tx_power_index,
1115 max_rx1_data_rate_offset: plan.max_rx1_data_rate_offset,
1116 has_dwell_time_limit: u8::from(plan.has_dwell_time_limit),
1117 has_dwell_limited_payloads: u8::from(plan.max_payload_dwell_limited.is_some()),
1118 has_dwell_limited_rx1: u8::from(plan.rx1_data_rate_offsets_dwell_limited.is_some()),
1119 });
1120 PamojaStatus::Ok
1121}
1122
1123#[no_mangle]
1149pub unsafe extern "C" fn pamoja_lora_plan_data_rate(
1150 plan: *const PamojaLoraPlan,
1151 direction: u32,
1152 data_rate: u8,
1153 out_rate: *mut PamojaLoraDataRate,
1154) -> PamojaStatus {
1155 let (Some(plan), false) = (plan.as_ref(), out_rate.is_null()) else {
1156 set_last_error("plan and out_rate must not be null".to_owned());
1157 return PamojaStatus::InvalidArgument;
1158 };
1159 let found = plan.with(|plan| match direction {
1160 PAMOJA_LORA_DIRECTION_UPLINK => Ok(plan
1161 .uplink_data_rates
1162 .get(usize::from(data_rate))
1163 .copied()
1164 .map(data_rate_out)),
1165 PAMOJA_LORA_DIRECTION_DOWNLINK => Ok(plan
1166 .downlink_data_rates
1167 .get(usize::from(data_rate))
1168 .copied()
1169 .map(data_rate_out)),
1170 other => Err(other),
1171 });
1172 match found {
1173 Ok(Some(rate)) => {
1174 *out_rate = rate;
1175 PamojaStatus::Ok
1176 }
1177 Ok(None) => {
1178 set_last_error(format!("this plan defines no data rate {data_rate}"));
1179 PamojaStatus::InvalidArgument
1180 }
1181 Err(other) => {
1182 set_last_error(format!("{other} is not a direction"));
1183 PamojaStatus::InvalidArgument
1184 }
1185 }
1186}
1187
1188#[no_mangle]
1215pub unsafe extern "C" fn pamoja_lora_plan_link_settings(
1216 plan: *const PamojaLoraPlan,
1217 data_rate: u8,
1218 out_link: *mut PamojaLoraLink,
1219) -> PamojaStatus {
1220 let (Some(plan), false) = (plan.as_ref(), out_link.is_null()) else {
1221 set_last_error("plan and out_link must not be null".to_owned());
1222 return PamojaStatus::InvalidArgument;
1223 };
1224 let Some(settings) = plan.with(|plan| plan.link_settings(data_rate)) else {
1225 set_last_error(format!(
1226 "data rate {data_rate} is reserved or is not carried by LoRa in this plan"
1227 ));
1228 return PamojaStatus::Unsupported;
1229 };
1230 *out_link = PamojaLoraLink {
1231 bandwidth_hz: settings.bandwidth_hz(),
1232 preamble_symbols: 8,
1233 spreading_factor: settings.spreading_factor(),
1234 coding_rate_denominator: 5,
1235 explicit_header: 1,
1236 crc: 1,
1237 };
1238 PamojaStatus::Ok
1239}
1240
1241#[no_mangle]
1266pub unsafe extern "C" fn pamoja_lora_plan_max_payload(
1267 plan: *const PamojaLoraPlan,
1268 table: u32,
1269 data_rate: u8,
1270 out_payload: *mut PamojaLoraMaxPayload,
1271) -> PamojaStatus {
1272 let (Some(plan), false) = (plan.as_ref(), out_payload.is_null()) else {
1273 set_last_error("plan and out_payload must not be null".to_owned());
1274 return PamojaStatus::InvalidArgument;
1275 };
1276 let found = plan.with(|plan| match table {
1277 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_REPEATER => Ok(plan.max_payload(data_rate, true)),
1278 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_DIRECT => Ok(plan.max_payload(data_rate, false)),
1279 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_REPEATER => {
1280 Ok(plan.downlink_max_payload(data_rate, true))
1281 }
1282 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_DIRECT => {
1283 Ok(plan.downlink_max_payload(data_rate, false))
1284 }
1285 PAMOJA_LORA_PAYLOAD_TABLE_DWELL_LIMITED => {
1286 if plan.max_payload_dwell_limited.is_none() {
1287 Err(PamojaStatus::Unsupported)
1288 } else {
1289 Ok(plan.max_payload_dwell_limited(data_rate))
1290 }
1291 }
1292 _ => Err(PamojaStatus::InvalidArgument),
1293 });
1294 match found {
1295 Ok(Some(payload)) => {
1296 *out_payload = PamojaLoraMaxPayload {
1297 mac_payload: payload.mac_payload,
1298 application: payload.application,
1299 };
1300 PamojaStatus::Ok
1301 }
1302 Ok(None) => {
1303 set_last_error(format!(
1304 "this plan publishes no payload limit for data rate {data_rate}"
1305 ));
1306 PamojaStatus::InvalidArgument
1307 }
1308 Err(PamojaStatus::Unsupported) => {
1309 set_last_error("this plan has no dwell-time limit".to_owned());
1310 PamojaStatus::Unsupported
1311 }
1312 Err(_) => {
1313 set_last_error(format!("{table} is not a payload table"));
1314 PamojaStatus::InvalidArgument
1315 }
1316 }
1317}
1318
1319#[no_mangle]
1347pub unsafe extern "C" fn pamoja_lora_plan_duty_cycle_permille(
1348 plan: *const PamojaLoraPlan,
1349 frequency_hz: u32,
1350 out_permille: *mut u32,
1351) -> PamojaStatus {
1352 let (Some(plan), false) = (plan.as_ref(), out_permille.is_null()) else {
1353 set_last_error("plan and out_permille must not be null".to_owned());
1354 return PamojaStatus::InvalidArgument;
1355 };
1356 let Some(permille) = plan.with(|plan| plan.duty_cycle_permille(frequency_hz)) else {
1357 set_last_error(format!(
1358 "{frequency_hz} Hz falls in no sub-band this plan describes"
1359 ));
1360 return PamojaStatus::Unsupported;
1361 };
1362 *out_permille = permille;
1363 PamojaStatus::Ok
1364}
1365
1366#[no_mangle]
1388pub unsafe extern "C" fn pamoja_lora_plan_max_eirp_dbm(
1389 plan: *const PamojaLoraPlan,
1390 frequency_hz: u32,
1391 out_dbm: *mut i8,
1392) -> PamojaStatus {
1393 let (Some(plan), false) = (plan.as_ref(), out_dbm.is_null()) else {
1394 set_last_error("plan and out_dbm must not be null".to_owned());
1395 return PamojaStatus::InvalidArgument;
1396 };
1397 *out_dbm = plan.with(|plan| plan.max_eirp_dbm(frequency_hz));
1398 PamojaStatus::Ok
1399}
1400
1401#[no_mangle]
1425pub unsafe extern "C" fn pamoja_lora_plan_tx_power_dbm(
1426 plan: *const PamojaLoraPlan,
1427 index: u8,
1428 max_eirp_dbm: i8,
1429 out_dbm: *mut i8,
1430) -> PamojaStatus {
1431 let (Some(plan), false) = (plan.as_ref(), out_dbm.is_null()) else {
1432 set_last_error("plan and out_dbm must not be null".to_owned());
1433 return PamojaStatus::InvalidArgument;
1434 };
1435 let Some(dbm) = plan.with(|plan| plan.tx_power_dbm(index, max_eirp_dbm)) else {
1436 set_last_error(format!("this plan defines no transmit-power index {index}"));
1437 return PamojaStatus::InvalidArgument;
1438 };
1439 *out_dbm = dbm;
1440 PamojaStatus::Ok
1441}
1442
1443#[no_mangle]
1470pub unsafe extern "C" fn pamoja_lora_plan_rx1_data_rate(
1471 plan: *const PamojaLoraPlan,
1472 uplink_data_rate: u8,
1473 offset: u8,
1474 dwell_limited: u8,
1475 out_data_rate: *mut u8,
1476) -> PamojaStatus {
1477 let (Some(plan), false) = (plan.as_ref(), out_data_rate.is_null()) else {
1478 set_last_error("plan and out_data_rate must not be null".to_owned());
1479 return PamojaStatus::InvalidArgument;
1480 };
1481 let wants_dwell = dwell_limited != 0;
1482 if wants_dwell && plan.with(|plan| plan.rx1_data_rate_offsets_dwell_limited.is_none()) {
1483 set_last_error("this plan publishes no dwell-limited RX1 mapping".to_owned());
1484 return PamojaStatus::Unsupported;
1485 }
1486 let found = plan.with(|plan| {
1487 if wants_dwell {
1488 plan.rx1_data_rate_dwell_limited(uplink_data_rate, offset)
1489 } else {
1490 plan.rx1_data_rate(uplink_data_rate, offset)
1491 }
1492 });
1493 let Some(data_rate) = found else {
1494 set_last_error(format!(
1495 "this plan maps no RX1 downlink for uplink data rate {uplink_data_rate} at offset {offset}"
1496 ));
1497 return PamojaStatus::InvalidArgument;
1498 };
1499 *out_data_rate = data_rate;
1500 PamojaStatus::Ok
1501}
1502
1503#[no_mangle]
1529pub unsafe extern "C" fn pamoja_lora_plan_next_backoff_data_rate(
1530 plan: *const PamojaLoraPlan,
1531 data_rate: u8,
1532 out_data_rate: *mut u8,
1533) -> PamojaStatus {
1534 let (Some(plan), false) = (plan.as_ref(), out_data_rate.is_null()) else {
1535 set_last_error("plan and out_data_rate must not be null".to_owned());
1536 return PamojaStatus::InvalidArgument;
1537 };
1538 let known = plan.with(|plan| usize::from(data_rate) < plan.data_rate_backoff.len());
1539 if !known {
1540 set_last_error(format!("this plan defines no data rate {data_rate}"));
1541 return PamojaStatus::InvalidArgument;
1542 }
1543 let Some(lower) = plan.with(|plan| plan.next_backoff_data_rate(data_rate)) else {
1544 set_last_error(format!(
1545 "data rate {data_rate} is the slowest this plan has"
1546 ));
1547 return PamojaStatus::Unsupported;
1548 };
1549 *out_data_rate = lower;
1550 PamojaStatus::Ok
1551}
1552
1553#[no_mangle]
1575pub unsafe extern "C" fn pamoja_lora_plan_channel_frequency_hz(
1576 plan: *const PamojaLoraPlan,
1577 channel: u16,
1578 out_frequency_hz: *mut u32,
1579) -> PamojaStatus {
1580 let (Some(plan), false) = (plan.as_ref(), out_frequency_hz.is_null()) else {
1581 set_last_error("plan and out_frequency_hz must not be null".to_owned());
1582 return PamojaStatus::InvalidArgument;
1583 };
1584 let Some(frequency) = plan.with(|plan| plan.channel_frequency_hz(channel)) else {
1585 set_last_error(format!("this plan has no default channel {channel}"));
1586 return PamojaStatus::InvalidArgument;
1587 };
1588 *out_frequency_hz = frequency;
1589 PamojaStatus::Ok
1590}
1591
1592#[no_mangle]
1617pub unsafe extern "C" fn pamoja_lora_plan_channel_block(
1618 plan: *const PamojaLoraPlan,
1619 which: u32,
1620 index: u16,
1621 out_block: *mut PamojaLoraChannelBlock,
1622) -> PamojaStatus {
1623 let (Some(plan), false) = (plan.as_ref(), out_block.is_null()) else {
1624 set_last_error("plan and out_block must not be null".to_owned());
1625 return PamojaStatus::InvalidArgument;
1626 };
1627 if !(PAMOJA_LORA_CHANNELS_JOIN..=PAMOJA_LORA_CHANNELS_DOWNLINK).contains(&which) {
1628 set_last_error(format!("{which} is not a channel set"));
1629 return PamojaStatus::InvalidArgument;
1630 }
1631 let found = plan.with(|plan| {
1632 let blocks = match which {
1633 PAMOJA_LORA_CHANNELS_JOIN => plan.join_channels,
1634 PAMOJA_LORA_CHANNELS_DEFAULT => plan.default_channels,
1635 _ => plan.downlink_channels,
1636 };
1637 blocks.get(usize::from(index)).copied()
1638 });
1639 let Some(block) = found else {
1640 set_last_error(format!("this plan has no channel block {index}"));
1641 return PamojaStatus::InvalidArgument;
1642 };
1643 *out_block = PamojaLoraChannelBlock {
1644 start_hz: block.start_hz,
1645 step_hz: block.step_hz,
1646 count: block.count,
1647 min_data_rate: block.min_data_rate,
1648 max_data_rate: block.max_data_rate,
1649 };
1650 PamojaStatus::Ok
1651}
1652
1653#[no_mangle]
1673pub unsafe extern "C" fn pamoja_lora_plan_relay_channel_count(
1674 plan: *const PamojaLoraPlan,
1675 out_count: *mut u8,
1676) -> PamojaStatus {
1677 let (Some(plan), false) = (plan.as_ref(), out_count.is_null()) else {
1678 set_last_error("plan and out_count must not be null".to_owned());
1679 return PamojaStatus::InvalidArgument;
1680 };
1681 *out_count = plan.with(|plan| plan.relay_channels.len().min(usize::from(u8::MAX)) as u8);
1682 PamojaStatus::Ok
1683}
1684
1685#[no_mangle]
1708pub unsafe extern "C" fn pamoja_lora_plan_relay_channel(
1709 plan: *const PamojaLoraPlan,
1710 index: u8,
1711 out_channel: *mut PamojaLoraRelayChannel,
1712) -> PamojaStatus {
1713 let (Some(plan), false) = (plan.as_ref(), out_channel.is_null()) else {
1714 set_last_error("plan and out_channel must not be null".to_owned());
1715 return PamojaStatus::InvalidArgument;
1716 };
1717 let Some(channel) = plan.with(|plan| plan.relay_channel(index)) else {
1718 set_last_error(format!("this plan has no relay channel {index}"));
1719 return PamojaStatus::InvalidArgument;
1720 };
1721 *out_channel = PamojaLoraRelayChannel {
1722 wor_frequency_hz: channel.wor_frequency_hz,
1723 ack_frequency_hz: channel.ack_frequency_hz,
1724 data_rate: channel.data_rate,
1725 };
1726 PamojaStatus::Ok
1727}
1728
1729#[no_mangle]
1752pub unsafe extern "C" fn pamoja_lora_plan_sub_band(
1753 plan: *const PamojaLoraPlan,
1754 index: u16,
1755 out_band: *mut PamojaLoraSubBand,
1756) -> PamojaStatus {
1757 let (Some(plan), false) = (plan.as_ref(), out_band.is_null()) else {
1758 set_last_error("plan and out_band must not be null".to_owned());
1759 return PamojaStatus::InvalidArgument;
1760 };
1761 let found = plan.with(|plan| plan.sub_bands.get(usize::from(index)).copied());
1762 let Some(band) = found else {
1763 set_last_error(format!("this plan has no sub-band {index}"));
1764 return PamojaStatus::InvalidArgument;
1765 };
1766 *out_band = PamojaLoraSubBand {
1767 start_hz: band.start_hz,
1768 end_hz: band.end_hz,
1769 duty_cycle_permille: band.duty_cycle_permille,
1770 max_eirp_dbm: band.max_eirp_dbm,
1771 };
1772 PamojaStatus::Ok
1773}
1774
1775pub struct PamojaLoraPlanBuilder {
1780 builder: Option<ChannelPlanBuilder>,
1781}
1782
1783fn update(
1801 builder: &mut PamojaLoraPlanBuilder,
1802 step: impl FnOnce(ChannelPlanBuilder) -> ChannelPlanBuilder,
1803) -> PamojaStatus {
1804 let Some(inner) = builder.builder.take() else {
1805 set_last_error("this builder has already been built".to_owned());
1806 return PamojaStatus::Closed;
1807 };
1808 builder.builder = Some(step(inner));
1809 PamojaStatus::Ok
1810}
1811
1812fn payload_table(table: u32) -> Option<PayloadTable> {
1822 match table {
1823 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_REPEATER => Some(PayloadTable::UplinkRepeater),
1824 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_DIRECT => Some(PayloadTable::UplinkDirect),
1825 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_REPEATER => Some(PayloadTable::DownlinkRepeater),
1826 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_DIRECT => Some(PayloadTable::DownlinkDirect),
1827 PAMOJA_LORA_PAYLOAD_TABLE_DWELL_LIMITED => Some(PayloadTable::DwellLimited),
1828 _ => None,
1829 }
1830}
1831
1832#[no_mangle]
1856pub unsafe extern "C" fn pamoja_lora_plan_builder_new(
1857 name: *const std::os::raw::c_char,
1858 out_builder: *mut *mut PamojaLoraPlanBuilder,
1859) -> PamojaStatus {
1860 if out_builder.is_null() {
1861 set_last_error("out_builder must not be null".to_owned());
1862 return PamojaStatus::InvalidArgument;
1863 }
1864 let slot = &mut *out_builder;
1865 *slot = std::ptr::null_mut();
1866
1867 let Some(name) = crate::read_str(name, "name") else {
1868 return PamojaStatus::InvalidArgument;
1869 };
1870
1871 *slot = Box::into_raw(Box::new(PamojaLoraPlanBuilder {
1872 builder: Some(ChannelPlanBuilder::new(name)),
1873 }));
1874 PamojaStatus::Ok
1875}
1876
1877#[no_mangle]
1889pub unsafe extern "C" fn pamoja_lora_plan_builder_free(builder: *mut PamojaLoraPlanBuilder) {
1890 if !builder.is_null() {
1891 drop(Box::from_raw(builder));
1892 }
1893}
1894
1895#[no_mangle]
1924pub unsafe extern "C" fn pamoja_lora_plan_builder_push_data_rate(
1925 builder: *mut PamojaLoraPlanBuilder,
1926 direction: u32,
1927 rate: *const PamojaLoraDataRate,
1928) -> PamojaStatus {
1929 let (Some(builder), Some(rate)) = (builder.as_mut(), rate.as_ref()) else {
1930 set_last_error("builder and rate must not be null".to_owned());
1931 return PamojaStatus::InvalidArgument;
1932 };
1933 let rate = match data_rate_in(rate) {
1934 Ok(rate) => rate,
1935 Err(status) => return status,
1936 };
1937 match direction {
1938 PAMOJA_LORA_DIRECTION_UPLINK => update(builder, |b| b.uplink_data_rate(rate)),
1939 PAMOJA_LORA_DIRECTION_DOWNLINK => update(builder, |b| b.downlink_data_rate(rate)),
1940 other => {
1941 set_last_error(format!("{other} is not a direction"));
1942 PamojaStatus::InvalidArgument
1943 }
1944 }
1945}
1946
1947#[no_mangle]
1975pub unsafe extern "C" fn pamoja_lora_plan_builder_push_max_payload(
1976 builder: *mut PamojaLoraPlanBuilder,
1977 table: u32,
1978 present: u8,
1979 mac_payload: u16,
1980 application: u16,
1981) -> PamojaStatus {
1982 let Some(builder) = builder.as_mut() else {
1983 set_last_error("builder must not be null".to_owned());
1984 return PamojaStatus::InvalidArgument;
1985 };
1986 let Some(table) = payload_table(table) else {
1987 set_last_error(format!("{table} is not a payload table"));
1988 return PamojaStatus::InvalidArgument;
1989 };
1990 let entry = (present != 0).then(|| MaxPayload::new(mac_payload, application));
1991 update(builder, |b| b.max_payload(table, entry))
1992}
1993
1994#[no_mangle]
2018pub unsafe extern "C" fn pamoja_lora_plan_builder_push_channel_block(
2019 builder: *mut PamojaLoraPlanBuilder,
2020 which: u32,
2021 block: *const PamojaLoraChannelBlock,
2022) -> PamojaStatus {
2023 let (Some(builder), Some(block)) = (builder.as_mut(), block.as_ref()) else {
2024 set_last_error("builder and block must not be null".to_owned());
2025 return PamojaStatus::InvalidArgument;
2026 };
2027 let entry = ChannelBlock::new(
2028 block.start_hz,
2029 block.step_hz,
2030 block.count,
2031 block.min_data_rate,
2032 block.max_data_rate,
2033 );
2034 match which {
2035 PAMOJA_LORA_CHANNELS_JOIN => update(builder, |b| b.join_channel(entry)),
2036 PAMOJA_LORA_CHANNELS_DEFAULT => update(builder, |b| b.default_channel(entry)),
2037 PAMOJA_LORA_CHANNELS_DOWNLINK => update(builder, |b| b.downlink_channel(entry)),
2038 other => {
2039 set_last_error(format!("{other} is not a channel set"));
2040 PamojaStatus::InvalidArgument
2041 }
2042 }
2043}
2044
2045#[no_mangle]
2069pub unsafe extern "C" fn pamoja_lora_plan_builder_push_sub_band(
2070 builder: *mut PamojaLoraPlanBuilder,
2071 band: *const PamojaLoraSubBand,
2072) -> PamojaStatus {
2073 let (Some(builder), Some(band)) = (builder.as_mut(), band.as_ref()) else {
2074 set_last_error("builder and band must not be null".to_owned());
2075 return PamojaStatus::InvalidArgument;
2076 };
2077 let entry = SubBand::new(
2078 band.start_hz,
2079 band.end_hz,
2080 band.duty_cycle_permille,
2081 band.max_eirp_dbm,
2082 );
2083 update(builder, |b| b.sub_band(entry))
2084}
2085
2086#[no_mangle]
2113pub unsafe extern "C" fn pamoja_lora_plan_builder_push_rx1_row(
2114 builder: *mut PamojaLoraPlanBuilder,
2115 dwell_limited: u8,
2116 offsets: *const u8,
2117 offsets_len: usize,
2118) -> PamojaStatus {
2119 let Some(builder) = builder.as_mut() else {
2120 set_last_error("builder must not be null".to_owned());
2121 return PamojaStatus::InvalidArgument;
2122 };
2123 let row = match crate::read_bytes(offsets, offsets_len) {
2124 Ok(row) => row,
2125 Err(status) => return status,
2126 };
2127 if dwell_limited == 0 {
2128 update(builder, |b| b.rx1_row(&row))
2129 } else {
2130 update(builder, |b| b.rx1_row_dwell_limited(&row))
2131 }
2132}
2133
2134#[no_mangle]
2158pub unsafe extern "C" fn pamoja_lora_plan_builder_push_backoff(
2159 builder: *mut PamojaLoraPlanBuilder,
2160 has_lower: u8,
2161 data_rate: u8,
2162) -> PamojaStatus {
2163 let Some(builder) = builder.as_mut() else {
2164 set_last_error("builder must not be null".to_owned());
2165 return PamojaStatus::InvalidArgument;
2166 };
2167 let lower = (has_lower != 0).then_some(data_rate);
2168 update(builder, |b| b.backoff(lower))
2169}
2170
2171#[no_mangle]
2194pub unsafe extern "C" fn pamoja_lora_plan_builder_set_power(
2195 builder: *mut PamojaLoraPlanBuilder,
2196 default_max_eirp_dbm: i8,
2197 tx_power_step_db: u8,
2198 max_tx_power_index: u8,
2199) -> PamojaStatus {
2200 let Some(builder) = builder.as_mut() else {
2201 set_last_error("builder must not be null".to_owned());
2202 return PamojaStatus::InvalidArgument;
2203 };
2204 update(builder, |b| {
2205 b.power(default_max_eirp_dbm, tx_power_step_db, max_tx_power_index)
2206 })
2207}
2208
2209#[no_mangle]
2232pub unsafe extern "C" fn pamoja_lora_plan_builder_set_rx(
2233 builder: *mut PamojaLoraPlanBuilder,
2234 rx2_frequency_hz: u32,
2235 rx2_data_rate: u8,
2236 max_rx1_data_rate_offset: u8,
2237) -> PamojaStatus {
2238 let Some(builder) = builder.as_mut() else {
2239 set_last_error("builder must not be null".to_owned());
2240 return PamojaStatus::InvalidArgument;
2241 };
2242 update(builder, |b| {
2243 b.rx(rx2_frequency_hz, rx2_data_rate, max_rx1_data_rate_offset)
2244 })
2245}
2246
2247#[no_mangle]
2270pub unsafe extern "C" fn pamoja_lora_plan_builder_set_beacon(
2271 builder: *mut PamojaLoraPlanBuilder,
2272 beacon: *const PamojaLoraBeacon,
2273 has_dwell_time_limit: u8,
2274) -> PamojaStatus {
2275 let (Some(builder), Some(beacon)) = (builder.as_mut(), beacon.as_ref()) else {
2276 set_last_error("builder and beacon must not be null".to_owned());
2277 return PamojaStatus::InvalidArgument;
2278 };
2279 let entry = Beacon {
2280 data_rate: beacon.data_rate,
2281 frequency_hz: beacon.frequency_hz,
2282 ping_slot_frequency_hz: beacon.ping_slot_frequency_hz,
2283 };
2284 update(builder, |b| {
2285 b.beacon(entry).dwell_time_limit(has_dwell_time_limit != 0)
2286 })
2287}
2288
2289#[no_mangle]
2311pub unsafe extern "C" fn pamoja_lora_plan_builder_add_relay_channel(
2312 builder: *mut PamojaLoraPlanBuilder,
2313 channel: *const PamojaLoraRelayChannel,
2314) -> PamojaStatus {
2315 let (Some(builder), Some(channel)) = (builder.as_mut(), channel.as_ref()) else {
2316 set_last_error("builder and channel must not be null".to_owned());
2317 return PamojaStatus::InvalidArgument;
2318 };
2319 let entry = RelayChannel::new(
2320 channel.wor_frequency_hz,
2321 channel.ack_frequency_hz,
2322 channel.data_rate,
2323 );
2324 update(builder, |b| b.relay_channel(entry))
2325}
2326
2327#[no_mangle]
2350pub unsafe extern "C" fn pamoja_lora_plan_builder_set_kind(
2351 builder: *mut PamojaLoraPlanBuilder,
2352 kind: u8,
2353 channel_list: u8,
2354) -> PamojaStatus {
2355 let Some(builder) = builder.as_mut() else {
2356 set_last_error("builder must not be null".to_owned());
2357 return PamojaStatus::InvalidArgument;
2358 };
2359 let kind = match (kind, channel_list) {
2360 (PAMOJA_LORA_PLAN_KIND_FIXED, _) => PlanKind::Fixed,
2361 (PAMOJA_LORA_PLAN_KIND_DYNAMIC, PAMOJA_LORA_CHANNEL_LIST_NONE) => {
2362 PlanKind::Dynamic { channel_list: None }
2363 }
2364 (PAMOJA_LORA_PLAN_KIND_DYNAMIC, PAMOJA_LORA_CHANNEL_LIST_MHZ800) => PlanKind::Dynamic {
2365 channel_list: Some(FixedChannelList::Mhz800),
2366 },
2367 (PAMOJA_LORA_PLAN_KIND_DYNAMIC, PAMOJA_LORA_CHANNEL_LIST_MHZ900) => PlanKind::Dynamic {
2368 channel_list: Some(FixedChannelList::Mhz900),
2369 },
2370 _ => {
2371 set_last_error(format!(
2372 "{kind} with channel list {channel_list} is not a plan kind"
2373 ));
2374 return PamojaStatus::InvalidArgument;
2375 }
2376 };
2377 update(builder, |b| b.kind(kind))
2378}
2379
2380#[no_mangle]
2400pub unsafe extern "C" fn pamoja_lora_plan_builder_set_tx_param_setup(
2401 builder: *mut PamojaLoraPlanBuilder,
2402 answered: u8,
2403) -> PamojaStatus {
2404 let Some(builder) = builder.as_mut() else {
2405 set_last_error("builder must not be null".to_owned());
2406 return PamojaStatus::InvalidArgument;
2407 };
2408 update(builder, |b| b.tx_param_setup(answered != 0))
2409}
2410
2411#[no_mangle]
2434pub unsafe extern "C" fn pamoja_lora_plan_builder_set_mask_controls(
2435 builder: *mut PamojaLoraPlanBuilder,
2436 controls: *const PamojaLoraMaskControl,
2437 len: usize,
2438) -> PamojaStatus {
2439 let Some(builder) = builder.as_mut() else {
2440 set_last_error("builder must not be null".to_owned());
2441 return PamojaStatus::InvalidArgument;
2442 };
2443 if controls.is_null() || len != 8 {
2444 set_last_error("controls must point at exactly eight controls".to_owned());
2445 return PamojaStatus::InvalidArgument;
2446 }
2447 let crossed = std::slice::from_raw_parts(controls, len);
2448 let mut table = [MaskControl::Reserved; 8];
2449 for (slot, control) in table.iter_mut().zip(crossed) {
2450 *slot = match mask_control_in(control) {
2451 Ok(control) => control,
2452 Err(status) => return status,
2453 };
2454 }
2455 update(builder, |b| b.mask_controls(table))
2456}
2457
2458#[no_mangle]
2478pub unsafe extern "C" fn pamoja_lora_plan_builder_set_join_sequence(
2479 builder: *mut PamojaLoraPlanBuilder,
2480 sequence: u8,
2481) -> PamojaStatus {
2482 let Some(builder) = builder.as_mut() else {
2483 set_last_error("builder must not be null".to_owned());
2484 return PamojaStatus::InvalidArgument;
2485 };
2486 let sequence = match sequence {
2487 PAMOJA_LORA_JOIN_RANDOM => JoinSequence::Random,
2488 PAMOJA_LORA_JOIN_OCTET_PASSES => JoinSequence::OctetPasses,
2489 other => {
2490 set_last_error(format!("{other} is not a join sequence"));
2491 return PamojaStatus::InvalidArgument;
2492 }
2493 };
2494 update(builder, |b| b.join_sequence(sequence))
2495}
2496
2497#[no_mangle]
2518pub unsafe extern "C" fn pamoja_lora_plan_builder_set_power_reference(
2519 builder: *mut PamojaLoraPlanBuilder,
2520 reference: u8,
2521 gain_allowance_db: u8,
2522) -> PamojaStatus {
2523 let Some(builder) = builder.as_mut() else {
2524 set_last_error("builder must not be null".to_owned());
2525 return PamojaStatus::InvalidArgument;
2526 };
2527 let reference = match reference {
2528 PAMOJA_LORA_POWER_EIRP => PowerReference::Eirp,
2529 PAMOJA_LORA_POWER_CONDUCTED => PowerReference::Conducted { gain_allowance_db },
2530 other => {
2531 set_last_error(format!("{other} is not a power reference"));
2532 return PamojaStatus::InvalidArgument;
2533 }
2534 };
2535 update(builder, |b| b.power_reference(reference))
2536}
2537
2538#[no_mangle]
2572pub unsafe extern "C" fn pamoja_lora_plan_builder_build(
2573 builder: *mut PamojaLoraPlanBuilder,
2574 out_plan: *mut *mut PamojaLoraPlan,
2575) -> PamojaStatus {
2576 if builder.is_null() || out_plan.is_null() {
2577 set_last_error("builder and out_plan must not be null".to_owned());
2578 return PamojaStatus::InvalidArgument;
2579 }
2580 let slot = &mut *out_plan;
2581 *slot = std::ptr::null_mut();
2582
2583 let Some(inner) = Box::from_raw(builder).builder else {
2584 set_last_error("this builder has already been built".to_owned());
2585 return PamojaStatus::Closed;
2586 };
2587
2588 match inner.build() {
2589 Ok(plan) => {
2590 *slot = PamojaLoraPlan::into_handle(plan);
2591 PamojaStatus::Ok
2592 }
2593 Err(error) => {
2594 set_last_error(error.to_string());
2595 PamojaStatus::InvalidArgument
2596 }
2597 }
2598}
2599
2600#[cfg(test)]
2601mod tests {
2602 use super::*;
2603 use std::ffi::CString;
2604 use std::ptr;
2605
2606 unsafe fn private_plan() -> *mut PamojaLoraPlan {
2609 let name = CString::new("private-915").expect("name");
2610 let mut builder = ptr::null_mut();
2611 assert_eq!(
2612 pamoja_lora_plan_builder_new(name.as_ptr(), &mut builder),
2613 PamojaStatus::Ok
2614 );
2615
2616 for rate in [
2617 PamojaLoraDataRate {
2618 bitrate_bps: 250,
2619 bandwidth_hz: 125_000,
2620 kind: PAMOJA_LORA_MODULATION_LORA,
2621 spreading_factor: 12,
2622 coding_rate_numerator: 0,
2623 coding_rate_denominator: 0,
2624 },
2625 PamojaLoraDataRate {
2626 bitrate_bps: 5_470,
2627 bandwidth_hz: 125_000,
2628 kind: PAMOJA_LORA_MODULATION_LORA,
2629 spreading_factor: 7,
2630 coding_rate_numerator: 0,
2631 coding_rate_denominator: 0,
2632 },
2633 ] {
2634 assert_eq!(
2635 pamoja_lora_plan_builder_push_data_rate(
2636 builder,
2637 PAMOJA_LORA_DIRECTION_UPLINK,
2638 &rate
2639 ),
2640 PamojaStatus::Ok
2641 );
2642 }
2643
2644 for (mac, app) in [(59u16, 51u16), (230, 222)] {
2645 for table in [
2646 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_REPEATER,
2647 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_DIRECT,
2648 ] {
2649 assert_eq!(
2650 pamoja_lora_plan_builder_push_max_payload(builder, table, 1, mac, app),
2651 PamojaStatus::Ok
2652 );
2653 }
2654 }
2655
2656 let block = PamojaLoraChannelBlock {
2657 start_hz: 915_000_000,
2658 step_hz: 500_000,
2659 count: 4,
2660 min_data_rate: 0,
2661 max_data_rate: 1,
2662 };
2663 assert_eq!(
2664 pamoja_lora_plan_builder_push_channel_block(
2665 builder,
2666 PAMOJA_LORA_CHANNELS_DEFAULT,
2667 &block
2668 ),
2669 PamojaStatus::Ok
2670 );
2671 assert_eq!(
2672 pamoja_lora_plan_builder_push_channel_block(builder, PAMOJA_LORA_CHANNELS_JOIN, &block),
2673 PamojaStatus::Ok
2674 );
2675
2676 let band = PamojaLoraSubBand {
2678 start_hz: 915_000_000,
2679 end_hz: 917_000_000,
2680 duty_cycle_permille: 1000,
2681 max_eirp_dbm: 30,
2682 };
2683 assert_eq!(
2684 pamoja_lora_plan_builder_push_sub_band(builder, &band),
2685 PamojaStatus::Ok
2686 );
2687
2688 assert_eq!(
2689 pamoja_lora_plan_builder_set_rx(builder, 915_000_000, 0, 0),
2690 PamojaStatus::Ok
2691 );
2692 for row in [[0u8], [1u8]] {
2693 assert_eq!(
2694 pamoja_lora_plan_builder_push_rx1_row(builder, 0, row.as_ptr(), row.len()),
2695 PamojaStatus::Ok
2696 );
2697 }
2698 assert_eq!(
2699 pamoja_lora_plan_builder_set_power(builder, 30, 2, 7),
2700 PamojaStatus::Ok
2701 );
2702 let relay = PamojaLoraRelayChannel {
2703 wor_frequency_hz: 915_200_000,
2704 ack_frequency_hz: 915_400_000,
2705 data_rate: 1,
2706 };
2707 assert_eq!(
2708 pamoja_lora_plan_builder_add_relay_channel(builder, &relay),
2709 PamojaStatus::Ok
2710 );
2711
2712 let mut plan = ptr::null_mut();
2713 assert_eq!(
2714 pamoja_lora_plan_builder_build(builder, &mut plan),
2715 PamojaStatus::Ok
2716 );
2717 assert!(!plan.is_null());
2718 plan
2719 }
2720
2721 #[test]
2722 #[cfg(feature = "eu868")]
2723 fn a_published_region_reports_its_own_tables() {
2724 unsafe {
2725 let mut plan = ptr::null_mut();
2726 assert_eq!(
2727 pamoja_lora_plan_for_region(PAMOJA_LORA_REGION_EU868, &mut plan),
2728 PamojaStatus::Ok
2729 );
2730
2731 let name = pamoja_lora_plan_name(plan);
2732 let text = std::ffi::CStr::from_ptr(crate::pamoja_string_data(name));
2733 assert_eq!(text.to_str().expect("utf-8"), "EU863-870");
2734 crate::pamoja_string_free(name);
2735
2736 let mut link = PamojaLoraLink {
2737 bandwidth_hz: 0,
2738 preamble_symbols: 0,
2739 spreading_factor: 0,
2740 coding_rate_denominator: 0,
2741 explicit_header: 0,
2742 crc: 0,
2743 };
2744 assert_eq!(
2745 pamoja_lora_plan_link_settings(plan, 0, &mut link),
2746 PamojaStatus::Ok
2747 );
2748 assert_eq!(link.spreading_factor, 12);
2749 assert_eq!(link.bandwidth_hz, 125_000);
2750
2751 let mut permille = 0;
2752 assert_eq!(
2753 pamoja_lora_plan_duty_cycle_permille(plan, 868_100_000, &mut permille),
2754 PamojaStatus::Ok
2755 );
2756 assert_eq!(permille, 10, "the 868.1 MHz sub-band is limited to 1%");
2757
2758 let mut count = 0u8;
2759 assert_eq!(
2760 pamoja_lora_plan_relay_channel_count(plan, &mut count),
2761 PamojaStatus::Ok
2762 );
2763 assert_eq!(count, 2);
2764 let mut channel = PamojaLoraRelayChannel {
2765 wor_frequency_hz: 0,
2766 ack_frequency_hz: 0,
2767 data_rate: 0,
2768 };
2769 assert_eq!(
2770 pamoja_lora_plan_relay_channel(plan, 1, &mut channel),
2771 PamojaStatus::Ok
2772 );
2773 assert_eq!(
2774 (
2775 channel.wor_frequency_hz,
2776 channel.ack_frequency_hz,
2777 channel.data_rate
2778 ),
2779 (865_500_000, 865_900_000, 5),
2780 "RP002-1.0.5 table 18"
2781 );
2782 assert_eq!(
2783 pamoja_lora_plan_relay_channel(plan, 2, &mut channel),
2784 PamojaStatus::InvalidArgument
2785 );
2786
2787 pamoja_lora_plan_free(plan);
2788 }
2789 }
2790
2791 #[test]
2792 fn an_unknown_region_code_is_told_from_one_left_out_of_the_build() {
2793 unsafe {
2794 let mut plan = ptr::null_mut();
2795 assert_eq!(
2796 pamoja_lora_plan_for_region(4242, &mut plan),
2797 PamojaStatus::InvalidArgument
2798 );
2799 assert!(plan.is_null());
2800 }
2801
2802 for region in PAMOJA_LORA_REGION_EU868..=PAMOJA_LORA_REGION_RU864 {
2805 let mut plan = ptr::null_mut();
2806 let status = unsafe { pamoja_lora_plan_for_region(region, &mut plan) };
2807 assert!(
2808 status == PamojaStatus::Ok || status == PamojaStatus::Unsupported,
2809 "region {region} reported {status:?}"
2810 );
2811 assert_eq!(
2812 status == PamojaStatus::Ok,
2813 pamoja_lora_region_is_available(region) == 1
2814 );
2815 unsafe { pamoja_lora_plan_free(plan) };
2816 }
2817 }
2818
2819 #[test]
2820 fn a_private_plan_answers_the_same_questions_a_published_one_does() {
2821 unsafe {
2822 let plan = private_plan();
2823
2824 let name = pamoja_lora_plan_name(plan);
2825 let text = std::ffi::CStr::from_ptr(crate::pamoja_string_data(name));
2826 assert_eq!(text.to_str().expect("utf-8"), "private-915");
2827 crate::pamoja_string_free(name);
2828
2829 let mut info = PamojaLoraPlanInfo {
2830 rx2_frequency_hz: 0,
2831 uplink_data_rate_count: 0,
2832 downlink_data_rate_count: 0,
2833 default_channel_count: 0,
2834 join_channel_block_count: 0,
2835 default_channel_block_count: 0,
2836 sub_band_count: 0,
2837 beacon: PamojaLoraBeacon {
2838 frequency_hz: 0,
2839 ping_slot_frequency_hz: 0,
2840 data_rate: 0,
2841 },
2842 rx2_data_rate: 0,
2843 default_max_eirp_dbm: 0,
2844 tx_power_step_db: 0,
2845 max_tx_power_index: 0,
2846 max_rx1_data_rate_offset: 0,
2847 has_dwell_time_limit: 1,
2848 has_dwell_limited_payloads: 1,
2849 has_dwell_limited_rx1: 1,
2850 };
2851 assert_eq!(pamoja_lora_plan_info(plan, &mut info), PamojaStatus::Ok);
2852 assert_eq!(info.default_channel_count, 4);
2853 assert_eq!(info.uplink_data_rate_count, 2);
2854 assert_eq!(info.downlink_data_rate_count, 2);
2856 assert_eq!(info.has_dwell_time_limit, 0);
2857 assert_eq!(info.has_dwell_limited_payloads, 0);
2858 assert_eq!(info.has_dwell_limited_rx1, 0);
2859
2860 let mut frequency = 0;
2861 assert_eq!(
2862 pamoja_lora_plan_channel_frequency_hz(plan, 3, &mut frequency),
2863 PamojaStatus::Ok
2864 );
2865 assert_eq!(frequency, 916_500_000);
2866
2867 let mut permille = 0;
2868 assert_eq!(
2869 pamoja_lora_plan_duty_cycle_permille(plan, 915_000_000, &mut permille),
2870 PamojaStatus::Ok
2871 );
2872 assert_eq!(permille, 1000, "licensed spectrum is unrestricted");
2873
2874 let mut payload = PamojaLoraMaxPayload {
2875 mac_payload: 0,
2876 application: 0,
2877 };
2878 assert_eq!(
2879 pamoja_lora_plan_max_payload(
2880 plan,
2881 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_DIRECT,
2882 1,
2883 &mut payload
2884 ),
2885 PamojaStatus::Ok
2886 );
2887 assert_eq!(
2888 payload.application, 222,
2889 "the downlink table mirrors uplink"
2890 );
2891
2892 let mut dbm = 0;
2893 assert_eq!(
2894 pamoja_lora_plan_max_eirp_dbm(plan, 915_000_000, &mut dbm),
2895 PamojaStatus::Ok
2896 );
2897 assert_eq!(dbm, 30);
2898
2899 let mut lower = 0;
2900 assert_eq!(
2901 pamoja_lora_plan_next_backoff_data_rate(plan, 1, &mut lower),
2902 PamojaStatus::Ok
2903 );
2904 assert_eq!(lower, 0, "an unset chain steps down one rate at a time");
2905 assert_eq!(
2906 pamoja_lora_plan_next_backoff_data_rate(plan, 0, &mut lower),
2907 PamojaStatus::Unsupported,
2908 "the slowest rate has nothing below it"
2909 );
2910
2911 pamoja_lora_plan_free(plan);
2912 }
2913 }
2914
2915 #[test]
2916 fn a_plan_whose_rx1_rows_are_too_narrow_is_refused() {
2917 unsafe {
2918 let name = CString::new("too-narrow").expect("name");
2919 let mut builder = ptr::null_mut();
2920 assert_eq!(
2921 pamoja_lora_plan_builder_new(name.as_ptr(), &mut builder),
2922 PamojaStatus::Ok
2923 );
2924 let rate = PamojaLoraDataRate {
2925 bitrate_bps: 250,
2926 bandwidth_hz: 125_000,
2927 kind: PAMOJA_LORA_MODULATION_LORA,
2928 spreading_factor: 12,
2929 coding_rate_numerator: 0,
2930 coding_rate_denominator: 0,
2931 };
2932 assert_eq!(
2933 pamoja_lora_plan_builder_push_data_rate(
2934 builder,
2935 PAMOJA_LORA_DIRECTION_UPLINK,
2936 &rate
2937 ),
2938 PamojaStatus::Ok
2939 );
2940 assert_eq!(
2942 pamoja_lora_plan_builder_set_rx(builder, 915_000_000, 0, 5),
2943 PamojaStatus::Ok
2944 );
2945 let row = [0u8];
2946 assert_eq!(
2947 pamoja_lora_plan_builder_push_rx1_row(builder, 0, row.as_ptr(), row.len()),
2948 PamojaStatus::Ok
2949 );
2950
2951 let mut plan = ptr::null_mut();
2952 assert_eq!(
2953 pamoja_lora_plan_builder_build(builder, &mut plan),
2954 PamojaStatus::InvalidArgument
2955 );
2956 assert!(plan.is_null());
2957 }
2958 }
2959
2960 #[test]
2961 fn a_plan_that_listens_at_a_data_rate_it_lacks_is_refused() {
2962 unsafe {
2963 let name = CString::new("bad-rx2").expect("name");
2964 let mut builder = ptr::null_mut();
2965 assert_eq!(
2966 pamoja_lora_plan_builder_new(name.as_ptr(), &mut builder),
2967 PamojaStatus::Ok
2968 );
2969 let rate = PamojaLoraDataRate {
2970 bitrate_bps: 250,
2971 bandwidth_hz: 125_000,
2972 kind: PAMOJA_LORA_MODULATION_LORA,
2973 spreading_factor: 12,
2974 coding_rate_numerator: 0,
2975 coding_rate_denominator: 0,
2976 };
2977 assert_eq!(
2978 pamoja_lora_plan_builder_push_data_rate(
2979 builder,
2980 PAMOJA_LORA_DIRECTION_UPLINK,
2981 &rate
2982 ),
2983 PamojaStatus::Ok
2984 );
2985 assert_eq!(
2987 pamoja_lora_plan_builder_set_rx(builder, 915_000_000, 3, 0),
2988 PamojaStatus::Ok
2989 );
2990 let row = [0u8];
2991 assert_eq!(
2992 pamoja_lora_plan_builder_push_rx1_row(builder, 0, row.as_ptr(), row.len()),
2993 PamojaStatus::Ok
2994 );
2995
2996 let mut plan = ptr::null_mut();
2997 assert_eq!(
2998 pamoja_lora_plan_builder_build(builder, &mut plan),
2999 PamojaStatus::InvalidArgument
3000 );
3001 assert!(plan.is_null());
3002 }
3003 }
3004
3005 #[test]
3006 #[cfg(all(feature = "us915", feature = "cn470", feature = "eu868"))]
3007 fn the_rules_a_published_plan_follows_cross_the_boundary() {
3008 unsafe {
3009 let empty_rules = PamojaLoraPlanRules {
3010 kind: 9,
3011 channel_list: 9,
3012 tx_param_setup: 9,
3013 join_sequence: 9,
3014 power_reference: 9,
3015 gain_allowance_db: 9,
3016 downlink_channel_block_count: 9,
3017 join_plan_count: 9,
3018 };
3019
3020 let mut plan = ptr::null_mut();
3021 assert_eq!(
3022 pamoja_lora_plan_for_region(PAMOJA_LORA_REGION_US915, &mut plan),
3023 PamojaStatus::Ok
3024 );
3025 let mut rules = empty_rules;
3026 assert_eq!(pamoja_lora_plan_rules(plan, &mut rules), PamojaStatus::Ok);
3027 assert_eq!(rules.kind, PAMOJA_LORA_PLAN_KIND_FIXED);
3028 assert_eq!(rules.join_sequence, PAMOJA_LORA_JOIN_OCTET_PASSES);
3029 assert_eq!(
3030 (rules.power_reference, rules.gain_allowance_db),
3031 (PAMOJA_LORA_POWER_CONDUCTED, 6)
3032 );
3033 assert_eq!(
3034 (rules.downlink_channel_block_count, rules.join_plan_count),
3035 (1, 0)
3036 );
3037
3038 let mut control = mask_control_out(MaskControl::Reserved);
3039 assert_eq!(
3040 pamoja_lora_plan_mask_control(plan, 5, &mut control),
3041 PamojaStatus::Ok
3042 );
3043 assert_eq!(control.kind, PAMOJA_LORA_MASK_PAIRED_BANKS);
3044 assert_eq!(
3045 pamoja_lora_plan_mask_control(plan, 7, &mut control),
3046 PamojaStatus::Ok
3047 );
3048 assert_eq!(
3049 (
3050 control.kind,
3051 control.on,
3052 control.has_then_group,
3053 control.then_group
3054 ),
3055 (PAMOJA_LORA_MASK_ALL, 0, 1, 4)
3056 );
3057 assert_eq!(
3058 pamoja_lora_plan_mask_control(plan, 8, &mut control),
3059 PamojaStatus::InvalidArgument
3060 );
3061
3062 let mut hz = 0;
3063 assert_eq!(
3064 pamoja_lora_plan_rx1_frequency_hz(plan, 65, 904_600_000, &mut hz),
3065 PamojaStatus::Ok
3066 );
3067 assert_eq!(hz, 923_900_000);
3068 assert_eq!(
3069 pamoja_lora_plan_downlink_channel_frequency_hz(plan, 7, &mut hz),
3070 PamojaStatus::Ok
3071 );
3072 assert_eq!(hz, 927_500_000);
3073 pamoja_lora_plan_free(plan);
3074
3075 let mut plan = ptr::null_mut();
3076 assert_eq!(
3077 pamoja_lora_plan_for_region(PAMOJA_LORA_REGION_EU868, &mut plan),
3078 PamojaStatus::Ok
3079 );
3080 let mut rules = empty_rules;
3081 assert_eq!(pamoja_lora_plan_rules(plan, &mut rules), PamojaStatus::Ok);
3082 assert_eq!(
3083 (rules.kind, rules.channel_list),
3084 (
3085 PAMOJA_LORA_PLAN_KIND_DYNAMIC,
3086 PAMOJA_LORA_CHANNEL_LIST_MHZ800
3087 )
3088 );
3089 assert_eq!(
3090 pamoja_lora_plan_rx1_frequency_hz(plan, 2, 868_500_000, &mut hz),
3091 PamojaStatus::Ok
3092 );
3093 assert_eq!(hz, 868_500_000, "a dynamic plan answers where it sent");
3094 pamoja_lora_plan_free(plan);
3095
3096 let mut plan = ptr::null_mut();
3099 assert_eq!(
3100 pamoja_lora_plan_for_cn470(PAMOJA_LORA_CN470_ANTENNA_26MHZ_A, &mut plan),
3101 PamojaStatus::Ok
3102 );
3103 let mut rules = empty_rules;
3104 assert_eq!(pamoja_lora_plan_rules(plan, &mut rules), PamojaStatus::Ok);
3105 assert_eq!(rules.join_plan_count, 5);
3106 let mut run = PamojaLoraJoinPlan {
3107 channels: PamojaLoraChannelBlock {
3108 start_hz: 0,
3109 step_hz: 0,
3110 count: 0,
3111 min_data_rate: 0,
3112 max_data_rate: 0,
3113 },
3114 accept_start_hz: 0,
3115 accept_step_hz: 0,
3116 rx2_start_hz: 0,
3117 rx2_step_hz: 0,
3118 cn470_plan: 0,
3119 };
3120 assert_eq!(
3121 pamoja_lora_plan_join_plan(plan, 2, &mut run),
3122 PamojaStatus::Ok
3123 );
3124 assert_eq!(
3125 (
3126 run.channels.start_hz,
3127 run.channels.step_hz,
3128 run.channels.count
3129 ),
3130 (479_900_000, 20_000_000, 2)
3131 );
3132 assert_eq!(
3133 (run.accept_start_hz, run.rx2_start_hz, run.rx2_step_hz),
3134 (479_900_000, 478_300_000, 20_000_000)
3135 );
3136 assert_eq!(run.cn470_plan, PAMOJA_LORA_CN470_ANTENNA_20MHZ_B);
3137 assert_eq!(
3138 pamoja_lora_plan_join_plan(plan, 5, &mut run),
3139 PamojaStatus::InvalidArgument
3140 );
3141 let (mut index, mut offset) = (0, 0);
3142 assert_eq!(
3143 pamoja_lora_plan_join_plan_for_channel(plan, 9, &mut index, &mut offset),
3144 PamojaStatus::Ok
3145 );
3146 assert_eq!((index, offset), (2, 1));
3147 assert_eq!(
3148 pamoja_lora_plan_join_plan_for_channel(plan, 20, &mut index, &mut offset),
3149 PamojaStatus::InvalidArgument
3150 );
3151 pamoja_lora_plan_free(plan);
3152
3153 assert_eq!(
3154 pamoja_lora_plan_for_cn470(PAMOJA_LORA_CN470_NONE, &mut plan),
3155 PamojaStatus::InvalidArgument
3156 );
3157 }
3158 }
3159
3160 #[test]
3161 fn a_built_plan_takes_every_rule_the_builder_is_given() {
3162 unsafe {
3163 let name = CString::new("fixed-private").expect("name");
3164 let mut builder = ptr::null_mut();
3165 assert_eq!(
3166 pamoja_lora_plan_builder_new(name.as_ptr(), &mut builder),
3167 PamojaStatus::Ok
3168 );
3169 let rate = PamojaLoraDataRate {
3170 bitrate_bps: 5_470,
3171 bandwidth_hz: 125_000,
3172 kind: PAMOJA_LORA_MODULATION_LORA,
3173 spreading_factor: 7,
3174 coding_rate_numerator: 0,
3175 coding_rate_denominator: 0,
3176 };
3177 assert_eq!(
3178 pamoja_lora_plan_builder_push_data_rate(
3179 builder,
3180 PAMOJA_LORA_DIRECTION_UPLINK,
3181 &rate
3182 ),
3183 PamojaStatus::Ok
3184 );
3185 assert_eq!(
3186 pamoja_lora_plan_builder_set_rx(builder, 923_300_000, 0, 0),
3187 PamojaStatus::Ok
3188 );
3189 let row = [0u8];
3190 assert_eq!(
3191 pamoja_lora_plan_builder_push_rx1_row(builder, 0, row.as_ptr(), row.len()),
3192 PamojaStatus::Ok
3193 );
3194 for (which, start) in [
3195 (PAMOJA_LORA_CHANNELS_DEFAULT, 902_300_000),
3196 (PAMOJA_LORA_CHANNELS_DOWNLINK, 923_300_000),
3197 ] {
3198 let block = PamojaLoraChannelBlock {
3199 start_hz: start,
3200 step_hz: 600_000,
3201 count: 4,
3202 min_data_rate: 0,
3203 max_data_rate: 0,
3204 };
3205 assert_eq!(
3206 pamoja_lora_plan_builder_push_channel_block(builder, which, &block),
3207 PamojaStatus::Ok
3208 );
3209 }
3210 assert_eq!(
3211 pamoja_lora_plan_builder_set_kind(builder, PAMOJA_LORA_PLAN_KIND_FIXED, 0),
3212 PamojaStatus::Ok
3213 );
3214 assert_eq!(
3215 pamoja_lora_plan_builder_set_tx_param_setup(builder, 1),
3216 PamojaStatus::Ok
3217 );
3218 let controls = [mask_control_out(MaskControl::All {
3219 on: true,
3220 then_group: Some(1),
3221 }); 8];
3222 assert_eq!(
3223 pamoja_lora_plan_builder_set_mask_controls(builder, controls.as_ptr(), 8),
3224 PamojaStatus::Ok
3225 );
3226 assert_eq!(
3227 pamoja_lora_plan_builder_set_mask_controls(builder, controls.as_ptr(), 7),
3228 PamojaStatus::InvalidArgument
3229 );
3230 assert_eq!(
3231 pamoja_lora_plan_builder_set_join_sequence(builder, PAMOJA_LORA_JOIN_OCTET_PASSES),
3232 PamojaStatus::Ok
3233 );
3234 assert_eq!(
3235 pamoja_lora_plan_builder_set_power_reference(
3236 builder,
3237 PAMOJA_LORA_POWER_CONDUCTED,
3238 3
3239 ),
3240 PamojaStatus::Ok
3241 );
3242 assert_eq!(
3243 pamoja_lora_plan_builder_set_kind(builder, PAMOJA_LORA_PLAN_KIND_DYNAMIC, 9),
3244 PamojaStatus::InvalidArgument
3245 );
3246
3247 let mut plan = ptr::null_mut();
3248 assert_eq!(
3249 pamoja_lora_plan_builder_build(builder, &mut plan),
3250 PamojaStatus::Ok
3251 );
3252 let mut rules = PamojaLoraPlanRules {
3253 kind: 0,
3254 channel_list: 0,
3255 tx_param_setup: 0,
3256 join_sequence: 0,
3257 power_reference: 0,
3258 gain_allowance_db: 0,
3259 downlink_channel_block_count: 0,
3260 join_plan_count: 9,
3261 };
3262 assert_eq!(pamoja_lora_plan_rules(plan, &mut rules), PamojaStatus::Ok);
3263 assert_eq!(
3264 rules,
3265 PamojaLoraPlanRules {
3266 kind: PAMOJA_LORA_PLAN_KIND_FIXED,
3267 channel_list: PAMOJA_LORA_CHANNEL_LIST_NONE,
3268 tx_param_setup: 1,
3269 join_sequence: PAMOJA_LORA_JOIN_OCTET_PASSES,
3270 power_reference: PAMOJA_LORA_POWER_CONDUCTED,
3271 gain_allowance_db: 3,
3272 downlink_channel_block_count: 1,
3273 join_plan_count: 0,
3274 }
3275 );
3276 let mut hz = 0;
3277 assert_eq!(
3278 pamoja_lora_plan_rx1_frequency_hz(plan, 5, 905_300_000, &mut hz),
3279 PamojaStatus::Ok
3280 );
3281 assert_eq!(
3282 hz, 923_900_000,
3283 "uplink channel 5 answers on downlink channel 1"
3284 );
3285 let mut control = mask_control_out(MaskControl::Reserved);
3286 assert_eq!(
3287 pamoja_lora_plan_mask_control(plan, 3, &mut control),
3288 PamojaStatus::Ok
3289 );
3290 assert_eq!(
3291 (
3292 control.kind,
3293 control.on,
3294 control.has_then_group,
3295 control.then_group
3296 ),
3297 (PAMOJA_LORA_MASK_ALL, 1, 1, 1)
3298 );
3299 pamoja_lora_plan_free(plan);
3300 }
3301 }
3302
3303 #[test]
3304 fn a_reserved_data_rate_crosses_and_comes_back_reserved() {
3305 let reserved = PamojaLoraDataRate {
3306 bitrate_bps: 0,
3307 bandwidth_hz: 0,
3308 kind: PAMOJA_LORA_MODULATION_RESERVED,
3309 spreading_factor: 0,
3310 coding_rate_numerator: 0,
3311 coding_rate_denominator: 0,
3312 };
3313 assert_eq!(data_rate_in(&reserved).expect("reserved"), None);
3314 assert_eq!(data_rate_out(None), reserved);
3315 }
3316
3317 #[test]
3318 fn every_modulation_survives_the_round_trip() {
3319 for rate in [
3320 DataRate::lora(9, 125_000, 1_760),
3321 DataRate::fsk(50_000),
3322 DataRate::lr_fhss(1, 3, 137_000, 162),
3323 ] {
3324 let crossed = data_rate_out(Some(rate));
3325 let back = data_rate_in(&crossed).expect("valid").expect("present");
3326 assert_eq!(back, rate);
3327 }
3328 }
3329}