lora_modulation/
lib.rs

1#![deny(rust_2018_idioms)]
2#![cfg_attr(not(test), no_std)]
3#![doc = include_str!("../README.md")]
4
5#[cfg_attr(feature = "defmt", derive(defmt::Format))]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8/// Channel width. Lower values increase time on air, but may be able to find clear frequencies.
9pub enum Bandwidth {
10    _7KHz,
11    _10KHz,
12    _15KHz,
13    _20KHz,
14    _31KHz,
15    _41KHz,
16    _62KHz,
17    _125KHz,
18    _250KHz,
19    _500KHz,
20}
21
22impl Bandwidth {
23    pub const fn hz(self) -> u32 {
24        match self {
25            Bandwidth::_7KHz => 7810u32,
26            Bandwidth::_10KHz => 10420u32,
27            Bandwidth::_15KHz => 15630u32,
28            Bandwidth::_20KHz => 20830u32,
29            Bandwidth::_31KHz => 31250u32,
30            Bandwidth::_41KHz => 41670u32,
31            Bandwidth::_62KHz => 62500u32,
32            Bandwidth::_125KHz => 125000u32,
33            Bandwidth::_250KHz => 250000u32,
34            Bandwidth::_500KHz => 500000u32,
35        }
36    }
37}
38
39impl From<Bandwidth> for u32 {
40    fn from(value: Bandwidth) -> Self {
41        value.hz()
42    }
43}
44
45#[cfg_attr(feature = "defmt", derive(defmt::Format))]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48/// Controls the chirp rate. Lower values are slower bandwidth (longer time on air), but more robust.
49pub enum SpreadingFactor {
50    _5,
51    _6,
52    _7,
53    _8,
54    _9,
55    _10,
56    _11,
57    _12,
58}
59
60impl SpreadingFactor {
61    pub const fn factor(self) -> u32 {
62        match self {
63            SpreadingFactor::_5 => 5,
64            SpreadingFactor::_6 => 6,
65            SpreadingFactor::_7 => 7,
66            SpreadingFactor::_8 => 8,
67            SpreadingFactor::_9 => 9,
68            SpreadingFactor::_10 => 10,
69            SpreadingFactor::_11 => 11,
70            SpreadingFactor::_12 => 12,
71        }
72    }
73}
74
75impl From<SpreadingFactor> for u32 {
76    fn from(sf: SpreadingFactor) -> Self {
77        sf.factor()
78    }
79}
80
81#[cfg_attr(feature = "defmt", derive(defmt::Format))]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84/// Controls the forward error correction. Higher values are more robust, but reduces the ratio
85/// of actual data in transmissions.
86pub enum CodingRate {
87    _4_5,
88    _4_6,
89    _4_7,
90    _4_8,
91}
92
93impl CodingRate {
94    pub const fn denom(&self) -> u32 {
95        match self {
96            CodingRate::_4_5 => 5,
97            CodingRate::_4_6 => 6,
98            CodingRate::_4_7 => 7,
99            CodingRate::_4_8 => 8,
100        }
101    }
102}
103
104/// LoRa modulation parameters barring frequency
105#[cfg_attr(feature = "defmt", derive(defmt::Format))]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107#[derive(Debug, Clone, Copy, PartialEq)]
108pub struct BaseBandModulationParams {
109    pub sf: SpreadingFactor,
110    pub bw: Bandwidth,
111    pub cr: CodingRate,
112    /// low data rate optimization, see SX127x datasheet section 4.1.1.6
113    pub ldro: bool,
114    /// eagerly pre-calculated symbol duration in microseconds
115    t_sym_us: u32,
116}
117
118impl BaseBandModulationParams {
119    /// Create a set of parameters, possible forcing low data rate optimization on or off.
120    /// Low data rate optimization is determined automatically
121    /// based on `sf` and `bw` according to Semtech's datasheets for SX126x/SX127x
122    /// (enabled if symbol length is >= 16.38ms)
123    pub const fn new(sf: SpreadingFactor, bw: Bandwidth, cr: CodingRate) -> Self {
124        let t_sym_us = 2u32.pow(sf.factor()) * 1_000_000 / bw.hz();
125        // according to SX127x 4.1.1.6 it's 16ms
126        // SX126x says it's 16.38ms
127        // probably it's 16.384ms which is SF11@125kHz
128        let ldro = t_sym_us >= 16_384;
129        Self { sf, bw, cr, ldro, t_sym_us }
130    }
131
132    pub const fn delay_in_symbols(&self, delay_in_ms: u32) -> u16 {
133        (delay_in_ms * 1000 / self.t_sym_us) as u16
134    }
135
136    pub const fn symbols_to_ms(&self, symbols: u32) -> u32 {
137        (self.t_sym_us * symbols) / 1_000
138    }
139
140    /// Calculates time on air for a given payload and modulation parameters.
141    /// If `preamble` is None, the whole preamble including syncword is excluded from calculation.
142    pub const fn time_on_air_us(
143        &self,
144        preamble: Option<u8>,
145        explicit_header: bool,
146        len: u8,
147    ) -> u32 {
148        let sf = self.sf.factor() as i32;
149        let t_sym_us = self.t_sym_us;
150
151        let cr = self.cr.denom() as i32;
152        let de = if self.ldro {
153            1
154        } else {
155            0
156        };
157        let h = if explicit_header {
158            0
159        } else {
160            1
161        };
162
163        const fn div_ceil(num: i32, denom: i32) -> i32 {
164            (num - 1) / denom + 1
165        }
166
167        let big_ratio = div_ceil(8 * len as i32 - 4 * sf + 28 + 16 - 20 * h, 4 * (sf - 2 * de));
168        let big_ratio = if big_ratio > 0 {
169            big_ratio
170        } else {
171            0
172        };
173        let payload_symb_nb = (8 + big_ratio * cr) as u32;
174
175        match preamble {
176            None => t_sym_us * payload_symb_nb,
177            Some(preamble) => (4 * preamble as u32 + 17 + 4 * payload_symb_nb) * t_sym_us / 4,
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    const LORAWAN_OVERHEAD: u8 = 13;
187    // the shortest t_sym
188    const SF5BW500: BaseBandModulationParams =
189        BaseBandModulationParams::new(SpreadingFactor::_5, Bandwidth::_500KHz, CodingRate::_4_5);
190
191    // EU868 DR6
192    const SF7BW250: BaseBandModulationParams =
193        BaseBandModulationParams::new(SpreadingFactor::_7, Bandwidth::_250KHz, CodingRate::_4_5);
194    // EU868 DR5
195    const SF7BW125: BaseBandModulationParams =
196        BaseBandModulationParams::new(SpreadingFactor::_7, Bandwidth::_125KHz, CodingRate::_4_5);
197    // EU868 DR4
198    const SF8BW125: BaseBandModulationParams =
199        BaseBandModulationParams::new(SpreadingFactor::_8, Bandwidth::_125KHz, CodingRate::_4_5);
200    // EU868 DR3
201    const SF9BW125: BaseBandModulationParams =
202        BaseBandModulationParams::new(SpreadingFactor::_9, Bandwidth::_125KHz, CodingRate::_4_5);
203    // EU868 DR2
204    const SF10BW125: BaseBandModulationParams =
205        BaseBandModulationParams::new(SpreadingFactor::_10, Bandwidth::_125KHz, CodingRate::_4_5);
206    // EU868 DR1
207    const SF11BW125: BaseBandModulationParams =
208        BaseBandModulationParams::new(SpreadingFactor::_11, Bandwidth::_125KHz, CodingRate::_4_5);
209    // EU868 DR0
210    const SF12BW125: BaseBandModulationParams =
211        BaseBandModulationParams::new(SpreadingFactor::_12, Bandwidth::_125KHz, CodingRate::_4_5);
212
213    fn lorawan_airtime_us(params: &BaseBandModulationParams, app_payload_length: u8) -> u32 {
214        params.time_on_air_us(Some(8), true, LORAWAN_OVERHEAD + app_payload_length)
215    }
216
217    // data for time-on-air tests is verified against:
218    // * https://www.thethingsnetwork.org/airtime-calculator
219    // * https://avbentem.github.io/airtime-calculator/ttn/
220
221    #[test]
222    fn time_on_air_for_short_messages() {
223        assert_eq!(1152, SF5BW500.time_on_air_us(None, true, 0));
224        assert_eq!(6656, SF7BW250.time_on_air_us(None, true, 0));
225        assert_eq!(13312, SF7BW125.time_on_air_us(None, true, 0));
226    }
227
228    #[test]
229    fn time_on_air() {
230        let length = 25;
231        assert_eq!(41_088, lorawan_airtime_us(&SF7BW250, length));
232        assert_eq!(82_176, lorawan_airtime_us(&SF7BW125, length));
233        assert_eq!(143_872, lorawan_airtime_us(&SF8BW125, length));
234        assert_eq!(267_264, lorawan_airtime_us(&SF9BW125, length));
235        assert_eq!(493_568, lorawan_airtime_us(&SF10BW125, length));
236        assert_eq!(1_069_056, lorawan_airtime_us(&SF11BW125, length));
237        assert_eq!(1_974_272, lorawan_airtime_us(&SF12BW125, length));
238
239        let length = 26;
240        assert_eq!(41_088, lorawan_airtime_us(&SF7BW250, length));
241        assert_eq!(82_176, lorawan_airtime_us(&SF7BW125, length));
242        assert_eq!(154_112, lorawan_airtime_us(&SF8BW125, length));
243        assert_eq!(267_264, lorawan_airtime_us(&SF9BW125, length));
244        assert_eq!(493_568, lorawan_airtime_us(&SF10BW125, length));
245        assert_eq!(1_069_056, lorawan_airtime_us(&SF11BW125, length));
246        assert_eq!(1_974_272, lorawan_airtime_us(&SF12BW125, length));
247
248        let length = 27;
249        assert_eq!(41_088, lorawan_airtime_us(&SF7BW250, length));
250        assert_eq!(82_176, lorawan_airtime_us(&SF7BW125, length));
251        assert_eq!(154_112, lorawan_airtime_us(&SF8BW125, length));
252        assert_eq!(287_744, lorawan_airtime_us(&SF9BW125, length));
253        assert_eq!(534_528, lorawan_airtime_us(&SF10BW125, length));
254        assert_eq!(1_069_056, lorawan_airtime_us(&SF11BW125, length));
255        assert_eq!(1_974_272, lorawan_airtime_us(&SF12BW125, length));
256
257        let length = 28;
258        assert_eq!(43_648, lorawan_airtime_us(&SF7BW250, length));
259        assert_eq!(87_296, lorawan_airtime_us(&SF7BW125, length));
260        assert_eq!(154_112, lorawan_airtime_us(&SF8BW125, length));
261        assert_eq!(287_744, lorawan_airtime_us(&SF9BW125, length));
262        assert_eq!(534_528, lorawan_airtime_us(&SF10BW125, length));
263        assert_eq!(1_150_976, lorawan_airtime_us(&SF11BW125, length));
264        assert_eq!(2_138_112, lorawan_airtime_us(&SF12BW125, length));
265    }
266}