Skip to main content

pamoja_ffi/
lorawan_relay.rs

1//! The C ABI for a LoRaWAN relay, TS011-1.0.1: the wake-on-radio frames an end device and a
2//! relay exchange, the uplinks a relay forwards, and the timing that keeps a wake-on-radio
3//! preamble short.
4//!
5//! Everything here is a pure function of its arguments. Keys cross as a
6//! [`PamojaLorawanWorKeys`] of two sixteen-byte arrays, carriers and states as small structs,
7//! and frames as fixed-length arrays written in place, except a forwarded uplink, whose
8//! length follows the end device's frame and crosses as a buffer.
9
10use std::ptr;
11
12use pamoja_lorawan::mac::relay_second_channel;
13use pamoja_lorawan::relay::{
14    self, open_wor_ack, t_offset_ms, unsynchronized_preamble_symbols, wor_ack, wor_join_request,
15    wor_uplink, CadPeriodicity, CadToRx, Carrier, Forward, ForwardedUplink, StateSync,
16    Synchronization, UplinkMetadata, Wor, WorChannel, WorKeys, XtalAccuracy,
17};
18
19use crate::lora_region::PamojaLoraRelayChannel;
20use crate::lorawan::{failed, PamojaLorawanSession, PAMOJA_LORAWAN_KEY_LEN};
21use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
22
23/// The port every message between a relay and its network uses.
24pub const PAMOJA_LORAWAN_LA_FPORT_RELAY: u8 = 226;
25/// How many end devices a relay verifies wake-on-radio frames for.
26pub const PAMOJA_LORAWAN_TRUSTED_ED_NUMBER: usize = 16;
27/// How many WOR frames go without an acknowledgment before the uplink goes anyway.
28pub const PAMOJA_LORAWAN_WOR_ATTEMPTS_WO_ACK: u8 = 8;
29/// The gap between a WOR frame, or its acknowledgment, and the LoRaWAN frame after it.
30pub const PAMOJA_LORAWAN_WOR_DATA_DELAY_US: u32 = 50_000;
31/// The gap between a WOR frame and its acknowledgment.
32pub const PAMOJA_LORAWAN_WOR_ACK_DELAY_US: u32 = 50_000;
33/// The gap between a relay hearing an uplink and forwarding it.
34pub const PAMOJA_LORAWAN_RELAY_FWD_DELAY_US: u32 = 50_000;
35/// How long after an uplink an end device's RXR window opens at the latest.
36pub const PAMOJA_LORAWAN_RXR_DELAY_US: u32 = 18_000_000;
37/// The length of a WOR frame ahead of a join request.
38pub const PAMOJA_LORAWAN_WOR_JOIN_REQUEST_LEN: usize = 5;
39/// The length of a WOR frame ahead of a Class A uplink.
40pub const PAMOJA_LORAWAN_WOR_UPLINK_LEN: usize = 15;
41/// The length of a WOR ACK.
42pub const PAMOJA_LORAWAN_WOR_ACK_LEN: usize = 7;
43/// The bytes a forwarded uplink adds in front of the end device's frame.
44pub const PAMOJA_LORAWAN_FORWARD_OVERHEAD: usize = 6;
45/// The shortest WOR preamble, in symbols.
46pub const PAMOJA_LORAWAN_MIN_WOR_PREAMBLE_SYMBOLS: u16 = 8;
47
48// The header carries these as literals, because cbindgen drops a constant whose value
49// names another crate's. These hold them to what those crates say.
50const _: () = assert!(PAMOJA_LORAWAN_LA_FPORT_RELAY == relay::LA_FPORT_RELAY);
51const _: () = assert!(PAMOJA_LORAWAN_TRUSTED_ED_NUMBER == relay::TRUSTED_ED_NUMBER);
52const _: () = assert!(PAMOJA_LORAWAN_WOR_ATTEMPTS_WO_ACK == relay::WOR_ATTEMPTS_WO_ACK);
53const _: () = assert!(PAMOJA_LORAWAN_WOR_DATA_DELAY_US == relay::WOR_DATA_DELAY_US);
54const _: () = assert!(PAMOJA_LORAWAN_WOR_ACK_DELAY_US == relay::WOR_ACK_DELAY_US);
55const _: () = assert!(PAMOJA_LORAWAN_RELAY_FWD_DELAY_US == relay::RELAY_FWD_DELAY_US);
56const _: () = assert!(PAMOJA_LORAWAN_RXR_DELAY_US == relay::RXR_DELAY_US);
57const _: () = assert!(PAMOJA_LORAWAN_WOR_JOIN_REQUEST_LEN == relay::WOR_JOIN_REQUEST_LEN);
58const _: () = assert!(PAMOJA_LORAWAN_WOR_UPLINK_LEN == relay::WOR_UPLINK_LEN);
59const _: () = assert!(PAMOJA_LORAWAN_WOR_ACK_LEN == relay::WOR_ACK_LEN);
60const _: () = assert!(PAMOJA_LORAWAN_FORWARD_OVERHEAD == relay::FORWARD_OVERHEAD);
61const _: () = assert!(PAMOJA_LORAWAN_MIN_WOR_PREAMBLE_SYMBOLS == relay::MIN_WOR_PREAMBLE_SYMBOLS);
62
63/// A WOR frame ahead of a join request.
64pub const PAMOJA_LORAWAN_WOR_JOIN_REQUEST: u8 = 0;
65/// A WOR frame ahead of a Class A uplink.
66pub const PAMOJA_LORAWAN_WOR_UPLINK: u8 = 1;
67
68/// The integrity and encryption keys of one end device's wake-on-radio frames.
69#[repr(C)]
70#[derive(Clone, Copy, PartialEq, Eq)]
71pub struct PamojaLorawanWorKeys {
72    /// `WorSIntKey`.
73    pub integrity: [u8; 16],
74    /// `WorSEncKey`.
75    pub encryption: [u8; 16],
76}
77
78/// Where a frame goes and how fast.
79#[repr(C)]
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub struct PamojaLorawanCarrier {
82    /// The frequency in hertz.
83    pub frequency_hz: u32,
84    /// The data rate.
85    pub data_rate: u8,
86}
87
88/// A wake-on-radio frame, as a relay reads it.
89#[repr(C)]
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub struct PamojaLorawanWor {
92    /// [`PAMOJA_LORAWAN_WOR_JOIN_REQUEST`] or [`PAMOJA_LORAWAN_WOR_UPLINK`].
93    pub kind: u8,
94    /// For a join request, where and how fast it follows; zero for an uplink, whose carrier
95    /// is sealed until [`pamoja_lorawan_relay_wor_open`] reads it.
96    pub uplink: PamojaLorawanCarrier,
97    /// For an uplink, the address it names.
98    pub dev_addr: u32,
99    /// For an uplink, the low sixteen bits of its frame counter.
100    pub wfcnt: u16,
101}
102
103/// What a relay tells an end device about itself in a WOR ACK, as TS011-1.0.1 table 14
104/// codes each field.
105#[repr(C)]
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub struct PamojaLorawanStateSync {
108    /// Symbols from detecting activity to receiving: 0 for 2, 1 for 4, 2 for 6, 3 for 8.
109    pub cad_to_rx: u8,
110    /// Whether the relay forwards: 0 yes, 1 and 2 retry in 30 or 60 minutes, 3 disabled.
111    pub forward: u8,
112    /// The data rate the relay forwards at.
113    pub relay_data_rate: u8,
114    /// Crystal accuracy: 0 to 3 for 10 to 40 parts per million.
115    pub xtal_accuracy: u8,
116    /// How often the relay scans: 0 to 5 for 1000, 500, 250, 100, 50 and 20 milliseconds.
117    pub cad_periodicity: u8,
118    /// Milliseconds from the start of the scan to the end of the WOR preamble.
119    pub t_offset_ms: u16,
120}
121
122/// What a relay heard of an uplink it forwards.
123#[repr(C)]
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub struct PamojaLorawanUplinkMetadata {
126    /// The WOR channel: 0 for the default, 1 for the second.
127    pub wor_channel: u8,
128    /// The uplink's signal strength in dBm.
129    pub rssi_dbm: i16,
130    /// Its signal-to-noise ratio in dB.
131    pub snr_db: i8,
132    /// The data rate it arrived at.
133    pub data_rate: u8,
134}
135
136/// What an end device knows of a relay's scans once a WOR ACK has arrived.
137#[repr(C)]
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub struct PamojaLorawanSynchronization {
140    /// When the relay scanned, in the device's microseconds.
141    pub reference_us: u64,
142    /// How often it scans, coded as in [`PamojaLorawanStateSync`].
143    pub cad_periodicity: u8,
144    /// Its crystal accuracy, coded.
145    pub relay_xtal: u8,
146    /// Its time to start receiving, coded.
147    pub cad_to_rx: u8,
148}
149
150/// When a synchronized end device's next WOR frame goes out.
151#[repr(C)]
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub struct PamojaLorawanWorSlot {
154    /// When to start sending, in microseconds.
155    pub start_us: u64,
156    /// The preamble length in symbols.
157    pub preamble_symbols: u16,
158}
159
160fn keys_in(keys: &PamojaLorawanWorKeys) -> WorKeys {
161    WorKeys::new(keys.integrity, keys.encryption)
162}
163
164fn keys_out(keys: &WorKeys) -> PamojaLorawanWorKeys {
165    PamojaLorawanWorKeys {
166        integrity: *keys.integrity(),
167        encryption: *keys.encryption(),
168    }
169}
170
171fn carrier_in(carrier: &PamojaLorawanCarrier) -> Carrier {
172    Carrier::new(carrier.frequency_hz, carrier.data_rate)
173}
174
175fn carrier_out(carrier: Carrier) -> PamojaLorawanCarrier {
176    PamojaLorawanCarrier {
177        frequency_hz: carrier.frequency_hz,
178        data_rate: carrier.data_rate,
179    }
180}
181
182fn two_bits(value: u8, what: &str) -> Result<u8, PamojaStatus> {
183    if value > 3 {
184        set_last_error(format!("{what} is coded in two bits, and {value} is not"));
185        return Err(PamojaStatus::InvalidArgument);
186    }
187    Ok(value)
188}
189
190fn periodicity(code: u8) -> Result<CadPeriodicity, PamojaStatus> {
191    CadPeriodicity::from_code(code).ok_or_else(|| {
192        set_last_error(format!(
193            "{code} is not a scan periodicity, which runs 0 to 5"
194        ));
195        PamojaStatus::InvalidArgument
196    })
197}
198
199fn state_in(state: &PamojaLorawanStateSync) -> Result<StateSync, PamojaStatus> {
200    Ok(StateSync {
201        cad_to_rx: CadToRx::from_code(two_bits(state.cad_to_rx, "CadToRx")?),
202        forward: Forward::from_code(two_bits(state.forward, "Forward")?),
203        relay_data_rate: state.relay_data_rate,
204        xtal_accuracy: XtalAccuracy::from_code(two_bits(state.xtal_accuracy, "XTALAccuracy")?),
205        cad_periodicity: periodicity(state.cad_periodicity)?,
206        t_offset_ms: state.t_offset_ms,
207    })
208}
209
210fn state_out(state: StateSync) -> PamojaLorawanStateSync {
211    PamojaLorawanStateSync {
212        cad_to_rx: state.cad_to_rx.code(),
213        forward: state.forward.code(),
214        relay_data_rate: state.relay_data_rate,
215        xtal_accuracy: state.xtal_accuracy.code(),
216        cad_periodicity: state.cad_periodicity.code(),
217        t_offset_ms: state.t_offset_ms,
218    }
219}
220
221fn null(what: &str) -> PamojaStatus {
222    set_last_error(format!("{what} must not be null"));
223    PamojaStatus::InvalidArgument
224}
225
226/// Derives an end device's root relay session key from its network session key,
227/// TS011-1.0.1 section 4.4.
228///
229/// # Arguments
230///
231/// * `network_key` - the network session key, `NwkSKey` under LoRaWAN 1.0.x.
232/// * `network_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
233/// * `out_key` - receives the sixteen-byte root key.
234///
235/// # Returns
236///
237/// [`PamojaStatus::Ok`] on success.
238///
239/// # Errors
240///
241/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer or a key of the wrong length.
242///
243/// # Safety
244///
245/// `network_key` must point to `network_key_len` readable bytes and `out_key` to sixteen
246/// writable ones.
247#[no_mangle]
248pub unsafe extern "C" fn pamoja_lorawan_relay_root_wor_s_key(
249    network_key: *const u8,
250    network_key_len: usize,
251    out_key: *mut u8,
252) -> PamojaStatus {
253    if out_key.is_null() {
254        return null("out_key");
255    }
256    let key = match sixteen(network_key, network_key_len, "the network key") {
257        Ok(key) => key,
258        Err(status) => return status,
259    };
260    let root = relay::root_wor_s_key(&key);
261    ptr::copy_nonoverlapping(root.as_ptr(), out_key, root.len());
262    PamojaStatus::Ok
263}
264
265/// Derives an end device's wake-on-radio keys from its root relay session key, TS011-1.0.1
266/// section 4.5.
267///
268/// # Arguments
269///
270/// * `root_key` - the root relay session key.
271/// * `root_key_len` - its length, which must be [`PAMOJA_LORAWAN_KEY_LEN`].
272/// * `dev_addr` - the device's address.
273/// * `out_keys` - receives the keys.
274///
275/// # Returns
276///
277/// [`PamojaStatus::Ok`] on success.
278///
279/// # Errors
280///
281/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer or a key of the wrong length.
282///
283/// # Safety
284///
285/// `root_key` must point to `root_key_len` readable bytes and `out_keys` must be writable.
286#[no_mangle]
287pub unsafe extern "C" fn pamoja_lorawan_relay_wor_keys(
288    root_key: *const u8,
289    root_key_len: usize,
290    dev_addr: u32,
291    out_keys: *mut PamojaLorawanWorKeys,
292) -> PamojaStatus {
293    if out_keys.is_null() {
294        return null("out_keys");
295    }
296    let root = match sixteen(root_key, root_key_len, "the root key") {
297        Ok(root) => root,
298        Err(status) => return status,
299    };
300    *out_keys = keys_out(&WorKeys::derive(&root, dev_addr));
301    PamojaStatus::Ok
302}
303
304/// Returns the root relay session key of a session's device.
305///
306/// # Arguments
307///
308/// * `session` - the session.
309/// * `out_key` - receives the sixteen-byte key.
310///
311/// # Returns
312///
313/// [`PamojaStatus::Ok`] on success.
314///
315/// # Errors
316///
317/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
318///
319/// # Safety
320///
321/// `session` must be a live handle and `out_key` must point to sixteen writable bytes.
322#[no_mangle]
323pub unsafe extern "C" fn pamoja_lorawan_session_root_wor_s_key(
324    session: *const PamojaLorawanSession,
325    out_key: *mut u8,
326) -> PamojaStatus {
327    let (Some(session), false) = (session.as_ref(), out_key.is_null()) else {
328        return null("session and out_key");
329    };
330    let root = session.session.root_wor_s_key();
331    ptr::copy_nonoverlapping(root.as_ptr(), out_key, root.len());
332    PamojaStatus::Ok
333}
334
335/// Returns the wake-on-radio keys of a session's device.
336///
337/// # Arguments
338///
339/// * `session` - the session.
340/// * `out_keys` - receives the keys.
341///
342/// # Returns
343///
344/// [`PamojaStatus::Ok`] on success.
345///
346/// # Errors
347///
348/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
349///
350/// # Safety
351///
352/// `session` must be a live handle and `out_keys` must be writable.
353#[no_mangle]
354pub unsafe extern "C" fn pamoja_lorawan_session_wor_keys(
355    session: *const PamojaLorawanSession,
356    out_keys: *mut PamojaLorawanWorKeys,
357) -> PamojaStatus {
358    let (Some(session), false) = (session.as_ref(), out_keys.is_null()) else {
359        return null("session and out_keys");
360    };
361    *out_keys = keys_out(&session.session.wor_keys());
362    PamojaStatus::Ok
363}
364
365/// Builds the WOR frame ahead of a join request, TS011-1.0.1 section 5.3.1.
366///
367/// # Arguments
368///
369/// * `uplink` - where and how fast the join request follows.
370/// * `out_frame` - receives the [`PAMOJA_LORAWAN_WOR_JOIN_REQUEST_LEN`] bytes.
371///
372/// # Returns
373///
374/// [`PamojaStatus::Ok`] on success.
375///
376/// # Errors
377///
378/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, and [`PamojaStatus::Codec`]
379/// for a carrier the fields cannot hold.
380///
381/// # Safety
382///
383/// `uplink` must be readable and `out_frame` must point to
384/// [`PAMOJA_LORAWAN_WOR_JOIN_REQUEST_LEN`] writable bytes.
385#[no_mangle]
386pub unsafe extern "C" fn pamoja_lorawan_relay_wor_join_request(
387    uplink: *const PamojaLorawanCarrier,
388    out_frame: *mut u8,
389) -> PamojaStatus {
390    let (Some(uplink), false) = (uplink.as_ref(), out_frame.is_null()) else {
391        return null("uplink and out_frame");
392    };
393    match wor_join_request(carrier_in(uplink)) {
394        Ok(frame) => {
395            ptr::copy_nonoverlapping(frame.as_ptr(), out_frame, frame.len());
396            PamojaStatus::Ok
397        }
398        Err(error) => failed(error),
399    }
400}
401
402/// Builds the WOR frame ahead of a Class A uplink, TS011-1.0.1 section 5.3.2.
403///
404/// # Arguments
405///
406/// * `keys` - the device's keys.
407/// * `dev_addr` - its address.
408/// * `wfcnt` - the WOR frame counter.
409/// * `uplink` - where and how fast the uplink follows.
410/// * `wor` - the carrier this WOR frame goes out on.
411/// * `out_frame` - receives the [`PAMOJA_LORAWAN_WOR_UPLINK_LEN`] bytes.
412///
413/// # Returns
414///
415/// [`PamojaStatus::Ok`] on success.
416///
417/// # Errors
418///
419/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, and [`PamojaStatus::Codec`]
420/// for a carrier the fields cannot hold.
421///
422/// # Safety
423///
424/// The pointers must be readable and `out_frame` must point to
425/// [`PAMOJA_LORAWAN_WOR_UPLINK_LEN`] writable bytes.
426#[no_mangle]
427pub unsafe extern "C" fn pamoja_lorawan_relay_wor_uplink(
428    keys: *const PamojaLorawanWorKeys,
429    dev_addr: u32,
430    wfcnt: u32,
431    uplink: *const PamojaLorawanCarrier,
432    wor: *const PamojaLorawanCarrier,
433    out_frame: *mut u8,
434) -> PamojaStatus {
435    let (Some(keys), Some(uplink), Some(wor), false) = (
436        keys.as_ref(),
437        uplink.as_ref(),
438        wor.as_ref(),
439        out_frame.is_null(),
440    ) else {
441        return null("keys, uplink, wor and out_frame");
442    };
443    match wor_uplink(
444        &keys_in(keys),
445        dev_addr,
446        wfcnt,
447        carrier_in(uplink),
448        carrier_in(wor),
449    ) {
450        Ok(frame) => {
451            ptr::copy_nonoverlapping(frame.as_ptr(), out_frame, frame.len());
452            PamojaStatus::Ok
453        }
454        Err(error) => failed(error),
455    }
456}
457
458/// Reads a WOR frame.
459///
460/// # Arguments
461///
462/// * `frame` - the bytes a relay received.
463/// * `frame_len` - their length.
464/// * `out_wor` - receives what the frame is.
465///
466/// # Returns
467///
468/// [`PamojaStatus::Ok`] on success.
469///
470/// # Errors
471///
472/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, and [`PamojaStatus::Codec`]
473/// for a reserved or proprietary type, or a length that is not the type's.
474///
475/// # Safety
476///
477/// `frame` must point to `frame_len` readable bytes and `out_wor` must be writable.
478#[no_mangle]
479pub unsafe extern "C" fn pamoja_lorawan_relay_wor_parse(
480    frame: *const u8,
481    frame_len: usize,
482    out_wor: *mut PamojaLorawanWor,
483) -> PamojaStatus {
484    if out_wor.is_null() {
485        return null("out_wor");
486    }
487    let bytes = match read_bytes(frame, frame_len) {
488        Ok(bytes) => bytes,
489        Err(status) => return status,
490    };
491    match Wor::parse(&bytes) {
492        Ok(Wor::JoinRequest { uplink }) => {
493            *out_wor = PamojaLorawanWor {
494                kind: PAMOJA_LORAWAN_WOR_JOIN_REQUEST,
495                uplink: carrier_out(uplink),
496                dev_addr: 0,
497                wfcnt: 0,
498            };
499            PamojaStatus::Ok
500        }
501        Ok(Wor::Uplink(sealed)) => {
502            *out_wor = PamojaLorawanWor {
503                kind: PAMOJA_LORAWAN_WOR_UPLINK,
504                uplink: PamojaLorawanCarrier {
505                    frequency_hz: 0,
506                    data_rate: 0,
507                },
508                dev_addr: sealed.dev_addr(),
509                wfcnt: sealed.wfcnt(),
510            };
511            PamojaStatus::Ok
512        }
513        Err(error) => failed(error),
514    }
515}
516
517/// Checks a WOR frame ahead of a Class A uplink and reads where the uplink follows.
518///
519/// # Arguments
520///
521/// * `frame` - the frame a relay received.
522/// * `frame_len` - its length.
523/// * `keys` - the keys of the device it names.
524/// * `wfcnt` - the full 32-bit counter the relay takes it to carry.
525/// * `wor` - the carrier it arrived on.
526/// * `out_uplink` - receives where and how fast the uplink follows.
527///
528/// # Returns
529///
530/// [`PamojaStatus::Ok`] on success.
531///
532/// # Errors
533///
534/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, [`PamojaStatus::Codec`] for
535/// a frame that is not an uplink WOR, and [`PamojaStatus::Auth`] for a counter whose low bits
536/// differ or an integrity code that does not verify.
537///
538/// # Safety
539///
540/// `frame` must point to `frame_len` readable bytes, and the other pointers must be readable
541/// or writable as they are used.
542#[no_mangle]
543pub unsafe extern "C" fn pamoja_lorawan_relay_wor_open(
544    frame: *const u8,
545    frame_len: usize,
546    keys: *const PamojaLorawanWorKeys,
547    wfcnt: u32,
548    wor: *const PamojaLorawanCarrier,
549    out_uplink: *mut PamojaLorawanCarrier,
550) -> PamojaStatus {
551    let (Some(keys), Some(wor), false) = (keys.as_ref(), wor.as_ref(), out_uplink.is_null()) else {
552        return null("keys, wor and out_uplink");
553    };
554    let bytes = match read_bytes(frame, frame_len) {
555        Ok(bytes) => bytes,
556        Err(status) => return status,
557    };
558    let sealed = match Wor::parse(&bytes) {
559        Ok(Wor::Uplink(sealed)) => sealed,
560        Ok(Wor::JoinRequest { .. }) => {
561            set_last_error("a join request WOR carries nothing sealed".to_owned());
562            return PamojaStatus::Codec;
563        }
564        Err(error) => return failed(error),
565    };
566    match sealed.open(&keys_in(keys), wfcnt, carrier_in(wor)) {
567        Ok(uplink) => {
568            *out_uplink = carrier_out(uplink);
569            PamojaStatus::Ok
570        }
571        Err(error) => failed(error),
572    }
573}
574
575/// Builds a relay's WOR ACK, TS011-1.0.1 section 6.2.
576///
577/// # Arguments
578///
579/// * `keys` - the end device's keys.
580/// * `dev_addr` - its address.
581/// * `wfcnt` - the 32-bit counter of the acknowledged WOR frame.
582/// * `ack` - the carrier the acknowledgment goes out on.
583/// * `uplink` - the carrier the WOR frame named for the uplink.
584/// * `state` - what the relay tells the device.
585/// * `out_frame` - receives the [`PAMOJA_LORAWAN_WOR_ACK_LEN`] bytes.
586///
587/// # Returns
588///
589/// [`PamojaStatus::Ok`] on success.
590///
591/// # Errors
592///
593/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer or a coded field out of range,
594/// and [`PamojaStatus::Codec`] for a state or carrier the fields cannot hold.
595///
596/// # Safety
597///
598/// The pointers must be readable and `out_frame` must point to
599/// [`PAMOJA_LORAWAN_WOR_ACK_LEN`] writable bytes.
600#[no_mangle]
601pub unsafe extern "C" fn pamoja_lorawan_relay_wor_ack(
602    keys: *const PamojaLorawanWorKeys,
603    dev_addr: u32,
604    wfcnt: u32,
605    ack: *const PamojaLorawanCarrier,
606    uplink: *const PamojaLorawanCarrier,
607    state: *const PamojaLorawanStateSync,
608    out_frame: *mut u8,
609) -> PamojaStatus {
610    let (Some(keys), Some(ack), Some(uplink), Some(state), false) = (
611        keys.as_ref(),
612        ack.as_ref(),
613        uplink.as_ref(),
614        state.as_ref(),
615        out_frame.is_null(),
616    ) else {
617        return null("keys, ack, uplink, state and out_frame");
618    };
619    let state = match state_in(state) {
620        Ok(state) => state,
621        Err(status) => return status,
622    };
623    match wor_ack(
624        &keys_in(keys),
625        dev_addr,
626        wfcnt,
627        carrier_in(ack),
628        carrier_in(uplink),
629        state,
630    ) {
631        Ok(frame) => {
632            ptr::copy_nonoverlapping(frame.as_ptr(), out_frame, frame.len());
633            PamojaStatus::Ok
634        }
635        Err(error) => failed(error),
636    }
637}
638
639/// Checks and reads a WOR ACK, TS011-1.0.1 section 6.2.
640///
641/// # Arguments
642///
643/// * `frame` - the acknowledgment an end device received.
644/// * `frame_len` - its length.
645/// * `keys` - the device's keys.
646/// * `dev_addr` - its address.
647/// * `wfcnt` - the counter of the WOR frame it sent.
648/// * `ack` - the carrier the acknowledgment arrived on.
649/// * `uplink` - the carrier the WOR frame named.
650/// * `out_state` - receives what the relay said.
651///
652/// # Returns
653///
654/// [`PamojaStatus::Ok`] on success.
655///
656/// # Errors
657///
658/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, [`PamojaStatus::Codec`] for
659/// a frame of the wrong length or a reserved periodicity, and [`PamojaStatus::Auth`] when the
660/// integrity code does not verify.
661///
662/// # Safety
663///
664/// `frame` must point to `frame_len` readable bytes, and the other pointers must be readable
665/// or writable as they are used.
666#[no_mangle]
667pub unsafe extern "C" fn pamoja_lorawan_relay_wor_ack_open(
668    frame: *const u8,
669    frame_len: usize,
670    keys: *const PamojaLorawanWorKeys,
671    dev_addr: u32,
672    wfcnt: u32,
673    ack: *const PamojaLorawanCarrier,
674    uplink: *const PamojaLorawanCarrier,
675    out_state: *mut PamojaLorawanStateSync,
676) -> PamojaStatus {
677    let (Some(keys), Some(ack), Some(uplink), false) = (
678        keys.as_ref(),
679        ack.as_ref(),
680        uplink.as_ref(),
681        out_state.is_null(),
682    ) else {
683        return null("keys, ack, uplink and out_state");
684    };
685    let bytes = match read_bytes(frame, frame_len) {
686        Ok(bytes) => bytes,
687        Err(status) => return status,
688    };
689    match open_wor_ack(
690        &bytes,
691        &keys_in(keys),
692        dev_addr,
693        wfcnt,
694        carrier_in(ack),
695        carrier_in(uplink),
696    ) {
697        Ok(state) => {
698            *out_state = state_out(state);
699            PamojaStatus::Ok
700        }
701        Err(error) => failed(error),
702    }
703}
704
705/// Writes an uplink a relay forwards, TS011-1.0.1 section 9.1.
706///
707/// # Arguments
708///
709/// * `metadata` - what the relay heard of it.
710/// * `frequency_hz` - the frequency it arrived on.
711/// * `phy_payload` - the end device's frame.
712/// * `phy_payload_len` - its length.
713/// * `out_payload` - receives the relay uplink's payload for port 226, released with
714///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
715///
716/// # Returns
717///
718/// [`PamojaStatus::Ok`] on success.
719///
720/// # Errors
721///
722/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer or a WOR channel past 1, and
723/// [`PamojaStatus::Codec`] for a data rate or frequency the fields cannot hold.
724///
725/// # Safety
726///
727/// `metadata` must be readable, `phy_payload` must point to `phy_payload_len` readable bytes,
728/// and `out_payload` must be writable.
729#[no_mangle]
730pub unsafe extern "C" fn pamoja_lorawan_relay_forward_encode(
731    metadata: *const PamojaLorawanUplinkMetadata,
732    frequency_hz: u32,
733    phy_payload: *const u8,
734    phy_payload_len: usize,
735    out_payload: *mut *mut PamojaBuffer,
736) -> PamojaStatus {
737    let (Some(metadata), false) = (metadata.as_ref(), out_payload.is_null()) else {
738        return null("metadata and out_payload");
739    };
740    let wor_channel = match metadata.wor_channel {
741        0 => WorChannel::Default,
742        1 => WorChannel::Second,
743        other => {
744            set_last_error(format!("{other} is not a WOR channel, which is 0 or 1"));
745            return PamojaStatus::InvalidArgument;
746        }
747    };
748    let frame = match read_bytes(phy_payload, phy_payload_len) {
749        Ok(frame) => frame,
750        Err(status) => return status,
751    };
752    let forwarded = ForwardedUplink {
753        metadata: UplinkMetadata {
754            wor_channel,
755            rssi_dbm: metadata.rssi_dbm,
756            snr_db: metadata.snr_db,
757            data_rate: metadata.data_rate,
758        },
759        frequency_hz,
760        phy_payload: &frame,
761    };
762    let mut out = vec![0u8; relay::FORWARD_OVERHEAD + frame.len()];
763    match forwarded.encode(&mut out) {
764        Ok(_) => {
765            *out_payload = PamojaBuffer::into_raw(out);
766            PamojaStatus::Ok
767        }
768        Err(error) => failed(error),
769    }
770}
771
772/// Reads an uplink a relay forwarded, TS011-1.0.1 section 9.1.
773///
774/// # Arguments
775///
776/// * `payload` - the relay uplink's payload on port 226.
777/// * `payload_len` - its length.
778/// * `out_metadata` - receives what the relay heard.
779/// * `out_frequency_hz` - receives the frequency the uplink arrived on.
780/// * `out_phy_payload` - receives the end device's frame, released with
781///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
782///
783/// # Returns
784///
785/// [`PamojaStatus::Ok`] on success.
786///
787/// # Errors
788///
789/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, and [`PamojaStatus::Codec`]
790/// for a payload shorter than [`PAMOJA_LORAWAN_FORWARD_OVERHEAD`] or a reserved WOR channel.
791///
792/// # Safety
793///
794/// `payload` must point to `payload_len` readable bytes and the out pointers must be
795/// writable.
796#[no_mangle]
797pub unsafe extern "C" fn pamoja_lorawan_relay_forward_parse(
798    payload: *const u8,
799    payload_len: usize,
800    out_metadata: *mut PamojaLorawanUplinkMetadata,
801    out_frequency_hz: *mut u32,
802    out_phy_payload: *mut *mut PamojaBuffer,
803) -> PamojaStatus {
804    if out_metadata.is_null() || out_frequency_hz.is_null() || out_phy_payload.is_null() {
805        return null("out_metadata, out_frequency_hz and out_phy_payload");
806    }
807    let bytes = match read_bytes(payload, payload_len) {
808        Ok(bytes) => bytes,
809        Err(status) => return status,
810    };
811    match ForwardedUplink::parse(&bytes) {
812        Ok(forwarded) => {
813            *out_metadata = PamojaLorawanUplinkMetadata {
814                wor_channel: match forwarded.metadata.wor_channel {
815                    WorChannel::Default => 0,
816                    WorChannel::Second => 1,
817                },
818                rssi_dbm: forwarded.metadata.rssi_dbm,
819                snr_db: forwarded.metadata.snr_db,
820                data_rate: forwarded.metadata.data_rate,
821            };
822            *out_frequency_hz = forwarded.frequency_hz;
823            *out_phy_payload = PamojaBuffer::into_raw(forwarded.phy_payload.to_vec());
824            PamojaStatus::Ok
825        }
826        Err(error) => failed(error),
827    }
828}
829
830/// Works out the WOR preamble of an end device that does not know when the relay scans,
831/// TS011-1.0.1 section 5.2.
832///
833/// # Arguments
834///
835/// * `cad_periodicity` - how often the relay scans, coded 0 to 5.
836/// * `symbol_us` - the symbol time of the WOR frame's data rate, in microseconds.
837/// * `cad_to_rx` - the relay's time to start receiving, coded 0 to 3.
838/// * `out_symbols` - receives the preamble length in symbols.
839///
840/// # Returns
841///
842/// [`PamojaStatus::Ok`] on success.
843///
844/// # Errors
845///
846/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer or a code out of range.
847///
848/// # Safety
849///
850/// `out_symbols` must be writable.
851#[no_mangle]
852pub unsafe extern "C" fn pamoja_lorawan_relay_unsynchronized_preamble(
853    cad_periodicity: u8,
854    symbol_us: u64,
855    cad_to_rx: u8,
856    out_symbols: *mut u16,
857) -> PamojaStatus {
858    if out_symbols.is_null() {
859        return null("out_symbols");
860    }
861    let (period, receive) = match (periodicity(cad_periodicity), two_bits(cad_to_rx, "CadToRx")) {
862        (Ok(period), Ok(receive)) => (period, CadToRx::from_code(receive)),
863        (Err(status), _) | (_, Err(status)) => return status,
864    };
865    *out_symbols = unsynchronized_preamble_symbols(period, symbol_us, receive);
866    PamojaStatus::Ok
867}
868
869/// Works out the offset a relay reports in a WOR ACK, TS011-1.0.1 appendix 1.
870///
871/// # Arguments
872///
873/// * `scan_start_us` - when the scan that detected the frame started.
874/// * `preamble_end_us` - when the frame's preamble ended: when it finished arriving, less
875///   the airtime of its sync word and payload.
876/// * `out_offset_ms` - receives the offset in milliseconds.
877///
878/// # Returns
879///
880/// [`PamojaStatus::Ok`] on success.
881///
882/// # Errors
883///
884/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, a preamble that ended
885/// before the scan started, or an offset past the eleven bits a WOR ACK carries.
886///
887/// # Safety
888///
889/// `out_offset_ms` must be writable.
890#[no_mangle]
891pub unsafe extern "C" fn pamoja_lorawan_relay_t_offset_ms(
892    scan_start_us: u64,
893    preamble_end_us: u64,
894    out_offset_ms: *mut u16,
895) -> PamojaStatus {
896    if out_offset_ms.is_null() {
897        return null("out_offset_ms");
898    }
899    match t_offset_ms(scan_start_us, preamble_end_us) {
900        Some(offset) => {
901            *out_offset_ms = offset;
902            PamojaStatus::Ok
903        }
904        None => {
905            set_last_error(
906                "the preamble ended before the scan, or past the eleven bits a WOR ACK carries"
907                    .to_owned(),
908            );
909            PamojaStatus::InvalidArgument
910        }
911    }
912}
913
914/// Works out when a relay scanned from the WOR ACK that answered a frame.
915///
916/// # Arguments
917///
918/// * `wor_start_us` - when the acknowledged WOR frame started going out.
919/// * `preamble_symbols` - its preamble length.
920/// * `symbol_us` - its symbol time.
921/// * `state` - what the acknowledgment said.
922/// * `out_synchronization` - receives the synchronization.
923///
924/// # Returns
925///
926/// [`PamojaStatus::Ok`] on success.
927///
928/// # Errors
929///
930/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer or a coded field out of
931/// range.
932///
933/// # Safety
934///
935/// `state` must be readable and `out_synchronization` writable.
936#[no_mangle]
937pub unsafe extern "C" fn pamoja_lorawan_relay_synchronization(
938    wor_start_us: u64,
939    preamble_symbols: u16,
940    symbol_us: u64,
941    state: *const PamojaLorawanStateSync,
942    out_synchronization: *mut PamojaLorawanSynchronization,
943) -> PamojaStatus {
944    let (Some(state), false) = (state.as_ref(), out_synchronization.is_null()) else {
945        return null("state and out_synchronization");
946    };
947    let state = match state_in(state) {
948        Ok(state) => state,
949        Err(status) => return status,
950    };
951    let sync = Synchronization::from_ack(wor_start_us, preamble_symbols, symbol_us, &state);
952    *out_synchronization = PamojaLorawanSynchronization {
953        reference_us: sync.reference_us,
954        cad_periodicity: sync.cad_periodicity.code(),
955        relay_xtal: sync.relay_xtal.code(),
956        cad_to_rx: sync.cad_to_rx.code(),
957    };
958    PamojaStatus::Ok
959}
960
961/// Picks the relay scan a synchronized end device aims its next WOR frame at, TS011-1.0.1
962/// appendix 1.
963///
964/// # Arguments
965///
966/// * `synchronization` - what the device knows of the relay.
967/// * `now_us` - the time.
968/// * `device_xtal_ppm` - the device's crystal accuracy.
969/// * `symbol_us` - the symbol time of the WOR frame's data rate.
970/// * `other_channel` - `1` when the frame goes out on the relay's other channel.
971/// * `out_slot` - receives when to send and with how long a preamble.
972/// * `out_synchronized` - receives `1` with a slot, and `0` once the drift exceeds a period
973///   and the device should send an unsynchronized preamble instead.
974///
975/// # Returns
976///
977/// [`PamojaStatus::Ok`] on success.
978///
979/// # Errors
980///
981/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer or a coded field out of
982/// range.
983///
984/// # Safety
985///
986/// `synchronization` must be readable and the out pointers writable.
987#[no_mangle]
988pub unsafe extern "C" fn pamoja_lorawan_relay_next_wor(
989    synchronization: *const PamojaLorawanSynchronization,
990    now_us: u64,
991    device_xtal_ppm: u32,
992    symbol_us: u64,
993    other_channel: u8,
994    out_slot: *mut PamojaLorawanWorSlot,
995    out_synchronized: *mut u8,
996) -> PamojaStatus {
997    let (Some(sync), false, false) = (
998        synchronization.as_ref(),
999        out_slot.is_null(),
1000        out_synchronized.is_null(),
1001    ) else {
1002        return null("synchronization, out_slot and out_synchronized");
1003    };
1004    let sync = match (
1005        periodicity(sync.cad_periodicity),
1006        two_bits(sync.relay_xtal, "the relay crystal accuracy"),
1007        two_bits(sync.cad_to_rx, "CadToRx"),
1008    ) {
1009        (Ok(period), Ok(xtal), Ok(receive)) => Synchronization {
1010            reference_us: sync.reference_us,
1011            cad_periodicity: period,
1012            relay_xtal: XtalAccuracy::from_code(xtal),
1013            cad_to_rx: CadToRx::from_code(receive),
1014        },
1015        (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => return status,
1016    };
1017    match sync.next_wor(now_us, device_xtal_ppm, symbol_us, other_channel != 0) {
1018        Some(slot) => {
1019            *out_slot = PamojaLorawanWorSlot {
1020                start_us: slot.start_us,
1021                preamble_symbols: slot.preamble_symbols,
1022            };
1023            *out_synchronized = 1;
1024        }
1025        None => {
1026            *out_slot = PamojaLorawanWorSlot {
1027                start_us: 0,
1028                preamble_symbols: 0,
1029            };
1030            *out_synchronized = 0;
1031        }
1032    }
1033    PamojaStatus::Ok
1034}
1035
1036/// Reads the second channel a relay or end device configuration describes.
1037///
1038/// # Arguments
1039///
1040/// * `second_channel_index` - the coded index, 1 for a second channel.
1041/// * `data_rate` - its data rate.
1042/// * `ack_offset` - the coded acknowledgment offset, TS011-1.0.1 table 35.
1043/// * `frequency_hz` - its frequency.
1044/// * `out_channel` - receives the channel with its acknowledgment frequency.
1045///
1046/// # Returns
1047///
1048/// [`PamojaStatus::Ok`] on success.
1049///
1050/// # Errors
1051///
1052/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, an index that is not 1, or a
1053/// reserved offset.
1054///
1055/// # Safety
1056///
1057/// `out_channel` must be writable.
1058#[no_mangle]
1059pub unsafe extern "C" fn pamoja_lorawan_relay_second_channel(
1060    second_channel_index: u8,
1061    data_rate: u8,
1062    ack_offset: u8,
1063    frequency_hz: u32,
1064    out_channel: *mut PamojaLoraRelayChannel,
1065) -> PamojaStatus {
1066    if out_channel.is_null() {
1067        return null("out_channel");
1068    }
1069    match relay_second_channel(second_channel_index, data_rate, ack_offset, frequency_hz) {
1070        Some(channel) => {
1071            *out_channel = PamojaLoraRelayChannel {
1072                wor_frequency_hz: channel.wor_frequency_hz,
1073                ack_frequency_hz: channel.ack_frequency_hz,
1074                data_rate: channel.data_rate,
1075            };
1076            PamojaStatus::Ok
1077        }
1078        None => {
1079            set_last_error(format!(
1080                "index {second_channel_index} with offset {ack_offset} names no second channel"
1081            ));
1082            PamojaStatus::InvalidArgument
1083        }
1084    }
1085}
1086
1087unsafe fn sixteen(bytes: *const u8, len: usize, what: &str) -> Result<[u8; 16], PamojaStatus> {
1088    let bytes = read_bytes(bytes, len)?;
1089    <[u8; PAMOJA_LORAWAN_KEY_LEN]>::try_from(&bytes[..]).map_err(|_| {
1090        set_last_error(format!(
1091            "{what} must be exactly {PAMOJA_LORAWAN_KEY_LEN} bytes"
1092        ));
1093        PamojaStatus::InvalidArgument
1094    })
1095}
1096
1097#[cfg(test)]
1098mod tests {
1099    use super::*;
1100    use crate::lorawan::{pamoja_lorawan_session_free, pamoja_lorawan_session_new};
1101    use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1102
1103    #[test]
1104    fn a_wor_goes_out_opens_and_is_acknowledged_across_the_boundary() {
1105        unsafe {
1106            let mut session = ptr::null_mut();
1107            assert_eq!(
1108                pamoja_lorawan_session_new(
1109                    0x2601_1BDA,
1110                    [0x2B; 16].as_ptr(),
1111                    16,
1112                    [0x99; 16].as_ptr(),
1113                    16,
1114                    &mut session
1115                ),
1116                PamojaStatus::Ok
1117            );
1118            let mut keys = PamojaLorawanWorKeys {
1119                integrity: [0; 16],
1120                encryption: [0; 16],
1121            };
1122            assert_eq!(
1123                pamoja_lorawan_session_wor_keys(session, &mut keys),
1124                PamojaStatus::Ok
1125            );
1126            let mut root = [0u8; 16];
1127            pamoja_lorawan_session_root_wor_s_key(session, root.as_mut_ptr());
1128            let mut derived = keys;
1129            derived.integrity = [0; 16];
1130            assert_eq!(
1131                pamoja_lorawan_relay_wor_keys(root.as_ptr(), 16, 0x2601_1BDA, &mut derived),
1132                PamojaStatus::Ok
1133            );
1134            assert!(derived == keys, "the session and the root key agree");
1135
1136            let wor = PamojaLorawanCarrier {
1137                frequency_hz: 865_100_000,
1138                data_rate: 3,
1139            };
1140            let uplink = PamojaLorawanCarrier {
1141                frequency_hz: 868_100_000,
1142                data_rate: 5,
1143            };
1144            let mut frame = [0u8; PAMOJA_LORAWAN_WOR_UPLINK_LEN];
1145            assert_eq!(
1146                pamoja_lorawan_relay_wor_uplink(
1147                    &keys,
1148                    0x2601_1BDA,
1149                    1,
1150                    &uplink,
1151                    &wor,
1152                    frame.as_mut_ptr()
1153                ),
1154                PamojaStatus::Ok
1155            );
1156            assert_eq!(frame[..5], [0x01, 0xda, 0x1b, 0x01, 0x26]);
1157
1158            let mut read = std::mem::zeroed::<PamojaLorawanWor>();
1159            assert_eq!(
1160                pamoja_lorawan_relay_wor_parse(frame.as_ptr(), frame.len(), &mut read),
1161                PamojaStatus::Ok
1162            );
1163            assert_eq!(
1164                (read.kind, read.dev_addr, read.wfcnt),
1165                (PAMOJA_LORAWAN_WOR_UPLINK, 0x2601_1BDA, 1)
1166            );
1167            let mut opened = std::mem::zeroed::<PamojaLorawanCarrier>();
1168            assert_eq!(
1169                pamoja_lorawan_relay_wor_open(
1170                    frame.as_ptr(),
1171                    frame.len(),
1172                    &keys,
1173                    1,
1174                    &wor,
1175                    &mut opened
1176                ),
1177                PamojaStatus::Ok
1178            );
1179            assert_eq!(opened, uplink);
1180            assert_eq!(
1181                pamoja_lorawan_relay_wor_open(
1182                    frame.as_ptr(),
1183                    frame.len(),
1184                    &keys,
1185                    0x0001_0001,
1186                    &wor,
1187                    &mut opened
1188                ),
1189                PamojaStatus::Auth
1190            );
1191
1192            let ack = PamojaLorawanCarrier {
1193                frequency_hz: 865_300_000,
1194                data_rate: 3,
1195            };
1196            let state = PamojaLorawanStateSync {
1197                cad_to_rx: 1,
1198                forward: 0,
1199                relay_data_rate: 5,
1200                xtal_accuracy: 2,
1201                cad_periodicity: 1,
1202                t_offset_ms: 892,
1203            };
1204            let mut answer = [0u8; PAMOJA_LORAWAN_WOR_ACK_LEN];
1205            assert_eq!(
1206                pamoja_lorawan_relay_wor_ack(
1207                    &keys,
1208                    0x2601_1BDA,
1209                    1,
1210                    &ack,
1211                    &uplink,
1212                    &state,
1213                    answer.as_mut_ptr()
1214                ),
1215                PamojaStatus::Ok
1216            );
1217            assert_eq!(answer, [0xc0, 0x91, 0xac, 0x43, 0x19, 0x78, 0xef]);
1218            let mut heard = std::mem::zeroed::<PamojaLorawanStateSync>();
1219            assert_eq!(
1220                pamoja_lorawan_relay_wor_ack_open(
1221                    answer.as_ptr(),
1222                    answer.len(),
1223                    &keys,
1224                    0x2601_1BDA,
1225                    1,
1226                    &ack,
1227                    &uplink,
1228                    &mut heard
1229                ),
1230                PamojaStatus::Ok
1231            );
1232            assert_eq!(heard, state);
1233
1234            let mut bad = state;
1235            bad.cad_periodicity = 6;
1236            assert_eq!(
1237                pamoja_lorawan_relay_wor_ack(
1238                    &keys,
1239                    0x2601_1BDA,
1240                    1,
1241                    &ack,
1242                    &uplink,
1243                    &bad,
1244                    answer.as_mut_ptr()
1245                ),
1246                PamojaStatus::InvalidArgument
1247            );
1248            pamoja_lorawan_session_free(session);
1249        }
1250    }
1251
1252    #[test]
1253    fn a_forwarded_uplink_and_the_timing_cross_the_boundary() {
1254        unsafe {
1255            let metadata = PamojaLorawanUplinkMetadata {
1256                wor_channel: 1,
1257                rssi_dbm: -100,
1258                snr_db: 5,
1259                data_rate: 5,
1260            };
1261            let frame = [0x40u8, 0x01, 0x02];
1262            let mut payload = ptr::null_mut();
1263            assert_eq!(
1264                pamoja_lorawan_relay_forward_encode(
1265                    &metadata,
1266                    868_100_000,
1267                    frame.as_ptr(),
1268                    frame.len(),
1269                    &mut payload
1270                ),
1271                PamojaStatus::Ok
1272            );
1273            let bytes =
1274                std::slice::from_raw_parts(pamoja_buffer_data(payload), pamoja_buffer_len(payload))
1275                    .to_vec();
1276            pamoja_buffer_free(payload);
1277            assert_eq!(
1278                bytes,
1279                [0x95, 0xab, 0x01, 0x28, 0x76, 0x84, 0x40, 0x01, 0x02]
1280            );
1281
1282            let mut read = std::mem::zeroed::<PamojaLorawanUplinkMetadata>();
1283            let mut frequency = 0u32;
1284            let mut phy = ptr::null_mut();
1285            assert_eq!(
1286                pamoja_lorawan_relay_forward_parse(
1287                    bytes.as_ptr(),
1288                    bytes.len(),
1289                    &mut read,
1290                    &mut frequency,
1291                    &mut phy
1292                ),
1293                PamojaStatus::Ok
1294            );
1295            assert_eq!((read, frequency), (metadata, 868_100_000));
1296            assert_eq!(pamoja_buffer_len(phy), 3);
1297            pamoja_buffer_free(phy);
1298
1299            let mut symbols = 0u16;
1300            assert_eq!(
1301                pamoja_lorawan_relay_unsynchronized_preamble(1, 8_192, 1, &mut symbols),
1302                PamojaStatus::Ok
1303            );
1304            assert_eq!(symbols, 72);
1305            let mut offset = 0u16;
1306            assert_eq!(
1307                pamoja_lorawan_relay_t_offset_ms(87_654_000, 88_545_584, &mut offset),
1308                PamojaStatus::Ok
1309            );
1310            assert_eq!(offset, 892);
1311
1312            let state = PamojaLorawanStateSync {
1313                cad_to_rx: 1,
1314                forward: 0,
1315                relay_data_rate: 3,
1316                xtal_accuracy: 2,
1317                cad_periodicity: 1,
1318                t_offset_ms: 892,
1319            };
1320            let mut sync = std::mem::zeroed::<PamojaLorawanSynchronization>();
1321            assert_eq!(
1322                pamoja_lorawan_relay_synchronization(1_234_000, 133, 8_192, &state, &mut sync),
1323                PamojaStatus::Ok
1324            );
1325            assert_eq!(sync.reference_us, 1_431_536);
1326            let mut slot = std::mem::zeroed::<PamojaLorawanWorSlot>();
1327            let mut synchronized = 0u8;
1328            assert_eq!(
1329                pamoja_lorawan_relay_next_wor(
1330                    &sync,
1331                    61_000_000,
1332                    20,
1333                    8_192,
1334                    0,
1335                    &mut slot,
1336                    &mut synchronized
1337                ),
1338                PamojaStatus::Ok
1339            );
1340            assert_eq!(
1341                (synchronized, slot.start_us, slot.preamble_symbols),
1342                (1, 61_430_036, 11)
1343            );
1344
1345            let mut channel = std::mem::zeroed::<PamojaLoraRelayChannel>();
1346            assert_eq!(
1347                pamoja_lorawan_relay_second_channel(1, 3, 1, 868_100_000, &mut channel),
1348                PamojaStatus::Ok
1349            );
1350            assert_eq!(channel.ack_frequency_hz, 868_300_000);
1351        }
1352    }
1353}