Skip to main content

pamoja_ffi/
lorawan_link.rs

1//! The C ABI for what keeps a LoRaWAN link running: the specification revision a device
2//! follows, the timings and counts its regional parameters recommend, the back-off that
3//! hands back what adaptive data rate tuned away once the network falls silent, and the
4//! channel list a join accept can carry.
5//!
6//! A back-off is a counter that moves with every uplink, so it crosses as an opaque
7//! handle. A channel list is sixteen bytes, so it crosses as those bytes and every call
8//! reads or writes them in place.
9
10use std::ptr;
11
12use pamoja_lorawan::adr::{Backoff, Standing};
13use pamoja_lorawan::{
14    CfList, CfListKind, Version, CFLIST_FREQUENCIES, CFLIST_LEN, CFLIST_MASK_GROUPS,
15};
16
17use crate::{read_bytes, set_last_error, PamojaStatus};
18
19/// LoRaWAN 1.0.3.
20pub const PAMOJA_LORAWAN_VERSION_1_0_3: u8 = 3;
21/// TS001-1.0.4, the LoRaWAN 1.0.4 link layer.
22pub const PAMOJA_LORAWAN_VERSION_1_0_4: u8 = 4;
23
24/// How long after an uplink the first receive window opens, RP002-1.0.5 section 3.3.
25pub const PAMOJA_LORAWAN_RECEIVE_DELAY1_US: u32 = 1_000_000;
26/// How long after an uplink the second receive window opens.
27pub const PAMOJA_LORAWAN_RECEIVE_DELAY2_US: u32 = 2_000_000;
28/// How long after a join request the first join accept window opens.
29pub const PAMOJA_LORAWAN_JOIN_ACCEPT_DELAY1_US: u32 = 5_000_000;
30/// How long after a join request the second join accept window opens.
31pub const PAMOJA_LORAWAN_JOIN_ACCEPT_DELAY2_US: u32 = 6_000_000;
32/// How far a receive window may open either side of its time, LoRaWAN 1.0.3 section 3.3.1.
33pub const PAMOJA_LORAWAN_RECEIVE_WINDOW_TOLERANCE_US: u32 = 20;
34/// The largest gap a frame counter may jump across and still be accepted.
35pub const PAMOJA_LORAWAN_MAX_FCNT_GAP: u32 = 16_384;
36/// How many unanswered uplinks before a device asks the network to answer.
37pub const PAMOJA_LORAWAN_ADR_ACK_LIMIT: u32 = 64;
38/// How many more before a device starts giving back what adaptive data rate took.
39pub const PAMOJA_LORAWAN_ADR_ACK_DELAY: u32 = 32;
40/// The shortest wait before a confirmed uplink is sent again.
41pub const PAMOJA_LORAWAN_RETRANSMIT_TIMEOUT_MIN_US: u32 = 1_000_000;
42/// The longest wait before a confirmed uplink is sent again.
43pub const PAMOJA_LORAWAN_RETRANSMIT_TIMEOUT_MAX_US: u32 = 3_000_000;
44
45/// The number of bytes a channel list occupies.
46pub const PAMOJA_LORAWAN_CFLIST_LEN: usize = 16;
47/// How many frequencies a type 0 channel list carries.
48pub const PAMOJA_LORAWAN_CFLIST_FREQUENCIES: usize = 5;
49/// How many sixteen-bit masks a type 1 channel list carries.
50pub const PAMOJA_LORAWAN_CFLIST_MASK_GROUPS: usize = 6;
51/// The CFListType byte of a list of frequencies.
52pub const PAMOJA_LORAWAN_CFLIST_TYPE_FREQUENCIES: u8 = 0;
53/// The CFListType byte of a list of channel mask groups.
54pub const PAMOJA_LORAWAN_CFLIST_TYPE_CHANNEL_MASKS: u8 = 1;
55
56/// What a back-off says to do with one uplink.
57///
58/// Each field is `1` for yes and `0` for no.
59#[repr(C)]
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub struct PamojaLorawanBackoffStep {
62    /// Set the ADRACKReq bit, asking the network to answer.
63    pub request_ack: u8,
64    /// Go back to the default transmit power before sending. Only TS001-1.0.4 takes this
65    /// step.
66    pub restore_power: u8,
67    /// Step the data rate down by the region's back-off table before sending.
68    pub lower_data_rate: u8,
69    /// Re-enable the default channels and set the repetition count back to one before
70    /// sending. Only TS001-1.0.4 takes this step.
71    pub restore_channels: u8,
72}
73
74/// An opaque handle to a device's count of how long the network has been silent.
75///
76/// Release it with [`pamoja_lorawan_backoff_free`].
77pub struct PamojaLorawanBackoff {
78    backoff: Backoff,
79}
80
81/// Reads a revision code that crossed the boundary.
82pub(crate) fn version(code: u8) -> Result<Version, PamojaStatus> {
83    match code {
84        PAMOJA_LORAWAN_VERSION_1_0_3 => Ok(Version::V1_0_3),
85        PAMOJA_LORAWAN_VERSION_1_0_4 => Ok(Version::V1_0_4),
86        other => {
87            set_last_error(format!(
88                "{other} is not a LoRaWAN version; expected {PAMOJA_LORAWAN_VERSION_1_0_3} or {PAMOJA_LORAWAN_VERSION_1_0_4}"
89            ));
90            Err(PamojaStatus::InvalidArgument)
91        }
92    }
93}
94
95/// Starts a back-off count from zero.
96///
97/// # Arguments
98///
99/// * `version` - [`PAMOJA_LORAWAN_VERSION_1_0_3`] or [`PAMOJA_LORAWAN_VERSION_1_0_4`], the
100///   revision whose steps to follow.
101/// * `limit` - how many unanswered uplinks before the device starts asking, usually
102///   [`PAMOJA_LORAWAN_ADR_ACK_LIMIT`].
103/// * `delay` - how many more before its first step, and between each step after that,
104///   usually [`PAMOJA_LORAWAN_ADR_ACK_DELAY`]. Zero is taken as one.
105/// * `out_backoff` - receives the count.
106///
107/// # Returns
108///
109/// [`PamojaStatus::Ok`] on success, with `*out_backoff` set to a handle the caller must
110/// release with [`pamoja_lorawan_backoff_free`].
111///
112/// # Errors
113///
114/// Returns [`PamojaStatus::InvalidArgument`] if `out_backoff` is null or `version` names no
115/// revision.
116///
117/// # Safety
118///
119/// `out_backoff` must point to a writable `*mut PamojaLorawanBackoff`.
120#[no_mangle]
121pub unsafe extern "C" fn pamoja_lorawan_backoff_new(
122    version: u8,
123    limit: u32,
124    delay: u32,
125    out_backoff: *mut *mut PamojaLorawanBackoff,
126) -> PamojaStatus {
127    if out_backoff.is_null() {
128        set_last_error("out_backoff must not be null".to_owned());
129        return PamojaStatus::InvalidArgument;
130    }
131    let slot = &mut *out_backoff;
132    *slot = ptr::null_mut();
133    let version = match self::version(version) {
134        Ok(version) => version,
135        Err(status) => return status,
136    };
137    *slot = Box::into_raw(Box::new(PamojaLorawanBackoff {
138        backoff: Backoff::new(version, limit, delay),
139    }));
140    PamojaStatus::Ok
141}
142
143/// Counts one new uplink, and says what to do before sending it.
144///
145/// Call it once per uplink the frame counter moves for. A repeat of the same uplink does
146/// not count.
147///
148/// # Arguments
149///
150/// * `backoff` - the count.
151/// * `default_data_rate` - `1` if the device is already at its default data rate, the
152///   slowest it uses, so there is no lower rate to step to.
153/// * `out_step` - receives what to do.
154///
155/// # Returns
156///
157/// [`PamojaStatus::Ok`] on success.
158///
159/// # Errors
160///
161/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null.
162///
163/// # Safety
164///
165/// `backoff` must be a live handle from [`pamoja_lorawan_backoff_new`], and `out_step` must
166/// point to a writable [`PamojaLorawanBackoffStep`].
167#[no_mangle]
168pub unsafe extern "C" fn pamoja_lorawan_backoff_uplink(
169    backoff: *mut PamojaLorawanBackoff,
170    default_data_rate: u8,
171    out_step: *mut PamojaLorawanBackoffStep,
172) -> PamojaStatus {
173    let (Some(backoff), false) = (backoff.as_mut(), out_step.is_null()) else {
174        set_last_error("backoff and out_step must not be null".to_owned());
175        return PamojaStatus::InvalidArgument;
176    };
177    let step = backoff.backoff.uplink(Standing {
178        default_data_rate: default_data_rate != 0,
179    });
180    *out_step = PamojaLorawanBackoffStep {
181        request_ack: u8::from(step.request_ack),
182        restore_power: u8::from(step.restore_power),
183        lower_data_rate: u8::from(step.lower_data_rate),
184        restore_channels: u8::from(step.restore_channels),
185    };
186    PamojaStatus::Ok
187}
188
189/// Counts a Class A downlink, which proves the network still hears the device and resets
190/// the count.
191///
192/// # Arguments
193///
194/// * `backoff` - the count.
195///
196/// # Returns
197///
198/// [`PamojaStatus::Ok`] on success.
199///
200/// # Errors
201///
202/// Returns [`PamojaStatus::InvalidArgument`] if `backoff` is null.
203///
204/// # Safety
205///
206/// `backoff` must be a live handle from [`pamoja_lorawan_backoff_new`], or null.
207#[no_mangle]
208pub unsafe extern "C" fn pamoja_lorawan_backoff_downlink(
209    backoff: *mut PamojaLorawanBackoff,
210) -> PamojaStatus {
211    let Some(backoff) = backoff.as_mut() else {
212        set_last_error("backoff must not be null".to_owned());
213        return PamojaStatus::InvalidArgument;
214    };
215    backoff.backoff.downlink();
216    PamojaStatus::Ok
217}
218
219/// Returns how many uplinks have gone unanswered.
220///
221/// # Returns
222///
223/// The counter, or 0 if `backoff` is null.
224///
225/// # Safety
226///
227/// `backoff` must be a live handle from [`pamoja_lorawan_backoff_new`], or null.
228#[no_mangle]
229pub unsafe extern "C" fn pamoja_lorawan_backoff_counter(
230    backoff: *const PamojaLorawanBackoff,
231) -> u32 {
232    backoff
233        .as_ref()
234        .map_or(0, |backoff| backoff.backoff.counter())
235}
236
237/// Returns the revision whose steps a count follows.
238///
239/// # Returns
240///
241/// [`PAMOJA_LORAWAN_VERSION_1_0_3`] or [`PAMOJA_LORAWAN_VERSION_1_0_4`], or 0 if `backoff`
242/// is null.
243///
244/// # Safety
245///
246/// `backoff` must be a live handle from [`pamoja_lorawan_backoff_new`], or null.
247#[no_mangle]
248pub unsafe extern "C" fn pamoja_lorawan_backoff_version(
249    backoff: *const PamojaLorawanBackoff,
250) -> u8 {
251    backoff
252        .as_ref()
253        .map_or(0, |backoff| match backoff.backoff.version() {
254            Version::V1_0_3 => PAMOJA_LORAWAN_VERSION_1_0_3,
255            Version::V1_0_4 => PAMOJA_LORAWAN_VERSION_1_0_4,
256        })
257}
258
259/// Releases a back-off handle.
260///
261/// Passing null is a no-op.
262///
263/// # Safety
264///
265/// `backoff` must be a handle from [`pamoja_lorawan_backoff_new`] that has not already been
266/// freed, or null. After this call it must not be used again.
267#[no_mangle]
268pub unsafe extern "C" fn pamoja_lorawan_backoff_free(backoff: *mut PamojaLorawanBackoff) {
269    if !backoff.is_null() {
270        drop(Box::from_raw(backoff));
271    }
272}
273
274/// Reads a channel list that crossed the boundary.
275///
276/// # Safety
277///
278/// `cflist` must point to at least `cflist_len` readable bytes when that length is
279/// non-zero.
280unsafe fn cflist(cflist: *const u8, cflist_len: usize) -> Result<CfList, PamojaStatus> {
281    let bytes = read_bytes(cflist, cflist_len)?;
282    let Ok(bytes) = <[u8; CFLIST_LEN]>::try_from(&bytes[..]) else {
283        set_last_error(format!(
284            "a channel list is exactly {CFLIST_LEN} bytes, not {cflist_len}"
285        ));
286        return Err(PamojaStatus::InvalidArgument);
287    };
288    Ok(CfList::from_bytes(bytes))
289}
290
291/// Writes a channel list's bytes back out.
292///
293/// # Safety
294///
295/// `out_cflist` must point to at least [`PAMOJA_LORAWAN_CFLIST_LEN`] writable bytes.
296unsafe fn write_cflist(list: CfList, out_cflist: *mut u8) {
297    let bytes = list.to_bytes();
298    ptr::copy_nonoverlapping(bytes.as_ptr(), out_cflist, bytes.len());
299}
300
301/// Builds a type 0 channel list from frequencies.
302///
303/// # Arguments
304///
305/// * `frequencies_hz` - the frequencies in hertz, with `0` for a slot left unused.
306/// * `len` - how many `frequencies_hz` points at, which must be
307///   [`PAMOJA_LORAWAN_CFLIST_FREQUENCIES`].
308/// * `out_cflist` - receives the sixteen bytes.
309///
310/// # Returns
311///
312/// [`PamojaStatus::Ok`] on success.
313///
314/// # Errors
315///
316/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null or `len` is not five, and
317/// [`PamojaStatus::Codec`] if a frequency is not a whole number of hundreds of hertz, lies
318/// below the 100 MHz RP002-1.0.5 reserves, or does not fit three bytes.
319///
320/// # Safety
321///
322/// `frequencies_hz` must point to `len` readable values and `out_cflist` to at least
323/// [`PAMOJA_LORAWAN_CFLIST_LEN`] writable bytes.
324#[no_mangle]
325pub unsafe extern "C" fn pamoja_lorawan_cflist_from_frequencies(
326    frequencies_hz: *const u32,
327    len: usize,
328    out_cflist: *mut u8,
329) -> PamojaStatus {
330    if frequencies_hz.is_null() || out_cflist.is_null() || len != CFLIST_FREQUENCIES {
331        set_last_error(format!(
332            "a channel list takes {CFLIST_FREQUENCIES} frequencies and room for {CFLIST_LEN} bytes"
333        ));
334        return PamojaStatus::InvalidArgument;
335    }
336    let mut slots = [0u32; CFLIST_FREQUENCIES];
337    slots.copy_from_slice(std::slice::from_raw_parts(frequencies_hz, len));
338    match CfList::frequencies(slots) {
339        Ok(list) => {
340            write_cflist(list, out_cflist);
341            PamojaStatus::Ok
342        }
343        Err(error) => {
344            set_last_error(format!(
345                "{error}: a channel list frequency is a whole number of hundreds of hertz from 100 MHz to just under 1.678 GHz"
346            ));
347            PamojaStatus::Codec
348        }
349    }
350}
351
352/// Builds a type 1 channel list from channel mask groups.
353///
354/// # Arguments
355///
356/// * `masks` - the groups, where bit *n* of group *g* enables channel `g * 16 + n`.
357/// * `len` - how many `masks` points at, which must be
358///   [`PAMOJA_LORAWAN_CFLIST_MASK_GROUPS`].
359/// * `out_cflist` - receives the sixteen bytes.
360///
361/// # Returns
362///
363/// [`PamojaStatus::Ok`] on success.
364///
365/// # Errors
366///
367/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null or `len` is not six.
368///
369/// # Safety
370///
371/// `masks` must point to `len` readable values and `out_cflist` to at least
372/// [`PAMOJA_LORAWAN_CFLIST_LEN`] writable bytes.
373#[no_mangle]
374pub unsafe extern "C" fn pamoja_lorawan_cflist_from_channel_masks(
375    masks: *const u16,
376    len: usize,
377    out_cflist: *mut u8,
378) -> PamojaStatus {
379    if masks.is_null() || out_cflist.is_null() || len != CFLIST_MASK_GROUPS {
380        set_last_error(format!(
381            "a channel list takes {CFLIST_MASK_GROUPS} mask groups and room for {CFLIST_LEN} bytes"
382        ));
383        return PamojaStatus::InvalidArgument;
384    }
385    let mut groups = [0u16; CFLIST_MASK_GROUPS];
386    groups.copy_from_slice(std::slice::from_raw_parts(masks, len));
387    write_cflist(CfList::channel_masks(groups), out_cflist);
388    PamojaStatus::Ok
389}
390
391/// Reads which form a channel list takes.
392///
393/// # Arguments
394///
395/// * `cflist` - the sixteen bytes.
396/// * `cflist_len` - their length, which must be [`PAMOJA_LORAWAN_CFLIST_LEN`].
397/// * `out_type` - receives the CFListType byte: [`PAMOJA_LORAWAN_CFLIST_TYPE_FREQUENCIES`],
398///   [`PAMOJA_LORAWAN_CFLIST_TYPE_CHANNEL_MASKS`], or a type the regional parameters
399///   reserve, which a device ignores.
400///
401/// # Returns
402///
403/// [`PamojaStatus::Ok`] on success.
404///
405/// # Errors
406///
407/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null or the list is not
408/// sixteen bytes.
409///
410/// # Safety
411///
412/// `cflist` must point to `cflist_len` readable bytes and `out_type` to a writable byte.
413#[no_mangle]
414pub unsafe extern "C" fn pamoja_lorawan_cflist_type(
415    cflist: *const u8,
416    cflist_len: usize,
417    out_type: *mut u8,
418) -> PamojaStatus {
419    if out_type.is_null() {
420        set_last_error("out_type must not be null".to_owned());
421        return PamojaStatus::InvalidArgument;
422    }
423    match self::cflist(cflist, cflist_len) {
424        Ok(list) => {
425            *out_type = list.kind().to_byte();
426            PamojaStatus::Ok
427        }
428        Err(status) => status,
429    }
430}
431
432/// Reads the frequencies out of a type 0 channel list.
433///
434/// # Arguments
435///
436/// * `cflist` - the sixteen bytes.
437/// * `cflist_len` - their length, which must be [`PAMOJA_LORAWAN_CFLIST_LEN`].
438/// * `out_frequencies_hz` - receives the frequencies in hertz, `0` for an unused slot.
439/// * `len` - room at `out_frequencies_hz`, which must be
440///   [`PAMOJA_LORAWAN_CFLIST_FREQUENCIES`].
441///
442/// # Returns
443///
444/// [`PamojaStatus::Ok`] on success.
445///
446/// # Errors
447///
448/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, a length is wrong, or
449/// the list is not type 0.
450///
451/// # Safety
452///
453/// `cflist` must point to `cflist_len` readable bytes and `out_frequencies_hz` to `len`
454/// writable values.
455#[no_mangle]
456pub unsafe extern "C" fn pamoja_lorawan_cflist_frequencies(
457    cflist: *const u8,
458    cflist_len: usize,
459    out_frequencies_hz: *mut u32,
460    len: usize,
461) -> PamojaStatus {
462    if out_frequencies_hz.is_null() || len != CFLIST_FREQUENCIES {
463        set_last_error(format!(
464            "out_frequencies_hz must have room for {CFLIST_FREQUENCIES} frequencies"
465        ));
466        return PamojaStatus::InvalidArgument;
467    }
468    let list = match self::cflist(cflist, cflist_len) {
469        Ok(list) => list,
470        Err(status) => return status,
471    };
472    let Some(frequencies) = list.frequencies_hz() else {
473        set_last_error(format!(
474            "the channel list is type {}, not a list of frequencies",
475            list.kind().to_byte()
476        ));
477        return PamojaStatus::InvalidArgument;
478    };
479    ptr::copy_nonoverlapping(frequencies.as_ptr(), out_frequencies_hz, len);
480    PamojaStatus::Ok
481}
482
483/// Reads the mask groups out of a type 1 channel list.
484///
485/// # Arguments
486///
487/// * `cflist` - the sixteen bytes.
488/// * `cflist_len` - their length, which must be [`PAMOJA_LORAWAN_CFLIST_LEN`].
489/// * `out_masks` - receives the groups.
490/// * `len` - room at `out_masks`, which must be [`PAMOJA_LORAWAN_CFLIST_MASK_GROUPS`].
491///
492/// # Returns
493///
494/// [`PamojaStatus::Ok`] on success.
495///
496/// # Errors
497///
498/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, a length is wrong, or
499/// the list is not type 1.
500///
501/// # Safety
502///
503/// `cflist` must point to `cflist_len` readable bytes and `out_masks` to `len` writable
504/// values.
505#[no_mangle]
506pub unsafe extern "C" fn pamoja_lorawan_cflist_channel_masks(
507    cflist: *const u8,
508    cflist_len: usize,
509    out_masks: *mut u16,
510    len: usize,
511) -> PamojaStatus {
512    if out_masks.is_null() || len != CFLIST_MASK_GROUPS {
513        set_last_error(format!(
514            "out_masks must have room for {CFLIST_MASK_GROUPS} mask groups"
515        ));
516        return PamojaStatus::InvalidArgument;
517    }
518    let list = match self::cflist(cflist, cflist_len) {
519        Ok(list) => list,
520        Err(status) => return status,
521    };
522    let Some(groups) = list.channel_mask_groups() else {
523        set_last_error(format!(
524            "the channel list is type {}, not a list of channel masks",
525            list.kind().to_byte()
526        ));
527        return PamojaStatus::InvalidArgument;
528    };
529    ptr::copy_nonoverlapping(groups.as_ptr(), out_masks, len);
530    PamojaStatus::Ok
531}
532
533/// Reports whether a type 1 channel list enables a channel.
534///
535/// # Arguments
536///
537/// * `cflist` - the sixteen bytes.
538/// * `cflist_len` - their length, which must be [`PAMOJA_LORAWAN_CFLIST_LEN`].
539/// * `channel` - the channel number, `group * 16 + bit`.
540/// * `out_enabled` - receives `1` if its bit is set and `0` if not.
541///
542/// # Returns
543///
544/// [`PamojaStatus::Ok`] on success.
545///
546/// # Errors
547///
548/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the list is not sixteen
549/// bytes or not type 1, or the channel is past the 96 the groups cover.
550///
551/// # Safety
552///
553/// `cflist` must point to `cflist_len` readable bytes and `out_enabled` to a writable byte.
554#[no_mangle]
555pub unsafe extern "C" fn pamoja_lorawan_cflist_enables(
556    cflist: *const u8,
557    cflist_len: usize,
558    channel: u8,
559    out_enabled: *mut u8,
560) -> PamojaStatus {
561    if out_enabled.is_null() {
562        set_last_error("out_enabled must not be null".to_owned());
563        return PamojaStatus::InvalidArgument;
564    }
565    let list = match self::cflist(cflist, cflist_len) {
566        Ok(list) => list,
567        Err(status) => return status,
568    };
569    match list.enables(channel) {
570        Some(enabled) => {
571            *out_enabled = u8::from(enabled);
572            PamojaStatus::Ok
573        }
574        None if matches!(list.kind(), CfListKind::ChannelMasks) => {
575            set_last_error(format!(
576                "channel {channel} is past the {} a channel list covers",
577                CFLIST_MASK_GROUPS * 16
578            ));
579            PamojaStatus::InvalidArgument
580        }
581        None => {
582            set_last_error(format!(
583                "the channel list is type {}, not a list of channel masks",
584                list.kind().to_byte()
585            ));
586            PamojaStatus::InvalidArgument
587        }
588    }
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    use pamoja_lorawan::defaults;
596
597    #[test]
598    fn the_constants_are_the_crate_values() {
599        assert_eq!(
600            PAMOJA_LORAWAN_RECEIVE_DELAY1_US,
601            defaults::RECEIVE_DELAY1_US
602        );
603        assert_eq!(
604            PAMOJA_LORAWAN_RECEIVE_DELAY2_US,
605            defaults::RECEIVE_DELAY2_US
606        );
607        assert_eq!(
608            PAMOJA_LORAWAN_JOIN_ACCEPT_DELAY1_US,
609            defaults::JOIN_ACCEPT_DELAY1_US
610        );
611        assert_eq!(
612            PAMOJA_LORAWAN_JOIN_ACCEPT_DELAY2_US,
613            defaults::JOIN_ACCEPT_DELAY2_US
614        );
615        assert_eq!(
616            PAMOJA_LORAWAN_RECEIVE_WINDOW_TOLERANCE_US,
617            defaults::RECEIVE_WINDOW_TOLERANCE_US
618        );
619        assert_eq!(PAMOJA_LORAWAN_MAX_FCNT_GAP, defaults::MAX_FCNT_GAP);
620        assert_eq!(PAMOJA_LORAWAN_ADR_ACK_LIMIT, defaults::ADR_ACK_LIMIT);
621        assert_eq!(PAMOJA_LORAWAN_ADR_ACK_DELAY, defaults::ADR_ACK_DELAY);
622        assert_eq!(
623            PAMOJA_LORAWAN_RETRANSMIT_TIMEOUT_MIN_US,
624            defaults::RETRANSMIT_TIMEOUT_MIN_US
625        );
626        assert_eq!(
627            PAMOJA_LORAWAN_RETRANSMIT_TIMEOUT_MAX_US,
628            defaults::RETRANSMIT_TIMEOUT_MAX_US
629        );
630        assert_eq!(PAMOJA_LORAWAN_CFLIST_LEN, CFLIST_LEN);
631        assert_eq!(PAMOJA_LORAWAN_CFLIST_FREQUENCIES, CFLIST_FREQUENCIES);
632        assert_eq!(PAMOJA_LORAWAN_CFLIST_MASK_GROUPS, CFLIST_MASK_GROUPS);
633    }
634
635    #[test]
636    fn a_back_off_follows_table_9_across_the_boundary() {
637        unsafe {
638            let mut backoff = ptr::null_mut();
639            assert_eq!(
640                pamoja_lorawan_backoff_new(PAMOJA_LORAWAN_VERSION_1_0_4, 64, 32, &mut backoff),
641                PamojaStatus::Ok
642            );
643            assert_eq!(
644                pamoja_lorawan_backoff_version(backoff),
645                PAMOJA_LORAWAN_VERSION_1_0_4
646            );
647
648            // TS001-1.0.4 table 9 from DR2: power at 96, a rate at 128 and 160, channels
649            // at 192 once the default rate is reached.
650            let mut data_rate = 2;
651            let mut steps = Vec::new();
652            let mut step = PamojaLorawanBackoffStep {
653                request_ack: 0,
654                restore_power: 0,
655                lower_data_rate: 0,
656                restore_channels: 0,
657            };
658            for sent in 1..=200 {
659                assert_eq!(
660                    pamoja_lorawan_backoff_uplink(backoff, u8::from(data_rate == 0), &mut step),
661                    PamojaStatus::Ok
662                );
663                if sent == 64 {
664                    assert_eq!(step.request_ack, 1, "the sixty-fourth asks");
665                }
666                if step.restore_power == 1 {
667                    steps.push((sent, "power"));
668                }
669                if step.lower_data_rate == 1 {
670                    data_rate -= 1;
671                    steps.push((sent, "rate"));
672                }
673                if step.restore_channels == 1 {
674                    steps.push((sent, "channels"));
675                }
676            }
677            assert_eq!(
678                steps,
679                [
680                    (96, "power"),
681                    (128, "rate"),
682                    (160, "rate"),
683                    (192, "channels")
684                ]
685            );
686            assert_eq!(pamoja_lorawan_backoff_counter(backoff), 200);
687
688            assert_eq!(pamoja_lorawan_backoff_downlink(backoff), PamojaStatus::Ok);
689            assert_eq!(pamoja_lorawan_backoff_counter(backoff), 0);
690            pamoja_lorawan_backoff_free(backoff);
691
692            assert_eq!(
693                pamoja_lorawan_backoff_new(2, 64, 32, &mut backoff),
694                PamojaStatus::InvalidArgument
695            );
696            assert!(backoff.is_null());
697            assert_eq!(
698                pamoja_lorawan_backoff_uplink(ptr::null_mut(), 0, &mut step),
699                PamojaStatus::InvalidArgument
700            );
701        }
702    }
703
704    #[test]
705    fn a_channel_list_crosses_in_both_forms() {
706        unsafe {
707            // The published EU868 join accept of lora-packet issue 10 carries these five.
708            let frequencies = [
709                867_100_000,
710                867_300_000,
711                867_500_000,
712                867_700_000,
713                867_900_000,
714            ];
715            let mut bytes = [0xAAu8; 16];
716            assert_eq!(
717                pamoja_lorawan_cflist_from_frequencies(frequencies.as_ptr(), 5, bytes.as_mut_ptr()),
718                PamojaStatus::Ok
719            );
720            assert_eq!(
721                bytes,
722                [
723                    0x18, 0x4F, 0x84, 0xE8, 0x56, 0x84, 0xB8, 0x5E, 0x84, 0x88, 0x66, 0x84, 0x58,
724                    0x6E, 0x84, 0x00
725                ]
726            );
727            let mut kind = 9;
728            assert_eq!(
729                pamoja_lorawan_cflist_type(bytes.as_ptr(), 16, &mut kind),
730                PamojaStatus::Ok
731            );
732            assert_eq!(kind, PAMOJA_LORAWAN_CFLIST_TYPE_FREQUENCIES);
733            let mut read = [0u32; 5];
734            assert_eq!(
735                pamoja_lorawan_cflist_frequencies(bytes.as_ptr(), 16, read.as_mut_ptr(), 5),
736                PamojaStatus::Ok
737            );
738            assert_eq!(read, frequencies);
739            let mut masks = [0u16; 6];
740            assert_eq!(
741                pamoja_lorawan_cflist_channel_masks(bytes.as_ptr(), 16, masks.as_mut_ptr(), 6),
742                PamojaStatus::InvalidArgument
743            );
744            let mut enabled = 9;
745            assert_eq!(
746                pamoja_lorawan_cflist_enables(bytes.as_ptr(), 16, 0, &mut enabled),
747                PamojaStatus::InvalidArgument
748            );
749
750            for refused in [867_100_050, 99_999_900, 1_677_721_600] {
751                let one = [refused, 0, 0, 0, 0];
752                assert_eq!(
753                    pamoja_lorawan_cflist_from_frequencies(one.as_ptr(), 5, bytes.as_mut_ptr()),
754                    PamojaStatus::Codec,
755                    "{refused} Hz"
756                );
757            }
758
759            // A US915 network on its second sub-band, channels 8 to 15 and 65.
760            let groups = [0xFF00, 0, 0, 0, 0x0002, 0];
761            assert_eq!(
762                pamoja_lorawan_cflist_from_channel_masks(groups.as_ptr(), 6, bytes.as_mut_ptr()),
763                PamojaStatus::Ok
764            );
765            assert_eq!(
766                bytes,
767                [0x00, 0xFF, 0, 0, 0, 0, 0, 0, 0x02, 0x00, 0, 0, 0, 0, 0, 0x01]
768            );
769            assert_eq!(
770                pamoja_lorawan_cflist_channel_masks(bytes.as_ptr(), 16, masks.as_mut_ptr(), 6),
771                PamojaStatus::Ok
772            );
773            assert_eq!(masks, groups);
774            for (channel, want) in [(7, 0), (8, 1), (15, 1), (64, 0), (65, 1)] {
775                assert_eq!(
776                    pamoja_lorawan_cflist_enables(bytes.as_ptr(), 16, channel, &mut enabled),
777                    PamojaStatus::Ok
778                );
779                assert_eq!(enabled, want, "channel {channel}");
780            }
781            assert_eq!(
782                pamoja_lorawan_cflist_enables(bytes.as_ptr(), 16, 96, &mut enabled),
783                PamojaStatus::InvalidArgument
784            );
785            assert_eq!(
786                pamoja_lorawan_cflist_type(bytes.as_ptr(), 15, &mut kind),
787                PamojaStatus::InvalidArgument
788            );
789        }
790    }
791}