use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use super::{Beacon, ChannelBlock, ChannelPlan, DataRate, MaxPayload, SubBand};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PayloadTable {
UplinkRepeater,
UplinkDirect,
DownlinkRepeater,
DownlinkDirect,
DwellLimited,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PlanError {
NoDataRates,
Rx1RowCount {
rows: usize,
expected: usize,
dwell_limited: bool,
},
Rx1RowWidth {
row: usize,
width: usize,
expected: usize,
dwell_limited: bool,
},
TableLength {
length: usize,
expected: usize,
},
Rx2DataRate {
data_rate: u8,
defined: usize,
},
}
impl fmt::Display for PlanError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoDataRates => write!(f, "a channel plan needs at least one data rate"),
Self::Rx1RowCount {
rows,
expected,
dwell_limited,
} => {
let which = if *dwell_limited { "dwell-limited " } else { "" };
write!(
f,
"a plan needs one {which}RX1 row per uplink data rate: {expected} data rates, {rows} rows"
)
}
Self::Rx1RowWidth {
row,
width,
expected,
dwell_limited,
} => {
let which = if *dwell_limited { "dwell-limited " } else { "" };
write!(
f,
"{which}RX1 row {row} has {width} entries, but the plan's offsets need {expected}"
)
}
Self::TableLength { length, expected } => {
write!(f, "a table has {length} entries for {expected} data rates")
}
Self::Rx2DataRate { data_rate, defined } => write!(
f,
"RX2 listens at data rate {data_rate}, but the plan defines {defined}"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for PlanError {}
#[derive(Clone, Debug)]
pub struct OwnedChannelPlan {
name: String,
uplink_data_rates: Vec<Option<DataRate>>,
downlink_data_rates: Vec<Option<DataRate>>,
max_payload_repeater: Vec<Option<MaxPayload>>,
max_payload_direct: Vec<Option<MaxPayload>>,
downlink_max_payload_repeater: Vec<Option<MaxPayload>>,
downlink_max_payload_direct: Vec<Option<MaxPayload>>,
max_payload_dwell_limited: Option<Vec<Option<MaxPayload>>>,
join_channels: Vec<ChannelBlock>,
default_channels: Vec<ChannelBlock>,
sub_bands: Vec<SubBand>,
default_max_eirp_dbm: i8,
tx_power_step_db: u8,
max_tx_power_index: u8,
rx1_rows: Vec<Box<[u8]>>,
rx1_rows_dwell_limited: Option<Vec<Box<[u8]>>>,
max_rx1_data_rate_offset: u8,
rx2_frequency_hz: u32,
rx2_data_rate: u8,
data_rate_backoff: Vec<Option<u8>>,
beacon: Beacon,
has_dwell_time_limit: bool,
}
impl OwnedChannelPlan {
pub fn from_plan(plan: &ChannelPlan<'_>) -> Self {
Self {
name: plan.name.into(),
uplink_data_rates: plan.uplink_data_rates.to_vec(),
downlink_data_rates: plan.downlink_data_rates.to_vec(),
max_payload_repeater: plan.max_payload_repeater.to_vec(),
max_payload_direct: plan.max_payload_direct.to_vec(),
downlink_max_payload_repeater: plan.downlink_max_payload_repeater.to_vec(),
downlink_max_payload_direct: plan.downlink_max_payload_direct.to_vec(),
max_payload_dwell_limited: plan.max_payload_dwell_limited.map(<[_]>::to_vec),
join_channels: plan.join_channels.to_vec(),
default_channels: plan.default_channels.to_vec(),
sub_bands: plan.sub_bands.to_vec(),
default_max_eirp_dbm: plan.default_max_eirp_dbm,
tx_power_step_db: plan.tx_power_step_db,
max_tx_power_index: plan.max_tx_power_index,
rx1_rows: plan
.rx1_data_rate_offsets
.iter()
.map(|&r| r.into())
.collect(),
rx1_rows_dwell_limited: plan
.rx1_data_rate_offsets_dwell_limited
.map(|rows| rows.iter().map(|&r| r.into()).collect()),
max_rx1_data_rate_offset: plan.max_rx1_data_rate_offset,
rx2_frequency_hz: plan.rx2_frequency_hz,
rx2_data_rate: plan.rx2_data_rate,
data_rate_backoff: plan.data_rate_backoff.to_vec(),
beacon: plan.beacon,
has_dwell_time_limit: plan.has_dwell_time_limit,
}
}
pub fn with_plan<R>(&self, query: impl FnOnce(&ChannelPlan<'_>) -> R) -> R {
let rx1: Vec<&[u8]> = self.rx1_rows.iter().map(|row| &row[..]).collect();
let dwell_rx1: Option<Vec<&[u8]>> = self
.rx1_rows_dwell_limited
.as_ref()
.map(|rows| rows.iter().map(|row| &row[..]).collect());
let plan = ChannelPlan {
name: &self.name,
uplink_data_rates: &self.uplink_data_rates,
downlink_data_rates: &self.downlink_data_rates,
max_payload_repeater: &self.max_payload_repeater,
max_payload_direct: &self.max_payload_direct,
downlink_max_payload_repeater: &self.downlink_max_payload_repeater,
downlink_max_payload_direct: &self.downlink_max_payload_direct,
max_payload_dwell_limited: self.max_payload_dwell_limited.as_deref(),
join_channels: &self.join_channels,
default_channels: &self.default_channels,
sub_bands: &self.sub_bands,
default_max_eirp_dbm: self.default_max_eirp_dbm,
tx_power_step_db: self.tx_power_step_db,
max_tx_power_index: self.max_tx_power_index,
rx1_data_rate_offsets: &rx1,
rx1_data_rate_offsets_dwell_limited: dwell_rx1.as_deref(),
max_rx1_data_rate_offset: self.max_rx1_data_rate_offset,
rx2_frequency_hz: self.rx2_frequency_hz,
rx2_data_rate: self.rx2_data_rate,
data_rate_backoff: &self.data_rate_backoff,
beacon: self.beacon,
has_dwell_time_limit: self.has_dwell_time_limit,
};
query(&plan)
}
}
#[derive(Clone, Debug)]
pub struct ChannelPlanBuilder {
plan: OwnedChannelPlan,
}
impl ChannelPlanBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
plan: OwnedChannelPlan {
name: name.into(),
uplink_data_rates: Vec::new(),
downlink_data_rates: Vec::new(),
max_payload_repeater: Vec::new(),
max_payload_direct: Vec::new(),
downlink_max_payload_repeater: Vec::new(),
downlink_max_payload_direct: Vec::new(),
max_payload_dwell_limited: None,
join_channels: Vec::new(),
default_channels: Vec::new(),
sub_bands: Vec::new(),
default_max_eirp_dbm: 16,
tx_power_step_db: 2,
max_tx_power_index: 7,
rx1_rows: Vec::new(),
rx1_rows_dwell_limited: None,
max_rx1_data_rate_offset: 0,
rx2_frequency_hz: 0,
rx2_data_rate: 0,
data_rate_backoff: Vec::new(),
beacon: Beacon {
data_rate: 0,
frequency_hz: 0,
ping_slot_frequency_hz: 0,
},
has_dwell_time_limit: false,
},
}
}
#[must_use]
pub fn uplink_data_rate(mut self, rate: Option<DataRate>) -> Self {
self.plan.uplink_data_rates.push(rate);
self
}
#[must_use]
pub fn downlink_data_rate(mut self, rate: Option<DataRate>) -> Self {
self.plan.downlink_data_rates.push(rate);
self
}
#[must_use]
pub fn max_payload(mut self, table: PayloadTable, payload: Option<MaxPayload>) -> Self {
match table {
PayloadTable::UplinkRepeater => self.plan.max_payload_repeater.push(payload),
PayloadTable::UplinkDirect => self.plan.max_payload_direct.push(payload),
PayloadTable::DownlinkRepeater => self.plan.downlink_max_payload_repeater.push(payload),
PayloadTable::DownlinkDirect => self.plan.downlink_max_payload_direct.push(payload),
PayloadTable::DwellLimited => self
.plan
.max_payload_dwell_limited
.get_or_insert_with(Vec::new)
.push(payload),
}
self
}
#[must_use]
pub fn join_channel(mut self, block: ChannelBlock) -> Self {
self.plan.join_channels.push(block);
self
}
#[must_use]
pub fn default_channel(mut self, block: ChannelBlock) -> Self {
self.plan.default_channels.push(block);
self
}
#[must_use]
pub fn sub_band(mut self, band: SubBand) -> Self {
self.plan.sub_bands.push(band);
self
}
#[must_use]
pub fn rx1_row(mut self, offsets: &[u8]) -> Self {
self.plan.rx1_rows.push(offsets.into());
self
}
#[must_use]
pub fn rx1_row_dwell_limited(mut self, offsets: &[u8]) -> Self {
self.plan
.rx1_rows_dwell_limited
.get_or_insert_with(Vec::new)
.push(offsets.into());
self
}
#[must_use]
pub fn backoff(mut self, lower: Option<u8>) -> Self {
self.plan.data_rate_backoff.push(lower);
self
}
#[must_use]
pub fn power(mut self, default_max_eirp_dbm: i8, step_db: u8, max_index: u8) -> Self {
self.plan.default_max_eirp_dbm = default_max_eirp_dbm;
self.plan.tx_power_step_db = step_db;
self.plan.max_tx_power_index = max_index;
self
}
#[must_use]
pub fn rx(mut self, rx2_frequency_hz: u32, rx2_data_rate: u8, max_rx1_offset: u8) -> Self {
self.plan.rx2_frequency_hz = rx2_frequency_hz;
self.plan.rx2_data_rate = rx2_data_rate;
self.plan.max_rx1_data_rate_offset = max_rx1_offset;
self
}
#[must_use]
pub fn beacon(mut self, beacon: Beacon) -> Self {
self.plan.beacon = beacon;
self
}
#[must_use]
pub fn dwell_time_limit(mut self, limited: bool) -> Self {
self.plan.has_dwell_time_limit = limited;
self
}
pub fn build(self) -> Result<OwnedChannelPlan, PlanError> {
let mut plan = self.plan;
if plan.downlink_data_rates.is_empty() {
plan.downlink_data_rates = plan.uplink_data_rates.clone();
}
if plan.downlink_max_payload_repeater.is_empty() {
plan.downlink_max_payload_repeater = plan.max_payload_repeater.clone();
}
if plan.downlink_max_payload_direct.is_empty() {
plan.downlink_max_payload_direct = plan.max_payload_direct.clone();
}
if plan.data_rate_backoff.is_empty() {
plan.data_rate_backoff = (0..plan.uplink_data_rates.len())
.map(|index| index.checked_sub(1).map(|lower| lower as u8))
.collect();
}
check(&plan)?;
Ok(plan)
}
}
fn check(plan: &OwnedChannelPlan) -> Result<(), PlanError> {
let rates = plan.uplink_data_rates.len();
if rates == 0 {
return Err(PlanError::NoDataRates);
}
let width = usize::from(plan.max_rx1_data_rate_offset) + 1;
for (dwell_limited, rows) in [
(false, Some(&plan.rx1_rows)),
(true, plan.rx1_rows_dwell_limited.as_ref()),
] {
let Some(rows) = rows else {
continue;
};
if rows.len() != rates {
return Err(PlanError::Rx1RowCount {
rows: rows.len(),
expected: rates,
dwell_limited,
});
}
for (row, entries) in rows.iter().enumerate() {
if entries.len() != width {
return Err(PlanError::Rx1RowWidth {
row,
width: entries.len(),
expected: width,
dwell_limited,
});
}
}
}
if plan.data_rate_backoff.len() != rates {
return Err(PlanError::TableLength {
length: plan.data_rate_backoff.len(),
expected: rates,
});
}
for table in [&plan.max_payload_repeater, &plan.max_payload_direct] {
if !table.is_empty() && table.len() != rates {
return Err(PlanError::TableLength {
length: table.len(),
expected: rates,
});
}
}
if let Some(table) = &plan.max_payload_dwell_limited {
if table.len() != rates {
return Err(PlanError::TableLength {
length: table.len(),
expected: rates,
});
}
}
let downlink = plan.downlink_data_rates.len();
for table in [
&plan.downlink_max_payload_repeater,
&plan.downlink_max_payload_direct,
] {
if !table.is_empty() && table.len() != downlink {
return Err(PlanError::TableLength {
length: table.len(),
expected: downlink,
});
}
}
if usize::from(plan.rx2_data_rate) >= downlink {
return Err(PlanError::Rx2DataRate {
data_rate: plan.rx2_data_rate,
defined: downlink,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal() -> ChannelPlanBuilder {
ChannelPlanBuilder::new("test")
.uplink_data_rate(Some(DataRate::lora(12, 125_000, 250)))
.uplink_data_rate(Some(DataRate::lora(7, 125_000, 5_470)))
.rx(915_000_000, 0, 0)
.rx1_row(&[0])
.rx1_row(&[1])
}
#[test]
fn an_empty_plan_is_refused() {
assert_eq!(
ChannelPlanBuilder::new("empty").build().unwrap_err(),
PlanError::NoDataRates
);
}
#[test]
fn the_downlink_tables_mirror_the_uplink_ones_when_left_empty() {
let plan = minimal()
.max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(59, 51)))
.max_payload(PayloadTable::UplinkDirect, Some(MaxPayload::new(230, 222)))
.build()
.expect("consistent");
assert_eq!(
plan.with_plan(|plan| plan.downlink_max_payload(1, false)),
Some(MaxPayload::new(230, 222))
);
assert_eq!(
plan.with_plan(|plan| plan.downlink_data_rate(1)),
Some(DataRate::lora(7, 125_000, 5_470))
);
}
#[test]
fn an_unset_backoff_chain_steps_down_one_rate_at_a_time() {
let plan = minimal().build().expect("consistent");
assert_eq!(
plan.with_plan(|plan| plan.next_backoff_data_rate(1)),
Some(0)
);
assert_eq!(plan.with_plan(|plan| plan.next_backoff_data_rate(0)), None);
}
#[test]
fn a_row_narrower_than_the_offsets_allow_is_refused() {
let error = minimal().rx(915_000_000, 0, 5).build().unwrap_err();
assert_eq!(
error,
PlanError::Rx1RowWidth {
row: 0,
width: 1,
expected: 6,
dwell_limited: false,
}
);
}
#[test]
fn a_missing_rx1_row_is_refused() {
let error = ChannelPlanBuilder::new("short")
.uplink_data_rate(Some(DataRate::lora(12, 125_000, 250)))
.uplink_data_rate(Some(DataRate::lora(7, 125_000, 5_470)))
.rx(915_000_000, 0, 0)
.rx1_row(&[0])
.build()
.unwrap_err();
assert_eq!(
error,
PlanError::Rx1RowCount {
rows: 1,
expected: 2,
dwell_limited: false,
}
);
}
#[test]
fn listening_at_a_data_rate_the_plan_lacks_is_refused() {
let error = minimal().rx(915_000_000, 9, 0).build().unwrap_err();
assert_eq!(
error,
PlanError::Rx2DataRate {
data_rate: 9,
defined: 2,
}
);
}
#[test]
fn a_dwell_limited_mapping_is_checked_like_the_ordinary_one() {
let error = minimal().rx1_row_dwell_limited(&[0]).build().unwrap_err();
assert_eq!(
error,
PlanError::Rx1RowCount {
rows: 1,
expected: 2,
dwell_limited: true,
}
);
}
#[test]
#[cfg(feature = "au915")]
fn a_published_plan_survives_the_round_trip_into_owned_storage() {
use super::super::Region;
let published = Region::Au915.plan();
let owned = OwnedChannelPlan::from_plan(published);
owned.with_plan(|copy| {
assert_eq!(copy.name, published.name);
assert_eq!(copy.rx2(), published.rx2());
assert_eq!(
copy.default_channel_count(),
published.default_channel_count()
);
for data_rate in 0..16 {
assert_eq!(
copy.uplink_data_rate(data_rate),
published.uplink_data_rate(data_rate)
);
assert_eq!(
copy.downlink_data_rate(data_rate),
published.downlink_data_rate(data_rate)
);
assert_eq!(
copy.max_payload(data_rate, true),
published.max_payload(data_rate, true)
);
assert_eq!(
copy.max_payload_dwell_limited(data_rate),
published.max_payload_dwell_limited(data_rate)
);
for offset in 0..8 {
assert_eq!(
copy.rx1_data_rate(data_rate, offset),
published.rx1_data_rate(data_rate, offset)
);
}
}
});
}
}