pamoja_lora/region/owned.rs
1//! A channel plan that owns its tables, for hosts that assemble one at runtime.
2//!
3//! [`ChannelPlan`] borrows its tables, which is what keeps the published plans
4//! free of allocation and usable on a microcontroller. A host reading a plan out
5//! of a configuration file, or building one across a language boundary, has
6//! nowhere to put those tables: it needs storage that outlives the call that
7//! created it. [`OwnedChannelPlan`] is that storage, and
8//! [`ChannelPlanBuilder`] assembles one.
9//!
10//! This is the same capability the published regions have, not a lesser one. A
11//! deployment holding licensed spectrum, or working somewhere no published plan
12//! describes, gets every answer a named region gives.
13
14use alloc::boxed::Box;
15use alloc::string::String;
16use alloc::vec::Vec;
17use core::fmt;
18
19use super::{Beacon, ChannelBlock, ChannelPlan, DataRate, MaxPayload, SubBand};
20
21/// Which of a plan's payload tables an entry belongs to.
22///
23/// A region publishes separate limits for a device that may sit behind a
24/// repeater and one that will not, in each direction, plus a fifth table where
25/// a dwell-time limit applies.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27pub enum PayloadTable {
28 /// Uplink, for a device that may sit behind a repeater.
29 UplinkRepeater,
30 /// Uplink, for a device that will not.
31 UplinkDirect,
32 /// Downlink, for a device that may sit behind a repeater.
33 DownlinkRepeater,
34 /// Downlink, for a device that will not.
35 DownlinkDirect,
36 /// The limits that apply under a dwell-time limit.
37 DwellLimited,
38}
39
40/// Why a plan could not be built.
41///
42/// Every variant describes a plan that would answer some question wrongly, so it
43/// is refused at the point it is assembled rather than at the question.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub enum PlanError {
46 /// The plan defines no data rates, so it can answer nothing.
47 NoDataRates,
48 /// The number of RX1 rows does not match the number of uplink data rates.
49 Rx1RowCount {
50 /// How many rows the plan carries.
51 rows: usize,
52 /// How many it needs, one per uplink data rate.
53 expected: usize,
54 /// Whether this is the dwell-limited mapping rather than the ordinary one.
55 dwell_limited: bool,
56 },
57 /// An RX1 row is not as wide as the plan's highest offset allows.
58 Rx1RowWidth {
59 /// The row's position, which is the uplink data rate it maps.
60 row: usize,
61 /// How many entries the row carries.
62 width: usize,
63 /// How many it needs, one per allowed offset.
64 expected: usize,
65 /// Whether this is the dwell-limited mapping rather than the ordinary one.
66 dwell_limited: bool,
67 },
68 /// A table's length does not match the data-rate table it indexes.
69 TableLength {
70 /// How many entries the table carries.
71 length: usize,
72 /// How many data rates it must cover.
73 expected: usize,
74 },
75 /// The second receive window listens at a data rate the plan does not define.
76 Rx2DataRate {
77 /// The data rate RX2 was set to.
78 data_rate: u8,
79 /// How many downlink data rates the plan defines.
80 defined: usize,
81 },
82}
83
84impl fmt::Display for PlanError {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 Self::NoDataRates => write!(f, "a channel plan needs at least one data rate"),
88 Self::Rx1RowCount {
89 rows,
90 expected,
91 dwell_limited,
92 } => {
93 let which = if *dwell_limited { "dwell-limited " } else { "" };
94 write!(
95 f,
96 "a plan needs one {which}RX1 row per uplink data rate: {expected} data rates, {rows} rows"
97 )
98 }
99 Self::Rx1RowWidth {
100 row,
101 width,
102 expected,
103 dwell_limited,
104 } => {
105 let which = if *dwell_limited { "dwell-limited " } else { "" };
106 write!(
107 f,
108 "{which}RX1 row {row} has {width} entries, but the plan's offsets need {expected}"
109 )
110 }
111 Self::TableLength { length, expected } => {
112 write!(f, "a table has {length} entries for {expected} data rates")
113 }
114 Self::Rx2DataRate { data_rate, defined } => write!(
115 f,
116 "RX2 listens at data rate {data_rate}, but the plan defines {defined}"
117 ),
118 }
119 }
120}
121
122#[cfg(feature = "std")]
123impl std::error::Error for PlanError {}
124
125/// A channel plan that owns its tables.
126///
127/// Query it through [`with_plan`](Self::with_plan), which lends the tables to a
128/// borrowed [`ChannelPlan`] for the duration of one call.
129#[derive(Clone, Debug)]
130pub struct OwnedChannelPlan {
131 name: String,
132 uplink_data_rates: Vec<Option<DataRate>>,
133 downlink_data_rates: Vec<Option<DataRate>>,
134 max_payload_repeater: Vec<Option<MaxPayload>>,
135 max_payload_direct: Vec<Option<MaxPayload>>,
136 downlink_max_payload_repeater: Vec<Option<MaxPayload>>,
137 downlink_max_payload_direct: Vec<Option<MaxPayload>>,
138 max_payload_dwell_limited: Option<Vec<Option<MaxPayload>>>,
139 join_channels: Vec<ChannelBlock>,
140 default_channels: Vec<ChannelBlock>,
141 sub_bands: Vec<SubBand>,
142 default_max_eirp_dbm: i8,
143 tx_power_step_db: u8,
144 max_tx_power_index: u8,
145 rx1_rows: Vec<Box<[u8]>>,
146 rx1_rows_dwell_limited: Option<Vec<Box<[u8]>>>,
147 max_rx1_data_rate_offset: u8,
148 rx2_frequency_hz: u32,
149 rx2_data_rate: u8,
150 data_rate_backoff: Vec<Option<u8>>,
151 beacon: Beacon,
152 has_dwell_time_limit: bool,
153}
154
155impl OwnedChannelPlan {
156 /// Copies a borrowed plan into owned storage.
157 ///
158 /// This is how a host takes a published region and holds onto it: the result
159 /// is independent of where the original tables lived, so one type serves both
160 /// a named region and a plan built here.
161 ///
162 /// # Arguments
163 ///
164 /// * `plan` - the plan to copy.
165 ///
166 /// # Returns
167 ///
168 /// An owned copy answering exactly what the original does.
169 ///
170 /// # Examples
171 ///
172 /// ```
173 /// # #[cfg(feature = "eu868")] {
174 /// use pamoja_lora::region::{OwnedChannelPlan, Region};
175 ///
176 /// let held = OwnedChannelPlan::from_plan(Region::Eu868.plan());
177 /// assert_eq!(held.with_plan(|plan| plan.rx2()), (869_525_000, 0));
178 /// # }
179 /// ```
180 pub fn from_plan(plan: &ChannelPlan<'_>) -> Self {
181 Self {
182 name: plan.name.into(),
183 uplink_data_rates: plan.uplink_data_rates.to_vec(),
184 downlink_data_rates: plan.downlink_data_rates.to_vec(),
185 max_payload_repeater: plan.max_payload_repeater.to_vec(),
186 max_payload_direct: plan.max_payload_direct.to_vec(),
187 downlink_max_payload_repeater: plan.downlink_max_payload_repeater.to_vec(),
188 downlink_max_payload_direct: plan.downlink_max_payload_direct.to_vec(),
189 max_payload_dwell_limited: plan.max_payload_dwell_limited.map(<[_]>::to_vec),
190 join_channels: plan.join_channels.to_vec(),
191 default_channels: plan.default_channels.to_vec(),
192 sub_bands: plan.sub_bands.to_vec(),
193 default_max_eirp_dbm: plan.default_max_eirp_dbm,
194 tx_power_step_db: plan.tx_power_step_db,
195 max_tx_power_index: plan.max_tx_power_index,
196 rx1_rows: plan
197 .rx1_data_rate_offsets
198 .iter()
199 .map(|&r| r.into())
200 .collect(),
201 rx1_rows_dwell_limited: plan
202 .rx1_data_rate_offsets_dwell_limited
203 .map(|rows| rows.iter().map(|&r| r.into()).collect()),
204 max_rx1_data_rate_offset: plan.max_rx1_data_rate_offset,
205 rx2_frequency_hz: plan.rx2_frequency_hz,
206 rx2_data_rate: plan.rx2_data_rate,
207 data_rate_backoff: plan.data_rate_backoff.to_vec(),
208 beacon: plan.beacon,
209 has_dwell_time_limit: plan.has_dwell_time_limit,
210 }
211 }
212
213 /// Lends the owned tables to a borrowed plan for one query.
214 ///
215 /// The row pointers a plan needs are assembled on the stack for the call, so
216 /// nothing outlives it and the storage stays here.
217 ///
218 /// # Arguments
219 ///
220 /// * `query` - what to ask the plan.
221 ///
222 /// # Returns
223 ///
224 /// Whatever the query returned.
225 ///
226 /// # Examples
227 ///
228 /// ```
229 /// # #[cfg(feature = "in865")] {
230 /// use pamoja_lora::region::{OwnedChannelPlan, Region};
231 ///
232 /// let held = OwnedChannelPlan::from_plan(Region::In865.plan());
233 /// let name = held.with_plan(|plan| plan.name.to_owned());
234 /// assert_eq!(name, "IN865");
235 /// # }
236 /// ```
237 pub fn with_plan<R>(&self, query: impl FnOnce(&ChannelPlan<'_>) -> R) -> R {
238 let rx1: Vec<&[u8]> = self.rx1_rows.iter().map(|row| &row[..]).collect();
239 let dwell_rx1: Option<Vec<&[u8]>> = self
240 .rx1_rows_dwell_limited
241 .as_ref()
242 .map(|rows| rows.iter().map(|row| &row[..]).collect());
243 let plan = ChannelPlan {
244 name: &self.name,
245 uplink_data_rates: &self.uplink_data_rates,
246 downlink_data_rates: &self.downlink_data_rates,
247 max_payload_repeater: &self.max_payload_repeater,
248 max_payload_direct: &self.max_payload_direct,
249 downlink_max_payload_repeater: &self.downlink_max_payload_repeater,
250 downlink_max_payload_direct: &self.downlink_max_payload_direct,
251 max_payload_dwell_limited: self.max_payload_dwell_limited.as_deref(),
252 join_channels: &self.join_channels,
253 default_channels: &self.default_channels,
254 sub_bands: &self.sub_bands,
255 default_max_eirp_dbm: self.default_max_eirp_dbm,
256 tx_power_step_db: self.tx_power_step_db,
257 max_tx_power_index: self.max_tx_power_index,
258 rx1_data_rate_offsets: &rx1,
259 rx1_data_rate_offsets_dwell_limited: dwell_rx1.as_deref(),
260 max_rx1_data_rate_offset: self.max_rx1_data_rate_offset,
261 rx2_frequency_hz: self.rx2_frequency_hz,
262 rx2_data_rate: self.rx2_data_rate,
263 data_rate_backoff: &self.data_rate_backoff,
264 beacon: self.beacon,
265 has_dwell_time_limit: self.has_dwell_time_limit,
266 };
267 query(&plan)
268 }
269}
270
271/// Assembles a [`OwnedChannelPlan`] a table at a time.
272///
273/// Tables are indexed by position, so entries are pushed in data-rate order and
274/// a number the plan does not use is pushed as `None`. What a region would share
275/// between directions is filled in at [`build`](Self::build) rather than being
276/// repeated here.
277///
278/// # Examples
279///
280/// ```
281/// use pamoja_lora::region::{
282/// ChannelBlock, ChannelPlanBuilder, DataRate, MaxPayload, PayloadTable, SubBand,
283/// };
284///
285/// // A private deployment on licensed spectrum: two data rates and no duty cycle.
286/// let plan = ChannelPlanBuilder::new("private-915")
287/// .uplink_data_rate(Some(DataRate::lora(12, 125_000, 250)))
288/// .uplink_data_rate(Some(DataRate::lora(7, 125_000, 5_470)))
289/// .max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(59, 51)))
290/// .max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(230, 222)))
291/// .default_channel(ChannelBlock::new(915_000_000, 500_000, 4, 0, 1))
292/// .sub_band(SubBand::new(915_000_000, 917_000_000, 1000, 30))
293/// .rx(915_000_000, 0, 0)
294/// .rx1_row(&[0])
295/// .rx1_row(&[1])
296/// .build()
297/// .expect("a consistent plan");
298///
299/// // Licensed spectrum is reported as unrestricted, not refused.
300/// assert_eq!(plan.with_plan(|p| p.duty_cycle_permille(915_500_000)), Some(1000));
301/// assert_eq!(plan.with_plan(|p| p.default_channel_count()), 4);
302/// ```
303#[derive(Clone, Debug)]
304pub struct ChannelPlanBuilder {
305 plan: OwnedChannelPlan,
306}
307
308impl ChannelPlanBuilder {
309 /// Starts an empty plan.
310 ///
311 /// The plan begins with no data rates, channels, or sub-bands, a two-decibel
312 /// power ladder, and no dwell-time limit.
313 ///
314 /// # Arguments
315 ///
316 /// * `name` - what to call the plan, such as the band it covers.
317 ///
318 /// # Returns
319 ///
320 /// The builder.
321 pub fn new(name: impl Into<String>) -> Self {
322 Self {
323 plan: OwnedChannelPlan {
324 name: name.into(),
325 uplink_data_rates: Vec::new(),
326 downlink_data_rates: Vec::new(),
327 max_payload_repeater: Vec::new(),
328 max_payload_direct: Vec::new(),
329 downlink_max_payload_repeater: Vec::new(),
330 downlink_max_payload_direct: Vec::new(),
331 max_payload_dwell_limited: None,
332 join_channels: Vec::new(),
333 default_channels: Vec::new(),
334 sub_bands: Vec::new(),
335 default_max_eirp_dbm: 16,
336 tx_power_step_db: 2,
337 max_tx_power_index: 7,
338 rx1_rows: Vec::new(),
339 rx1_rows_dwell_limited: None,
340 max_rx1_data_rate_offset: 0,
341 rx2_frequency_hz: 0,
342 rx2_data_rate: 0,
343 data_rate_backoff: Vec::new(),
344 beacon: Beacon {
345 data_rate: 0,
346 frequency_hz: 0,
347 ping_slot_frequency_hz: 0,
348 },
349 has_dwell_time_limit: false,
350 },
351 }
352 }
353
354 /// Appends the next uplink data rate.
355 ///
356 /// # Arguments
357 ///
358 /// * `rate` - the data rate, or `None` for a number the plan reserves.
359 ///
360 /// # Returns
361 ///
362 /// The builder.
363 #[must_use]
364 pub fn uplink_data_rate(mut self, rate: Option<DataRate>) -> Self {
365 self.plan.uplink_data_rates.push(rate);
366 self
367 }
368
369 /// Appends the next downlink data rate.
370 ///
371 /// A plan that never calls this uses its uplink table in both directions,
372 /// which is what every region but the 900 MHz plans does.
373 ///
374 /// # Arguments
375 ///
376 /// * `rate` - the data rate, or `None` for a number the plan reserves.
377 ///
378 /// # Returns
379 ///
380 /// The builder.
381 #[must_use]
382 pub fn downlink_data_rate(mut self, rate: Option<DataRate>) -> Self {
383 self.plan.downlink_data_rates.push(rate);
384 self
385 }
386
387 /// Appends the next entry of one payload table.
388 ///
389 /// A downlink table left empty mirrors the matching uplink one.
390 ///
391 /// # Arguments
392 ///
393 /// * `table` - which table the entry belongs to.
394 /// * `payload` - the limits, or `None` where the data rate carries nothing.
395 ///
396 /// # Returns
397 ///
398 /// The builder.
399 #[must_use]
400 pub fn max_payload(mut self, table: PayloadTable, payload: Option<MaxPayload>) -> Self {
401 match table {
402 PayloadTable::UplinkRepeater => self.plan.max_payload_repeater.push(payload),
403 PayloadTable::UplinkDirect => self.plan.max_payload_direct.push(payload),
404 PayloadTable::DownlinkRepeater => self.plan.downlink_max_payload_repeater.push(payload),
405 PayloadTable::DownlinkDirect => self.plan.downlink_max_payload_direct.push(payload),
406 PayloadTable::DwellLimited => self
407 .plan
408 .max_payload_dwell_limited
409 .get_or_insert_with(Vec::new)
410 .push(payload),
411 }
412 self
413 }
414
415 /// Adds a run of channels a device may send a join request on.
416 ///
417 /// # Arguments
418 ///
419 /// * `block` - the channels to add.
420 ///
421 /// # Returns
422 ///
423 /// The builder.
424 #[must_use]
425 pub fn join_channel(mut self, block: ChannelBlock) -> Self {
426 self.plan.join_channels.push(block);
427 self
428 }
429
430 /// Adds a run of channels a device starts with.
431 ///
432 /// # Arguments
433 ///
434 /// * `block` - the channels to add.
435 ///
436 /// # Returns
437 ///
438 /// The builder.
439 #[must_use]
440 pub fn default_channel(mut self, block: ChannelBlock) -> Self {
441 self.plan.default_channels.push(block);
442 self
443 }
444
445 /// Adds a sub-band and the transmit limits inside it.
446 ///
447 /// A deployment on licensed spectrum gives its sub-band a duty cycle of
448 /// `1000`, which reports as unrestricted.
449 ///
450 /// # Arguments
451 ///
452 /// * `band` - the sub-band to add.
453 ///
454 /// # Returns
455 ///
456 /// The builder.
457 #[must_use]
458 pub fn sub_band(mut self, band: SubBand) -> Self {
459 self.plan.sub_bands.push(band);
460 self
461 }
462
463 /// Appends the RX1 downlink data rates for the next uplink data rate.
464 ///
465 /// # Arguments
466 ///
467 /// * `offsets` - the downlink data rate at each RX1 offset, in order.
468 ///
469 /// # Returns
470 ///
471 /// The builder.
472 #[must_use]
473 pub fn rx1_row(mut self, offsets: &[u8]) -> Self {
474 self.plan.rx1_rows.push(offsets.into());
475 self
476 }
477
478 /// Appends the dwell-limited RX1 downlink data rates for the next uplink
479 /// data rate.
480 ///
481 /// # Arguments
482 ///
483 /// * `offsets` - the downlink data rate at each RX1 offset, in order.
484 ///
485 /// # Returns
486 ///
487 /// The builder.
488 #[must_use]
489 pub fn rx1_row_dwell_limited(mut self, offsets: &[u8]) -> Self {
490 self.plan
491 .rx1_rows_dwell_limited
492 .get_or_insert_with(Vec::new)
493 .push(offsets.into());
494 self
495 }
496
497 /// Appends the next entry of the adaptive back-off chain.
498 ///
499 /// A chain left empty steps down one data rate at a time.
500 ///
501 /// # Arguments
502 ///
503 /// * `lower` - the data rate to fall back to, or `None` at the slowest.
504 ///
505 /// # Returns
506 ///
507 /// The builder.
508 #[must_use]
509 pub fn backoff(mut self, lower: Option<u8>) -> Self {
510 self.plan.data_rate_backoff.push(lower);
511 self
512 }
513
514 /// Sets the transmit-power ladder.
515 ///
516 /// # Arguments
517 ///
518 /// * `default_max_eirp_dbm` - the ceiling where no sub-band says otherwise.
519 /// * `step_db` - the step between power settings, in decibels.
520 /// * `max_index` - the highest power index the plan defines.
521 ///
522 /// # Returns
523 ///
524 /// The builder.
525 #[must_use]
526 pub fn power(mut self, default_max_eirp_dbm: i8, step_db: u8, max_index: u8) -> Self {
527 self.plan.default_max_eirp_dbm = default_max_eirp_dbm;
528 self.plan.tx_power_step_db = step_db;
529 self.plan.max_tx_power_index = max_index;
530 self
531 }
532
533 /// Sets the receive windows.
534 ///
535 /// # Arguments
536 ///
537 /// * `rx2_frequency_hz` - the fixed frequency the second window listens on.
538 /// * `rx2_data_rate` - the data rate the second window listens at.
539 /// * `max_rx1_offset` - the highest RX1 offset the plan allows, which fixes
540 /// how wide every RX1 row must be.
541 ///
542 /// # Returns
543 ///
544 /// The builder.
545 #[must_use]
546 pub fn rx(mut self, rx2_frequency_hz: u32, rx2_data_rate: u8, max_rx1_offset: u8) -> Self {
547 self.plan.rx2_frequency_hz = rx2_frequency_hz;
548 self.plan.rx2_data_rate = rx2_data_rate;
549 self.plan.max_rx1_data_rate_offset = max_rx1_offset;
550 self
551 }
552
553 /// Sets the Class B beacon.
554 ///
555 /// # Arguments
556 ///
557 /// * `beacon` - the beacon settings.
558 ///
559 /// # Returns
560 ///
561 /// The builder.
562 #[must_use]
563 pub fn beacon(mut self, beacon: Beacon) -> Self {
564 self.plan.beacon = beacon;
565 self
566 }
567
568 /// Sets whether the plan limits how long one transmission may hold a channel.
569 ///
570 /// # Arguments
571 ///
572 /// * `limited` - whether a dwell-time limit applies.
573 ///
574 /// # Returns
575 ///
576 /// The builder.
577 #[must_use]
578 pub fn dwell_time_limit(mut self, limited: bool) -> Self {
579 self.plan.has_dwell_time_limit = limited;
580 self
581 }
582
583 /// Finishes the plan.
584 ///
585 /// Tables a region would share are filled in first: an empty downlink
586 /// data-rate table mirrors the uplink one, an empty downlink payload table
587 /// mirrors its uplink counterpart, and an empty back-off chain steps down one
588 /// data rate at a time. What cannot be inferred is checked.
589 ///
590 /// # Returns
591 ///
592 /// The finished plan.
593 ///
594 /// # Errors
595 ///
596 /// Returns the [`PlanError`] describing the question this plan would answer
597 /// wrongly.
598 pub fn build(self) -> Result<OwnedChannelPlan, PlanError> {
599 let mut plan = self.plan;
600
601 if plan.downlink_data_rates.is_empty() {
602 plan.downlink_data_rates = plan.uplink_data_rates.clone();
603 }
604 if plan.downlink_max_payload_repeater.is_empty() {
605 plan.downlink_max_payload_repeater = plan.max_payload_repeater.clone();
606 }
607 if plan.downlink_max_payload_direct.is_empty() {
608 plan.downlink_max_payload_direct = plan.max_payload_direct.clone();
609 }
610 if plan.data_rate_backoff.is_empty() {
611 plan.data_rate_backoff = (0..plan.uplink_data_rates.len())
612 .map(|index| index.checked_sub(1).map(|lower| lower as u8))
613 .collect();
614 }
615
616 check(&plan)?;
617 Ok(plan)
618 }
619}
620
621/// Checks that a plan can answer every question asked of it.
622///
623/// # Arguments
624///
625/// * `plan` - the assembled plan.
626///
627/// # Returns
628///
629/// `Ok(())` if the plan is consistent.
630///
631/// # Errors
632///
633/// Returns the [`PlanError`] describing what is inconsistent.
634fn check(plan: &OwnedChannelPlan) -> Result<(), PlanError> {
635 let rates = plan.uplink_data_rates.len();
636 if rates == 0 {
637 return Err(PlanError::NoDataRates);
638 }
639
640 let width = usize::from(plan.max_rx1_data_rate_offset) + 1;
641 for (dwell_limited, rows) in [
642 (false, Some(&plan.rx1_rows)),
643 (true, plan.rx1_rows_dwell_limited.as_ref()),
644 ] {
645 let Some(rows) = rows else {
646 continue;
647 };
648 if rows.len() != rates {
649 return Err(PlanError::Rx1RowCount {
650 rows: rows.len(),
651 expected: rates,
652 dwell_limited,
653 });
654 }
655 for (row, entries) in rows.iter().enumerate() {
656 if entries.len() != width {
657 return Err(PlanError::Rx1RowWidth {
658 row,
659 width: entries.len(),
660 expected: width,
661 dwell_limited,
662 });
663 }
664 }
665 }
666
667 if plan.data_rate_backoff.len() != rates {
668 return Err(PlanError::TableLength {
669 length: plan.data_rate_backoff.len(),
670 expected: rates,
671 });
672 }
673
674 for table in [&plan.max_payload_repeater, &plan.max_payload_direct] {
675 if !table.is_empty() && table.len() != rates {
676 return Err(PlanError::TableLength {
677 length: table.len(),
678 expected: rates,
679 });
680 }
681 }
682 if let Some(table) = &plan.max_payload_dwell_limited {
683 if table.len() != rates {
684 return Err(PlanError::TableLength {
685 length: table.len(),
686 expected: rates,
687 });
688 }
689 }
690
691 let downlink = plan.downlink_data_rates.len();
692 for table in [
693 &plan.downlink_max_payload_repeater,
694 &plan.downlink_max_payload_direct,
695 ] {
696 if !table.is_empty() && table.len() != downlink {
697 return Err(PlanError::TableLength {
698 length: table.len(),
699 expected: downlink,
700 });
701 }
702 }
703
704 if usize::from(plan.rx2_data_rate) >= downlink {
705 return Err(PlanError::Rx2DataRate {
706 data_rate: plan.rx2_data_rate,
707 defined: downlink,
708 });
709 }
710
711 Ok(())
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 /// A minimal plan that passes every check, for tests that then break one
719 /// thing about it.
720 fn minimal() -> ChannelPlanBuilder {
721 ChannelPlanBuilder::new("test")
722 .uplink_data_rate(Some(DataRate::lora(12, 125_000, 250)))
723 .uplink_data_rate(Some(DataRate::lora(7, 125_000, 5_470)))
724 .rx(915_000_000, 0, 0)
725 .rx1_row(&[0])
726 .rx1_row(&[1])
727 }
728
729 #[test]
730 fn an_empty_plan_is_refused() {
731 assert_eq!(
732 ChannelPlanBuilder::new("empty").build().unwrap_err(),
733 PlanError::NoDataRates
734 );
735 }
736
737 #[test]
738 fn the_downlink_tables_mirror_the_uplink_ones_when_left_empty() {
739 let plan = minimal()
740 .max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(59, 51)))
741 .max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(230, 222)))
742 .build()
743 .expect("consistent");
744
745 assert_eq!(
746 plan.with_plan(|plan| plan.downlink_max_payload(1, false)),
747 Some(MaxPayload::new(230, 222))
748 );
749 assert_eq!(
750 plan.with_plan(|plan| plan.downlink_data_rate(1)),
751 Some(DataRate::lora(7, 125_000, 5_470))
752 );
753 }
754
755 #[test]
756 fn an_unset_backoff_chain_steps_down_one_rate_at_a_time() {
757 let plan = minimal().build().expect("consistent");
758 assert_eq!(
759 plan.with_plan(|plan| plan.next_backoff_data_rate(1)),
760 Some(0)
761 );
762 assert_eq!(plan.with_plan(|plan| plan.next_backoff_data_rate(0)), None);
763 }
764
765 #[test]
766 fn a_row_narrower_than_the_offsets_allow_is_refused() {
767 // Offsets up to 5 mean every row needs six entries.
768 let error = minimal().rx(915_000_000, 0, 5).build().unwrap_err();
769 assert_eq!(
770 error,
771 PlanError::Rx1RowWidth {
772 row: 0,
773 width: 1,
774 expected: 6,
775 dwell_limited: false,
776 }
777 );
778 }
779
780 #[test]
781 fn a_missing_rx1_row_is_refused() {
782 let error = ChannelPlanBuilder::new("short")
783 .uplink_data_rate(Some(DataRate::lora(12, 125_000, 250)))
784 .uplink_data_rate(Some(DataRate::lora(7, 125_000, 5_470)))
785 .rx(915_000_000, 0, 0)
786 .rx1_row(&[0])
787 .build()
788 .unwrap_err();
789 assert_eq!(
790 error,
791 PlanError::Rx1RowCount {
792 rows: 1,
793 expected: 2,
794 dwell_limited: false,
795 }
796 );
797 }
798
799 #[test]
800 fn listening_at_a_data_rate_the_plan_lacks_is_refused() {
801 let error = minimal().rx(915_000_000, 9, 0).build().unwrap_err();
802 assert_eq!(
803 error,
804 PlanError::Rx2DataRate {
805 data_rate: 9,
806 defined: 2,
807 }
808 );
809 }
810
811 #[test]
812 fn a_dwell_limited_mapping_is_checked_like_the_ordinary_one() {
813 let error = minimal().rx1_row_dwell_limited(&[0]).build().unwrap_err();
814 assert_eq!(
815 error,
816 PlanError::Rx1RowCount {
817 rows: 1,
818 expected: 2,
819 dwell_limited: true,
820 }
821 );
822 }
823
824 #[test]
825 #[cfg(feature = "au915")]
826 fn a_published_plan_survives_the_round_trip_into_owned_storage() {
827 use super::super::Region;
828
829 // AU915 exercises the awkward parts: separate downlink data rates, a
830 // dwell-limited payload table, and a wide RX1 mapping.
831 let published = Region::Au915.plan();
832 let owned = OwnedChannelPlan::from_plan(published);
833
834 owned.with_plan(|copy| {
835 assert_eq!(copy.name, published.name);
836 assert_eq!(copy.rx2(), published.rx2());
837 assert_eq!(
838 copy.default_channel_count(),
839 published.default_channel_count()
840 );
841 for data_rate in 0..16 {
842 assert_eq!(
843 copy.uplink_data_rate(data_rate),
844 published.uplink_data_rate(data_rate)
845 );
846 assert_eq!(
847 copy.downlink_data_rate(data_rate),
848 published.downlink_data_rate(data_rate)
849 );
850 assert_eq!(
851 copy.max_payload(data_rate, true),
852 published.max_payload(data_rate, true)
853 );
854 assert_eq!(
855 copy.max_payload_dwell_limited(data_rate),
856 published.max_payload_dwell_limited(data_rate)
857 );
858 for offset in 0..8 {
859 assert_eq!(
860 copy.rx1_data_rate(data_rate, offset),
861 published.rx1_data_rate(data_rate, offset)
862 );
863 }
864 }
865 });
866 }
867}