Skip to main content

pamoja_ffi/
lorawan_relay_node.rs

1//! A LoRaWAN relay across the C ABI, TS011-1.0.1.
2//!
3//! [`pamoja_lorawan::relay::Relay`] is an end device that also listens for others: it scans
4//! its channels, verifies the wake-on-radio frames of the devices its network trusts it
5//! with, answers them, takes the uplinks behind them, forwards those to the network on port
6//! 226, and sends the answers on in each device's relay window. It owns no radio and no
7//! clock, so every call takes the time and hands back what to put on the air.
8//!
9//! A relay is opened on a plan and a session or a set of credentials, and released with
10//! [`pamoja_lorawan_relay_free`]. Its own device joins and sends through
11//! [`pamoja_lorawan_relay_join`] and [`pamoja_lorawan_relay_send`], and every downlink goes
12//! through [`pamoja_lorawan_relay_heard_in`], which acts on the relay commands in it.
13
14use std::ptr;
15
16use crate::lora::PamojaLoraLink;
17use crate::lorawan_device::{
18    heard_out, link_out, made_from, next_out, status_out, transmitted, window_in,
19    PamojaLorawanDeviceSettings, PamojaLorawanEndDeviceStatus, PamojaLorawanHeard,
20    PamojaLorawanTransmission,
21};
22use crate::lorawan_relay::PAMOJA_LORAWAN_WOR_ACK_LEN;
23use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
24use pamoja_lora::region::RelayChannel;
25use pamoja_lorawan::device::{DeviceError, Heard, Transmission};
26use pamoja_lorawan::relay::{
27    Acknowledgment, CadPeriodicity, CadToRx, Carrier, Listen, Relay, RelayConfig, RelayError,
28    RelayHeard, RelaySettings, RxrDownlink, Scan, Wake, WorChannel, XtalAccuracy,
29};
30
31/// A join request follows the wake-on-radio frame; listen for it.
32pub const PAMOJA_LORAWAN_WAKE_JOIN_REQUEST: u8 = 0;
33/// A trusted device's uplink follows.
34pub const PAMOJA_LORAWAN_WAKE_UPLINK: u8 = 1;
35/// A device the relay does not trust woke it, and its network will hear of it.
36pub const PAMOJA_LORAWAN_WAKE_NOTIFIED: u8 = 2;
37
38/// The frame was for the relay's own device.
39pub const PAMOJA_LORAWAN_RELAY_HEARD_DEVICE: u8 = 0;
40/// It carried a downlink for an end device, to send on in its relay window.
41pub const PAMOJA_LORAWAN_RELAY_HEARD_DOWNLINK: u8 = 1;
42/// It carried one the relay cannot send on.
43pub const PAMOJA_LORAWAN_RELAY_HEARD_UNDELIVERABLE: u8 = 2;
44
45/// An opaque handle to a LoRaWAN relay, which owns the end device it was made from.
46///
47/// Release it with [`pamoja_lorawan_relay_free`].
48pub struct PamojaLorawanRelay {
49    relay: Relay<'static>,
50    error: Option<RelayError>,
51}
52
53/// A scan for wake-on-radio frames, due next.
54#[repr(C)]
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct PamojaLorawanScan {
57    /// When to start detecting, in microseconds.
58    pub start_us: u64,
59    /// The frequency to listen on, in hertz.
60    pub frequency_hz: u32,
61    /// The data rate to listen at.
62    pub data_rate: u8,
63    /// `0` for the default channel, `1` for the second.
64    pub channel: u8,
65    /// The LoRa settings of a wake-on-radio frame, heard with inverted IQ.
66    pub link: PamojaLoraLink,
67    /// The longest preamble an end device sends on the channel, in symbols.
68    pub preamble_symbols: u16,
69}
70
71/// What a wake-on-radio frame led a relay to do.
72#[repr(C)]
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub struct PamojaLorawanWake {
75    /// When to start sending the acknowledgment, in microseconds.
76    pub ack_start_us: u64,
77    /// How long it holds the air, in microseconds.
78    pub ack_airtime_us: u64,
79    /// When the uplink starts arriving, in microseconds.
80    pub listen_start_us: u64,
81    /// [`PAMOJA_LORAWAN_WAKE_JOIN_REQUEST`], [`PAMOJA_LORAWAN_WAKE_UPLINK`] or
82    /// [`PAMOJA_LORAWAN_WAKE_NOTIFIED`].
83    pub kind: u8,
84    /// The device the frame came from, for an uplink or a notification.
85    pub dev_addr: u32,
86    /// The wake-on-radio frame counter it carried, for an uplink.
87    pub wfcnt: u32,
88    /// Whether the relay forwards the uplink, as TS011-1.0.1 table 16 codes it.
89    pub forward: u8,
90    /// `1` when there is an acknowledgment to send.
91    pub has_ack: u8,
92    /// The acknowledgment, seven bytes.
93    pub ack_frame: [u8; PAMOJA_LORAWAN_WOR_ACK_LEN],
94    /// Where it goes, in hertz.
95    pub ack_frequency_hz: u32,
96    /// The data rate it goes out at.
97    pub ack_data_rate: u8,
98    /// Its LoRa settings, sent with inverted IQ.
99    pub ack_link: PamojaLoraLink,
100    /// The power to ask of the radio for it, conducted, in dBm.
101    pub ack_output_dbm: i8,
102    /// `1` when the relay listens for the uplink behind the frame.
103    pub has_listen: u8,
104    /// Where the uplink arrives, in hertz.
105    pub listen_frequency_hz: u32,
106    /// The data rate it arrives at.
107    pub listen_data_rate: u8,
108    /// Its LoRa settings, heard with standard IQ.
109    pub listen_link: PamojaLoraLink,
110    /// The longest frame to receive; stop at anything longer.
111    pub listen_max_len: usize,
112}
113
114/// A downlink for an end device, to send in its relay window.
115///
116/// The frame's bytes cross beside it as a [`PamojaBuffer`].
117#[repr(C)]
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct PamojaLorawanRxr {
120    /// When to start sending it, in microseconds.
121    pub start_us: u64,
122    /// How long it holds the air, in microseconds.
123    pub airtime_us: u64,
124    /// The frequency, in hertz: the one the wake-on-radio frame arrived on.
125    pub frequency_hz: u32,
126    /// The data rate.
127    pub data_rate: u8,
128    /// The LoRa settings, sent with inverted IQ and a payload CRC.
129    pub link: PamojaLoraLink,
130    /// The power to ask of the radio, conducted, in dBm.
131    pub output_dbm: i8,
132}
133
134/// Makes a relay whose own device is activated by personalization.
135///
136/// # Arguments
137///
138/// * `plan` - a published channel plan.
139/// * `session` - the relay's own address and session keys.
140/// * `settings` - what its radio can do.
141/// * `xtal_accuracy` - how accurate the relay's crystal is, as TS011-1.0.1 table 17 codes
142///   it.
143/// * `cad_to_rx` - how many symbols it takes from detecting a preamble to receiving, as
144///   table 15 codes it.
145/// * `out_relay` - receives the relay.
146///
147/// # Returns
148///
149/// [`PamojaStatus::Ok`] on success.
150///
151/// # Errors
152///
153/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, a plan that is not a
154/// published one, or settings out of range.
155///
156/// # Safety
157///
158/// `plan` and `session` must be live handles, `settings` must point to a readable
159/// [`PamojaLorawanDeviceSettings`], and `out_relay` to a writable pointer.
160#[no_mangle]
161pub unsafe extern "C" fn pamoja_lorawan_relay_personalized(
162    plan: *const crate::lora_region::PamojaLoraPlan,
163    session: *const crate::lorawan::PamojaLorawanSession,
164    settings: *const PamojaLorawanDeviceSettings,
165    xtal_accuracy: u8,
166    cad_to_rx: u8,
167    out_relay: *mut *mut PamojaLorawanRelay,
168) -> PamojaStatus {
169    let (Some(session), false) = (session.as_ref(), out_relay.is_null()) else {
170        set_last_error("session and out_relay must not be null".to_owned());
171        return PamojaStatus::InvalidArgument;
172    };
173    let (plan, settings) = match made_from(plan, settings) {
174        Ok(parts) => parts,
175        Err(status) => return status,
176    };
177    match pamoja_lorawan::device::EndDevice::personalized(plan, session.session, settings) {
178        Ok(device) => {
179            *out_relay = Box::into_raw(Box::new(PamojaLorawanRelay {
180                relay: Relay::new(device, relay_settings(xtal_accuracy, cad_to_rx)),
181                error: None,
182            }));
183            PamojaStatus::Ok
184        }
185        Err(error) => {
186            set_last_error(error.to_string());
187            PamojaStatus::InvalidArgument
188        }
189    }
190}
191
192/// Makes a relay whose own device joins over the air.
193///
194/// # Arguments
195///
196/// * `plan` - a published channel plan.
197/// * `credentials` - the relay's own identifiers and root key.
198/// * `settings` - what its radio can do.
199/// * `xtal_accuracy` - how accurate its crystal is, table 17.
200/// * `cad_to_rx` - how long it takes to start receiving, table 15.
201/// * `out_relay` - receives the relay.
202///
203/// # Returns
204///
205/// [`PamojaStatus::Ok`] on success.
206///
207/// # Errors
208///
209/// As [`pamoja_lorawan_relay_personalized`].
210///
211/// # Safety
212///
213/// `plan` and `credentials` must be live handles, `settings` must point to a readable
214/// [`PamojaLorawanDeviceSettings`], and `out_relay` to a writable pointer.
215#[no_mangle]
216pub unsafe extern "C" fn pamoja_lorawan_relay_new(
217    plan: *const crate::lora_region::PamojaLoraPlan,
218    credentials: *const crate::lorawan::PamojaLorawanDevice,
219    settings: *const PamojaLorawanDeviceSettings,
220    xtal_accuracy: u8,
221    cad_to_rx: u8,
222    out_relay: *mut *mut PamojaLorawanRelay,
223) -> PamojaStatus {
224    let (Some(credentials), false) = (credentials.as_ref(), out_relay.is_null()) else {
225        set_last_error("credentials and out_relay must not be null".to_owned());
226        return PamojaStatus::InvalidArgument;
227    };
228    let (plan, settings) = match made_from(plan, settings) {
229        Ok(parts) => parts,
230        Err(status) => return status,
231    };
232    match pamoja_lorawan::device::EndDevice::new(plan, credentials.device.clone(), settings) {
233        Ok(device) => {
234            *out_relay = Box::into_raw(Box::new(PamojaLorawanRelay {
235                relay: Relay::new(device, relay_settings(xtal_accuracy, cad_to_rx)),
236                error: None,
237            }));
238            PamojaStatus::Ok
239        }
240        Err(error) => {
241            set_last_error(error.to_string());
242            PamojaStatus::InvalidArgument
243        }
244    }
245}
246
247/// The relay settings two codes describe.
248fn relay_settings(xtal_accuracy: u8, cad_to_rx: u8) -> RelaySettings {
249    RelaySettings::new(
250        XtalAccuracy::from_code(xtal_accuracy),
251        CadToRx::from_code(cad_to_rx),
252    )
253}
254
255/// Releases a relay and its own device.
256///
257/// # Arguments
258///
259/// * `relay` - the relay, or null.
260///
261/// # Safety
262///
263/// `relay` must come from [`pamoja_lorawan_relay_new`] or
264/// [`pamoja_lorawan_relay_personalized`] and not be used afterward.
265#[no_mangle]
266pub unsafe extern "C" fn pamoja_lorawan_relay_free(relay: *mut PamojaLorawanRelay) {
267    if !relay.is_null() {
268        drop(Box::from_raw(relay));
269    }
270}
271
272/// Makes the relay's own join request, as a device's own would be.
273///
274/// # Arguments
275///
276/// * `relay` - the relay.
277/// * `dev_nonce` - a nonce this device has never used.
278/// * `now_us` - the time, in microseconds.
279/// * `out_frame` - receives the frame to transmit.
280/// * `out_transmission` - receives where and how to transmit it, and the accept windows.
281///
282/// # Returns
283///
284/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller must
285/// release with [`pamoja_buffer_free`](crate::pamoja_buffer_free).
286///
287/// # Errors
288///
289/// Returns a failing status, with the reason on [`pamoja_lorawan_relay_error`].
290///
291/// # Safety
292///
293/// `relay` must be a live handle and the out pointers writable.
294#[no_mangle]
295pub unsafe extern "C" fn pamoja_lorawan_relay_join(
296    relay: *mut PamojaLorawanRelay,
297    dev_nonce: u16,
298    now_us: u64,
299    out_frame: *mut *mut PamojaBuffer,
300    out_transmission: *mut PamojaLorawanTransmission,
301) -> PamojaStatus {
302    relay_transmit(relay, out_frame, out_transmission, |device| {
303        device.join(dev_nonce, now_us)
304    })
305}
306
307/// Sends one of the relay's own uplinks, which also carries what it owes its network.
308///
309/// # Arguments
310///
311/// * `relay` - the relay.
312/// * `port` - the application port, or 0 for a frame with no payload.
313/// * `payload` - the payload, which may be null when `port` is 0.
314/// * `payload_len` - its length.
315/// * `confirmed` - `1` to ask the network to acknowledge it.
316/// * `now_us` - the time, in microseconds.
317/// * `out_frame` - receives the frame to transmit.
318/// * `out_transmission` - receives where and how to transmit it, and its windows.
319///
320/// # Returns
321///
322/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller must
323/// release with [`pamoja_buffer_free`](crate::pamoja_buffer_free).
324///
325/// # Errors
326///
327/// Returns a failing status, with the reason on [`pamoja_lorawan_relay_error`].
328///
329/// # Safety
330///
331/// `relay` must be a live handle, `payload` must point to `payload_len` readable bytes when
332/// that is non-zero, and the out pointers must be writable.
333#[no_mangle]
334#[allow(clippy::too_many_arguments)]
335pub unsafe extern "C" fn pamoja_lorawan_relay_send(
336    relay: *mut PamojaLorawanRelay,
337    port: u8,
338    payload: *const u8,
339    payload_len: usize,
340    confirmed: u8,
341    now_us: u64,
342    out_frame: *mut *mut PamojaBuffer,
343    out_transmission: *mut PamojaLorawanTransmission,
344) -> PamojaStatus {
345    let payload = match read_bytes(payload, payload_len) {
346        Ok(bytes) => bytes,
347        Err(status) => return status,
348    };
349    relay_transmit(relay, out_frame, out_transmission, |device| {
350        if port == 0 {
351            device.send_empty(now_us)
352        } else {
353            device.send(port, &payload, confirmed != 0, now_us)
354        }
355    })
356}
357
358/// Reports what the relay's own device is doing.
359///
360/// # Arguments
361///
362/// * `relay` - the relay.
363/// * `out_status` - receives the device's session, counters, channels and windows.
364///
365/// # Returns
366///
367/// [`PamojaStatus::Ok`] on success.
368///
369/// # Errors
370///
371/// Returns [`PamojaStatus::InvalidArgument`] for a null handle or out pointer.
372///
373/// # Safety
374///
375/// `relay` must be a live handle and `out_status` writable.
376#[no_mangle]
377pub unsafe extern "C" fn pamoja_lorawan_relay_status(
378    relay: *const PamojaLorawanRelay,
379    out_status: *mut PamojaLorawanEndDeviceStatus,
380) -> PamojaStatus {
381    let (Some(relay), false) = (relay.as_ref(), out_status.is_null()) else {
382        set_last_error("relay and out_status must not be null".to_owned());
383        return PamojaStatus::InvalidArgument;
384    };
385    *out_status = status_out(relay.relay.device());
386    PamojaStatus::Ok
387}
388
389/// Runs a call that puts one of the relay's own frames on the air.
390///
391/// # Safety
392///
393/// `relay` must be a live handle and the out pointers writable.
394unsafe fn relay_transmit(
395    relay: *mut PamojaLorawanRelay,
396    out_frame: *mut *mut PamojaBuffer,
397    out_transmission: *mut PamojaLorawanTransmission,
398    call: impl FnOnce(
399        &mut pamoja_lorawan::device::EndDevice<'static>,
400    ) -> Result<Transmission, DeviceError>,
401) -> PamojaStatus {
402    let (Some(relay), false, false) = (
403        relay.as_mut(),
404        out_frame.is_null(),
405        out_transmission.is_null(),
406    ) else {
407        set_last_error("relay, out_frame and out_transmission must not be null".to_owned());
408        return PamojaStatus::InvalidArgument;
409    };
410    *out_frame = ptr::null_mut();
411    let result = call(relay.relay.device_mut()).map_err(RelayError::Device);
412    match relay.settle(result) {
413        Ok(transmission) => {
414            transmitted(transmission, out_frame, out_transmission);
415            PamojaStatus::Ok
416        }
417        Err(status) => status,
418    }
419}
420
421/// Returns why the last relay call failed.
422///
423/// # Arguments
424///
425/// * `relay` - the relay.
426///
427/// # Returns
428///
429/// The reason, as a string the caller releases with
430/// [`pamoja_string_free`](crate::pamoja_string_free), or null when the last call succeeded.
431///
432/// # Safety
433///
434/// `relay` must be a live handle.
435#[no_mangle]
436pub unsafe extern "C" fn pamoja_lorawan_relay_error(
437    relay: *mut PamojaLorawanRelay,
438) -> *mut crate::PamojaString {
439    match relay.as_ref().and_then(|relay| relay.error) {
440        Some(error) => crate::PamojaString::into_raw(error.to_string()),
441        None => ptr::null_mut(),
442    }
443}
444
445/// Starts the relay scanning, or changes what a running one scans.
446///
447/// # Arguments
448///
449/// * `relay` - the relay.
450/// * `cad_periodicity` - how often it scans each channel, as TS011-1.0.1 table 18 codes it.
451/// * `default_channel_index` - which of the region's wake-on-radio channels is the default
452///   one.
453/// * `has_second_channel` - `1` to scan a second channel as well.
454/// * `second_wor_frequency_hz` - where wake-on-radio frames arrive on it.
455/// * `second_ack_frequency_hz` - where the relay answers them.
456/// * `second_data_rate` - the data rate of both.
457///
458/// # Returns
459///
460/// [`PamojaStatus::Ok`] on success.
461///
462/// # Errors
463///
464/// Returns a failing status, with the reason on [`pamoja_lorawan_relay_error`], for a
465/// reserved scan period, a channel the region does not define or the radio cannot tune, or
466/// scans too close together for the data rate.
467///
468/// # Safety
469///
470/// `relay` must be a live handle.
471#[no_mangle]
472pub unsafe extern "C" fn pamoja_lorawan_relay_start(
473    relay: *mut PamojaLorawanRelay,
474    cad_periodicity: u8,
475    default_channel_index: u8,
476    has_second_channel: u8,
477    second_wor_frequency_hz: u32,
478    second_ack_frequency_hz: u32,
479    second_data_rate: u8,
480) -> PamojaStatus {
481    let Some(relay) = relay.as_mut() else {
482        set_last_error("relay must not be null".to_owned());
483        return PamojaStatus::InvalidArgument;
484    };
485    let Some(periodicity) = CadPeriodicity::from_code(cad_periodicity) else {
486        set_last_error("the scan period is reserved".to_owned());
487        return PamojaStatus::InvalidArgument;
488    };
489    let Some(default_channel) = relay.relay.region_channel(default_channel_index) else {
490        set_last_error("the region does not define that wake-on-radio channel".to_owned());
491        return PamojaStatus::InvalidArgument;
492    };
493    let mut config = RelayConfig::new(periodicity, default_channel);
494    if has_second_channel != 0 {
495        config = config.with_second_channel(RelayChannel::new(
496            second_wor_frequency_hz,
497            second_ack_frequency_hz,
498            second_data_rate,
499        ));
500    }
501    let started = relay.relay.start(config);
502    relay.settle(started).err().unwrap_or(PamojaStatus::Ok)
503}
504
505/// Stops the relay scanning.
506///
507/// # Arguments
508///
509/// * `relay` - the relay.
510///
511/// # Safety
512///
513/// `relay` must be a live handle.
514#[no_mangle]
515pub unsafe extern "C" fn pamoja_lorawan_relay_stop(relay: *mut PamojaLorawanRelay) {
516    if let Some(relay) = relay.as_mut() {
517        relay.relay.stop();
518    }
519}
520
521/// Reports whether the relay is scanning.
522///
523/// # Arguments
524///
525/// * `relay` - the relay.
526///
527/// # Returns
528///
529/// `1` while it scans, and `0` while it is stopped or the handle is null.
530///
531/// # Safety
532///
533/// `relay` must be a live handle.
534#[no_mangle]
535pub unsafe extern "C" fn pamoja_lorawan_relay_running(relay: *const PamojaLorawanRelay) -> u8 {
536    match relay.as_ref() {
537        Some(relay) => u8::from(relay.relay.config().is_some()),
538        None => 0,
539    }
540}
541
542/// Trusts an end device, as an `UpdateUplinkListReq` with the same fields does.
543///
544/// # Arguments
545///
546/// * `relay` - the relay.
547/// * `index` - the entry, 0 to 15.
548/// * `dev_addr` - the device's address.
549/// * `root_wor_s_key` - its sixteen-byte root relay session key.
550/// * `next_wfcnt` - the wake-on-radio frame counter to expect from it next.
551/// * `reload_rate` - its uplinks forwarded an hour, 63 for no limit.
552/// * `bucket_size` - the coded bucket size multiplier, TS011-1.0.1 table 55.
553///
554/// # Returns
555///
556/// [`PamojaStatus::Ok`] on success.
557///
558/// # Errors
559///
560/// Returns [`PamojaStatus::InvalidArgument`] for a null handle or key, or an index past 15.
561///
562/// # Safety
563///
564/// `relay` must be a live handle and `root_wor_s_key` point to sixteen readable bytes.
565#[no_mangle]
566pub unsafe extern "C" fn pamoja_lorawan_relay_trust(
567    relay: *mut PamojaLorawanRelay,
568    index: u8,
569    dev_addr: u32,
570    root_wor_s_key: *const u8,
571    next_wfcnt: u32,
572    reload_rate: u8,
573    bucket_size: u8,
574) -> PamojaStatus {
575    let (Some(relay), false) = (relay.as_mut(), root_wor_s_key.is_null()) else {
576        set_last_error("relay and root_wor_s_key must not be null".to_owned());
577        return PamojaStatus::InvalidArgument;
578    };
579    let mut key = [0u8; 16];
580    key.copy_from_slice(std::slice::from_raw_parts(root_wor_s_key, 16));
581    if relay
582        .relay
583        .trust(index, dev_addr, &key, next_wfcnt, reload_rate, bucket_size)
584    {
585        PamojaStatus::Ok
586    } else {
587        set_last_error("a relay trusts sixteen devices, at indexes 0 to 15".to_owned());
588        PamojaStatus::InvalidArgument
589    }
590}
591
592/// Says when and where to scan next.
593///
594/// # Arguments
595///
596/// * `relay` - the relay.
597/// * `now_us` - the time, in microseconds.
598/// * `out_scan` - receives the scan.
599///
600/// # Returns
601///
602/// [`PamojaStatus::Ok`] on success.
603///
604/// # Errors
605///
606/// Returns a failing status, with the reason on [`pamoja_lorawan_relay_error`], while the
607/// relay is not running.
608///
609/// # Safety
610///
611/// `relay` must be a live handle and `out_scan` writable.
612#[no_mangle]
613pub unsafe extern "C" fn pamoja_lorawan_relay_next_scan(
614    relay: *mut PamojaLorawanRelay,
615    now_us: u64,
616    out_scan: *mut PamojaLorawanScan,
617) -> PamojaStatus {
618    let (Some(relay), false) = (relay.as_mut(), out_scan.is_null()) else {
619        set_last_error("relay and out_scan must not be null".to_owned());
620        return PamojaStatus::InvalidArgument;
621    };
622    match relay.relay.next_scan(now_us) {
623        Some(scan) => {
624            *out_scan = scan_out(&scan);
625            relay.error = None;
626            PamojaStatus::Ok
627        }
628        None => {
629            relay.error = Some(RelayError::Stopped);
630            set_last_error(RelayError::Stopped.to_string());
631            PamojaStatus::Other
632        }
633    }
634}
635
636/// Reads a wake-on-radio frame a scan heard.
637///
638/// # Arguments
639///
640/// * `relay` - the relay.
641/// * `scan` - the scan that heard it, as [`pamoja_lorawan_relay_next_scan`] gave it.
642/// * `frame` - the frame.
643/// * `frame_len` - its length.
644/// * `rssi_dbm` - its signal strength.
645/// * `snr_db` - its signal-to-noise ratio.
646/// * `ended_us` - when it finished arriving, in microseconds.
647/// * `out_wake` - receives what to do next.
648///
649/// # Returns
650///
651/// [`PamojaStatus::Ok`] on success.
652///
653/// # Errors
654///
655/// Returns a failing status, with the reason on [`pamoja_lorawan_relay_error`]: stopped, a
656/// forward still waiting, a frame that does not decode or verify, a carrier the relay
657/// cannot hear, or a forwarding limit.
658///
659/// # Safety
660///
661/// `relay` must be a live handle, `scan` and `out_wake` must be readable and writable, and
662/// `frame` must point to `frame_len` readable bytes.
663#[no_mangle]
664#[allow(clippy::too_many_arguments)]
665pub unsafe extern "C" fn pamoja_lorawan_relay_heard_wor(
666    relay: *mut PamojaLorawanRelay,
667    scan: *const PamojaLorawanScan,
668    frame: *const u8,
669    frame_len: usize,
670    rssi_dbm: i16,
671    snr_db: i8,
672    ended_us: u64,
673    out_wake: *mut PamojaLorawanWake,
674) -> PamojaStatus {
675    let (Some(relay), Some(scan), false) = (relay.as_mut(), scan.as_ref(), out_wake.is_null())
676    else {
677        set_last_error("relay, scan and out_wake must not be null".to_owned());
678        return PamojaStatus::InvalidArgument;
679    };
680    let frame = match read_bytes(frame, frame_len) {
681        Ok(bytes) => bytes,
682        Err(status) => return status,
683    };
684    let scan = match scan_in(scan) {
685        Some(scan) => scan,
686        None => {
687            set_last_error("the scan names a data rate the region does not define".to_owned());
688            return PamojaStatus::InvalidArgument;
689        }
690    };
691    let heard = relay
692        .relay
693        .heard_wor(&scan, &frame, rssi_dbm, snr_db, ended_us);
694    match relay.settle(heard) {
695        Ok(wake) => {
696            *out_wake = wake_out(&wake, &scan);
697            PamojaStatus::Ok
698        }
699        Err(status) => status,
700    }
701}
702
703/// Reads the uplink a wake-on-radio frame announced, and holds it to forward.
704///
705/// # Arguments
706///
707/// * `relay` - the relay.
708/// * `frame` - the uplink.
709/// * `frame_len` - its length.
710/// * `rssi_dbm` - its signal strength.
711/// * `snr_db` - its signal-to-noise ratio.
712/// * `ended_us` - when it finished arriving, in microseconds.
713/// * `out_due_us` - receives when to forward it.
714///
715/// # Returns
716///
717/// [`PamojaStatus::Ok`] on success.
718///
719/// # Errors
720///
721/// Returns a failing status, with the reason on [`pamoja_lorawan_relay_error`]: no uplink
722/// announced, a frame that is not the kind announced or is from another device, a join
723/// request the filter drops, or a forwarding limit.
724///
725/// # Safety
726///
727/// `relay` must be a live handle, `frame` must point to `frame_len` readable bytes, and
728/// `out_due_us` must be writable.
729#[no_mangle]
730pub unsafe extern "C" fn pamoja_lorawan_relay_heard_uplink(
731    relay: *mut PamojaLorawanRelay,
732    frame: *const u8,
733    frame_len: usize,
734    rssi_dbm: i16,
735    snr_db: i8,
736    ended_us: u64,
737    out_due_us: *mut u64,
738) -> PamojaStatus {
739    let (Some(relay), false) = (relay.as_mut(), out_due_us.is_null()) else {
740        set_last_error("relay and out_due_us must not be null".to_owned());
741        return PamojaStatus::InvalidArgument;
742    };
743    let frame = match read_bytes(frame, frame_len) {
744        Ok(bytes) => bytes,
745        Err(status) => return status,
746    };
747    let held = relay.relay.heard_uplink(&frame, rssi_dbm, snr_db, ended_us);
748    match relay.settle(held) {
749        Ok(due_us) => {
750            *out_due_us = due_us;
751            PamojaStatus::Ok
752        }
753        Err(status) => status,
754    }
755}
756
757/// Clears the uplink a wake-on-radio frame announced, once listening for it heard nothing.
758///
759/// # Arguments
760///
761/// * `relay` - the relay.
762///
763/// # Safety
764///
765/// `relay` must be a live handle.
766#[no_mangle]
767pub unsafe extern "C" fn pamoja_lorawan_relay_uplink_missed(relay: *mut PamojaLorawanRelay) {
768    if let Some(relay) = relay.as_mut() {
769        relay.relay.uplink_missed();
770    }
771}
772
773/// Sends the uplink the relay is holding, in its own uplink on port 226.
774///
775/// # Arguments
776///
777/// * `relay` - the relay.
778/// * `now_us` - the time, in microseconds.
779/// * `out_frame` - receives the frame to transmit.
780/// * `out_transmission` - receives where and how to transmit it, and its windows.
781///
782/// # Returns
783///
784/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller must
785/// release with [`pamoja_buffer_free`](crate::pamoja_buffer_free).
786///
787/// # Errors
788///
789/// Returns a failing status, with the reason on [`pamoja_lorawan_relay_error`]: nothing
790/// held, or the relay's own device not free to send yet.
791///
792/// # Safety
793///
794/// `relay` must be a live handle and the out pointers writable.
795#[no_mangle]
796pub unsafe extern "C" fn pamoja_lorawan_relay_forward(
797    relay: *mut PamojaLorawanRelay,
798    now_us: u64,
799    out_frame: *mut *mut PamojaBuffer,
800    out_transmission: *mut PamojaLorawanTransmission,
801) -> PamojaStatus {
802    let (Some(relay), false, false) = (
803        relay.as_mut(),
804        out_frame.is_null(),
805        out_transmission.is_null(),
806    ) else {
807        set_last_error("relay, out_frame and out_transmission must not be null".to_owned());
808        return PamojaStatus::InvalidArgument;
809    };
810    *out_frame = ptr::null_mut();
811    let sent = relay.relay.forward(now_us);
812    match relay.settle(sent) {
813        Ok(transmission) => {
814            transmitted(transmission, out_frame, out_transmission);
815            PamojaStatus::Ok
816        }
817        Err(status) => status,
818    }
819}
820
821/// Returns when the forwarded uplink waiting to go out is due.
822///
823/// # Arguments
824///
825/// * `relay` - the relay.
826/// * `out_due_us` - receives the time.
827///
828/// # Returns
829///
830/// `1` when one is waiting, `0` when none is.
831///
832/// # Safety
833///
834/// `relay` must be a live handle and `out_due_us` writable.
835#[no_mangle]
836pub unsafe extern "C" fn pamoja_lorawan_relay_forward_due(
837    relay: *mut PamojaLorawanRelay,
838    out_due_us: *mut u64,
839) -> u8 {
840    let (Some(relay), false) = (relay.as_ref(), out_due_us.is_null()) else {
841        return 0;
842    };
843    match relay.relay.forward_due() {
844        Some(due_us) => {
845            *out_due_us = due_us;
846            1
847        }
848        None => 0,
849    }
850}
851
852/// Reads a frame the relay's own device heard, acting on the relay commands in it.
853///
854/// A downlink on port 226 answering a forwarded uplink becomes a frame for the end device,
855/// handed back through `out_frame` with the window to send it in.
856///
857/// # Arguments
858///
859/// * `relay` - the relay.
860/// * `window` - which window it arrived in, as
861///   [`PAMOJA_LORAWAN_WINDOW_RX1`](crate::lorawan_device::PAMOJA_LORAWAN_WINDOW_RX1) codes
862///   it.
863/// * `frame` - the bytes the radio received.
864/// * `frame_len` - their length.
865/// * `snr_db` - the frame's signal-to-noise ratio.
866/// * `out_kind` - receives [`PAMOJA_LORAWAN_RELAY_HEARD_DEVICE`],
867///   [`PAMOJA_LORAWAN_RELAY_HEARD_DOWNLINK`] or
868///   [`PAMOJA_LORAWAN_RELAY_HEARD_UNDELIVERABLE`].
869/// * `out_heard` - receives what the relay's own device made of it.
870/// * `out_payload` - receives the payload of that, as a buffer to release.
871/// * `out_frame` - receives the frame to send the end device, as a buffer to release.
872/// * `out_downlink` - receives when and where to send it.
873///
874/// # Returns
875///
876/// [`PamojaStatus::Ok`] on success.
877///
878/// # Errors
879///
880/// Returns a failing status, with the reason on [`pamoja_lorawan_relay_error`], for
881/// whatever the relay's own device refuses.
882///
883/// # Safety
884///
885/// `relay` must be a live handle, `frame` must point to `frame_len` readable bytes, and the
886/// out pointers must be writable.
887#[no_mangle]
888#[allow(clippy::too_many_arguments)]
889pub unsafe extern "C" fn pamoja_lorawan_relay_heard_in(
890    relay: *mut PamojaLorawanRelay,
891    window: u8,
892    frame: *const u8,
893    frame_len: usize,
894    snr_db: i8,
895    out_kind: *mut u8,
896    out_heard: *mut PamojaLorawanHeard,
897    out_payload: *mut *mut PamojaBuffer,
898    out_frame: *mut *mut PamojaBuffer,
899    out_downlink: *mut PamojaLorawanRxr,
900) -> PamojaStatus {
901    let (Some(relay), false, false, false, false, false) = (
902        relay.as_mut(),
903        out_kind.is_null(),
904        out_heard.is_null(),
905        out_payload.is_null(),
906        out_frame.is_null(),
907        out_downlink.is_null(),
908    ) else {
909        set_last_error("relay and every out pointer must not be null".to_owned());
910        return PamojaStatus::InvalidArgument;
911    };
912    let bytes = match read_bytes(frame, frame_len) {
913        Ok(bytes) => bytes,
914        Err(status) => return status,
915    };
916    let dev_addr = relay.relay.device().dev_addr().unwrap_or(0);
917    let Some(window) = window_in(window) else {
918        set_last_error("the window must be RX1, RX2 or RXR".to_owned());
919        return PamojaStatus::InvalidArgument;
920    };
921    *out_payload = ptr::null_mut();
922    *out_frame = ptr::null_mut();
923
924    let heard = relay.relay.heard_in(window, &bytes, snr_db);
925    match relay.settle(heard) {
926        Ok(RelayHeard::Device(heard)) => {
927            *out_kind = PAMOJA_LORAWAN_RELAY_HEARD_DEVICE;
928            *out_heard = heard_out(heard, dev_addr, out_payload);
929            *out_downlink = empty_rxr();
930            PamojaStatus::Ok
931        }
932        Ok(RelayHeard::Downlink { delivery, downlink }) => {
933            *out_kind = PAMOJA_LORAWAN_RELAY_HEARD_DOWNLINK;
934            *out_heard = heard_out(Heard::Data(delivery), dev_addr, out_payload);
935            *out_frame = PamojaBuffer::into_raw(downlink.frame().to_vec());
936            *out_downlink = rxr_out(&downlink);
937            PamojaStatus::Ok
938        }
939        Ok(RelayHeard::Undeliverable { delivery, reason }) => {
940            *out_kind = PAMOJA_LORAWAN_RELAY_HEARD_UNDELIVERABLE;
941            *out_heard = heard_out(Heard::Data(delivery), dev_addr, out_payload);
942            *out_downlink = empty_rxr();
943            relay.error = Some(reason);
944            set_last_error(reason.to_string());
945            PamojaStatus::Ok
946        }
947        Err(status) => status,
948    }
949}
950
951/// Says what comes next once the relay's own receive windows closed with nothing in them.
952///
953/// # Arguments
954///
955/// * `relay` - the relay.
956/// * `now_us` - the time the second window closed, in microseconds.
957/// * `out_next` - receives the outcome, as
958///   [`PAMOJA_LORAWAN_NEXT_REPEAT`](crate::lorawan_device::PAMOJA_LORAWAN_NEXT_REPEAT) codes
959///   it.
960/// * `out_not_before_us` - receives the earliest time to send again, where one applies.
961///
962/// # Returns
963///
964/// [`PamojaStatus::Ok`] on success.
965///
966/// # Errors
967///
968/// Returns a failing status, with the reason on [`pamoja_lorawan_relay_error`], when no
969/// transmission waits on its windows.
970///
971/// # Safety
972///
973/// `relay` must be a live handle and the out pointers writable.
974#[no_mangle]
975pub unsafe extern "C" fn pamoja_lorawan_relay_nothing_heard(
976    relay: *mut PamojaLorawanRelay,
977    now_us: u64,
978    out_next: *mut u8,
979    out_not_before_us: *mut u64,
980) -> PamojaStatus {
981    let (Some(relay), false, false) = (
982        relay.as_mut(),
983        out_next.is_null(),
984        out_not_before_us.is_null(),
985    ) else {
986        set_last_error("relay, out_next and out_not_before_us must not be null".to_owned());
987        return PamojaStatus::InvalidArgument;
988    };
989    let next = relay.relay.nothing_heard(now_us);
990    match relay.settle(next) {
991        Ok(next) => {
992            let (code, not_before_us) = next_out(next);
993            *out_next = code;
994            *out_not_before_us = not_before_us;
995            PamojaStatus::Ok
996        }
997        Err(status) => status,
998    }
999}
1000
1001impl PamojaLorawanRelay {
1002    /// Keeps the reason a call failed, and maps it onto a status.
1003    fn settle<T>(&mut self, result: Result<T, RelayError>) -> Result<T, PamojaStatus> {
1004        match result {
1005            Ok(value) => {
1006                self.error = None;
1007                Ok(value)
1008            }
1009            Err(error) => {
1010                self.error = Some(error);
1011                set_last_error(error.to_string());
1012                Err(PamojaStatus::Other)
1013            }
1014        }
1015    }
1016}
1017
1018/// A scan, as C sees it.
1019fn scan_out(scan: &Scan) -> PamojaLorawanScan {
1020    PamojaLorawanScan {
1021        start_us: scan.start_us,
1022        frequency_hz: scan.carrier.frequency_hz,
1023        data_rate: scan.carrier.data_rate,
1024        channel: match scan.channel {
1025            WorChannel::Default => 0,
1026            WorChannel::Second => 1,
1027        },
1028        link: link_out(scan.link),
1029        preamble_symbols: scan.preamble_symbols,
1030    }
1031}
1032
1033/// A scan, as the relay reads it back.
1034#[allow(clippy::unnecessary_wraps)]
1035fn scan_in(scan: &PamojaLorawanScan) -> Option<Scan> {
1036    Some(Scan {
1037        start_us: scan.start_us,
1038        channel: match scan.channel {
1039            0 => WorChannel::Default,
1040            1 => WorChannel::Second,
1041            _ => return None,
1042        },
1043        carrier: Carrier::new(scan.frequency_hz, scan.data_rate),
1044        link: crate::lora::settings(scan.link),
1045        preamble_symbols: scan.preamble_symbols,
1046    })
1047}
1048
1049/// What a wake-on-radio frame led to, as C sees it.
1050fn wake_out(wake: &Wake, scan: &Scan) -> PamojaLorawanWake {
1051    let empty_link = link_out(scan.link);
1052    let mut out = PamojaLorawanWake {
1053        ack_start_us: 0,
1054        ack_airtime_us: 0,
1055        listen_start_us: 0,
1056        kind: PAMOJA_LORAWAN_WAKE_JOIN_REQUEST,
1057        dev_addr: 0,
1058        wfcnt: 0,
1059        forward: 0,
1060        has_ack: 0,
1061        ack_frame: [0; PAMOJA_LORAWAN_WOR_ACK_LEN],
1062        ack_frequency_hz: 0,
1063        ack_data_rate: 0,
1064        ack_link: empty_link,
1065        ack_output_dbm: 0,
1066        has_listen: 0,
1067        listen_frequency_hz: 0,
1068        listen_data_rate: 0,
1069        listen_link: empty_link,
1070        listen_max_len: 0,
1071    };
1072    match wake {
1073        Wake::JoinRequest { listen } => {
1074            out.kind = PAMOJA_LORAWAN_WAKE_JOIN_REQUEST;
1075            listen_out(&mut out, listen);
1076        }
1077        Wake::Uplink {
1078            dev_addr,
1079            wfcnt,
1080            forward,
1081            acknowledgment,
1082            listen,
1083        } => {
1084            out.kind = PAMOJA_LORAWAN_WAKE_UPLINK;
1085            out.dev_addr = *dev_addr;
1086            out.wfcnt = *wfcnt;
1087            out.forward = forward.code();
1088            if let Some(acknowledgment) = acknowledgment {
1089                ack_out(&mut out, acknowledgment);
1090            }
1091            if let Some(listen) = listen {
1092                listen_out(&mut out, listen);
1093            }
1094        }
1095        Wake::Notified { dev_addr } => {
1096            out.kind = PAMOJA_LORAWAN_WAKE_NOTIFIED;
1097            out.dev_addr = *dev_addr;
1098        }
1099    }
1100    out
1101}
1102
1103/// Fills in the acknowledgment a relay answers with.
1104fn ack_out(out: &mut PamojaLorawanWake, acknowledgment: &Acknowledgment) {
1105    out.has_ack = 1;
1106    out.ack_frame = acknowledgment.frame;
1107    out.ack_start_us = acknowledgment.start_us;
1108    out.ack_airtime_us = acknowledgment.airtime_us;
1109    out.ack_frequency_hz = acknowledgment.carrier.frequency_hz;
1110    out.ack_data_rate = acknowledgment.carrier.data_rate;
1111    out.ack_link = link_out(acknowledgment.link);
1112    out.ack_output_dbm = acknowledgment.output_dbm;
1113}
1114
1115/// Fills in where the uplink behind a frame arrives.
1116fn listen_out(out: &mut PamojaLorawanWake, listen: &Listen) {
1117    out.has_listen = 1;
1118    out.listen_start_us = listen.start_us;
1119    out.listen_frequency_hz = listen.carrier.frequency_hz;
1120    out.listen_data_rate = listen.carrier.data_rate;
1121    out.listen_link = link_out(listen.link);
1122    out.listen_max_len = listen.max_len;
1123}
1124
1125/// A downlink for an end device, as C sees it.
1126fn rxr_out(downlink: &RxrDownlink) -> PamojaLorawanRxr {
1127    PamojaLorawanRxr {
1128        start_us: downlink.start_us,
1129        airtime_us: downlink.airtime_us,
1130        frequency_hz: downlink.carrier.frequency_hz,
1131        data_rate: downlink.carrier.data_rate,
1132        link: link_out(downlink.link),
1133        output_dbm: downlink.output_dbm,
1134    }
1135}
1136
1137/// The window field of a frame that carries none.
1138fn empty_rxr() -> PamojaLorawanRxr {
1139    PamojaLorawanRxr {
1140        start_us: 0,
1141        airtime_us: 0,
1142        frequency_hz: 0,
1143        data_rate: 0,
1144        link: link_out(pamoja_lora::LinkSettings::new(7, 125_000)),
1145        output_dbm: 0,
1146    }
1147}
1148
1149#[cfg(test)]
1150mod tests {
1151    use super::*;
1152
1153    use pamoja_lorawan::relay::{root_wor_s_key, wor_uplink, WorKeys, LA_FPORT_RELAY};
1154    use pamoja_lorawan::{Downlink, Session, Uplink};
1155
1156    use crate::lora_region::{
1157        pamoja_lora_plan_for_region, PamojaLoraPlan, PAMOJA_LORA_REGION_EU868,
1158    };
1159    use crate::lorawan::{pamoja_lorawan_session_new, PamojaLorawanSession};
1160    use crate::lorawan_device::{pamoja_lorawan_device_settings, PAMOJA_LORAWAN_WINDOW_RX1};
1161
1162    /// The bytes of a buffer the boundary handed back.
1163    unsafe fn buffer_bytes(buffer: *const PamojaBuffer) -> Vec<u8> {
1164        let len = crate::pamoja_buffer_len(buffer);
1165        std::slice::from_raw_parts(crate::pamoja_buffer_data(buffer), len).to_vec()
1166    }
1167
1168    const RELAY_ADDR: u32 = 0x2601_0001;
1169    const SENSOR_ADDR: u32 = 0x2601_1BDA;
1170    const SENSOR_NWK_S_KEY: [u8; 16] = [0x2B; 16];
1171
1172    /// The region's plan, the relay's session, and a relay handle built on both.
1173    unsafe fn relay() -> (
1174        *mut PamojaLoraPlan,
1175        *mut PamojaLorawanSession,
1176        *mut PamojaLorawanRelay,
1177    ) {
1178        let mut plan: *mut PamojaLoraPlan = ptr::null_mut();
1179        assert_eq!(
1180            pamoja_lora_plan_for_region(PAMOJA_LORA_REGION_EU868, &mut plan),
1181            PamojaStatus::Ok
1182        );
1183        let mut session: *mut PamojaLorawanSession = ptr::null_mut();
1184        assert_eq!(
1185            pamoja_lorawan_session_new(
1186                RELAY_ADDR,
1187                [0x11; 16].as_ptr(),
1188                16,
1189                [0x22; 16].as_ptr(),
1190                16,
1191                &mut session
1192            ),
1193            PamojaStatus::Ok
1194        );
1195        let mut settings = core::mem::zeroed::<PamojaLorawanDeviceSettings>();
1196        assert_eq!(
1197            pamoja_lorawan_device_settings(2, 14, &mut settings),
1198            PamojaStatus::Ok
1199        );
1200        settings.lowest_hz = 863_000_000;
1201        settings.highest_hz = 870_000_000;
1202        let mut relay: *mut PamojaLorawanRelay = ptr::null_mut();
1203        assert_eq!(
1204            pamoja_lorawan_relay_personalized(plan, session, &settings, 0, 0, &mut relay),
1205            PamojaStatus::Ok
1206        );
1207        assert_eq!(
1208            pamoja_lorawan_relay_start(relay, 0, 0, 0, 0, 0, 0),
1209            PamojaStatus::Ok
1210        );
1211        let root = root_wor_s_key(&SENSOR_NWK_S_KEY);
1212        assert_eq!(
1213            pamoja_lorawan_relay_trust(relay, 0, SENSOR_ADDR, root.as_ptr(), 0, 63, 0),
1214            PamojaStatus::Ok
1215        );
1216        (plan, session, relay)
1217    }
1218
1219    #[test]
1220    fn a_relay_scans_answers_and_forwards_across_the_boundary() {
1221        unsafe {
1222            let (plan, session, relay) = relay();
1223
1224            // The first scan is on the region's first wake-on-radio channel.
1225            let mut scan = PamojaLorawanScan {
1226                start_us: 0,
1227                frequency_hz: 0,
1228                data_rate: 0,
1229                channel: 0,
1230                link: crate::lorawan_device::link_out(pamoja_lora::LinkSettings::new(7, 125_000)),
1231                preamble_symbols: 0,
1232            };
1233            assert_eq!(
1234                pamoja_lorawan_relay_next_scan(relay, 0, &mut scan),
1235                PamojaStatus::Ok
1236            );
1237            assert_eq!(scan.frequency_hz, 865_100_000);
1238            assert_eq!(scan.data_rate, 3);
1239
1240            // A trusted sensor wakes it, and the relay answers and listens.
1241            let sensor = Session::new(SENSOR_ADDR, SENSOR_NWK_S_KEY, [0x99; 16]);
1242            let keys = WorKeys::derive(&root_wor_s_key(&SENSOR_NWK_S_KEY), SENSOR_ADDR);
1243            let uplink = pamoja_lorawan::relay::Carrier::new(868_100_000, 5);
1244            let wor = wor_uplink(
1245                &keys,
1246                SENSOR_ADDR,
1247                0,
1248                uplink,
1249                pamoja_lorawan::relay::Carrier::new(scan.frequency_hz, scan.data_rate),
1250            )
1251            .expect("a wake-on-radio frame");
1252
1253            let mut wake = core::mem::zeroed::<PamojaLorawanWake>();
1254            assert_eq!(
1255                pamoja_lorawan_relay_heard_wor(
1256                    relay,
1257                    &scan,
1258                    wor.as_ptr(),
1259                    wor.len(),
1260                    -90,
1261                    4,
1262                    scan.start_us + 900_000,
1263                    &mut wake,
1264                ),
1265                PamojaStatus::Ok
1266            );
1267            assert_eq!(wake.kind, PAMOJA_LORAWAN_WAKE_UPLINK);
1268            assert_eq!(wake.dev_addr, SENSOR_ADDR);
1269            assert_eq!(wake.has_ack, 1);
1270            assert_eq!(wake.ack_frequency_hz, 865_300_000);
1271            assert_eq!(wake.has_listen, 1);
1272            assert_eq!(wake.listen_frequency_hz, 868_100_000);
1273
1274            // The uplink behind it is forwarded to the network on port 226.
1275            let frame = sensor
1276                .encode_uplink(&Uplink::new(0, 1, b"21.5"))
1277                .expect("an uplink");
1278            let mut due_us = 0u64;
1279            assert_eq!(
1280                pamoja_lorawan_relay_heard_uplink(
1281                    relay,
1282                    frame.as_bytes().as_ptr(),
1283                    frame.as_bytes().len(),
1284                    -88,
1285                    6,
1286                    wake.listen_start_us + 60_000,
1287                    &mut due_us,
1288                ),
1289                PamojaStatus::Ok
1290            );
1291            let mut waiting = 0u64;
1292            assert_eq!(pamoja_lorawan_relay_forward_due(relay, &mut waiting), 1);
1293            assert_eq!(waiting, due_us);
1294
1295            let mut forwarded: *mut PamojaBuffer = ptr::null_mut();
1296            let mut transmission = core::mem::zeroed::<PamojaLorawanTransmission>();
1297            assert_eq!(
1298                pamoja_lorawan_relay_forward(relay, due_us, &mut forwarded, &mut transmission),
1299                PamojaStatus::Ok
1300            );
1301            let sent = buffer_bytes(forwarded);
1302            crate::pamoja_buffer_free(forwarded);
1303            let relay_session = Session::new(RELAY_ADDR, [0x11; 16], [0x22; 16]);
1304            let read = relay_session.decode(&sent, 0).expect("it decodes");
1305            assert_eq!(read.fport(), Some(LA_FPORT_RELAY));
1306
1307            // The network answers the sensor through the relay, which sends it on.
1308            let answer = sensor
1309                .encode_downlink(&Downlink::new(0, 1, b"ok"))
1310                .expect("a downlink");
1311            let reply = relay_session
1312                .encode_downlink(&Downlink::new(0, LA_FPORT_RELAY, answer.as_bytes()))
1313                .expect("a downlink");
1314            let mut kind = 0u8;
1315            let mut heard = core::mem::zeroed::<PamojaLorawanHeard>();
1316            let mut payload: *mut PamojaBuffer = ptr::null_mut();
1317            let mut out_frame: *mut PamojaBuffer = ptr::null_mut();
1318            let mut downlink = core::mem::zeroed::<PamojaLorawanRxr>();
1319            assert_eq!(
1320                pamoja_lorawan_relay_heard_in(
1321                    relay,
1322                    PAMOJA_LORAWAN_WINDOW_RX1,
1323                    reply.as_bytes().as_ptr(),
1324                    reply.as_bytes().len(),
1325                    7,
1326                    &mut kind,
1327                    &mut heard,
1328                    &mut payload,
1329                    &mut out_frame,
1330                    &mut downlink,
1331                ),
1332                PamojaStatus::Ok
1333            );
1334            assert_eq!(kind, PAMOJA_LORAWAN_RELAY_HEARD_DOWNLINK);
1335            assert_eq!(downlink.frequency_hz, 865_100_000);
1336            assert_eq!(buffer_bytes(out_frame), answer.as_bytes());
1337            crate::pamoja_buffer_free(payload);
1338            crate::pamoja_buffer_free(out_frame);
1339
1340            pamoja_lorawan_relay_free(relay);
1341            crate::lorawan::pamoja_lorawan_session_free(session);
1342            crate::lora_region::pamoja_lora_plan_free(plan);
1343        }
1344    }
1345
1346    #[test]
1347    fn a_stopped_relay_says_so_and_a_bad_channel_is_refused() {
1348        unsafe {
1349            let (plan, session, relay) = relay();
1350            pamoja_lorawan_relay_stop(relay);
1351            let mut scan = core::mem::zeroed::<PamojaLorawanScan>();
1352            assert_eq!(
1353                pamoja_lorawan_relay_next_scan(relay, 0, &mut scan),
1354                PamojaStatus::Other
1355            );
1356            let reason = pamoja_lorawan_relay_error(relay);
1357            assert!(!reason.is_null());
1358            crate::pamoja_string_free(reason);
1359
1360            assert_eq!(
1361                pamoja_lorawan_relay_start(relay, 7, 0, 0, 0, 0, 0),
1362                PamojaStatus::InvalidArgument,
1363                "a reserved scan period",
1364            );
1365            assert_eq!(
1366                pamoja_lorawan_relay_start(relay, 0, 9, 0, 0, 0, 0),
1367                PamojaStatus::InvalidArgument,
1368                "a channel the region does not define",
1369            );
1370
1371            pamoja_lorawan_relay_free(relay);
1372            crate::lorawan::pamoja_lorawan_session_free(session);
1373            crate::lora_region::pamoja_lora_plan_free(plan);
1374        }
1375    }
1376}