Skip to main content

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}