hdds_micro/transport/lora/
config.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[repr(u8)]
11#[derive(Default)]
12pub enum SpreadingFactor {
13 Sf7 = 7,
15 Sf8 = 8,
17 #[default]
19 Sf9 = 9,
20 Sf10 = 10,
22 Sf11 = 11,
24 Sf12 = 12,
26}
27
28impl SpreadingFactor {
29 pub const fn value(self) -> u8 {
31 self as u8
32 }
33
34 pub const fn approx_bitrate(self) -> u32 {
36 match self {
37 Self::Sf7 => 5470,
38 Self::Sf8 => 3125,
39 Self::Sf9 => 1760,
40 Self::Sf10 => 980,
41 Self::Sf11 => 440,
42 Self::Sf12 => 250,
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[repr(u8)]
50#[derive(Default)]
51pub enum Bandwidth {
52 Bw7_8 = 0,
54 Bw10_4 = 1,
56 Bw15_6 = 2,
58 Bw20_8 = 3,
60 Bw31_25 = 4,
62 Bw41_7 = 5,
64 Bw62_5 = 6,
66 #[default]
68 Bw125 = 7,
69 Bw250 = 8,
71 Bw500 = 9,
73}
74
75impl Bandwidth {
76 pub const fn value(self) -> u8 {
78 self as u8
79 }
80
81 pub const fn hz(self) -> u32 {
83 match self {
84 Self::Bw7_8 => 7_800,
85 Self::Bw10_4 => 10_400,
86 Self::Bw15_6 => 15_600,
87 Self::Bw20_8 => 20_800,
88 Self::Bw31_25 => 31_250,
89 Self::Bw41_7 => 41_700,
90 Self::Bw62_5 => 62_500,
91 Self::Bw125 => 125_000,
92 Self::Bw250 => 250_000,
93 Self::Bw500 => 500_000,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100#[repr(u8)]
101#[derive(Default)]
102pub enum CodingRate {
103 #[default]
105 Cr4_5 = 1,
106 Cr4_6 = 2,
108 Cr4_7 = 3,
110 Cr4_8 = 4,
112}
113
114impl CodingRate {
115 pub const fn value(self) -> u8 {
117 self as u8
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct LoRaConfig {
124 pub frequency_mhz: f32,
126
127 pub spreading_factor: SpreadingFactor,
129
130 pub bandwidth: Bandwidth,
132
133 pub coding_rate: CodingRate,
135
136 pub tx_power_dbm: i8,
138
139 pub rx_timeout_ms: u32,
141
142 pub preamble_length: u16,
144
145 pub crc_enabled: bool,
147}
148
149impl LoRaConfig {
150 pub fn from_profile(profile: LoRaProfile, frequency_mhz: f32) -> Self {
152 match profile {
153 LoRaProfile::Fast => Self {
154 frequency_mhz,
155 spreading_factor: SpreadingFactor::Sf7,
156 bandwidth: Bandwidth::Bw250,
157 coding_rate: CodingRate::Cr4_5,
158 tx_power_dbm: 14,
159 rx_timeout_ms: 1000,
160 preamble_length: 8,
161 crc_enabled: true,
162 },
163 LoRaProfile::Balanced => Self {
164 frequency_mhz,
165 spreading_factor: SpreadingFactor::Sf9,
166 bandwidth: Bandwidth::Bw125,
167 coding_rate: CodingRate::Cr4_5,
168 tx_power_dbm: 14,
169 rx_timeout_ms: 3000,
170 preamble_length: 8,
171 crc_enabled: true,
172 },
173 LoRaProfile::LongRange => Self {
174 frequency_mhz,
175 spreading_factor: SpreadingFactor::Sf12,
176 bandwidth: Bandwidth::Bw125,
177 coding_rate: CodingRate::Cr4_8,
178 tx_power_dbm: 20,
179 rx_timeout_ms: 10000,
180 preamble_length: 12,
181 crc_enabled: true,
182 },
183 }
184 }
185
186 pub fn eu868(profile: LoRaProfile) -> Self {
188 Self::from_profile(profile, 868.0)
189 }
190
191 pub fn us915(profile: LoRaProfile) -> Self {
193 Self::from_profile(profile, 915.0)
194 }
195
196 pub fn time_on_air_ms(&self, payload_bytes: usize) -> u32 {
200 let sf = self.spreading_factor.value() as u32;
201 let bw = self.bandwidth.hz();
202 let cr = self.coding_rate.value() as u32;
203 let preamble = self.preamble_length as u32;
204 let payload = payload_bytes as u32;
205
206 let t_sym_us = ((1u64 << sf) * 1_000_000) / (bw as u64);
209
210 let t_preamble_us = (preamble + 4) * t_sym_us as u32;
213
214 let de = if sf >= 11 { 1u32 } else { 0 };
217
218 let header_bits = 20u32; let numerator = (8 * payload + header_bits + 16).saturating_sub(4 * sf);
225 let denominator = 4 * (sf.saturating_sub(2 * de));
226
227 let n_payload = if denominator > 0 && numerator > 0 {
228 let ceil_div = numerator.div_ceil(denominator);
229 8 + ceil_div * (cr + 4)
230 } else {
231 8
232 };
233
234 let t_payload_us = n_payload as u64 * t_sym_us;
235
236 ((t_preamble_us as u64 + t_payload_us) / 1000) as u32
238 }
239}
240
241impl Default for LoRaConfig {
242 fn default() -> Self {
243 Self::eu868(LoRaProfile::Balanced)
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub enum LoRaProfile {
250 Fast,
252 Balanced,
254 LongRange,
256}
257
258impl LoRaProfile {
259 pub const fn approx_bitrate(self) -> u32 {
261 match self {
262 Self::Fast => 11000,
263 Self::Balanced => 3000,
264 Self::LongRange => 300,
265 }
266 }
267
268 pub const fn approx_range_m(self) -> u32 {
270 match self {
271 Self::Fast => 2000,
272 Self::Balanced => 5000,
273 Self::LongRange => 15000,
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_spreading_factor_values() {
284 assert_eq!(SpreadingFactor::Sf7.value(), 7);
285 assert_eq!(SpreadingFactor::Sf12.value(), 12);
286 }
287
288 #[test]
289 fn test_bandwidth_hz() {
290 assert_eq!(Bandwidth::Bw125.hz(), 125_000);
291 assert_eq!(Bandwidth::Bw250.hz(), 250_000);
292 }
293
294 #[test]
295 fn test_profile_configs() {
296 let fast = LoRaConfig::from_profile(LoRaProfile::Fast, 868.0);
297 assert_eq!(fast.spreading_factor, SpreadingFactor::Sf7);
298 assert_eq!(fast.bandwidth, Bandwidth::Bw250);
299
300 let long = LoRaConfig::from_profile(LoRaProfile::LongRange, 915.0);
301 assert_eq!(long.spreading_factor, SpreadingFactor::Sf12);
302 assert_eq!(long.tx_power_dbm, 20);
303 }
304
305 #[test]
306 fn test_time_on_air() {
307 let config = LoRaConfig::eu868(LoRaProfile::Balanced);
308 let toa = config.time_on_air_ms(50);
309
310 assert!(
317 toa > 100 && toa < 600,
318 "ToA was {} ms, expected 100-600",
319 toa
320 );
321 }
322}