Skip to main content

pamoja_ffi/
gateway.rs

1//! The C ABI for the LoRaWAN gateway protocols.
2//!
3//! These functions wrap [`pamoja_gateway`] for callers that reach the SDK through the flat C
4//! boundary: the six datagrams a gateway and a network server exchange over UDP, and the
5//! objects they carry.
6//!
7//! A datagram carries a payload of variable length, so a packet crosses as an opaque handle:
8//! build one, read it with the `pamoja_gateway_packet_*` calls, write it with
9//! [`pamoja_gateway_packet_to_buffer`], and release it with [`pamoja_gateway_packet_free`].
10//! The packets a PUSH_DATA forwards are collected in an uplink handle first, since there may
11//! be any number of them.
12//!
13//! Times cross as counts rather than as the strings the protocol writes: a reception in
14//! microseconds since the Unix epoch and a gateway's clock in seconds, each with a flag that
15//! says whether the datagram carried one at all.
16
17use std::ptr;
18
19use pamoja_gateway::udp::{
20    CrcStatus, Eui, Modulation, Packet, PacketKind, Rxpk, Stat, TxStatus, Txpk, Uplink,
21};
22use pamoja_lora::budget::Decibels;
23use pamoja_lora::LinkSettings;
24
25use crate::lora::{settings, PamojaLoraLink};
26use crate::{read_bytes, set_last_error, PamojaBuffer, PamojaStatus};
27
28/// The protocol version every datagram starts with.
29pub const PAMOJA_GATEWAY_PROTOCOL_VERSION: u8 = 2;
30
31/// The length of a gateway's unique identifier.
32pub const PAMOJA_GATEWAY_EUI_LEN: usize = 8;
33
34/// The gateway forwarding what it heard.
35pub const PAMOJA_GATEWAY_PUSH_DATA: u8 = 0x00;
36
37/// The server acknowledging a PUSH_DATA.
38pub const PAMOJA_GATEWAY_PUSH_ACK: u8 = 0x01;
39
40/// The gateway holding its route open.
41pub const PAMOJA_GATEWAY_PULL_DATA: u8 = 0x02;
42
43/// The server sending a packet to transmit.
44pub const PAMOJA_GATEWAY_PULL_RESP: u8 = 0x03;
45
46/// The server acknowledging a PULL_DATA.
47pub const PAMOJA_GATEWAY_PULL_ACK: u8 = 0x04;
48
49/// The gateway reporting what became of a PULL_RESP.
50pub const PAMOJA_GATEWAY_TX_ACK: u8 = 0x05;
51
52/// A LoRa packet, whose settings are in the link.
53pub const PAMOJA_GATEWAY_MODULATION_LORA: u8 = 0;
54
55/// An FSK packet, whose bitrate is in `bitrate_bps`.
56pub const PAMOJA_GATEWAY_MODULATION_FSK: u8 = 1;
57
58/// The CRC checked.
59pub const PAMOJA_GATEWAY_CRC_OK: i8 = 1;
60
61/// The CRC failed.
62pub const PAMOJA_GATEWAY_CRC_FAILED: i8 = -1;
63
64/// The packet carried no CRC.
65pub const PAMOJA_GATEWAY_CRC_ABSENT: i8 = 0;
66
67/// The downlink was scheduled, which the protocol writes as `NONE`.
68pub const PAMOJA_GATEWAY_TX_NONE: u8 = 0;
69
70/// It arrived too late to schedule.
71pub const PAMOJA_GATEWAY_TX_TOO_LATE: u8 = 1;
72
73/// Its timestamp is too far ahead.
74pub const PAMOJA_GATEWAY_TX_TOO_EARLY: u8 = 2;
75
76/// Another packet was already scheduled then.
77pub const PAMOJA_GATEWAY_TX_COLLISION_PACKET: u8 = 3;
78
79/// A beacon was already scheduled then.
80pub const PAMOJA_GATEWAY_TX_COLLISION_BEACON: u8 = 4;
81
82/// The radio chain cannot reach that frequency.
83pub const PAMOJA_GATEWAY_TX_FREQ: u8 = 5;
84
85/// The gateway cannot transmit at that power.
86pub const PAMOJA_GATEWAY_TX_POWER: u8 = 6;
87
88/// A GPS timestamp was asked for while the GPS is unlocked.
89pub const PAMOJA_GATEWAY_TX_GPS_UNLOCKED: u8 = 7;
90
91const _: () = assert!(
92    PAMOJA_GATEWAY_PROTOCOL_VERSION == pamoja_gateway::udp::PROTOCOL_VERSION,
93    "the protocol version must match the crate"
94);
95const _: () = assert!(
96    PAMOJA_GATEWAY_EUI_LEN == pamoja_gateway::udp::EUI_LEN,
97    "the identifier length must match the crate"
98);
99const _: () = assert!(PAMOJA_GATEWAY_PUSH_DATA == PacketKind::PushData.identifier());
100const _: () = assert!(PAMOJA_GATEWAY_PUSH_ACK == PacketKind::PushAck.identifier());
101const _: () = assert!(PAMOJA_GATEWAY_PULL_DATA == PacketKind::PullData.identifier());
102const _: () = assert!(PAMOJA_GATEWAY_PULL_RESP == PacketKind::PullResp.identifier());
103const _: () = assert!(PAMOJA_GATEWAY_PULL_ACK == PacketKind::PullAck.identifier());
104const _: () = assert!(PAMOJA_GATEWAY_TX_ACK == PacketKind::TxAck.identifier());
105const _: () = assert!(PAMOJA_GATEWAY_CRC_OK == CrcStatus::Ok.code());
106const _: () = assert!(PAMOJA_GATEWAY_CRC_FAILED == CrcStatus::Failed.code());
107const _: () = assert!(PAMOJA_GATEWAY_CRC_ABSENT == CrcStatus::Absent.code());
108
109/// An opaque handle to one datagram of the protocol.
110///
111/// Read it with the `pamoja_gateway_packet_*` calls, then release it with
112/// [`pamoja_gateway_packet_free`].
113pub struct PamojaGatewayPacket {
114    packet: Packet,
115}
116
117/// An opaque handle to what a PUSH_DATA carries: the packets heard, and the report.
118///
119/// Fill it with [`pamoja_gateway_uplink_add_rxpk`] and [`pamoja_gateway_uplink_set_stat`],
120/// then hand it to [`pamoja_gateway_push_data`], which takes it over.
121pub struct PamojaGatewayUplink {
122    uplink: Uplink,
123}
124
125/// A packet the gateway heard, without its payload, which crosses beside it.
126#[repr(C)]
127#[derive(Clone, Copy, Debug, Default, PartialEq)]
128pub struct PamojaGatewayRxpk {
129    /// When it arrived, in microseconds since 1970-01-01 UTC, when `has_received_at`.
130    pub received_at_us: u64,
131    /// When it arrived on the GPS clock, in milliseconds, when `has_gps_millis`.
132    pub gps_millis: u64,
133    /// The carrier it arrived on, in hertz.
134    pub frequency_hz: u32,
135    /// The concentrator's own timestamp of the reception, when `has_timestamp`.
136    pub timestamp_us: u32,
137    /// The spreading factor, bandwidth, coding rate, header, and CRC, for a LoRa packet.
138    pub link: PamojaLoraLink,
139    /// The bitrate in bits per second, for an FSK packet.
140    pub bitrate_bps: u32,
141    /// The received signal strength, in hundredths of a dBm.
142    pub rssi_centi_dbm: i32,
143    /// The signal-to-noise ratio, in hundredths of a dB, when `has_snr`.
144    pub snr_centi_db: i32,
145    /// The concentrator channel it arrived on.
146    pub channel: u8,
147    /// The radio chain it arrived on.
148    pub rf_chain: u8,
149    /// What the CRC said: [`PAMOJA_GATEWAY_CRC_OK`], `_FAILED`, or `_ABSENT`.
150    pub crc: i8,
151    /// [`PAMOJA_GATEWAY_MODULATION_LORA`] or `_FSK`.
152    pub modulation: u8,
153    /// Whether the datagram carried a reception time.
154    pub has_received_at: bool,
155    /// Whether it carried a GPS time.
156    pub has_gps_millis: bool,
157    /// Whether it carried the concentrator's timestamp.
158    pub has_timestamp: bool,
159    /// Whether it carried a signal-to-noise ratio.
160    pub has_snr: bool,
161}
162
163/// A gateway's own status report.
164#[repr(C)]
165#[derive(Clone, Copy, Debug, Default, PartialEq)]
166pub struct PamojaGatewayStat {
167    /// The gateway's clock, in seconds since 1970-01-01 UTC, when `has_time`.
168    pub time_s: u64,
169    /// Its latitude in degrees, north positive, when `has_position`.
170    pub latitude_deg: f64,
171    /// Its longitude in degrees, east positive, when `has_position`.
172    pub longitude_deg: f64,
173    /// What share of its datagrams were acknowledged, as a percentage.
174    pub acknowledged_percent: f64,
175    /// Its altitude in meters, when `has_altitude`.
176    pub altitude_m: i32,
177    /// How many packets its radio received.
178    pub received: u32,
179    /// How many of those had a good CRC.
180    pub received_ok: u32,
181    /// How many it forwarded.
182    pub forwarded: u32,
183    /// How many downlink datagrams it received.
184    pub downlinks: u32,
185    /// How many packets it transmitted.
186    pub transmitted: u32,
187    /// Whether the report carried a clock reading.
188    pub has_time: bool,
189    /// Whether it carried a position.
190    pub has_position: bool,
191    /// Whether it carried an altitude.
192    pub has_altitude: bool,
193}
194
195/// A packet the server asks the gateway to transmit, without its payload.
196#[repr(C)]
197#[derive(Clone, Copy, Debug, Default, PartialEq)]
198pub struct PamojaGatewayTxpk {
199    /// The GPS time to transmit at, in milliseconds, when `has_gps_millis`.
200    pub gps_millis: u64,
201    /// The carrier to transmit on, in hertz.
202    pub frequency_hz: u32,
203    /// The concentrator timestamp to transmit at, when `has_timestamp`.
204    pub timestamp_us: u32,
205    /// The spreading factor, bandwidth, and coding rate, for a LoRa packet.
206    pub link: PamojaLoraLink,
207    /// The bitrate in bits per second, for an FSK packet.
208    pub bitrate_bps: u32,
209    /// The FSK frequency deviation in hertz, when `has_deviation`.
210    pub frequency_deviation_hz: u32,
211    /// How long a preamble to send, in symbols, when `has_preamble`.
212    pub preamble_symbols: u16,
213    /// The radio chain to transmit from.
214    pub rf_chain: u8,
215    /// The power to transmit at, in dBm.
216    pub power_dbm: i8,
217    /// [`PAMOJA_GATEWAY_MODULATION_LORA`] or `_FSK`.
218    pub modulation: u8,
219    /// Whether to transmit at once, which ignores the timestamps.
220    pub immediate: bool,
221    /// Whether to invert the LoRa polarity, as a LoRaWAN downlink is sent.
222    pub invert_polarity: bool,
223    /// Whether to leave the physical CRC off, as LoRaWAN downlinks are.
224    pub without_crc: bool,
225    /// Whether a concentrator timestamp was given.
226    pub has_timestamp: bool,
227    /// Whether a GPS time was given.
228    pub has_gps_millis: bool,
229    /// Whether an FSK deviation was given.
230    pub has_deviation: bool,
231    /// Whether a preamble length was given.
232    pub has_preamble: bool,
233}
234
235/// Creates an empty uplink for a PUSH_DATA to carry.
236///
237/// # Returns
238///
239/// A handle the caller releases with [`pamoja_gateway_uplink_free`], or hands to
240/// [`pamoja_gateway_push_data`], which takes it over.
241#[no_mangle]
242pub extern "C" fn pamoja_gateway_uplink_new() -> *mut PamojaGatewayUplink {
243    Box::into_raw(Box::new(PamojaGatewayUplink {
244        uplink: Uplink::default(),
245    }))
246}
247
248/// Adds a packet the gateway heard.
249///
250/// # Arguments
251///
252/// * `uplink` - the uplink being built.
253/// * `packet` - the metadata, whose `modulation` says how to read its link or bitrate.
254/// * `payload` - the packet itself.
255/// * `payload_len` - its length.
256///
257/// # Returns
258///
259/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null handle or a null
260/// payload with a length.
261///
262/// # Safety
263///
264/// `uplink` must be a live handle from [`pamoja_gateway_uplink_new`], and `payload` must
265/// point at `payload_len` readable bytes or be null.
266#[no_mangle]
267pub unsafe extern "C" fn pamoja_gateway_uplink_add_rxpk(
268    uplink: *mut PamojaGatewayUplink,
269    packet: PamojaGatewayRxpk,
270    payload: *const u8,
271    payload_len: usize,
272) -> PamojaStatus {
273    let Some(uplink) = uplink.as_mut() else {
274        return missing("uplink");
275    };
276    let payload = match read_bytes(payload, payload_len) {
277        Ok(payload) => payload,
278        Err(status) => return status,
279    };
280    uplink.uplink.packets.push(rxpk_of(packet, payload));
281    PamojaStatus::Ok
282}
283
284/// Sets the gateway's status report on an uplink.
285///
286/// # Arguments
287///
288/// * `uplink` - the uplink being built.
289/// * `status` - the report.
290///
291/// # Returns
292///
293/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null handle.
294///
295/// # Safety
296///
297/// `uplink` must be a live handle from [`pamoja_gateway_uplink_new`].
298#[no_mangle]
299pub unsafe extern "C" fn pamoja_gateway_uplink_set_stat(
300    uplink: *mut PamojaGatewayUplink,
301    status: PamojaGatewayStat,
302) -> PamojaStatus {
303    let Some(uplink) = uplink.as_mut() else {
304        return missing("uplink");
305    };
306    uplink.uplink.status = Some(stat_of(status));
307    PamojaStatus::Ok
308}
309
310/// Releases an uplink that will not be sent.
311///
312/// # Arguments
313///
314/// * `uplink` - the uplink, which must not be used again.
315///
316/// # Safety
317///
318/// `uplink` must be a live handle from [`pamoja_gateway_uplink_new`] that was not handed to
319/// [`pamoja_gateway_push_data`], or null.
320#[no_mangle]
321pub unsafe extern "C" fn pamoja_gateway_uplink_free(uplink: *mut PamojaGatewayUplink) {
322    if !uplink.is_null() {
323        drop(Box::from_raw(uplink));
324    }
325}
326
327/// Builds the PUSH_DATA that forwards an uplink.
328///
329/// # Arguments
330///
331/// * `token` - the random token the acknowledgment carries back.
332/// * `gateway` - the gateway's eight-byte identifier.
333/// * `uplink` - the uplink, which this call takes over and releases.
334/// * `out_packet` - receives the packet.
335///
336/// # Returns
337///
338/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
339///
340/// # Safety
341///
342/// `gateway` must point at [`PAMOJA_GATEWAY_EUI_LEN`] readable bytes, `uplink` must be a live
343/// handle that is not used again, and `out_packet` must be writable.
344#[no_mangle]
345pub unsafe extern "C" fn pamoja_gateway_push_data(
346    token: u16,
347    gateway: *const u8,
348    uplink: *mut PamojaGatewayUplink,
349    out_packet: *mut *mut PamojaGatewayPacket,
350) -> PamojaStatus {
351    if !clear(out_packet) {
352        return PamojaStatus::InvalidArgument;
353    }
354    let Some(identifier) = eui_of(gateway) else {
355        return missing("gateway");
356    };
357    if uplink.is_null() {
358        return missing("uplink");
359    }
360    let uplink = Box::from_raw(uplink).uplink;
361    *out_packet = wrap(Packet::PushData {
362        token,
363        gateway: identifier,
364        uplink,
365    });
366    PamojaStatus::Ok
367}
368
369/// Builds the PUSH_ACK that answers a PUSH_DATA.
370///
371/// # Arguments
372///
373/// * `token` - the token of the datagram being acknowledged.
374///
375/// # Returns
376///
377/// A handle the caller releases with [`pamoja_gateway_packet_free`].
378#[no_mangle]
379pub extern "C" fn pamoja_gateway_push_ack(token: u16) -> *mut PamojaGatewayPacket {
380    wrap(Packet::PushAck { token })
381}
382
383/// Builds the PULL_DATA that holds a route open.
384///
385/// # Arguments
386///
387/// * `token` - the random token the acknowledgment carries back.
388/// * `gateway` - the gateway's eight-byte identifier.
389/// * `out_packet` - receives the packet.
390///
391/// # Returns
392///
393/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
394///
395/// # Safety
396///
397/// `gateway` must point at [`PAMOJA_GATEWAY_EUI_LEN`] readable bytes and `out_packet` must be
398/// writable.
399#[no_mangle]
400pub unsafe extern "C" fn pamoja_gateway_pull_data(
401    token: u16,
402    gateway: *const u8,
403    out_packet: *mut *mut PamojaGatewayPacket,
404) -> PamojaStatus {
405    if !clear(out_packet) {
406        return PamojaStatus::InvalidArgument;
407    }
408    let Some(identifier) = eui_of(gateway) else {
409        return missing("gateway");
410    };
411    *out_packet = wrap(Packet::PullData {
412        token,
413        gateway: identifier,
414    });
415    PamojaStatus::Ok
416}
417
418/// Builds the PULL_ACK that answers a PULL_DATA.
419///
420/// # Arguments
421///
422/// * `token` - the token of the datagram being acknowledged.
423///
424/// # Returns
425///
426/// A handle the caller releases with [`pamoja_gateway_packet_free`].
427#[no_mangle]
428pub extern "C" fn pamoja_gateway_pull_ack(token: u16) -> *mut PamojaGatewayPacket {
429    wrap(Packet::PullAck { token })
430}
431
432/// Builds the PULL_RESP that asks a gateway to transmit.
433///
434/// # Arguments
435///
436/// * `token` - the random token the TX_ACK carries back.
437/// * `transmit` - what to transmit, and when.
438/// * `payload` - the packet itself.
439/// * `payload_len` - its length.
440/// * `out_packet` - receives the packet.
441///
442/// # Returns
443///
444/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument.
445///
446/// # Safety
447///
448/// `payload` must point at `payload_len` readable bytes or be null, and `out_packet` must be
449/// writable.
450#[no_mangle]
451pub unsafe extern "C" fn pamoja_gateway_pull_resp(
452    token: u16,
453    transmit: PamojaGatewayTxpk,
454    payload: *const u8,
455    payload_len: usize,
456    out_packet: *mut *mut PamojaGatewayPacket,
457) -> PamojaStatus {
458    if !clear(out_packet) {
459        return PamojaStatus::InvalidArgument;
460    }
461    let payload = match read_bytes(payload, payload_len) {
462        Ok(payload) => payload,
463        Err(status) => return status,
464    };
465    *out_packet = wrap(Packet::PullResp {
466        token,
467        transmit: txpk_of(transmit, payload),
468    });
469    PamojaStatus::Ok
470}
471
472/// Builds the TX_ACK that reports what became of a PULL_RESP.
473///
474/// # Arguments
475///
476/// * `token` - the token of the PULL_RESP being answered.
477/// * `gateway` - the gateway's eight-byte identifier.
478/// * `status` - [`PAMOJA_GATEWAY_TX_NONE`] when it was scheduled, or why it was refused.
479/// * `out_packet` - receives the packet.
480///
481/// # Returns
482///
483/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument or a status
484/// the protocol does not define.
485///
486/// # Safety
487///
488/// `gateway` must point at [`PAMOJA_GATEWAY_EUI_LEN`] readable bytes and `out_packet` must be
489/// writable.
490#[no_mangle]
491pub unsafe extern "C" fn pamoja_gateway_tx_ack(
492    token: u16,
493    gateway: *const u8,
494    status: u8,
495    out_packet: *mut *mut PamojaGatewayPacket,
496) -> PamojaStatus {
497    if !clear(out_packet) {
498        return PamojaStatus::InvalidArgument;
499    }
500    let Some(identifier) = eui_of(gateway) else {
501        return missing("gateway");
502    };
503    let Some(status) = tx_status_of(status) else {
504        set_last_error(format!("{status} is not a TX_ACK status"));
505        return PamojaStatus::InvalidArgument;
506    };
507    *out_packet = wrap(Packet::TxAck {
508        token,
509        gateway: identifier,
510        status,
511    });
512    PamojaStatus::Ok
513}
514
515/// Reads a datagram.
516///
517/// # Arguments
518///
519/// * `bytes` - the datagram as it arrived.
520/// * `len` - its length.
521/// * `out_packet` - receives the packet.
522///
523/// # Returns
524///
525/// [`PamojaStatus::Ok`], or [`PamojaStatus::Codec`] for a datagram this protocol does not
526/// describe, with the reason in the last error message.
527///
528/// # Safety
529///
530/// `bytes` must point at `len` readable bytes or be null, and `out_packet` must be writable.
531#[no_mangle]
532pub unsafe extern "C" fn pamoja_gateway_packet_parse(
533    bytes: *const u8,
534    len: usize,
535    out_packet: *mut *mut PamojaGatewayPacket,
536) -> PamojaStatus {
537    if !clear(out_packet) {
538        return PamojaStatus::InvalidArgument;
539    }
540    let datagram = match read_bytes(bytes, len) {
541        Ok(datagram) => datagram,
542        Err(status) => return status,
543    };
544    match Packet::parse(&datagram) {
545        Ok(packet) => {
546            *out_packet = wrap(packet);
547            PamojaStatus::Ok
548        }
549        Err(error) => {
550            set_last_error(error.to_string());
551            PamojaStatus::Codec
552        }
553    }
554}
555
556/// Returns which kind of datagram a packet is.
557///
558/// # Arguments
559///
560/// * `packet` - the packet.
561///
562/// # Returns
563///
564/// One of [`PAMOJA_GATEWAY_PUSH_DATA`] through [`PAMOJA_GATEWAY_TX_ACK`], or 255 if `packet`
565/// is null.
566///
567/// # Safety
568///
569/// `packet` must be a live handle, or null.
570#[no_mangle]
571pub unsafe extern "C" fn pamoja_gateway_packet_kind(packet: *const PamojaGatewayPacket) -> u8 {
572    match packet.as_ref() {
573        Some(packet) => packet.packet.kind().identifier(),
574        None => u8::MAX,
575    }
576}
577
578/// Returns a packet's token, which pairs it with its answer.
579///
580/// # Arguments
581///
582/// * `packet` - the packet.
583///
584/// # Returns
585///
586/// The token, or 0 if `packet` is null.
587///
588/// # Safety
589///
590/// `packet` must be a live handle, or null.
591#[no_mangle]
592pub unsafe extern "C" fn pamoja_gateway_packet_token(packet: *const PamojaGatewayPacket) -> u16 {
593    packet.as_ref().map_or(0, |packet| packet.packet.token())
594}
595
596/// Writes the gateway's identifier, for the datagrams that carry one.
597///
598/// # Arguments
599///
600/// * `packet` - the packet.
601/// * `out_gateway` - receives [`PAMOJA_GATEWAY_EUI_LEN`] bytes.
602///
603/// # Returns
604///
605/// `true` when the packet carries an identifier, which the datagrams a server sends do not.
606///
607/// # Safety
608///
609/// `packet` must be a live handle or null, and `out_gateway` must point at
610/// [`PAMOJA_GATEWAY_EUI_LEN`] writable bytes or be null.
611#[no_mangle]
612pub unsafe extern "C" fn pamoja_gateway_packet_gateway(
613    packet: *const PamojaGatewayPacket,
614    out_gateway: *mut u8,
615) -> bool {
616    let Some(packet) = packet.as_ref() else {
617        return false;
618    };
619    let Some(identifier) = packet.packet.gateway() else {
620        return false;
621    };
622    if out_gateway.is_null() {
623        return false;
624    }
625    ptr::copy_nonoverlapping(
626        identifier.bytes().as_ptr(),
627        out_gateway,
628        PAMOJA_GATEWAY_EUI_LEN,
629    );
630    true
631}
632
633/// Returns how many packets a PUSH_DATA forwards.
634///
635/// # Arguments
636///
637/// * `packet` - the packet.
638///
639/// # Returns
640///
641/// The count, or 0 for any other kind.
642///
643/// # Safety
644///
645/// `packet` must be a live handle, or null.
646#[no_mangle]
647pub unsafe extern "C" fn pamoja_gateway_packet_rxpk_count(
648    packet: *const PamojaGatewayPacket,
649) -> usize {
650    match packet.as_ref().map(|packet| &packet.packet) {
651        Some(Packet::PushData { uplink, .. }) => uplink.packets.len(),
652        _ => 0,
653    }
654}
655
656/// Reads one of the packets a PUSH_DATA forwards.
657///
658/// # Arguments
659///
660/// * `packet` - the packet.
661/// * `index` - which forwarded packet, from zero.
662/// * `out_rxpk` - receives its metadata.
663///
664/// # Returns
665///
666/// `true` when there is a packet at that index.
667///
668/// # Safety
669///
670/// `packet` must be a live handle or null, and `out_rxpk` must be writable or null.
671#[no_mangle]
672pub unsafe extern "C" fn pamoja_gateway_packet_rxpk(
673    packet: *const PamojaGatewayPacket,
674    index: usize,
675    out_rxpk: *mut PamojaGatewayRxpk,
676) -> bool {
677    let Some(heard) = heard_at(packet, index) else {
678        return false;
679    };
680    if out_rxpk.is_null() {
681        return false;
682    }
683    *out_rxpk = rxpk_to_c(heard);
684    true
685}
686
687/// Returns a pointer to one forwarded packet's payload.
688///
689/// Use [`pamoja_gateway_packet_rxpk_payload_len`] for its length. The pointer is valid until
690/// the packet is freed.
691///
692/// # Arguments
693///
694/// * `packet` - the packet.
695/// * `index` - which forwarded packet, from zero.
696///
697/// # Returns
698///
699/// The pointer, or null when there is no packet at that index.
700///
701/// # Safety
702///
703/// `packet` must be a live handle, or null.
704#[no_mangle]
705pub unsafe extern "C" fn pamoja_gateway_packet_rxpk_payload(
706    packet: *const PamojaGatewayPacket,
707    index: usize,
708) -> *const u8 {
709    heard_at(packet, index).map_or(ptr::null(), |heard| heard.payload.as_ptr())
710}
711
712/// Returns the length of one forwarded packet's payload.
713///
714/// # Arguments
715///
716/// * `packet` - the packet.
717/// * `index` - which forwarded packet, from zero.
718///
719/// # Returns
720///
721/// The length, or 0 when there is no packet at that index.
722///
723/// # Safety
724///
725/// `packet` must be a live handle, or null.
726#[no_mangle]
727pub unsafe extern "C" fn pamoja_gateway_packet_rxpk_payload_len(
728    packet: *const PamojaGatewayPacket,
729    index: usize,
730) -> usize {
731    heard_at(packet, index).map_or(0, |heard| heard.payload.len())
732}
733
734/// Reads the gateway's status report, when a PUSH_DATA carries one.
735///
736/// # Arguments
737///
738/// * `packet` - the packet.
739/// * `out_stat` - receives the report.
740///
741/// # Returns
742///
743/// `true` when the datagram carried a report.
744///
745/// # Safety
746///
747/// `packet` must be a live handle or null, and `out_stat` must be writable or null.
748#[no_mangle]
749pub unsafe extern "C" fn pamoja_gateway_packet_stat(
750    packet: *const PamojaGatewayPacket,
751    out_stat: *mut PamojaGatewayStat,
752) -> bool {
753    let report = match packet.as_ref().map(|packet| &packet.packet) {
754        Some(Packet::PushData { uplink, .. }) => uplink.status.as_ref(),
755        _ => None,
756    };
757    let Some(report) = report else {
758        return false;
759    };
760    if out_stat.is_null() {
761        return false;
762    }
763    *out_stat = stat_to_c(report);
764    true
765}
766
767/// Reads what a PULL_RESP asks the gateway to transmit.
768///
769/// # Arguments
770///
771/// * `packet` - the packet.
772/// * `out_txpk` - receives the request.
773///
774/// # Returns
775///
776/// `true` when the packet is a PULL_RESP.
777///
778/// # Safety
779///
780/// `packet` must be a live handle or null, and `out_txpk` must be writable or null.
781#[no_mangle]
782pub unsafe extern "C" fn pamoja_gateway_packet_txpk(
783    packet: *const PamojaGatewayPacket,
784    out_txpk: *mut PamojaGatewayTxpk,
785) -> bool {
786    let Some(request) = requested(packet) else {
787        return false;
788    };
789    if out_txpk.is_null() {
790        return false;
791    }
792    *out_txpk = txpk_to_c(request);
793    true
794}
795
796/// Returns a pointer to the payload a PULL_RESP carries.
797///
798/// Use [`pamoja_gateway_packet_txpk_payload_len`] for its length. The pointer is valid until
799/// the packet is freed.
800///
801/// # Arguments
802///
803/// * `packet` - the packet.
804///
805/// # Returns
806///
807/// The pointer, or null when the packet is not a PULL_RESP.
808///
809/// # Safety
810///
811/// `packet` must be a live handle, or null.
812#[no_mangle]
813pub unsafe extern "C" fn pamoja_gateway_packet_txpk_payload(
814    packet: *const PamojaGatewayPacket,
815) -> *const u8 {
816    requested(packet).map_or(ptr::null(), |request| request.payload.as_ptr())
817}
818
819/// Returns the length of the payload a PULL_RESP carries.
820///
821/// # Arguments
822///
823/// * `packet` - the packet.
824///
825/// # Returns
826///
827/// The length, or 0 when the packet is not a PULL_RESP.
828///
829/// # Safety
830///
831/// `packet` must be a live handle, or null.
832#[no_mangle]
833pub unsafe extern "C" fn pamoja_gateway_packet_txpk_payload_len(
834    packet: *const PamojaGatewayPacket,
835) -> usize {
836    requested(packet).map_or(0, |request| request.payload.len())
837}
838
839/// Returns what a TX_ACK reports.
840///
841/// # Arguments
842///
843/// * `packet` - the packet.
844///
845/// # Returns
846///
847/// One of [`PAMOJA_GATEWAY_TX_NONE`] through [`PAMOJA_GATEWAY_TX_GPS_UNLOCKED`], or 255 when
848/// the packet is not a TX_ACK.
849///
850/// # Safety
851///
852/// `packet` must be a live handle, or null.
853#[no_mangle]
854pub unsafe extern "C" fn pamoja_gateway_packet_tx_status(packet: *const PamojaGatewayPacket) -> u8 {
855    match packet.as_ref().map(|packet| &packet.packet) {
856        Some(Packet::TxAck { status, .. }) => tx_status_code(*status),
857        _ => u8::MAX,
858    }
859}
860
861/// Builds the acknowledgment a server owes a datagram.
862///
863/// # Arguments
864///
865/// * `packet` - the datagram that arrived.
866/// * `out_packet` - receives the acknowledgment.
867///
868/// # Returns
869///
870/// `true` for a PUSH_DATA or a PULL_DATA, which are the datagrams a server acknowledges. A
871/// PULL_RESP is answered with a TX_ACK, which names the gateway, so the gateway builds that
872/// one with [`pamoja_gateway_tx_ack`].
873///
874/// # Safety
875///
876/// `packet` must be a live handle or null, and `out_packet` must be writable or null.
877#[no_mangle]
878pub unsafe extern "C" fn pamoja_gateway_packet_acknowledgment(
879    packet: *const PamojaGatewayPacket,
880    out_packet: *mut *mut PamojaGatewayPacket,
881) -> bool {
882    if !clear(out_packet) {
883        return false;
884    }
885    let Some(packet) = packet.as_ref() else {
886        return false;
887    };
888    match packet.packet.acknowledgment() {
889        Some(acknowledgment) => {
890            *out_packet = wrap(acknowledgment);
891            true
892        }
893        None => false,
894    }
895}
896
897/// Writes a packet as the datagram to send.
898///
899/// # Arguments
900///
901/// * `packet` - the packet.
902///
903/// # Returns
904///
905/// A buffer the caller releases with `pamoja_buffer_free`, or null if `packet` is null.
906///
907/// # Safety
908///
909/// `packet` must be a live handle, or null.
910#[no_mangle]
911pub unsafe extern "C" fn pamoja_gateway_packet_to_buffer(
912    packet: *const PamojaGatewayPacket,
913) -> *mut PamojaBuffer {
914    match packet.as_ref() {
915        Some(packet) => PamojaBuffer::into_raw(packet.packet.to_bytes()),
916        None => ptr::null_mut(),
917    }
918}
919
920/// Releases a packet.
921///
922/// # Arguments
923///
924/// * `packet` - the packet, which must not be used again.
925///
926/// # Safety
927///
928/// `packet` must be a live handle from one of the builders or from
929/// [`pamoja_gateway_packet_parse`], or null.
930#[no_mangle]
931pub unsafe extern "C" fn pamoja_gateway_packet_free(packet: *mut PamojaGatewayPacket) {
932    if !packet.is_null() {
933        drop(Box::from_raw(packet));
934    }
935}
936
937/// Wraps a packet in a handle for the caller to own.
938fn wrap(packet: Packet) -> *mut PamojaGatewayPacket {
939    Box::into_raw(Box::new(PamojaGatewayPacket { packet }))
940}
941
942/// Nulls an out pointer before a handle is written into it, refusing a null pointer.
943unsafe fn clear(out_packet: *mut *mut PamojaGatewayPacket) -> bool {
944    if out_packet.is_null() {
945        set_last_error("out_packet must not be null".to_owned());
946        return false;
947    }
948    *out_packet = ptr::null_mut();
949    true
950}
951
952/// Records a null argument and reports it.
953pub(crate) fn missing(name: &str) -> PamojaStatus {
954    set_last_error(format!("{name} must not be null"));
955    PamojaStatus::InvalidArgument
956}
957
958/// Reads a gateway identifier from the boundary.
959unsafe fn eui_of(gateway: *const u8) -> Option<Eui> {
960    if gateway.is_null() {
961        return None;
962    }
963    let mut bytes = [0u8; PAMOJA_GATEWAY_EUI_LEN];
964    ptr::copy_nonoverlapping(gateway, bytes.as_mut_ptr(), PAMOJA_GATEWAY_EUI_LEN);
965    Some(Eui::new(bytes))
966}
967
968/// Borrows one of the packets a PUSH_DATA forwards.
969unsafe fn heard_at<'a>(packet: *const PamojaGatewayPacket, index: usize) -> Option<&'a Rxpk> {
970    match packet.as_ref().map(|packet| &packet.packet) {
971        Some(Packet::PushData { uplink, .. }) => uplink.packets.get(index),
972        _ => None,
973    }
974}
975
976/// Borrows what a PULL_RESP asks for.
977unsafe fn requested<'a>(packet: *const PamojaGatewayPacket) -> Option<&'a Txpk> {
978    match packet.as_ref().map(|packet| &packet.packet) {
979        Some(Packet::PullResp { transmit, .. }) => Some(transmit),
980        _ => None,
981    }
982}
983
984/// Names the TX_ACK status a code selects.
985fn tx_status_of(code: u8) -> Option<TxStatus> {
986    Some(match code {
987        PAMOJA_GATEWAY_TX_NONE => TxStatus::None,
988        PAMOJA_GATEWAY_TX_TOO_LATE => TxStatus::TooLate,
989        PAMOJA_GATEWAY_TX_TOO_EARLY => TxStatus::TooEarly,
990        PAMOJA_GATEWAY_TX_COLLISION_PACKET => TxStatus::CollisionPacket,
991        PAMOJA_GATEWAY_TX_COLLISION_BEACON => TxStatus::CollisionBeacon,
992        PAMOJA_GATEWAY_TX_FREQ => TxStatus::TxFreq,
993        PAMOJA_GATEWAY_TX_POWER => TxStatus::TxPower,
994        PAMOJA_GATEWAY_TX_GPS_UNLOCKED => TxStatus::GpsUnlocked,
995        _ => return None,
996    })
997}
998
999/// Returns the code a TX_ACK status crosses as.
1000fn tx_status_code(status: TxStatus) -> u8 {
1001    match status {
1002        TxStatus::None => PAMOJA_GATEWAY_TX_NONE,
1003        TxStatus::TooLate => PAMOJA_GATEWAY_TX_TOO_LATE,
1004        TxStatus::TooEarly => PAMOJA_GATEWAY_TX_TOO_EARLY,
1005        TxStatus::CollisionPacket => PAMOJA_GATEWAY_TX_COLLISION_PACKET,
1006        TxStatus::CollisionBeacon => PAMOJA_GATEWAY_TX_COLLISION_BEACON,
1007        TxStatus::TxFreq => PAMOJA_GATEWAY_TX_FREQ,
1008        TxStatus::TxPower => PAMOJA_GATEWAY_TX_POWER,
1009        TxStatus::GpsUnlocked => PAMOJA_GATEWAY_TX_GPS_UNLOCKED,
1010    }
1011}
1012
1013/// Reads a modulation from the boundary.
1014fn modulation_of(code: u8, link: PamojaLoraLink, bitrate_bps: u32) -> Modulation {
1015    if code == PAMOJA_GATEWAY_MODULATION_FSK {
1016        Modulation::Fsk(bitrate_bps)
1017    } else {
1018        Modulation::Lora(settings(link))
1019    }
1020}
1021
1022/// Flattens a modulation for the boundary.
1023fn modulation_to_c(modulation: Modulation) -> (u8, PamojaLoraLink, u32) {
1024    match modulation {
1025        Modulation::Lora(link) => (PAMOJA_GATEWAY_MODULATION_LORA, link_to_c(link), 0),
1026        Modulation::Fsk(bitrate) => (
1027            PAMOJA_GATEWAY_MODULATION_FSK,
1028            link_to_c(LinkSettings::new(7, 125_000)),
1029            bitrate,
1030        ),
1031    }
1032}
1033
1034/// Flattens link settings for the boundary.
1035pub(crate) fn link_to_c(link: LinkSettings) -> PamojaLoraLink {
1036    PamojaLoraLink {
1037        bandwidth_hz: link.bandwidth_hz(),
1038        preamble_symbols: link.preamble_symbols(),
1039        spreading_factor: link.spreading_factor(),
1040        coding_rate_denominator: link.coding_rate_denominator(),
1041        explicit_header: u8::from(link.explicit_header()),
1042        crc: u8::from(link.crc()),
1043    }
1044}
1045
1046/// Reads a forwarded packet from the boundary.
1047pub(crate) fn rxpk_of(packet: PamojaGatewayRxpk, payload: Vec<u8>) -> Rxpk {
1048    Rxpk {
1049        received_at: packet
1050            .has_received_at
1051            .then(|| pamoja_gateway::time::compact(packet.received_at_us)),
1052        gps_millis: packet.has_gps_millis.then_some(packet.gps_millis),
1053        timestamp_us: packet.has_timestamp.then_some(packet.timestamp_us),
1054        frequency_hz: packet.frequency_hz,
1055        channel: packet.channel,
1056        rf_chain: packet.rf_chain,
1057        crc: CrcStatus::from_code(i64::from(packet.crc)).unwrap_or(CrcStatus::Absent),
1058        modulation: modulation_of(packet.modulation, packet.link, packet.bitrate_bps),
1059        rssi_dbm: Decibels::from_hundredths(packet.rssi_centi_dbm),
1060        snr_db: packet
1061            .has_snr
1062            .then(|| Decibels::from_hundredths(packet.snr_centi_db)),
1063        payload,
1064    }
1065}
1066
1067/// Flattens a forwarded packet for the boundary.
1068fn rxpk_to_c(heard: &Rxpk) -> PamojaGatewayRxpk {
1069    let (modulation, link, bitrate_bps) = modulation_to_c(heard.modulation);
1070    let received_at = heard
1071        .received_at
1072        .as_deref()
1073        .and_then(pamoja_gateway::time::from_compact);
1074    PamojaGatewayRxpk {
1075        received_at_us: received_at.unwrap_or(0),
1076        gps_millis: heard.gps_millis.unwrap_or(0),
1077        frequency_hz: heard.frequency_hz,
1078        timestamp_us: heard.timestamp_us.unwrap_or(0),
1079        link,
1080        bitrate_bps,
1081        rssi_centi_dbm: heard.rssi_dbm.hundredths(),
1082        snr_centi_db: heard.snr_db.map_or(0, Decibels::hundredths),
1083        channel: heard.channel,
1084        rf_chain: heard.rf_chain,
1085        crc: heard.crc.code(),
1086        modulation,
1087        has_received_at: received_at.is_some(),
1088        has_gps_millis: heard.gps_millis.is_some(),
1089        has_timestamp: heard.timestamp_us.is_some(),
1090        has_snr: heard.snr_db.is_some(),
1091    }
1092}
1093
1094/// Reads a status report from the boundary.
1095fn stat_of(report: PamojaGatewayStat) -> Stat {
1096    Stat {
1097        time: report
1098            .has_time
1099            .then(|| pamoja_gateway::time::expanded(report.time_s)),
1100        latitude_deg: report.has_position.then_some(report.latitude_deg),
1101        longitude_deg: report.has_position.then_some(report.longitude_deg),
1102        altitude_m: report.has_altitude.then_some(report.altitude_m),
1103        received: report.received,
1104        received_ok: report.received_ok,
1105        forwarded: report.forwarded,
1106        acknowledged_percent: report.acknowledged_percent,
1107        downlinks: report.downlinks,
1108        transmitted: report.transmitted,
1109    }
1110}
1111
1112/// Flattens a status report for the boundary.
1113fn stat_to_c(report: &Stat) -> PamojaGatewayStat {
1114    let clock = report
1115        .time
1116        .as_deref()
1117        .and_then(pamoja_gateway::time::from_expanded);
1118    PamojaGatewayStat {
1119        time_s: clock.unwrap_or(0),
1120        latitude_deg: report.latitude_deg.unwrap_or(0.0),
1121        longitude_deg: report.longitude_deg.unwrap_or(0.0),
1122        acknowledged_percent: report.acknowledged_percent,
1123        altitude_m: report.altitude_m.unwrap_or(0),
1124        received: report.received,
1125        received_ok: report.received_ok,
1126        forwarded: report.forwarded,
1127        downlinks: report.downlinks,
1128        transmitted: report.transmitted,
1129        has_time: clock.is_some(),
1130        has_position: report.latitude_deg.is_some() && report.longitude_deg.is_some(),
1131        has_altitude: report.altitude_m.is_some(),
1132    }
1133}
1134
1135/// Reads a transmission request from the boundary.
1136fn txpk_of(request: PamojaGatewayTxpk, payload: Vec<u8>) -> Txpk {
1137    Txpk {
1138        immediate: request.immediate,
1139        timestamp_us: request.has_timestamp.then_some(request.timestamp_us),
1140        gps_millis: request.has_gps_millis.then_some(request.gps_millis),
1141        frequency_hz: request.frequency_hz,
1142        rf_chain: request.rf_chain,
1143        power_dbm: request.power_dbm,
1144        modulation: modulation_of(request.modulation, request.link, request.bitrate_bps),
1145        frequency_deviation_hz: request
1146            .has_deviation
1147            .then_some(request.frequency_deviation_hz),
1148        invert_polarity: request.invert_polarity,
1149        preamble_symbols: request.has_preamble.then_some(request.preamble_symbols),
1150        without_crc: request.without_crc,
1151        payload,
1152    }
1153}
1154
1155/// Flattens a transmission request for the boundary.
1156pub(crate) fn txpk_to_c(request: &Txpk) -> PamojaGatewayTxpk {
1157    let (modulation, link, bitrate_bps) = modulation_to_c(request.modulation);
1158    PamojaGatewayTxpk {
1159        gps_millis: request.gps_millis.unwrap_or(0),
1160        frequency_hz: request.frequency_hz,
1161        timestamp_us: request.timestamp_us.unwrap_or(0),
1162        link,
1163        bitrate_bps,
1164        frequency_deviation_hz: request.frequency_deviation_hz.unwrap_or(0),
1165        preamble_symbols: request.preamble_symbols.unwrap_or(0),
1166        rf_chain: request.rf_chain,
1167        power_dbm: request.power_dbm,
1168        modulation,
1169        immediate: request.immediate,
1170        invert_polarity: request.invert_polarity,
1171        without_crc: request.without_crc,
1172        has_timestamp: request.timestamp_us.is_some(),
1173        has_gps_millis: request.gps_millis.is_some(),
1174        has_deviation: request.frequency_deviation_hz.is_some(),
1175        has_preamble: request.preamble_symbols.is_some(),
1176    }
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181    use super::*;
1182    use crate::lora::pamoja_lora_link_default;
1183    use crate::{pamoja_buffer_data, pamoja_buffer_free, pamoja_buffer_len};
1184
1185    const GATEWAY: [u8; 8] = [0xB8, 0x27, 0xEB, 0xFF, 0xFE, 0x01, 0x02, 0x03];
1186
1187    fn bytes_of(packet: *const PamojaGatewayPacket) -> Vec<u8> {
1188        unsafe {
1189            let buffer = pamoja_gateway_packet_to_buffer(packet);
1190            let len = pamoja_buffer_len(buffer);
1191            let bytes = std::slice::from_raw_parts(pamoja_buffer_data(buffer), len).to_vec();
1192            pamoja_buffer_free(buffer);
1193            bytes
1194        }
1195    }
1196
1197    #[test]
1198    fn a_push_data_crosses_and_comes_back() {
1199        unsafe {
1200            let uplink = pamoja_gateway_uplink_new();
1201            let heard = PamojaGatewayRxpk {
1202                frequency_hz: 868_100_000,
1203                link: pamoja_lora_link_default(7, 125_000),
1204                rssi_centi_dbm: -3_500,
1205                snr_centi_db: 510,
1206                has_snr: true,
1207                timestamp_us: 3_512_348_611,
1208                has_timestamp: true,
1209                received_at_us: 1_364_746_877_528_002,
1210                has_received_at: true,
1211                crc: PAMOJA_GATEWAY_CRC_OK,
1212                channel: 2,
1213                ..PamojaGatewayRxpk::default()
1214            };
1215            let payload = b"TEST_PACKET_1234";
1216            assert_eq!(
1217                pamoja_gateway_uplink_add_rxpk(uplink, heard, payload.as_ptr(), payload.len()),
1218                PamojaStatus::Ok
1219            );
1220
1221            let mut packet = ptr::null_mut();
1222            assert_eq!(
1223                pamoja_gateway_push_data(0x1234, GATEWAY.as_ptr(), uplink, &mut packet),
1224                PamojaStatus::Ok
1225            );
1226            let datagram = bytes_of(packet);
1227            pamoja_gateway_packet_free(packet);
1228
1229            let mut read = ptr::null_mut();
1230            assert_eq!(
1231                pamoja_gateway_packet_parse(datagram.as_ptr(), datagram.len(), &mut read),
1232                PamojaStatus::Ok
1233            );
1234            assert_eq!(pamoja_gateway_packet_kind(read), PAMOJA_GATEWAY_PUSH_DATA);
1235            assert_eq!(pamoja_gateway_packet_token(read), 0x1234);
1236            let mut identifier = [0u8; 8];
1237            assert!(pamoja_gateway_packet_gateway(read, identifier.as_mut_ptr()));
1238            assert_eq!(identifier, GATEWAY);
1239            assert_eq!(pamoja_gateway_packet_rxpk_count(read), 1);
1240
1241            let mut back = PamojaGatewayRxpk::default();
1242            assert!(pamoja_gateway_packet_rxpk(read, 0, &mut back));
1243            assert_eq!(back, heard);
1244            let len = pamoja_gateway_packet_rxpk_payload_len(read, 0);
1245            let carried =
1246                std::slice::from_raw_parts(pamoja_gateway_packet_rxpk_payload(read, 0), len);
1247            assert_eq!(carried, payload);
1248
1249            let mut ack = ptr::null_mut();
1250            assert!(pamoja_gateway_packet_acknowledgment(read, &mut ack));
1251            assert_eq!(bytes_of(ack), [2, 0x12, 0x34, 0x01]);
1252            pamoja_gateway_packet_free(ack);
1253            pamoja_gateway_packet_free(read);
1254        }
1255    }
1256
1257    #[test]
1258    fn a_pull_resp_and_its_tx_ack_cross() {
1259        unsafe {
1260            let request = PamojaGatewayTxpk {
1261                frequency_hz: 869_525_000,
1262                link: pamoja_lora_link_default(9, 125_000),
1263                power_dbm: 27,
1264                timestamp_us: 3_512_348_611,
1265                has_timestamp: true,
1266                invert_polarity: true,
1267                without_crc: true,
1268                ..PamojaGatewayTxpk::default()
1269            };
1270            let payload = b"downlink";
1271            let mut packet = ptr::null_mut();
1272            assert_eq!(
1273                pamoja_gateway_pull_resp(
1274                    0x00AB,
1275                    request,
1276                    payload.as_ptr(),
1277                    payload.len(),
1278                    &mut packet
1279                ),
1280                PamojaStatus::Ok
1281            );
1282            let datagram = bytes_of(packet);
1283            pamoja_gateway_packet_free(packet);
1284
1285            let mut read = ptr::null_mut();
1286            assert_eq!(
1287                pamoja_gateway_packet_parse(datagram.as_ptr(), datagram.len(), &mut read),
1288                PamojaStatus::Ok
1289            );
1290            let mut back = PamojaGatewayTxpk::default();
1291            assert!(pamoja_gateway_packet_txpk(read, &mut back));
1292            assert_eq!(back, request);
1293            let len = pamoja_gateway_packet_txpk_payload_len(read);
1294            assert_eq!(
1295                std::slice::from_raw_parts(pamoja_gateway_packet_txpk_payload(read), len),
1296                payload
1297            );
1298            assert!(!pamoja_gateway_packet_acknowledgment(
1299                read,
1300                &mut ptr::null_mut()
1301            ));
1302            pamoja_gateway_packet_free(read);
1303
1304            let mut ack = ptr::null_mut();
1305            assert_eq!(
1306                pamoja_gateway_tx_ack(
1307                    0x00AB,
1308                    GATEWAY.as_ptr(),
1309                    PAMOJA_GATEWAY_TX_COLLISION_PACKET,
1310                    &mut ack
1311                ),
1312                PamojaStatus::Ok
1313            );
1314            let datagram = bytes_of(ack);
1315            pamoja_gateway_packet_free(ack);
1316
1317            let mut read = ptr::null_mut();
1318            assert_eq!(
1319                pamoja_gateway_packet_parse(datagram.as_ptr(), datagram.len(), &mut read),
1320                PamojaStatus::Ok
1321            );
1322            assert_eq!(
1323                pamoja_gateway_packet_tx_status(read),
1324                PAMOJA_GATEWAY_TX_COLLISION_PACKET
1325            );
1326            pamoja_gateway_packet_free(read);
1327        }
1328    }
1329
1330    #[test]
1331    fn the_acknowledgments_are_four_bytes() {
1332        unsafe {
1333            let push = pamoja_gateway_push_ack(0x0102);
1334            let pull = pamoja_gateway_pull_ack(0x0304);
1335            assert_eq!(bytes_of(push), [2, 0x01, 0x02, 0x01]);
1336            assert_eq!(bytes_of(pull), [2, 0x03, 0x04, 0x04]);
1337            pamoja_gateway_packet_free(push);
1338            pamoja_gateway_packet_free(pull);
1339
1340            let mut pull_data = ptr::null_mut();
1341            assert_eq!(
1342                pamoja_gateway_pull_data(0x0506, GATEWAY.as_ptr(), &mut pull_data),
1343                PamojaStatus::Ok
1344            );
1345            assert_eq!(bytes_of(pull_data).len(), 12);
1346            pamoja_gateway_packet_free(pull_data);
1347        }
1348    }
1349
1350    #[test]
1351    fn a_null_argument_or_a_bad_datagram_is_refused() {
1352        unsafe {
1353            assert_eq!(
1354                pamoja_gateway_uplink_add_rxpk(
1355                    ptr::null_mut(),
1356                    PamojaGatewayRxpk::default(),
1357                    ptr::null(),
1358                    0
1359                ),
1360                PamojaStatus::InvalidArgument
1361            );
1362            let mut packet = ptr::null_mut();
1363            assert_eq!(
1364                pamoja_gateway_pull_data(1, ptr::null(), &mut packet),
1365                PamojaStatus::InvalidArgument
1366            );
1367            assert!(packet.is_null());
1368            assert_eq!(
1369                pamoja_gateway_tx_ack(1, GATEWAY.as_ptr(), 9, &mut packet),
1370                PamojaStatus::InvalidArgument
1371            );
1372            let datagram = [1u8, 0, 1, 0];
1373            assert_eq!(
1374                pamoja_gateway_packet_parse(datagram.as_ptr(), datagram.len(), &mut packet),
1375                PamojaStatus::Codec
1376            );
1377            assert_eq!(pamoja_gateway_packet_kind(ptr::null()), u8::MAX);
1378            assert_eq!(pamoja_gateway_packet_tx_status(ptr::null()), u8::MAX);
1379            pamoja_gateway_packet_free(ptr::null_mut());
1380            pamoja_gateway_uplink_free(ptr::null_mut());
1381        }
1382    }
1383}