Skip to main content

pamoja_ffi/
lorawan_packages.rs

1//! The C ABI for the LoRaWAN application layer packages: clock synchronization TS003-2.0.0,
2//! fragmented data block transport TS004-2.0.0, remote multicast setup TS005-2.0.0, and
3//! firmware management TS006-2.0.0.
4//!
5//! The key derivations and the parity matrix are pure functions of their arguments. What has
6//! to remember something crosses as an opaque handle: a clock synchronization package, a
7//! firmware manager, a fragmentation session being put back together, and the code taken
8//! over a block as it arrives.
9
10use std::ptr;
11
12use pamoja_lorawan::packages::clock::{ClockSync, PORT as CLOCK_PORT};
13use pamoja_lorawan::packages::firmware::{
14    FirmwareManager, Image, UpImageStatus, PORT as FIRMWARE_PORT,
15};
16use pamoja_lorawan::packages::fragment::{
17    data_block_int_key, parity_line, prbs23, BlockMic, BlockMicKey, Defragmenter, FragError,
18    Fragmenter, Progress, MAX_FRAGMENTS, PORT as FRAGMENT_PORT,
19};
20use pamoja_lorawan::packages::multicast::{
21    mc_app_s_key, mc_ke_key, mc_key, mc_nwk_s_key, mc_root_key_for, wrap_mc_key,
22    PORT as MULTICAST_PORT,
23};
24
25use pamoja_lorawan::packages::clock::ClockCommand;
26use pamoja_lorawan::packages::firmware::{DeleteStatus, FirmwareCommand};
27use pamoja_lorawan::packages::fragment::{FragCommand, SetupStatus};
28use pamoja_lorawan::packages::multicast::{McCommand, SessionStatus};
29use pamoja_lorawan::packages::PackageVersion;
30use pamoja_lorawan::Direction;
31
32use crate::{read_bytes, set_last_error, PamojaStatus};
33
34/// The port clock synchronization is spoken on, TS003-2.0.0.
35pub const PAMOJA_LORAWAN_CLOCK_PORT: u8 = 202;
36/// The port fragmented data block transport is spoken on, TS004-2.0.0.
37pub const PAMOJA_LORAWAN_FRAGMENT_PORT: u8 = 201;
38/// The port remote multicast setup is spoken on, TS005-2.0.0.
39pub const PAMOJA_LORAWAN_MULTICAST_PORT: u8 = 200;
40/// The port firmware management is spoken on, TS006-1.0.0.
41pub const PAMOJA_LORAWAN_FIRMWARE_PORT: u8 = 203;
42/// The most fragments one session carries.
43pub const PAMOJA_LORAWAN_MAX_FRAGMENTS: u16 = 16383;
44
45// The header carries these as literals, because a generated macro cannot name a Rust
46// constant. These hold them to what the crate says.
47const _: () = assert!(PAMOJA_LORAWAN_CLOCK_PORT == CLOCK_PORT);
48const _: () = assert!(PAMOJA_LORAWAN_FRAGMENT_PORT == FRAGMENT_PORT);
49const _: () = assert!(PAMOJA_LORAWAN_MULTICAST_PORT == MULTICAST_PORT);
50const _: () = assert!(PAMOJA_LORAWAN_FIRMWARE_PORT == FIRMWARE_PORT);
51const _: () = assert!(PAMOJA_LORAWAN_MAX_FRAGMENTS == MAX_FRAGMENTS);
52
53/// The device holds no firmware upgrade image.
54pub const PAMOJA_LORAWAN_IMAGE_NONE: u8 = 0;
55/// One is there, but it is corrupt or its signature does not verify.
56pub const PAMOJA_LORAWAN_IMAGE_CORRUPT: u8 = 1;
57/// One is there and authentic, but it is not for this hardware.
58pub const PAMOJA_LORAWAN_IMAGE_WRONG_HARDWARE: u8 = 2;
59/// One is there, and it can be installed.
60pub const PAMOJA_LORAWAN_IMAGE_VALID: u8 = 3;
61
62/// Derives a device's multicast root key, TS005-2.0.0 section 4.3.
63///
64/// # Arguments
65///
66/// * `root_key` - the device's sixteen-byte `GenAppKey` on LoRaWAN 1.0.x, or its `AppKey`
67///   on 1.1.
68/// * `lorawan_11` - `1` for the 1.1 scheme, which starts from another constant.
69/// * `out_key` - receives the sixteen-byte `McRootKey`.
70///
71/// # Returns
72///
73/// [`PamojaStatus::Ok`] on success.
74///
75/// # Errors
76///
77/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
78///
79/// # Safety
80///
81/// `root_key` must point to sixteen readable bytes and `out_key` to sixteen writable ones.
82#[no_mangle]
83pub unsafe extern "C" fn pamoja_lorawan_mc_root_key(
84    root_key: *const u8,
85    lorawan_11: u8,
86    out_key: *mut u8,
87) -> PamojaStatus {
88    derive(root_key, out_key, |key| {
89        mc_root_key_for(key, lorawan_11 != 0)
90    })
91}
92
93/// Derives the key a multicast group's key travels under, section 4.3.
94///
95/// # Arguments
96///
97/// * `mc_root_key` - the sixteen-byte key [`pamoja_lorawan_mc_root_key`] derived.
98/// * `out_key` - receives the sixteen-byte `McKEKey`.
99///
100/// # Returns
101///
102/// [`PamojaStatus::Ok`] on success.
103///
104/// # Errors
105///
106/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
107///
108/// # Safety
109///
110/// Both pointers must be valid for sixteen bytes.
111#[no_mangle]
112pub unsafe extern "C" fn pamoja_lorawan_mc_ke_key(
113    mc_root_key: *const u8,
114    out_key: *mut u8,
115) -> PamojaStatus {
116    derive(mc_root_key, out_key, mc_ke_key)
117}
118
119/// Unwraps the group key a setup command carried, section 4.3.
120///
121/// # Arguments
122///
123/// * `mc_ke_key` - the sixteen-byte key it travels under.
124/// * `wrapped` - the sixteen bytes the command carried.
125/// * `out_key` - receives the group's sixteen-byte `McKey`.
126///
127/// # Returns
128///
129/// [`PamojaStatus::Ok`] on success.
130///
131/// # Errors
132///
133/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
134///
135/// # Safety
136///
137/// Every pointer must be valid for sixteen bytes.
138#[no_mangle]
139pub unsafe extern "C" fn pamoja_lorawan_mc_key(
140    mc_ke_key: *const u8,
141    wrapped: *const u8,
142    out_key: *mut u8,
143) -> PamojaStatus {
144    let (Some(ke), Some(wrapped), false) =
145        (sixteen(mc_ke_key), sixteen(wrapped), out_key.is_null())
146    else {
147        return missing();
148    };
149    ptr::copy_nonoverlapping(mc_key(&ke, &wrapped).as_ptr(), out_key, 16);
150    PamojaStatus::Ok
151}
152
153/// Wraps a group key for a device, which is what a server does before sending it.
154///
155/// # Arguments
156///
157/// * `mc_ke_key` - the device's sixteen-byte key encryption key.
158/// * `mc_key` - the sixteen-byte group key.
159/// * `out_wrapped` - receives the sixteen bytes a setup command carries.
160///
161/// # Returns
162///
163/// [`PamojaStatus::Ok`] on success.
164///
165/// # Errors
166///
167/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
168///
169/// # Safety
170///
171/// Every pointer must be valid for sixteen bytes.
172#[no_mangle]
173pub unsafe extern "C" fn pamoja_lorawan_wrap_mc_key(
174    mc_ke_key: *const u8,
175    mc_key: *const u8,
176    out_wrapped: *mut u8,
177) -> PamojaStatus {
178    let (Some(ke), Some(key), false) = (sixteen(mc_ke_key), sixteen(mc_key), out_wrapped.is_null())
179    else {
180        return missing();
181    };
182    ptr::copy_nonoverlapping(wrap_mc_key(&ke, &key).as_ptr(), out_wrapped, 16);
183    PamojaStatus::Ok
184}
185
186/// Derives the key that reads a multicast group's payloads, section 4.3.
187///
188/// # Arguments
189///
190/// * `mc_key` - the sixteen-byte group key.
191/// * `mc_addr` - the group's address.
192/// * `out_key` - receives the sixteen-byte `McAppSKey`.
193///
194/// # Returns
195///
196/// [`PamojaStatus::Ok`] on success.
197///
198/// # Errors
199///
200/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
201///
202/// # Safety
203///
204/// Both pointers must be valid for sixteen bytes.
205#[no_mangle]
206pub unsafe extern "C" fn pamoja_lorawan_mc_app_s_key(
207    mc_key: *const u8,
208    mc_addr: u32,
209    out_key: *mut u8,
210) -> PamojaStatus {
211    derive(mc_key, out_key, |key| mc_app_s_key(key, mc_addr))
212}
213
214/// Derives the key that verifies a multicast group's frames, section 4.3.
215///
216/// # Arguments
217///
218/// * `mc_key` - the sixteen-byte group key.
219/// * `mc_addr` - the group's address.
220/// * `out_key` - receives the sixteen-byte `McNwkSKey`.
221///
222/// # Returns
223///
224/// [`PamojaStatus::Ok`] on success.
225///
226/// # Errors
227///
228/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
229///
230/// # Safety
231///
232/// Both pointers must be valid for sixteen bytes.
233#[no_mangle]
234pub unsafe extern "C" fn pamoja_lorawan_mc_nwk_s_key(
235    mc_key: *const u8,
236    mc_addr: u32,
237    out_key: *mut u8,
238) -> PamojaStatus {
239    derive(mc_key, out_key, |key| mc_nwk_s_key(key, mc_addr))
240}
241
242/// Derives the key that signs a data block, TS004-2.0.0 section 3.3.
243///
244/// # Arguments
245///
246/// * `root_key` - the device's sixteen-byte `GenAppKey` or `AppKey`.
247/// * `out_key` - receives the sixteen-byte `DataBlockIntKey`.
248///
249/// # Returns
250///
251/// [`PamojaStatus::Ok`] on success.
252///
253/// # Errors
254///
255/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
256///
257/// # Safety
258///
259/// Both pointers must be valid for sixteen bytes.
260#[no_mangle]
261pub unsafe extern "C" fn pamoja_lorawan_data_block_int_key(
262    root_key: *const u8,
263    out_key: *mut u8,
264) -> PamojaStatus {
265    derive(root_key, out_key, data_block_int_key)
266}
267
268/// Steps the pseudo-random sequence the parity matrix is drawn from, appendix A.1.
269///
270/// # Arguments
271///
272/// * `x` - the current value.
273///
274/// # Returns
275///
276/// The next one.
277#[no_mangle]
278pub extern "C" fn pamoja_lorawan_frag_prbs23(x: u32) -> u32 {
279    prbs23(x)
280}
281
282/// Builds one row of the parity matrix: which uncoded fragments a coded one is made of.
283///
284/// # Arguments
285///
286/// * `coded` - which coded fragment, counting from one past the uncoded ones.
287/// * `nb_frag` - how many uncoded fragments the block was cut into.
288/// * `out_line` - receives a bit for every uncoded fragment, little end first.
289/// * `line_len` - how many bytes that buffer holds, at least `nb_frag` bits' worth.
290/// * `out_ones` - receives how many fragments the coded one is made of.
291///
292/// # Returns
293///
294/// [`PamojaStatus::Ok`] on success.
295///
296/// # Errors
297///
298/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, a session of no fragments,
299/// or a buffer too small.
300///
301/// # Safety
302///
303/// `out_line` must point to `line_len` writable bytes and `out_ones` must be writable.
304#[no_mangle]
305pub unsafe extern "C" fn pamoja_lorawan_frag_parity_line(
306    coded: u16,
307    nb_frag: u16,
308    out_line: *mut u8,
309    line_len: usize,
310    out_ones: *mut usize,
311) -> PamojaStatus {
312    if out_line.is_null() || out_ones.is_null() || nb_frag == 0 {
313        return missing();
314    }
315    let needed = usize::from(nb_frag).div_ceil(8);
316    if line_len < needed {
317        set_last_error(format!("the line needs {needed} bytes, not {line_len}"));
318        return PamojaStatus::InvalidArgument;
319    }
320    let line = core::slice::from_raw_parts_mut(out_line, line_len);
321    *out_ones = parity_line(coded, nb_frag, line);
322    PamojaStatus::Ok
323}
324
325/// How many fragments a block of a given size takes, and how much padding the last one needs.
326///
327/// # Arguments
328///
329/// * `block_len` - the block's length in bytes.
330/// * `frag_size` - how many bytes each fragment carries.
331/// * `out_nb_frag` - receives the fragment count.
332/// * `out_padding` - receives the padding the last fragment carries.
333///
334/// # Returns
335///
336/// [`PamojaStatus::Ok`] on success.
337///
338/// # Errors
339///
340/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, a fragment size of zero, or
341/// a block needing more than [`PAMOJA_LORAWAN_MAX_FRAGMENTS`] fragments.
342///
343/// # Safety
344///
345/// Both out pointers must be writable.
346#[no_mangle]
347pub unsafe extern "C" fn pamoja_lorawan_frag_session(
348    block_len: usize,
349    frag_size: u8,
350    out_nb_frag: *mut u16,
351    out_padding: *mut u8,
352) -> PamojaStatus {
353    if out_nb_frag.is_null() || out_padding.is_null() || frag_size == 0 || block_len == 0 {
354        return missing();
355    }
356    let size = usize::from(frag_size);
357    let nb_frag = block_len.div_ceil(size);
358    if nb_frag > MAX_FRAGMENTS as usize {
359        set_last_error(format!(
360            "{nb_frag} fragments is more than a session carries"
361        ));
362        return PamojaStatus::InvalidArgument;
363    }
364    *out_nb_frag = nb_frag as u16;
365    *out_padding = (nb_frag * size - block_len) as u8;
366    PamojaStatus::Ok
367}
368
369/// Builds one fragment of a session out of a block held whole.
370///
371/// # Arguments
372///
373/// * `block` - the block to send.
374/// * `block_len` - its length.
375/// * `frag_size` - how many bytes each fragment carries.
376/// * `n` - which fragment, counting from one; past the uncoded ones it is a coded fragment.
377/// * `out_fragment` - receives the fragment.
378/// * `out_len` - how many bytes that buffer holds, at least `frag_size`.
379///
380/// # Returns
381///
382/// [`PamojaStatus::Ok`] on success.
383///
384/// # Errors
385///
386/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer, a session this build does
387/// not run, or a buffer too small.
388///
389/// # Safety
390///
391/// `block` must point to `block_len` readable bytes and `out_fragment` to `out_len` writable
392/// ones.
393#[no_mangle]
394pub unsafe extern "C" fn pamoja_lorawan_frag_fragment(
395    block: *const u8,
396    block_len: usize,
397    frag_size: u8,
398    n: u16,
399    out_fragment: *mut u8,
400    out_len: usize,
401) -> PamojaStatus {
402    let bytes = match read_bytes(block, block_len) {
403        Ok(bytes) => bytes,
404        Err(status) => return status,
405    };
406    if out_fragment.is_null() {
407        return missing();
408    }
409    let Ok(sender) = Fragmenter::new(&bytes, frag_size) else {
410        set_last_error("the block and fragment size do not make a session".to_owned());
411        return PamojaStatus::InvalidArgument;
412    };
413    let out = core::slice::from_raw_parts_mut(out_fragment, out_len);
414    match sender.fragment(n, out) {
415        Ok(_) => PamojaStatus::Ok,
416        Err(error) => {
417            set_last_error(error.to_string());
418            PamojaStatus::InvalidArgument
419        }
420    }
421}
422
423/// A fragmentation session being put back together, released with
424/// [`pamoja_lorawan_defrag_free`].
425pub struct PamojaLorawanDefrag {
426    block: Vec<u8>,
427    matrix: Vec<u8>,
428    nb_frag: u16,
429    frag_size: u8,
430    progress: Progress,
431    missing: u16,
432}
433
434/// How many bytes of working storage a session needs.
435///
436/// # Arguments
437///
438/// * `nb_frag` - how many uncoded fragments the block was cut into.
439/// * `max_lost` - the most uncoded fragments the session should survive losing.
440///
441/// # Returns
442///
443/// The byte count, which [`pamoja_lorawan_defrag_new`] allocates for itself.
444#[no_mangle]
445pub extern "C" fn pamoja_lorawan_defrag_matrix_len(nb_frag: u16, max_lost: u16) -> usize {
446    Defragmenter::matrix_len(nb_frag, max_lost)
447}
448
449/// Opens a fragmentation session.
450///
451/// # Arguments
452///
453/// * `nb_frag` - how many uncoded fragments the block was cut into.
454/// * `frag_size` - how many bytes each fragment carries.
455/// * `max_lost` - the most uncoded fragments to be able to solve for.
456/// * `out_session` - receives the session.
457///
458/// # Returns
459///
460/// [`PamojaStatus::Ok`] on success, with `*out_session` set to a handle the caller must
461/// release with [`pamoja_lorawan_defrag_free`].
462///
463/// # Errors
464///
465/// Returns [`PamojaStatus::InvalidArgument`] for a session this build does not run.
466///
467/// # Safety
468///
469/// `out_session` must be writable.
470#[no_mangle]
471pub unsafe extern "C" fn pamoja_lorawan_defrag_new(
472    nb_frag: u16,
473    frag_size: u8,
474    max_lost: u16,
475    out_session: *mut *mut PamojaLorawanDefrag,
476) -> PamojaStatus {
477    if out_session.is_null() {
478        return missing();
479    }
480    *out_session = ptr::null_mut();
481    if nb_frag == 0 || nb_frag > MAX_FRAGMENTS || frag_size == 0 {
482        set_last_error("the session is not one this build runs".to_owned());
483        return PamojaStatus::InvalidArgument;
484    }
485    let block = vec![0u8; usize::from(nb_frag) * usize::from(frag_size)];
486    let matrix = vec![0u8; Defragmenter::matrix_len(nb_frag, max_lost.min(nb_frag))];
487    let session = PamojaLorawanDefrag {
488        block,
489        matrix,
490        nb_frag,
491        frag_size,
492        progress: Progress::default(),
493        missing: nb_frag,
494    };
495    *out_session = Box::into_raw(Box::new(session));
496    PamojaStatus::Ok
497}
498
499/// Releases a fragmentation session. Passing null is a no-op.
500///
501/// # Arguments
502///
503/// * `session` - the session, which must not be used again.
504///
505/// # Safety
506///
507/// `session` must be a live handle from [`pamoja_lorawan_defrag_new`], or null.
508#[no_mangle]
509pub unsafe extern "C" fn pamoja_lorawan_defrag_free(session: *mut PamojaLorawanDefrag) {
510    if !session.is_null() {
511        drop(Box::from_raw(session));
512    }
513}
514
515/// Takes one fragment of a session.
516///
517/// # Arguments
518///
519/// * `session` - the session.
520/// * `n` - which fragment, counting from one.
521/// * `fragment` - its bytes.
522/// * `fragment_len` - how many, which must be at least the session's fragment size.
523/// * `out_done` - receives `1` once the block is whole.
524///
525/// # Returns
526///
527/// [`PamojaStatus::Ok`] on success.
528///
529/// # Errors
530///
531/// Returns [`PamojaStatus::InvalidArgument`] for a fragment outside the session, and
532/// [`PamojaStatus::Other`] when more fragments were lost than there is room to solve for.
533///
534/// # Safety
535///
536/// `session` must be a live handle, `fragment` must point to `fragment_len` readable bytes,
537/// and `out_done` must be writable.
538#[no_mangle]
539pub unsafe extern "C" fn pamoja_lorawan_defrag_fragment(
540    session: *mut PamojaLorawanDefrag,
541    n: u16,
542    fragment: *const u8,
543    fragment_len: usize,
544    out_done: *mut u8,
545) -> PamojaStatus {
546    let (Some(session), false) = (session.as_mut(), out_done.is_null()) else {
547        return missing();
548    };
549    let bytes = match read_bytes(fragment, fragment_len) {
550        Ok(bytes) => bytes,
551        Err(status) => return status,
552    };
553    // The session's buffers hold everything but a handful of numbers, so it is opened
554    // again where it left off for each fragment and put away again after.
555    let mut receiver = match Defragmenter::resumed(
556        session.nb_frag,
557        session.frag_size,
558        &mut session.block,
559        &mut session.matrix,
560        session.progress,
561    ) {
562        Ok(receiver) => receiver,
563        Err(error) => {
564            set_last_error(error.to_string());
565            return PamojaStatus::InvalidArgument;
566        }
567    };
568    let outcome = receiver.fragment(n, &bytes);
569    let progress = receiver.progress();
570    let missing = receiver.missing();
571    session.progress = progress;
572    session.missing = missing;
573    match outcome {
574        Ok(done) => {
575            *out_done = u8::from(done);
576            PamojaStatus::Ok
577        }
578        Err(FragError::Memory) => {
579            set_last_error(FragError::Memory.to_string());
580            PamojaStatus::Other
581        }
582        Err(error) => {
583            set_last_error(error.to_string());
584            PamojaStatus::InvalidArgument
585        }
586    }
587}
588
589/// Reports what a session has taken so far.
590///
591/// # Arguments
592///
593/// * `session` - the session.
594/// * `out_received` - receives how many fragments arrived, coded and uncoded.
595/// * `out_missing` - receives how many uncoded fragments are still missing.
596/// * `out_done` - receives `1` once the block is whole.
597///
598/// # Returns
599///
600/// [`PamojaStatus::Ok`] on success. Every out pointer may be null.
601///
602/// # Errors
603///
604/// Returns [`PamojaStatus::InvalidArgument`] for a null handle.
605///
606/// # Safety
607///
608/// `session` must be a live handle and every non-null out pointer writable.
609#[no_mangle]
610pub unsafe extern "C" fn pamoja_lorawan_defrag_status(
611    session: *const PamojaLorawanDefrag,
612    out_received: *mut u16,
613    out_missing: *mut u16,
614    out_done: *mut u8,
615) -> PamojaStatus {
616    let Some(session) = session.as_ref() else {
617        return missing();
618    };
619    if !out_received.is_null() {
620        *out_received = session.progress.received;
621    }
622    if !out_missing.is_null() {
623        *out_missing = session.missing;
624    }
625    if !out_done.is_null() {
626        *out_done = u8::from(session.progress.done);
627    }
628    PamojaStatus::Ok
629}
630
631/// Reads the block a session has put back together.
632///
633/// # Arguments
634///
635/// * `session` - the session.
636/// * `out_block` - receives the bytes, or null to ask only for the length.
637/// * `capacity` - how many bytes that buffer holds.
638/// * `out_len` - receives the block's length.
639///
640/// # Returns
641///
642/// [`PamojaStatus::Ok`] on success.
643///
644/// # Errors
645///
646/// Returns [`PamojaStatus::InvalidArgument`] for a null handle or a buffer too small.
647///
648/// # Safety
649///
650/// `session` must be a live handle, `out_block` must point to `capacity` writable bytes or
651/// be null, and `out_len` must be writable.
652#[no_mangle]
653pub unsafe extern "C" fn pamoja_lorawan_defrag_block(
654    session: *const PamojaLorawanDefrag,
655    out_block: *mut u8,
656    capacity: usize,
657    out_len: *mut usize,
658) -> PamojaStatus {
659    let (Some(session), false) = (session.as_ref(), out_len.is_null()) else {
660        return missing();
661    };
662    let len = usize::from(session.nb_frag) * usize::from(session.frag_size);
663    *out_len = len;
664    if out_block.is_null() {
665        return PamojaStatus::Ok;
666    }
667    if capacity < len {
668        set_last_error(format!("the block needs {len} bytes, not {capacity}"));
669        return PamojaStatus::InvalidArgument;
670    }
671    ptr::copy_nonoverlapping(session.block.as_ptr(), out_block, len);
672    PamojaStatus::Ok
673}
674
675/// A code being taken over a data block, released with [`pamoja_lorawan_block_mic_free`].
676pub struct PamojaLorawanBlockMic {
677    key: BlockMicKey,
678    mic: Option<BlockMic<'static>>,
679}
680
681/// Starts a code over one session's block, TS004-2.0.0 section 3.3.
682///
683/// # Arguments
684///
685/// * `data_block_int_key` - the sixteen-byte key [`pamoja_lorawan_data_block_int_key`]
686///   derived.
687/// * `session_cnt` - the session counter the setup carried.
688/// * `frag_index` - which of the device's sessions this is.
689/// * `descriptor` - the four bytes the setup described the block with.
690/// * `block_len` - the block's length without its padding.
691/// * `out_mic` - receives the code.
692///
693/// # Returns
694///
695/// [`PamojaStatus::Ok`] on success, with `*out_mic` set to a handle the caller must release
696/// with [`pamoja_lorawan_block_mic_free`].
697///
698/// # Errors
699///
700/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
701///
702/// # Safety
703///
704/// `data_block_int_key` must point to sixteen readable bytes, `descriptor` to four, and
705/// `out_mic` must be writable.
706#[no_mangle]
707pub unsafe extern "C" fn pamoja_lorawan_block_mic_start(
708    data_block_int_key: *const u8,
709    session_cnt: u16,
710    frag_index: u8,
711    descriptor: *const u8,
712    block_len: u32,
713    out_mic: *mut *mut PamojaLorawanBlockMic,
714) -> PamojaStatus {
715    let (Some(key), false, false) = (
716        sixteen(data_block_int_key),
717        descriptor.is_null(),
718        out_mic.is_null(),
719    ) else {
720        return missing();
721    };
722    *out_mic = ptr::null_mut();
723    let mut four = [0u8; 4];
724    ptr::copy_nonoverlapping(descriptor, four.as_mut_ptr(), 4);
725
726    let mut held = Box::new(PamojaLorawanBlockMic {
727        key: BlockMicKey::new(&key),
728        mic: None,
729    });
730    // The code borrows the key it is taken under, which lives in the same box, so the
731    // borrow lasts exactly as long as the handle does.
732    let borrowed: &'static BlockMicKey = &*ptr::from_ref(&held.key);
733    held.mic = Some(borrowed.start(session_cnt, frag_index, four, block_len));
734    *out_mic = Box::into_raw(held);
735    PamojaStatus::Ok
736}
737
738/// Adds a piece of the block to a code.
739///
740/// # Arguments
741///
742/// * `mic` - the code.
743/// * `data` - the next bytes of the block, in order, without any padding.
744/// * `data_len` - how many.
745///
746/// # Returns
747///
748/// [`PamojaStatus::Ok`] on success.
749///
750/// # Errors
751///
752/// Returns [`PamojaStatus::InvalidArgument`] for a null handle.
753///
754/// # Safety
755///
756/// `mic` must be a live handle and `data` must point to `data_len` readable bytes.
757#[no_mangle]
758pub unsafe extern "C" fn pamoja_lorawan_block_mic_update(
759    mic: *mut PamojaLorawanBlockMic,
760    data: *const u8,
761    data_len: usize,
762) -> PamojaStatus {
763    let Some(held) = mic.as_mut() else {
764        return missing();
765    };
766    let bytes = match read_bytes(data, data_len) {
767        Ok(bytes) => bytes,
768        Err(status) => return status,
769    };
770    if let Some(mic) = held.mic.as_mut() {
771        mic.update(&bytes);
772    }
773    PamojaStatus::Ok
774}
775
776/// Finishes a code and releases it.
777///
778/// # Arguments
779///
780/// * `mic` - the code, which must not be used again.
781/// * `out_mic` - receives the four bytes a session setup carries.
782///
783/// # Returns
784///
785/// [`PamojaStatus::Ok`] on success.
786///
787/// # Errors
788///
789/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer.
790///
791/// # Safety
792///
793/// `mic` must be a live handle and `out_mic` must point to four writable bytes.
794#[no_mangle]
795pub unsafe extern "C" fn pamoja_lorawan_block_mic_finish(
796    mic: *mut PamojaLorawanBlockMic,
797    out_mic: *mut u8,
798) -> PamojaStatus {
799    if mic.is_null() || out_mic.is_null() {
800        return missing();
801    }
802    let mut held = Box::from_raw(mic);
803    let Some(taken) = held.mic.take() else {
804        return missing();
805    };
806    let code = taken.finish();
807    ptr::copy_nonoverlapping(code.as_ptr(), out_mic, 4);
808    PamojaStatus::Ok
809}
810
811/// Releases a code that was never finished. Passing null is a no-op.
812///
813/// # Arguments
814///
815/// * `mic` - the code, which must not be used again.
816///
817/// # Safety
818///
819/// `mic` must be a live handle from [`pamoja_lorawan_block_mic_start`], or null.
820#[no_mangle]
821pub unsafe extern "C" fn pamoja_lorawan_block_mic_free(mic: *mut PamojaLorawanBlockMic) {
822    if !mic.is_null() {
823        let mut held = Box::from_raw(mic);
824        held.mic = None;
825    }
826}
827
828/// A clock synchronization package on a device, released with
829/// [`pamoja_lorawan_clock_sync_free`].
830pub struct PamojaLorawanClockSync {
831    sync: ClockSync,
832}
833
834/// Starts the clock synchronization package on a device, TS003-2.0.0.
835///
836/// # Arguments
837///
838/// * `self_managed` - `1` for a device that keeps its own periodicity and refuses the
839///   server's.
840/// * `out_sync` - receives the package.
841///
842/// # Returns
843///
844/// [`PamojaStatus::Ok`] on success, with `*out_sync` set to a handle the caller must release
845/// with [`pamoja_lorawan_clock_sync_free`].
846///
847/// # Errors
848///
849/// Returns [`PamojaStatus::InvalidArgument`] for a null out pointer.
850///
851/// # Safety
852///
853/// `out_sync` must be writable.
854#[no_mangle]
855pub unsafe extern "C" fn pamoja_lorawan_clock_sync_new(
856    self_managed: u8,
857    out_sync: *mut *mut PamojaLorawanClockSync,
858) -> PamojaStatus {
859    if out_sync.is_null() {
860        return missing();
861    }
862    let sync = if self_managed != 0 {
863        ClockSync::self_managed()
864    } else {
865        ClockSync::new()
866    };
867    *out_sync = Box::into_raw(Box::new(PamojaLorawanClockSync { sync }));
868    PamojaStatus::Ok
869}
870
871/// Releases a clock synchronization package. Passing null is a no-op.
872///
873/// # Arguments
874///
875/// * `sync` - the package, which must not be used again.
876///
877/// # Safety
878///
879/// `sync` must be a live handle from [`pamoja_lorawan_clock_sync_new`], or null.
880#[no_mangle]
881pub unsafe extern "C" fn pamoja_lorawan_clock_sync_free(sync: *mut PamojaLorawanClockSync) {
882    if !sync.is_null() {
883        drop(Box::from_raw(sync));
884    }
885}
886
887/// Builds the request that asks a server for a clock correction, section 3.2.
888///
889/// # Arguments
890///
891/// * `sync` - the package.
892/// * `device_time` - what the device believes the time is, in seconds since the GPS epoch.
893/// * `ans_required` - `1` to make the server answer even when the clock is right.
894/// * `out_command` - receives the command.
895/// * `capacity` - how many bytes that buffer holds, at least six.
896/// * `out_len` - receives how many were written.
897///
898/// # Returns
899///
900/// [`PamojaStatus::Ok`] on success.
901///
902/// # Errors
903///
904/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer or a buffer too small.
905///
906/// # Safety
907///
908/// `sync` must be a live handle, `out_command` must point to `capacity` writable bytes, and
909/// `out_len` must be writable.
910#[no_mangle]
911pub unsafe extern "C" fn pamoja_lorawan_clock_sync_request(
912    sync: *mut PamojaLorawanClockSync,
913    device_time: u32,
914    ans_required: u8,
915    out_command: *mut u8,
916    capacity: usize,
917    out_len: *mut usize,
918) -> PamojaStatus {
919    let (Some(sync), false, false) = (sync.as_mut(), out_command.is_null(), out_len.is_null())
920    else {
921        return missing();
922    };
923    let out = core::slice::from_raw_parts_mut(out_command, capacity);
924    match sync.sync.app_time_req(device_time, ans_required != 0, out) {
925        Ok(written) => {
926            *out_len = written;
927            PamojaStatus::Ok
928        }
929        Err(error) => {
930            set_last_error(error.to_string());
931            PamojaStatus::InvalidArgument
932        }
933    }
934}
935
936/// Reads a downlink on the clock port and acts on it, section 3.2.
937///
938/// # Arguments
939///
940/// * `sync` - the package.
941/// * `payload` - what arrived on port [`PAMOJA_LORAWAN_CLOCK_PORT`].
942/// * `payload_len` - its length.
943/// * `out_correction` - receives the seconds to add to the device's clock.
944/// * `out_has_correction` - receives `1` when there was one.
945/// * `out_more` - receives `1` when another correction is coming.
946/// * `out_resync` - receives how many requests a resynchronization command asked for, or 0.
947/// * `out_answer_due` - receives `1` when the device now owes an answer.
948///
949/// # Returns
950///
951/// [`PamojaStatus::Ok`] on success. Every out pointer may be null.
952///
953/// # Errors
954///
955/// Returns [`PamojaStatus::Codec`] for a message this package cannot read.
956///
957/// # Safety
958///
959/// `sync` must be a live handle, `payload` must point to `payload_len` readable bytes, and
960/// every non-null out pointer must be writable.
961#[no_mangle]
962#[allow(clippy::too_many_arguments)]
963pub unsafe extern "C" fn pamoja_lorawan_clock_sync_heard(
964    sync: *mut PamojaLorawanClockSync,
965    payload: *const u8,
966    payload_len: usize,
967    out_correction: *mut i32,
968    out_has_correction: *mut u8,
969    out_more: *mut u8,
970    out_resync: *mut u8,
971    out_answer_due: *mut u8,
972) -> PamojaStatus {
973    let Some(sync) = sync.as_mut() else {
974        return missing();
975    };
976    let bytes = match read_bytes(payload, payload_len) {
977        Ok(bytes) => bytes,
978        Err(status) => return status,
979    };
980    let heard = match sync.sync.heard(&bytes) {
981        Ok(heard) => heard,
982        Err(error) => {
983            set_last_error(error.to_string());
984            return PamojaStatus::Codec;
985        }
986    };
987    if !out_correction.is_null() {
988        *out_correction = heard.correction.unwrap_or(0);
989    }
990    if !out_has_correction.is_null() {
991        *out_has_correction = u8::from(heard.correction.is_some());
992    }
993    if !out_more.is_null() {
994        *out_more = u8::from(heard.more_correction);
995    }
996    if !out_resync.is_null() {
997        *out_resync = heard.resync.unwrap_or(0);
998    }
999    if !out_answer_due.is_null() {
1000        *out_answer_due = u8::from(heard.answer_due);
1001    }
1002    PamojaStatus::Ok
1003}
1004
1005/// Writes the answer a device owes its clock server, if it owes one.
1006///
1007/// # Arguments
1008///
1009/// * `sync` - the package.
1010/// * `device_time` - what the device believes the time is.
1011/// * `out_command` - receives the command.
1012/// * `capacity` - how many bytes that buffer holds.
1013/// * `out_len` - receives how many were written, which is zero when nothing is owed.
1014///
1015/// # Returns
1016///
1017/// [`PamojaStatus::Ok`] on success.
1018///
1019/// # Errors
1020///
1021/// Returns [`PamojaStatus::InvalidArgument`] for a null pointer or a buffer too small.
1022///
1023/// # Safety
1024///
1025/// `sync` must be a live handle, `out_command` must point to `capacity` writable bytes, and
1026/// `out_len` must be writable.
1027#[no_mangle]
1028pub unsafe extern "C" fn pamoja_lorawan_clock_sync_answer(
1029    sync: *mut PamojaLorawanClockSync,
1030    device_time: u32,
1031    out_command: *mut u8,
1032    capacity: usize,
1033    out_len: *mut usize,
1034) -> PamojaStatus {
1035    let (Some(sync), false, false) = (sync.as_mut(), out_command.is_null(), out_len.is_null())
1036    else {
1037        return missing();
1038    };
1039    let out = core::slice::from_raw_parts_mut(out_command, capacity);
1040    match sync.sync.answer(device_time, out) {
1041        Ok(written) => {
1042            *out_len = written;
1043            PamojaStatus::Ok
1044        }
1045        Err(error) => {
1046            set_last_error(error.to_string());
1047            PamojaStatus::InvalidArgument
1048        }
1049    }
1050}
1051
1052/// Reports where a clock synchronization package stands.
1053///
1054/// # Arguments
1055///
1056/// * `sync` - the package.
1057/// * `out_token` - receives the token the next request carries.
1058/// * `out_period_s` - receives the seconds between requests the server last set.
1059/// * `out_answer_due` - receives `1` when the device owes an answer.
1060///
1061/// # Returns
1062///
1063/// [`PamojaStatus::Ok`] on success. Every out pointer may be null.
1064///
1065/// # Errors
1066///
1067/// Returns [`PamojaStatus::InvalidArgument`] for a null handle.
1068///
1069/// # Safety
1070///
1071/// `sync` must be a live handle and every non-null out pointer writable.
1072#[no_mangle]
1073pub unsafe extern "C" fn pamoja_lorawan_clock_sync_status(
1074    sync: *const PamojaLorawanClockSync,
1075    out_token: *mut u8,
1076    out_period_s: *mut u32,
1077    out_answer_due: *mut u8,
1078) -> PamojaStatus {
1079    let Some(sync) = sync.as_ref() else {
1080        return missing();
1081    };
1082    if !out_token.is_null() {
1083        *out_token = sync.sync.token();
1084    }
1085    if !out_period_s.is_null() {
1086        *out_period_s = sync.sync.period_s();
1087    }
1088    if !out_answer_due.is_null() {
1089        *out_answer_due = u8::from(sync.sync.answer_due());
1090    }
1091    PamojaStatus::Ok
1092}
1093
1094/// A firmware management package on a device, released with
1095/// [`pamoja_lorawan_firmware_free`].
1096pub struct PamojaLorawanFirmware {
1097    manager: FirmwareManager,
1098}
1099
1100/// Starts the firmware management package on a device, TS006-1.0.0.
1101///
1102/// # Arguments
1103///
1104/// * `firmware` - the version the device is running, as its manufacturer numbers it.
1105/// * `hardware` - the platform it runs on.
1106/// * `out_manager` - receives the package.
1107///
1108/// # Returns
1109///
1110/// [`PamojaStatus::Ok`] on success, with `*out_manager` set to a handle the caller must
1111/// release with [`pamoja_lorawan_firmware_free`].
1112///
1113/// # Errors
1114///
1115/// Returns [`PamojaStatus::InvalidArgument`] for a null out pointer.
1116///
1117/// # Safety
1118///
1119/// `out_manager` must be writable.
1120#[no_mangle]
1121pub unsafe extern "C" fn pamoja_lorawan_firmware_new(
1122    firmware: u32,
1123    hardware: u32,
1124    out_manager: *mut *mut PamojaLorawanFirmware,
1125) -> PamojaStatus {
1126    if out_manager.is_null() {
1127        return missing();
1128    }
1129    *out_manager = Box::into_raw(Box::new(PamojaLorawanFirmware {
1130        manager: FirmwareManager::new(firmware, hardware),
1131    }));
1132    PamojaStatus::Ok
1133}
1134
1135/// Releases a firmware management package. Passing null is a no-op.
1136///
1137/// # Arguments
1138///
1139/// * `manager` - the package, which must not be used again.
1140///
1141/// # Safety
1142///
1143/// `manager` must be a live handle from [`pamoja_lorawan_firmware_new`], or null.
1144#[no_mangle]
1145pub unsafe extern "C" fn pamoja_lorawan_firmware_free(manager: *mut PamojaLorawanFirmware) {
1146    if !manager.is_null() {
1147        drop(Box::from_raw(manager));
1148    }
1149}
1150
1151/// Says what firmware upgrade image the device is holding.
1152///
1153/// # Arguments
1154///
1155/// * `manager` - the package.
1156/// * `status` - one of [`PAMOJA_LORAWAN_IMAGE_NONE`], [`PAMOJA_LORAWAN_IMAGE_CORRUPT`],
1157///   [`PAMOJA_LORAWAN_IMAGE_WRONG_HARDWARE`] or [`PAMOJA_LORAWAN_IMAGE_VALID`].
1158/// * `version` - the version the device would run once it is installed, for a valid image.
1159///
1160/// # Returns
1161///
1162/// [`PamojaStatus::Ok`] on success.
1163///
1164/// # Errors
1165///
1166/// Returns [`PamojaStatus::InvalidArgument`] for a null handle.
1167///
1168/// # Safety
1169///
1170/// `manager` must be a live handle.
1171#[no_mangle]
1172pub unsafe extern "C" fn pamoja_lorawan_firmware_set_image(
1173    manager: *mut PamojaLorawanFirmware,
1174    status: u8,
1175    version: u32,
1176) -> PamojaStatus {
1177    let Some(manager) = manager.as_mut() else {
1178        return missing();
1179    };
1180    manager.manager.set_image(match status {
1181        PAMOJA_LORAWAN_IMAGE_NONE => None,
1182        PAMOJA_LORAWAN_IMAGE_VALID => Some(Image::valid(version)),
1183        other => Some(Image::refused(UpImageStatus::from_bits(other))),
1184    });
1185    PamojaStatus::Ok
1186}
1187
1188/// Reads a downlink on the firmware port and writes the answers it calls for.
1189///
1190/// # Arguments
1191///
1192/// * `manager` - the package.
1193/// * `payload` - what arrived on port [`PAMOJA_LORAWAN_FIRMWARE_PORT`].
1194/// * `payload_len` - its length.
1195/// * `now_s` - what the device believes the time is, in seconds since the GPS epoch.
1196/// * `has_now` - `1` when the device knows the time; a device that does not refuses a reboot
1197///   set for a moment in time.
1198/// * `out_answers` - receives the answers.
1199/// * `capacity` - how many bytes that buffer holds.
1200/// * `out_len` - receives how many were written.
1201///
1202/// # Returns
1203///
1204/// [`PamojaStatus::Ok`] on success.
1205///
1206/// # Errors
1207///
1208/// Returns [`PamojaStatus::Codec`] for a message this package cannot read, and
1209/// [`PamojaStatus::InvalidArgument`] for a null pointer or a buffer too small.
1210///
1211/// # Safety
1212///
1213/// `manager` must be a live handle, `payload` must point to `payload_len` readable bytes,
1214/// `out_answers` to `capacity` writable ones, and `out_len` must be writable.
1215#[no_mangle]
1216#[allow(clippy::too_many_arguments)]
1217pub unsafe extern "C" fn pamoja_lorawan_firmware_heard(
1218    manager: *mut PamojaLorawanFirmware,
1219    payload: *const u8,
1220    payload_len: usize,
1221    now_s: u32,
1222    has_now: u8,
1223    out_answers: *mut u8,
1224    capacity: usize,
1225    out_len: *mut usize,
1226) -> PamojaStatus {
1227    let (Some(manager), false, false) =
1228        (manager.as_mut(), out_answers.is_null(), out_len.is_null())
1229    else {
1230        return missing();
1231    };
1232    let bytes = match read_bytes(payload, payload_len) {
1233        Ok(bytes) => bytes,
1234        Err(status) => return status,
1235    };
1236    let out = core::slice::from_raw_parts_mut(out_answers, capacity);
1237    let now = (has_now != 0).then_some(now_s);
1238    match manager.manager.heard_at(&bytes, now, out) {
1239        Ok(written) => {
1240            *out_len = written;
1241            PamojaStatus::Ok
1242        }
1243        Err(error) => {
1244            set_last_error(error.to_string());
1245            PamojaStatus::Codec
1246        }
1247    }
1248}
1249
1250/// Reports the reboot a firmware management package is holding.
1251///
1252/// # Arguments
1253///
1254/// * `manager` - the package.
1255/// * `out_at_s` - receives the moment the device is to reboot, where one was set as a time.
1256/// * `out_has_at` - receives `1` when there is one.
1257/// * `out_in_s` - receives how long until it reboots, where one was set as a countdown.
1258/// * `out_has_in` - receives `1` when there is one.
1259/// * `out_now` - receives `1` when the device was told to reboot at once.
1260///
1261/// # Returns
1262///
1263/// [`PamojaStatus::Ok`] on success. Every out pointer may be null.
1264///
1265/// # Errors
1266///
1267/// Returns [`PamojaStatus::InvalidArgument`] for a null handle.
1268///
1269/// # Safety
1270///
1271/// `manager` must be a live handle and every non-null out pointer writable.
1272#[no_mangle]
1273pub unsafe extern "C" fn pamoja_lorawan_firmware_reboot(
1274    manager: *const PamojaLorawanFirmware,
1275    out_at_s: *mut u32,
1276    out_has_at: *mut u8,
1277    out_in_s: *mut u32,
1278    out_has_in: *mut u8,
1279    out_now: *mut u8,
1280) -> PamojaStatus {
1281    let Some(manager) = manager.as_ref() else {
1282        return missing();
1283    };
1284    let at = manager.manager.reboot_at_s();
1285    let left = manager.manager.reboot_in_s();
1286    if !out_at_s.is_null() {
1287        *out_at_s = at.unwrap_or(0);
1288    }
1289    if !out_has_at.is_null() {
1290        *out_has_at = u8::from(at.is_some());
1291    }
1292    if !out_in_s.is_null() {
1293        *out_in_s = left.unwrap_or(0);
1294    }
1295    if !out_has_in.is_null() {
1296        *out_has_in = u8::from(left.is_some());
1297    }
1298    if !out_now.is_null() {
1299        *out_now = u8::from(manager.manager.reboot_now());
1300    }
1301    PamojaStatus::Ok
1302}
1303
1304/// Says what firmware upgrade image the device is holding.
1305///
1306/// # Arguments
1307///
1308/// * `manager` - the manager.
1309/// * `out_status` - receives [`PAMOJA_LORAWAN_IMAGE_NONE`], [`PAMOJA_LORAWAN_IMAGE_CORRUPT`],
1310///   [`PAMOJA_LORAWAN_IMAGE_WRONG_HARDWARE`] or [`PAMOJA_LORAWAN_IMAGE_VALID`].
1311/// * `out_next_version` - receives what the device would boot into, for an image it can
1312///   install.
1313/// * `out_has_next_version` - receives `1` when there is one.
1314///
1315/// # Returns
1316///
1317/// [`PamojaStatus::Ok`] on success. Every out pointer may be null.
1318///
1319/// # Errors
1320///
1321/// Returns [`PamojaStatus::InvalidArgument`] for a null handle.
1322///
1323/// # Safety
1324///
1325/// `manager` must be a live handle and every non-null out pointer writable.
1326#[no_mangle]
1327pub unsafe extern "C" fn pamoja_lorawan_firmware_image(
1328    manager: *const PamojaLorawanFirmware,
1329    out_status: *mut u8,
1330    out_next_version: *mut u32,
1331    out_has_next_version: *mut u8,
1332) -> PamojaStatus {
1333    let Some(manager) = manager.as_ref() else {
1334        return missing();
1335    };
1336    let image = manager.manager.image();
1337    if !out_status.is_null() {
1338        *out_status = image.map_or(UpImageStatus::None, |image| image.status) as u8;
1339    }
1340    let next = image
1341        .filter(|image| matches!(image.status, UpImageStatus::Valid))
1342        .map(|image| image.version);
1343    if !out_next_version.is_null() {
1344        *out_next_version = next.unwrap_or(0);
1345    }
1346    if !out_has_next_version.is_null() {
1347        *out_has_next_version = u8::from(next.is_some());
1348    }
1349    PamojaStatus::Ok
1350}
1351
1352/// Forgets the programmed reboot, for a device that has carried it out.
1353///
1354/// # Arguments
1355///
1356/// * `manager` - the manager.
1357///
1358/// # Returns
1359///
1360/// [`PamojaStatus::Ok`] on success.
1361///
1362/// # Errors
1363///
1364/// Returns [`PamojaStatus::InvalidArgument`] for a null handle.
1365///
1366/// # Safety
1367///
1368/// `manager` must be a live handle.
1369#[no_mangle]
1370pub unsafe extern "C" fn pamoja_lorawan_firmware_rebooted(
1371    manager: *mut PamojaLorawanFirmware,
1372) -> PamojaStatus {
1373    let Some(manager) = manager.as_mut() else {
1374        return missing();
1375    };
1376    manager.manager.rebooted();
1377    PamojaStatus::Ok
1378}
1379
1380/// Reads sixteen bytes from a pointer that may be null.
1381unsafe fn sixteen(from: *const u8) -> Option<[u8; 16]> {
1382    if from.is_null() {
1383        return None;
1384    }
1385    let mut key = [0u8; 16];
1386    ptr::copy_nonoverlapping(from, key.as_mut_ptr(), 16);
1387    Some(key)
1388}
1389
1390/// Reports a null argument the same way everywhere.
1391fn missing() -> PamojaStatus {
1392    set_last_error("a required argument was null".to_owned());
1393    PamojaStatus::InvalidArgument
1394}
1395
1396/// Runs a derivation that takes one key and writes another.
1397unsafe fn derive(
1398    from: *const u8,
1399    out: *mut u8,
1400    step: impl FnOnce(&[u8; 16]) -> [u8; 16],
1401) -> PamojaStatus {
1402    let (Some(key), false) = (sixteen(from), out.is_null()) else {
1403        return missing();
1404    };
1405    ptr::copy_nonoverlapping(step(&key).as_ptr(), out, 16);
1406    PamojaStatus::Ok
1407}
1408
1409/// One command of an application layer package, whichever package it belongs to.
1410///
1411/// `port` says which package: [`PAMOJA_LORAWAN_CLOCK_PORT`],
1412/// [`PAMOJA_LORAWAN_FRAGMENT_PORT`], [`PAMOJA_LORAWAN_MULTICAST_PORT`] or
1413/// [`PAMOJA_LORAWAN_FIRMWARE_PORT`]. `cid` names the command within it, and `uplink` says
1414/// which way it travels; together they decide which of the other fields carry anything. The
1415/// rest are zero.
1416#[repr(C)]
1417#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1418pub struct PamojaLorawanPackageCommand {
1419    /// Which package this command belongs to, as its port.
1420    pub port: u8,
1421    /// Which command within that package.
1422    pub cid: u8,
1423    /// `1` for what a device sends, `0` for what a server sends.
1424    pub uplink: u8,
1425    /// The package identifier a version answer carries.
1426    pub package: u8,
1427    /// The package version it implements.
1428    pub version: u8,
1429    /// A device's own clock, in seconds since the GPS epoch.
1430    pub device_time: u32,
1431    /// The seconds to add to a device's clock.
1432    pub time_correction: i32,
1433    /// The token that pairs a clock answer with its request.
1434    pub token: u8,
1435    /// Whether a clock request must be answered.
1436    pub ans_required: u8,
1437    /// The coded period between clock requests.
1438    pub period: u8,
1439    /// Whether a device manages its own clock periodicity.
1440    pub not_supported: u8,
1441    /// How many requests a resynchronization command asks for.
1442    pub transmissions: u8,
1443    /// The firmware a device reports running.
1444    pub firmware: u32,
1445    /// The hardware it runs on.
1446    pub hardware: u32,
1447    /// The moment or the delay a reboot is set for.
1448    pub reboot: u32,
1449    /// What a device makes of the upgrade image it holds.
1450    pub image_status: u8,
1451    /// The version it would run once that image is installed.
1452    pub next_version: u32,
1453    /// Whether an image answer carried a version.
1454    pub has_next_version: u8,
1455    /// The version a delete command names.
1456    pub delete_version: u32,
1457    /// Whether a device holds no valid image.
1458    pub no_valid_image: u8,
1459    /// Whether the version named is not the one held.
1460    pub invalid_version: u8,
1461    /// Which fragmentation session, 0 to 3.
1462    pub frag_index: u8,
1463    /// Which multicast groups may feed it, a bit for each.
1464    pub mc_group_bit_mask: u8,
1465    /// How many uncoded fragments a block was cut into.
1466    pub nb_frag: u16,
1467    /// How many bytes each fragment carries.
1468    pub frag_size: u8,
1469    /// Whether a device reports the block once it has it.
1470    pub ack_reception: u8,
1471    /// Which fragmentation algorithm to run.
1472    pub frag_algo: u8,
1473    /// The coded spread of the delay before a device answers.
1474    pub block_ack_delay: u8,
1475    /// How many bytes of padding the last fragment carries.
1476    pub padding: u8,
1477    /// The four bytes a server describes a block with.
1478    pub descriptor: [u8; 4],
1479    /// The session counter, which must rise for each new block.
1480    pub session_cnt: u16,
1481    /// The code over the block a device checks once it has it all.
1482    pub mic: [u8; 4],
1483    /// How many fragments arrived, coded, uncoded and repeated.
1484    pub received: u16,
1485    /// How many uncoded fragments are still missing.
1486    pub missing: u8,
1487    /// Whether the block's code did not check out.
1488    pub mic_error: u8,
1489    /// Whether a session ran out of memory to defragment with.
1490    pub memory_error: u8,
1491    /// Whether the session or group named does not exist on the device.
1492    pub no_session: u8,
1493    /// Whether the setup named a fragmentation algorithm the device does not run.
1494    pub unsupported_algorithm: u8,
1495    /// Whether the setup named a session index the device does not keep.
1496    pub unsupported_index: u8,
1497    /// Whether the descriptor is not one the device accepts.
1498    pub wrong_descriptor: u8,
1499    /// Whether the session counter repeats one already used for that index.
1500    pub session_replay: u8,
1501    /// Whether every device answers a status request, or only those still missing fragments.
1502    pub all_participants: u8,
1503    /// Whether this is one group record of a multicast status answer, which shares its
1504    /// identifier with the answer that names how many follow.
1505    pub status_item: u8,
1506    /// Which fragment of a session a data fragment carries, counting from one.
1507    pub fragment_n: u16,
1508    /// Which multicast group, 0 to 3.
1509    pub mc_group_id: u8,
1510    /// The address a group answers to.
1511    pub mc_addr: u32,
1512    /// A group's key, wrapped under the device's key encryption key.
1513    pub mc_key_encrypted: [u8; 16],
1514    /// The first frame counter a device accepts from a group.
1515    pub min_mc_fcnt: u32,
1516    /// The last one, which ends the group's life.
1517    pub max_mc_fcnt: u32,
1518    /// Which groups a status request or answer covers, a bit for each.
1519    pub group_mask: u8,
1520    /// How many groups a device holds in all.
1521    pub nb_total_groups: u8,
1522    /// Whether a device holds no group by the identifier named.
1523    pub id_error: u8,
1524    /// When a multicast window opens, in seconds since the GPS epoch.
1525    pub session_time: u32,
1526    /// How long it lasts at most, coded.
1527    pub time_out: u8,
1528    /// How often a device opens a ping slot inside a Class B window.
1529    pub periodicity: u8,
1530    /// Where a group listens, in hertz.
1531    pub dl_frequency_hz: u32,
1532    /// The data rate it listens at.
1533    pub data_rate: u8,
1534    /// How many seconds until a window opens.
1535    pub time_to_start: u32,
1536    /// Whether a session answer carried a start time.
1537    pub has_time_to_start: u8,
1538    /// Whether the data rate named is not one the device has.
1539    pub dr_error: u8,
1540    /// Whether the frequency named is not one it can use.
1541    pub freq_error: u8,
1542    /// Whether the window was to start at a time already past.
1543    pub start_missed: u8,
1544}
1545
1546impl Default for PamojaLorawanPackageCommand {
1547    fn default() -> PamojaLorawanPackageCommand {
1548        // Every field is a scalar or an array of them, so zero is the "carries nothing"
1549        // value the struct documents.
1550        unsafe { core::mem::zeroed() }
1551    }
1552}
1553
1554/// Reads one command of an application layer package.
1555///
1556/// A data fragment takes the whole message, as TS004-2.0.0 section 3 asks, and its bytes are
1557/// left in the caller's buffer: `out_command.fragment_n` says which fragment it is, and the
1558/// data starts three bytes into the message.
1559///
1560/// # Arguments
1561///
1562/// * `port` - which package the message arrived on.
1563/// * `uplink` - `1` when a device sent it, `0` when a server did.
1564/// * `payload` - the message, from this command's identifier on.
1565/// * `payload_len` - its length.
1566/// * `out_command` - receives the command.
1567/// * `out_taken` - receives how many bytes it took.
1568///
1569/// # Returns
1570///
1571/// [`PamojaStatus::Ok`] on success.
1572///
1573/// # Errors
1574///
1575/// Returns [`PamojaStatus::Codec`] for a message this package cannot read, and
1576/// [`PamojaStatus::InvalidArgument`] for a port that names no package or a null pointer.
1577///
1578/// # Safety
1579///
1580/// `payload` must point to `payload_len` readable bytes and both out pointers must be
1581/// writable.
1582#[no_mangle]
1583pub unsafe extern "C" fn pamoja_lorawan_package_parse(
1584    port: u8,
1585    uplink: u8,
1586    payload: *const u8,
1587    payload_len: usize,
1588    out_command: *mut PamojaLorawanPackageCommand,
1589    out_taken: *mut usize,
1590) -> PamojaStatus {
1591    if out_command.is_null() || out_taken.is_null() {
1592        return missing();
1593    }
1594    let bytes = match read_bytes(payload, payload_len) {
1595        Ok(bytes) => bytes,
1596        Err(status) => return status,
1597    };
1598    let direction = if uplink != 0 {
1599        Direction::Uplink
1600    } else {
1601        Direction::Downlink
1602    };
1603    let read = match port {
1604        CLOCK_PORT => ClockCommand::parse(direction, &bytes)
1605            .map(|(command, taken)| (clock_out(command), taken)),
1606        FIRMWARE_PORT => FirmwareCommand::parse(direction, &bytes)
1607            .map(|(command, taken)| (firmware_out(command), taken)),
1608        FRAGMENT_PORT => {
1609            FragCommand::parse(direction, &bytes).map(|(command, taken)| (frag_out(command), taken))
1610        }
1611        MULTICAST_PORT => {
1612            McCommand::parse(direction, &bytes).map(|(command, taken)| (mc_out(command), taken))
1613        }
1614        other => {
1615            set_last_error(format!("port {other} is not an application layer package"));
1616            return PamojaStatus::InvalidArgument;
1617        }
1618    };
1619    match read {
1620        Ok((command, taken)) => {
1621            *out_command = command;
1622            *out_taken = taken;
1623            PamojaStatus::Ok
1624        }
1625        Err(error) => {
1626            set_last_error(error.to_string());
1627            PamojaStatus::Codec
1628        }
1629    }
1630}
1631
1632/// Writes one command of an application layer package.
1633///
1634/// A data fragment carries its bytes separately: pass them as `data`, and the command's
1635/// `fragment_n` and `frag_index` say where they belong.
1636///
1637/// # Arguments
1638///
1639/// * `command` - the command, whose `port`, `cid` and `uplink` decide which fields are read.
1640/// * `data` - the bytes a data fragment carries, or null for every other command.
1641/// * `data_len` - how many.
1642/// * `out_payload` - receives the message.
1643/// * `capacity` - how many bytes that buffer holds.
1644/// * `out_len` - receives how many were written.
1645///
1646/// # Returns
1647///
1648/// [`PamojaStatus::Ok`] on success.
1649///
1650/// # Errors
1651///
1652/// Returns [`PamojaStatus::Codec`] for a command this build does not write, and
1653/// [`PamojaStatus::InvalidArgument`] for a null pointer or a buffer too small.
1654///
1655/// # Safety
1656///
1657/// `command` must be readable, `data` must point to `data_len` readable bytes or be null,
1658/// `out_payload` must point to `capacity` writable bytes, and `out_len` must be writable.
1659#[no_mangle]
1660pub unsafe extern "C" fn pamoja_lorawan_package_encode(
1661    command: *const PamojaLorawanPackageCommand,
1662    data: *const u8,
1663    data_len: usize,
1664    out_payload: *mut u8,
1665    capacity: usize,
1666    out_len: *mut usize,
1667) -> PamojaStatus {
1668    let (Some(command), false, false) =
1669        (command.as_ref(), out_payload.is_null(), out_len.is_null())
1670    else {
1671        return missing();
1672    };
1673    let carried = match read_bytes(data, data_len) {
1674        Ok(bytes) => bytes,
1675        Err(status) => return status,
1676    };
1677    let out = core::slice::from_raw_parts_mut(out_payload, capacity);
1678    let written = match command.port {
1679        CLOCK_PORT => clock_in(command).map(|c| c.encode(out)),
1680        FIRMWARE_PORT => firmware_in(command).map(|c| c.encode(out)),
1681        FRAGMENT_PORT => frag_in(command, &carried).map(|c| c.encode(out)),
1682        MULTICAST_PORT => mc_in(command).map(|c| c.encode(out)),
1683        other => {
1684            set_last_error(format!("port {other} is not an application layer package"));
1685            return PamojaStatus::InvalidArgument;
1686        }
1687    };
1688    match written {
1689        Some(Ok(len)) => {
1690            *out_len = len;
1691            PamojaStatus::Ok
1692        }
1693        Some(Err(error)) => {
1694            set_last_error(error.to_string());
1695            PamojaStatus::InvalidArgument
1696        }
1697        None => {
1698            set_last_error(format!(
1699                "command {:#04x} on port {} is not one this build writes",
1700                command.cid, command.port
1701            ));
1702            PamojaStatus::Codec
1703        }
1704    }
1705}
1706
1707/// Describes a clock synchronization command the way C holds it.
1708fn clock_out(command: ClockCommand) -> PamojaLorawanPackageCommand {
1709    let mut flat = PamojaLorawanPackageCommand {
1710        port: CLOCK_PORT,
1711        cid: command.cid(),
1712        uplink: u8::from(matches!(command.direction(), Direction::Uplink)),
1713        ..PamojaLorawanPackageCommand::default()
1714    };
1715    match command {
1716        ClockCommand::PackageVersionAns(version) => {
1717            flat.package = version.package;
1718            flat.version = version.version;
1719        }
1720        ClockCommand::AppTimeReq {
1721            device_time,
1722            ans_required,
1723            token,
1724        } => {
1725            flat.device_time = device_time;
1726            flat.ans_required = u8::from(ans_required);
1727            flat.token = token;
1728        }
1729        ClockCommand::AppTimeAns {
1730            time_correction,
1731            token,
1732        } => {
1733            flat.time_correction = time_correction;
1734            flat.token = token;
1735        }
1736        ClockCommand::DeviceAppTimePeriodicityReq { period } => flat.period = period,
1737        ClockCommand::DeviceAppTimePeriodicityAns {
1738            not_supported,
1739            device_time,
1740        } => {
1741            flat.not_supported = u8::from(not_supported);
1742            flat.device_time = device_time;
1743        }
1744        ClockCommand::ForceDeviceResyncCmd { transmissions } => flat.transmissions = transmissions,
1745        ClockCommand::PackageVersionReq => {}
1746    }
1747    flat
1748}
1749
1750/// Reads a clock synchronization command out of the record C holds.
1751fn clock_in(flat: &PamojaLorawanPackageCommand) -> Option<ClockCommand> {
1752    let up = flat.uplink != 0;
1753    Some(match (flat.cid, up) {
1754        (0x00, false) => ClockCommand::PackageVersionReq,
1755        (0x00, true) => ClockCommand::PackageVersionAns(PackageVersion {
1756            package: flat.package,
1757            version: flat.version,
1758        }),
1759        (0x01, true) => ClockCommand::AppTimeReq {
1760            device_time: flat.device_time,
1761            ans_required: flat.ans_required != 0,
1762            token: flat.token,
1763        },
1764        (0x01, false) => ClockCommand::AppTimeAns {
1765            time_correction: flat.time_correction,
1766            token: flat.token,
1767        },
1768        (0x02, false) => ClockCommand::DeviceAppTimePeriodicityReq {
1769            period: flat.period,
1770        },
1771        (0x02, true) => ClockCommand::DeviceAppTimePeriodicityAns {
1772            not_supported: flat.not_supported != 0,
1773            device_time: flat.device_time,
1774        },
1775        (0x03, false) => ClockCommand::ForceDeviceResyncCmd {
1776            transmissions: flat.transmissions,
1777        },
1778        _ => return None,
1779    })
1780}
1781
1782/// Describes a firmware management command the way C holds it.
1783fn firmware_out(command: FirmwareCommand) -> PamojaLorawanPackageCommand {
1784    let mut flat = PamojaLorawanPackageCommand {
1785        port: FIRMWARE_PORT,
1786        cid: command.cid(),
1787        uplink: u8::from(matches!(command.direction(), Direction::Uplink)),
1788        ..PamojaLorawanPackageCommand::default()
1789    };
1790    match command {
1791        FirmwareCommand::PackageVersionAns(version) => {
1792            flat.package = version.package;
1793            flat.version = version.version;
1794        }
1795        FirmwareCommand::DevVersionAns { firmware, hardware } => {
1796            flat.firmware = firmware;
1797            flat.hardware = hardware;
1798        }
1799        FirmwareCommand::DevRebootTimeReq { reboot_time }
1800        | FirmwareCommand::DevRebootTimeAns { reboot_time } => flat.reboot = reboot_time,
1801        FirmwareCommand::DevRebootCountdownReq { countdown }
1802        | FirmwareCommand::DevRebootCountdownAns { countdown } => flat.reboot = countdown,
1803        FirmwareCommand::DevUpgradeImageAns {
1804            status,
1805            next_version,
1806        } => {
1807            flat.image_status = status as u8;
1808            flat.next_version = next_version.unwrap_or(0);
1809            flat.has_next_version = u8::from(next_version.is_some());
1810        }
1811        FirmwareCommand::DevDeleteImageReq { version } => flat.delete_version = version,
1812        FirmwareCommand::DevDeleteImageAns(status) => {
1813            flat.no_valid_image = u8::from(status.no_valid_image);
1814            flat.invalid_version = u8::from(status.invalid_version);
1815        }
1816        FirmwareCommand::PackageVersionReq
1817        | FirmwareCommand::DevVersionReq
1818        | FirmwareCommand::DevUpgradeImageReq => {}
1819    }
1820    flat
1821}
1822
1823/// Reads a firmware management command out of the record C holds.
1824fn firmware_in(flat: &PamojaLorawanPackageCommand) -> Option<FirmwareCommand> {
1825    let up = flat.uplink != 0;
1826    Some(match (flat.cid, up) {
1827        (0x00, false) => FirmwareCommand::PackageVersionReq,
1828        (0x00, true) => FirmwareCommand::PackageVersionAns(PackageVersion {
1829            package: flat.package,
1830            version: flat.version,
1831        }),
1832        (0x01, false) => FirmwareCommand::DevVersionReq,
1833        (0x01, true) => FirmwareCommand::DevVersionAns {
1834            firmware: flat.firmware,
1835            hardware: flat.hardware,
1836        },
1837        (0x02, false) => FirmwareCommand::DevRebootTimeReq {
1838            reboot_time: flat.reboot,
1839        },
1840        (0x02, true) => FirmwareCommand::DevRebootTimeAns {
1841            reboot_time: flat.reboot,
1842        },
1843        (0x03, false) => FirmwareCommand::DevRebootCountdownReq {
1844            countdown: flat.reboot,
1845        },
1846        (0x03, true) => FirmwareCommand::DevRebootCountdownAns {
1847            countdown: flat.reboot,
1848        },
1849        (0x04, false) => FirmwareCommand::DevUpgradeImageReq,
1850        (0x04, true) => FirmwareCommand::DevUpgradeImageAns {
1851            status: UpImageStatus::from_bits(flat.image_status),
1852            next_version: (flat.has_next_version != 0).then_some(flat.next_version),
1853        },
1854        (0x05, false) => FirmwareCommand::DevDeleteImageReq {
1855            version: flat.delete_version,
1856        },
1857        (0x05, true) => FirmwareCommand::DevDeleteImageAns(DeleteStatus {
1858            no_valid_image: flat.no_valid_image != 0,
1859            invalid_version: flat.invalid_version != 0,
1860        }),
1861        _ => return None,
1862    })
1863}
1864
1865/// Describes a fragmentation command the way C holds it.
1866fn frag_out(command: FragCommand<'_>) -> PamojaLorawanPackageCommand {
1867    let mut flat = PamojaLorawanPackageCommand {
1868        port: FRAGMENT_PORT,
1869        cid: command.cid(),
1870        uplink: u8::from(matches!(command.direction(), Direction::Uplink)),
1871        ..PamojaLorawanPackageCommand::default()
1872    };
1873    match command {
1874        FragCommand::PackageVersionAns(version) => {
1875            flat.package = version.package;
1876            flat.version = version.version;
1877        }
1878        FragCommand::FragSessionStatusReq {
1879            frag_index,
1880            all_participants,
1881        } => {
1882            flat.frag_index = frag_index;
1883            flat.all_participants = u8::from(all_participants);
1884        }
1885        FragCommand::FragSessionStatusAns {
1886            frag_index,
1887            received,
1888            missing,
1889            mic_error,
1890            memory_error,
1891            no_session,
1892        } => {
1893            flat.frag_index = frag_index;
1894            flat.received = received;
1895            flat.missing = missing;
1896            flat.mic_error = u8::from(mic_error);
1897            flat.memory_error = u8::from(memory_error);
1898            flat.no_session = u8::from(no_session);
1899        }
1900        FragCommand::FragSessionSetupReq {
1901            frag_index,
1902            mc_group_bit_mask,
1903            nb_frag,
1904            frag_size,
1905            ack_reception,
1906            frag_algo,
1907            block_ack_delay,
1908            padding,
1909            descriptor,
1910            session_cnt,
1911            mic,
1912        } => {
1913            flat.frag_index = frag_index;
1914            flat.mc_group_bit_mask = mc_group_bit_mask;
1915            flat.nb_frag = nb_frag;
1916            flat.frag_size = frag_size;
1917            flat.ack_reception = u8::from(ack_reception);
1918            flat.frag_algo = frag_algo;
1919            flat.block_ack_delay = block_ack_delay;
1920            flat.padding = padding;
1921            flat.descriptor = descriptor;
1922            flat.session_cnt = session_cnt;
1923            flat.mic = mic;
1924        }
1925        FragCommand::FragSessionSetupAns(status) => {
1926            flat.frag_index = status.frag_index;
1927            flat.unsupported_algorithm = u8::from(status.unsupported_algorithm);
1928            flat.memory_error = u8::from(status.not_enough_memory);
1929            flat.unsupported_index = u8::from(status.unsupported_index);
1930            flat.wrong_descriptor = u8::from(status.wrong_descriptor);
1931            flat.session_replay = u8::from(status.session_replay);
1932        }
1933        FragCommand::FragSessionDeleteReq { frag_index } => flat.frag_index = frag_index,
1934        FragCommand::FragSessionDeleteAns {
1935            frag_index,
1936            no_session,
1937        } => {
1938            flat.frag_index = frag_index;
1939            flat.no_session = u8::from(no_session);
1940        }
1941        FragCommand::FragDataBlockReceivedReq {
1942            frag_index,
1943            mic_error,
1944        } => {
1945            flat.frag_index = frag_index;
1946            flat.mic_error = u8::from(mic_error);
1947        }
1948        FragCommand::FragDataBlockReceivedAns { frag_index } => flat.frag_index = frag_index,
1949        FragCommand::DataFragment {
1950            frag_index,
1951            n,
1952            data: _,
1953        } => {
1954            flat.frag_index = frag_index;
1955            flat.fragment_n = n;
1956        }
1957        FragCommand::PackageVersionReq => {}
1958    }
1959    flat
1960}
1961
1962/// Reads a fragmentation command out of the record C holds.
1963fn frag_in<'a>(flat: &PamojaLorawanPackageCommand, data: &'a [u8]) -> Option<FragCommand<'a>> {
1964    let up = flat.uplink != 0;
1965    Some(match (flat.cid, up) {
1966        (0x00, false) => FragCommand::PackageVersionReq,
1967        (0x00, true) => FragCommand::PackageVersionAns(PackageVersion {
1968            package: flat.package,
1969            version: flat.version,
1970        }),
1971        (0x01, false) => FragCommand::FragSessionStatusReq {
1972            frag_index: flat.frag_index,
1973            all_participants: flat.all_participants != 0,
1974        },
1975        (0x01, true) => FragCommand::FragSessionStatusAns {
1976            frag_index: flat.frag_index,
1977            received: flat.received,
1978            missing: flat.missing,
1979            mic_error: flat.mic_error != 0,
1980            memory_error: flat.memory_error != 0,
1981            no_session: flat.no_session != 0,
1982        },
1983        (0x02, false) => FragCommand::FragSessionSetupReq {
1984            frag_index: flat.frag_index,
1985            mc_group_bit_mask: flat.mc_group_bit_mask,
1986            nb_frag: flat.nb_frag,
1987            frag_size: flat.frag_size,
1988            ack_reception: flat.ack_reception != 0,
1989            frag_algo: flat.frag_algo,
1990            block_ack_delay: flat.block_ack_delay,
1991            padding: flat.padding,
1992            descriptor: flat.descriptor,
1993            session_cnt: flat.session_cnt,
1994            mic: flat.mic,
1995        },
1996        (0x02, true) => FragCommand::FragSessionSetupAns(SetupStatus {
1997            unsupported_algorithm: flat.unsupported_algorithm != 0,
1998            not_enough_memory: flat.memory_error != 0,
1999            unsupported_index: flat.unsupported_index != 0,
2000            wrong_descriptor: flat.wrong_descriptor != 0,
2001            session_replay: flat.session_replay != 0,
2002            frag_index: flat.frag_index,
2003        }),
2004        (0x03, false) => FragCommand::FragSessionDeleteReq {
2005            frag_index: flat.frag_index,
2006        },
2007        (0x03, true) => FragCommand::FragSessionDeleteAns {
2008            frag_index: flat.frag_index,
2009            no_session: flat.no_session != 0,
2010        },
2011        (0x04, true) => FragCommand::FragDataBlockReceivedReq {
2012            frag_index: flat.frag_index,
2013            mic_error: flat.mic_error != 0,
2014        },
2015        (0x04, false) => FragCommand::FragDataBlockReceivedAns {
2016            frag_index: flat.frag_index,
2017        },
2018        (0x08, false) => FragCommand::DataFragment {
2019            frag_index: flat.frag_index,
2020            n: flat.fragment_n,
2021            data,
2022        },
2023        _ => return None,
2024    })
2025}
2026
2027/// Describes a multicast setup command the way C holds it.
2028fn mc_out(command: McCommand) -> PamojaLorawanPackageCommand {
2029    let mut flat = PamojaLorawanPackageCommand {
2030        port: MULTICAST_PORT,
2031        cid: command.cid(),
2032        uplink: u8::from(matches!(command.direction(), Direction::Uplink)),
2033        ..PamojaLorawanPackageCommand::default()
2034    };
2035    match command {
2036        McCommand::PackageVersionAns(version) => {
2037            flat.package = version.package;
2038            flat.version = version.version;
2039        }
2040        McCommand::McGroupStatusReq { req_group_mask } => flat.group_mask = req_group_mask,
2041        McCommand::McGroupStatusAns {
2042            ans_group_mask,
2043            nb_total_groups,
2044        } => {
2045            flat.group_mask = ans_group_mask;
2046            flat.nb_total_groups = nb_total_groups;
2047        }
2048        McCommand::McGroupStatusItem {
2049            mc_group_id,
2050            mc_addr,
2051        } => {
2052            flat.status_item = 1;
2053            flat.mc_group_id = mc_group_id;
2054            flat.mc_addr = mc_addr;
2055        }
2056        McCommand::McGroupSetupReq {
2057            mc_group_id,
2058            mc_addr,
2059            mc_key_encrypted,
2060            min_mc_fcnt,
2061            max_mc_fcnt,
2062        } => {
2063            flat.mc_group_id = mc_group_id;
2064            flat.mc_addr = mc_addr;
2065            flat.mc_key_encrypted = mc_key_encrypted;
2066            flat.min_mc_fcnt = min_mc_fcnt;
2067            flat.max_mc_fcnt = max_mc_fcnt;
2068        }
2069        McCommand::McGroupSetupAns {
2070            mc_group_id,
2071            id_error,
2072        } => {
2073            flat.mc_group_id = mc_group_id;
2074            flat.id_error = u8::from(id_error);
2075        }
2076        McCommand::McGroupDeleteReq { mc_group_id } => flat.mc_group_id = mc_group_id,
2077        McCommand::McGroupDeleteAns {
2078            mc_group_id,
2079            group_undefined,
2080        } => {
2081            flat.mc_group_id = mc_group_id;
2082            flat.no_session = u8::from(group_undefined);
2083        }
2084        McCommand::McClassCSessionReq {
2085            mc_group_id,
2086            session_time,
2087            time_out,
2088            dl_frequency_hz,
2089            data_rate,
2090        } => {
2091            flat.mc_group_id = mc_group_id;
2092            flat.session_time = session_time;
2093            flat.time_out = time_out;
2094            flat.dl_frequency_hz = dl_frequency_hz;
2095            flat.data_rate = data_rate;
2096        }
2097        McCommand::McClassBSessionReq {
2098            mc_group_id,
2099            session_time,
2100            time_out,
2101            periodicity,
2102            dl_frequency_hz,
2103            data_rate,
2104        } => {
2105            flat.mc_group_id = mc_group_id;
2106            flat.session_time = session_time;
2107            flat.time_out = time_out;
2108            flat.periodicity = periodicity;
2109            flat.dl_frequency_hz = dl_frequency_hz;
2110            flat.data_rate = data_rate;
2111        }
2112        McCommand::McClassCSessionAns {
2113            status,
2114            time_to_start,
2115        }
2116        | McCommand::McClassBSessionAns {
2117            status,
2118            time_to_start,
2119        } => {
2120            flat.mc_group_id = status.mc_group_id;
2121            flat.dr_error = u8::from(status.dr_error);
2122            flat.freq_error = u8::from(status.freq_error);
2123            flat.no_session = u8::from(status.group_undefined);
2124            flat.start_missed = u8::from(status.start_missed);
2125            flat.time_to_start = time_to_start.unwrap_or(0);
2126            flat.has_time_to_start = u8::from(time_to_start.is_some());
2127        }
2128        McCommand::PackageVersionReq => {}
2129    }
2130    flat
2131}
2132
2133/// Reads a multicast setup command out of the record C holds.
2134fn mc_in(flat: &PamojaLorawanPackageCommand) -> Option<McCommand> {
2135    let up = flat.uplink != 0;
2136    let status = SessionStatus {
2137        mc_group_id: flat.mc_group_id,
2138        dr_error: flat.dr_error != 0,
2139        freq_error: flat.freq_error != 0,
2140        group_undefined: flat.no_session != 0,
2141        start_missed: flat.start_missed != 0,
2142    };
2143    let time_to_start = (flat.has_time_to_start != 0).then_some(flat.time_to_start);
2144    if flat.status_item != 0 {
2145        return Some(McCommand::McGroupStatusItem {
2146            mc_group_id: flat.mc_group_id,
2147            mc_addr: flat.mc_addr,
2148        });
2149    }
2150    Some(match (flat.cid, up) {
2151        (0x00, false) => McCommand::PackageVersionReq,
2152        (0x00, true) => McCommand::PackageVersionAns(PackageVersion {
2153            package: flat.package,
2154            version: flat.version,
2155        }),
2156        (0x01, false) => McCommand::McGroupStatusReq {
2157            req_group_mask: flat.group_mask,
2158        },
2159        (0x01, true) => McCommand::McGroupStatusAns {
2160            ans_group_mask: flat.group_mask,
2161            nb_total_groups: flat.nb_total_groups,
2162        },
2163        (0x02, false) => McCommand::McGroupSetupReq {
2164            mc_group_id: flat.mc_group_id,
2165            mc_addr: flat.mc_addr,
2166            mc_key_encrypted: flat.mc_key_encrypted,
2167            min_mc_fcnt: flat.min_mc_fcnt,
2168            max_mc_fcnt: flat.max_mc_fcnt,
2169        },
2170        (0x02, true) => McCommand::McGroupSetupAns {
2171            mc_group_id: flat.mc_group_id,
2172            id_error: flat.id_error != 0,
2173        },
2174        (0x03, false) => McCommand::McGroupDeleteReq {
2175            mc_group_id: flat.mc_group_id,
2176        },
2177        (0x03, true) => McCommand::McGroupDeleteAns {
2178            mc_group_id: flat.mc_group_id,
2179            group_undefined: flat.no_session != 0,
2180        },
2181        (0x04, false) => McCommand::McClassCSessionReq {
2182            mc_group_id: flat.mc_group_id,
2183            session_time: flat.session_time,
2184            time_out: flat.time_out,
2185            dl_frequency_hz: flat.dl_frequency_hz,
2186            data_rate: flat.data_rate,
2187        },
2188        (0x04, true) => McCommand::McClassCSessionAns {
2189            status,
2190            time_to_start,
2191        },
2192        (0x05, false) => McCommand::McClassBSessionReq {
2193            mc_group_id: flat.mc_group_id,
2194            session_time: flat.session_time,
2195            time_out: flat.time_out,
2196            periodicity: flat.periodicity,
2197            dl_frequency_hz: flat.dl_frequency_hz,
2198            data_rate: flat.data_rate,
2199        },
2200        (0x05, true) => McCommand::McClassBSessionAns {
2201            status,
2202            time_to_start,
2203        },
2204        _ => return None,
2205    })
2206}
2207
2208/// Reads one group record of a multicast status answer, TS005-2.0.0 section 4.2.
2209///
2210/// # Arguments
2211///
2212/// * `payload` - the message from the record on, five bytes or more.
2213/// * `payload_len` - its length.
2214/// * `out_command` - receives the record.
2215///
2216/// # Returns
2217///
2218/// [`PamojaStatus::Ok`] on success.
2219///
2220/// # Errors
2221///
2222/// Returns [`PamojaStatus::Codec`] when the message ends inside the record.
2223///
2224/// # Safety
2225///
2226/// `payload` must point to `payload_len` readable bytes and `out_command` must be writable.
2227#[no_mangle]
2228pub unsafe extern "C" fn pamoja_lorawan_package_status_item(
2229    payload: *const u8,
2230    payload_len: usize,
2231    out_command: *mut PamojaLorawanPackageCommand,
2232) -> PamojaStatus {
2233    if out_command.is_null() {
2234        return missing();
2235    }
2236    let bytes = match read_bytes(payload, payload_len) {
2237        Ok(bytes) => bytes,
2238        Err(status) => return status,
2239    };
2240    match McCommand::status_item(&bytes) {
2241        Ok((command, _)) => {
2242            *out_command = mc_out(command);
2243            PamojaStatus::Ok
2244        }
2245        Err(error) => {
2246            set_last_error(error.to_string());
2247            PamojaStatus::Codec
2248        }
2249    }
2250}
2251
2252#[cfg(test)]
2253mod tests {
2254    use super::*;
2255
2256    /// Reads a command of each package across the boundary and writes it back.
2257    #[test]
2258    fn a_command_of_each_package_crosses_the_boundary_both_ways() {
2259        unsafe {
2260            // Bytes taken from the specifications and from ChirpStack's own vectors.
2261            let cases: [(u8, u8, &[u8]); 6] = [
2262                (
2263                    PAMOJA_LORAWAN_CLOCK_PORT,
2264                    1,
2265                    &[0x01, 0x78, 0x56, 0x34, 0x12, 0x1A],
2266                ),
2267                (PAMOJA_LORAWAN_CLOCK_PORT, 0, &[0x03, 0x02]),
2268                (PAMOJA_LORAWAN_FIRMWARE_PORT, 0, &[0x03, 0x10, 0x0E, 0x00]),
2269                (
2270                    PAMOJA_LORAWAN_FRAGMENT_PORT,
2271                    0,
2272                    &[
2273                        0x02, 0x31, 0x00, 0x04, 0x80, 0x0D, 0x40, 0x01, 0x02, 0x03, 0x04, 0x80,
2274                        0x00, 0x01, 0x02, 0x03, 0x04,
2275                    ],
2276                ),
2277                (
2278                    PAMOJA_LORAWAN_FRAGMENT_PORT,
2279                    1,
2280                    &[0x01, 0x05, 0x00, 0xC4, 0x80],
2281                ),
2282                (
2283                    PAMOJA_LORAWAN_MULTICAST_PORT,
2284                    0,
2285                    &[
2286                        0x04, 0x02, 0x00, 0x04, 0x00, 0x00, 0x0F, 0x28, 0x76, 0x84, 0x05,
2287                    ],
2288                ),
2289            ];
2290
2291            for (port, uplink, bytes) in cases {
2292                let mut command = PamojaLorawanPackageCommand::default();
2293                let mut taken = 0usize;
2294                assert_eq!(
2295                    pamoja_lorawan_package_parse(
2296                        port,
2297                        uplink,
2298                        bytes.as_ptr(),
2299                        bytes.len(),
2300                        &mut command,
2301                        &mut taken,
2302                    ),
2303                    PamojaStatus::Ok,
2304                    "reading {bytes:02x?} on port {port}"
2305                );
2306                assert_eq!(taken, bytes.len());
2307
2308                let mut out = [0u8; 64];
2309                let mut written = 0usize;
2310                assert_eq!(
2311                    pamoja_lorawan_package_encode(
2312                        &command,
2313                        ptr::null(),
2314                        0,
2315                        out.as_mut_ptr(),
2316                        out.len(),
2317                        &mut written,
2318                    ),
2319                    PamojaStatus::Ok,
2320                    "writing it back"
2321                );
2322                assert_eq!(&out[..written], bytes, "on port {port}");
2323            }
2324        }
2325    }
2326
2327    /// A data fragment carries its bytes beside the command.
2328    #[test]
2329    fn a_data_fragment_carries_its_bytes_beside_the_command() {
2330        unsafe {
2331            let message = [0x08u8, 0x00, 0x84, 0x01, 0x02, 0x03, 0x04];
2332            let mut command = PamojaLorawanPackageCommand::default();
2333            let mut taken = 0usize;
2334            assert_eq!(
2335                pamoja_lorawan_package_parse(
2336                    PAMOJA_LORAWAN_FRAGMENT_PORT,
2337                    0,
2338                    message.as_ptr(),
2339                    message.len(),
2340                    &mut command,
2341                    &mut taken,
2342                ),
2343                PamojaStatus::Ok
2344            );
2345            assert_eq!((command.frag_index, command.fragment_n), (2, 1024));
2346            assert_eq!(taken, message.len());
2347
2348            let data = [0x01u8, 0x02, 0x03, 0x04];
2349            let mut out = [0u8; 16];
2350            let mut written = 0usize;
2351            assert_eq!(
2352                pamoja_lorawan_package_encode(
2353                    &command,
2354                    data.as_ptr(),
2355                    data.len(),
2356                    out.as_mut_ptr(),
2357                    out.len(),
2358                    &mut written,
2359                ),
2360                PamojaStatus::Ok
2361            );
2362            assert_eq!(&out[..written], &message);
2363        }
2364    }
2365
2366    /// A port that names no package, and a command this build does not write.
2367    #[test]
2368    fn a_port_that_names_no_package_is_refused() {
2369        unsafe {
2370            let bytes = [0x00u8];
2371            let mut command = PamojaLorawanPackageCommand::default();
2372            let mut taken = 0usize;
2373            assert_eq!(
2374                pamoja_lorawan_package_parse(
2375                    1,
2376                    0,
2377                    bytes.as_ptr(),
2378                    bytes.len(),
2379                    &mut command,
2380                    &mut taken,
2381                ),
2382                PamojaStatus::InvalidArgument
2383            );
2384
2385            let unknown = PamojaLorawanPackageCommand {
2386                port: PAMOJA_LORAWAN_CLOCK_PORT,
2387                cid: 0x7F,
2388                ..PamojaLorawanPackageCommand::default()
2389            };
2390            let mut out = [0u8; 16];
2391            let mut written = 0usize;
2392            assert_eq!(
2393                pamoja_lorawan_package_encode(
2394                    &unknown,
2395                    ptr::null(),
2396                    0,
2397                    out.as_mut_ptr(),
2398                    out.len(),
2399                    &mut written,
2400                ),
2401                PamojaStatus::Codec
2402            );
2403        }
2404    }
2405
2406    /// Runs a whole fragmentation session across the boundary, losing every third fragment.
2407    #[test]
2408    fn a_block_crosses_the_boundary_in_fragments_and_comes_back_whole() {
2409        unsafe {
2410            // A session of two dozen fragments, which is where this code starts to pay:
2411            // the specification warns it is weak below about twenty.
2412            let block: [u8; 384] = core::array::from_fn(|i| (i * 13 + 5) as u8);
2413            let (mut nb_frag, mut padding) = (0u16, 0u8);
2414            assert_eq!(
2415                pamoja_lorawan_frag_session(block.len(), 16, &mut nb_frag, &mut padding),
2416                PamojaStatus::Ok
2417            );
2418            assert_eq!((nb_frag, padding), (24, 0));
2419
2420            let mut session: *mut PamojaLorawanDefrag = ptr::null_mut();
2421            assert_eq!(
2422                pamoja_lorawan_defrag_new(nb_frag, 16, 12, &mut session),
2423                PamojaStatus::Ok
2424            );
2425
2426            let mut fragment = [0u8; 16];
2427            let mut done = 0u8;
2428            for n in 1..=nb_frag * 3 {
2429                if n % 3 == 0 {
2430                    continue;
2431                }
2432                assert_eq!(
2433                    pamoja_lorawan_frag_fragment(
2434                        block.as_ptr(),
2435                        block.len(),
2436                        16,
2437                        n,
2438                        fragment.as_mut_ptr(),
2439                        fragment.len(),
2440                    ),
2441                    PamojaStatus::Ok
2442                );
2443                assert_eq!(
2444                    pamoja_lorawan_defrag_fragment(
2445                        session,
2446                        n,
2447                        fragment.as_ptr(),
2448                        fragment.len(),
2449                        &mut done,
2450                    ),
2451                    PamojaStatus::Ok
2452                );
2453                if done == 1 {
2454                    break;
2455                }
2456            }
2457            assert_eq!(done, 1, "the coded fragments made up for the losses");
2458
2459            let (mut received, mut missing, mut finished) = (0u16, 0u16, 0u8);
2460            assert_eq!(
2461                pamoja_lorawan_defrag_status(session, &mut received, &mut missing, &mut finished),
2462                PamojaStatus::Ok
2463            );
2464            assert_eq!((missing, finished), (0, 1));
2465            assert!(received >= nb_frag);
2466
2467            let mut out = [0u8; 384];
2468            let mut len = 0usize;
2469            assert_eq!(
2470                pamoja_lorawan_defrag_block(session, out.as_mut_ptr(), out.len(), &mut len),
2471                PamojaStatus::Ok
2472            );
2473            assert_eq!(len, block.len());
2474            assert_eq!(out, block);
2475
2476            pamoja_lorawan_defrag_free(session);
2477        }
2478    }
2479
2480    /// The keys of a multicast group and the code over a block, across the boundary.
2481    #[test]
2482    fn a_group_key_and_a_block_code_cross_the_boundary() {
2483        unsafe {
2484            let app_key = [0x2Bu8; 16];
2485            let group_key = [0x77u8; 16];
2486            let mut root = [0u8; 16];
2487            let mut ke = [0u8; 16];
2488            assert_eq!(
2489                pamoja_lorawan_mc_root_key(app_key.as_ptr(), 0, root.as_mut_ptr()),
2490                PamojaStatus::Ok
2491            );
2492            assert_eq!(
2493                pamoja_lorawan_mc_ke_key(root.as_ptr(), ke.as_mut_ptr()),
2494                PamojaStatus::Ok
2495            );
2496
2497            let mut wrapped = [0u8; 16];
2498            let mut unwrapped = [0u8; 16];
2499            assert_eq!(
2500                pamoja_lorawan_wrap_mc_key(ke.as_ptr(), group_key.as_ptr(), wrapped.as_mut_ptr()),
2501                PamojaStatus::Ok
2502            );
2503            assert_eq!(
2504                pamoja_lorawan_mc_key(ke.as_ptr(), wrapped.as_ptr(), unwrapped.as_mut_ptr()),
2505                PamojaStatus::Ok
2506            );
2507            assert_eq!(unwrapped, group_key, "the device gets the group key back");
2508
2509            let mut app = [0u8; 16];
2510            let mut nwk = [0u8; 16];
2511            assert_eq!(
2512                pamoja_lorawan_mc_app_s_key(group_key.as_ptr(), 0x2601_1BDA, app.as_mut_ptr()),
2513                PamojaStatus::Ok
2514            );
2515            assert_eq!(
2516                pamoja_lorawan_mc_nwk_s_key(group_key.as_ptr(), 0x2601_1BDA, nwk.as_mut_ptr()),
2517                PamojaStatus::Ok
2518            );
2519            assert_ne!(app, nwk);
2520
2521            // The block code, taken in two pieces, matches the one taken in a single call.
2522            let mut block_key = [0u8; 16];
2523            assert_eq!(
2524                pamoja_lorawan_data_block_int_key(app_key.as_ptr(), block_key.as_mut_ptr()),
2525                PamojaStatus::Ok
2526            );
2527            let block = [0xA5u8; 40];
2528            let descriptor = *b"PJU1";
2529            let mut whole: *mut PamojaLorawanBlockMic = ptr::null_mut();
2530            assert_eq!(
2531                pamoja_lorawan_block_mic_start(
2532                    block_key.as_ptr(),
2533                    7,
2534                    2,
2535                    descriptor.as_ptr(),
2536                    block.len() as u32,
2537                    &mut whole,
2538                ),
2539                PamojaStatus::Ok
2540            );
2541            assert_eq!(
2542                pamoja_lorawan_block_mic_update(whole, block.as_ptr(), block.len()),
2543                PamojaStatus::Ok
2544            );
2545            let mut one = [0u8; 4];
2546            assert_eq!(
2547                pamoja_lorawan_block_mic_finish(whole, one.as_mut_ptr()),
2548                PamojaStatus::Ok
2549            );
2550
2551            let mut pieces: *mut PamojaLorawanBlockMic = ptr::null_mut();
2552            assert_eq!(
2553                pamoja_lorawan_block_mic_start(
2554                    block_key.as_ptr(),
2555                    7,
2556                    2,
2557                    descriptor.as_ptr(),
2558                    block.len() as u32,
2559                    &mut pieces,
2560                ),
2561                PamojaStatus::Ok
2562            );
2563            for chunk in block.chunks(7) {
2564                assert_eq!(
2565                    pamoja_lorawan_block_mic_update(pieces, chunk.as_ptr(), chunk.len()),
2566                    PamojaStatus::Ok
2567                );
2568            }
2569            let mut two = [0u8; 4];
2570            assert_eq!(
2571                pamoja_lorawan_block_mic_finish(pieces, two.as_mut_ptr()),
2572                PamojaStatus::Ok
2573            );
2574            assert_eq!(one, two);
2575        }
2576    }
2577
2578    /// A clock correction and a firmware reboot, across the boundary.
2579    #[test]
2580    fn a_clock_correction_and_a_reboot_cross_the_boundary() {
2581        unsafe {
2582            let mut sync: *mut PamojaLorawanClockSync = ptr::null_mut();
2583            assert_eq!(
2584                pamoja_lorawan_clock_sync_new(0, &mut sync),
2585                PamojaStatus::Ok
2586            );
2587
2588            let mut request = [0u8; 8];
2589            let mut len = 0usize;
2590            assert_eq!(
2591                pamoja_lorawan_clock_sync_request(
2592                    sync,
2593                    1_000,
2594                    1,
2595                    request.as_mut_ptr(),
2596                    request.len(),
2597                    &mut len,
2598                ),
2599                PamojaStatus::Ok
2600            );
2601            assert_eq!(len, 6);
2602
2603            // The server answers the token the request carried.
2604            let answer = [0x01u8, 12, 0, 0, 0, 0];
2605            let (mut correction, mut has, mut more, mut resync, mut due) =
2606                (0i32, 0u8, 0u8, 0u8, 0u8);
2607            assert_eq!(
2608                pamoja_lorawan_clock_sync_heard(
2609                    sync,
2610                    answer.as_ptr(),
2611                    answer.len(),
2612                    &mut correction,
2613                    &mut has,
2614                    &mut more,
2615                    &mut resync,
2616                    &mut due,
2617                ),
2618                PamojaStatus::Ok
2619            );
2620            assert_eq!((correction, has, more), (12, 1, 0));
2621            let mut token = 0u8;
2622            assert_eq!(
2623                pamoja_lorawan_clock_sync_status(
2624                    sync,
2625                    &mut token,
2626                    ptr::null_mut(),
2627                    ptr::null_mut()
2628                ),
2629                PamojaStatus::Ok
2630            );
2631            assert_eq!(token, 1, "the token moved on with the correction");
2632            pamoja_lorawan_clock_sync_free(sync);
2633
2634            let mut manager: *mut PamojaLorawanFirmware = ptr::null_mut();
2635            assert_eq!(
2636                pamoja_lorawan_firmware_new(0x0001_0203, 0xAABB_CCDD, &mut manager),
2637                PamojaStatus::Ok
2638            );
2639            assert_eq!(
2640                pamoja_lorawan_firmware_set_image(manager, PAMOJA_LORAWAN_IMAGE_VALID, 0x0001_0204),
2641                PamojaStatus::Ok
2642            );
2643
2644            // DevUpgradeImageReq, then a reboot in an hour.
2645            let asked = [0x04u8, 0x03, 0x10, 0x0E, 0x00];
2646            let mut answers = [0u8; 32];
2647            let mut written = 0usize;
2648            assert_eq!(
2649                pamoja_lorawan_firmware_heard(
2650                    manager,
2651                    asked.as_ptr(),
2652                    asked.len(),
2653                    0,
2654                    0,
2655                    answers.as_mut_ptr(),
2656                    answers.len(),
2657                    &mut written,
2658                ),
2659                PamojaStatus::Ok
2660            );
2661            assert_eq!(&answers[..2], &[0x04, PAMOJA_LORAWAN_IMAGE_VALID]);
2662
2663            let (mut left, mut has_in) = (0u32, 0u8);
2664            assert_eq!(
2665                pamoja_lorawan_firmware_reboot(
2666                    manager,
2667                    ptr::null_mut(),
2668                    ptr::null_mut(),
2669                    &mut left,
2670                    &mut has_in,
2671                    ptr::null_mut(),
2672                ),
2673                PamojaStatus::Ok
2674            );
2675            assert_eq!((left, has_in), (3_600, 1));
2676            pamoja_lorawan_firmware_free(manager);
2677        }
2678    }
2679}