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::{
20    Beacon, ChannelBlock, ChannelPlan, DataRate, JoinSequence, MaskControl, MaxPayload, Modulation,
21    PlanKind, PowerReference, RelayChannel, SubBand,
22};
23
24/// Which of a plan's payload tables an entry belongs to.
25///
26/// A region publishes separate limits for a device that may sit behind a
27/// repeater and one that will not, in each direction, plus a fifth table where
28/// a dwell-time limit applies.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum PayloadTable {
31    /// Uplink, for a device that may sit behind a repeater.
32    UplinkRepeater,
33    /// Uplink, for a device that will not.
34    UplinkDirect,
35    /// Downlink, for a device that may sit behind a repeater.
36    DownlinkRepeater,
37    /// Downlink, for a device that will not.
38    DownlinkDirect,
39    /// The limits that apply under a dwell-time limit.
40    DwellLimited,
41}
42
43/// Why a plan could not be built.
44///
45/// Every variant describes a plan that would answer some question wrongly, so it
46/// is refused at the point it is assembled rather than at the question.
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum PlanError {
49    /// The plan defines no data rates, so it can answer nothing.
50    NoDataRates,
51    /// The number of RX1 rows does not match the number of uplink data rates.
52    Rx1RowCount {
53        /// How many rows the plan carries.
54        rows: usize,
55        /// How many it needs, one per uplink data rate.
56        expected: usize,
57        /// Whether this is the dwell-limited mapping rather than the ordinary one.
58        dwell_limited: bool,
59    },
60    /// An RX1 row is not as wide as the plan's highest offset allows.
61    Rx1RowWidth {
62        /// The row's position, which is the uplink data rate it maps.
63        row: usize,
64        /// How many entries the row carries.
65        width: usize,
66        /// How many it needs, one per allowed offset.
67        expected: usize,
68        /// Whether this is the dwell-limited mapping rather than the ordinary one.
69        dwell_limited: bool,
70    },
71    /// A table's length does not match the data-rate table it indexes.
72    TableLength {
73        /// How many entries the table carries.
74        length: usize,
75        /// How many data rates it must cover.
76        expected: usize,
77    },
78    /// The second receive window listens at a data rate the plan does not define.
79    Rx2DataRate {
80        /// The data rate RX2 was set to.
81        data_rate: u8,
82        /// How many downlink data rates the plan defines.
83        defined: usize,
84    },
85    /// A relay channel names a data rate that is not a LoRa downlink data rate of the plan,
86    /// which TS011-1.0.1 needs for its wake-on-radio frames.
87    RelayDataRate {
88        /// The data rate the channel was given.
89        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/// A channel plan that owns its tables.
139///
140/// Query it through [`with_plan`](Self::with_plan), which lends the tables to a
141/// borrowed [`ChannelPlan`] for the duration of one call.
142#[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    /// Copies a borrowed plan into owned storage.
177    ///
178    /// This is how a host takes a published region and holds onto it: the result
179    /// is independent of where the original tables lived, so one type serves both
180    /// a named region and a plan built here.
181    ///
182    /// The copy describes the one plan it was taken from. The plans a join selects
183    /// between, which only the published CN470-510 plans name, point at other
184    /// published plans and are not carried over.
185    ///
186    /// # Arguments
187    ///
188    /// * `plan` - the plan to copy.
189    ///
190    /// # Returns
191    ///
192    /// An owned copy answering exactly what the original does.
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// # #[cfg(feature = "eu868")] {
198    /// use pamoja_lora::region::{OwnedChannelPlan, Region};
199    ///
200    /// let held = OwnedChannelPlan::from_plan(Region::Eu868.plan());
201    /// assert_eq!(held.with_plan(|plan| plan.rx2()), (869_525_000, 0));
202    /// # }
203    /// ```
204    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    /// Lends the owned tables to a borrowed plan for one query.
245    ///
246    /// The row pointers a plan needs are assembled on the stack for the call, so
247    /// nothing outlives it and the storage stays here.
248    ///
249    /// # Arguments
250    ///
251    /// * `query` - what to ask the plan.
252    ///
253    /// # Returns
254    ///
255    /// Whatever the query returned.
256    ///
257    /// # Examples
258    ///
259    /// ```
260    /// # #[cfg(feature = "in865")] {
261    /// use pamoja_lora::region::{OwnedChannelPlan, Region};
262    ///
263    /// let held = OwnedChannelPlan::from_plan(Region::In865.plan());
264    /// let name = held.with_plan(|plan| plan.name.to_owned());
265    /// assert_eq!(name, "IN865");
266    /// # }
267    /// ```
268    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/// Assembles a [`OwnedChannelPlan`] a table at a time.
311///
312/// Tables are indexed by position, so entries are pushed in data-rate order and
313/// a number the plan does not use is pushed as `None`. What a region would share
314/// between directions is filled in at [`build`](Self::build) rather than being
315/// repeated here.
316///
317/// # Examples
318///
319/// ```
320/// use pamoja_lora::region::{
321///     ChannelBlock, ChannelPlanBuilder, DataRate, MaxPayload, PayloadTable, SubBand,
322/// };
323///
324/// // A private deployment on licensed spectrum: two data rates and no duty cycle.
325/// let plan = ChannelPlanBuilder::new("private-915")
326///     .uplink_data_rate(Some(DataRate::lora(12, 125_000, 250)))
327///     .uplink_data_rate(Some(DataRate::lora(7, 125_000, 5_470)))
328///     .max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(59, 51)))
329///     .max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(230, 222)))
330///     .default_channel(ChannelBlock::new(915_000_000, 500_000, 4, 0, 1))
331///     .sub_band(SubBand::new(915_000_000, 917_000_000, 1000, 30))
332///     .rx(915_000_000, 0, 0)
333///     .rx1_row(&[0])
334///     .rx1_row(&[1])
335///     .build()
336///     .expect("a consistent plan");
337///
338/// // Licensed spectrum is reported as unrestricted, not refused.
339/// assert_eq!(plan.with_plan(|p| p.duty_cycle_permille(915_500_000)), Some(1000));
340/// assert_eq!(plan.with_plan(|p| p.default_channel_count()), 4);
341/// ```
342#[derive(Clone, Debug)]
343pub struct ChannelPlanBuilder {
344    plan: OwnedChannelPlan,
345}
346
347impl ChannelPlanBuilder {
348    /// Starts an empty plan.
349    ///
350    /// The plan begins with no data rates, channels, or sub-bands, a two-decibel
351    /// power ladder, and no dwell-time limit.
352    ///
353    /// # Arguments
354    ///
355    /// * `name` - what to call the plan, such as the band it covers.
356    ///
357    /// # Returns
358    ///
359    /// The builder.
360    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    /// Appends the next uplink data rate.
401    ///
402    /// # Arguments
403    ///
404    /// * `rate` - the data rate, or `None` for a number the plan reserves.
405    ///
406    /// # Returns
407    ///
408    /// The builder.
409    #[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    /// Appends the next downlink data rate.
416    ///
417    /// A plan that never calls this uses its uplink table in both directions,
418    /// which is what every region but the 900 MHz plans does.
419    ///
420    /// # Arguments
421    ///
422    /// * `rate` - the data rate, or `None` for a number the plan reserves.
423    ///
424    /// # Returns
425    ///
426    /// The builder.
427    #[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    /// Appends the next entry of one payload table.
434    ///
435    /// A downlink table left empty mirrors the matching uplink one.
436    ///
437    /// # Arguments
438    ///
439    /// * `table` - which table the entry belongs to.
440    /// * `payload` - the limits, or `None` where the data rate carries nothing.
441    ///
442    /// # Returns
443    ///
444    /// The builder.
445    #[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    /// Adds a run of channels a device may send a join request on.
462    ///
463    /// # Arguments
464    ///
465    /// * `block` - the channels to add.
466    ///
467    /// # Returns
468    ///
469    /// The builder.
470    #[must_use]
471    pub fn join_channel(mut self, block: ChannelBlock) -> Self {
472        self.plan.join_channels.push(block);
473        self
474    }
475
476    /// Adds a run of channels a device starts with.
477    ///
478    /// # Arguments
479    ///
480    /// * `block` - the channels to add.
481    ///
482    /// # Returns
483    ///
484    /// The builder.
485    #[must_use]
486    pub fn default_channel(mut self, block: ChannelBlock) -> Self {
487        self.plan.default_channels.push(block);
488        self
489    }
490
491    /// Adds a sub-band and the transmit limits inside it.
492    ///
493    /// A deployment on licensed spectrum gives its sub-band a duty cycle of
494    /// `1000`, which reports as unrestricted.
495    ///
496    /// # Arguments
497    ///
498    /// * `band` - the sub-band to add.
499    ///
500    /// # Returns
501    ///
502    /// The builder.
503    #[must_use]
504    pub fn sub_band(mut self, band: SubBand) -> Self {
505        self.plan.sub_bands.push(band);
506        self
507    }
508
509    /// Appends the RX1 downlink data rates for the next uplink data rate.
510    ///
511    /// # Arguments
512    ///
513    /// * `offsets` - the downlink data rate at each RX1 offset, in order.
514    ///
515    /// # Returns
516    ///
517    /// The builder.
518    #[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    /// Appends the dwell-limited RX1 downlink data rates for the next uplink
525    /// data rate.
526    ///
527    /// # Arguments
528    ///
529    /// * `offsets` - the downlink data rate at each RX1 offset, in order.
530    ///
531    /// # Returns
532    ///
533    /// The builder.
534    #[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    /// Appends the next entry of the adaptive back-off chain.
544    ///
545    /// A chain left empty steps down one data rate at a time.
546    ///
547    /// # Arguments
548    ///
549    /// * `lower` - the data rate to fall back to, or `None` at the slowest.
550    ///
551    /// # Returns
552    ///
553    /// The builder.
554    #[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    /// Sets the transmit-power ladder.
561    ///
562    /// # Arguments
563    ///
564    /// * `default_max_eirp_dbm` - the ceiling where no sub-band says otherwise.
565    /// * `step_db` - the step between power settings, in decibels.
566    /// * `max_index` - the highest power index the plan defines.
567    ///
568    /// # Returns
569    ///
570    /// The builder.
571    #[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    /// Sets the receive windows.
580    ///
581    /// # Arguments
582    ///
583    /// * `rx2_frequency_hz` - the fixed frequency the second window listens on.
584    /// * `rx2_data_rate` - the data rate the second window listens at.
585    /// * `max_rx1_offset` - the highest RX1 offset the plan allows, which fixes
586    ///   how wide every RX1 row must be.
587    ///
588    /// # Returns
589    ///
590    /// The builder.
591    #[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    /// Sets the Class B beacon.
600    ///
601    /// # Arguments
602    ///
603    /// * `beacon` - the beacon settings.
604    ///
605    /// # Returns
606    ///
607    /// The builder.
608    #[must_use]
609    pub fn beacon(mut self, beacon: Beacon) -> Self {
610        self.plan.beacon = beacon;
611        self
612    }
613
614    /// Appends the next default relay channel.
615    ///
616    /// # Arguments
617    ///
618    /// * `channel` - the channel, whose position is the index a relay configuration names.
619    ///
620    /// # Returns
621    ///
622    /// The builder.
623    #[must_use]
624    pub fn relay_channel(mut self, channel: RelayChannel) -> Self {
625        self.plan.relay_channels.push(channel);
626        self
627    }
628
629    /// Sets whether the plan limits how long one transmission may hold a channel.
630    ///
631    /// # Arguments
632    ///
633    /// * `limited` - whether a dwell-time limit applies.
634    ///
635    /// # Returns
636    ///
637    /// The builder.
638    #[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    /// Sets how the plan defines its channels.
645    ///
646    /// A plan starts dynamic, with no numbering for a type 1 channel list, which is the
647    /// shape most regions take.
648    ///
649    /// # Arguments
650    ///
651    /// * `kind` - dynamic or fixed.
652    ///
653    /// # Returns
654    ///
655    /// The builder.
656    #[must_use]
657    pub fn kind(mut self, kind: PlanKind) -> Self {
658        self.plan.kind = kind;
659        self
660    }
661
662    /// Sets whether devices on the plan answer `TXParamSetupReq`.
663    ///
664    /// # Arguments
665    ///
666    /// * `answered` - whether the command applies.
667    ///
668    /// # Returns
669    ///
670    /// The builder.
671    #[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    /// Sets what each `ChMaskCntl` value of a `LinkADRReq` does.
678    ///
679    /// A plan starts with the table every dynamic region shares,
680    /// [`MaskControl::DYNAMIC`].
681    ///
682    /// # Arguments
683    ///
684    /// * `controls` - the eight controls, indexed by value.
685    ///
686    /// # Returns
687    ///
688    /// The builder.
689    #[must_use]
690    pub fn mask_controls(mut self, controls: [MaskControl; 8]) -> Self {
691        self.plan.mask_controls = controls;
692        self
693    }
694
695    /// Adds a run of numbered downlink channels the first receive window answers on.
696    ///
697    /// A plan with none answers on the uplink's own frequency.
698    ///
699    /// # Arguments
700    ///
701    /// * `block` - the channels to add.
702    ///
703    /// # Returns
704    ///
705    /// The builder.
706    #[must_use]
707    pub fn downlink_channel(mut self, block: ChannelBlock) -> Self {
708        self.plan.downlink_channels.push(block);
709        self
710    }
711
712    /// Sets the order a device tries the join channels in.
713    ///
714    /// # Arguments
715    ///
716    /// * `sequence` - the order.
717    ///
718    /// # Returns
719    ///
720    /// The builder.
721    #[must_use]
722    pub fn join_sequence(mut self, sequence: JoinSequence) -> Self {
723        self.plan.join_sequence = sequence;
724        self
725    }
726
727    /// Sets what the transmit power indexes count down from.
728    ///
729    /// # Arguments
730    ///
731    /// * `reference` - a radiated or a conducted ceiling.
732    ///
733    /// # Returns
734    ///
735    /// The builder.
736    #[must_use]
737    pub fn power_reference(mut self, reference: PowerReference) -> Self {
738        self.plan.power_reference = reference;
739        self
740    }
741
742    /// Finishes the plan.
743    ///
744    /// Tables a region would share are filled in first: an empty downlink
745    /// data-rate table mirrors the uplink one, an empty downlink payload table
746    /// mirrors its uplink counterpart, and an empty back-off chain steps down one
747    /// data rate at a time. What cannot be inferred is checked.
748    ///
749    /// # Returns
750    ///
751    /// The finished plan.
752    ///
753    /// # Errors
754    ///
755    /// Returns the [`PlanError`] describing the question this plan would answer
756    /// wrongly.
757    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
780/// Checks that a plan can answer every question asked of it.
781///
782/// # Arguments
783///
784/// * `plan` - the assembled plan.
785///
786/// # Returns
787///
788/// `Ok(())` if the plan is consistent.
789///
790/// # Errors
791///
792/// Returns the [`PlanError`] describing what is inconsistent.
793fn 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    /// A minimal plan that passes every check, for tests that then break one
894    /// thing about it.
895    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        // Offsets up to 5 mean every row needs six entries.
943        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        // AU915 exercises the awkward parts: separate downlink data rates, a
1031        // dwell-limited payload table, and a wide RX1 mapping.
1032        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}