#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
#[derive(Default)]
pub enum SpreadingFactor {
Sf7 = 7,
Sf8 = 8,
#[default]
Sf9 = 9,
Sf10 = 10,
Sf11 = 11,
Sf12 = 12,
}
impl SpreadingFactor {
pub const fn value(self) -> u8 {
self as u8
}
pub const fn approx_bitrate(self) -> u32 {
match self {
Self::Sf7 => 5470,
Self::Sf8 => 3125,
Self::Sf9 => 1760,
Self::Sf10 => 980,
Self::Sf11 => 440,
Self::Sf12 => 250,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
#[derive(Default)]
pub enum Bandwidth {
Bw7_8 = 0,
Bw10_4 = 1,
Bw15_6 = 2,
Bw20_8 = 3,
Bw31_25 = 4,
Bw41_7 = 5,
Bw62_5 = 6,
#[default]
Bw125 = 7,
Bw250 = 8,
Bw500 = 9,
}
impl Bandwidth {
pub const fn value(self) -> u8 {
self as u8
}
pub const fn hz(self) -> u32 {
match self {
Self::Bw7_8 => 7_800,
Self::Bw10_4 => 10_400,
Self::Bw15_6 => 15_600,
Self::Bw20_8 => 20_800,
Self::Bw31_25 => 31_250,
Self::Bw41_7 => 41_700,
Self::Bw62_5 => 62_500,
Self::Bw125 => 125_000,
Self::Bw250 => 250_000,
Self::Bw500 => 500_000,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
#[derive(Default)]
pub enum CodingRate {
#[default]
Cr4_5 = 1,
Cr4_6 = 2,
Cr4_7 = 3,
Cr4_8 = 4,
}
impl CodingRate {
pub const fn value(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone)]
pub struct LoRaConfig {
pub frequency_mhz: f32,
pub spreading_factor: SpreadingFactor,
pub bandwidth: Bandwidth,
pub coding_rate: CodingRate,
pub tx_power_dbm: i8,
pub rx_timeout_ms: u32,
pub preamble_length: u16,
pub crc_enabled: bool,
}
impl LoRaConfig {
pub fn from_profile(profile: LoRaProfile, frequency_mhz: f32) -> Self {
match profile {
LoRaProfile::Fast => Self {
frequency_mhz,
spreading_factor: SpreadingFactor::Sf7,
bandwidth: Bandwidth::Bw250,
coding_rate: CodingRate::Cr4_5,
tx_power_dbm: 14,
rx_timeout_ms: 1000,
preamble_length: 8,
crc_enabled: true,
},
LoRaProfile::Balanced => Self {
frequency_mhz,
spreading_factor: SpreadingFactor::Sf9,
bandwidth: Bandwidth::Bw125,
coding_rate: CodingRate::Cr4_5,
tx_power_dbm: 14,
rx_timeout_ms: 3000,
preamble_length: 8,
crc_enabled: true,
},
LoRaProfile::LongRange => Self {
frequency_mhz,
spreading_factor: SpreadingFactor::Sf12,
bandwidth: Bandwidth::Bw125,
coding_rate: CodingRate::Cr4_8,
tx_power_dbm: 20,
rx_timeout_ms: 10000,
preamble_length: 12,
crc_enabled: true,
},
}
}
pub fn eu868(profile: LoRaProfile) -> Self {
Self::from_profile(profile, 868.0)
}
pub fn us915(profile: LoRaProfile) -> Self {
Self::from_profile(profile, 915.0)
}
pub fn time_on_air_ms(&self, payload_bytes: usize) -> u32 {
let sf = self.spreading_factor.value() as u32;
let bw = self.bandwidth.hz();
let cr = self.coding_rate.value() as u32;
let preamble = self.preamble_length as u32;
let payload = payload_bytes as u32;
let t_sym_us = ((1u64 << sf) * 1_000_000) / (bw as u64);
let t_preamble_us = (preamble + 4) * t_sym_us as u32;
let de = if sf >= 11 { 1u32 } else { 0 };
let header_bits = 20u32;
let numerator = (8 * payload + header_bits + 16).saturating_sub(4 * sf);
let denominator = 4 * (sf.saturating_sub(2 * de));
let n_payload = if denominator > 0 && numerator > 0 {
let ceil_div = numerator.div_ceil(denominator);
8 + ceil_div * (cr + 4)
} else {
8
};
let t_payload_us = n_payload as u64 * t_sym_us;
((t_preamble_us as u64 + t_payload_us) / 1000) as u32
}
}
impl Default for LoRaConfig {
fn default() -> Self {
Self::eu868(LoRaProfile::Balanced)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoRaProfile {
Fast,
Balanced,
LongRange,
}
impl LoRaProfile {
pub const fn approx_bitrate(self) -> u32 {
match self {
Self::Fast => 11000,
Self::Balanced => 3000,
Self::LongRange => 300,
}
}
pub const fn approx_range_m(self) -> u32 {
match self {
Self::Fast => 2000,
Self::Balanced => 5000,
Self::LongRange => 15000,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spreading_factor_values() {
assert_eq!(SpreadingFactor::Sf7.value(), 7);
assert_eq!(SpreadingFactor::Sf12.value(), 12);
}
#[test]
fn test_bandwidth_hz() {
assert_eq!(Bandwidth::Bw125.hz(), 125_000);
assert_eq!(Bandwidth::Bw250.hz(), 250_000);
}
#[test]
fn test_profile_configs() {
let fast = LoRaConfig::from_profile(LoRaProfile::Fast, 868.0);
assert_eq!(fast.spreading_factor, SpreadingFactor::Sf7);
assert_eq!(fast.bandwidth, Bandwidth::Bw250);
let long = LoRaConfig::from_profile(LoRaProfile::LongRange, 915.0);
assert_eq!(long.spreading_factor, SpreadingFactor::Sf12);
assert_eq!(long.tx_power_dbm, 20);
}
#[test]
fn test_time_on_air() {
let config = LoRaConfig::eu868(LoRaProfile::Balanced);
let toa = config.time_on_air_ms(50);
assert!(
toa > 100 && toa < 600,
"ToA was {} ms, expected 100-600",
toa
);
}
}