1use alloc::boxed::Box;
15use alloc::string::String;
16use alloc::vec::Vec;
17use core::fmt;
18
19use super::{
20 Beacon, ChannelBlock, ChannelPlan, DataRate, JoinSequence, MaskControl, MaxPayload, Modulation,
21 PlanKind, PowerReference, RelayChannel, SubBand,
22};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum PayloadTable {
31 UplinkRepeater,
33 UplinkDirect,
35 DownlinkRepeater,
37 DownlinkDirect,
39 DwellLimited,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum PlanError {
49 NoDataRates,
51 Rx1RowCount {
53 rows: usize,
55 expected: usize,
57 dwell_limited: bool,
59 },
60 Rx1RowWidth {
62 row: usize,
64 width: usize,
66 expected: usize,
68 dwell_limited: bool,
70 },
71 TableLength {
73 length: usize,
75 expected: usize,
77 },
78 Rx2DataRate {
80 data_rate: u8,
82 defined: usize,
84 },
85 RelayDataRate {
88 data_rate: u8,
90 },
91}
92
93impl fmt::Display for PlanError {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 Self::NoDataRates => write!(f, "a channel plan needs at least one data rate"),
97 Self::Rx1RowCount {
98 rows,
99 expected,
100 dwell_limited,
101 } => {
102 let which = if *dwell_limited { "dwell-limited " } else { "" };
103 write!(
104 f,
105 "a plan needs one {which}RX1 row per uplink data rate: {expected} data rates, {rows} rows"
106 )
107 }
108 Self::Rx1RowWidth {
109 row,
110 width,
111 expected,
112 dwell_limited,
113 } => {
114 let which = if *dwell_limited { "dwell-limited " } else { "" };
115 write!(
116 f,
117 "{which}RX1 row {row} has {width} entries, but the plan's offsets need {expected}"
118 )
119 }
120 Self::TableLength { length, expected } => {
121 write!(f, "a table has {length} entries for {expected} data rates")
122 }
123 Self::Rx2DataRate { data_rate, defined } => write!(
124 f,
125 "RX2 listens at data rate {data_rate}, but the plan defines {defined}"
126 ),
127 Self::RelayDataRate { data_rate } => write!(
128 f,
129 "a relay channel names data rate {data_rate}, which is not a LoRa downlink rate of the plan"
130 ),
131 }
132 }
133}
134
135#[cfg(feature = "std")]
136impl std::error::Error for PlanError {}
137
138#[derive(Clone, Debug)]
143pub struct OwnedChannelPlan {
144 name: String,
145 uplink_data_rates: Vec<Option<DataRate>>,
146 downlink_data_rates: Vec<Option<DataRate>>,
147 max_payload_repeater: Vec<Option<MaxPayload>>,
148 max_payload_direct: Vec<Option<MaxPayload>>,
149 downlink_max_payload_repeater: Vec<Option<MaxPayload>>,
150 downlink_max_payload_direct: Vec<Option<MaxPayload>>,
151 max_payload_dwell_limited: Option<Vec<Option<MaxPayload>>>,
152 join_channels: Vec<ChannelBlock>,
153 default_channels: Vec<ChannelBlock>,
154 sub_bands: Vec<SubBand>,
155 default_max_eirp_dbm: i8,
156 tx_power_step_db: u8,
157 max_tx_power_index: u8,
158 rx1_rows: Vec<Box<[u8]>>,
159 rx1_rows_dwell_limited: Option<Vec<Box<[u8]>>>,
160 max_rx1_data_rate_offset: u8,
161 rx2_frequency_hz: u32,
162 rx2_data_rate: u8,
163 data_rate_backoff: Vec<Option<u8>>,
164 beacon: Beacon,
165 relay_channels: Vec<RelayChannel>,
166 has_dwell_time_limit: bool,
167 kind: PlanKind,
168 tx_param_setup: bool,
169 mask_controls: [MaskControl; 8],
170 downlink_channels: Vec<ChannelBlock>,
171 join_sequence: JoinSequence,
172 power_reference: PowerReference,
173}
174
175impl OwnedChannelPlan {
176 pub fn from_plan(plan: &ChannelPlan<'_>) -> Self {
205 Self {
206 name: plan.name.into(),
207 uplink_data_rates: plan.uplink_data_rates.to_vec(),
208 downlink_data_rates: plan.downlink_data_rates.to_vec(),
209 max_payload_repeater: plan.max_payload_repeater.to_vec(),
210 max_payload_direct: plan.max_payload_direct.to_vec(),
211 downlink_max_payload_repeater: plan.downlink_max_payload_repeater.to_vec(),
212 downlink_max_payload_direct: plan.downlink_max_payload_direct.to_vec(),
213 max_payload_dwell_limited: plan.max_payload_dwell_limited.map(<[_]>::to_vec),
214 join_channels: plan.join_channels.to_vec(),
215 default_channels: plan.default_channels.to_vec(),
216 sub_bands: plan.sub_bands.to_vec(),
217 default_max_eirp_dbm: plan.default_max_eirp_dbm,
218 tx_power_step_db: plan.tx_power_step_db,
219 max_tx_power_index: plan.max_tx_power_index,
220 rx1_rows: plan
221 .rx1_data_rate_offsets
222 .iter()
223 .map(|&r| r.into())
224 .collect(),
225 rx1_rows_dwell_limited: plan
226 .rx1_data_rate_offsets_dwell_limited
227 .map(|rows| rows.iter().map(|&r| r.into()).collect()),
228 max_rx1_data_rate_offset: plan.max_rx1_data_rate_offset,
229 rx2_frequency_hz: plan.rx2_frequency_hz,
230 rx2_data_rate: plan.rx2_data_rate,
231 data_rate_backoff: plan.data_rate_backoff.to_vec(),
232 beacon: plan.beacon,
233 relay_channels: plan.relay_channels.to_vec(),
234 has_dwell_time_limit: plan.has_dwell_time_limit,
235 kind: plan.kind,
236 tx_param_setup: plan.tx_param_setup,
237 mask_controls: plan.mask_controls,
238 downlink_channels: plan.downlink_channels.to_vec(),
239 join_sequence: plan.join_sequence,
240 power_reference: plan.power_reference,
241 }
242 }
243
244 pub fn with_plan<R>(&self, query: impl FnOnce(&ChannelPlan<'_>) -> R) -> R {
269 let rx1: Vec<&[u8]> = self.rx1_rows.iter().map(|row| &row[..]).collect();
270 let dwell_rx1: Option<Vec<&[u8]>> = self
271 .rx1_rows_dwell_limited
272 .as_ref()
273 .map(|rows| rows.iter().map(|row| &row[..]).collect());
274 let plan = ChannelPlan {
275 name: &self.name,
276 uplink_data_rates: &self.uplink_data_rates,
277 downlink_data_rates: &self.downlink_data_rates,
278 max_payload_repeater: &self.max_payload_repeater,
279 max_payload_direct: &self.max_payload_direct,
280 downlink_max_payload_repeater: &self.downlink_max_payload_repeater,
281 downlink_max_payload_direct: &self.downlink_max_payload_direct,
282 max_payload_dwell_limited: self.max_payload_dwell_limited.as_deref(),
283 join_channels: &self.join_channels,
284 default_channels: &self.default_channels,
285 sub_bands: &self.sub_bands,
286 default_max_eirp_dbm: self.default_max_eirp_dbm,
287 tx_power_step_db: self.tx_power_step_db,
288 max_tx_power_index: self.max_tx_power_index,
289 rx1_data_rate_offsets: &rx1,
290 rx1_data_rate_offsets_dwell_limited: dwell_rx1.as_deref(),
291 max_rx1_data_rate_offset: self.max_rx1_data_rate_offset,
292 rx2_frequency_hz: self.rx2_frequency_hz,
293 rx2_data_rate: self.rx2_data_rate,
294 data_rate_backoff: &self.data_rate_backoff,
295 beacon: self.beacon,
296 relay_channels: &self.relay_channels,
297 has_dwell_time_limit: self.has_dwell_time_limit,
298 kind: self.kind,
299 tx_param_setup: self.tx_param_setup,
300 mask_controls: self.mask_controls,
301 downlink_channels: &self.downlink_channels,
302 join_sequence: self.join_sequence,
303 power_reference: self.power_reference,
304 join_plans: &[],
305 };
306 query(&plan)
307 }
308}
309
310#[derive(Clone, Debug)]
343pub struct ChannelPlanBuilder {
344 plan: OwnedChannelPlan,
345}
346
347impl ChannelPlanBuilder {
348 pub fn new(name: impl Into<String>) -> Self {
361 Self {
362 plan: OwnedChannelPlan {
363 name: name.into(),
364 uplink_data_rates: Vec::new(),
365 downlink_data_rates: Vec::new(),
366 max_payload_repeater: Vec::new(),
367 max_payload_direct: Vec::new(),
368 downlink_max_payload_repeater: Vec::new(),
369 downlink_max_payload_direct: Vec::new(),
370 max_payload_dwell_limited: None,
371 join_channels: Vec::new(),
372 default_channels: Vec::new(),
373 sub_bands: Vec::new(),
374 default_max_eirp_dbm: 16,
375 tx_power_step_db: 2,
376 max_tx_power_index: 7,
377 rx1_rows: Vec::new(),
378 rx1_rows_dwell_limited: None,
379 max_rx1_data_rate_offset: 0,
380 rx2_frequency_hz: 0,
381 rx2_data_rate: 0,
382 data_rate_backoff: Vec::new(),
383 beacon: Beacon {
384 data_rate: 0,
385 frequency_hz: 0,
386 ping_slot_frequency_hz: 0,
387 },
388 relay_channels: Vec::new(),
389 has_dwell_time_limit: false,
390 kind: PlanKind::Dynamic { channel_list: None },
391 tx_param_setup: false,
392 mask_controls: MaskControl::DYNAMIC,
393 downlink_channels: Vec::new(),
394 join_sequence: JoinSequence::Random,
395 power_reference: PowerReference::Eirp,
396 },
397 }
398 }
399
400 #[must_use]
410 pub fn uplink_data_rate(mut self, rate: Option<DataRate>) -> Self {
411 self.plan.uplink_data_rates.push(rate);
412 self
413 }
414
415 #[must_use]
428 pub fn downlink_data_rate(mut self, rate: Option<DataRate>) -> Self {
429 self.plan.downlink_data_rates.push(rate);
430 self
431 }
432
433 #[must_use]
446 pub fn max_payload(mut self, table: PayloadTable, payload: Option<MaxPayload>) -> Self {
447 match table {
448 PayloadTable::UplinkRepeater => self.plan.max_payload_repeater.push(payload),
449 PayloadTable::UplinkDirect => self.plan.max_payload_direct.push(payload),
450 PayloadTable::DownlinkRepeater => self.plan.downlink_max_payload_repeater.push(payload),
451 PayloadTable::DownlinkDirect => self.plan.downlink_max_payload_direct.push(payload),
452 PayloadTable::DwellLimited => self
453 .plan
454 .max_payload_dwell_limited
455 .get_or_insert_with(Vec::new)
456 .push(payload),
457 }
458 self
459 }
460
461 #[must_use]
471 pub fn join_channel(mut self, block: ChannelBlock) -> Self {
472 self.plan.join_channels.push(block);
473 self
474 }
475
476 #[must_use]
486 pub fn default_channel(mut self, block: ChannelBlock) -> Self {
487 self.plan.default_channels.push(block);
488 self
489 }
490
491 #[must_use]
504 pub fn sub_band(mut self, band: SubBand) -> Self {
505 self.plan.sub_bands.push(band);
506 self
507 }
508
509 #[must_use]
519 pub fn rx1_row(mut self, offsets: &[u8]) -> Self {
520 self.plan.rx1_rows.push(offsets.into());
521 self
522 }
523
524 #[must_use]
535 pub fn rx1_row_dwell_limited(mut self, offsets: &[u8]) -> Self {
536 self.plan
537 .rx1_rows_dwell_limited
538 .get_or_insert_with(Vec::new)
539 .push(offsets.into());
540 self
541 }
542
543 #[must_use]
555 pub fn backoff(mut self, lower: Option<u8>) -> Self {
556 self.plan.data_rate_backoff.push(lower);
557 self
558 }
559
560 #[must_use]
572 pub fn power(mut self, default_max_eirp_dbm: i8, step_db: u8, max_index: u8) -> Self {
573 self.plan.default_max_eirp_dbm = default_max_eirp_dbm;
574 self.plan.tx_power_step_db = step_db;
575 self.plan.max_tx_power_index = max_index;
576 self
577 }
578
579 #[must_use]
592 pub fn rx(mut self, rx2_frequency_hz: u32, rx2_data_rate: u8, max_rx1_offset: u8) -> Self {
593 self.plan.rx2_frequency_hz = rx2_frequency_hz;
594 self.plan.rx2_data_rate = rx2_data_rate;
595 self.plan.max_rx1_data_rate_offset = max_rx1_offset;
596 self
597 }
598
599 #[must_use]
609 pub fn beacon(mut self, beacon: Beacon) -> Self {
610 self.plan.beacon = beacon;
611 self
612 }
613
614 #[must_use]
624 pub fn relay_channel(mut self, channel: RelayChannel) -> Self {
625 self.plan.relay_channels.push(channel);
626 self
627 }
628
629 #[must_use]
639 pub fn dwell_time_limit(mut self, limited: bool) -> Self {
640 self.plan.has_dwell_time_limit = limited;
641 self
642 }
643
644 #[must_use]
657 pub fn kind(mut self, kind: PlanKind) -> Self {
658 self.plan.kind = kind;
659 self
660 }
661
662 #[must_use]
672 pub fn tx_param_setup(mut self, answered: bool) -> Self {
673 self.plan.tx_param_setup = answered;
674 self
675 }
676
677 #[must_use]
690 pub fn mask_controls(mut self, controls: [MaskControl; 8]) -> Self {
691 self.plan.mask_controls = controls;
692 self
693 }
694
695 #[must_use]
707 pub fn downlink_channel(mut self, block: ChannelBlock) -> Self {
708 self.plan.downlink_channels.push(block);
709 self
710 }
711
712 #[must_use]
722 pub fn join_sequence(mut self, sequence: JoinSequence) -> Self {
723 self.plan.join_sequence = sequence;
724 self
725 }
726
727 #[must_use]
737 pub fn power_reference(mut self, reference: PowerReference) -> Self {
738 self.plan.power_reference = reference;
739 self
740 }
741
742 pub fn build(self) -> Result<OwnedChannelPlan, PlanError> {
758 let mut plan = self.plan;
759
760 if plan.downlink_data_rates.is_empty() {
761 plan.downlink_data_rates = plan.uplink_data_rates.clone();
762 }
763 if plan.downlink_max_payload_repeater.is_empty() {
764 plan.downlink_max_payload_repeater = plan.max_payload_repeater.clone();
765 }
766 if plan.downlink_max_payload_direct.is_empty() {
767 plan.downlink_max_payload_direct = plan.max_payload_direct.clone();
768 }
769 if plan.data_rate_backoff.is_empty() {
770 plan.data_rate_backoff = (0..plan.uplink_data_rates.len())
771 .map(|index| index.checked_sub(1).map(|lower| lower as u8))
772 .collect();
773 }
774
775 check(&plan)?;
776 Ok(plan)
777 }
778}
779
780fn check(plan: &OwnedChannelPlan) -> Result<(), PlanError> {
794 let rates = plan.uplink_data_rates.len();
795 if rates == 0 {
796 return Err(PlanError::NoDataRates);
797 }
798
799 let width = usize::from(plan.max_rx1_data_rate_offset) + 1;
800 for (dwell_limited, rows) in [
801 (false, Some(&plan.rx1_rows)),
802 (true, plan.rx1_rows_dwell_limited.as_ref()),
803 ] {
804 let Some(rows) = rows else {
805 continue;
806 };
807 if rows.len() != rates {
808 return Err(PlanError::Rx1RowCount {
809 rows: rows.len(),
810 expected: rates,
811 dwell_limited,
812 });
813 }
814 for (row, entries) in rows.iter().enumerate() {
815 if entries.len() != width {
816 return Err(PlanError::Rx1RowWidth {
817 row,
818 width: entries.len(),
819 expected: width,
820 dwell_limited,
821 });
822 }
823 }
824 }
825
826 if plan.data_rate_backoff.len() != rates {
827 return Err(PlanError::TableLength {
828 length: plan.data_rate_backoff.len(),
829 expected: rates,
830 });
831 }
832
833 for table in [&plan.max_payload_repeater, &plan.max_payload_direct] {
834 if !table.is_empty() && table.len() != rates {
835 return Err(PlanError::TableLength {
836 length: table.len(),
837 expected: rates,
838 });
839 }
840 }
841 if let Some(table) = &plan.max_payload_dwell_limited {
842 if table.len() != rates {
843 return Err(PlanError::TableLength {
844 length: table.len(),
845 expected: rates,
846 });
847 }
848 }
849
850 let downlink = plan.downlink_data_rates.len();
851 for table in [
852 &plan.downlink_max_payload_repeater,
853 &plan.downlink_max_payload_direct,
854 ] {
855 if !table.is_empty() && table.len() != downlink {
856 return Err(PlanError::TableLength {
857 length: table.len(),
858 expected: downlink,
859 });
860 }
861 }
862
863 if usize::from(plan.rx2_data_rate) >= downlink {
864 return Err(PlanError::Rx2DataRate {
865 data_rate: plan.rx2_data_rate,
866 defined: downlink,
867 });
868 }
869
870 for channel in &plan.relay_channels {
871 let lora = matches!(
872 plan.downlink_data_rates
873 .get(usize::from(channel.data_rate))
874 .copied()
875 .flatten()
876 .map(|rate| rate.modulation),
877 Some(Modulation::LoRa { .. })
878 );
879 if !lora {
880 return Err(PlanError::RelayDataRate {
881 data_rate: channel.data_rate,
882 });
883 }
884 }
885
886 Ok(())
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892
893 fn minimal() -> ChannelPlanBuilder {
896 ChannelPlanBuilder::new("test")
897 .uplink_data_rate(Some(DataRate::lora(12, 125_000, 250)))
898 .uplink_data_rate(Some(DataRate::lora(7, 125_000, 5_470)))
899 .rx(915_000_000, 0, 0)
900 .rx1_row(&[0])
901 .rx1_row(&[1])
902 }
903
904 #[test]
905 fn an_empty_plan_is_refused() {
906 assert_eq!(
907 ChannelPlanBuilder::new("empty").build().unwrap_err(),
908 PlanError::NoDataRates
909 );
910 }
911
912 #[test]
913 fn the_downlink_tables_mirror_the_uplink_ones_when_left_empty() {
914 let plan = minimal()
915 .max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(59, 51)))
916 .max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(230, 222)))
917 .build()
918 .expect("consistent");
919
920 assert_eq!(
921 plan.with_plan(|plan| plan.downlink_max_payload(1, false)),
922 Some(MaxPayload::new(230, 222))
923 );
924 assert_eq!(
925 plan.with_plan(|plan| plan.downlink_data_rate(1)),
926 Some(DataRate::lora(7, 125_000, 5_470))
927 );
928 }
929
930 #[test]
931 fn an_unset_backoff_chain_steps_down_one_rate_at_a_time() {
932 let plan = minimal().build().expect("consistent");
933 assert_eq!(
934 plan.with_plan(|plan| plan.next_backoff_data_rate(1)),
935 Some(0)
936 );
937 assert_eq!(plan.with_plan(|plan| plan.next_backoff_data_rate(0)), None);
938 }
939
940 #[test]
941 fn a_row_narrower_than_the_offsets_allow_is_refused() {
942 let error = minimal().rx(915_000_000, 0, 5).build().unwrap_err();
944 assert_eq!(
945 error,
946 PlanError::Rx1RowWidth {
947 row: 0,
948 width: 1,
949 expected: 6,
950 dwell_limited: false,
951 }
952 );
953 }
954
955 #[test]
956 fn a_missing_rx1_row_is_refused() {
957 let error = ChannelPlanBuilder::new("short")
958 .uplink_data_rate(Some(DataRate::lora(12, 125_000, 250)))
959 .uplink_data_rate(Some(DataRate::lora(7, 125_000, 5_470)))
960 .rx(915_000_000, 0, 0)
961 .rx1_row(&[0])
962 .build()
963 .unwrap_err();
964 assert_eq!(
965 error,
966 PlanError::Rx1RowCount {
967 rows: 1,
968 expected: 2,
969 dwell_limited: false,
970 }
971 );
972 }
973
974 #[test]
975 fn listening_at_a_data_rate_the_plan_lacks_is_refused() {
976 let error = minimal().rx(915_000_000, 9, 0).build().unwrap_err();
977 assert_eq!(
978 error,
979 PlanError::Rx2DataRate {
980 data_rate: 9,
981 defined: 2,
982 }
983 );
984 }
985
986 #[test]
987 fn a_relay_channel_needs_a_lora_downlink_data_rate() {
988 let built = minimal()
989 .relay_channel(RelayChannel::new(915_200_000, 915_400_000, 1))
990 .build()
991 .expect("DR1 is a LoRa rate of the plan");
992 assert_eq!(
993 built.with_plan(|plan| plan.relay_channel(0)),
994 Some(RelayChannel::new(915_200_000, 915_400_000, 1))
995 );
996
997 let error = minimal()
998 .relay_channel(RelayChannel::new(915_200_000, 915_400_000, 2))
999 .build()
1000 .unwrap_err();
1001 assert_eq!(error, PlanError::RelayDataRate { data_rate: 2 });
1002
1003 let fsk = minimal()
1004 .uplink_data_rate(Some(DataRate::fsk(50_000)))
1005 .rx1_row(&[2])
1006 .relay_channel(RelayChannel::new(915_200_000, 915_400_000, 2))
1007 .build()
1008 .unwrap_err();
1009 assert_eq!(fsk, PlanError::RelayDataRate { data_rate: 2 });
1010 }
1011
1012 #[test]
1013 fn a_dwell_limited_mapping_is_checked_like_the_ordinary_one() {
1014 let error = minimal().rx1_row_dwell_limited(&[0]).build().unwrap_err();
1015 assert_eq!(
1016 error,
1017 PlanError::Rx1RowCount {
1018 rows: 1,
1019 expected: 2,
1020 dwell_limited: true,
1021 }
1022 );
1023 }
1024
1025 #[test]
1026 #[cfg(feature = "au915")]
1027 fn a_published_plan_survives_the_round_trip_into_owned_storage() {
1028 use super::super::Region;
1029
1030 let published = Region::Au915.plan();
1033 let owned = OwnedChannelPlan::from_plan(published);
1034
1035 owned.with_plan(|copy| {
1036 assert_eq!(copy.name, published.name);
1037 assert_eq!(copy.rx2(), published.rx2());
1038 assert_eq!(
1039 copy.default_channel_count(),
1040 published.default_channel_count()
1041 );
1042 assert_eq!(copy.mask_controls, published.mask_controls);
1043 assert_eq!(copy.join_sequence, published.join_sequence);
1044 assert_eq!(copy.power_reference, published.power_reference);
1045 assert_eq!(copy.downlink_channels, published.downlink_channels);
1046 for channel in 0..72 {
1047 assert_eq!(
1048 copy.rx1_frequency_hz(channel, 915_200_000),
1049 published.rx1_frequency_hz(channel, 915_200_000)
1050 );
1051 }
1052 for data_rate in 0..16 {
1053 assert_eq!(
1054 copy.uplink_data_rate(data_rate),
1055 published.uplink_data_rate(data_rate)
1056 );
1057 assert_eq!(
1058 copy.downlink_data_rate(data_rate),
1059 published.downlink_data_rate(data_rate)
1060 );
1061 assert_eq!(
1062 copy.max_payload(data_rate, true),
1063 published.max_payload(data_rate, true)
1064 );
1065 assert_eq!(
1066 copy.max_payload_dwell_limited(data_rate),
1067 published.max_payload_dwell_limited(data_rate)
1068 );
1069 for offset in 0..8 {
1070 assert_eq!(
1071 copy.rx1_data_rate(data_rate, offset),
1072 published.rx1_data_rate(data_rate, offset)
1073 );
1074 }
1075 }
1076 });
1077 }
1078}