use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Modem {
pub sf: u8,
pub bw_khz: u32,
pub cr: u8,
pub preamble: u16,
pub crc: bool,
pub implicit_header: bool,
}
impl Modem {
pub const SF9_BW125: Modem = Modem {
sf: 9,
bw_khz: 125,
cr: 1,
preamble: 8,
crc: true,
implicit_header: false,
};
pub fn symbol_us(&self) -> f64 {
(1u64 << self.sf) as f64 * 1000.0 / self.bw_khz as f64
}
pub fn snr_floor_db(&self) -> f64 {
-7.5 - 2.5 * (f64::from(self.sf.clamp(7, 12)) - 7.0)
}
}
pub fn time_on_air_us(m: &Modem, payload_len: usize) -> u64 {
let t_sym = m.symbol_us();
let de = if t_sym >= 16_000.0 { 1.0 } else { 0.0 };
let sf = f64::from(m.sf);
let crc = if m.crc { 1.0 } else { 0.0 };
let ih = if m.implicit_header { 1.0 } else { 0.0 };
let num = 8.0 * payload_len as f64 - 4.0 * sf + 28.0 + 16.0 * crc - 20.0 * ih;
let den = 4.0 * (sf - 2.0 * de);
let blocks = (num / den).ceil() * (f64::from(m.cr) + 4.0);
let n_pay = 8.0 + blocks.max(0.0);
let t_pre = (f64::from(m.preamble) + 4.25) * t_sym;
(t_pre + n_pay * t_sym).ceil() as u64
}
#[cfg(test)]
mod tests {
use super::*;
fn modem(sf: u8, bw_khz: u32, cr: u8, preamble: u16) -> Modem {
Modem {
sf,
bw_khz,
cr,
preamble,
crc: true,
implicit_header: false,
}
}
#[test]
fn sf9_bw125_22_bytes_is_the_spec_worked_example() {
assert_eq!(time_on_air_us(&Modem::SF9_BW125, 22), 205_824);
}
#[test]
fn sf7_bw125_10_bytes() {
assert_eq!(time_on_air_us(&modem(7, 125, 1, 8), 10), 41_216);
}
#[test]
fn sf12_bw125_low_data_rate_optimisation_on() {
assert_eq!(time_on_air_us(&modem(12, 125, 1, 8), 51), 2_465_792);
}
#[test]
fn sf10_bw250_cr48_16_symbol_preamble() {
assert_eq!(time_on_air_us(&modem(10, 250, 4, 16), 32), 345_088);
}
#[test]
fn snr_floor_steps_by_spreading_factor() {
assert_eq!(modem(7, 125, 1, 8).snr_floor_db(), -7.5);
assert_eq!(modem(9, 125, 1, 8).snr_floor_db(), -12.5);
assert_eq!(modem(12, 125, 1, 8).snr_floor_db(), -20.0);
}
}