const LOW_DATA_RATE_THRESHOLD_US: u64 = 16_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LinkSettings {
spreading_factor: u8,
bandwidth_hz: u32,
cr_denominator: u8,
preamble_symbols: u16,
explicit_header: bool,
crc: bool,
}
impl LinkSettings {
pub fn new(spreading_factor: u8, bandwidth_hz: u32) -> Self {
Self {
spreading_factor: spreading_factor.clamp(5, 12),
bandwidth_hz,
cr_denominator: 5,
preamble_symbols: 8,
explicit_header: true,
crc: true,
}
}
pub fn with_coding_rate(mut self, denominator: u8) -> Self {
self.cr_denominator = denominator.clamp(5, 8);
self
}
pub fn with_preamble(mut self, symbols: u16) -> Self {
self.preamble_symbols = symbols;
self
}
pub fn implicit_header(mut self) -> Self {
self.explicit_header = false;
self
}
pub fn without_crc(mut self) -> Self {
self.crc = false;
self
}
pub fn spreading_factor(&self) -> u8 {
self.spreading_factor
}
pub fn bandwidth_hz(&self) -> u32 {
self.bandwidth_hz
}
pub fn symbol_time_us(&self) -> u64 {
(1u64 << self.spreading_factor) * 1_000_000 / u64::from(self.bandwidth_hz)
}
fn payload_symbols(&self, payload_len: usize) -> u32 {
let sf = i32::from(self.spreading_factor);
let low_data_rate = self.symbol_time_us() > LOW_DATA_RATE_THRESHOLD_US;
let de = i32::from(low_data_rate);
let ih = i32::from(!self.explicit_header);
let crc = i32::from(self.crc);
let numerator = 8 * payload_len as i32 - 4 * sf + 28 + 16 * crc - 20 * ih;
let denominator = 4 * (sf - 2 * de); let term = if numerator > 0 {
let groups = (numerator as u32).div_ceil(denominator as u32);
groups * u32::from(self.cr_denominator)
} else {
0
};
8 + term
}
pub fn airtime_us(&self, payload_len: usize) -> u64 {
let payload_symbols = u64::from(self.payload_symbols(payload_len));
let quarter_symbols = (4 * u64::from(self.preamble_symbols) + 17) + 4 * payload_symbols;
let symbol_units = quarter_symbols * (1u64 << self.spreading_factor);
symbol_units * 1_000_000 / (4 * u64::from(self.bandwidth_hz))
}
pub fn min_off_time_us(&self, payload_len: usize, duty_cycle_permille: u32) -> u64 {
if duty_cycle_permille == 0 {
return u64::MAX;
}
let airtime = self.airtime_us(payload_len);
let permille = u64::from(duty_cycle_permille);
airtime * (1000 - permille) / permille
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn airtime_matches_the_reference_sf12() {
let link = LinkSettings::new(12, 125_000);
assert_eq!(link.airtime_us(10), 991_232);
}
#[test]
fn airtime_matches_the_reference_sf7() {
let link = LinkSettings::new(7, 125_000);
assert_eq!(link.airtime_us(10), 41_216);
}
#[test]
fn a_higher_spreading_factor_is_slower() {
let slow = LinkSettings::new(12, 125_000);
let fast = LinkSettings::new(7, 125_000);
assert!(slow.airtime_us(20) > fast.airtime_us(20) * 10);
}
#[test]
fn symbol_time_follows_spreading_factor_and_bandwidth() {
assert_eq!(LinkSettings::new(12, 125_000).symbol_time_us(), 32_768);
assert_eq!(LinkSettings::new(7, 125_000).symbol_time_us(), 1_024);
assert_eq!(LinkSettings::new(7, 250_000).symbol_time_us(), 512);
}
#[test]
fn the_spreading_factor_is_clamped_to_the_lora_range() {
assert_eq!(LinkSettings::new(3, 125_000).spreading_factor(), 5);
assert_eq!(LinkSettings::new(20, 125_000).spreading_factor(), 12);
}
#[test]
fn the_fastest_spreading_factors_survive_unclamped() {
assert_eq!(LinkSettings::new(6, 125_000).spreading_factor(), 6);
assert_eq!(LinkSettings::new(5, 125_000).spreading_factor(), 5);
assert!(
LinkSettings::new(5, 125_000).airtime_us(10)
< LinkSettings::new(7, 125_000).airtime_us(10),
"a lower spreading factor is faster"
);
}
#[test]
fn a_one_percent_duty_cycle_forces_ninety_nine_times_the_airtime() {
let link = LinkSettings::new(7, 125_000);
let airtime = link.airtime_us(10);
assert_eq!(link.min_off_time_us(10, 10), airtime * 99);
}
#[test]
fn a_zero_duty_cycle_never_allows_another_send() {
let link = LinkSettings::new(7, 125_000);
assert_eq!(link.min_off_time_us(10, 0), u64::MAX);
}
#[test]
fn implicit_header_and_no_crc_shorten_the_frame() {
let full = LinkSettings::new(9, 125_000);
let lean = LinkSettings::new(9, 125_000)
.implicit_header()
.without_crc();
assert!(lean.airtime_us(20) < full.airtime_us(20));
}
#[test]
fn a_zero_byte_payload_still_costs_the_preamble_and_header() {
let link = LinkSettings::new(7, 125_000);
assert!(link.airtime_us(0) > 0);
assert!(link.airtime_us(0) < link.airtime_us(10));
}
#[test]
fn airtime_grows_with_the_payload() {
let link = LinkSettings::new(10, 125_000);
assert!(link.airtime_us(1) <= link.airtime_us(10));
assert!(link.airtime_us(10) < link.airtime_us(64));
}
#[test]
fn a_wider_bandwidth_is_faster() {
let narrow = LinkSettings::new(9, 125_000).airtime_us(20);
let wide = LinkSettings::new(9, 250_000).airtime_us(20);
assert!(wide < narrow);
}
}