Skip to main content

pamoja_ffi/
gateway_station.rs

1//! The C ABI for the LoRa Basics Station protocol.
2//!
3//! These functions wrap [`pamoja_gateway::station`] for callers that reach the SDK through the
4//! flat C boundary. A station and its network server exchange JSON messages over a websocket
5//! the caller owns, so nothing here opens a socket: a message crosses as an opaque handle,
6//! built from a frame the radio heard, read from text that arrived, or built from its fields,
7//! and written back out as the text to send.
8//!
9//! A handle is used rather than one flat struct because the messages differ too much to share
10//! one shape: a configuration carries a table of data rates and a schedule carries a list of
11//! frames, while a join request carries eight fixed fields. [`PamojaGatewayStationFields`]
12//! holds every fixed field of every kind, and the calls below add and read the text, the bytes
13//! and the lists a kind carries besides.
14//!
15//! To build a message, fill the fields of its kind, call
16//! [`pamoja_gateway_station_message_new`], then add its text, bytes and entries. To read one,
17//! take its fields with [`pamoja_gateway_station_message_fields`], then read whichever text,
18//! bytes and entries the counts there say it carries.
19
20use std::os::raw::c_char;
21use std::ptr;
22
23use pamoja_gateway::station::{eui_of, id6, Broadcast, Discovery, Levels, Message, Router, Xtime};
24use pamoja_gateway::udp::Eui;
25
26use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
27
28/// A join request the station heard.
29pub const PAMOJA_GATEWAY_STATION_JOIN_REQUEST: u8 = 0;
30
31/// A data frame the station heard.
32pub const PAMOJA_GATEWAY_STATION_UPLINK: u8 = 1;
33
34/// A frame of a kind this protocol does not describe, carried whole.
35pub const PAMOJA_GATEWAY_STATION_PROPRIETARY: u8 = 2;
36
37/// What the station reports about itself when a session opens.
38pub const PAMOJA_GATEWAY_STATION_VERSION: u8 = 3;
39
40/// How the server tells the station to configure its radios.
41pub const PAMOJA_GATEWAY_STATION_ROUTER_CONFIG: u8 = 4;
42
43/// A frame the server asks the station to transmit.
44pub const PAMOJA_GATEWAY_STATION_DOWNLINK: u8 = 5;
45
46/// Frames the server asks the station to transmit to a group.
47pub const PAMOJA_GATEWAY_STATION_SCHEDULE: u8 = 6;
48
49/// What became of a frame the station was asked to transmit.
50pub const PAMOJA_GATEWAY_STATION_TRANSMITTED: u8 = 7;
51
52/// The clock the two keep between them.
53pub const PAMOJA_GATEWAY_STATION_TIME_SYNC: u8 = 8;
54
55/// A kind this build does not model, readable only as its text.
56pub const PAMOJA_GATEWAY_STATION_OTHER: u8 = 9;
57
58/// The protocol version a station reports.
59pub const PAMOJA_GATEWAY_STATION_PROTOCOL_VERSION: u32 = 2;
60
61// The header carries this as a literal, because cbindgen drops a constant whose value
62// names another crate's. This holds it to what that crate says.
63const _: () =
64    assert!(PAMOJA_GATEWAY_STATION_PROTOCOL_VERSION == pamoja_gateway::station::PROTOCOL_VERSION);
65
66/// The station software a version reports.
67pub const PAMOJA_GATEWAY_STATION_TEXT_STATION: u8 = 0;
68
69/// The firmware a version reports.
70pub const PAMOJA_GATEWAY_STATION_TEXT_FIRMWARE: u8 = 1;
71
72/// The package a version reports.
73pub const PAMOJA_GATEWAY_STATION_TEXT_PACKAGE: u8 = 2;
74
75/// The hardware model a version reports.
76pub const PAMOJA_GATEWAY_STATION_TEXT_MODEL: u8 = 3;
77
78/// What a version says the station can do, such as `gps`.
79pub const PAMOJA_GATEWAY_STATION_TEXT_FEATURES: u8 = 4;
80
81/// The region a configuration names, such as `EU868`.
82pub const PAMOJA_GATEWAY_STATION_TEXT_REGION: u8 = 5;
83
84/// The concentrator a configuration is written for, such as `sx1301/1`.
85pub const PAMOJA_GATEWAY_STATION_TEXT_HWSPEC: u8 = 6;
86
87/// The word a message calls itself on the wire, which is what names a kind this build does
88/// not model.
89pub const PAMOJA_GATEWAY_STATION_TEXT_MSGTYPE: u8 = 7;
90
91/// A message either side of a session sends.
92pub struct PamojaGatewayStationMessage {
93    message: Message,
94}
95
96/// How a station heard a packet, as it reports it.
97#[repr(C)]
98#[derive(Clone, Copy, Debug)]
99pub struct PamojaGatewayStationLevels {
100    /// The radio the packet arrived on, which an answer goes back out on.
101    pub rctx: i64,
102    /// The station clock, in microseconds.
103    pub xtime: i64,
104    /// The GPS time, when `has_gpstime`.
105    pub gpstime: i64,
106    /// Whether the station has a GPS time.
107    pub has_gpstime: bool,
108    /// The received signal strength, in dBm.
109    pub rssi: f64,
110    /// The signal-to-noise ratio, in dB.
111    pub snr: f64,
112}
113
114/// A receive window, or the ping slot of a class B downlink: a data rate and a frequency.
115#[repr(C)]
116#[derive(Clone, Copy, Debug)]
117pub struct PamojaGatewayStationWindow {
118    /// The data rate.
119    pub data_rate: u8,
120    /// The frequency in hertz.
121    pub frequency_hz: u32,
122    /// Whether the message names this window at all.
123    pub present: bool,
124}
125
126/// One number of a configuration's data-rate table.
127#[repr(C)]
128#[derive(Clone, Copy, Debug)]
129pub struct PamojaGatewayStationDataRate {
130    /// The spreading factor, 0 for FSK.
131    pub spreading_factor: u8,
132    /// The bandwidth in hertz.
133    pub bandwidth_hz: u32,
134    /// Whether the rate is used only for downlinks.
135    pub downlink_only: bool,
136    /// Whether the table defines this number at all. An undefined one keeps its place so the
137    /// numbers after it keep theirs.
138    pub defined: bool,
139}
140
141/// A range of join identifiers whose join requests a station forwards, both ends included.
142#[repr(C)]
143#[derive(Clone, Copy, Debug)]
144pub struct PamojaGatewayStationJoinRange {
145    /// The first identifier, read as a big-endian number.
146    pub first: u64,
147    /// The last identifier, read the same way.
148    pub last: u64,
149}
150
151/// One frame of a schedule, apart from its bytes.
152#[repr(C)]
153#[derive(Clone, Copy, Debug)]
154pub struct PamojaGatewayStationBroadcast {
155    /// The data rate to transmit at.
156    pub data_rate: u8,
157    /// The frequency to transmit on, in hertz.
158    pub frequency_hz: u32,
159    /// How urgent it is.
160    pub priority: u8,
161    /// When to transmit it, in microseconds since the GPS epoch, when `has_gpstime`.
162    pub gpstime: i64,
163    /// Whether it names a GPS time.
164    pub has_gpstime: bool,
165    /// The radio to transmit on, when `has_rctx`.
166    pub rctx: i64,
167    /// Whether it names a radio.
168    pub has_rctx: bool,
169}
170
171/// A station clock value taken apart.
172#[repr(C)]
173#[derive(Clone, Copy, Debug)]
174pub struct PamojaGatewayStationXtime {
175    /// The radio unit the time was read on, 0 to 127.
176    pub unit: u8,
177    /// The run of the station the time belongs to.
178    pub session: u8,
179    /// The microseconds the run had counted, below 2^48.
180    pub micros: u64,
181}
182
183/// The identities a discovery answer names.
184#[repr(C)]
185#[derive(Clone, Copy, Debug)]
186pub struct PamojaGatewayStationRouterIds {
187    /// The station, as the server read it, when `has_router`.
188    pub router: [u8; 8],
189    /// Whether the answer names the station.
190    pub has_router: bool,
191    /// The server endpoint carrying the session, when `has_muxs`.
192    pub muxs: [u8; 8],
193    /// Whether the answer names the endpoint.
194    pub has_muxs: bool,
195}
196
197/// Every fixed field a message carries, for any kind.
198///
199/// A kind uses the fields its protocol message names and leaves the rest at zero. The text,
200/// the bytes and the lists a kind carries are read and added with the calls that take a
201/// message handle.
202#[repr(C)]
203#[derive(Clone, Copy, Debug)]
204pub struct PamojaGatewayStationFields {
205    /// Which kind this is, one of the `PAMOJA_GATEWAY_STATION_*` kind constants.
206    pub kind: u8,
207    /// The MAC header byte, for a join request or a data frame.
208    pub mhdr: u8,
209    /// The application being joined, for a join request.
210    pub join_eui: [u8; 8],
211    /// The device, for a join request, a downlink, or a transmission report.
212    pub dev_eui: [u8; 8],
213    /// The nonce a join request used.
214    pub dev_nonce: u16,
215    /// The address a data frame came from.
216    pub dev_addr: i32,
217    /// The frame control byte.
218    pub fctrl: u8,
219    /// The frame counter, as the sixteen bits on the air.
220    pub fcnt: u16,
221    /// The port a data frame was sent on, when `has_fport`.
222    pub fport: u8,
223    /// Whether the frame carried a port at all.
224    pub has_fport: bool,
225    /// The message integrity code.
226    pub mic: i32,
227    /// The data rate a frame arrived at.
228    pub data_rate: u8,
229    /// The frequency a frame arrived on, in hertz.
230    pub frequency_hz: u32,
231    /// How it was heard, for the kinds a station sends up.
232    pub levels: PamojaGatewayStationLevels,
233    /// Which class of downlink this is: 0 for A, 1 for B, 2 for C.
234    pub class: u8,
235    /// The identifier a downlink and its transmission report share.
236    pub diid: i64,
237    /// The delay before the first receive window, in seconds, when `has_rx_delay`.
238    pub rx_delay: u8,
239    /// Whether a downlink named a receive delay.
240    pub has_rx_delay: bool,
241    /// How urgent a downlink is.
242    pub priority: u8,
243    /// The protocol version a version message reports.
244    pub protocol: u32,
245    /// The highest radiated power a configuration allows, in dBm.
246    pub max_eirp: f64,
247    /// The lowest frequency a configuration allows, in hertz.
248    pub freq_min_hz: u32,
249    /// The highest frequency a configuration allows, in hertz.
250    pub freq_max_hz: u32,
251    /// Whether a configuration names the networks whose data frames are forwarded. When it
252    /// does not, every network's are, and the list is written as `null`.
253    pub filters_networks: bool,
254    /// How many networks a configuration names.
255    pub net_id_count: usize,
256    /// How many join identifier ranges a configuration names.
257    pub join_range_count: usize,
258    /// How many numbers a configuration's data-rate table holds.
259    pub data_rate_count: usize,
260    /// How many frames a schedule carries.
261    pub broadcast_count: usize,
262    /// The first receive window a downlink names.
263    pub rx1: PamojaGatewayStationWindow,
264    /// The second receive window a downlink names.
265    pub rx2: PamojaGatewayStationWindow,
266    /// The ping slot a class B downlink goes out in.
267    pub ping_slot: PamojaGatewayStationWindow,
268    /// The station clock in microseconds, when `has_xtime`: the uplink a downlink answers,
269    /// the moment a reported frame went out, or the one a time sync carries.
270    pub xtime: i64,
271    /// Whether the message carries a station clock.
272    pub has_xtime: bool,
273    /// The radio a downlink goes out on or a report says it went out on, when `has_rctx`.
274    pub rctx: i64,
275    /// Whether the message names a radio.
276    pub has_rctx: bool,
277    /// The GPS time in microseconds since the GPS epoch, when `has_gpstime`.
278    pub gpstime: i64,
279    /// Whether the message carries a GPS time.
280    pub has_gpstime: bool,
281    /// When a reported frame went out, in seconds, or the station time a time sync carries,
282    /// in microseconds, when `has_txtime`.
283    pub txtime: f64,
284    /// Whether the message carries that time.
285    pub has_txtime: bool,
286}
287
288/// Reads a frame the radio heard into the message that reports it.
289///
290/// # Arguments
291///
292/// * `frame` - the bytes as they arrived, header through integrity code.
293/// * `frame_len` - how many bytes `frame` holds.
294/// * `data_rate` - the data rate it arrived at.
295/// * `frequency_hz` - the frequency it arrived on, in hertz.
296/// * `levels` - how it was heard.
297///
298/// # Returns
299///
300/// A handle the caller must release with [`pamoja_gateway_station_message_free`], or null on
301/// failure with the reason available from
302/// [`pamoja_last_error_message`](crate::pamoja_last_error_message).
303///
304/// # Safety
305///
306/// `frame` must point to `frame_len` readable bytes.
307#[no_mangle]
308pub unsafe extern "C" fn pamoja_gateway_station_heard(
309    frame: *const u8,
310    frame_len: usize,
311    data_rate: u8,
312    frequency_hz: u32,
313    levels: PamojaGatewayStationLevels,
314) -> *mut PamojaGatewayStationMessage {
315    let Ok(frame) = read_bytes(frame, frame_len) else {
316        return ptr::null_mut();
317    };
318    match Message::heard(&frame, data_rate, frequency_hz, levels_of(levels)) {
319        Ok(message) => held(message),
320        Err(error) => {
321            set_last_error(error.to_string());
322            ptr::null_mut()
323        }
324    }
325}
326
327/// Reads a message that arrived over the websocket.
328///
329/// # Arguments
330///
331/// * `text` - the message text.
332/// * `text_len` - how many bytes `text` holds.
333///
334/// # Returns
335///
336/// A handle the caller must release with [`pamoja_gateway_station_message_free`], or null on
337/// failure.
338///
339/// # Safety
340///
341/// `text` must point to `text_len` readable bytes.
342#[no_mangle]
343pub unsafe extern "C" fn pamoja_gateway_station_message_parse(
344    text: *const u8,
345    text_len: usize,
346) -> *mut PamojaGatewayStationMessage {
347    let Ok(text) = read_bytes(text, text_len) else {
348        return ptr::null_mut();
349    };
350    match Message::from_json(&text) {
351        Ok(message) => held(message),
352        Err(error) => {
353            set_last_error(error.to_string());
354            ptr::null_mut()
355        }
356    }
357}
358
359/// Builds a message of any kind from its fixed fields.
360///
361/// The message starts with no text, no bytes, and empty lists; add those with the
362/// `pamoja_gateway_station_message_set_*` and `pamoja_gateway_station_message_add_*` calls.
363/// The counts in `fields` are not read.
364///
365/// # Arguments
366///
367/// * `fields` - the fields, whose `kind` names the message.
368///
369/// # Returns
370///
371/// A handle the caller must release with [`pamoja_gateway_station_message_free`], or null
372/// when `fields` is null or names no kind.
373///
374/// # Safety
375///
376/// `fields` must point to a readable [`PamojaGatewayStationFields`] or be null.
377#[no_mangle]
378pub unsafe extern "C" fn pamoja_gateway_station_message_new(
379    fields: *const PamojaGatewayStationFields,
380) -> *mut PamojaGatewayStationMessage {
381    let Some(fields) = fields.as_ref() else {
382        set_last_error("fields must not be null".to_owned());
383        return ptr::null_mut();
384    };
385    match message_from(fields) {
386        Ok(message) => held(message),
387        Err(why) => {
388            set_last_error(why);
389            ptr::null_mut()
390        }
391    }
392}
393
394/// Writes a message as the websocket carries it.
395///
396/// # Arguments
397///
398/// * `message` - the message.
399/// * `out_text` - receives the text, which the caller releases with
400///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
401///
402/// # Returns
403///
404/// [`PamojaStatus::Ok`] once written.
405///
406/// # Safety
407///
408/// `message` must be a live handle, and `out_text` must point to writable storage.
409#[no_mangle]
410pub unsafe extern "C" fn pamoja_gateway_station_message_json(
411    message: *const PamojaGatewayStationMessage,
412    out_text: *mut *mut PamojaBuffer,
413) -> PamojaStatus {
414    if message.is_null() || out_text.is_null() {
415        set_last_error("message and out_text must not be null".to_owned());
416        return PamojaStatus::InvalidArgument;
417    }
418    *out_text = PamojaBuffer::into_raw((*message).message.to_json().into_bytes());
419    PamojaStatus::Ok
420}
421
422/// Returns which kind a message is.
423///
424/// # Arguments
425///
426/// * `message` - the message.
427///
428/// # Returns
429///
430/// One of the `PAMOJA_GATEWAY_STATION_*` kind constants, or
431/// [`PAMOJA_GATEWAY_STATION_OTHER`] when `message` is null.
432///
433/// # Safety
434///
435/// `message` must be a live handle or null.
436#[no_mangle]
437pub unsafe extern "C" fn pamoja_gateway_station_message_kind(
438    message: *const PamojaGatewayStationMessage,
439) -> u8 {
440    message
441        .as_ref()
442        .map_or(PAMOJA_GATEWAY_STATION_OTHER, |held| kind_of(&held.message))
443}
444
445/// Reads the fixed fields a message carries, and how many entries each of its lists holds.
446///
447/// # Arguments
448///
449/// * `message` - the message.
450/// * `out_fields` - receives the fields.
451///
452/// # Returns
453///
454/// [`PamojaStatus::Ok`] once read.
455///
456/// # Safety
457///
458/// `message` must be a live handle, and `out_fields` must point to writable storage.
459#[no_mangle]
460pub unsafe extern "C" fn pamoja_gateway_station_message_fields(
461    message: *const PamojaGatewayStationMessage,
462    out_fields: *mut PamojaGatewayStationFields,
463) -> PamojaStatus {
464    if message.is_null() || out_fields.is_null() {
465        set_last_error("message and out_fields must not be null".to_owned());
466        return PamojaStatus::InvalidArgument;
467    }
468    *out_fields = fields_of(&(*message).message);
469    PamojaStatus::Ok
470}
471
472/// Returns the bytes a message carries: the payload of a frame, or the frame to transmit.
473///
474/// # Arguments
475///
476/// * `message` - the message.
477/// * `out_payload` - receives the bytes, which the caller releases with
478///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
479///
480/// # Returns
481///
482/// [`PamojaStatus::Ok`] once written, with an empty buffer for a kind carrying none.
483///
484/// # Safety
485///
486/// `message` must be a live handle, and `out_payload` must point to writable storage.
487#[no_mangle]
488pub unsafe extern "C" fn pamoja_gateway_station_message_payload(
489    message: *const PamojaGatewayStationMessage,
490    out_payload: *mut *mut PamojaBuffer,
491) -> PamojaStatus {
492    if message.is_null() || out_payload.is_null() {
493        set_last_error("message and out_payload must not be null".to_owned());
494        return PamojaStatus::InvalidArgument;
495    }
496    let bytes = match &(*message).message {
497        Message::Uplink { payload, .. } | Message::Proprietary { payload, .. } => payload.clone(),
498        Message::Downlink { pdu, .. } => pdu.clone(),
499        _ => Vec::new(),
500    };
501    *out_payload = PamojaBuffer::into_raw(bytes);
502    PamojaStatus::Ok
503}
504
505/// Returns the frame options a data frame carries.
506///
507/// # Arguments
508///
509/// * `message` - the message.
510/// * `out_options` - receives the bytes, which the caller releases with
511///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
512///
513/// # Returns
514///
515/// [`PamojaStatus::Ok`] once written, with an empty buffer for a kind carrying none.
516///
517/// # Safety
518///
519/// `message` must be a live handle, and `out_options` must point to writable storage.
520#[no_mangle]
521pub unsafe extern "C" fn pamoja_gateway_station_message_options(
522    message: *const PamojaGatewayStationMessage,
523    out_options: *mut *mut PamojaBuffer,
524) -> PamojaStatus {
525    if message.is_null() || out_options.is_null() {
526        set_last_error("message and out_options must not be null".to_owned());
527        return PamojaStatus::InvalidArgument;
528    }
529    let bytes = match &(*message).message {
530        Message::Uplink { fopts, .. } => fopts.clone(),
531        _ => Vec::new(),
532    };
533    *out_options = PamojaBuffer::into_raw(bytes);
534    PamojaStatus::Ok
535}
536
537/// Returns a piece of text a message carries.
538///
539/// # Arguments
540///
541/// * `message` - the message.
542/// * `field` - which text, one of the `PAMOJA_GATEWAY_STATION_TEXT_*` constants.
543/// * `out_text` - receives the text, which the caller releases with
544///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
545///
546/// # Returns
547///
548/// [`PamojaStatus::Ok`] once written, with empty text for a field the kind does not carry, or
549/// [`PamojaStatus::InvalidArgument`] when `field` names no text.
550///
551/// # Safety
552///
553/// `message` must be a live handle, and `out_text` must point to writable storage.
554#[no_mangle]
555pub unsafe extern "C" fn pamoja_gateway_station_message_text(
556    message: *const PamojaGatewayStationMessage,
557    field: u8,
558    out_text: *mut *mut PamojaBuffer,
559) -> PamojaStatus {
560    if message.is_null() || out_text.is_null() {
561        set_last_error("message and out_text must not be null".to_owned());
562        return PamojaStatus::InvalidArgument;
563    }
564    if field > PAMOJA_GATEWAY_STATION_TEXT_MSGTYPE {
565        set_last_error(format!("{field} names no text a message carries"));
566        return PamojaStatus::InvalidArgument;
567    }
568    let text = text_of(&(*message).message, field);
569    *out_text = PamojaBuffer::into_raw(text.as_bytes().to_vec());
570    PamojaStatus::Ok
571}
572
573/// Sets a piece of text a message carries.
574///
575/// # Arguments
576///
577/// * `message` - the message.
578/// * `field` - which text, one of the `PAMOJA_GATEWAY_STATION_TEXT_*` constants.
579/// * `text` - the text, UTF-8.
580/// * `text_len` - how many bytes `text` holds.
581///
582/// # Returns
583///
584/// [`PamojaStatus::Ok`] once set, or [`PamojaStatus::InvalidArgument`] when the kind carries
585/// no such text or the text is not UTF-8.
586///
587/// # Safety
588///
589/// `message` must be a live handle, and `text` must point to `text_len` readable bytes.
590#[no_mangle]
591pub unsafe extern "C" fn pamoja_gateway_station_message_set_text(
592    message: *mut PamojaGatewayStationMessage,
593    field: u8,
594    text: *const u8,
595    text_len: usize,
596) -> PamojaStatus {
597    let Some(held) = message.as_mut() else {
598        set_last_error("message must not be null".to_owned());
599        return PamojaStatus::InvalidArgument;
600    };
601    let Ok(bytes) = read_bytes(text, text_len) else {
602        return PamojaStatus::InvalidArgument;
603    };
604    let Ok(text) = String::from_utf8(bytes) else {
605        set_last_error("the text is not valid UTF-8".to_owned());
606        return PamojaStatus::InvalidArgument;
607    };
608    let kind = held.message.msgtype().to_owned();
609    match text_slot(&mut held.message, field) {
610        Some(slot) => {
611            *slot = text;
612            PamojaStatus::Ok
613        }
614        None => {
615            set_last_error(format!("a {kind} message carries no text field {field}"));
616            PamojaStatus::InvalidArgument
617        }
618    }
619}
620
621/// Sets the bytes a message carries: the payload of a data frame or a proprietary frame, or
622/// the frame a downlink transmits.
623///
624/// # Arguments
625///
626/// * `message` - the message.
627/// * `bytes` - the bytes.
628/// * `bytes_len` - how many bytes `bytes` holds.
629///
630/// # Returns
631///
632/// [`PamojaStatus::Ok`] once set, or [`PamojaStatus::InvalidArgument`] for a kind that
633/// carries none.
634///
635/// # Safety
636///
637/// `message` must be a live handle, and `bytes` must point to `bytes_len` readable bytes.
638#[no_mangle]
639pub unsafe extern "C" fn pamoja_gateway_station_message_set_payload(
640    message: *mut PamojaGatewayStationMessage,
641    bytes: *const u8,
642    bytes_len: usize,
643) -> PamojaStatus {
644    let Some(held) = message.as_mut() else {
645        set_last_error("message must not be null".to_owned());
646        return PamojaStatus::InvalidArgument;
647    };
648    let Ok(bytes) = read_bytes(bytes, bytes_len) else {
649        return PamojaStatus::InvalidArgument;
650    };
651    match &mut held.message {
652        Message::Uplink { payload, .. } | Message::Proprietary { payload, .. } => *payload = bytes,
653        Message::Downlink { pdu, .. } => *pdu = bytes,
654        other => return refused(other, "no payload"),
655    }
656    PamojaStatus::Ok
657}
658
659/// Sets the frame options a data frame carries.
660///
661/// # Arguments
662///
663/// * `message` - the message.
664/// * `bytes` - the options.
665/// * `bytes_len` - how many bytes `bytes` holds.
666///
667/// # Returns
668///
669/// [`PamojaStatus::Ok`] once set, or [`PamojaStatus::InvalidArgument`] for a kind that
670/// carries none.
671///
672/// # Safety
673///
674/// `message` must be a live handle, and `bytes` must point to `bytes_len` readable bytes.
675#[no_mangle]
676pub unsafe extern "C" fn pamoja_gateway_station_message_set_options(
677    message: *mut PamojaGatewayStationMessage,
678    bytes: *const u8,
679    bytes_len: usize,
680) -> PamojaStatus {
681    let Some(held) = message.as_mut() else {
682        set_last_error("message must not be null".to_owned());
683        return PamojaStatus::InvalidArgument;
684    };
685    let Ok(bytes) = read_bytes(bytes, bytes_len) else {
686        return PamojaStatus::InvalidArgument;
687    };
688    match &mut held.message {
689        Message::Uplink { fopts, .. } => *fopts = bytes,
690        other => return refused(other, "no frame options"),
691    }
692    PamojaStatus::Ok
693}
694
695/// Adds a network whose data frames a configuration forwards.
696///
697/// A configuration that names one network forwards no other, so the first call turns the
698/// filter on.
699///
700/// # Arguments
701///
702/// * `message` - the configuration.
703/// * `net_id` - the network identifier.
704///
705/// # Returns
706///
707/// [`PamojaStatus::Ok`] once added, or [`PamojaStatus::InvalidArgument`] for a message that
708/// is not a configuration.
709///
710/// # Safety
711///
712/// `message` must be a live handle.
713#[no_mangle]
714pub unsafe extern "C" fn pamoja_gateway_station_message_add_net_id(
715    message: *mut PamojaGatewayStationMessage,
716    net_id: u32,
717) -> PamojaStatus {
718    let Some(held) = message.as_mut() else {
719        set_last_error("message must not be null".to_owned());
720        return PamojaStatus::InvalidArgument;
721    };
722    match &mut held.message {
723        Message::RouterConfig { net_id: ids, .. } => {
724            ids.get_or_insert_with(Vec::new).push(net_id);
725            PamojaStatus::Ok
726        }
727        other => refused(other, "no network list"),
728    }
729}
730
731/// Adds a range of join identifiers whose join requests a configuration forwards.
732///
733/// # Arguments
734///
735/// * `message` - the configuration.
736/// * `range` - the range, both ends included.
737///
738/// # Returns
739///
740/// [`PamojaStatus::Ok`] once added, or [`PamojaStatus::InvalidArgument`] for a message that
741/// is not a configuration.
742///
743/// # Safety
744///
745/// `message` must be a live handle, and `range` must point to a readable range.
746#[no_mangle]
747pub unsafe extern "C" fn pamoja_gateway_station_message_add_join_range(
748    message: *mut PamojaGatewayStationMessage,
749    range: *const PamojaGatewayStationJoinRange,
750) -> PamojaStatus {
751    let (Some(held), Some(range)) = (message.as_mut(), range.as_ref()) else {
752        set_last_error("message and range must not be null".to_owned());
753        return PamojaStatus::InvalidArgument;
754    };
755    match &mut held.message {
756        Message::RouterConfig { join_eui, .. } => {
757            join_eui.push((range.first, range.last));
758            PamojaStatus::Ok
759        }
760        other => refused(other, "no join identifier ranges"),
761    }
762}
763
764/// Adds the next number of a configuration's data-rate table.
765///
766/// # Arguments
767///
768/// * `message` - the configuration.
769/// * `rate` - the data rate, whose `defined` false keeps the number without one.
770///
771/// # Returns
772///
773/// [`PamojaStatus::Ok`] once added, or [`PamojaStatus::InvalidArgument`] for a message that
774/// is not a configuration.
775///
776/// # Safety
777///
778/// `message` must be a live handle, and `rate` must point to a readable data rate.
779#[no_mangle]
780pub unsafe extern "C" fn pamoja_gateway_station_message_add_data_rate(
781    message: *mut PamojaGatewayStationMessage,
782    rate: *const PamojaGatewayStationDataRate,
783) -> PamojaStatus {
784    let (Some(held), Some(rate)) = (message.as_mut(), rate.as_ref()) else {
785        set_last_error("message and rate must not be null".to_owned());
786        return PamojaStatus::InvalidArgument;
787    };
788    match &mut held.message {
789        Message::RouterConfig { data_rates, .. } => {
790            data_rates.push(rate.defined.then_some((
791                rate.spreading_factor,
792                rate.bandwidth_hz,
793                rate.downlink_only,
794            )));
795            PamojaStatus::Ok
796        }
797        other => refused(other, "no data-rate table"),
798    }
799}
800
801/// Adds a frame to a schedule.
802///
803/// # Arguments
804///
805/// * `message` - the schedule.
806/// * `frame` - when and how to transmit it.
807/// * `pdu` - the frame's bytes.
808/// * `pdu_len` - how many bytes `pdu` holds.
809///
810/// # Returns
811///
812/// [`PamojaStatus::Ok`] once added, or [`PamojaStatus::InvalidArgument`] for a message that
813/// is not a schedule.
814///
815/// # Safety
816///
817/// `message` must be a live handle, `frame` must point to a readable frame, and `pdu` to
818/// `pdu_len` readable bytes.
819#[no_mangle]
820pub unsafe extern "C" fn pamoja_gateway_station_message_add_broadcast(
821    message: *mut PamojaGatewayStationMessage,
822    frame: *const PamojaGatewayStationBroadcast,
823    pdu: *const u8,
824    pdu_len: usize,
825) -> PamojaStatus {
826    let (Some(held), Some(frame)) = (message.as_mut(), frame.as_ref()) else {
827        set_last_error("message and frame must not be null".to_owned());
828        return PamojaStatus::InvalidArgument;
829    };
830    let Ok(pdu) = read_bytes(pdu, pdu_len) else {
831        return PamojaStatus::InvalidArgument;
832    };
833    match &mut held.message {
834        Message::Schedule { frames } => {
835            frames.push(Broadcast {
836                pdu,
837                data_rate: frame.data_rate,
838                frequency_hz: frame.frequency_hz,
839                priority: frame.priority,
840                gpstime: frame.has_gpstime.then_some(frame.gpstime),
841                rctx: frame.has_rctx.then_some(frame.rctx),
842            });
843            PamojaStatus::Ok
844        }
845        other => refused(other, "no frames"),
846    }
847}
848
849/// Reads one network a configuration names.
850///
851/// # Arguments
852///
853/// * `message` - the configuration.
854/// * `index` - which one, below the `net_id_count` its fields report.
855/// * `out_net_id` - receives the network identifier.
856///
857/// # Returns
858///
859/// [`PamojaStatus::Ok`] once read, or [`PamojaStatus::InvalidArgument`] for an index past the
860/// list or a message that is not a configuration.
861///
862/// # Safety
863///
864/// `message` must be a live handle, and `out_net_id` must point to writable storage.
865#[no_mangle]
866pub unsafe extern "C" fn pamoja_gateway_station_message_net_id(
867    message: *const PamojaGatewayStationMessage,
868    index: usize,
869    out_net_id: *mut u32,
870) -> PamojaStatus {
871    if message.is_null() || out_net_id.is_null() {
872        set_last_error("message and out_net_id must not be null".to_owned());
873        return PamojaStatus::InvalidArgument;
874    }
875    let found = match &(*message).message {
876        Message::RouterConfig { net_id, .. } => {
877            net_id.as_deref().and_then(|ids| ids.get(index)).copied()
878        }
879        _ => None,
880    };
881    match found {
882        Some(id) => {
883            *out_net_id = id;
884            PamojaStatus::Ok
885        }
886        None => missing(index, "network"),
887    }
888}
889
890/// Reads one join identifier range a configuration names.
891///
892/// # Arguments
893///
894/// * `message` - the configuration.
895/// * `index` - which one, below the `join_range_count` its fields report.
896/// * `out_range` - receives the range.
897///
898/// # Returns
899///
900/// [`PamojaStatus::Ok`] once read, or [`PamojaStatus::InvalidArgument`] for an index past the
901/// list or a message that is not a configuration.
902///
903/// # Safety
904///
905/// `message` must be a live handle, and `out_range` must point to writable storage.
906#[no_mangle]
907pub unsafe extern "C" fn pamoja_gateway_station_message_join_range(
908    message: *const PamojaGatewayStationMessage,
909    index: usize,
910    out_range: *mut PamojaGatewayStationJoinRange,
911) -> PamojaStatus {
912    if message.is_null() || out_range.is_null() {
913        set_last_error("message and out_range must not be null".to_owned());
914        return PamojaStatus::InvalidArgument;
915    }
916    let found = match &(*message).message {
917        Message::RouterConfig { join_eui, .. } => join_eui.get(index).copied(),
918        _ => None,
919    };
920    match found {
921        Some((first, last)) => {
922            *out_range = PamojaGatewayStationJoinRange { first, last };
923            PamojaStatus::Ok
924        }
925        None => missing(index, "join identifier range"),
926    }
927}
928
929/// Reads one number of a configuration's data-rate table.
930///
931/// # Arguments
932///
933/// * `message` - the configuration.
934/// * `index` - the data-rate number, below the `data_rate_count` its fields report.
935/// * `out_rate` - receives the data rate, with `defined` false for a number the table leaves
936///   undefined.
937///
938/// # Returns
939///
940/// [`PamojaStatus::Ok`] once read, or [`PamojaStatus::InvalidArgument`] for an index past the
941/// table or a message that is not a configuration.
942///
943/// # Safety
944///
945/// `message` must be a live handle, and `out_rate` must point to writable storage.
946#[no_mangle]
947pub unsafe extern "C" fn pamoja_gateway_station_message_data_rate(
948    message: *const PamojaGatewayStationMessage,
949    index: usize,
950    out_rate: *mut PamojaGatewayStationDataRate,
951) -> PamojaStatus {
952    if message.is_null() || out_rate.is_null() {
953        set_last_error("message and out_rate must not be null".to_owned());
954        return PamojaStatus::InvalidArgument;
955    }
956    let found = match &(*message).message {
957        Message::RouterConfig { data_rates, .. } => data_rates.get(index).copied(),
958        _ => None,
959    };
960    match found {
961        Some(entry) => {
962            let (spreading_factor, bandwidth_hz, downlink_only) = entry.unwrap_or_default();
963            *out_rate = PamojaGatewayStationDataRate {
964                spreading_factor,
965                bandwidth_hz,
966                downlink_only,
967                defined: entry.is_some(),
968            };
969            PamojaStatus::Ok
970        }
971        None => missing(index, "data rate"),
972    }
973}
974
975/// Reads one frame of a schedule.
976///
977/// # Arguments
978///
979/// * `message` - the schedule.
980/// * `index` - which frame, below the `broadcast_count` its fields report.
981/// * `out_frame` - receives when and how to transmit it.
982/// * `out_pdu` - receives its bytes, which the caller releases with
983///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
984///
985/// # Returns
986///
987/// [`PamojaStatus::Ok`] once read, or [`PamojaStatus::InvalidArgument`] for an index past the
988/// schedule or a message that is not one.
989///
990/// # Safety
991///
992/// `message` must be a live handle, and both out pointers must point to writable storage.
993#[no_mangle]
994pub unsafe extern "C" fn pamoja_gateway_station_message_broadcast(
995    message: *const PamojaGatewayStationMessage,
996    index: usize,
997    out_frame: *mut PamojaGatewayStationBroadcast,
998    out_pdu: *mut *mut PamojaBuffer,
999) -> PamojaStatus {
1000    if message.is_null() || out_frame.is_null() || out_pdu.is_null() {
1001        set_last_error("message, out_frame and out_pdu must not be null".to_owned());
1002        return PamojaStatus::InvalidArgument;
1003    }
1004    let found = match &(*message).message {
1005        Message::Schedule { frames } => frames.get(index),
1006        _ => None,
1007    };
1008    match found {
1009        Some(frame) => {
1010            *out_frame = PamojaGatewayStationBroadcast {
1011                data_rate: frame.data_rate,
1012                frequency_hz: frame.frequency_hz,
1013                priority: frame.priority,
1014                gpstime: frame.gpstime.unwrap_or_default(),
1015                has_gpstime: frame.gpstime.is_some(),
1016                rctx: frame.rctx.unwrap_or_default(),
1017                has_rctx: frame.rctx.is_some(),
1018            };
1019            *out_pdu = PamojaBuffer::into_raw(frame.pdu.clone());
1020            PamojaStatus::Ok
1021        }
1022        None => missing(index, "scheduled frame"),
1023    }
1024}
1025
1026/// Releases a message.
1027///
1028/// # Arguments
1029///
1030/// * `message` - the message, or null.
1031///
1032/// # Safety
1033///
1034/// `message` must be a live handle from a call that produced one, or null, and must not be
1035/// used again afterwards.
1036#[no_mangle]
1037pub unsafe extern "C" fn pamoja_gateway_station_message_free(
1038    message: *mut PamojaGatewayStationMessage,
1039) {
1040    if !message.is_null() {
1041        drop(Box::from_raw(message));
1042    }
1043}
1044
1045/// Writes the request a station sends to find its network server.
1046///
1047/// # Arguments
1048///
1049/// * `router` - the station asking, eight bytes.
1050/// * `out_text` - receives the text, which the caller releases with
1051///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
1052///
1053/// # Returns
1054///
1055/// [`PamojaStatus::Ok`] once written.
1056///
1057/// # Safety
1058///
1059/// `router` must point to eight readable bytes, and `out_text` to writable storage.
1060#[no_mangle]
1061pub unsafe extern "C" fn pamoja_gateway_station_discovery(
1062    router: *const u8,
1063    out_text: *mut *mut PamojaBuffer,
1064) -> PamojaStatus {
1065    let (Some(router), false) = (eight(router), out_text.is_null()) else {
1066        set_last_error("router and out_text must not be null".to_owned());
1067        return PamojaStatus::InvalidArgument;
1068    };
1069    let asking = Discovery::new(Eui::new(router));
1070    *out_text = PamojaBuffer::into_raw(asking.to_json().into_bytes());
1071    PamojaStatus::Ok
1072}
1073
1074/// Reads the request a station sent to find its network server, as the server does.
1075///
1076/// # Arguments
1077///
1078/// * `text` - the request text.
1079/// * `text_len` - how many bytes `text` holds.
1080/// * `out_router` - receives the station asking, eight bytes.
1081///
1082/// # Returns
1083///
1084/// [`PamojaStatus::Ok`] once read, or [`PamojaStatus::Codec`] when the text is not this
1085/// request.
1086///
1087/// # Safety
1088///
1089/// `text` must point to `text_len` readable bytes, and `out_router` to eight writable bytes.
1090#[no_mangle]
1091pub unsafe extern "C" fn pamoja_gateway_station_discovery_parse(
1092    text: *const u8,
1093    text_len: usize,
1094    out_router: *mut u8,
1095) -> PamojaStatus {
1096    if out_router.is_null() {
1097        set_last_error("out_router must not be null".to_owned());
1098        return PamojaStatus::InvalidArgument;
1099    }
1100    let Ok(text) = read_bytes(text, text_len) else {
1101        return PamojaStatus::InvalidArgument;
1102    };
1103    match Discovery::from_json(&text) {
1104        Ok(asked) => {
1105            ptr::copy_nonoverlapping(asked.router.bytes().as_ptr(), out_router, 8);
1106            PamojaStatus::Ok
1107        }
1108        Err(error) => {
1109            set_last_error(error.to_string());
1110            PamojaStatus::Codec
1111        }
1112    }
1113}
1114
1115/// Writes the answer that sends a station to the websocket its session runs on.
1116///
1117/// # Arguments
1118///
1119/// * `router` - the station being answered, eight bytes.
1120/// * `muxs` - the server endpoint that carries the session, eight bytes.
1121/// * `uri` - the websocket address, UTF-8.
1122/// * `uri_len` - how many bytes `uri` holds.
1123/// * `out_text` - receives the answer, which the caller releases with
1124///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
1125///
1126/// # Returns
1127///
1128/// [`PamojaStatus::Ok`] once written.
1129///
1130/// # Safety
1131///
1132/// `router` and `muxs` must point to eight readable bytes each, `uri` to `uri_len` readable
1133/// bytes, and `out_text` to writable storage.
1134#[no_mangle]
1135pub unsafe extern "C" fn pamoja_gateway_station_router_accepted(
1136    router: *const u8,
1137    muxs: *const u8,
1138    uri: *const u8,
1139    uri_len: usize,
1140    out_text: *mut *mut PamojaBuffer,
1141) -> PamojaStatus {
1142    let (Some(router), Some(muxs), false) = (eight(router), eight(muxs), out_text.is_null()) else {
1143        set_last_error("router, muxs and out_text must not be null".to_owned());
1144        return PamojaStatus::InvalidArgument;
1145    };
1146    let Some(uri) = utf8(uri, uri_len) else {
1147        return PamojaStatus::InvalidArgument;
1148    };
1149    let answer = Router::accepted(Eui::new(router), Eui::new(muxs), uri);
1150    *out_text = PamojaBuffer::into_raw(answer.to_json().into_bytes());
1151    PamojaStatus::Ok
1152}
1153
1154/// Writes the answer that refuses a station, saying why.
1155///
1156/// # Arguments
1157///
1158/// * `router` - the station being refused, eight bytes.
1159/// * `error` - what is wrong, in words the operator can act on, UTF-8.
1160/// * `error_len` - how many bytes `error` holds.
1161/// * `out_text` - receives the answer, which the caller releases with
1162///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
1163///
1164/// # Returns
1165///
1166/// [`PamojaStatus::Ok`] once written.
1167///
1168/// # Safety
1169///
1170/// `router` must point to eight readable bytes, `error` to `error_len` readable bytes, and
1171/// `out_text` to writable storage.
1172#[no_mangle]
1173pub unsafe extern "C" fn pamoja_gateway_station_router_refused(
1174    router: *const u8,
1175    error: *const u8,
1176    error_len: usize,
1177    out_text: *mut *mut PamojaBuffer,
1178) -> PamojaStatus {
1179    let (Some(router), false) = (eight(router), out_text.is_null()) else {
1180        set_last_error("router and out_text must not be null".to_owned());
1181        return PamojaStatus::InvalidArgument;
1182    };
1183    let Some(error) = utf8(error, error_len) else {
1184        return PamojaStatus::InvalidArgument;
1185    };
1186    let answer = Router::refused(Eui::new(router), error);
1187    *out_text = PamojaBuffer::into_raw(answer.to_json().into_bytes());
1188    PamojaStatus::Ok
1189}
1190
1191/// Reads the answer a discovery endpoint gives.
1192///
1193/// # Arguments
1194///
1195/// * `text` - the answer text.
1196/// * `text_len` - how many bytes `text` holds.
1197/// * `out_uri` - receives the websocket address to open, empty when the station was refused.
1198/// * `out_error` - receives why the station was refused, empty when it was not.
1199///
1200/// # Returns
1201///
1202/// [`PamojaStatus::Ok`] once read. Both buffers are released by the caller with
1203/// [`pamoja_buffer_free`](crate::pamoja_buffer_free).
1204///
1205/// # Safety
1206///
1207/// `text` must point to `text_len` readable bytes, and both out pointers to writable storage.
1208#[no_mangle]
1209pub unsafe extern "C" fn pamoja_gateway_station_router_parse(
1210    text: *const u8,
1211    text_len: usize,
1212    out_uri: *mut *mut PamojaBuffer,
1213    out_error: *mut *mut PamojaBuffer,
1214) -> PamojaStatus {
1215    if out_uri.is_null() || out_error.is_null() {
1216        set_last_error("out_uri and out_error must not be null".to_owned());
1217        return PamojaStatus::InvalidArgument;
1218    }
1219    let Ok(text) = read_bytes(text, text_len) else {
1220        return PamojaStatus::InvalidArgument;
1221    };
1222    match Router::from_json(&text) {
1223        Ok(answer) => {
1224            *out_uri = PamojaBuffer::into_raw(answer.uri.unwrap_or_default().into_bytes());
1225            *out_error = PamojaBuffer::into_raw(answer.error.unwrap_or_default().into_bytes());
1226            PamojaStatus::Ok
1227        }
1228        Err(error) => {
1229            set_last_error(error.to_string());
1230            PamojaStatus::Codec
1231        }
1232    }
1233}
1234
1235/// Reads the identities a discovery answer names: the station, and the server endpoint that
1236/// carries its session.
1237///
1238/// # Arguments
1239///
1240/// * `text` - the answer text.
1241/// * `text_len` - how many bytes `text` holds.
1242/// * `out_ids` - receives the identities.
1243///
1244/// # Returns
1245///
1246/// [`PamojaStatus::Ok`] once read, or [`PamojaStatus::Codec`] when the text is not JSON.
1247///
1248/// # Safety
1249///
1250/// `text` must point to `text_len` readable bytes, and `out_ids` to writable storage.
1251#[no_mangle]
1252pub unsafe extern "C" fn pamoja_gateway_station_router_identities(
1253    text: *const u8,
1254    text_len: usize,
1255    out_ids: *mut PamojaGatewayStationRouterIds,
1256) -> PamojaStatus {
1257    if out_ids.is_null() {
1258        set_last_error("out_ids must not be null".to_owned());
1259        return PamojaStatus::InvalidArgument;
1260    }
1261    let Ok(text) = read_bytes(text, text_len) else {
1262        return PamojaStatus::InvalidArgument;
1263    };
1264    match Router::from_json(&text) {
1265        Ok(answer) => {
1266            *out_ids = PamojaGatewayStationRouterIds {
1267                router: answer.router.map(|eui| eui.bytes()).unwrap_or_default(),
1268                has_router: answer.router.is_some(),
1269                muxs: answer.muxs.map(|eui| eui.bytes()).unwrap_or_default(),
1270                has_muxs: answer.muxs.is_some(),
1271            };
1272            PamojaStatus::Ok
1273        }
1274        Err(error) => {
1275            set_last_error(error.to_string());
1276            PamojaStatus::Codec
1277        }
1278    }
1279}
1280
1281/// Builds a station clock value from the radio it was read on, the run of the station, and
1282/// the microseconds that run had counted.
1283///
1284/// # Arguments
1285///
1286/// * `unit` - the radio unit, 0 to 127.
1287/// * `session` - the run of the station, which the reference station never leaves at 0.
1288/// * `micros` - the microseconds the run had counted.
1289/// * `out_value` - receives the value a message carries as its `xtime`.
1290///
1291/// # Returns
1292///
1293/// [`PamojaStatus::Ok`] once written, or [`PamojaStatus::InvalidArgument`] when the unit is
1294/// past 127 or the microseconds do not fit 48 bits.
1295///
1296/// # Safety
1297///
1298/// `out_value` must point to writable storage.
1299#[no_mangle]
1300pub unsafe extern "C" fn pamoja_gateway_station_xtime(
1301    unit: u8,
1302    session: u8,
1303    micros: u64,
1304    out_value: *mut i64,
1305) -> PamojaStatus {
1306    if out_value.is_null() {
1307        set_last_error("out_value must not be null".to_owned());
1308        return PamojaStatus::InvalidArgument;
1309    }
1310    match Xtime::new(unit, session, micros) {
1311        Some(clock) => {
1312            *out_value = clock.value();
1313            PamojaStatus::Ok
1314        }
1315        None => {
1316            set_last_error(format!(
1317                "a station clock takes a unit up to 127 and 48 bits of microseconds, not unit {unit} and {micros} us"
1318            ));
1319            PamojaStatus::InvalidArgument
1320        }
1321    }
1322}
1323
1324/// Takes a station clock value apart into its radio unit, run, and microseconds.
1325///
1326/// # Arguments
1327///
1328/// * `value` - the `xtime` a message carried.
1329/// * `out_parts` - receives the parts.
1330///
1331/// # Returns
1332///
1333/// [`PamojaStatus::Ok`] once written.
1334///
1335/// # Safety
1336///
1337/// `out_parts` must point to writable storage.
1338#[no_mangle]
1339pub unsafe extern "C" fn pamoja_gateway_station_xtime_parts(
1340    value: i64,
1341    out_parts: *mut PamojaGatewayStationXtime,
1342) -> PamojaStatus {
1343    if out_parts.is_null() {
1344        set_last_error("out_parts must not be null".to_owned());
1345        return PamojaStatus::InvalidArgument;
1346    }
1347    let clock = Xtime::of(value);
1348    *out_parts = PamojaGatewayStationXtime {
1349        unit: clock.unit,
1350        session: clock.session,
1351        micros: clock.micros,
1352    };
1353    PamojaStatus::Ok
1354}
1355
1356/// Writes an identifier in the ID6 form the protocol prefers.
1357///
1358/// # Arguments
1359///
1360/// * `eui` - the identifier, eight bytes.
1361/// * `out_text` - receives the text, which the caller releases with
1362///   [`pamoja_buffer_free`](crate::pamoja_buffer_free).
1363///
1364/// # Returns
1365///
1366/// [`PamojaStatus::Ok`] once written.
1367///
1368/// # Safety
1369///
1370/// `eui` must point to eight readable bytes, and `out_text` to writable storage.
1371#[no_mangle]
1372pub unsafe extern "C" fn pamoja_gateway_station_id6(
1373    eui: *const u8,
1374    out_text: *mut *mut PamojaBuffer,
1375) -> PamojaStatus {
1376    let (Some(eui), false) = (eight(eui), out_text.is_null()) else {
1377        set_last_error("eui and out_text must not be null".to_owned());
1378        return PamojaStatus::InvalidArgument;
1379    };
1380    *out_text = PamojaBuffer::into_raw(id6(Eui::new(eui)).into_bytes());
1381    PamojaStatus::Ok
1382}
1383
1384/// Reads an identifier written in any form the protocol accepts.
1385///
1386/// # Arguments
1387///
1388/// * `text` - the identifier, null-terminated.
1389/// * `out_eui` - receives the eight bytes.
1390///
1391/// # Returns
1392///
1393/// [`PamojaStatus::Ok`] once read, or [`PamojaStatus::Codec`] when the text is not an
1394/// identifier.
1395///
1396/// # Safety
1397///
1398/// `text` must be a valid null-terminated UTF-8 string, and `out_eui` must point to eight
1399/// writable bytes.
1400#[no_mangle]
1401pub unsafe extern "C" fn pamoja_gateway_station_eui_of(
1402    text: *const c_char,
1403    out_eui: *mut u8,
1404) -> PamojaStatus {
1405    if text.is_null() || out_eui.is_null() {
1406        set_last_error("text and out_eui must not be null".to_owned());
1407        return PamojaStatus::InvalidArgument;
1408    }
1409    let Ok(text) = std::ffi::CStr::from_ptr(text).to_str() else {
1410        set_last_error("the identifier is not valid UTF-8".to_owned());
1411        return PamojaStatus::InvalidArgument;
1412    };
1413    match eui_of(text) {
1414        Some(eui) => {
1415            ptr::copy_nonoverlapping(eui.bytes().as_ptr(), out_eui, 8);
1416            PamojaStatus::Ok
1417        }
1418        None => {
1419            set_last_error(format!("{text} is not an identifier"));
1420            PamojaStatus::Codec
1421        }
1422    }
1423}
1424
1425/// Hands a message to the caller as a handle.
1426fn held(message: Message) -> *mut PamojaGatewayStationMessage {
1427    Box::into_raw(Box::new(PamojaGatewayStationMessage { message }))
1428}
1429
1430/// Refuses an addition a kind does not carry, saying which.
1431fn refused(message: &Message, carries: &str) -> PamojaStatus {
1432    set_last_error(format!("a {} message carries {carries}", message.msgtype()));
1433    PamojaStatus::InvalidArgument
1434}
1435
1436/// Refuses an index past the list asked for.
1437fn missing(index: usize, what: &str) -> PamojaStatus {
1438    set_last_error(format!("the message carries no {what} at index {index}"));
1439    PamojaStatus::InvalidArgument
1440}
1441
1442/// Reads UTF-8 text from the boundary.
1443unsafe fn utf8(text: *const u8, text_len: usize) -> Option<String> {
1444    let bytes = read_bytes(text, text_len).ok()?;
1445    match String::from_utf8(bytes) {
1446        Ok(text) => Some(text),
1447        Err(_) => {
1448            set_last_error("the text is not valid UTF-8".to_owned());
1449            None
1450        }
1451    }
1452}
1453
1454/// Reads how a packet was heard from the boundary.
1455fn levels_of(levels: PamojaGatewayStationLevels) -> Levels {
1456    Levels {
1457        rctx: levels.rctx,
1458        xtime: levels.xtime,
1459        gpstime: levels.has_gpstime.then_some(levels.gpstime),
1460        rssi: levels.rssi,
1461        snr: levels.snr,
1462    }
1463}
1464
1465/// Writes how a packet was heard for the boundary.
1466fn levels_to_c(levels: Levels) -> PamojaGatewayStationLevels {
1467    PamojaGatewayStationLevels {
1468        rctx: levels.rctx,
1469        xtime: levels.xtime,
1470        gpstime: levels.gpstime.unwrap_or_default(),
1471        has_gpstime: levels.gpstime.is_some(),
1472        rssi: levels.rssi,
1473        snr: levels.snr,
1474    }
1475}
1476
1477/// Reads a window from the boundary.
1478fn window_of(window: PamojaGatewayStationWindow) -> Option<(u8, u32)> {
1479    window
1480        .present
1481        .then_some((window.data_rate, window.frequency_hz))
1482}
1483
1484/// Writes a window for the boundary.
1485fn window_to_c(window: Option<(u8, u32)>) -> PamojaGatewayStationWindow {
1486    let (data_rate, frequency_hz) = window.unwrap_or_default();
1487    PamojaGatewayStationWindow {
1488        data_rate,
1489        frequency_hz,
1490        present: window.is_some(),
1491    }
1492}
1493
1494/// Names which kind a message is.
1495fn kind_of(message: &Message) -> u8 {
1496    match message {
1497        Message::JoinRequest { .. } => PAMOJA_GATEWAY_STATION_JOIN_REQUEST,
1498        Message::Uplink { .. } => PAMOJA_GATEWAY_STATION_UPLINK,
1499        Message::Proprietary { .. } => PAMOJA_GATEWAY_STATION_PROPRIETARY,
1500        Message::Version { .. } => PAMOJA_GATEWAY_STATION_VERSION,
1501        Message::RouterConfig { .. } => PAMOJA_GATEWAY_STATION_ROUTER_CONFIG,
1502        Message::Downlink { .. } => PAMOJA_GATEWAY_STATION_DOWNLINK,
1503        Message::Schedule { .. } => PAMOJA_GATEWAY_STATION_SCHEDULE,
1504        Message::Transmitted { .. } => PAMOJA_GATEWAY_STATION_TRANSMITTED,
1505        Message::TimeSync { .. } => PAMOJA_GATEWAY_STATION_TIME_SYNC,
1506        Message::Other { .. } => PAMOJA_GATEWAY_STATION_OTHER,
1507    }
1508}
1509
1510/// Builds a message of the kind the fields name, with no text, bytes or entries yet.
1511fn message_from(fields: &PamojaGatewayStationFields) -> Result<Message, String> {
1512    let clock = |value: i64, present: bool| present.then_some(value);
1513    Ok(match fields.kind {
1514        PAMOJA_GATEWAY_STATION_JOIN_REQUEST => Message::JoinRequest {
1515            mhdr: fields.mhdr,
1516            join_eui: Eui::new(fields.join_eui),
1517            dev_eui: Eui::new(fields.dev_eui),
1518            dev_nonce: fields.dev_nonce,
1519            mic: fields.mic,
1520            data_rate: fields.data_rate,
1521            frequency_hz: fields.frequency_hz,
1522            levels: levels_of(fields.levels),
1523        },
1524        PAMOJA_GATEWAY_STATION_UPLINK => Message::Uplink {
1525            mhdr: fields.mhdr,
1526            dev_addr: fields.dev_addr,
1527            fctrl: fields.fctrl,
1528            fcnt: fields.fcnt,
1529            fopts: Vec::new(),
1530            fport: fields.has_fport.then_some(fields.fport),
1531            payload: Vec::new(),
1532            mic: fields.mic,
1533            data_rate: fields.data_rate,
1534            frequency_hz: fields.frequency_hz,
1535            levels: levels_of(fields.levels),
1536        },
1537        PAMOJA_GATEWAY_STATION_PROPRIETARY => Message::Proprietary {
1538            payload: Vec::new(),
1539            data_rate: fields.data_rate,
1540            frequency_hz: fields.frequency_hz,
1541            levels: levels_of(fields.levels),
1542        },
1543        PAMOJA_GATEWAY_STATION_VERSION => Message::Version {
1544            station: String::new(),
1545            firmware: String::new(),
1546            package: String::new(),
1547            model: String::new(),
1548            protocol: fields.protocol,
1549            features: String::new(),
1550        },
1551        PAMOJA_GATEWAY_STATION_ROUTER_CONFIG => Message::RouterConfig {
1552            net_id: fields.filters_networks.then(Vec::new),
1553            join_eui: Vec::new(),
1554            region: String::new(),
1555            max_eirp: fields.max_eirp,
1556            hwspec: String::new(),
1557            freq_range: (fields.freq_min_hz, fields.freq_max_hz),
1558            data_rates: Vec::new(),
1559        },
1560        PAMOJA_GATEWAY_STATION_DOWNLINK => Message::Downlink {
1561            dev_eui: Eui::new(fields.dev_eui),
1562            class: fields.class,
1563            diid: fields.diid,
1564            pdu: Vec::new(),
1565            rx_delay: fields.has_rx_delay.then_some(fields.rx_delay),
1566            rx1: window_of(fields.rx1),
1567            rx2: window_of(fields.rx2),
1568            ping_slot: window_of(fields.ping_slot),
1569            priority: fields.priority,
1570            xtime: clock(fields.xtime, fields.has_xtime),
1571            rctx: clock(fields.rctx, fields.has_rctx),
1572            gpstime: clock(fields.gpstime, fields.has_gpstime),
1573        },
1574        PAMOJA_GATEWAY_STATION_SCHEDULE => Message::Schedule { frames: Vec::new() },
1575        PAMOJA_GATEWAY_STATION_TRANSMITTED => Message::Transmitted {
1576            diid: fields.diid,
1577            dev_eui: Eui::new(fields.dev_eui),
1578            rctx: fields.rctx,
1579            xtime: fields.xtime,
1580            txtime: fields.txtime,
1581            gpstime: clock(fields.gpstime, fields.has_gpstime),
1582        },
1583        PAMOJA_GATEWAY_STATION_TIME_SYNC => Message::TimeSync {
1584            txtime: fields.has_txtime.then_some(fields.txtime.round() as i64),
1585            xtime: clock(fields.xtime, fields.has_xtime),
1586            gpstime: clock(fields.gpstime, fields.has_gpstime),
1587        },
1588        PAMOJA_GATEWAY_STATION_OTHER => Message::Other {
1589            msgtype: String::new(),
1590        },
1591        kind => return Err(format!("{kind} names no message kind")),
1592    })
1593}
1594
1595/// Writes whichever fields the kind in hand carries, leaving the rest at zero.
1596fn fields_of(message: &Message) -> PamojaGatewayStationFields {
1597    let mut fields = PamojaGatewayStationFields {
1598        kind: kind_of(message),
1599        mhdr: 0,
1600        join_eui: [0; 8],
1601        dev_eui: [0; 8],
1602        dev_nonce: 0,
1603        dev_addr: 0,
1604        fctrl: 0,
1605        fcnt: 0,
1606        fport: 0,
1607        has_fport: false,
1608        mic: 0,
1609        data_rate: 0,
1610        frequency_hz: 0,
1611        levels: levels_to_c(Levels::default()),
1612        class: 0,
1613        diid: 0,
1614        rx_delay: 0,
1615        has_rx_delay: false,
1616        priority: 0,
1617        protocol: 0,
1618        max_eirp: 0.0,
1619        freq_min_hz: 0,
1620        freq_max_hz: 0,
1621        filters_networks: false,
1622        net_id_count: 0,
1623        join_range_count: 0,
1624        data_rate_count: 0,
1625        broadcast_count: 0,
1626        rx1: window_to_c(None),
1627        rx2: window_to_c(None),
1628        ping_slot: window_to_c(None),
1629        xtime: 0,
1630        has_xtime: false,
1631        rctx: 0,
1632        has_rctx: false,
1633        gpstime: 0,
1634        has_gpstime: false,
1635        txtime: 0.0,
1636        has_txtime: false,
1637    };
1638
1639    match message {
1640        Message::JoinRequest {
1641            mhdr,
1642            join_eui,
1643            dev_eui,
1644            dev_nonce,
1645            mic,
1646            data_rate,
1647            frequency_hz,
1648            levels,
1649        } => {
1650            fields.mhdr = *mhdr;
1651            fields.join_eui = join_eui.bytes();
1652            fields.dev_eui = dev_eui.bytes();
1653            fields.dev_nonce = *dev_nonce;
1654            fields.mic = *mic;
1655            fields.data_rate = *data_rate;
1656            fields.frequency_hz = *frequency_hz;
1657            fields.levels = levels_to_c(*levels);
1658        }
1659        Message::Uplink {
1660            mhdr,
1661            dev_addr,
1662            fctrl,
1663            fcnt,
1664            fport,
1665            mic,
1666            data_rate,
1667            frequency_hz,
1668            levels,
1669            ..
1670        } => {
1671            fields.mhdr = *mhdr;
1672            fields.dev_addr = *dev_addr;
1673            fields.fctrl = *fctrl;
1674            fields.fcnt = *fcnt;
1675            fields.fport = fport.unwrap_or_default();
1676            fields.has_fport = fport.is_some();
1677            fields.mic = *mic;
1678            fields.data_rate = *data_rate;
1679            fields.frequency_hz = *frequency_hz;
1680            fields.levels = levels_to_c(*levels);
1681        }
1682        Message::Proprietary {
1683            data_rate,
1684            frequency_hz,
1685            levels,
1686            ..
1687        } => {
1688            fields.data_rate = *data_rate;
1689            fields.frequency_hz = *frequency_hz;
1690            fields.levels = levels_to_c(*levels);
1691        }
1692        Message::Version { protocol, .. } => fields.protocol = *protocol,
1693        Message::RouterConfig {
1694            net_id,
1695            join_eui,
1696            max_eirp,
1697            freq_range,
1698            data_rates,
1699            ..
1700        } => {
1701            fields.max_eirp = *max_eirp;
1702            fields.freq_min_hz = freq_range.0;
1703            fields.freq_max_hz = freq_range.1;
1704            fields.filters_networks = net_id.is_some();
1705            fields.net_id_count = net_id.as_ref().map_or(0, Vec::len);
1706            fields.join_range_count = join_eui.len();
1707            fields.data_rate_count = data_rates.len();
1708        }
1709        Message::Downlink {
1710            dev_eui,
1711            class,
1712            diid,
1713            rx_delay,
1714            rx1,
1715            rx2,
1716            ping_slot,
1717            priority,
1718            xtime,
1719            rctx,
1720            gpstime,
1721            ..
1722        } => {
1723            fields.dev_eui = dev_eui.bytes();
1724            fields.class = *class;
1725            fields.diid = *diid;
1726            fields.rx_delay = rx_delay.unwrap_or_default();
1727            fields.has_rx_delay = rx_delay.is_some();
1728            fields.rx1 = window_to_c(*rx1);
1729            fields.rx2 = window_to_c(*rx2);
1730            fields.ping_slot = window_to_c(*ping_slot);
1731            fields.priority = *priority;
1732            fields.xtime = xtime.unwrap_or_default();
1733            fields.has_xtime = xtime.is_some();
1734            fields.rctx = rctx.unwrap_or_default();
1735            fields.has_rctx = rctx.is_some();
1736            fields.gpstime = gpstime.unwrap_or_default();
1737            fields.has_gpstime = gpstime.is_some();
1738        }
1739        Message::Schedule { frames } => fields.broadcast_count = frames.len(),
1740        Message::Transmitted {
1741            diid,
1742            dev_eui,
1743            rctx,
1744            xtime,
1745            txtime,
1746            gpstime,
1747        } => {
1748            fields.diid = *diid;
1749            fields.dev_eui = dev_eui.bytes();
1750            fields.rctx = *rctx;
1751            fields.has_rctx = true;
1752            fields.xtime = *xtime;
1753            fields.has_xtime = true;
1754            fields.txtime = *txtime;
1755            fields.has_txtime = true;
1756            fields.gpstime = gpstime.unwrap_or_default();
1757            fields.has_gpstime = gpstime.is_some();
1758        }
1759        Message::TimeSync {
1760            txtime,
1761            xtime,
1762            gpstime,
1763        } => {
1764            fields.txtime = txtime.unwrap_or_default() as f64;
1765            fields.has_txtime = txtime.is_some();
1766            fields.xtime = xtime.unwrap_or_default();
1767            fields.has_xtime = xtime.is_some();
1768            fields.gpstime = gpstime.unwrap_or_default();
1769            fields.has_gpstime = gpstime.is_some();
1770        }
1771        Message::Other { .. } => {}
1772    }
1773
1774    fields
1775}
1776
1777/// Reads a piece of text a message carries, empty for one its kind does not.
1778fn text_of(message: &Message, field: u8) -> &str {
1779    match (message, field) {
1780        (_, PAMOJA_GATEWAY_STATION_TEXT_MSGTYPE) => message.msgtype(),
1781        (Message::Version { station, .. }, PAMOJA_GATEWAY_STATION_TEXT_STATION) => station,
1782        (Message::Version { firmware, .. }, PAMOJA_GATEWAY_STATION_TEXT_FIRMWARE) => firmware,
1783        (Message::Version { package, .. }, PAMOJA_GATEWAY_STATION_TEXT_PACKAGE) => package,
1784        (Message::Version { model, .. }, PAMOJA_GATEWAY_STATION_TEXT_MODEL) => model,
1785        (Message::Version { features, .. }, PAMOJA_GATEWAY_STATION_TEXT_FEATURES) => features,
1786        (Message::RouterConfig { region, .. }, PAMOJA_GATEWAY_STATION_TEXT_REGION) => region,
1787        (Message::RouterConfig { hwspec, .. }, PAMOJA_GATEWAY_STATION_TEXT_HWSPEC) => hwspec,
1788        _ => "",
1789    }
1790}
1791
1792/// Finds where a piece of text a kind carries is kept.
1793fn text_slot(message: &mut Message, field: u8) -> Option<&mut String> {
1794    match (message, field) {
1795        (Message::Version { station, .. }, PAMOJA_GATEWAY_STATION_TEXT_STATION) => Some(station),
1796        (Message::Version { firmware, .. }, PAMOJA_GATEWAY_STATION_TEXT_FIRMWARE) => Some(firmware),
1797        (Message::Version { package, .. }, PAMOJA_GATEWAY_STATION_TEXT_PACKAGE) => Some(package),
1798        (Message::Version { model, .. }, PAMOJA_GATEWAY_STATION_TEXT_MODEL) => Some(model),
1799        (Message::Version { features, .. }, PAMOJA_GATEWAY_STATION_TEXT_FEATURES) => Some(features),
1800        (Message::RouterConfig { region, .. }, PAMOJA_GATEWAY_STATION_TEXT_REGION) => Some(region),
1801        (Message::RouterConfig { hwspec, .. }, PAMOJA_GATEWAY_STATION_TEXT_HWSPEC) => Some(hwspec),
1802        (Message::Other { msgtype }, PAMOJA_GATEWAY_STATION_TEXT_MSGTYPE) => Some(msgtype),
1803        _ => None,
1804    }
1805}
1806
1807/// Reads an eight-byte identifier from the boundary.
1808unsafe fn eight(bytes: *const u8) -> Option<[u8; 8]> {
1809    if bytes.is_null() {
1810        return None;
1811    }
1812    let mut read = [0u8; 8];
1813    ptr::copy_nonoverlapping(bytes, read.as_mut_ptr(), read.len());
1814    Some(read)
1815}
1816
1817#[cfg(test)]
1818mod tests {
1819    use super::*;
1820    use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1821
1822    /// Takes the bytes out of a buffer the library handed over, and releases it.
1823    unsafe fn taken(buffer: *mut PamojaBuffer) -> Vec<u8> {
1824        let bytes =
1825            std::slice::from_raw_parts(pamoja_buffer_data(buffer), pamoja_buffer_len(buffer))
1826                .to_vec();
1827        pamoja_buffer_free(buffer);
1828        bytes
1829    }
1830
1831    /// Writes a message through the boundary, and releases the handle.
1832    unsafe fn written(message: *mut PamojaGatewayStationMessage) -> String {
1833        assert!(!message.is_null(), "the message was built");
1834        let mut text = ptr::null_mut();
1835        assert_eq!(
1836            pamoja_gateway_station_message_json(message, &mut text),
1837            PamojaStatus::Ok
1838        );
1839        pamoja_gateway_station_message_free(message);
1840        String::from_utf8(taken(text)).expect("the text is UTF-8")
1841    }
1842
1843    /// The fields of a message of one kind, every other field at zero.
1844    fn blank(kind: u8) -> PamojaGatewayStationFields {
1845        PamojaGatewayStationFields {
1846            kind,
1847            ..fields_of(&Message::Other {
1848                msgtype: String::new(),
1849            })
1850        }
1851    }
1852
1853    #[test]
1854    fn a_configuration_built_through_the_boundary_matches_the_one_built_in_rust() {
1855        let expected = Message::RouterConfig {
1856            net_id: Some(vec![0x13]),
1857            join_eui: vec![(0x70b3_d57e_d000_0000, 0x70b3_d57e_d000_00ff)],
1858            region: "US915".to_owned(),
1859            max_eirp: 30.0,
1860            hwspec: "sx1301/1".to_owned(),
1861            freq_range: (902_000_000, 928_000_000),
1862            data_rates: vec![Some((10, 125_000, false)), None, Some((12, 500_000, true))],
1863        };
1864
1865        unsafe {
1866            let mut fields = blank(PAMOJA_GATEWAY_STATION_ROUTER_CONFIG);
1867            fields.max_eirp = 30.0;
1868            fields.freq_min_hz = 902_000_000;
1869            fields.freq_max_hz = 928_000_000;
1870            let built = pamoja_gateway_station_message_new(&fields);
1871            for (field, text) in [
1872                (PAMOJA_GATEWAY_STATION_TEXT_REGION, "US915"),
1873                (PAMOJA_GATEWAY_STATION_TEXT_HWSPEC, "sx1301/1"),
1874            ] {
1875                assert_eq!(
1876                    pamoja_gateway_station_message_set_text(
1877                        built,
1878                        field,
1879                        text.as_ptr(),
1880                        text.len()
1881                    ),
1882                    PamojaStatus::Ok
1883                );
1884            }
1885            assert_eq!(
1886                pamoja_gateway_station_message_add_net_id(built, 0x13),
1887                PamojaStatus::Ok
1888            );
1889            let range = PamojaGatewayStationJoinRange {
1890                first: 0x70b3_d57e_d000_0000,
1891                last: 0x70b3_d57e_d000_00ff,
1892            };
1893            assert_eq!(
1894                pamoja_gateway_station_message_add_join_range(built, &range),
1895                PamojaStatus::Ok
1896            );
1897            for rate in [
1898                PamojaGatewayStationDataRate {
1899                    spreading_factor: 10,
1900                    bandwidth_hz: 125_000,
1901                    downlink_only: false,
1902                    defined: true,
1903                },
1904                PamojaGatewayStationDataRate {
1905                    spreading_factor: 0,
1906                    bandwidth_hz: 0,
1907                    downlink_only: false,
1908                    defined: false,
1909                },
1910                PamojaGatewayStationDataRate {
1911                    spreading_factor: 12,
1912                    bandwidth_hz: 500_000,
1913                    downlink_only: true,
1914                    defined: true,
1915                },
1916            ] {
1917                assert_eq!(
1918                    pamoja_gateway_station_message_add_data_rate(built, &rate),
1919                    PamojaStatus::Ok
1920                );
1921            }
1922            assert_eq!(written(built), expected.to_json());
1923        }
1924    }
1925
1926    #[test]
1927    fn a_configuration_reads_back_entry_for_entry() {
1928        let config = Message::RouterConfig {
1929            net_id: None,
1930            join_eui: vec![(1, 2)],
1931            region: "EU868".to_owned(),
1932            max_eirp: 16.0,
1933            hwspec: "sx1301/1".to_owned(),
1934            freq_range: (863_000_000, 870_000_000),
1935            data_rates: vec![Some((12, 125_000, false)), None],
1936        };
1937        let text = config.to_json();
1938
1939        unsafe {
1940            let read = pamoja_gateway_station_message_parse(text.as_ptr(), text.len());
1941            let mut fields = blank(PAMOJA_GATEWAY_STATION_OTHER);
1942            assert_eq!(
1943                pamoja_gateway_station_message_fields(read, &mut fields),
1944                PamojaStatus::Ok
1945            );
1946            assert_eq!(fields.kind, PAMOJA_GATEWAY_STATION_ROUTER_CONFIG);
1947            assert!(!fields.filters_networks, "no filter was written as null");
1948            assert_eq!(fields.join_range_count, 1);
1949            assert_eq!(fields.data_rate_count, 2);
1950
1951            let mut rate = PamojaGatewayStationDataRate {
1952                spreading_factor: 0,
1953                bandwidth_hz: 0,
1954                downlink_only: false,
1955                defined: true,
1956            };
1957            assert_eq!(
1958                pamoja_gateway_station_message_data_rate(read, 1, &mut rate),
1959                PamojaStatus::Ok
1960            );
1961            assert!(!rate.defined, "the second number is left undefined");
1962            assert_eq!(
1963                pamoja_gateway_station_message_data_rate(read, 2, &mut rate),
1964                PamojaStatus::InvalidArgument
1965            );
1966
1967            let mut region = ptr::null_mut();
1968            assert_eq!(
1969                pamoja_gateway_station_message_text(
1970                    read,
1971                    PAMOJA_GATEWAY_STATION_TEXT_REGION,
1972                    &mut region
1973                ),
1974                PamojaStatus::Ok
1975            );
1976            assert_eq!(taken(region), b"EU868");
1977            pamoja_gateway_station_message_free(read);
1978        }
1979    }
1980
1981    #[test]
1982    fn a_downlink_carries_its_windows_and_clock_both_ways() {
1983        let xtime = (0xa5_i64 << 48) | 1_000_000;
1984        let expected = Message::Downlink {
1985            dev_eui: Eui::new([0x11; 8]),
1986            class: 0,
1987            diid: 42,
1988            pdu: vec![0x60, 0x01],
1989            rx_delay: Some(1),
1990            rx1: Some((5, 868_100_000)),
1991            rx2: Some((0, 869_525_000)),
1992            ping_slot: None,
1993            priority: 0,
1994            xtime: Some(xtime),
1995            rctx: Some(0),
1996            gpstime: None,
1997        };
1998
1999        unsafe {
2000            let mut fields = blank(PAMOJA_GATEWAY_STATION_DOWNLINK);
2001            fields.dev_eui = [0x11; 8];
2002            fields.diid = 42;
2003            fields.rx_delay = 1;
2004            fields.has_rx_delay = true;
2005            fields.rx1 = window_to_c(Some((5, 868_100_000)));
2006            fields.rx2 = window_to_c(Some((0, 869_525_000)));
2007            fields.xtime = xtime;
2008            fields.has_xtime = true;
2009            fields.has_rctx = true;
2010            let built = pamoja_gateway_station_message_new(&fields);
2011            let pdu = [0x60, 0x01];
2012            assert_eq!(
2013                pamoja_gateway_station_message_set_payload(built, pdu.as_ptr(), pdu.len()),
2014                PamojaStatus::Ok
2015            );
2016            let text = written(built);
2017            assert_eq!(text, expected.to_json());
2018
2019            let read = pamoja_gateway_station_message_parse(text.as_ptr(), text.len());
2020            let mut back = blank(PAMOJA_GATEWAY_STATION_OTHER);
2021            pamoja_gateway_station_message_fields(read, &mut back);
2022            assert_eq!(back.xtime, xtime, "the clock comes back to the microsecond");
2023            assert!(back.rx1.present && back.rx2.present && !back.ping_slot.present);
2024            pamoja_gateway_station_message_free(read);
2025        }
2026    }
2027
2028    #[test]
2029    fn what_a_kind_does_not_carry_is_refused() {
2030        unsafe {
2031            let built = pamoja_gateway_station_message_new(&blank(PAMOJA_GATEWAY_STATION_VERSION));
2032            let text = "EU868";
2033            assert_eq!(
2034                pamoja_gateway_station_message_set_text(
2035                    built,
2036                    PAMOJA_GATEWAY_STATION_TEXT_REGION,
2037                    text.as_ptr(),
2038                    text.len()
2039                ),
2040                PamojaStatus::InvalidArgument
2041            );
2042            assert_eq!(
2043                pamoja_gateway_station_message_add_net_id(built, 1),
2044                PamojaStatus::InvalidArgument
2045            );
2046            let mut out = ptr::null_mut();
2047            assert_eq!(
2048                pamoja_gateway_station_message_text(built, 99, &mut out),
2049                PamojaStatus::InvalidArgument
2050            );
2051            pamoja_gateway_station_message_free(built);
2052
2053            assert!(pamoja_gateway_station_message_new(&blank(42)).is_null());
2054            assert!(pamoja_gateway_station_message_new(ptr::null()).is_null());
2055        }
2056    }
2057
2058    #[test]
2059    fn a_station_clock_is_built_and_taken_apart() {
2060        unsafe {
2061            let mut value = 0i64;
2062            assert_eq!(
2063                pamoja_gateway_station_xtime(0, 0xa5, 3_512_348_611, &mut value),
2064                PamojaStatus::Ok
2065            );
2066            assert_eq!(value, (0xa5_i64 << 48) | 3_512_348_611);
2067
2068            let mut parts = PamojaGatewayStationXtime {
2069                unit: 9,
2070                session: 0,
2071                micros: 0,
2072            };
2073            assert_eq!(
2074                pamoja_gateway_station_xtime_parts(value, &mut parts),
2075                PamojaStatus::Ok
2076            );
2077            assert_eq!(
2078                (parts.unit, parts.session, parts.micros),
2079                (0, 0xa5, 3_512_348_611)
2080            );
2081
2082            assert_eq!(
2083                pamoja_gateway_station_xtime(128, 1, 0, &mut value),
2084                PamojaStatus::InvalidArgument
2085            );
2086            assert_eq!(
2087                pamoja_gateway_station_xtime(0, 1, 1 << 48, &mut value),
2088                PamojaStatus::InvalidArgument
2089            );
2090        }
2091    }
2092
2093    #[test]
2094    fn a_server_answers_a_station_and_reads_who_asked() {
2095        let station = [0xb8, 0x27, 0xeb, 0xff, 0xfe, 0x01, 0x02, 0x03];
2096        let asked = Discovery::new(Eui::new(station)).to_json();
2097        let uri = "ws://lns.example.invalid:3001/router";
2098
2099        unsafe {
2100            let mut router = [0u8; 8];
2101            assert_eq!(
2102                pamoja_gateway_station_discovery_parse(
2103                    asked.as_ptr(),
2104                    asked.len(),
2105                    router.as_mut_ptr()
2106                ),
2107                PamojaStatus::Ok
2108            );
2109            assert_eq!(router, station);
2110
2111            let muxs = [0u8; 8];
2112            let mut text = ptr::null_mut();
2113            assert_eq!(
2114                pamoja_gateway_station_router_accepted(
2115                    router.as_ptr(),
2116                    muxs.as_ptr(),
2117                    uri.as_ptr(),
2118                    uri.len(),
2119                    &mut text
2120                ),
2121                PamojaStatus::Ok
2122            );
2123            let answer = taken(text);
2124            assert_eq!(
2125                answer,
2126                Router::accepted(Eui::new(station), Eui::new(muxs), uri)
2127                    .to_json()
2128                    .into_bytes()
2129            );
2130
2131            let mut ids = PamojaGatewayStationRouterIds {
2132                router: [0; 8],
2133                has_router: false,
2134                muxs: [1; 8],
2135                has_muxs: false,
2136            };
2137            assert_eq!(
2138                pamoja_gateway_station_router_identities(answer.as_ptr(), answer.len(), &mut ids),
2139                PamojaStatus::Ok
2140            );
2141            assert!(ids.has_router && ids.has_muxs);
2142            assert_eq!(ids.router, station);
2143            assert_eq!(ids.muxs, muxs);
2144
2145            let why = "this gateway is not registered";
2146            assert_eq!(
2147                pamoja_gateway_station_router_refused(
2148                    router.as_ptr(),
2149                    why.as_ptr(),
2150                    why.len(),
2151                    &mut text
2152                ),
2153                PamojaStatus::Ok
2154            );
2155            assert_eq!(
2156                taken(text),
2157                Router::refused(Eui::new(station), why)
2158                    .to_json()
2159                    .into_bytes()
2160            );
2161        }
2162    }
2163}