pamoja_ffi/lora.rs
1//! The C ABI for LoRa link math.
2//!
3//! These functions wrap [`pamoja_lora`] for callers that reach the SDK through the
4//! flat C boundary: the time a transmission spends on air, and the silence a
5//! duty-cycle limit then forces. Both are what a long-range node needs to stay
6//! inside its regional budget, and both are pure arithmetic.
7//!
8//! A link is only scalars, so it crosses by value as [`PamojaLoraLink`] rather
9//! than as a handle, which keeps the whole capability free of allocation.
10
11use pamoja_lora::LinkSettings;
12
13/// The radio settings of a LoRa link.
14///
15/// Build one with [`pamoja_lora_link_default`] and adjust the fields that differ
16/// from the defaults. Values outside the ranges LoRa defines are clamped when the
17/// link is used: the spreading factor to 5-12 and the coding-rate denominator to
18/// 5-8.
19#[repr(C)]
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct PamojaLoraLink {
22 /// The channel bandwidth in hertz, such as `125000`.
23 pub bandwidth_hz: u32,
24 /// The preamble length in symbols; the LoRa default is 8.
25 pub preamble_symbols: u16,
26 /// The spreading factor, 5 (fastest) to 12 (longest range).
27 pub spreading_factor: u8,
28 /// The coding-rate denominator, 5 to 8, for 4/5 to 4/8.
29 pub coding_rate_denominator: u8,
30 /// `1` for an explicit header, `0` to omit the header symbols.
31 pub explicit_header: u8,
32 /// `1` to append the frame CRC, `0` to leave it off.
33 pub crc: u8,
34}
35
36/// Returns the settings for a spreading factor and bandwidth, with LoRa defaults.
37///
38/// The defaults are coding rate 4/5, an eight-symbol preamble, an explicit header,
39/// and CRC on, which is a typical uplink.
40///
41/// # Arguments
42///
43/// * `spreading_factor` - the spreading factor, clamped to 5-12.
44/// * `bandwidth_hz` - the channel bandwidth in hertz.
45///
46/// # Returns
47///
48/// The link settings, with the spreading factor already clamped.
49#[no_mangle]
50pub extern "C" fn pamoja_lora_link_default(
51 spreading_factor: u8,
52 bandwidth_hz: u32,
53) -> PamojaLoraLink {
54 let settings = LinkSettings::new(spreading_factor, bandwidth_hz);
55 PamojaLoraLink {
56 bandwidth_hz: settings.bandwidth_hz(),
57 preamble_symbols: 8,
58 spreading_factor: settings.spreading_factor(),
59 coding_rate_denominator: 5,
60 explicit_header: 1,
61 crc: 1,
62 }
63}
64
65/// Returns the duration of one symbol on a link, in microseconds.
66///
67/// # Arguments
68///
69/// * `link` - the link settings.
70///
71/// # Returns
72///
73/// The symbol time in microseconds.
74#[no_mangle]
75pub extern "C" fn pamoja_lora_symbol_time_us(link: PamojaLoraLink) -> u64 {
76 settings(link).symbol_time_us()
77}
78
79/// Returns the time on air of a payload, in microseconds.
80///
81/// This is the channel occupancy a transmission costs: how long the radio holds
82/// the air, which sets both the duty-cycle budget and most of the energy the
83/// transmission spends.
84///
85/// # Arguments
86///
87/// * `link` - the link settings.
88/// * `payload_len` - the payload length in bytes.
89///
90/// # Returns
91///
92/// The time on air in microseconds.
93#[no_mangle]
94pub extern "C" fn pamoja_lora_airtime_us(link: PamojaLoraLink, payload_len: usize) -> u64 {
95 settings(link).airtime_us(payload_len)
96}
97
98/// Returns the minimum silence after a transmission to honor a duty-cycle limit.
99///
100/// # Arguments
101///
102/// * `link` - the link settings.
103/// * `payload_len` - the payload length in bytes.
104/// * `duty_cycle_permille` - the limit in parts per thousand, so `10` is 1%.
105///
106/// # Returns
107///
108/// The required off time in microseconds, or `UINT64_MAX` if the limit is zero,
109/// which forbids transmitting at all.
110#[no_mangle]
111pub extern "C" fn pamoja_lora_min_off_time_us(
112 link: PamojaLoraLink,
113 payload_len: usize,
114 duty_cycle_permille: u32,
115) -> u64 {
116 settings(link).min_off_time_us(payload_len, duty_cycle_permille)
117}
118
119/// Rebuilds the Rust link settings from the fields that crossed the boundary.
120///
121/// # Arguments
122///
123/// * `link` - the settings as the caller supplied them.
124///
125/// # Returns
126///
127/// The equivalent [`LinkSettings`], with every value clamped to its LoRa range.
128fn settings(link: PamojaLoraLink) -> LinkSettings {
129 let mut settings = LinkSettings::new(link.spreading_factor, link.bandwidth_hz)
130 .with_coding_rate(link.coding_rate_denominator)
131 .with_preamble(link.preamble_symbols);
132 if link.explicit_header == 0 {
133 settings = settings.implicit_header();
134 }
135 if link.crc == 0 {
136 settings = settings.without_crc();
137 }
138 settings
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn the_defaults_are_a_typical_uplink() {
147 let link = pamoja_lora_link_default(12, 125_000);
148 assert_eq!(link.spreading_factor, 12);
149 assert_eq!(link.bandwidth_hz, 125_000);
150 assert_eq!(link.coding_rate_denominator, 5);
151 assert_eq!(link.preamble_symbols, 8);
152 assert_eq!(link.explicit_header, 1);
153 assert_eq!(link.crc, 1);
154 }
155
156 #[test]
157 fn a_spreading_factor_beyond_lora_is_clamped() {
158 assert_eq!(pamoja_lora_link_default(15, 125_000).spreading_factor, 12);
159 assert_eq!(pamoja_lora_link_default(2, 125_000).spreading_factor, 5);
160 }
161
162 #[test]
163 fn airtime_matches_the_rust_crate() {
164 let link = pamoja_lora_link_default(12, 125_000);
165 assert_eq!(
166 pamoja_lora_airtime_us(link, 10),
167 LinkSettings::new(12, 125_000).airtime_us(10)
168 );
169 }
170
171 #[test]
172 fn a_one_percent_duty_cycle_costs_ninety_nine_times_the_airtime() {
173 let link = pamoja_lora_link_default(12, 125_000);
174 let airtime = pamoja_lora_airtime_us(link, 20);
175 assert_eq!(pamoja_lora_min_off_time_us(link, 20, 10), airtime * 99);
176 }
177
178 #[test]
179 fn a_zero_duty_cycle_forbids_transmitting() {
180 let link = pamoja_lora_link_default(7, 125_000);
181 assert_eq!(pamoja_lora_min_off_time_us(link, 20, 0), u64::MAX);
182 }
183}