Skip to main content

pamoja_ffi/
lorawan_device.rs

1//! The C ABI for a LoRaWAN Class A end device, without a radio.
2//!
3//! [`PamojaLorawanEndDevice`] wraps `pamoja_lorawan::device::EndDevice`: it joins, chooses a
4//! channel and data rate for each uplink, says when and where to listen for the answer, reads
5//! what comes back, and does what the network's MAC commands ask. It owns no radio and no
6//! clock, so every call takes the time in microseconds and hands back what to put on the air.
7//!
8//! A device lives as long as the program may keep it, so it runs on a published channel plan:
9//! a region from [`pamoja_lora_plan_for_region`](crate::lora_region::pamoja_lora_plan_for_region)
10//! or a CN470-510 plan from
11//! [`pamoja_lora_plan_for_cn470`](crate::lora_region::pamoja_lora_plan_for_cn470). The plan
12//! handle may be freed once the device is made.
13//!
14//! A call that fails returns a status and records why on the device. Read it with
15//! [`pamoja_lorawan_end_device_error`], which says, for example, until when the air is not
16//! free.
17
18use std::ptr;
19
20use pamoja_lora::LinkSettings;
21use pamoja_lorawan::device::{
22    Battery, DeviceError, EndDevice, Heard, Next, ReceiveWindow, RelayExchange, RelayStatus, Saved,
23    Settings, StateError, Transmission, Window, WorNext, SAVED_LEN,
24};
25use pamoja_lorawan::relay::{RelayActivation, RelaySync, WOR_UPLINK_LEN};
26use pamoja_lorawan::LorawanError;
27
28use crate::lora::PamojaLoraLink;
29use crate::lora_region::PamojaLoraPlan;
30use crate::lorawan::{failed, PamojaLorawanDevice, PamojaLorawanSession};
31use crate::lorawan_link::{PAMOJA_LORAWAN_VERSION_1_0_3, PAMOJA_LORAWAN_VERSION_1_0_4};
32use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
33
34/// How many bytes a saved device state takes.
35pub const PAMOJA_LORAWAN_SAVED_LEN: usize = 1678;
36
37/// A device running from an external supply.
38pub const PAMOJA_LORAWAN_BATTERY_EXTERNAL: u8 = 0;
39/// A device reporting a battery level from 1, empty, to 254, full.
40pub const PAMOJA_LORAWAN_BATTERY_LEVEL: u8 = 1;
41/// A device that cannot measure its battery.
42pub const PAMOJA_LORAWAN_BATTERY_UNKNOWN: u8 = 2;
43
44/// A join accept: the device is on the network.
45pub const PAMOJA_LORAWAN_HEARD_JOINED: u8 = 0;
46/// A data frame for the device.
47pub const PAMOJA_LORAWAN_HEARD_DATA: u8 = 1;
48
49/// The first receive window, on the uplink's downlink channel.
50pub const PAMOJA_LORAWAN_WINDOW_RX1: u8 = 1;
51/// The second receive window, on the fixed frequency and data rate.
52pub const PAMOJA_LORAWAN_WINDOW_RX2: u8 = 2;
53/// The relay window, which a device under a relay opens last, TS011-1.0.1 chapter 7.
54pub const PAMOJA_LORAWAN_WINDOW_RXR: u8 = 3;
55
56/// Never send through a relay, TS011-1.0.1 table 40.
57pub const PAMOJA_LORAWAN_RELAY_DISABLED: u8 = 0;
58/// Always send through a relay.
59pub const PAMOJA_LORAWAN_RELAY_ENABLED: u8 = 1;
60/// Start using one after enough uplinks go unanswered.
61pub const PAMOJA_LORAWAN_RELAY_DYNAMIC: u8 = 2;
62/// Leave it to the device, which is where every device starts.
63pub const PAMOJA_LORAWAN_RELAY_DEVICE_CONTROLLED: u8 = 3;
64
65/// The device knows nothing of a relay, TS011-1.0.1 section 3.9.
66pub const PAMOJA_LORAWAN_RELAY_INITIALIZED: u8 = 0;
67/// It knows how a relay scans, but not when.
68pub const PAMOJA_LORAWAN_RELAY_UNSYNCHRONIZED: u8 = 1;
69/// It knows when the relay next scans.
70pub const PAMOJA_LORAWAN_RELAY_SYNCHRONIZED: u8 = 2;
71
72/// The uplink goes out at the time the exchange named.
73pub const PAMOJA_LORAWAN_WOR_NEXT_UPLINK: u8 = 0;
74/// The relay is woken again first, as the network's BackOff asks.
75pub const PAMOJA_LORAWAN_WOR_NEXT_WAKE_UP: u8 = 1;
76
77/// Send the same frame again, no sooner than the time given.
78pub const PAMOJA_LORAWAN_NEXT_REPEAT: u8 = 0;
79/// The uplink is finished.
80pub const PAMOJA_LORAWAN_NEXT_DONE: u8 = 1;
81/// A confirmed uplink went out every time it may without an acknowledgment.
82pub const PAMOJA_LORAWAN_NEXT_UNACKNOWLEDGED: u8 = 2;
83/// The join got no answer; join again with a new nonce, no sooner than the time given.
84pub const PAMOJA_LORAWAN_NEXT_JOIN_AGAIN: u8 = 3;
85
86/// The last call succeeded.
87pub const PAMOJA_LORAWAN_DEVICE_OK: u8 = 0;
88/// The plan defines more channels than a device keeps.
89pub const PAMOJA_LORAWAN_DEVICE_TOO_MANY_CHANNELS: u8 = 1;
90/// A device activated by personalization has nothing to join with.
91pub const PAMOJA_LORAWAN_DEVICE_NO_CREDENTIALS: u8 = 2;
92/// The device has not joined.
93pub const PAMOJA_LORAWAN_DEVICE_NOT_JOINED: u8 = 3;
94/// A transmission is still waiting on its receive windows.
95pub const PAMOJA_LORAWAN_DEVICE_BUSY: u8 = 4;
96/// There is no transmission waiting on its windows or due to repeat.
97pub const PAMOJA_LORAWAN_DEVICE_NOTHING_PENDING: u8 = 5;
98/// The air is not free until `until_us`.
99pub const PAMOJA_LORAWAN_DEVICE_WAIT: u8 = 6;
100/// No enabled channel carries the data rate.
101pub const PAMOJA_LORAWAN_DEVICE_NO_CHANNEL: u8 = 7;
102/// The data rate `data_rate` is not a LoRa one the device can use.
103pub const PAMOJA_LORAWAN_DEVICE_DATA_RATE: u8 = 8;
104/// The payload does not fit; `max` bytes do.
105pub const PAMOJA_LORAWAN_DEVICE_PAYLOAD_TOO_LONG: u8 = 9;
106/// The uplink frame counter is spent, and the device has to join again.
107pub const PAMOJA_LORAWAN_DEVICE_COUNTER_EXHAUSTED: u8 = 10;
108/// The frame did not decode, or port 0 was asked for an application payload.
109pub const PAMOJA_LORAWAN_DEVICE_FRAME: u8 = 11;
110/// The frame is addressed to another device.
111pub const PAMOJA_LORAWAN_DEVICE_FOREIGN: u8 = 12;
112/// The frame repeats or precedes the last downlink the device accepted.
113pub const PAMOJA_LORAWAN_DEVICE_REPLAYED: u8 = 13;
114/// The frame counter jumped further ahead than the device follows.
115pub const PAMOJA_LORAWAN_DEVICE_COUNTER_GAP: u8 = 14;
116/// A join accept carries settings the region does not allow.
117pub const PAMOJA_LORAWAN_DEVICE_REFUSED: u8 = 15;
118/// A saved state was not resumed; `state` says why.
119pub const PAMOJA_LORAWAN_DEVICE_STATE: u8 = 16;
120
121/// A saved state of the wrong length.
122pub const PAMOJA_LORAWAN_STATE_LENGTH: u8 = 1;
123/// A saved state that is corrupt.
124pub const PAMOJA_LORAWAN_STATE_CORRUPT: u8 = 2;
125/// A saved state in a format this build does not read, `format`.
126pub const PAMOJA_LORAWAN_STATE_FORMAT: u8 = 3;
127/// A saved state from another channel plan.
128pub const PAMOJA_LORAWAN_STATE_PLAN: u8 = 4;
129
130/// What a device's radio can do, and how it takes part.
131///
132/// Fill one with [`pamoja_lorawan_device_settings`], then change what differs.
133#[repr(C)]
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub struct PamojaLorawanDeviceSettings {
136    /// The lowest frequency the radio and its front end can use, in hertz.
137    pub lowest_hz: u32,
138    /// The highest, in hertz.
139    pub highest_hz: u32,
140    /// A seed for the random choices of channel and retry delay, ideally from a hardware
141    /// random source. The device identifier is mixed in.
142    pub seed: u32,
143    /// The link layer revision, [`PAMOJA_LORAWAN_VERSION_1_0_3`] or
144    /// [`PAMOJA_LORAWAN_VERSION_1_0_4`].
145    pub version: u8,
146    /// `1` to let the network manage the data rate and power.
147    pub adr: u8,
148    /// The lowest power the radio puts out, conducted, in dBm.
149    pub min_output_dbm: i8,
150    /// The highest, conducted, in dBm.
151    pub max_output_dbm: i8,
152    /// The antenna gain less the cable and connector losses, in dB.
153    pub antenna_gain_db: i8,
154    /// `1` to hold the device to the region's sub-band duty cycles.
155    pub regional_duty_cycle: u8,
156    /// `1` to size payloads for a path through a relay.
157    pub behind_repeater: u8,
158}
159
160/// When and where to listen for a downlink.
161#[repr(C)]
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub struct PamojaLorawanWindow {
164    /// How long after the end of the transmission the window opens, in microseconds.
165    pub delay_us: u32,
166    /// The carrier, in hertz.
167    pub frequency_hz: u32,
168    /// The LoRa settings to listen with: no payload CRC, and inverted IQ, as RP002-1.0.5
169    /// table 112 has for a downlink.
170    pub link: PamojaLoraLink,
171    /// The downlink data rate, as the region numbers them.
172    pub data_rate: u8,
173}
174
175/// The wake-on-radio exchange an uplink under a relay goes out behind, TS011-1.0.1
176/// section 5.
177#[repr(C)]
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct PamojaLorawanRelayExchange {
180    /// When to start sending the frame that wakes the relay, in microseconds.
181    pub wake_up_start_us: u64,
182    /// How long that frame holds the air, in microseconds.
183    pub wake_up_airtime_us: u64,
184    /// When the relay's acknowledgment would start arriving, in microseconds.
185    pub ack_start_us: u64,
186    /// How long it would last, in microseconds.
187    pub ack_airtime_us: u64,
188    /// When the uplink itself goes out, in microseconds, whether or not the acknowledgment
189    /// arrives.
190    pub uplink_start_us: u64,
191    /// The frame that wakes the relay: five bytes ahead of a join request, fifteen ahead of
192    /// an uplink.
193    pub wake_up_frame: [u8; 15],
194    /// How many of those bytes to send.
195    pub wake_up_len: u8,
196    /// Where the frame goes, in hertz.
197    pub wake_up_frequency_hz: u32,
198    /// The data rate it goes out at.
199    pub wake_up_data_rate: u8,
200    /// Its LoRa settings, with the preamble this frame needs, sent with inverted IQ.
201    pub wake_up_link: PamojaLoraLink,
202    /// The power to ask of the radio for it, conducted, in dBm.
203    pub wake_up_output_dbm: i8,
204    /// `1` when an acknowledgment is expected at all; a join request is never acknowledged.
205    pub has_ack: u8,
206    /// Where the acknowledgment would arrive, in hertz.
207    pub ack_frequency_hz: u32,
208    /// The data rate it would arrive at.
209    pub ack_data_rate: u8,
210    /// Its LoRa settings.
211    pub ack_link: PamojaLoraLink,
212    /// The relay window, timed from the end of the uplink like the other two.
213    pub rxr: PamojaLorawanWindow,
214}
215
216/// What a relay's acknowledgment said about itself, TS011-1.0.1 table 14.
217#[repr(C)]
218#[derive(Clone, Copy, Debug, PartialEq, Eq)]
219pub struct PamojaLorawanRelayStatus {
220    /// How often it scans, as table 18 codes it.
221    pub cad_periodicity: u8,
222    /// How accurate its crystal is, as table 17 codes it.
223    pub xtal_accuracy: u8,
224    /// How long it takes to start receiving, as table 15 codes it.
225    pub cad_to_rx: u8,
226    /// The data rate it forwards at, which bounds what the device may send.
227    pub relay_data_rate: u8,
228    /// Whether it will forward, as table 16 codes it.
229    pub forward: u8,
230}
231
232/// A frame to put on the air, and where to listen afterward.
233///
234/// The frame's bytes cross beside it as a [`PamojaBuffer`].
235#[repr(C)]
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237pub struct PamojaLorawanTransmission {
238    /// How long the frame holds the air, in microseconds.
239    pub airtime_us: u64,
240    /// The carrier, in hertz.
241    pub frequency_hz: u32,
242    /// The first receive window.
243    pub rx1: PamojaLorawanWindow,
244    /// The second, which opens only if nothing for this device arrived in the first.
245    pub rx2: PamojaLorawanWindow,
246    /// The LoRa settings: an eight-symbol preamble, an explicit header and a payload CRC,
247    /// sent with standard IQ.
248    pub link: PamojaLoraLink,
249    /// The data rate, as the region numbers them.
250    pub data_rate: u8,
251    /// The power to ask of the radio, conducted, in dBm.
252    pub output_dbm: i8,
253    /// `1` if the application payload went out in this frame; `0` if the answers the
254    /// device owed left no room, and it has to be sent again.
255    pub carries_payload: u8,
256    /// `1` when this frame goes out behind a wake-on-radio frame, under a relay.
257    pub has_relay: u8,
258    /// The wake-on-radio exchange, when it does.
259    pub relay: PamojaLorawanRelayExchange,
260}
261
262/// What a frame heard in a receive window turned out to be.
263///
264/// For a data frame the payload crosses beside it as a [`PamojaBuffer`].
265#[repr(C)]
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub struct PamojaLorawanHeard {
268    /// For a join, the address the network gave the device.
269    pub dev_addr: u32,
270    /// For a time answer, whole seconds since the GPS epoch.
271    pub gps_seconds: u32,
272    /// [`PAMOJA_LORAWAN_HEARD_JOINED`] or [`PAMOJA_LORAWAN_HEARD_DATA`].
273    pub kind: u8,
274    /// `1` if the payload arrived on an application port.
275    pub has_port: u8,
276    /// The application port.
277    pub port: u8,
278    /// `1` if the network acknowledged the confirmed uplink this answered.
279    pub acknowledged: u8,
280    /// `1` if the network asked for this downlink to be acknowledged.
281    pub confirmed: u8,
282    /// `1` if the network has more waiting.
283    pub more_pending: u8,
284    /// `1` if the downlink answered a link check.
285    pub has_link_check: u8,
286    /// How far above the demodulation floor the best gateway heard the check, in dB.
287    pub margin_db: u8,
288    /// How many gateways heard it.
289    pub gateways: u8,
290    /// `1` if the downlink answered a time request.
291    pub has_device_time: u8,
292    /// The fraction of a second, in 256ths.
293    pub fraction: u8,
294}
295
296/// What to do once both receive windows closed with nothing for the device.
297#[repr(C)]
298#[derive(Clone, Copy, Debug, PartialEq, Eq)]
299pub struct PamojaLorawanNext {
300    /// For a repeat or another join, the earliest time to send, in microseconds.
301    pub not_before_us: u64,
302    /// One of the `PAMOJA_LORAWAN_NEXT_*` constants.
303    pub kind: u8,
304}
305
306/// Why a device's last call failed.
307#[repr(C)]
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
309pub struct PamojaLorawanDeviceError {
310    /// For [`PAMOJA_LORAWAN_DEVICE_WAIT`], the earliest time to try again, in microseconds.
311    pub until_us: u64,
312    /// For [`PAMOJA_LORAWAN_DEVICE_PAYLOAD_TOO_LONG`], the most the frame carries, and for
313    /// [`PAMOJA_LORAWAN_DEVICE_TOO_MANY_CHANNELS`], the most channels a device keeps.
314    pub max: u32,
315    /// One of the `PAMOJA_LORAWAN_DEVICE_*` constants.
316    pub kind: u8,
317    /// For [`PAMOJA_LORAWAN_DEVICE_DATA_RATE`], the data rate.
318    pub data_rate: u8,
319    /// For [`PAMOJA_LORAWAN_DEVICE_STATE`], one of the `PAMOJA_LORAWAN_STATE_*` constants.
320    pub state: u8,
321    /// For [`PAMOJA_LORAWAN_STATE_FORMAT`], the format the state was saved in.
322    pub format: u8,
323}
324
325/// Where a device stands, read in one call.
326#[repr(C)]
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328pub struct PamojaLorawanEndDeviceStatus {
329    /// The address the device is on the network by, meaningful when `joined` is `1`.
330    pub dev_addr: u32,
331    /// The next uplink frame counter.
332    pub fcnt_up: u32,
333    /// The last downlink frame counter accepted, meaningful when `has_fcnt_down` is `1`.
334    pub fcnt_down: u32,
335    /// Where the second receive window listens, in hertz.
336    pub rx2_frequency_hz: u32,
337    /// The delay from the end of an uplink to the first receive window, in microseconds.
338    pub receive_delay_us: u32,
339    /// The lowest frequency the device transmits or listens on.
340    pub lowest_hz: u32,
341    /// The highest.
342    pub highest_hz: u32,
343    /// How many channels are enabled.
344    pub channel_count: u16,
345    /// `1` once joined, or from the start for a personalized device.
346    pub joined: u8,
347    /// The data rate the next uplink goes out at, before any back-off step.
348    pub data_rate: u8,
349    /// `1` once a downlink has been accepted.
350    pub has_fcnt_down: u8,
351    /// How many times each uplink goes out, NbTrans.
352    pub transmissions: u8,
353    /// The data rate the second receive window listens at.
354    pub rx2_data_rate: u8,
355}
356
357/// A channel a device may send on.
358#[repr(C)]
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
360pub struct PamojaLorawanChannel {
361    /// Where uplinks go out, in hertz.
362    pub uplink_hz: u32,
363    /// Where the first receive window listens, in hertz.
364    pub downlink_hz: u32,
365    /// The slowest data rate the channel carries.
366    pub min_data_rate: u8,
367    /// The fastest.
368    pub max_data_rate: u8,
369}
370
371/// An opaque handle to a LoRaWAN Class A end device.
372///
373/// Release it with [`pamoja_lorawan_end_device_free`].
374pub struct PamojaLorawanEndDevice {
375    device: EndDevice<'static>,
376    error: Option<DeviceError>,
377}
378
379/// What a device is doing now, as C sees it.
380pub(crate) fn status_out(device: &EndDevice<'static>) -> PamojaLorawanEndDeviceStatus {
381    let (rx2_frequency_hz, rx2_data_rate) = device.rx2();
382    let (lowest_hz, highest_hz) = device.frequency_span();
383    PamojaLorawanEndDeviceStatus {
384        dev_addr: device.dev_addr().unwrap_or(0),
385        fcnt_up: device.fcnt_up(),
386        fcnt_down: device.fcnt_down().unwrap_or(0),
387        rx2_frequency_hz,
388        receive_delay_us: device.receive_delay_us(),
389        lowest_hz,
390        highest_hz,
391        channel_count: device.channels().count() as u16,
392        joined: u8::from(device.is_joined()),
393        data_rate: device.data_rate(),
394        has_fcnt_down: u8::from(device.fcnt_down().is_some()),
395        transmissions: device.transmissions(),
396        rx2_data_rate,
397    }
398}
399
400/// Fills in the settings of a typical node with a radio's output power range.
401///
402/// The rest start as TS001-1.0.4, adaptive data rate on, an antenna with no gain over its
403/// cable, a radio that tunes 137 to 1020 MHz as an SX1276 does, the region's duty cycle
404/// kept, no repeater in the path, and a seed of zero.
405///
406/// # Arguments
407///
408/// * `min_output_dbm` - the lowest power the radio puts out, conducted.
409/// * `max_output_dbm` - the highest, conducted.
410/// * `out_settings` - receives the settings.
411///
412/// # Returns
413///
414/// [`PamojaStatus::Ok`] on success.
415///
416/// # Errors
417///
418/// Returns [`PamojaStatus::InvalidArgument`] if `out_settings` is null.
419///
420/// # Safety
421///
422/// `out_settings` must point to a writable [`PamojaLorawanDeviceSettings`].
423#[no_mangle]
424pub unsafe extern "C" fn pamoja_lorawan_device_settings(
425    min_output_dbm: i8,
426    max_output_dbm: i8,
427    out_settings: *mut PamojaLorawanDeviceSettings,
428) -> PamojaStatus {
429    if out_settings.is_null() {
430        set_last_error("out_settings must not be null".to_owned());
431        return PamojaStatus::InvalidArgument;
432    }
433    *out_settings = PamojaLorawanDeviceSettings {
434        lowest_hz: 137_000_000,
435        highest_hz: 1_020_000_000,
436        seed: 0,
437        version: PAMOJA_LORAWAN_VERSION_1_0_4,
438        adr: 1,
439        min_output_dbm,
440        max_output_dbm,
441        antenna_gain_db: 0,
442        regional_duty_cycle: 1,
443        behind_repeater: 0,
444    };
445    PamojaStatus::Ok
446}
447
448/// Rebuilds the Rust settings from the fields that crossed the boundary.
449fn settings(crossed: &PamojaLorawanDeviceSettings) -> Result<Settings, PamojaStatus> {
450    let version = match crossed.version {
451        PAMOJA_LORAWAN_VERSION_1_0_3 => pamoja_lorawan::Version::V1_0_3,
452        PAMOJA_LORAWAN_VERSION_1_0_4 => pamoja_lorawan::Version::V1_0_4,
453        other => {
454            set_last_error(format!("{other} is not a LoRaWAN version"));
455            return Err(PamojaStatus::InvalidArgument);
456        }
457    };
458    if crossed.min_output_dbm > crossed.max_output_dbm || crossed.lowest_hz > crossed.highest_hz {
459        set_last_error("the output power and tuning ranges must run from low to high".to_owned());
460        return Err(PamojaStatus::InvalidArgument);
461    }
462    let mut settings = Settings::new(crossed.min_output_dbm, crossed.max_output_dbm)
463        .with_version(version)
464        .with_adr(crossed.adr != 0)
465        .with_antenna_gain(crossed.antenna_gain_db)
466        .with_tuning_range(crossed.lowest_hz, crossed.highest_hz)
467        .with_seed(crossed.seed);
468    if crossed.regional_duty_cycle == 0 {
469        settings = settings.without_regional_duty_cycle();
470    }
471    if crossed.behind_repeater != 0 {
472        settings = settings.behind_repeater();
473    }
474    Ok(settings)
475}
476
477/// Reads the published plan and settings a new device is made from.
478///
479/// # Safety
480///
481/// `plan` must be a live plan handle or null, and `settings` must point to a readable
482/// [`PamojaLorawanDeviceSettings`] or be null.
483pub(crate) unsafe fn made_from(
484    plan: *const PamojaLoraPlan,
485    settings: *const PamojaLorawanDeviceSettings,
486) -> Result<(&'static pamoja_lora::region::ChannelPlan<'static>, Settings), PamojaStatus> {
487    let (Some(plan), Some(crossed)) = (plan.as_ref(), settings.as_ref()) else {
488        set_last_error("plan and settings must not be null".to_owned());
489        return Err(PamojaStatus::InvalidArgument);
490    };
491    let Some(published) = plan.published() else {
492        set_last_error(
493            "a device runs on a published plan, from pamoja_lora_plan_for_region or pamoja_lora_plan_for_cn470"
494                .to_owned(),
495        );
496        return Err(PamojaStatus::InvalidArgument);
497    };
498    Ok((published, self::settings(crossed)?))
499}
500
501/// Hands a made device back through an out pointer.
502///
503/// # Safety
504///
505/// `slot` must be a writable pointer slot.
506unsafe fn made(
507    result: Result<EndDevice<'static>, DeviceError>,
508    fcnt_up: u32,
509    has_fcnt_down: u8,
510    fcnt_down: u32,
511    slot: &mut *mut PamojaLorawanEndDevice,
512) -> PamojaStatus {
513    match result {
514        Ok(device) => {
515            let down = (has_fcnt_down != 0).then_some(fcnt_down);
516            *slot = Box::into_raw(Box::new(PamojaLorawanEndDevice {
517                device: device.with_frame_counters(fcnt_up, down),
518                error: None,
519            }));
520            PamojaStatus::Ok
521        }
522        Err(error) => {
523            set_last_error(error.to_string());
524            status_of(error)
525        }
526    }
527}
528
529/// Makes a device that joins over the air.
530///
531/// # Arguments
532///
533/// * `plan` - a published channel plan.
534/// * `credentials` - the device's identifiers and root key, which the device copies.
535/// * `settings` - what its radio can do.
536/// * `fcnt_up` - the next uplink frame counter, 0 for a device that has never sent. A join
537///   starts it over.
538/// * `has_fcnt_down` - `1` if `fcnt_down` holds the last downlink counter accepted.
539/// * `fcnt_down` - that counter.
540/// * `out_device` - receives the device.
541///
542/// # Returns
543///
544/// [`PamojaStatus::Ok`] on success, with `*out_device` set to a handle the caller must
545/// release with [`pamoja_lorawan_end_device_free`].
546///
547/// # Errors
548///
549/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null, the plan was built rather
550/// than published, the settings name no version or run backward, or the plan defines more
551/// channels than a device keeps.
552///
553/// # Safety
554///
555/// `plan` and `credentials` must be live handles, `settings` must point to a readable
556/// [`PamojaLorawanDeviceSettings`], and `out_device` to a writable pointer.
557#[no_mangle]
558pub unsafe extern "C" fn pamoja_lorawan_end_device_new(
559    plan: *const PamojaLoraPlan,
560    credentials: *const PamojaLorawanDevice,
561    settings: *const PamojaLorawanDeviceSettings,
562    fcnt_up: u32,
563    has_fcnt_down: u8,
564    fcnt_down: u32,
565    out_device: *mut *mut PamojaLorawanEndDevice,
566) -> PamojaStatus {
567    if out_device.is_null() {
568        set_last_error("out_device must not be null".to_owned());
569        return PamojaStatus::InvalidArgument;
570    }
571    let slot = &mut *out_device;
572    *slot = ptr::null_mut();
573    let Some(credentials) = credentials.as_ref() else {
574        set_last_error("credentials must not be null".to_owned());
575        return PamojaStatus::InvalidArgument;
576    };
577    let (plan, settings) = match made_from(plan, settings) {
578        Ok(pair) => pair,
579        Err(status) => return status,
580    };
581    made(
582        EndDevice::new(plan, credentials.device.clone(), settings),
583        fcnt_up,
584        has_fcnt_down,
585        fcnt_down,
586        slot,
587    )
588}
589
590/// Makes a device activated by personalization, with its session provisioned.
591///
592/// Such a device never resets its frame counters, TS001-1.0.4 section 4.3.1.5, so a device
593/// that lost power passes the ones it kept.
594///
595/// # Arguments
596///
597/// * `plan` - a published channel plan.
598/// * `session` - the address and session keys it was provisioned with, which the device
599///   copies.
600/// * `settings` - what its radio can do.
601/// * `fcnt_up` - the next uplink frame counter.
602/// * `has_fcnt_down` - `1` if `fcnt_down` holds the last downlink counter accepted.
603/// * `fcnt_down` - that counter.
604/// * `out_device` - receives the device.
605///
606/// # Returns
607///
608/// [`PamojaStatus::Ok`] on success, with `*out_device` set to a handle the caller must
609/// release with [`pamoja_lorawan_end_device_free`].
610///
611/// # Errors
612///
613/// As [`pamoja_lorawan_end_device_new`].
614///
615/// # Safety
616///
617/// `plan` and `session` must be live handles, `settings` must point to a readable
618/// [`PamojaLorawanDeviceSettings`], and `out_device` to a writable pointer.
619#[no_mangle]
620pub unsafe extern "C" fn pamoja_lorawan_end_device_personalized(
621    plan: *const PamojaLoraPlan,
622    session: *const PamojaLorawanSession,
623    settings: *const PamojaLorawanDeviceSettings,
624    fcnt_up: u32,
625    has_fcnt_down: u8,
626    fcnt_down: u32,
627    out_device: *mut *mut PamojaLorawanEndDevice,
628) -> PamojaStatus {
629    if out_device.is_null() {
630        set_last_error("out_device must not be null".to_owned());
631        return PamojaStatus::InvalidArgument;
632    }
633    let slot = &mut *out_device;
634    *slot = ptr::null_mut();
635    let Some(session) = session.as_ref() else {
636        set_last_error("session must not be null".to_owned());
637        return PamojaStatus::InvalidArgument;
638    };
639    let (plan, settings) = match made_from(plan, settings) {
640        Ok(pair) => pair,
641        Err(status) => return status,
642    };
643    made(
644        EndDevice::personalized(plan, session.session, settings),
645        fcnt_up,
646        has_fcnt_down,
647        fcnt_down,
648        slot,
649    )
650}
651
652/// Releases a device handle.
653///
654/// Passing null is a no-op.
655///
656/// # Safety
657///
658/// `device` must be a handle from a call that made one and has not already been freed, or
659/// null. After this call it must not be used again.
660#[no_mangle]
661pub unsafe extern "C" fn pamoja_lorawan_end_device_free(device: *mut PamojaLorawanEndDevice) {
662    if !device.is_null() {
663        drop(Box::from_raw(device));
664    }
665}
666
667/// Reads why a device's last call failed.
668///
669/// # Arguments
670///
671/// * `device` - the device.
672/// * `out_error` - receives the reason, with `kind` [`PAMOJA_LORAWAN_DEVICE_OK`] when the
673///   last call succeeded.
674///
675/// # Returns
676///
677/// [`PamojaStatus::Ok`] on success.
678///
679/// # Errors
680///
681/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null.
682///
683/// # Safety
684///
685/// `device` must be a live handle and `out_error` must point to a writable
686/// [`PamojaLorawanDeviceError`].
687#[no_mangle]
688pub unsafe extern "C" fn pamoja_lorawan_end_device_error(
689    device: *const PamojaLorawanEndDevice,
690    out_error: *mut PamojaLorawanDeviceError,
691) -> PamojaStatus {
692    let (Some(device), false) = (device.as_ref(), out_error.is_null()) else {
693        set_last_error("device and out_error must not be null".to_owned());
694        return PamojaStatus::InvalidArgument;
695    };
696    *out_error = error_out(device.error);
697    PamojaStatus::Ok
698}
699
700/// Describes a device error in the shape that crosses the boundary.
701fn error_out(error: Option<DeviceError>) -> PamojaLorawanDeviceError {
702    let mut out = PamojaLorawanDeviceError {
703        until_us: 0,
704        max: 0,
705        kind: PAMOJA_LORAWAN_DEVICE_OK,
706        data_rate: 0,
707        state: 0,
708        format: 0,
709    };
710    let Some(error) = error else {
711        return out;
712    };
713    out.kind = match error {
714        DeviceError::TooManyChannels { max } => {
715            out.max = max as u32;
716            PAMOJA_LORAWAN_DEVICE_TOO_MANY_CHANNELS
717        }
718        DeviceError::NoCredentials => PAMOJA_LORAWAN_DEVICE_NO_CREDENTIALS,
719        DeviceError::NotJoined => PAMOJA_LORAWAN_DEVICE_NOT_JOINED,
720        DeviceError::Busy => PAMOJA_LORAWAN_DEVICE_BUSY,
721        DeviceError::NothingPending => PAMOJA_LORAWAN_DEVICE_NOTHING_PENDING,
722        DeviceError::Wait { until_us } => {
723            out.until_us = until_us;
724            PAMOJA_LORAWAN_DEVICE_WAIT
725        }
726        DeviceError::NoChannel => PAMOJA_LORAWAN_DEVICE_NO_CHANNEL,
727        DeviceError::DataRate(rate) => {
728            out.data_rate = rate;
729            PAMOJA_LORAWAN_DEVICE_DATA_RATE
730        }
731        DeviceError::PayloadTooLong { max } => {
732            out.max = max as u32;
733            PAMOJA_LORAWAN_DEVICE_PAYLOAD_TOO_LONG
734        }
735        DeviceError::CounterExhausted => PAMOJA_LORAWAN_DEVICE_COUNTER_EXHAUSTED,
736        DeviceError::Frame(_) => PAMOJA_LORAWAN_DEVICE_FRAME,
737        DeviceError::Foreign => PAMOJA_LORAWAN_DEVICE_FOREIGN,
738        DeviceError::Replayed => PAMOJA_LORAWAN_DEVICE_REPLAYED,
739        DeviceError::CounterGap => PAMOJA_LORAWAN_DEVICE_COUNTER_GAP,
740        DeviceError::Refused => PAMOJA_LORAWAN_DEVICE_REFUSED,
741        DeviceError::State(state) => {
742            out.state = match state {
743                StateError::Length => PAMOJA_LORAWAN_STATE_LENGTH,
744                StateError::Corrupt => PAMOJA_LORAWAN_STATE_CORRUPT,
745                StateError::Format(format) => {
746                    out.format = format;
747                    PAMOJA_LORAWAN_STATE_FORMAT
748                }
749                StateError::Plan => PAMOJA_LORAWAN_STATE_PLAN,
750            };
751            PAMOJA_LORAWAN_DEVICE_STATE
752        }
753    };
754    out
755}
756
757/// The status a device error crosses as.
758fn status_of(error: DeviceError) -> PamojaStatus {
759    match error {
760        DeviceError::TooManyChannels { .. }
761        | DeviceError::NoCredentials
762        | DeviceError::DataRate(_)
763        | DeviceError::PayloadTooLong { .. }
764        | DeviceError::State(StateError::Length) => PamojaStatus::InvalidArgument,
765        DeviceError::Frame(frame) => failed(frame),
766        DeviceError::Replayed | DeviceError::CounterGap => PamojaStatus::Auth,
767        DeviceError::Refused | DeviceError::State(_) => PamojaStatus::Codec,
768        DeviceError::CounterExhausted => PamojaStatus::Closed,
769        DeviceError::NotJoined
770        | DeviceError::Busy
771        | DeviceError::NothingPending
772        | DeviceError::Wait { .. }
773        | DeviceError::NoChannel
774        | DeviceError::Foreign => PamojaStatus::Other,
775    }
776}
777
778impl PamojaLorawanEndDevice {
779    /// Records how a call went and returns the status it crosses as.
780    fn settle<T>(&mut self, result: Result<T, DeviceError>) -> Result<T, PamojaStatus> {
781        match result {
782            Ok(value) => {
783                self.error = None;
784                Ok(value)
785            }
786            Err(error) => {
787                self.error = Some(error);
788                set_last_error(error.to_string());
789                Err(status_of(error))
790            }
791        }
792    }
793}
794
795/// Converts link settings into the shape that crosses the boundary.
796pub(crate) fn link_out(link: pamoja_lora::LinkSettings) -> PamojaLoraLink {
797    PamojaLoraLink {
798        bandwidth_hz: link.bandwidth_hz(),
799        preamble_symbols: link.preamble_symbols(),
800        spreading_factor: link.spreading_factor(),
801        coding_rate_denominator: link.coding_rate_denominator(),
802        explicit_header: u8::from(link.explicit_header()),
803        crc: u8::from(link.crc()),
804    }
805}
806
807/// Converts a receive window into the shape that crosses the boundary.
808fn window_out(window: Window) -> PamojaLorawanWindow {
809    PamojaLorawanWindow {
810        delay_us: window.delay_us,
811        frequency_hz: window.frequency_hz,
812        link: link_out(window.link),
813        data_rate: window.data_rate,
814    }
815}
816
817/// Hands a transmission back through its two out pointers.
818///
819/// # Safety
820///
821/// Both pointers must be writable.
822pub(crate) unsafe fn transmitted(
823    transmission: Transmission,
824    out_frame: *mut *mut PamojaBuffer,
825    out_transmission: *mut PamojaLorawanTransmission,
826) {
827    *out_frame = PamojaBuffer::into_raw(transmission.frame.as_bytes().to_vec());
828    *out_transmission = PamojaLorawanTransmission {
829        airtime_us: transmission.airtime_us,
830        frequency_hz: transmission.frequency_hz,
831        rx1: window_out(transmission.rx1),
832        rx2: window_out(transmission.rx2),
833        link: link_out(transmission.link),
834        data_rate: transmission.data_rate,
835        output_dbm: transmission.output_dbm,
836        carries_payload: u8::from(transmission.carries_payload),
837        has_relay: u8::from(transmission.relay.is_some()),
838        relay: transmission.relay.map_or_else(empty_exchange, exchange_out),
839    };
840}
841
842/// The wake-on-radio exchange of a relayed uplink, as C sees it.
843fn exchange_out(exchange: RelayExchange) -> PamojaLorawanRelayExchange {
844    let mut wake_up_frame = [0u8; WOR_UPLINK_LEN];
845    let frame = exchange.wake_up.frame();
846    wake_up_frame[..frame.len()].copy_from_slice(frame);
847    PamojaLorawanRelayExchange {
848        wake_up_start_us: exchange.wake_up.start_us,
849        wake_up_airtime_us: exchange.wake_up.airtime_us,
850        ack_start_us: exchange.ack.map_or(0, |ack| ack.start_us),
851        ack_airtime_us: exchange.ack.map_or(0, |ack| ack.airtime_us),
852        uplink_start_us: exchange.uplink_start_us,
853        wake_up_frame,
854        wake_up_len: frame.len() as u8,
855        wake_up_frequency_hz: exchange.wake_up.carrier.frequency_hz,
856        wake_up_data_rate: exchange.wake_up.carrier.data_rate,
857        wake_up_link: link_out(exchange.wake_up.link),
858        wake_up_output_dbm: exchange.wake_up.output_dbm,
859        has_ack: u8::from(exchange.ack.is_some()),
860        ack_frequency_hz: exchange.ack.map_or(0, |ack| ack.carrier.frequency_hz),
861        ack_data_rate: exchange.ack.map_or(0, |ack| ack.carrier.data_rate),
862        ack_link: exchange
863            .ack
864            .map_or_else(|| link_out(exchange.wake_up.link), |ack| link_out(ack.link)),
865        rxr: window_out(exchange.rxr),
866    }
867}
868
869/// The exchange field of a frame that goes out on its own.
870fn empty_exchange() -> PamojaLorawanRelayExchange {
871    PamojaLorawanRelayExchange {
872        wake_up_start_us: 0,
873        wake_up_airtime_us: 0,
874        ack_start_us: 0,
875        ack_airtime_us: 0,
876        uplink_start_us: 0,
877        wake_up_frame: [0; WOR_UPLINK_LEN],
878        wake_up_len: 0,
879        wake_up_frequency_hz: 0,
880        wake_up_data_rate: 0,
881        wake_up_link: link_out(LinkSettings::new(7, 125_000)),
882        wake_up_output_dbm: 0,
883        has_ack: 0,
884        ack_frequency_hz: 0,
885        ack_data_rate: 0,
886        ack_link: link_out(LinkSettings::new(7, 125_000)),
887        rxr: PamojaLorawanWindow {
888            delay_us: 0,
889            frequency_hz: 0,
890            link: link_out(LinkSettings::new(7, 125_000)),
891            data_rate: 0,
892        },
893    }
894}
895
896/// Runs a call that puts a frame on the air.
897///
898/// # Safety
899///
900/// `device` must be a live handle or null, and the out pointers writable or null.
901unsafe fn transmit(
902    device: *mut PamojaLorawanEndDevice,
903    out_frame: *mut *mut PamojaBuffer,
904    out_transmission: *mut PamojaLorawanTransmission,
905    call: impl FnOnce(&mut EndDevice<'static>) -> Result<Transmission, DeviceError>,
906) -> PamojaStatus {
907    let (Some(device), false, false) = (
908        device.as_mut(),
909        out_frame.is_null(),
910        out_transmission.is_null(),
911    ) else {
912        set_last_error("device, out_frame and out_transmission must not be null".to_owned());
913        return PamojaStatus::InvalidArgument;
914    };
915    *out_frame = ptr::null_mut();
916    let result = call(&mut device.device);
917    match device.settle(result) {
918        Ok(transmission) => {
919            transmitted(transmission, out_frame, out_transmission);
920            PamojaStatus::Ok
921        }
922        Err(status) => status,
923    }
924}
925
926/// Builds a join request.
927///
928/// # Arguments
929///
930/// * `device` - the device.
931/// * `dev_nonce` - a nonce this device has never used with its join identifier.
932/// * `now_us` - the time, in microseconds.
933/// * `out_frame` - receives the frame to transmit.
934/// * `out_transmission` - receives where and how to transmit it, and the accept windows.
935///
936/// # Returns
937///
938/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller must release
939/// with [`pamoja_buffer_free`](crate::pamoja_buffer_free).
940///
941/// # Errors
942///
943/// Returns a failing status, with the reason on [`pamoja_lorawan_end_device_error`]: no
944/// credentials, busy, or a wait for the air.
945///
946/// # Safety
947///
948/// `device` must be a live handle and the out pointers writable.
949#[no_mangle]
950pub unsafe extern "C" fn pamoja_lorawan_end_device_join(
951    device: *mut PamojaLorawanEndDevice,
952    dev_nonce: u16,
953    now_us: u64,
954    out_frame: *mut *mut PamojaBuffer,
955    out_transmission: *mut PamojaLorawanTransmission,
956) -> PamojaStatus {
957    transmit(device, out_frame, out_transmission, |device| {
958        device.join(dev_nonce, now_us)
959    })
960}
961
962/// Builds an uplink carrying a payload.
963///
964/// # Arguments
965///
966/// * `device` - the device.
967/// * `port` - the application port, 1 to 223, or 224 for the certification test port.
968/// * `payload` - the application payload.
969/// * `payload_len` - its length.
970/// * `confirmed` - `1` to ask the network to acknowledge it.
971/// * `now_us` - the time, in microseconds.
972/// * `out_frame` - receives the frame to transmit.
973/// * `out_transmission` - receives where and how to transmit it, and its windows.
974///
975/// # Returns
976///
977/// [`PamojaStatus::Ok`] on success, with `*out_frame` set to a buffer the caller must release
978/// with [`pamoja_buffer_free`](crate::pamoja_buffer_free).
979///
980/// # Errors
981///
982/// Returns [`PamojaStatus::InvalidArgument`] for port 0, which belongs to MAC commands, and
983/// a failing status with the reason on [`pamoja_lorawan_end_device_error`] otherwise: not
984/// joined, busy, a wait for the air, or a payload too long.
985///
986/// # Safety
987///
988/// `device` must be a live handle, `payload` must point to `payload_len` readable bytes when
989/// that is non-zero, and the out pointers must be writable.
990#[no_mangle]
991#[allow(clippy::too_many_arguments)]
992pub unsafe extern "C" fn pamoja_lorawan_end_device_send(
993    device: *mut PamojaLorawanEndDevice,
994    port: u8,
995    payload: *const u8,
996    payload_len: usize,
997    confirmed: u8,
998    now_us: u64,
999    out_frame: *mut *mut PamojaBuffer,
1000    out_transmission: *mut PamojaLorawanTransmission,
1001) -> PamojaStatus {
1002    let payload = match read_bytes(payload, payload_len) {
1003        Ok(bytes) => bytes,
1004        Err(status) => return status,
1005    };
1006    if port == 0 {
1007        if let Some(device) = device.as_mut() {
1008            device.error = Some(DeviceError::Frame(LorawanError::MalformedFrame));
1009        }
1010        set_last_error("port 0 carries MAC commands, not an application payload".to_owned());
1011        return PamojaStatus::InvalidArgument;
1012    }
1013    transmit(device, out_frame, out_transmission, |device| {
1014        device.send(port, &payload, confirmed != 0, now_us)
1015    })
1016}
1017
1018/// Builds an uplink with no payload, carrying the answers the device owes, an
1019/// acknowledgment, or an ADR acknowledgment request.
1020///
1021/// # Arguments
1022///
1023/// * `device` - the device.
1024/// * `now_us` - the time, in microseconds.
1025/// * `out_frame` - receives the frame to transmit.
1026/// * `out_transmission` - receives where and how to transmit it, and its windows.
1027///
1028/// # Returns
1029///
1030/// As [`pamoja_lorawan_end_device_send`].
1031///
1032/// # Errors
1033///
1034/// As [`pamoja_lorawan_end_device_send`].
1035///
1036/// # Safety
1037///
1038/// `device` must be a live handle and the out pointers writable.
1039#[no_mangle]
1040pub unsafe extern "C" fn pamoja_lorawan_end_device_send_empty(
1041    device: *mut PamojaLorawanEndDevice,
1042    now_us: u64,
1043    out_frame: *mut *mut PamojaBuffer,
1044    out_transmission: *mut PamojaLorawanTransmission,
1045) -> PamojaStatus {
1046    transmit(device, out_frame, out_transmission, |device| {
1047        device.send_empty(now_us)
1048    })
1049}
1050
1051/// Sends the last uplink again, the same frame on a channel chosen afresh.
1052///
1053/// # Arguments
1054///
1055/// * `device` - the device.
1056/// * `now_us` - the time, in microseconds.
1057/// * `out_frame` - receives the frame to transmit.
1058/// * `out_transmission` - receives where and how to transmit it, and its windows.
1059///
1060/// # Returns
1061///
1062/// As [`pamoja_lorawan_end_device_send`].
1063///
1064/// # Errors
1065///
1066/// Returns a failing status with the reason on [`pamoja_lorawan_end_device_error`]: nothing
1067/// due to repeat, or a wait.
1068///
1069/// # Safety
1070///
1071/// `device` must be a live handle and the out pointers writable.
1072#[no_mangle]
1073pub unsafe extern "C" fn pamoja_lorawan_end_device_repeat(
1074    device: *mut PamojaLorawanEndDevice,
1075    now_us: u64,
1076    out_frame: *mut *mut PamojaBuffer,
1077    out_transmission: *mut PamojaLorawanTransmission,
1078) -> PamojaStatus {
1079    transmit(device, out_frame, out_transmission, |device| {
1080        device.repeat(now_us)
1081    })
1082}
1083
1084/// Reads a frame heard in one of the receive windows of the last transmission, without saying
1085/// which.
1086///
1087/// A downlink may be as long as the faster of the two windows allows. When the radio knows the
1088/// window, [`pamoja_lorawan_end_device_heard_in`] holds the frame to that window's own limit.
1089///
1090/// # Arguments
1091///
1092/// * `device` - the device.
1093/// * `frame` - the bytes the radio received.
1094/// * `frame_len` - their length.
1095/// * `snr_db` - the frame's signal-to-noise ratio, which a `DevStatusAns` reports.
1096/// * `out_heard` - receives what the frame was.
1097/// * `out_payload` - receives the application payload of a data frame, and null for a join.
1098///
1099/// # Returns
1100///
1101/// [`PamojaStatus::Ok`] on success. A payload buffer, when set, must be released with
1102/// [`pamoja_buffer_free`](crate::pamoja_buffer_free).
1103///
1104/// # Errors
1105///
1106/// Returns a failing status with the reason on [`pamoja_lorawan_end_device_error`]: nothing
1107/// pending, another device's frame, a replayed or far-ahead counter, a refused join accept,
1108/// or a frame that did not decode.
1109///
1110/// # Safety
1111///
1112/// `device` must be a live handle, `frame` must point to `frame_len` readable bytes when that
1113/// is non-zero, and the out pointers must be writable.
1114#[no_mangle]
1115pub unsafe extern "C" fn pamoja_lorawan_end_device_heard(
1116    device: *mut PamojaLorawanEndDevice,
1117    frame: *const u8,
1118    frame_len: usize,
1119    snr_db: i8,
1120    out_heard: *mut PamojaLorawanHeard,
1121    out_payload: *mut *mut PamojaBuffer,
1122) -> PamojaStatus {
1123    let (Some(device), false, false) =
1124        (device.as_mut(), out_heard.is_null(), out_payload.is_null())
1125    else {
1126        set_last_error("device, out_heard and out_payload must not be null".to_owned());
1127        return PamojaStatus::InvalidArgument;
1128    };
1129    heard_frame(
1130        device,
1131        None,
1132        frame,
1133        frame_len,
1134        snr_db,
1135        out_heard,
1136        out_payload,
1137    )
1138}
1139
1140/// Reads a frame heard in a given receive window of the last transmission.
1141///
1142/// A frame that is not for this device, does not verify, or has a MACPayload longer than the
1143/// window's data rate carries leaves the transmission waiting, so the second window still
1144/// opens. TS001-1.0.4 section 4.1 has a device discard such a frame.
1145///
1146/// # Arguments
1147///
1148/// * `device` - the device.
1149/// * `window` - [`PAMOJA_LORAWAN_WINDOW_RX1`], [`PAMOJA_LORAWAN_WINDOW_RX2`] or
1150///   [`PAMOJA_LORAWAN_WINDOW_RXR`].
1151/// * `frame` - the bytes the radio received.
1152/// * `frame_len` - their length.
1153/// * `snr_db` - the frame's signal-to-noise ratio, which a `DevStatusAns` reports.
1154/// * `out_heard` - receives what the frame was.
1155/// * `out_payload` - receives the application payload of a data frame, and null for a join.
1156///
1157/// # Returns
1158///
1159/// [`PamojaStatus::Ok`] on success, or [`PamojaStatus::InvalidArgument`] for a window that is
1160/// neither. A payload buffer, when set, must be released with
1161/// [`pamoja_buffer_free`](crate::pamoja_buffer_free).
1162///
1163/// # Errors
1164///
1165/// As [`pamoja_lorawan_end_device_heard`], with a frame longer than the window carries
1166/// reported as one that did not decode.
1167///
1168/// # Safety
1169///
1170/// `device` must be a live handle, `frame` must point to `frame_len` readable bytes when that
1171/// is non-zero, and the out pointers must be writable.
1172#[no_mangle]
1173pub unsafe extern "C" fn pamoja_lorawan_end_device_heard_in(
1174    device: *mut PamojaLorawanEndDevice,
1175    window: u8,
1176    frame: *const u8,
1177    frame_len: usize,
1178    snr_db: i8,
1179    out_heard: *mut PamojaLorawanHeard,
1180    out_payload: *mut *mut PamojaBuffer,
1181) -> PamojaStatus {
1182    let (Some(device), false, false) =
1183        (device.as_mut(), out_heard.is_null(), out_payload.is_null())
1184    else {
1185        set_last_error("device, out_heard and out_payload must not be null".to_owned());
1186        return PamojaStatus::InvalidArgument;
1187    };
1188    let Some(window) = window_in(window) else {
1189        set_last_error(format!("{window} is not a receive window"));
1190        return PamojaStatus::InvalidArgument;
1191    };
1192    heard_frame(
1193        device,
1194        Some(window),
1195        frame,
1196        frame_len,
1197        snr_db,
1198        out_heard,
1199        out_payload,
1200    )
1201}
1202
1203/// Reads a heard frame into the out pointers, which the caller has checked.
1204unsafe fn heard_frame(
1205    device: &mut PamojaLorawanEndDevice,
1206    window: Option<ReceiveWindow>,
1207    frame: *const u8,
1208    frame_len: usize,
1209    snr_db: i8,
1210    out_heard: *mut PamojaLorawanHeard,
1211    out_payload: *mut *mut PamojaBuffer,
1212) -> PamojaStatus {
1213    *out_payload = ptr::null_mut();
1214    let frame = match read_bytes(frame, frame_len) {
1215        Ok(bytes) => bytes,
1216        Err(status) => return status,
1217    };
1218    let result = match window {
1219        Some(window) => device.device.heard_in(window, &frame, snr_db),
1220        None => device.device.heard(&frame, snr_db),
1221    };
1222    let heard = match device.settle(result) {
1223        Ok(heard) => heard,
1224        Err(status) => return status,
1225    };
1226    *out_heard = heard_out(heard, device.device.dev_addr().unwrap_or(0), out_payload);
1227    PamojaStatus::Ok
1228}
1229
1230/// What a frame turned out to be, as C sees it, with its payload handed over beside it.
1231///
1232/// # Safety
1233///
1234/// `out_payload` must be a writable pointer slot.
1235pub(crate) unsafe fn heard_out(
1236    heard: Heard,
1237    dev_addr: u32,
1238    out_payload: *mut *mut PamojaBuffer,
1239) -> PamojaLorawanHeard {
1240    let mut out = PamojaLorawanHeard {
1241        dev_addr: 0,
1242        gps_seconds: 0,
1243        kind: PAMOJA_LORAWAN_HEARD_JOINED,
1244        has_port: 0,
1245        port: 0,
1246        acknowledged: 0,
1247        confirmed: 0,
1248        more_pending: 0,
1249        has_link_check: 0,
1250        margin_db: 0,
1251        gateways: 0,
1252        has_device_time: 0,
1253        fraction: 0,
1254    };
1255    match heard {
1256        Heard::Joined { dev_addr } => out.dev_addr = dev_addr,
1257        Heard::Data(delivery) => {
1258            out.kind = PAMOJA_LORAWAN_HEARD_DATA;
1259            out.dev_addr = dev_addr;
1260            out.has_port = u8::from(delivery.port().is_some());
1261            out.port = delivery.port().unwrap_or(0);
1262            out.acknowledged = u8::from(delivery.acknowledged());
1263            out.confirmed = u8::from(delivery.confirmed());
1264            out.more_pending = u8::from(delivery.more_pending());
1265            if let Some(check) = delivery.link_check() {
1266                out.has_link_check = 1;
1267                out.margin_db = check.margin_db;
1268                out.gateways = check.gateways;
1269            }
1270            if let Some(time) = delivery.device_time() {
1271                out.has_device_time = 1;
1272                out.gps_seconds = time.gps_seconds;
1273                out.fraction = time.fraction;
1274            }
1275            *out_payload = PamojaBuffer::into_raw(delivery.payload().to_vec());
1276        }
1277    }
1278    out
1279}
1280
1281/// Reads a receive window code.
1282pub(crate) const fn window_in(window: u8) -> Option<ReceiveWindow> {
1283    match window {
1284        PAMOJA_LORAWAN_WINDOW_RX1 => Some(ReceiveWindow::Rx1),
1285        PAMOJA_LORAWAN_WINDOW_RX2 => Some(ReceiveWindow::Rx2),
1286        PAMOJA_LORAWAN_WINDOW_RXR => Some(ReceiveWindow::Rxr),
1287        _ => None,
1288    }
1289}
1290
1291/// What comes next, as its code and the time that goes with it.
1292pub(crate) const fn next_out(next: Next) -> (u8, u64) {
1293    match next {
1294        Next::Repeat { not_before_us } => (PAMOJA_LORAWAN_NEXT_REPEAT, not_before_us),
1295        Next::Done => (PAMOJA_LORAWAN_NEXT_DONE, 0),
1296        Next::Unacknowledged => (PAMOJA_LORAWAN_NEXT_UNACKNOWLEDGED, 0),
1297        Next::JoinAgain { not_before_us } => (PAMOJA_LORAWAN_NEXT_JOIN_AGAIN, not_before_us),
1298    }
1299}
1300
1301/// Says what comes next once both receive windows closed with nothing for the device.
1302///
1303/// # Arguments
1304///
1305/// * `device` - the device.
1306/// * `now_us` - the time the second window closed, in microseconds.
1307/// * `out_next` - receives what to do.
1308///
1309/// # Returns
1310///
1311/// [`PamojaStatus::Ok`] on success.
1312///
1313/// # Errors
1314///
1315/// Returns a failing status with the reason on [`pamoja_lorawan_end_device_error`] when no
1316/// transmission waits on its windows.
1317///
1318/// # Safety
1319///
1320/// `device` must be a live handle and `out_next` writable.
1321#[no_mangle]
1322pub unsafe extern "C" fn pamoja_lorawan_end_device_nothing_heard(
1323    device: *mut PamojaLorawanEndDevice,
1324    now_us: u64,
1325    out_next: *mut PamojaLorawanNext,
1326) -> PamojaStatus {
1327    let (Some(device), false) = (device.as_mut(), out_next.is_null()) else {
1328        set_last_error("device and out_next must not be null".to_owned());
1329        return PamojaStatus::InvalidArgument;
1330    };
1331    let result = device.device.nothing_heard(now_us);
1332    let next = match device.settle(result) {
1333        Ok(next) => next,
1334        Err(status) => return status,
1335    };
1336    let (kind, not_before_us) = next_out(next);
1337    *out_next = PamojaLorawanNext {
1338        not_before_us,
1339        kind,
1340    };
1341    PamojaStatus::Ok
1342}
1343
1344/// Sets what the device reports its battery as, when a network asks.
1345///
1346/// # Arguments
1347///
1348/// * `device` - the device.
1349/// * `kind` - one of the `PAMOJA_LORAWAN_BATTERY_*` constants.
1350/// * `level` - for [`PAMOJA_LORAWAN_BATTERY_LEVEL`], 1 for empty to 254 for full, clamped
1351///   into that range.
1352///
1353/// # Returns
1354///
1355/// [`PamojaStatus::Ok`] on success.
1356///
1357/// # Errors
1358///
1359/// Returns [`PamojaStatus::InvalidArgument`] if `device` is null or `kind` names nothing.
1360///
1361/// # Safety
1362///
1363/// `device` must be a live handle.
1364#[no_mangle]
1365pub unsafe extern "C" fn pamoja_lorawan_end_device_set_battery(
1366    device: *mut PamojaLorawanEndDevice,
1367    kind: u8,
1368    level: u8,
1369) -> PamojaStatus {
1370    let Some(device) = device.as_mut() else {
1371        set_last_error("device must not be null".to_owned());
1372        return PamojaStatus::InvalidArgument;
1373    };
1374    let battery = match kind {
1375        PAMOJA_LORAWAN_BATTERY_EXTERNAL => Battery::External,
1376        PAMOJA_LORAWAN_BATTERY_LEVEL => Battery::Level(level),
1377        PAMOJA_LORAWAN_BATTERY_UNKNOWN => Battery::Unknown,
1378        other => {
1379            set_last_error(format!("{other} is not a battery kind"));
1380            return PamojaStatus::InvalidArgument;
1381        }
1382    };
1383    device.device.set_battery(battery);
1384    PamojaStatus::Ok
1385}
1386
1387/// Asks the network, with the next uplink, how well it hears the device.
1388///
1389/// # Arguments
1390///
1391/// * `device` - the device.
1392///
1393/// # Returns
1394///
1395/// [`PamojaStatus::Ok`] on success.
1396///
1397/// # Errors
1398///
1399/// Returns [`PamojaStatus::InvalidArgument`] if `device` is null.
1400///
1401/// # Safety
1402///
1403/// `device` must be a live handle.
1404#[no_mangle]
1405pub unsafe extern "C" fn pamoja_lorawan_end_device_request_link_check(
1406    device: *mut PamojaLorawanEndDevice,
1407) -> PamojaStatus {
1408    let Some(device) = device.as_mut() else {
1409        set_last_error("device must not be null".to_owned());
1410        return PamojaStatus::InvalidArgument;
1411    };
1412    device.device.request_link_check();
1413    PamojaStatus::Ok
1414}
1415
1416/// Asks the network, with the next uplink, for the time.
1417///
1418/// # Arguments
1419///
1420/// * `device` - the device.
1421///
1422/// # Returns
1423///
1424/// [`PamojaStatus::Ok`] on success.
1425///
1426/// # Errors
1427///
1428/// Returns [`PamojaStatus::InvalidArgument`] if `device` is null.
1429///
1430/// # Safety
1431///
1432/// `device` must be a live handle.
1433#[no_mangle]
1434pub unsafe extern "C" fn pamoja_lorawan_end_device_request_device_time(
1435    device: *mut PamojaLorawanEndDevice,
1436) -> PamojaStatus {
1437    let Some(device) = device.as_mut() else {
1438        set_last_error("device must not be null".to_owned());
1439        return PamojaStatus::InvalidArgument;
1440    };
1441    device.device.request_device_time();
1442    PamojaStatus::Ok
1443}
1444
1445/// Reads where a device stands.
1446///
1447/// # Arguments
1448///
1449/// * `device` - the device.
1450/// * `out_status` - receives the status.
1451///
1452/// # Returns
1453///
1454/// [`PamojaStatus::Ok`] on success.
1455///
1456/// # Errors
1457///
1458/// Returns [`PamojaStatus::InvalidArgument`] if either pointer is null.
1459///
1460/// # Safety
1461///
1462/// `device` must be a live handle and `out_status` writable.
1463#[no_mangle]
1464pub unsafe extern "C" fn pamoja_lorawan_end_device_status(
1465    device: *const PamojaLorawanEndDevice,
1466    out_status: *mut PamojaLorawanEndDeviceStatus,
1467) -> PamojaStatus {
1468    let (Some(device), false) = (device.as_ref(), out_status.is_null()) else {
1469        set_last_error("device and out_status must not be null".to_owned());
1470        return PamojaStatus::InvalidArgument;
1471    };
1472    *out_status = status_out(&device.device);
1473    PamojaStatus::Ok
1474}
1475
1476/// Reads one of the channels a device may send on.
1477///
1478/// # Arguments
1479///
1480/// * `device` - the device.
1481/// * `position` - the channel's position among the enabled ones, below the count
1482///   [`pamoja_lorawan_end_device_status`] reports.
1483/// * `out_index` - receives the channel's index in the device's table.
1484/// * `out_channel` - receives the channel.
1485///
1486/// # Returns
1487///
1488/// [`PamojaStatus::Ok`] on success.
1489///
1490/// # Errors
1491///
1492/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null or `position` is past the
1493/// last enabled channel.
1494///
1495/// # Safety
1496///
1497/// `device` must be a live handle and the out pointers writable.
1498#[no_mangle]
1499pub unsafe extern "C" fn pamoja_lorawan_end_device_channel(
1500    device: *const PamojaLorawanEndDevice,
1501    position: u16,
1502    out_index: *mut u16,
1503    out_channel: *mut PamojaLorawanChannel,
1504) -> PamojaStatus {
1505    let (Some(device), false, false) =
1506        (device.as_ref(), out_index.is_null(), out_channel.is_null())
1507    else {
1508        set_last_error("device, out_index and out_channel must not be null".to_owned());
1509        return PamojaStatus::InvalidArgument;
1510    };
1511    let Some((index, channel)) = device.device.channels().nth(usize::from(position)) else {
1512        set_last_error(format!(
1513            "the device has no enabled channel at position {position}"
1514        ));
1515        return PamojaStatus::InvalidArgument;
1516    };
1517    *out_index = index as u16;
1518    *out_channel = PamojaLorawanChannel {
1519        uplink_hz: channel.uplink_hz,
1520        downlink_hz: channel.downlink_hz,
1521        min_data_rate: channel.min_data_rate,
1522        max_data_rate: channel.max_data_rate,
1523    };
1524    PamojaStatus::Ok
1525}
1526
1527/// Saves a joined device's state, to keep across a loss of power.
1528///
1529/// The bytes hold the session keys, so keep them wherever the keys would be safe.
1530///
1531/// # Arguments
1532///
1533/// * `device` - the device.
1534/// * `now_us` - the time, in microseconds, which the duty cycle waits are measured from.
1535/// * `out_saved` - receives the state.
1536/// * `len` - room at `out_saved`, which must be [`PAMOJA_LORAWAN_SAVED_LEN`].
1537///
1538/// # Returns
1539///
1540/// [`PamojaStatus::Ok`] on success.
1541///
1542/// # Errors
1543///
1544/// Returns [`PamojaStatus::InvalidArgument`] if a pointer is null or `len` is wrong, and a
1545/// failing status with the reason on [`pamoja_lorawan_end_device_error`] for a device that
1546/// has not joined or is waiting on its windows.
1547///
1548/// # Safety
1549///
1550/// `device` must be a live handle and `out_saved` must point to `len` writable bytes.
1551#[no_mangle]
1552pub unsafe extern "C" fn pamoja_lorawan_end_device_save(
1553    device: *mut PamojaLorawanEndDevice,
1554    now_us: u64,
1555    out_saved: *mut u8,
1556    len: usize,
1557) -> PamojaStatus {
1558    let (Some(device), false) = (device.as_mut(), out_saved.is_null()) else {
1559        set_last_error("device and out_saved must not be null".to_owned());
1560        return PamojaStatus::InvalidArgument;
1561    };
1562    if len != SAVED_LEN {
1563        set_last_error(format!("a saved state takes {SAVED_LEN} bytes, not {len}"));
1564        return PamojaStatus::InvalidArgument;
1565    }
1566    let result = device.device.save(now_us);
1567    match device.settle(result) {
1568        Ok(saved) => {
1569            ptr::copy_nonoverlapping(saved.as_bytes().as_ptr(), out_saved, SAVED_LEN);
1570            PamojaStatus::Ok
1571        }
1572        Err(status) => status,
1573    }
1574}
1575
1576/// Puts a saved state back on a device made on the same channel plan.
1577///
1578/// # Arguments
1579///
1580/// * `device` - the device, made the same way the saved one was.
1581/// * `saved` - the state [`pamoja_lorawan_end_device_save`] wrote.
1582/// * `saved_len` - its length.
1583/// * `now_us` - the time, in microseconds, on the clock the device woke to.
1584///
1585/// # Returns
1586///
1587/// [`PamojaStatus::Ok`] on success.
1588///
1589/// # Errors
1590///
1591/// Returns a failing status with the reason on [`pamoja_lorawan_end_device_error`]: a state
1592/// of the wrong length, a corrupt one, one in a format this build does not read, one from
1593/// another plan, or a device busy with a transmission.
1594///
1595/// # Safety
1596///
1597/// `device` must be a live handle and `saved` must point to `saved_len` readable bytes when
1598/// that is non-zero.
1599#[no_mangle]
1600pub unsafe extern "C" fn pamoja_lorawan_end_device_resume(
1601    device: *mut PamojaLorawanEndDevice,
1602    saved: *const u8,
1603    saved_len: usize,
1604    now_us: u64,
1605) -> PamojaStatus {
1606    let Some(device) = device.as_mut() else {
1607        set_last_error("device must not be null".to_owned());
1608        return PamojaStatus::InvalidArgument;
1609    };
1610    let bytes = match read_bytes(saved, saved_len) {
1611        Ok(bytes) => bytes,
1612        Err(status) => return status,
1613    };
1614    let result = Saved::from_bytes(&bytes)
1615        .map_err(DeviceError::State)
1616        .and_then(|saved| device.device.resume(&saved, now_us));
1617    match device.settle(result) {
1618        Ok(()) => PamojaStatus::Ok,
1619        Err(status) => status,
1620    }
1621}
1622
1623/// Turns relay mode on or off, TS011-1.0.1 section 10.2 and appendix 5.
1624///
1625/// From this call on, the decision is the caller's rather than the device's own policy,
1626/// until the network takes it over with `EndDeviceConfReq` or hands it back.
1627///
1628/// # Arguments
1629///
1630/// * `device` - the device.
1631/// * `on` - `1` to send through a relay.
1632/// * `out_taken` - receives `1` when the mode changed, and `0` when the network holds the
1633///   decision.
1634///
1635/// # Returns
1636///
1637/// [`PamojaStatus::Ok`] on success.
1638///
1639/// # Errors
1640///
1641/// Returns [`PamojaStatus::InvalidArgument`] for a null handle or out pointer.
1642///
1643/// # Safety
1644///
1645/// `device` must be a live handle and `out_taken` writable.
1646#[no_mangle]
1647pub unsafe extern "C" fn pamoja_lorawan_end_device_use_relay(
1648    device: *mut PamojaLorawanEndDevice,
1649    on: u8,
1650    out_taken: *mut u8,
1651) -> PamojaStatus {
1652    let (Some(device), false) = (device.as_mut(), out_taken.is_null()) else {
1653        set_last_error("device and out_taken must not be null".to_owned());
1654        return PamojaStatus::InvalidArgument;
1655    };
1656    *out_taken = u8::from(device.device.use_relay(on != 0));
1657    PamojaStatus::Ok
1658}
1659
1660/// Reports how a device uses a relay, TS011-1.0.1 sections 3.9 and 10.2.
1661///
1662/// # Arguments
1663///
1664/// * `device` - the device.
1665/// * `out_relaying` - receives `1` while relay mode is on.
1666/// * `out_activation` - receives the mode: [`PAMOJA_LORAWAN_RELAY_DISABLED`],
1667///   [`PAMOJA_LORAWAN_RELAY_ENABLED`], [`PAMOJA_LORAWAN_RELAY_DYNAMIC`] or
1668///   [`PAMOJA_LORAWAN_RELAY_DEVICE_CONTROLLED`].
1669/// * `out_sync` - receives what it knows of the relay's scans:
1670///   [`PAMOJA_LORAWAN_RELAY_INITIALIZED`], [`PAMOJA_LORAWAN_RELAY_UNSYNCHRONIZED`] or
1671///   [`PAMOJA_LORAWAN_RELAY_SYNCHRONIZED`].
1672/// * `out_wor_counter` - receives the wake-on-radio frame counter the next frame will use.
1673///
1674/// # Returns
1675///
1676/// [`PamojaStatus::Ok`] on success. Every out pointer may be null.
1677///
1678/// # Errors
1679///
1680/// Returns [`PamojaStatus::InvalidArgument`] for a null handle.
1681///
1682/// # Safety
1683///
1684/// `device` must be a live handle and every non-null out pointer writable.
1685#[no_mangle]
1686pub unsafe extern "C" fn pamoja_lorawan_end_device_relay_mode(
1687    device: *mut PamojaLorawanEndDevice,
1688    out_relaying: *mut u8,
1689    out_activation: *mut u8,
1690    out_sync: *mut u8,
1691    out_wor_counter: *mut u32,
1692) -> PamojaStatus {
1693    let Some(device) = device.as_mut() else {
1694        set_last_error("device must not be null".to_owned());
1695        return PamojaStatus::InvalidArgument;
1696    };
1697    if !out_relaying.is_null() {
1698        *out_relaying = u8::from(device.device.relaying());
1699    }
1700    if !out_activation.is_null() {
1701        *out_activation = match device.device.relay_activation() {
1702            RelayActivation::Disabled => PAMOJA_LORAWAN_RELAY_DISABLED,
1703            RelayActivation::Enabled => PAMOJA_LORAWAN_RELAY_ENABLED,
1704            RelayActivation::Dynamic => PAMOJA_LORAWAN_RELAY_DYNAMIC,
1705            RelayActivation::DeviceControlled => PAMOJA_LORAWAN_RELAY_DEVICE_CONTROLLED,
1706        };
1707    }
1708    if !out_sync.is_null() {
1709        *out_sync = match device.device.relay_sync() {
1710            RelaySync::Initialized => PAMOJA_LORAWAN_RELAY_INITIALIZED,
1711            RelaySync::Unsynchronized => PAMOJA_LORAWAN_RELAY_UNSYNCHRONIZED,
1712            RelaySync::Synchronized => PAMOJA_LORAWAN_RELAY_SYNCHRONIZED,
1713        };
1714    }
1715    if !out_wor_counter.is_null() {
1716        *out_wor_counter = device.device.wor_counter();
1717    }
1718    PamojaStatus::Ok
1719}
1720
1721/// Returns what the relay's last acknowledgment said about itself, TS011-1.0.1 table 14.
1722///
1723/// # Arguments
1724///
1725/// * `device` - the device.
1726/// * `out_status` - receives what the relay said.
1727///
1728/// # Returns
1729///
1730/// [`PamojaStatus::Ok`] when an acknowledgment has arrived, and
1731/// [`PamojaStatus::Other`] before one has.
1732///
1733/// # Errors
1734///
1735/// Returns [`PamojaStatus::InvalidArgument`] for a null handle or out pointer.
1736///
1737/// # Safety
1738///
1739/// `device` must be a live handle and `out_status` writable.
1740#[no_mangle]
1741pub unsafe extern "C" fn pamoja_lorawan_end_device_relay_status(
1742    device: *mut PamojaLorawanEndDevice,
1743    out_status: *mut PamojaLorawanRelayStatus,
1744) -> PamojaStatus {
1745    let (Some(device), false) = (device.as_mut(), out_status.is_null()) else {
1746        set_last_error("device and out_status must not be null".to_owned());
1747        return PamojaStatus::InvalidArgument;
1748    };
1749    match device.device.relay_status() {
1750        Some(status) => {
1751            *out_status = relay_status_out(status);
1752            PamojaStatus::Ok
1753        }
1754        None => {
1755            set_last_error("no acknowledgment has arrived from a relay".to_owned());
1756            PamojaStatus::Other
1757        }
1758    }
1759}
1760
1761/// Reads the acknowledgment a relay answered the last wake-on-radio frame with.
1762///
1763/// # Arguments
1764///
1765/// * `device` - the device.
1766/// * `frame` - the bytes the radio received in the acknowledgment window.
1767/// * `frame_len` - their length.
1768/// * `out_status` - receives what the relay said about itself.
1769///
1770/// # Returns
1771///
1772/// [`PamojaStatus::Ok`] when it verifies.
1773///
1774/// # Errors
1775///
1776/// Returns a failing status, with the reason on [`pamoja_lorawan_end_device_error`]: no
1777/// frame waiting on an answer, no session, or an acknowledgment that does not verify.
1778///
1779/// # Safety
1780///
1781/// `device` must be a live handle, `frame` must point to `frame_len` readable bytes, and
1782/// `out_status` must be writable.
1783#[no_mangle]
1784pub unsafe extern "C" fn pamoja_lorawan_end_device_heard_wor_ack(
1785    device: *mut PamojaLorawanEndDevice,
1786    frame: *const u8,
1787    frame_len: usize,
1788    out_status: *mut PamojaLorawanRelayStatus,
1789) -> PamojaStatus {
1790    let (Some(device), false) = (device.as_mut(), out_status.is_null()) else {
1791        set_last_error("device and out_status must not be null".to_owned());
1792        return PamojaStatus::InvalidArgument;
1793    };
1794    let frame = match read_bytes(frame, frame_len) {
1795        Ok(bytes) => bytes,
1796        Err(status) => return status,
1797    };
1798    let heard = device.device.heard_wor_ack(&frame);
1799    match device.settle(heard) {
1800        Ok(status) => {
1801            *out_status = relay_status_out(status);
1802            PamojaStatus::Ok
1803        }
1804        Err(status) => status,
1805    }
1806}
1807
1808/// Says what to do once the acknowledgment window closed with nothing in it.
1809///
1810/// # Arguments
1811///
1812/// * `device` - the device.
1813/// * `now_us` - the time, in microseconds.
1814/// * `out_next` - receives [`PAMOJA_LORAWAN_WOR_NEXT_UPLINK`] to send the uplink at the time the
1815///   exchange named, or [`PAMOJA_LORAWAN_WOR_NEXT_WAKE_UP`] to wake the relay again first.
1816/// * `out_exchange` - receives the next wake-on-radio exchange, for
1817///   [`PAMOJA_LORAWAN_WOR_NEXT_WAKE_UP`].
1818///
1819/// # Returns
1820///
1821/// [`PamojaStatus::Ok`] on success.
1822///
1823/// # Errors
1824///
1825/// Returns a failing status, with the reason on [`pamoja_lorawan_end_device_error`], when no
1826/// wake-on-radio frame waits on an answer.
1827///
1828/// # Safety
1829///
1830/// `device` must be a live handle and the out pointers writable.
1831#[no_mangle]
1832pub unsafe extern "C" fn pamoja_lorawan_end_device_no_wor_ack(
1833    device: *mut PamojaLorawanEndDevice,
1834    now_us: u64,
1835    out_next: *mut u8,
1836    out_exchange: *mut PamojaLorawanRelayExchange,
1837) -> PamojaStatus {
1838    let (Some(device), false, false) =
1839        (device.as_mut(), out_next.is_null(), out_exchange.is_null())
1840    else {
1841        set_last_error("device, out_next and out_exchange must not be null".to_owned());
1842        return PamojaStatus::InvalidArgument;
1843    };
1844    let next = device.device.no_wor_ack(now_us);
1845    match device.settle(next) {
1846        Ok(WorNext::Uplink) => {
1847            *out_next = PAMOJA_LORAWAN_WOR_NEXT_UPLINK;
1848            *out_exchange = empty_exchange();
1849            PamojaStatus::Ok
1850        }
1851        Ok(WorNext::WakeUp(exchange)) => {
1852            *out_next = PAMOJA_LORAWAN_WOR_NEXT_WAKE_UP;
1853            *out_exchange = exchange_out(exchange);
1854            PamojaStatus::Ok
1855        }
1856        Err(status) => status,
1857    }
1858}
1859
1860/// What a relay said about itself, as C sees it.
1861pub(crate) fn relay_status_out(status: RelayStatus) -> PamojaLorawanRelayStatus {
1862    PamojaLorawanRelayStatus {
1863        cad_periodicity: status.cad_periodicity.code(),
1864        xtal_accuracy: status.xtal_accuracy.code(),
1865        cad_to_rx: status.cad_to_rx.code(),
1866        relay_data_rate: status.relay_data_rate,
1867        forward: status.forward.code(),
1868    }
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873    use super::*;
1874
1875    use crate::lora_region::{
1876        pamoja_lora_plan_for_region, pamoja_lora_plan_free, PAMOJA_LORA_REGION_EU868,
1877    };
1878    use crate::lorawan::{pamoja_lorawan_device_free, pamoja_lorawan_device_new};
1879    use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1880    use pamoja_lorawan::{Downlink, JoinGrant};
1881
1882    const APP_KEY: [u8; 16] = [0x2B; 16];
1883
1884    /// Copies a buffer out and frees it.
1885    unsafe fn take(buffer: *mut PamojaBuffer) -> Vec<u8> {
1886        let bytes =
1887            std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
1888                .to_vec();
1889        pamoja_buffer_free(buffer);
1890        bytes
1891    }
1892
1893    #[test]
1894    fn the_saved_length_is_the_crate_value() {
1895        assert_eq!(PAMOJA_LORAWAN_SAVED_LEN, SAVED_LEN);
1896    }
1897
1898    #[test]
1899    #[cfg(feature = "eu868")]
1900    fn a_device_joins_sends_hears_saves_and_resumes_across_the_boundary() {
1901        unsafe {
1902            let mut plan = ptr::null_mut();
1903            assert_eq!(
1904                pamoja_lora_plan_for_region(PAMOJA_LORA_REGION_EU868, &mut plan),
1905                PamojaStatus::Ok
1906            );
1907            let mut credentials = ptr::null_mut();
1908            assert_eq!(
1909                pamoja_lorawan_device_new(
1910                    [0x11; 8].as_ptr(),
1911                    8,
1912                    [0x22; 8].as_ptr(),
1913                    8,
1914                    APP_KEY.as_ptr(),
1915                    16,
1916                    &mut credentials
1917                ),
1918                PamojaStatus::Ok
1919            );
1920            let mut settings = std::mem::zeroed();
1921            assert_eq!(
1922                pamoja_lorawan_device_settings(2, 14, &mut settings),
1923                PamojaStatus::Ok
1924            );
1925            settings.seed = 7;
1926            let mut device = ptr::null_mut();
1927            assert_eq!(
1928                pamoja_lorawan_end_device_new(plan, credentials, &settings, 0, 0, 0, &mut device),
1929                PamojaStatus::Ok
1930            );
1931
1932            let mut frame = ptr::null_mut();
1933            let mut transmission = std::mem::zeroed::<PamojaLorawanTransmission>();
1934            assert_eq!(
1935                pamoja_lorawan_end_device_send(
1936                    device,
1937                    2,
1938                    b"21.5".as_ptr(),
1939                    4,
1940                    0,
1941                    0,
1942                    &mut frame,
1943                    &mut transmission
1944                ),
1945                PamojaStatus::Other
1946            );
1947            let mut error = std::mem::zeroed::<PamojaLorawanDeviceError>();
1948            pamoja_lorawan_end_device_error(device, &mut error);
1949            assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_NOT_JOINED);
1950
1951            assert_eq!(
1952                pamoja_lorawan_end_device_join(device, 1, 0, &mut frame, &mut transmission),
1953                PamojaStatus::Ok
1954            );
1955            pamoja_lorawan_end_device_error(device, &mut error);
1956            assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_OK);
1957            let request = take(frame);
1958            assert_eq!(request.len(), 23);
1959            assert_eq!(transmission.rx1.delay_us, 5_000_000);
1960            assert_eq!(transmission.rx2.frequency_hz, 869_525_000);
1961            assert_eq!(transmission.rx2.link.crc, 0);
1962
1963            assert_eq!(
1964                pamoja_lorawan_end_device_join(device, 2, 0, &mut frame, &mut transmission),
1965                PamojaStatus::Other
1966            );
1967            pamoja_lorawan_end_device_error(device, &mut error);
1968            assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_BUSY);
1969
1970            let accept = JoinGrant::new(0x01, 0x13, 0x2601_2E43).accept(&APP_KEY, 1);
1971            let mut heard = std::mem::zeroed::<PamojaLorawanHeard>();
1972            let mut payload = ptr::null_mut();
1973            assert_eq!(
1974                pamoja_lorawan_end_device_heard(
1975                    device,
1976                    accept.as_bytes().as_ptr(),
1977                    accept.as_bytes().len(),
1978                    7,
1979                    &mut heard,
1980                    &mut payload
1981                ),
1982                PamojaStatus::Ok
1983            );
1984            assert_eq!(
1985                (heard.kind, heard.dev_addr),
1986                (PAMOJA_LORAWAN_HEARD_JOINED, 0x2601_2E43)
1987            );
1988            assert!(payload.is_null());
1989
1990            let mut status = std::mem::zeroed::<PamojaLorawanEndDeviceStatus>();
1991            pamoja_lorawan_end_device_status(device, &mut status);
1992            assert_eq!(
1993                (status.joined, status.dev_addr, status.channel_count),
1994                (1, 0x2601_2E43, 3)
1995            );
1996            let mut index = 0;
1997            let mut channel = std::mem::zeroed::<PamojaLorawanChannel>();
1998            assert_eq!(
1999                pamoja_lorawan_end_device_channel(device, 0, &mut index, &mut channel),
2000                PamojaStatus::Ok
2001            );
2002            assert_eq!((index, channel.uplink_hz), (0, 868_100_000));
2003            assert_eq!(
2004                pamoja_lorawan_end_device_channel(device, 3, &mut index, &mut channel),
2005                PamojaStatus::InvalidArgument
2006            );
2007
2008            assert_eq!(
2009                pamoja_lorawan_end_device_send(
2010                    device,
2011                    0,
2012                    ptr::null(),
2013                    0,
2014                    0,
2015                    7_000_000,
2016                    &mut frame,
2017                    &mut transmission
2018                ),
2019                PamojaStatus::InvalidArgument
2020            );
2021            assert_eq!(
2022                pamoja_lorawan_end_device_send(
2023                    device,
2024                    2,
2025                    b"21.5".as_ptr(),
2026                    4,
2027                    1,
2028                    7_000_000,
2029                    &mut frame,
2030                    &mut transmission
2031                ),
2032                PamojaStatus::Ok
2033            );
2034            let uplink = take(frame);
2035            assert_eq!(transmission.rx1.delay_us, 1_000_000);
2036            assert_eq!(transmission.carries_payload, 1);
2037
2038            // The network acknowledges on port 3 with a payload.
2039            let down = JoinGrant::new(0x01, 0x13, 0x2601_2E43)
2040                .session(&APP_KEY, 1)
2041                .encode_downlink(&Downlink::new(0, 3, b"ok").with_ack())
2042                .expect("a downlink");
2043            assert_eq!(
2044                pamoja_lorawan_end_device_heard(
2045                    device,
2046                    down.as_bytes().as_ptr(),
2047                    down.as_bytes().len(),
2048                    7,
2049                    &mut heard,
2050                    &mut payload
2051                ),
2052                PamojaStatus::Ok
2053            );
2054            assert_eq!(heard.kind, PAMOJA_LORAWAN_HEARD_DATA);
2055            assert_eq!((heard.has_port, heard.port, heard.acknowledged), (1, 3, 1));
2056            assert_eq!(take(payload), b"ok");
2057
2058            let mut next = std::mem::zeroed::<PamojaLorawanNext>();
2059            assert_eq!(
2060                pamoja_lorawan_end_device_nothing_heard(device, 9_000_000, &mut next),
2061                PamojaStatus::Other
2062            );
2063            pamoja_lorawan_end_device_error(device, &mut error);
2064            assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_NOTHING_PENDING);
2065
2066            let mut saved = vec![0u8; PAMOJA_LORAWAN_SAVED_LEN];
2067            assert_eq!(
2068                pamoja_lorawan_end_device_save(device, 9_000_000, saved.as_mut_ptr(), saved.len()),
2069                PamojaStatus::Ok
2070            );
2071            let mut next_uplink = ptr::null_mut();
2072            let mut next_transmission = std::mem::zeroed::<PamojaLorawanTransmission>();
2073            assert_eq!(
2074                pamoja_lorawan_end_device_send(
2075                    device,
2076                    2,
2077                    b"22.0".as_ptr(),
2078                    4,
2079                    0,
2080                    400_000_000,
2081                    &mut next_uplink,
2082                    &mut next_transmission
2083                ),
2084                PamojaStatus::Ok
2085            );
2086            let original = take(next_uplink);
2087
2088            let mut woken = ptr::null_mut();
2089            assert_eq!(
2090                pamoja_lorawan_end_device_new(plan, credentials, &settings, 0, 0, 0, &mut woken),
2091                PamojaStatus::Ok
2092            );
2093            assert_eq!(
2094                pamoja_lorawan_end_device_resume(woken, saved.as_ptr(), saved.len(), 9_000_000),
2095                PamojaStatus::Ok
2096            );
2097            assert_eq!(
2098                pamoja_lorawan_end_device_send(
2099                    woken,
2100                    2,
2101                    b"22.0".as_ptr(),
2102                    4,
2103                    0,
2104                    400_000_000,
2105                    &mut next_uplink,
2106                    &mut next_transmission
2107                ),
2108                PamojaStatus::Ok
2109            );
2110            assert_eq!(
2111                take(next_uplink),
2112                original,
2113                "the woken device sends the same frame"
2114            );
2115            assert_ne!(uplink, original);
2116
2117            // A 60-byte MACPayload is more than the second window's DR0 carries, but not
2118            // more than the first window's.
2119            let long = JoinGrant::new(0x01, 0x13, 0x2601_2E43)
2120                .session(&APP_KEY, 1)
2121                .encode_downlink(&Downlink::new(1, 3, &[0x5A; 52]))
2122                .expect("a downlink");
2123            let heard_long = |window| {
2124                let mut heard = std::mem::zeroed::<PamojaLorawanHeard>();
2125                let mut payload = ptr::null_mut();
2126                let status = pamoja_lorawan_end_device_heard_in(
2127                    woken,
2128                    window,
2129                    long.as_bytes().as_ptr(),
2130                    long.as_bytes().len(),
2131                    7,
2132                    &mut heard,
2133                    &mut payload,
2134                );
2135                (status, payload)
2136            };
2137            assert_eq!(
2138                heard_long(9).0,
2139                PamojaStatus::InvalidArgument,
2140                "a code that names no window"
2141            );
2142            assert_eq!(
2143                heard_long(PAMOJA_LORAWAN_WINDOW_RX2).0,
2144                PamojaStatus::InvalidArgument
2145            );
2146            pamoja_lorawan_end_device_error(woken, &mut error);
2147            assert_eq!(error.kind, PAMOJA_LORAWAN_DEVICE_FRAME);
2148            let (status, payload) = heard_long(PAMOJA_LORAWAN_WINDOW_RX1);
2149            assert_eq!(status, PamojaStatus::Ok);
2150            assert_eq!(take(payload), [0x5A; 52]);
2151
2152            let mut blank = ptr::null_mut();
2153            assert_eq!(
2154                pamoja_lorawan_end_device_new(plan, credentials, &settings, 0, 0, 0, &mut blank),
2155                PamojaStatus::Ok
2156            );
2157            assert_eq!(
2158                pamoja_lorawan_end_device_resume(blank, saved.as_ptr(), 10, 0),
2159                PamojaStatus::InvalidArgument
2160            );
2161            pamoja_lorawan_end_device_error(blank, &mut error);
2162            assert_eq!(
2163                (error.kind, error.state),
2164                (PAMOJA_LORAWAN_DEVICE_STATE, PAMOJA_LORAWAN_STATE_LENGTH)
2165            );
2166            saved[20] ^= 0xFF;
2167            assert_eq!(
2168                pamoja_lorawan_end_device_resume(blank, saved.as_ptr(), saved.len(), 0),
2169                PamojaStatus::Codec
2170            );
2171            pamoja_lorawan_end_device_error(blank, &mut error);
2172            assert_eq!(error.state, PAMOJA_LORAWAN_STATE_CORRUPT);
2173
2174            pamoja_lorawan_end_device_free(blank);
2175            pamoja_lorawan_end_device_free(woken);
2176            pamoja_lorawan_end_device_free(device);
2177            pamoja_lorawan_device_free(credentials);
2178            pamoja_lora_plan_free(plan);
2179        }
2180    }
2181}