umsh_ulcp/airtime.rs
1//! LoRa airtime estimation.
2//!
3//! Shared by the host client (worst-case `t_frame_ms` for the MAC
4//! scheduler) and the ULCP session (duty-cycle accounting for
5//! `PROP_PHY_DUTY_NOW`), so both sides account airtime identically.
6
7/// Conservative LoRa on-air time estimate in milliseconds.
8///
9/// Standard LoRa airtime formula with explicit header, CRC on, and
10/// auto-LDRO; the preamble of 8 + 4.25 symbols is approximated as 12.
11/// Numeric twin of `umsh_radio_loraphy::airtime_ms` (which takes
12/// `lora-modulation` enums), generalized over the coding-rate
13/// denominator (`5` for CR 4/5 through `8` for CR 4/8).
14///
15/// Out-of-range inputs are clamped rather than rejected: this is an
16/// estimate for scheduling and duty accounting, not a validator.
17pub fn lora_airtime_ms(sf: u8, bw_hz: u32, cr_denom: u8, payload_bytes: usize) -> u32 {
18 let sf = u32::from(sf.clamp(5, 12));
19 let t_sym_us = (1u64 << sf) * 1_000_000 / u64::from(bw_hz.max(1));
20
21 // LDRO required when t_sym > 16 ms.
22 let ldro: i64 = if t_sym_us > 16_000 { 1 } else { 0 };
23
24 let sf = i64::from(sf);
25 let payload = payload_bytes as i64;
26 let num = (8 * payload - 4 * sf + 44 + 20 - 16 * ldro).max(0);
27 let denom = 4 * (sf - 2 * ldro);
28 let ceil = (num + denom - 1) / denom;
29 let n_payload_sym = 8 + ceil * i64::from(cr_denom.clamp(5, 8));
30
31 let total_sym = 12 + n_payload_sym as u64;
32 ((total_sym * t_sym_us) / 1_000) as u32
33}
34
35#[cfg(test)]
36mod tests {
37 use super::*;
38
39 #[test]
40 fn plausible_magnitudes() {
41 // ~255-byte frame at SF11/BW250 is on the order of seconds.
42 let slow = lora_airtime_ms(11, 250_000, 5, 255);
43 assert!((500..5_000).contains(&slow), "airtime {slow}");
44 // Faster settings give shorter airtime.
45 assert!(lora_airtime_ms(7, 250_000, 5, 255) < slow);
46 // Higher coding overhead gives longer airtime.
47 assert!(lora_airtime_ms(7, 125_000, 8, 100) > lora_airtime_ms(7, 125_000, 5, 100));
48 }
49
50 #[test]
51 fn short_frame_nonzero() {
52 assert!(lora_airtime_ms(7, 500_000, 5, 1) > 0);
53 }
54}