1use pamoja_lora::region::{
22 Beacon, ChannelBlock, ChannelPlan, ChannelPlanBuilder, DataRate, MaxPayload, Modulation,
23 OwnedChannelPlan, PayloadTable, SubBand,
24};
25#[cfg(any(
28 feature = "eu868",
29 feature = "us915",
30 feature = "eu433",
31 feature = "au915",
32 feature = "cn470",
33 feature = "as923",
34 feature = "kr920",
35 feature = "in865",
36 feature = "ru864"
37))]
38use pamoja_lora::region::Region;
39
40use crate::lora::PamojaLoraLink;
41use crate::{set_last_error, PamojaStatus, PamojaString};
42
43pub const PAMOJA_LORA_REGION_EU868: u32 = 1;
45pub const PAMOJA_LORA_REGION_US915: u32 = 2;
47pub const PAMOJA_LORA_REGION_EU433: u32 = 3;
49pub const PAMOJA_LORA_REGION_AU915: u32 = 4;
51pub const PAMOJA_LORA_REGION_CN470: u32 = 5;
53pub const PAMOJA_LORA_REGION_AS923: u32 = 6;
55pub const PAMOJA_LORA_REGION_KR920: u32 = 7;
57pub const PAMOJA_LORA_REGION_IN865: u32 = 8;
59pub const PAMOJA_LORA_REGION_RU864: u32 = 9;
61
62pub const PAMOJA_LORA_MODULATION_LORA: u8 = 0;
64pub const PAMOJA_LORA_MODULATION_FSK: u8 = 1;
66pub const PAMOJA_LORA_MODULATION_LR_FHSS: u8 = 2;
68pub const PAMOJA_LORA_MODULATION_RESERVED: u8 = 3;
70
71pub const PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_REPEATER: u32 = 0;
73pub const PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_DIRECT: u32 = 1;
75pub const PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_REPEATER: u32 = 2;
77pub const PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_DIRECT: u32 = 3;
79pub const PAMOJA_LORA_PAYLOAD_TABLE_DWELL_LIMITED: u32 = 4;
81
82pub const PAMOJA_LORA_CHANNELS_JOIN: u32 = 0;
84pub const PAMOJA_LORA_CHANNELS_DEFAULT: u32 = 1;
86
87pub const PAMOJA_LORA_DIRECTION_UPLINK: u32 = 0;
89pub const PAMOJA_LORA_DIRECTION_DOWNLINK: u32 = 1;
91
92#[repr(C)]
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub struct PamojaLoraDataRate {
101 pub bitrate_bps: u32,
103 pub bandwidth_hz: u32,
105 pub kind: u8,
107 pub spreading_factor: u8,
109 pub coding_rate_numerator: u8,
111 pub coding_rate_denominator: u8,
113}
114
115#[repr(C)]
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub struct PamojaLoraMaxPayload {
119 pub mac_payload: u16,
121 pub application: u16,
123}
124
125#[repr(C)]
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub struct PamojaLoraChannelBlock {
129 pub start_hz: u32,
131 pub step_hz: u32,
133 pub count: u16,
135 pub min_data_rate: u8,
137 pub max_data_rate: u8,
139}
140
141#[repr(C)]
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct PamojaLoraSubBand {
145 pub start_hz: u32,
147 pub end_hz: u32,
149 pub duty_cycle_permille: u32,
152 pub max_eirp_dbm: i8,
154}
155
156#[repr(C)]
158#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159pub struct PamojaLoraBeacon {
160 pub frequency_hz: u32,
162 pub ping_slot_frequency_hz: u32,
164 pub data_rate: u8,
166}
167
168#[repr(C)]
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub struct PamojaLoraPlanInfo {
172 pub rx2_frequency_hz: u32,
174 pub uplink_data_rate_count: u16,
176 pub downlink_data_rate_count: u16,
178 pub default_channel_count: u16,
180 pub join_channel_block_count: u16,
182 pub default_channel_block_count: u16,
184 pub sub_band_count: u16,
186 pub beacon: PamojaLoraBeacon,
188 pub rx2_data_rate: u8,
190 pub default_max_eirp_dbm: i8,
192 pub tx_power_step_db: u8,
194 pub max_tx_power_index: u8,
196 pub max_rx1_data_rate_offset: u8,
198 pub has_dwell_time_limit: u8,
200 pub has_dwell_limited_payloads: u8,
202 pub has_dwell_limited_rx1: u8,
205}
206
207pub struct PamojaLoraPlan {
214 plan: OwnedChannelPlan,
215}
216
217impl PamojaLoraPlan {
218 fn into_handle(plan: OwnedChannelPlan) -> *mut Self {
228 Box::into_raw(Box::new(Self { plan }))
229 }
230
231 fn with<R>(&self, f: impl FnOnce(&ChannelPlan<'_>) -> R) -> R {
241 self.plan.with_plan(f)
242 }
243}
244
245fn data_rate_out(rate: Option<DataRate>) -> PamojaLoraDataRate {
255 let Some(rate) = rate else {
256 return PamojaLoraDataRate {
257 bitrate_bps: 0,
258 bandwidth_hz: 0,
259 kind: PAMOJA_LORA_MODULATION_RESERVED,
260 spreading_factor: 0,
261 coding_rate_numerator: 0,
262 coding_rate_denominator: 0,
263 };
264 };
265 let mut out = PamojaLoraDataRate {
266 bitrate_bps: rate.bitrate_bps,
267 bandwidth_hz: 0,
268 kind: PAMOJA_LORA_MODULATION_FSK,
269 spreading_factor: 0,
270 coding_rate_numerator: 0,
271 coding_rate_denominator: 0,
272 };
273 match rate.modulation {
274 Modulation::LoRa {
275 spreading_factor,
276 bandwidth_hz,
277 } => {
278 out.kind = PAMOJA_LORA_MODULATION_LORA;
279 out.spreading_factor = spreading_factor;
280 out.bandwidth_hz = bandwidth_hz;
281 }
282 Modulation::Fsk { .. } => {}
283 Modulation::LrFhss {
284 coding_rate_numerator,
285 coding_rate_denominator,
286 bandwidth_hz,
287 } => {
288 out.kind = PAMOJA_LORA_MODULATION_LR_FHSS;
289 out.coding_rate_numerator = coding_rate_numerator;
290 out.coding_rate_denominator = coding_rate_denominator;
291 out.bandwidth_hz = bandwidth_hz;
292 }
293 }
294 out
295}
296
297fn data_rate_in(rate: &PamojaLoraDataRate) -> Result<Option<DataRate>, PamojaStatus> {
308 match rate.kind {
309 PAMOJA_LORA_MODULATION_LORA => Ok(Some(DataRate::lora(
310 rate.spreading_factor,
311 rate.bandwidth_hz,
312 rate.bitrate_bps,
313 ))),
314 PAMOJA_LORA_MODULATION_FSK => Ok(Some(DataRate::fsk(rate.bitrate_bps))),
315 PAMOJA_LORA_MODULATION_LR_FHSS => Ok(Some(DataRate::lr_fhss(
316 rate.coding_rate_numerator,
317 rate.coding_rate_denominator,
318 rate.bandwidth_hz,
319 rate.bitrate_bps,
320 ))),
321 PAMOJA_LORA_MODULATION_RESERVED => Ok(None),
322 other => {
323 set_last_error(format!("{other} is not a modulation this build defines"));
324 Err(PamojaStatus::InvalidArgument)
325 }
326 }
327}
328
329fn is_region_code(region: u32) -> bool {
341 (PAMOJA_LORA_REGION_EU868..=PAMOJA_LORA_REGION_RU864).contains(®ion)
342}
343
344fn published(region: u32) -> Result<&'static ChannelPlan<'static>, PamojaStatus> {
359 let plan: Option<&'static ChannelPlan<'static>> = match region {
360 #[cfg(feature = "eu868")]
361 PAMOJA_LORA_REGION_EU868 => Some(Region::Eu868.plan()),
362 #[cfg(feature = "us915")]
363 PAMOJA_LORA_REGION_US915 => Some(Region::Us915.plan()),
364 #[cfg(feature = "eu433")]
365 PAMOJA_LORA_REGION_EU433 => Some(Region::Eu433.plan()),
366 #[cfg(feature = "au915")]
367 PAMOJA_LORA_REGION_AU915 => Some(Region::Au915.plan()),
368 #[cfg(feature = "cn470")]
369 PAMOJA_LORA_REGION_CN470 => Some(Region::Cn470.plan()),
370 #[cfg(feature = "as923")]
371 PAMOJA_LORA_REGION_AS923 => Some(Region::As923.plan()),
372 #[cfg(feature = "kr920")]
373 PAMOJA_LORA_REGION_KR920 => Some(Region::Kr920.plan()),
374 #[cfg(feature = "in865")]
375 PAMOJA_LORA_REGION_IN865 => Some(Region::In865.plan()),
376 #[cfg(feature = "ru864")]
377 PAMOJA_LORA_REGION_RU864 => Some(Region::Ru864.plan()),
378 _ => None,
379 };
380 match plan {
381 Some(plan) => Ok(plan),
382 None if is_region_code(region) => {
383 set_last_error(format!(
384 "region {region} is not compiled into this build of pamoja-lora"
385 ));
386 Err(PamojaStatus::Unsupported)
387 }
388 None => {
389 set_last_error(format!("{region} is not a region code"));
390 Err(PamojaStatus::InvalidArgument)
391 }
392 }
393}
394
395#[no_mangle]
416pub unsafe extern "C" fn pamoja_lora_plan_for_region(
417 region: u32,
418 out_plan: *mut *mut PamojaLoraPlan,
419) -> PamojaStatus {
420 if out_plan.is_null() {
421 set_last_error("out_plan must not be null".to_owned());
422 return PamojaStatus::InvalidArgument;
423 }
424 let slot = &mut *out_plan;
425 *slot = std::ptr::null_mut();
426
427 match published(region) {
428 Ok(plan) => {
429 *slot = PamojaLoraPlan::into_handle(OwnedChannelPlan::from_plan(plan));
430 PamojaStatus::Ok
431 }
432 Err(status) => status,
433 }
434}
435
436#[no_mangle]
450pub extern "C" fn pamoja_lora_region_is_available(region: u32) -> u8 {
451 u8::from(published(region).is_ok())
452}
453
454#[no_mangle]
465pub unsafe extern "C" fn pamoja_lora_plan_free(plan: *mut PamojaLoraPlan) {
466 if !plan.is_null() {
467 drop(Box::from_raw(plan));
468 }
469}
470
471#[no_mangle]
486pub unsafe extern "C" fn pamoja_lora_plan_name(plan: *const PamojaLoraPlan) -> *mut PamojaString {
487 let Some(plan) = plan.as_ref() else {
488 set_last_error("plan must not be null".to_owned());
489 return std::ptr::null_mut();
490 };
491 plan.with(|plan| PamojaString::into_raw(plan.name.to_owned()))
492}
493
494#[no_mangle]
514pub unsafe extern "C" fn pamoja_lora_plan_info(
515 plan: *const PamojaLoraPlan,
516 out_info: *mut PamojaLoraPlanInfo,
517) -> PamojaStatus {
518 let (Some(plan), false) = (plan.as_ref(), out_info.is_null()) else {
519 set_last_error("plan and out_info must not be null".to_owned());
520 return PamojaStatus::InvalidArgument;
521 };
522 *out_info = plan.with(|plan| PamojaLoraPlanInfo {
523 rx2_frequency_hz: plan.rx2_frequency_hz,
524 uplink_data_rate_count: plan.uplink_data_rates.len() as u16,
525 downlink_data_rate_count: plan.downlink_data_rates.len() as u16,
526 default_channel_count: plan.default_channel_count(),
527 join_channel_block_count: plan.join_channels.len() as u16,
528 default_channel_block_count: plan.default_channels.len() as u16,
529 sub_band_count: plan.sub_bands.len() as u16,
530 beacon: PamojaLoraBeacon {
531 frequency_hz: plan.beacon.frequency_hz,
532 ping_slot_frequency_hz: plan.beacon.ping_slot_frequency_hz,
533 data_rate: plan.beacon.data_rate,
534 },
535 rx2_data_rate: plan.rx2_data_rate,
536 default_max_eirp_dbm: plan.default_max_eirp_dbm,
537 tx_power_step_db: plan.tx_power_step_db,
538 max_tx_power_index: plan.max_tx_power_index,
539 max_rx1_data_rate_offset: plan.max_rx1_data_rate_offset,
540 has_dwell_time_limit: u8::from(plan.has_dwell_time_limit),
541 has_dwell_limited_payloads: u8::from(plan.max_payload_dwell_limited.is_some()),
542 has_dwell_limited_rx1: u8::from(plan.rx1_data_rate_offsets_dwell_limited.is_some()),
543 });
544 PamojaStatus::Ok
545}
546
547#[no_mangle]
573pub unsafe extern "C" fn pamoja_lora_plan_data_rate(
574 plan: *const PamojaLoraPlan,
575 direction: u32,
576 data_rate: u8,
577 out_rate: *mut PamojaLoraDataRate,
578) -> PamojaStatus {
579 let (Some(plan), false) = (plan.as_ref(), out_rate.is_null()) else {
580 set_last_error("plan and out_rate must not be null".to_owned());
581 return PamojaStatus::InvalidArgument;
582 };
583 let found = plan.with(|plan| match direction {
584 PAMOJA_LORA_DIRECTION_UPLINK => Ok(plan
585 .uplink_data_rates
586 .get(usize::from(data_rate))
587 .copied()
588 .map(data_rate_out)),
589 PAMOJA_LORA_DIRECTION_DOWNLINK => Ok(plan
590 .downlink_data_rates
591 .get(usize::from(data_rate))
592 .copied()
593 .map(data_rate_out)),
594 other => Err(other),
595 });
596 match found {
597 Ok(Some(rate)) => {
598 *out_rate = rate;
599 PamojaStatus::Ok
600 }
601 Ok(None) => {
602 set_last_error(format!("this plan defines no data rate {data_rate}"));
603 PamojaStatus::InvalidArgument
604 }
605 Err(other) => {
606 set_last_error(format!("{other} is not a direction"));
607 PamojaStatus::InvalidArgument
608 }
609 }
610}
611
612#[no_mangle]
639pub unsafe extern "C" fn pamoja_lora_plan_link_settings(
640 plan: *const PamojaLoraPlan,
641 data_rate: u8,
642 out_link: *mut PamojaLoraLink,
643) -> PamojaStatus {
644 let (Some(plan), false) = (plan.as_ref(), out_link.is_null()) else {
645 set_last_error("plan and out_link must not be null".to_owned());
646 return PamojaStatus::InvalidArgument;
647 };
648 let Some(settings) = plan.with(|plan| plan.link_settings(data_rate)) else {
649 set_last_error(format!(
650 "data rate {data_rate} is reserved or is not carried by LoRa in this plan"
651 ));
652 return PamojaStatus::Unsupported;
653 };
654 *out_link = PamojaLoraLink {
655 bandwidth_hz: settings.bandwidth_hz(),
656 preamble_symbols: 8,
657 spreading_factor: settings.spreading_factor(),
658 coding_rate_denominator: 5,
659 explicit_header: 1,
660 crc: 1,
661 };
662 PamojaStatus::Ok
663}
664
665#[no_mangle]
690pub unsafe extern "C" fn pamoja_lora_plan_max_payload(
691 plan: *const PamojaLoraPlan,
692 table: u32,
693 data_rate: u8,
694 out_payload: *mut PamojaLoraMaxPayload,
695) -> PamojaStatus {
696 let (Some(plan), false) = (plan.as_ref(), out_payload.is_null()) else {
697 set_last_error("plan and out_payload must not be null".to_owned());
698 return PamojaStatus::InvalidArgument;
699 };
700 let found = plan.with(|plan| match table {
701 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_REPEATER => Ok(plan.max_payload(data_rate, true)),
702 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_DIRECT => Ok(plan.max_payload(data_rate, false)),
703 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_REPEATER => {
704 Ok(plan.downlink_max_payload(data_rate, true))
705 }
706 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_DIRECT => {
707 Ok(plan.downlink_max_payload(data_rate, false))
708 }
709 PAMOJA_LORA_PAYLOAD_TABLE_DWELL_LIMITED => {
710 if plan.max_payload_dwell_limited.is_none() {
711 Err(PamojaStatus::Unsupported)
712 } else {
713 Ok(plan.max_payload_dwell_limited(data_rate))
714 }
715 }
716 _ => Err(PamojaStatus::InvalidArgument),
717 });
718 match found {
719 Ok(Some(payload)) => {
720 *out_payload = PamojaLoraMaxPayload {
721 mac_payload: payload.mac_payload,
722 application: payload.application,
723 };
724 PamojaStatus::Ok
725 }
726 Ok(None) => {
727 set_last_error(format!(
728 "this plan publishes no payload limit for data rate {data_rate}"
729 ));
730 PamojaStatus::InvalidArgument
731 }
732 Err(PamojaStatus::Unsupported) => {
733 set_last_error("this plan has no dwell-time limit".to_owned());
734 PamojaStatus::Unsupported
735 }
736 Err(_) => {
737 set_last_error(format!("{table} is not a payload table"));
738 PamojaStatus::InvalidArgument
739 }
740 }
741}
742
743#[no_mangle]
771pub unsafe extern "C" fn pamoja_lora_plan_duty_cycle_permille(
772 plan: *const PamojaLoraPlan,
773 frequency_hz: u32,
774 out_permille: *mut u32,
775) -> PamojaStatus {
776 let (Some(plan), false) = (plan.as_ref(), out_permille.is_null()) else {
777 set_last_error("plan and out_permille must not be null".to_owned());
778 return PamojaStatus::InvalidArgument;
779 };
780 let Some(permille) = plan.with(|plan| plan.duty_cycle_permille(frequency_hz)) else {
781 set_last_error(format!(
782 "{frequency_hz} Hz falls in no sub-band this plan describes"
783 ));
784 return PamojaStatus::Unsupported;
785 };
786 *out_permille = permille;
787 PamojaStatus::Ok
788}
789
790#[no_mangle]
812pub unsafe extern "C" fn pamoja_lora_plan_max_eirp_dbm(
813 plan: *const PamojaLoraPlan,
814 frequency_hz: u32,
815 out_dbm: *mut i8,
816) -> PamojaStatus {
817 let (Some(plan), false) = (plan.as_ref(), out_dbm.is_null()) else {
818 set_last_error("plan and out_dbm must not be null".to_owned());
819 return PamojaStatus::InvalidArgument;
820 };
821 *out_dbm = plan.with(|plan| plan.max_eirp_dbm(frequency_hz));
822 PamojaStatus::Ok
823}
824
825#[no_mangle]
849pub unsafe extern "C" fn pamoja_lora_plan_tx_power_dbm(
850 plan: *const PamojaLoraPlan,
851 index: u8,
852 max_eirp_dbm: i8,
853 out_dbm: *mut i8,
854) -> PamojaStatus {
855 let (Some(plan), false) = (plan.as_ref(), out_dbm.is_null()) else {
856 set_last_error("plan and out_dbm must not be null".to_owned());
857 return PamojaStatus::InvalidArgument;
858 };
859 let Some(dbm) = plan.with(|plan| plan.tx_power_dbm(index, max_eirp_dbm)) else {
860 set_last_error(format!("this plan defines no transmit-power index {index}"));
861 return PamojaStatus::InvalidArgument;
862 };
863 *out_dbm = dbm;
864 PamojaStatus::Ok
865}
866
867#[no_mangle]
894pub unsafe extern "C" fn pamoja_lora_plan_rx1_data_rate(
895 plan: *const PamojaLoraPlan,
896 uplink_data_rate: u8,
897 offset: u8,
898 dwell_limited: u8,
899 out_data_rate: *mut u8,
900) -> PamojaStatus {
901 let (Some(plan), false) = (plan.as_ref(), out_data_rate.is_null()) else {
902 set_last_error("plan and out_data_rate must not be null".to_owned());
903 return PamojaStatus::InvalidArgument;
904 };
905 let wants_dwell = dwell_limited != 0;
906 if wants_dwell && plan.with(|plan| plan.rx1_data_rate_offsets_dwell_limited.is_none()) {
907 set_last_error("this plan publishes no dwell-limited RX1 mapping".to_owned());
908 return PamojaStatus::Unsupported;
909 }
910 let found = plan.with(|plan| {
911 if wants_dwell {
912 plan.rx1_data_rate_dwell_limited(uplink_data_rate, offset)
913 } else {
914 plan.rx1_data_rate(uplink_data_rate, offset)
915 }
916 });
917 let Some(data_rate) = found else {
918 set_last_error(format!(
919 "this plan maps no RX1 downlink for uplink data rate {uplink_data_rate} at offset {offset}"
920 ));
921 return PamojaStatus::InvalidArgument;
922 };
923 *out_data_rate = data_rate;
924 PamojaStatus::Ok
925}
926
927#[no_mangle]
953pub unsafe extern "C" fn pamoja_lora_plan_next_backoff_data_rate(
954 plan: *const PamojaLoraPlan,
955 data_rate: u8,
956 out_data_rate: *mut u8,
957) -> PamojaStatus {
958 let (Some(plan), false) = (plan.as_ref(), out_data_rate.is_null()) else {
959 set_last_error("plan and out_data_rate must not be null".to_owned());
960 return PamojaStatus::InvalidArgument;
961 };
962 let known = plan.with(|plan| usize::from(data_rate) < plan.data_rate_backoff.len());
963 if !known {
964 set_last_error(format!("this plan defines no data rate {data_rate}"));
965 return PamojaStatus::InvalidArgument;
966 }
967 let Some(lower) = plan.with(|plan| plan.next_backoff_data_rate(data_rate)) else {
968 set_last_error(format!(
969 "data rate {data_rate} is the slowest this plan has"
970 ));
971 return PamojaStatus::Unsupported;
972 };
973 *out_data_rate = lower;
974 PamojaStatus::Ok
975}
976
977#[no_mangle]
999pub unsafe extern "C" fn pamoja_lora_plan_channel_frequency_hz(
1000 plan: *const PamojaLoraPlan,
1001 channel: u16,
1002 out_frequency_hz: *mut u32,
1003) -> PamojaStatus {
1004 let (Some(plan), false) = (plan.as_ref(), out_frequency_hz.is_null()) else {
1005 set_last_error("plan and out_frequency_hz must not be null".to_owned());
1006 return PamojaStatus::InvalidArgument;
1007 };
1008 let Some(frequency) = plan.with(|plan| plan.channel_frequency_hz(channel)) else {
1009 set_last_error(format!("this plan has no default channel {channel}"));
1010 return PamojaStatus::InvalidArgument;
1011 };
1012 *out_frequency_hz = frequency;
1013 PamojaStatus::Ok
1014}
1015
1016#[no_mangle]
1040pub unsafe extern "C" fn pamoja_lora_plan_channel_block(
1041 plan: *const PamojaLoraPlan,
1042 which: u32,
1043 index: u16,
1044 out_block: *mut PamojaLoraChannelBlock,
1045) -> PamojaStatus {
1046 let (Some(plan), false) = (plan.as_ref(), out_block.is_null()) else {
1047 set_last_error("plan and out_block must not be null".to_owned());
1048 return PamojaStatus::InvalidArgument;
1049 };
1050 if which != PAMOJA_LORA_CHANNELS_JOIN && which != PAMOJA_LORA_CHANNELS_DEFAULT {
1051 set_last_error(format!("{which} is not a channel set"));
1052 return PamojaStatus::InvalidArgument;
1053 }
1054 let found = plan.with(|plan| {
1055 let blocks = if which == PAMOJA_LORA_CHANNELS_JOIN {
1056 plan.join_channels
1057 } else {
1058 plan.default_channels
1059 };
1060 blocks.get(usize::from(index)).copied()
1061 });
1062 let Some(block) = found else {
1063 set_last_error(format!("this plan has no channel block {index}"));
1064 return PamojaStatus::InvalidArgument;
1065 };
1066 *out_block = PamojaLoraChannelBlock {
1067 start_hz: block.start_hz,
1068 step_hz: block.step_hz,
1069 count: block.count,
1070 min_data_rate: block.min_data_rate,
1071 max_data_rate: block.max_data_rate,
1072 };
1073 PamojaStatus::Ok
1074}
1075
1076#[no_mangle]
1099pub unsafe extern "C" fn pamoja_lora_plan_sub_band(
1100 plan: *const PamojaLoraPlan,
1101 index: u16,
1102 out_band: *mut PamojaLoraSubBand,
1103) -> PamojaStatus {
1104 let (Some(plan), false) = (plan.as_ref(), out_band.is_null()) else {
1105 set_last_error("plan and out_band must not be null".to_owned());
1106 return PamojaStatus::InvalidArgument;
1107 };
1108 let found = plan.with(|plan| plan.sub_bands.get(usize::from(index)).copied());
1109 let Some(band) = found else {
1110 set_last_error(format!("this plan has no sub-band {index}"));
1111 return PamojaStatus::InvalidArgument;
1112 };
1113 *out_band = PamojaLoraSubBand {
1114 start_hz: band.start_hz,
1115 end_hz: band.end_hz,
1116 duty_cycle_permille: band.duty_cycle_permille,
1117 max_eirp_dbm: band.max_eirp_dbm,
1118 };
1119 PamojaStatus::Ok
1120}
1121
1122pub struct PamojaLoraPlanBuilder {
1127 builder: Option<ChannelPlanBuilder>,
1128}
1129
1130fn update(
1148 builder: &mut PamojaLoraPlanBuilder,
1149 step: impl FnOnce(ChannelPlanBuilder) -> ChannelPlanBuilder,
1150) -> PamojaStatus {
1151 let Some(inner) = builder.builder.take() else {
1152 set_last_error("this builder has already been built".to_owned());
1153 return PamojaStatus::Closed;
1154 };
1155 builder.builder = Some(step(inner));
1156 PamojaStatus::Ok
1157}
1158
1159fn payload_table(table: u32) -> Option<PayloadTable> {
1169 match table {
1170 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_REPEATER => Some(PayloadTable::UplinkRepeater),
1171 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_DIRECT => Some(PayloadTable::UplinkDirect),
1172 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_REPEATER => Some(PayloadTable::DownlinkRepeater),
1173 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_DIRECT => Some(PayloadTable::DownlinkDirect),
1174 PAMOJA_LORA_PAYLOAD_TABLE_DWELL_LIMITED => Some(PayloadTable::DwellLimited),
1175 _ => None,
1176 }
1177}
1178
1179#[no_mangle]
1203pub unsafe extern "C" fn pamoja_lora_plan_builder_new(
1204 name: *const std::os::raw::c_char,
1205 out_builder: *mut *mut PamojaLoraPlanBuilder,
1206) -> PamojaStatus {
1207 if out_builder.is_null() {
1208 set_last_error("out_builder must not be null".to_owned());
1209 return PamojaStatus::InvalidArgument;
1210 }
1211 let slot = &mut *out_builder;
1212 *slot = std::ptr::null_mut();
1213
1214 let Some(name) = crate::read_str(name, "name") else {
1215 return PamojaStatus::InvalidArgument;
1216 };
1217
1218 *slot = Box::into_raw(Box::new(PamojaLoraPlanBuilder {
1219 builder: Some(ChannelPlanBuilder::new(name)),
1220 }));
1221 PamojaStatus::Ok
1222}
1223
1224#[no_mangle]
1236pub unsafe extern "C" fn pamoja_lora_plan_builder_free(builder: *mut PamojaLoraPlanBuilder) {
1237 if !builder.is_null() {
1238 drop(Box::from_raw(builder));
1239 }
1240}
1241
1242#[no_mangle]
1271pub unsafe extern "C" fn pamoja_lora_plan_builder_push_data_rate(
1272 builder: *mut PamojaLoraPlanBuilder,
1273 direction: u32,
1274 rate: *const PamojaLoraDataRate,
1275) -> PamojaStatus {
1276 let (Some(builder), Some(rate)) = (builder.as_mut(), rate.as_ref()) else {
1277 set_last_error("builder and rate must not be null".to_owned());
1278 return PamojaStatus::InvalidArgument;
1279 };
1280 let rate = match data_rate_in(rate) {
1281 Ok(rate) => rate,
1282 Err(status) => return status,
1283 };
1284 match direction {
1285 PAMOJA_LORA_DIRECTION_UPLINK => update(builder, |b| b.uplink_data_rate(rate)),
1286 PAMOJA_LORA_DIRECTION_DOWNLINK => update(builder, |b| b.downlink_data_rate(rate)),
1287 other => {
1288 set_last_error(format!("{other} is not a direction"));
1289 PamojaStatus::InvalidArgument
1290 }
1291 }
1292}
1293
1294#[no_mangle]
1322pub unsafe extern "C" fn pamoja_lora_plan_builder_push_max_payload(
1323 builder: *mut PamojaLoraPlanBuilder,
1324 table: u32,
1325 present: u8,
1326 mac_payload: u16,
1327 application: u16,
1328) -> PamojaStatus {
1329 let Some(builder) = builder.as_mut() else {
1330 set_last_error("builder must not be null".to_owned());
1331 return PamojaStatus::InvalidArgument;
1332 };
1333 let Some(table) = payload_table(table) else {
1334 set_last_error(format!("{table} is not a payload table"));
1335 return PamojaStatus::InvalidArgument;
1336 };
1337 let entry = (present != 0).then(|| MaxPayload::new(mac_payload, application));
1338 update(builder, |b| b.max_payload(table, entry))
1339}
1340
1341#[no_mangle]
1364pub unsafe extern "C" fn pamoja_lora_plan_builder_push_channel_block(
1365 builder: *mut PamojaLoraPlanBuilder,
1366 which: u32,
1367 block: *const PamojaLoraChannelBlock,
1368) -> PamojaStatus {
1369 let (Some(builder), Some(block)) = (builder.as_mut(), block.as_ref()) else {
1370 set_last_error("builder and block must not be null".to_owned());
1371 return PamojaStatus::InvalidArgument;
1372 };
1373 let entry = ChannelBlock::new(
1374 block.start_hz,
1375 block.step_hz,
1376 block.count,
1377 block.min_data_rate,
1378 block.max_data_rate,
1379 );
1380 match which {
1381 PAMOJA_LORA_CHANNELS_JOIN => update(builder, |b| b.join_channel(entry)),
1382 PAMOJA_LORA_CHANNELS_DEFAULT => update(builder, |b| b.default_channel(entry)),
1383 other => {
1384 set_last_error(format!("{other} is not a channel set"));
1385 PamojaStatus::InvalidArgument
1386 }
1387 }
1388}
1389
1390#[no_mangle]
1414pub unsafe extern "C" fn pamoja_lora_plan_builder_push_sub_band(
1415 builder: *mut PamojaLoraPlanBuilder,
1416 band: *const PamojaLoraSubBand,
1417) -> PamojaStatus {
1418 let (Some(builder), Some(band)) = (builder.as_mut(), band.as_ref()) else {
1419 set_last_error("builder and band must not be null".to_owned());
1420 return PamojaStatus::InvalidArgument;
1421 };
1422 let entry = SubBand::new(
1423 band.start_hz,
1424 band.end_hz,
1425 band.duty_cycle_permille,
1426 band.max_eirp_dbm,
1427 );
1428 update(builder, |b| b.sub_band(entry))
1429}
1430
1431#[no_mangle]
1458pub unsafe extern "C" fn pamoja_lora_plan_builder_push_rx1_row(
1459 builder: *mut PamojaLoraPlanBuilder,
1460 dwell_limited: u8,
1461 offsets: *const u8,
1462 offsets_len: usize,
1463) -> PamojaStatus {
1464 let Some(builder) = builder.as_mut() else {
1465 set_last_error("builder must not be null".to_owned());
1466 return PamojaStatus::InvalidArgument;
1467 };
1468 let row = match crate::read_bytes(offsets, offsets_len) {
1469 Ok(row) => row,
1470 Err(status) => return status,
1471 };
1472 if dwell_limited == 0 {
1473 update(builder, |b| b.rx1_row(&row))
1474 } else {
1475 update(builder, |b| b.rx1_row_dwell_limited(&row))
1476 }
1477}
1478
1479#[no_mangle]
1503pub unsafe extern "C" fn pamoja_lora_plan_builder_push_backoff(
1504 builder: *mut PamojaLoraPlanBuilder,
1505 has_lower: u8,
1506 data_rate: u8,
1507) -> PamojaStatus {
1508 let Some(builder) = builder.as_mut() else {
1509 set_last_error("builder must not be null".to_owned());
1510 return PamojaStatus::InvalidArgument;
1511 };
1512 let lower = (has_lower != 0).then_some(data_rate);
1513 update(builder, |b| b.backoff(lower))
1514}
1515
1516#[no_mangle]
1539pub unsafe extern "C" fn pamoja_lora_plan_builder_set_power(
1540 builder: *mut PamojaLoraPlanBuilder,
1541 default_max_eirp_dbm: i8,
1542 tx_power_step_db: u8,
1543 max_tx_power_index: u8,
1544) -> PamojaStatus {
1545 let Some(builder) = builder.as_mut() else {
1546 set_last_error("builder must not be null".to_owned());
1547 return PamojaStatus::InvalidArgument;
1548 };
1549 update(builder, |b| {
1550 b.power(default_max_eirp_dbm, tx_power_step_db, max_tx_power_index)
1551 })
1552}
1553
1554#[no_mangle]
1577pub unsafe extern "C" fn pamoja_lora_plan_builder_set_rx(
1578 builder: *mut PamojaLoraPlanBuilder,
1579 rx2_frequency_hz: u32,
1580 rx2_data_rate: u8,
1581 max_rx1_data_rate_offset: u8,
1582) -> PamojaStatus {
1583 let Some(builder) = builder.as_mut() else {
1584 set_last_error("builder must not be null".to_owned());
1585 return PamojaStatus::InvalidArgument;
1586 };
1587 update(builder, |b| {
1588 b.rx(rx2_frequency_hz, rx2_data_rate, max_rx1_data_rate_offset)
1589 })
1590}
1591
1592#[no_mangle]
1615pub unsafe extern "C" fn pamoja_lora_plan_builder_set_beacon(
1616 builder: *mut PamojaLoraPlanBuilder,
1617 beacon: *const PamojaLoraBeacon,
1618 has_dwell_time_limit: u8,
1619) -> PamojaStatus {
1620 let (Some(builder), Some(beacon)) = (builder.as_mut(), beacon.as_ref()) else {
1621 set_last_error("builder and beacon must not be null".to_owned());
1622 return PamojaStatus::InvalidArgument;
1623 };
1624 let entry = Beacon {
1625 data_rate: beacon.data_rate,
1626 frequency_hz: beacon.frequency_hz,
1627 ping_slot_frequency_hz: beacon.ping_slot_frequency_hz,
1628 };
1629 update(builder, |b| {
1630 b.beacon(entry).dwell_time_limit(has_dwell_time_limit != 0)
1631 })
1632}
1633
1634#[no_mangle]
1668pub unsafe extern "C" fn pamoja_lora_plan_builder_build(
1669 builder: *mut PamojaLoraPlanBuilder,
1670 out_plan: *mut *mut PamojaLoraPlan,
1671) -> PamojaStatus {
1672 if builder.is_null() || out_plan.is_null() {
1673 set_last_error("builder and out_plan must not be null".to_owned());
1674 return PamojaStatus::InvalidArgument;
1675 }
1676 let slot = &mut *out_plan;
1677 *slot = std::ptr::null_mut();
1678
1679 let Some(inner) = Box::from_raw(builder).builder else {
1680 set_last_error("this builder has already been built".to_owned());
1681 return PamojaStatus::Closed;
1682 };
1683
1684 match inner.build() {
1685 Ok(plan) => {
1686 *slot = PamojaLoraPlan::into_handle(plan);
1687 PamojaStatus::Ok
1688 }
1689 Err(error) => {
1690 set_last_error(error.to_string());
1691 PamojaStatus::InvalidArgument
1692 }
1693 }
1694}
1695
1696#[cfg(test)]
1697mod tests {
1698 use super::*;
1699 use std::ffi::CString;
1700 use std::ptr;
1701
1702 unsafe fn private_plan() -> *mut PamojaLoraPlan {
1705 let name = CString::new("private-915").expect("name");
1706 let mut builder = ptr::null_mut();
1707 assert_eq!(
1708 pamoja_lora_plan_builder_new(name.as_ptr(), &mut builder),
1709 PamojaStatus::Ok
1710 );
1711
1712 for rate in [
1713 PamojaLoraDataRate {
1714 bitrate_bps: 250,
1715 bandwidth_hz: 125_000,
1716 kind: PAMOJA_LORA_MODULATION_LORA,
1717 spreading_factor: 12,
1718 coding_rate_numerator: 0,
1719 coding_rate_denominator: 0,
1720 },
1721 PamojaLoraDataRate {
1722 bitrate_bps: 5_470,
1723 bandwidth_hz: 125_000,
1724 kind: PAMOJA_LORA_MODULATION_LORA,
1725 spreading_factor: 7,
1726 coding_rate_numerator: 0,
1727 coding_rate_denominator: 0,
1728 },
1729 ] {
1730 assert_eq!(
1731 pamoja_lora_plan_builder_push_data_rate(
1732 builder,
1733 PAMOJA_LORA_DIRECTION_UPLINK,
1734 &rate
1735 ),
1736 PamojaStatus::Ok
1737 );
1738 }
1739
1740 for (mac, app) in [(59u16, 51u16), (230, 222)] {
1741 for table in [
1742 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_REPEATER,
1743 PAMOJA_LORA_PAYLOAD_TABLE_UPLINK_DIRECT,
1744 ] {
1745 assert_eq!(
1746 pamoja_lora_plan_builder_push_max_payload(builder, table, 1, mac, app),
1747 PamojaStatus::Ok
1748 );
1749 }
1750 }
1751
1752 let block = PamojaLoraChannelBlock {
1753 start_hz: 915_000_000,
1754 step_hz: 500_000,
1755 count: 4,
1756 min_data_rate: 0,
1757 max_data_rate: 1,
1758 };
1759 assert_eq!(
1760 pamoja_lora_plan_builder_push_channel_block(
1761 builder,
1762 PAMOJA_LORA_CHANNELS_DEFAULT,
1763 &block
1764 ),
1765 PamojaStatus::Ok
1766 );
1767 assert_eq!(
1768 pamoja_lora_plan_builder_push_channel_block(builder, PAMOJA_LORA_CHANNELS_JOIN, &block),
1769 PamojaStatus::Ok
1770 );
1771
1772 let band = PamojaLoraSubBand {
1774 start_hz: 915_000_000,
1775 end_hz: 917_000_000,
1776 duty_cycle_permille: 1000,
1777 max_eirp_dbm: 30,
1778 };
1779 assert_eq!(
1780 pamoja_lora_plan_builder_push_sub_band(builder, &band),
1781 PamojaStatus::Ok
1782 );
1783
1784 assert_eq!(
1785 pamoja_lora_plan_builder_set_rx(builder, 915_000_000, 0, 0),
1786 PamojaStatus::Ok
1787 );
1788 for row in [[0u8], [1u8]] {
1789 assert_eq!(
1790 pamoja_lora_plan_builder_push_rx1_row(builder, 0, row.as_ptr(), row.len()),
1791 PamojaStatus::Ok
1792 );
1793 }
1794 assert_eq!(
1795 pamoja_lora_plan_builder_set_power(builder, 30, 2, 7),
1796 PamojaStatus::Ok
1797 );
1798
1799 let mut plan = ptr::null_mut();
1800 assert_eq!(
1801 pamoja_lora_plan_builder_build(builder, &mut plan),
1802 PamojaStatus::Ok
1803 );
1804 assert!(!plan.is_null());
1805 plan
1806 }
1807
1808 #[test]
1809 #[cfg(feature = "eu868")]
1810 fn a_published_region_reports_its_own_tables() {
1811 unsafe {
1812 let mut plan = ptr::null_mut();
1813 assert_eq!(
1814 pamoja_lora_plan_for_region(PAMOJA_LORA_REGION_EU868, &mut plan),
1815 PamojaStatus::Ok
1816 );
1817
1818 let name = pamoja_lora_plan_name(plan);
1819 let text = std::ffi::CStr::from_ptr(crate::pamoja_string_data(name));
1820 assert_eq!(text.to_str().expect("utf-8"), "EU863-870");
1821 crate::pamoja_string_free(name);
1822
1823 let mut link = PamojaLoraLink {
1824 bandwidth_hz: 0,
1825 preamble_symbols: 0,
1826 spreading_factor: 0,
1827 coding_rate_denominator: 0,
1828 explicit_header: 0,
1829 crc: 0,
1830 };
1831 assert_eq!(
1832 pamoja_lora_plan_link_settings(plan, 0, &mut link),
1833 PamojaStatus::Ok
1834 );
1835 assert_eq!(link.spreading_factor, 12);
1836 assert_eq!(link.bandwidth_hz, 125_000);
1837
1838 let mut permille = 0;
1839 assert_eq!(
1840 pamoja_lora_plan_duty_cycle_permille(plan, 868_100_000, &mut permille),
1841 PamojaStatus::Ok
1842 );
1843 assert_eq!(permille, 10, "the 868.1 MHz sub-band is limited to 1%");
1844
1845 pamoja_lora_plan_free(plan);
1846 }
1847 }
1848
1849 #[test]
1850 fn an_unknown_region_code_is_told_from_one_left_out_of_the_build() {
1851 unsafe {
1852 let mut plan = ptr::null_mut();
1853 assert_eq!(
1854 pamoja_lora_plan_for_region(4242, &mut plan),
1855 PamojaStatus::InvalidArgument
1856 );
1857 assert!(plan.is_null());
1858 }
1859
1860 for region in PAMOJA_LORA_REGION_EU868..=PAMOJA_LORA_REGION_RU864 {
1863 let mut plan = ptr::null_mut();
1864 let status = unsafe { pamoja_lora_plan_for_region(region, &mut plan) };
1865 assert!(
1866 status == PamojaStatus::Ok || status == PamojaStatus::Unsupported,
1867 "region {region} reported {status:?}"
1868 );
1869 assert_eq!(
1870 status == PamojaStatus::Ok,
1871 pamoja_lora_region_is_available(region) == 1
1872 );
1873 unsafe { pamoja_lora_plan_free(plan) };
1874 }
1875 }
1876
1877 #[test]
1878 fn a_private_plan_answers_the_same_questions_a_published_one_does() {
1879 unsafe {
1880 let plan = private_plan();
1881
1882 let name = pamoja_lora_plan_name(plan);
1883 let text = std::ffi::CStr::from_ptr(crate::pamoja_string_data(name));
1884 assert_eq!(text.to_str().expect("utf-8"), "private-915");
1885 crate::pamoja_string_free(name);
1886
1887 let mut info = PamojaLoraPlanInfo {
1888 rx2_frequency_hz: 0,
1889 uplink_data_rate_count: 0,
1890 downlink_data_rate_count: 0,
1891 default_channel_count: 0,
1892 join_channel_block_count: 0,
1893 default_channel_block_count: 0,
1894 sub_band_count: 0,
1895 beacon: PamojaLoraBeacon {
1896 frequency_hz: 0,
1897 ping_slot_frequency_hz: 0,
1898 data_rate: 0,
1899 },
1900 rx2_data_rate: 0,
1901 default_max_eirp_dbm: 0,
1902 tx_power_step_db: 0,
1903 max_tx_power_index: 0,
1904 max_rx1_data_rate_offset: 0,
1905 has_dwell_time_limit: 1,
1906 has_dwell_limited_payloads: 1,
1907 has_dwell_limited_rx1: 1,
1908 };
1909 assert_eq!(pamoja_lora_plan_info(plan, &mut info), PamojaStatus::Ok);
1910 assert_eq!(info.default_channel_count, 4);
1911 assert_eq!(info.uplink_data_rate_count, 2);
1912 assert_eq!(info.downlink_data_rate_count, 2);
1914 assert_eq!(info.has_dwell_time_limit, 0);
1915 assert_eq!(info.has_dwell_limited_payloads, 0);
1916 assert_eq!(info.has_dwell_limited_rx1, 0);
1917
1918 let mut frequency = 0;
1919 assert_eq!(
1920 pamoja_lora_plan_channel_frequency_hz(plan, 3, &mut frequency),
1921 PamojaStatus::Ok
1922 );
1923 assert_eq!(frequency, 916_500_000);
1924
1925 let mut permille = 0;
1926 assert_eq!(
1927 pamoja_lora_plan_duty_cycle_permille(plan, 915_000_000, &mut permille),
1928 PamojaStatus::Ok
1929 );
1930 assert_eq!(permille, 1000, "licensed spectrum is unrestricted");
1931
1932 let mut payload = PamojaLoraMaxPayload {
1933 mac_payload: 0,
1934 application: 0,
1935 };
1936 assert_eq!(
1937 pamoja_lora_plan_max_payload(
1938 plan,
1939 PAMOJA_LORA_PAYLOAD_TABLE_DOWNLINK_DIRECT,
1940 1,
1941 &mut payload
1942 ),
1943 PamojaStatus::Ok
1944 );
1945 assert_eq!(
1946 payload.application, 222,
1947 "the downlink table mirrors uplink"
1948 );
1949
1950 let mut dbm = 0;
1951 assert_eq!(
1952 pamoja_lora_plan_max_eirp_dbm(plan, 915_000_000, &mut dbm),
1953 PamojaStatus::Ok
1954 );
1955 assert_eq!(dbm, 30);
1956
1957 let mut lower = 0;
1958 assert_eq!(
1959 pamoja_lora_plan_next_backoff_data_rate(plan, 1, &mut lower),
1960 PamojaStatus::Ok
1961 );
1962 assert_eq!(lower, 0, "an unset chain steps down one rate at a time");
1963 assert_eq!(
1964 pamoja_lora_plan_next_backoff_data_rate(plan, 0, &mut lower),
1965 PamojaStatus::Unsupported,
1966 "the slowest rate has nothing below it"
1967 );
1968
1969 pamoja_lora_plan_free(plan);
1970 }
1971 }
1972
1973 #[test]
1974 fn a_plan_whose_rx1_rows_are_too_narrow_is_refused() {
1975 unsafe {
1976 let name = CString::new("too-narrow").expect("name");
1977 let mut builder = ptr::null_mut();
1978 assert_eq!(
1979 pamoja_lora_plan_builder_new(name.as_ptr(), &mut builder),
1980 PamojaStatus::Ok
1981 );
1982 let rate = PamojaLoraDataRate {
1983 bitrate_bps: 250,
1984 bandwidth_hz: 125_000,
1985 kind: PAMOJA_LORA_MODULATION_LORA,
1986 spreading_factor: 12,
1987 coding_rate_numerator: 0,
1988 coding_rate_denominator: 0,
1989 };
1990 assert_eq!(
1991 pamoja_lora_plan_builder_push_data_rate(
1992 builder,
1993 PAMOJA_LORA_DIRECTION_UPLINK,
1994 &rate
1995 ),
1996 PamojaStatus::Ok
1997 );
1998 assert_eq!(
2000 pamoja_lora_plan_builder_set_rx(builder, 915_000_000, 0, 5),
2001 PamojaStatus::Ok
2002 );
2003 let row = [0u8];
2004 assert_eq!(
2005 pamoja_lora_plan_builder_push_rx1_row(builder, 0, row.as_ptr(), row.len()),
2006 PamojaStatus::Ok
2007 );
2008
2009 let mut plan = ptr::null_mut();
2010 assert_eq!(
2011 pamoja_lora_plan_builder_build(builder, &mut plan),
2012 PamojaStatus::InvalidArgument
2013 );
2014 assert!(plan.is_null());
2015 }
2016 }
2017
2018 #[test]
2019 fn a_plan_that_listens_at_a_data_rate_it_lacks_is_refused() {
2020 unsafe {
2021 let name = CString::new("bad-rx2").expect("name");
2022 let mut builder = ptr::null_mut();
2023 assert_eq!(
2024 pamoja_lora_plan_builder_new(name.as_ptr(), &mut builder),
2025 PamojaStatus::Ok
2026 );
2027 let rate = PamojaLoraDataRate {
2028 bitrate_bps: 250,
2029 bandwidth_hz: 125_000,
2030 kind: PAMOJA_LORA_MODULATION_LORA,
2031 spreading_factor: 12,
2032 coding_rate_numerator: 0,
2033 coding_rate_denominator: 0,
2034 };
2035 assert_eq!(
2036 pamoja_lora_plan_builder_push_data_rate(
2037 builder,
2038 PAMOJA_LORA_DIRECTION_UPLINK,
2039 &rate
2040 ),
2041 PamojaStatus::Ok
2042 );
2043 assert_eq!(
2045 pamoja_lora_plan_builder_set_rx(builder, 915_000_000, 3, 0),
2046 PamojaStatus::Ok
2047 );
2048 let row = [0u8];
2049 assert_eq!(
2050 pamoja_lora_plan_builder_push_rx1_row(builder, 0, row.as_ptr(), row.len()),
2051 PamojaStatus::Ok
2052 );
2053
2054 let mut plan = ptr::null_mut();
2055 assert_eq!(
2056 pamoja_lora_plan_builder_build(builder, &mut plan),
2057 PamojaStatus::InvalidArgument
2058 );
2059 assert!(plan.is_null());
2060 }
2061 }
2062
2063 #[test]
2064 fn a_reserved_data_rate_crosses_and_comes_back_reserved() {
2065 let reserved = PamojaLoraDataRate {
2066 bitrate_bps: 0,
2067 bandwidth_hz: 0,
2068 kind: PAMOJA_LORA_MODULATION_RESERVED,
2069 spreading_factor: 0,
2070 coding_rate_numerator: 0,
2071 coding_rate_denominator: 0,
2072 };
2073 assert_eq!(data_rate_in(&reserved).expect("reserved"), None);
2074 assert_eq!(data_rate_out(None), reserved);
2075 }
2076
2077 #[test]
2078 fn every_modulation_survives_the_round_trip() {
2079 for rate in [
2080 DataRate::lora(9, 125_000, 1_760),
2081 DataRate::fsk(50_000),
2082 DataRate::lr_fhss(1, 3, 137_000, 162),
2083 ] {
2084 let crossed = data_rate_out(Some(rate));
2085 let back = data_rate_in(&crossed).expect("valid").expect("present");
2086 assert_eq!(back, rate);
2087 }
2088 }
2089}