Skip to main content

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}