Skip to main content

pamoja_ffi/
gateway_network.rs

1//! The C ABI for the network side of one site.
2//!
3//! These functions wrap [`pamoja_gateway::network`] for callers that reach the SDK through
4//! the flat C boundary. A network holds the devices it admits, the sessions it has granted,
5//! and the counters it has seen, so it crosses as an opaque handle: open one on a channel
6//! plan, register the devices it should admit, hand it each packet a gateway forwarded, and
7//! release it with [`pamoja_gateway_network_free`].
8//!
9//! A forwarded packet turns out to be one of three things, so the answer crosses as a flat
10//! event with an outcome byte that says which: a device joined and its accept is ready to
11//! transmit, a session frame arrived and was decrypted into the caller buffer, or the frame
12//! belongs to a network this site never granted, which a gateway hears all the time and is
13//! not an error.
14//!
15//! Where and when to answer comes back with the event, so a downlink is built for the same
16//! window the uplink opened without the caller working out any of it.
17
18use std::panic::{catch_unwind, AssertUnwindSafe};
19use std::ptr;
20
21use pamoja_gateway::network::{Event, Network, NetworkError, Registration, Rx1Channels, Slot};
22use pamoja_gateway::udp::Rxpk;
23use pamoja_lora::region::ChannelBlock;
24use pamoja_lorawan::relay::WorChannel;
25
26use crate::gateway::{
27    link_to_c, missing, rxpk_of, txpk_to_c, PamojaGatewayRxpk, PamojaGatewayTxpk,
28};
29use crate::lora::PamojaLoraLink;
30use crate::lora_region::PamojaLoraPlan;
31use crate::lorawan_mac::{read_command, write_command, PamojaLorawanMacCommand};
32use crate::{read_bytes, set_last_error, PamojaStatus};
33
34/// A device joined, and its accept is in the event.
35pub const PAMOJA_GATEWAY_NETWORK_JOINED: u8 = 0;
36
37/// A session frame arrived, decrypted into the caller buffer.
38pub const PAMOJA_GATEWAY_NETWORK_DATA: u8 = 1;
39
40/// The frame belongs to a device this site never granted.
41pub const PAMOJA_GATEWAY_NETWORK_FOREIGN: u8 = 2;
42
43/// The first receive window answers on the frequency the uplink arrived on.
44pub const PAMOJA_GATEWAY_NETWORK_RX1_SAME: u8 = 0;
45
46/// The first receive window answers on a run of downlink channels, chosen by the uplink
47/// channel number modulo how many the run holds.
48pub const PAMOJA_GATEWAY_NETWORK_RX1_DOWNSTREAM: u8 = 1;
49
50/// The first receive window answers where the network's channel plan says: on the uplink
51/// frequency in a dynamic plan, and on the downlink channel a fixed plan numbers for the
52/// uplink channel.
53pub const PAMOJA_GATEWAY_NETWORK_RX1_PLAN: u8 = 2;
54
55/// The delay before the first receive window, in microseconds.
56pub const PAMOJA_GATEWAY_NETWORK_RECEIVE_DELAY_US: u32 = 1_000_000;
57
58/// The delay before the window a join accept is sent in, in microseconds.
59pub const PAMOJA_GATEWAY_NETWORK_JOIN_DELAY_US: u32 = 5_000_000;
60
61// The header carries these as literals, because cbindgen drops a constant whose value
62// names another crate's. These hold them to what those crates say.
63const _: () =
64    assert!(PAMOJA_GATEWAY_NETWORK_RECEIVE_DELAY_US == pamoja_gateway::network::RECEIVE_DELAY1_US);
65const _: () =
66    assert!(PAMOJA_GATEWAY_NETWORK_JOIN_DELAY_US == pamoja_gateway::network::JOIN_ACCEPT_DELAY1_US);
67
68/// The network side of one site, released with [`pamoja_gateway_network_free`].
69pub struct PamojaGatewayNetwork {
70    network: Network,
71}
72
73/// When and where a network answers, and at what rate.
74///
75/// The recommended values are the delays above, no offset between the uplink data rate and
76/// the downlink one, and a first window where the channel plan says.
77#[repr(C)]
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79pub struct PamojaGatewayNetworkWindows {
80    /// The delay before the first receive window, in microseconds.
81    pub receive_delay_us: u32,
82    /// The delay before the window a join accept is sent in, in microseconds.
83    pub join_delay_us: u32,
84    /// The offset between the uplink data rate and the rate the first window answers at.
85    pub rx1_data_rate_offset: u8,
86    /// [`PAMOJA_GATEWAY_NETWORK_RX1_PLAN`], [`PAMOJA_GATEWAY_NETWORK_RX1_SAME`] or
87    /// [`PAMOJA_GATEWAY_NETWORK_RX1_DOWNSTREAM`].
88    pub rx1_channels: u8,
89    /// The first downlink channel, in hertz, when the channels are downstream.
90    pub downstream_start_hz: u32,
91    /// The spacing between those channels, in hertz.
92    pub downstream_step_hz: u32,
93    /// How many there are.
94    pub downstream_count: u16,
95}
96
97/// Where and when a downlink answers an uplink, in the concentrator's own terms.
98#[repr(C)]
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub struct PamojaGatewayNetworkSlot {
101    /// The concentrator timestamp to transmit at, in microseconds.
102    pub timestamp_us: u32,
103    /// The frequency to transmit on, in hertz.
104    pub frequency_hz: u32,
105    /// The settings to transmit with.
106    pub link: PamojaLoraLink,
107}
108
109/// What a forwarded packet turned out to be.
110#[repr(C)]
111#[derive(Clone, Copy, Debug, Default, PartialEq)]
112pub struct PamojaGatewayNetworkEvent {
113    /// [`PAMOJA_GATEWAY_NETWORK_JOINED`], [`PAMOJA_GATEWAY_NETWORK_DATA`], or
114    /// [`PAMOJA_GATEWAY_NETWORK_FOREIGN`].
115    pub outcome: u8,
116    /// The device that joined, for a join.
117    pub dev_eui: [u8; 8],
118    /// The address granted, or the address a frame claimed.
119    pub dev_addr: u32,
120    /// The counter the frame carried, reconstructed to its full width, for data.
121    pub fcnt: u32,
122    /// The port the frame was sent on, for data on a port.
123    pub fport: u8,
124    /// Whether the frame carried a port at all.
125    pub has_fport: bool,
126    /// Whether the device asked to be acknowledged.
127    pub confirmed: bool,
128    /// How many bytes were written into the caller buffer: the decrypted payload for data,
129    /// and the accept to transmit for a join.
130    pub len: usize,
131    /// Whether the payload was longer than the buffer, in which case nothing was written.
132    pub truncated: bool,
133    /// Where an answer goes, for a join or for data.
134    pub slot: PamojaGatewayNetworkSlot,
135    /// The packet that carries the accept, for a join.
136    pub accept: PamojaGatewayTxpk,
137    /// Whether a relay forwarded this uplink, TS011-1.0.1 section 9.1. An answer to it goes
138    /// back through the same relay, which [`pamoja_gateway_network_answer`] does by itself.
139    pub relayed: bool,
140    /// The relay that forwarded it.
141    pub relay_dev_addr: u32,
142    /// What the relay heard of the uplink, in dBm.
143    pub relay_rssi_dbm: i16,
144    /// Its signal-to-noise ratio, in dB.
145    pub relay_snr_db: i8,
146    /// The data rate it arrived at.
147    pub relay_data_rate: u8,
148    /// The WOR channel the device woke the relay on: 0 for the default, 1 for the second.
149    pub relay_wor_channel: u8,
150    /// The frequency the uplink arrived on, in hertz.
151    pub relay_frequency_hz: u32,
152}
153
154/// Returns the windows a network answers in by default.
155///
156/// # Returns
157///
158/// The recommended delays, no data-rate offset, and a first window where the plan says.
159#[no_mangle]
160pub extern "C" fn pamoja_gateway_network_windows_default() -> PamojaGatewayNetworkWindows {
161    PamojaGatewayNetworkWindows {
162        receive_delay_us: PAMOJA_GATEWAY_NETWORK_RECEIVE_DELAY_US,
163        join_delay_us: PAMOJA_GATEWAY_NETWORK_JOIN_DELAY_US,
164        rx1_data_rate_offset: 0,
165        rx1_channels: PAMOJA_GATEWAY_NETWORK_RX1_PLAN,
166        downstream_start_hz: 0,
167        downstream_step_hz: 0,
168        downstream_count: 0,
169    }
170}
171
172/// Opens the network side of a site on a channel plan.
173///
174/// # Arguments
175///
176/// * `plan` - the band this site operates in, which is copied into the network.
177/// * `net_id` - the network identifier granted addresses carry; only its low 24 bits travel.
178/// * `windows` - when and where to answer, from
179///   [`pamoja_gateway_network_windows_default`].
180/// * `first_dev_addr` - the first address to grant; later joins take the ones after it.
181/// * `out_network` - receives the handle.
182///
183/// # Returns
184///
185/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null plan or output.
186///
187/// # Safety
188///
189/// `plan` must be a live handle from the region calls, and `out_network` must point at a
190/// writable pointer.
191#[no_mangle]
192pub unsafe extern "C" fn pamoja_gateway_network_open(
193    plan: *const PamojaLoraPlan,
194    net_id: u32,
195    windows: PamojaGatewayNetworkWindows,
196    first_dev_addr: u32,
197    out_network: *mut *mut PamojaGatewayNetwork,
198) -> PamojaStatus {
199    if out_network.is_null() {
200        return missing("out_network");
201    }
202    *out_network = ptr::null_mut();
203    let Some(plan) = plan.as_ref() else {
204        return missing("plan");
205    };
206
207    let network = plan.with(|plan| {
208        Network::new(plan, net_id)
209            .with_windows(windows_of(windows))
210            .with_first_dev_addr(first_dev_addr)
211    });
212    *out_network = Box::into_raw(Box::new(PamojaGatewayNetwork { network }));
213    PamojaStatus::Ok
214}
215
216/// Admits a device, so a join request signed with its key is accepted.
217///
218/// # Arguments
219///
220/// * `network` - the network.
221/// * `dev_eui` - the device identifier, eight bytes.
222/// * `app_eui` - the application identifier, eight bytes.
223/// * `app_key` - the root key, sixteen bytes.
224/// * `version` - the link layer revision the device follows,
225///   [`PAMOJA_LORAWAN_VERSION_1_0_3`](crate::lorawan_link::PAMOJA_LORAWAN_VERSION_1_0_3) or
226///   [`PAMOJA_LORAWAN_VERSION_1_0_4`](crate::lorawan_link::PAMOJA_LORAWAN_VERSION_1_0_4). A
227///   1.0.3 device's frames are refused once their counter runs `MAX_FCNT_GAP` ahead.
228///
229/// # Returns
230///
231/// [`PamojaStatus::Ok`], or [`PamojaStatus::InvalidArgument`] for a null argument or a
232/// version that is not one of the two.
233///
234/// # Safety
235///
236/// `network` must be a live handle, and each identifier must point at its own length in
237/// readable bytes.
238#[no_mangle]
239pub unsafe extern "C" fn pamoja_gateway_network_register(
240    network: *mut PamojaGatewayNetwork,
241    dev_eui: *const u8,
242    app_eui: *const u8,
243    app_key: *const u8,
244    version: u8,
245) -> PamojaStatus {
246    let Some(network) = network.as_mut() else {
247        return missing("network");
248    };
249    let (Some(dev_eui), Some(app_eui)) = (eight(dev_eui), eight(app_eui)) else {
250        return missing("dev_eui and app_eui");
251    };
252    let Some(app_key) = sixteen(app_key) else {
253        return missing("app_key");
254    };
255    let version = match crate::lorawan_link::version(version) {
256        Ok(version) => version,
257        Err(status) => return status,
258    };
259
260    network
261        .network
262        .register(Registration::new(dev_eui, app_eui, app_key).with_version(version));
263    PamojaStatus::Ok
264}
265
266/// Reads a packet the gateway forwarded.
267///
268/// A join request is verified against every registered key, granted an address and a session,
269/// and answered with an accept written into the buffer and described by `event.accept`. A
270/// data frame is routed by its address, checked against the counter last seen, and decrypted
271/// into the buffer. A frame for a device this site never granted is reported rather than
272/// refused, because a gateway hears every network in range.
273///
274/// # Arguments
275///
276/// * `network` - the network.
277/// * `packet` - the metadata of what the gateway heard.
278/// * `payload` - the frame as it came off the air.
279/// * `payload_len` - its length.
280/// * `buffer` - where to write the decrypted payload, or the accept to transmit.
281/// * `capacity` - how many bytes the buffer holds.
282/// * `out_event` - receives what the packet turned out to be.
283///
284/// # Returns
285///
286/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null argument, or
287/// [`PamojaStatus::Codec`] when the frame is refused, whose reason is available from
288/// `pamoja_last_error_message`.
289///
290/// # Safety
291///
292/// `network` must be a live handle, `payload` must point at `payload_len` readable bytes or
293/// be null, `buffer` must point at `capacity` writable bytes or be null, and `out_event` must
294/// point at a writable event.
295#[no_mangle]
296pub unsafe extern "C" fn pamoja_gateway_network_uplink(
297    network: *mut PamojaGatewayNetwork,
298    packet: PamojaGatewayRxpk,
299    payload: *const u8,
300    payload_len: usize,
301    buffer: *mut u8,
302    capacity: usize,
303    out_event: *mut PamojaGatewayNetworkEvent,
304) -> PamojaStatus {
305    if out_event.is_null() {
306        return missing("out_event");
307    }
308    *out_event = PamojaGatewayNetworkEvent::default();
309    let Some(network) = network.as_mut() else {
310        return missing("network");
311    };
312    let payload = match read_bytes(payload, payload_len) {
313        Ok(payload) => payload,
314        Err(status) => return status,
315    };
316    let heard: Rxpk = rxpk_of(packet, payload);
317
318    let read = catch_unwind(AssertUnwindSafe(|| network.network.uplink(&heard)));
319    let event = match read {
320        Ok(Ok(event)) => event,
321        Ok(Err(error)) => return refused(&error),
322        Err(_) => {
323            set_last_error("a panic was caught while reading the packet".to_owned());
324            return PamojaStatus::Panic;
325        }
326    };
327
328    *out_event = event_to_c(event, buffer, capacity);
329    PamojaStatus::Ok
330}
331
332/// Builds a downlink for a device, encrypted with its session.
333///
334/// # Arguments
335///
336/// * `network` - the network.
337/// * `dev_addr` - the device to answer.
338/// * `slot` - where and when to transmit, from the event that reported the uplink.
339/// * `fport` - the port to answer on.
340/// * `payload` - what to send.
341/// * `payload_len` - its length.
342/// * `buffer` - where to write the frame to transmit.
343/// * `capacity` - how many bytes the buffer holds.
344/// * `out_txpk` - receives the packet that carries it.
345/// * `out_len` - receives how many bytes were written.
346///
347/// # Returns
348///
349/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null argument or a buffer
350/// too small, or [`PamojaStatus::Codec`] when no session is held for the address.
351///
352/// # Safety
353///
354/// `network` must be a live handle, `payload` must point at `payload_len` readable bytes or
355/// be null, `buffer` must point at `capacity` writable bytes or be null, and both outputs
356/// must be writable.
357#[no_mangle]
358pub unsafe extern "C" fn pamoja_gateway_network_answer(
359    network: *mut PamojaGatewayNetwork,
360    dev_addr: u32,
361    slot: PamojaGatewayNetworkSlot,
362    fport: u8,
363    payload: *const u8,
364    payload_len: usize,
365    buffer: *mut u8,
366    capacity: usize,
367    out_txpk: *mut PamojaGatewayTxpk,
368    out_len: *mut usize,
369) -> PamojaStatus {
370    if out_txpk.is_null() || out_len.is_null() {
371        return missing("out_txpk and out_len");
372    }
373    *out_txpk = PamojaGatewayTxpk::default();
374    *out_len = 0;
375    let Some(network) = network.as_mut() else {
376        return missing("network");
377    };
378    let payload = match read_bytes(payload, payload_len) {
379        Ok(payload) => payload,
380        Err(status) => return status,
381    };
382
383    let answered = network
384        .network
385        .answer(dev_addr, slot_of(slot), fport, &payload);
386    let downlink = match answered {
387        Ok(downlink) => downlink,
388        Err(error) => return refused(&error),
389    };
390
391    if !write_payload(&downlink.payload, buffer, capacity) {
392        set_last_error("the frame does not fit the buffer".to_owned());
393        return PamojaStatus::InvalidArgument;
394    }
395    *out_len = downlink.payload.len();
396    *out_txpk = txpk_to_c(&downlink);
397    PamojaStatus::Ok
398}
399
400/// A device a relay heard and could not verify, TS011-1.0.1 section 10.7.
401#[repr(C)]
402#[derive(Clone, Copy, Debug, Default)]
403pub struct PamojaGatewayNetworkNotice {
404    /// The relay that heard it.
405    pub relay: u32,
406    /// The address the wake-on-radio frame named.
407    pub dev_addr: u32,
408    /// The frame's signal strength in dBm.
409    pub rssi_dbm: i16,
410    /// Its signal-to-noise ratio in dB.
411    pub snr_db: i8,
412}
413
414/// Takes the devices relays have reported hearing and could not verify.
415///
416/// A relay carries these in the MAC commands of its own uplinks, alongside whatever else that
417/// uplink was for, so they wait in the network until they are read. Every notice is taken:
418/// a second call returns only what arrived since the first.
419///
420/// # Arguments
421///
422/// * `network` - the network.
423/// * `out_notices` - where to write them, or null to count what is waiting.
424/// * `capacity` - how many notices that buffer holds.
425/// * `out_len` - receives how many are waiting, which may be more than were written.
426///
427/// # Returns
428///
429/// [`PamojaStatus::Ok`] when every notice was written, and
430/// [`PamojaStatus::InvalidArgument`] when the buffer was too small, in which case none were
431/// taken and `*out_len` says how many there are.
432///
433/// # Errors
434///
435/// Returns [`PamojaStatus::InvalidArgument`] for a null handle or out pointer.
436///
437/// # Safety
438///
439/// `network` must be a live handle, `out_notices` must point at `capacity` writable notices
440/// or be null, and `out_len` must be writable.
441#[no_mangle]
442pub unsafe extern "C" fn pamoja_gateway_network_notices(
443    network: *mut PamojaGatewayNetwork,
444    out_notices: *mut PamojaGatewayNetworkNotice,
445    capacity: usize,
446    out_len: *mut usize,
447) -> PamojaStatus {
448    if out_len.is_null() {
449        return missing("out_len");
450    }
451    *out_len = 0;
452    let Some(network) = network.as_mut() else {
453        return missing("network");
454    };
455    let waiting = network.network.notice_count();
456    *out_len = waiting;
457    if out_notices.is_null() || capacity < waiting {
458        if waiting == 0 {
459            return PamojaStatus::Ok;
460        }
461        set_last_error(format!("the buffer holds {capacity} of {waiting} notices"));
462        return PamojaStatus::InvalidArgument;
463    }
464    for (index, notice) in network.network.notices().into_iter().enumerate() {
465        *out_notices.add(index) = PamojaGatewayNetworkNotice {
466            relay: notice.relay,
467            dev_addr: notice.dev_addr,
468            rssi_dbm: notice.rssi_dbm,
469            snr_db: notice.snr_db,
470        };
471    }
472    PamojaStatus::Ok
473}
474
475/// Builds a downlink carrying MAC commands, which is how a network configures a relay.
476///
477/// The commands ride in the frame options where they fit, and in a frame of their own on
478/// port 0 where they do not.
479///
480/// # Arguments
481///
482/// * `network` - the network.
483/// * `dev_addr` - the device to configure.
484/// * `slot` - where and when to transmit, from the event that reported its uplink.
485/// * `commands` - the commands to send.
486/// * `commands_len` - how many there are.
487/// * `buffer` - where to write the frame to transmit.
488/// * `capacity` - how many bytes the buffer holds.
489/// * `out_txpk` - receives the packet that carries it.
490/// * `out_len` - receives how many bytes were written.
491///
492/// # Returns
493///
494/// [`PamojaStatus::Ok`], [`PamojaStatus::InvalidArgument`] for a null argument, a command
495/// the boundary cannot read, or a buffer too small, or [`PamojaStatus::Codec`] when no
496/// session is held for the address.
497///
498/// # Safety
499///
500/// `network` must be a live handle, `commands` must point at `commands_len` readable
501/// commands, `buffer` must point at `capacity` writable bytes or be null, and both outputs
502/// must be writable.
503#[no_mangle]
504#[allow(clippy::too_many_arguments)]
505pub unsafe extern "C" fn pamoja_gateway_network_command(
506    network: *mut PamojaGatewayNetwork,
507    dev_addr: u32,
508    slot: PamojaGatewayNetworkSlot,
509    commands: *const PamojaLorawanMacCommand,
510    commands_len: usize,
511    buffer: *mut u8,
512    capacity: usize,
513    out_txpk: *mut PamojaGatewayTxpk,
514    out_len: *mut usize,
515) -> PamojaStatus {
516    if out_txpk.is_null() || out_len.is_null() {
517        return missing("out_txpk and out_len");
518    }
519    *out_txpk = PamojaGatewayTxpk::default();
520    *out_len = 0;
521    let (Some(network), false) = (network.as_mut(), commands.is_null() && commands_len > 0) else {
522        return missing("network and commands");
523    };
524    let mut built = Vec::with_capacity(commands_len);
525    for index in 0..commands_len {
526        let Some(command) = read_command(&*commands.add(index)) else {
527            set_last_error(format!("command {index} is not one this build writes"));
528            return PamojaStatus::InvalidArgument;
529        };
530        built.push(command);
531    }
532
533    let downlink = match network.network.command(dev_addr, slot_of(slot), &built) {
534        Ok(downlink) => downlink,
535        Err(error) => return refused(&error),
536    };
537    if !write_payload(&downlink.payload, buffer, capacity) {
538        set_last_error("the frame does not fit the buffer".to_owned());
539        return PamojaStatus::InvalidArgument;
540    }
541    *out_len = downlink.payload.len();
542    *out_txpk = txpk_to_c(&downlink);
543    PamojaStatus::Ok
544}
545
546/// Builds the command that tells a relay to trust a device, with the key that lets it verify
547/// the device's wake-on-radio frames, TS011-1.0.1 section 10.4.
548///
549/// # Arguments
550///
551/// * `network` - the network.
552/// * `dev_addr` - the device the relay should forward for.
553/// * `index` - the entry in the relay's list, 0 to 15.
554/// * `reload_rate` - how many of the device's uplinks the relay forwards an hour, 63 for no
555///   limit.
556/// * `bucket_size` - the coded bucket size multiplier, table 55.
557/// * `out_command` - receives the command, to send with
558///   [`pamoja_gateway_network_command`].
559///
560/// # Returns
561///
562/// [`PamojaStatus::Ok`], or [`PamojaStatus::Codec`] when no session is held for the address.
563///
564/// # Errors
565///
566/// Returns [`PamojaStatus::InvalidArgument`] for a null handle or out pointer.
567///
568/// # Safety
569///
570/// `network` must be a live handle and `out_command` writable.
571#[no_mangle]
572pub unsafe extern "C" fn pamoja_gateway_network_trust_command(
573    network: *mut PamojaGatewayNetwork,
574    dev_addr: u32,
575    index: u8,
576    reload_rate: u8,
577    bucket_size: u8,
578    out_command: *mut PamojaLorawanMacCommand,
579) -> PamojaStatus {
580    if out_command.is_null() {
581        return missing("out_command");
582    }
583    let Some(network) = network.as_mut() else {
584        return missing("network");
585    };
586    match network
587        .network
588        .trust_command(dev_addr, index, reload_rate, bucket_size)
589    {
590        Ok(command) => {
591            *out_command = write_command(command);
592            PamojaStatus::Ok
593        }
594        Err(error) => refused(&error),
595    }
596}
597
598/// Releases a network.
599///
600/// # Arguments
601///
602/// * `network` - the network, which must not be used again.
603///
604/// # Safety
605///
606/// `network` must be a live handle from [`pamoja_gateway_network_open`], or null.
607#[no_mangle]
608pub unsafe extern "C" fn pamoja_gateway_network_free(network: *mut PamojaGatewayNetwork) {
609    if !network.is_null() {
610        drop(Box::from_raw(network));
611    }
612}
613
614/// Reads the windows from the boundary.
615fn windows_of(windows: PamojaGatewayNetworkWindows) -> pamoja_gateway::network::Windows {
616    let channels = match windows.rx1_channels {
617        PAMOJA_GATEWAY_NETWORK_RX1_DOWNSTREAM => Rx1Channels::Downstream(ChannelBlock::new(
618            windows.downstream_start_hz,
619            windows.downstream_step_hz,
620            windows.downstream_count,
621            0,
622            0,
623        )),
624        PAMOJA_GATEWAY_NETWORK_RX1_SAME => Rx1Channels::SameAsUplink,
625        _ => Rx1Channels::Plan,
626    };
627
628    pamoja_gateway::network::Windows::new()
629        .with_receive_delay_us(windows.receive_delay_us)
630        .with_join_delay_us(windows.join_delay_us)
631        .with_rx1_data_rate_offset(windows.rx1_data_rate_offset)
632        .with_rx1_channels(channels)
633}
634
635/// Reads a window from the boundary.
636fn slot_of(slot: PamojaGatewayNetworkSlot) -> Slot {
637    Slot {
638        timestamp_us: slot.timestamp_us,
639        frequency_hz: slot.frequency_hz,
640        link: crate::lora::settings(slot.link),
641    }
642}
643
644/// Writes a window to the boundary.
645fn slot_to_c(slot: Slot) -> PamojaGatewayNetworkSlot {
646    PamojaGatewayNetworkSlot {
647        timestamp_us: slot.timestamp_us,
648        frequency_hz: slot.frequency_hz,
649        link: link_to_c(slot.link),
650    }
651}
652
653/// Writes an event to the boundary, filling the caller buffer with what it carried.
654unsafe fn event_to_c(event: Event, buffer: *mut u8, capacity: usize) -> PamojaGatewayNetworkEvent {
655    match event {
656        Event::Joined {
657            dev_eui,
658            dev_addr,
659            accept,
660        } => {
661            let written = write_payload(&accept.payload, buffer, capacity);
662            PamojaGatewayNetworkEvent {
663                outcome: PAMOJA_GATEWAY_NETWORK_JOINED,
664                dev_eui,
665                dev_addr,
666                len: if written { accept.payload.len() } else { 0 },
667                truncated: !written,
668                slot: PamojaGatewayNetworkSlot {
669                    timestamp_us: accept.timestamp_us.unwrap_or(0),
670                    frequency_hz: accept.frequency_hz,
671                    link: link_to_c(
672                        accept
673                            .modulation
674                            .link()
675                            .unwrap_or(pamoja_lora::LinkSettings::new(7, 125_000)),
676                    ),
677                },
678                accept: txpk_to_c(&accept),
679                ..PamojaGatewayNetworkEvent::default()
680            }
681        }
682        Event::Data {
683            dev_addr,
684            fcnt,
685            fport,
686            payload,
687            confirmed,
688            slot,
689            relay,
690        } => {
691            let written = write_payload(&payload, buffer, capacity);
692            PamojaGatewayNetworkEvent {
693                outcome: PAMOJA_GATEWAY_NETWORK_DATA,
694                dev_addr,
695                fcnt,
696                fport: fport.unwrap_or(0),
697                has_fport: fport.is_some(),
698                confirmed,
699                len: if written { payload.len() } else { 0 },
700                truncated: !written,
701                slot: slot_to_c(slot),
702                relayed: relay.is_some(),
703                relay_dev_addr: relay.map_or(0, |relay| relay.relay),
704                relay_rssi_dbm: relay.map_or(0, |relay| relay.metadata.rssi_dbm),
705                relay_snr_db: relay.map_or(0, |relay| relay.metadata.snr_db),
706                relay_data_rate: relay.map_or(0, |relay| relay.metadata.data_rate),
707                relay_wor_channel: relay.map_or(0, |relay| match relay.metadata.wor_channel {
708                    WorChannel::Default => 0,
709                    WorChannel::Second => 1,
710                }),
711                relay_frequency_hz: relay.map_or(0, |relay| relay.frequency_hz),
712                ..PamojaGatewayNetworkEvent::default()
713            }
714        }
715        Event::Foreign { dev_addr } => PamojaGatewayNetworkEvent {
716            outcome: PAMOJA_GATEWAY_NETWORK_FOREIGN,
717            dev_addr,
718            ..PamojaGatewayNetworkEvent::default()
719        },
720    }
721}
722
723/// Copies bytes into the caller buffer, reporting whether they fit.
724unsafe fn write_payload(payload: &[u8], buffer: *mut u8, capacity: usize) -> bool {
725    if payload.is_empty() {
726        return true;
727    }
728    if buffer.is_null() || capacity < payload.len() {
729        return false;
730    }
731    ptr::copy_nonoverlapping(payload.as_ptr(), buffer, payload.len());
732    true
733}
734
735/// Reports a refused frame by name.
736fn refused(error: &NetworkError) -> PamojaStatus {
737    set_last_error(error.to_string());
738    PamojaStatus::Codec
739}
740
741/// Reads an eight-byte identifier from the boundary.
742unsafe fn eight(bytes: *const u8) -> Option<[u8; 8]> {
743    if bytes.is_null() {
744        return None;
745    }
746    let mut read = [0u8; 8];
747    ptr::copy_nonoverlapping(bytes, read.as_mut_ptr(), read.len());
748    Some(read)
749}
750
751/// Reads a sixteen-byte key from the boundary.
752unsafe fn sixteen(bytes: *const u8) -> Option<[u8; 16]> {
753    if bytes.is_null() {
754        return None;
755    }
756    let mut read = [0u8; 16];
757    ptr::copy_nonoverlapping(bytes, read.as_mut_ptr(), read.len());
758    Some(read)
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764    use crate::lora_region::{
765        pamoja_lora_plan_for_region, pamoja_lora_plan_free, PAMOJA_LORA_REGION_EU868,
766    };
767    use crate::lorawan_link::{PAMOJA_LORAWAN_VERSION_1_0_3, PAMOJA_LORAWAN_VERSION_1_0_4};
768
769    #[test]
770    fn a_device_is_registered_under_a_version_code() {
771        let (dev_eui, app_eui, app_key) = ([0x11u8; 8], [0x22u8; 8], [0x33u8; 16]);
772        unsafe {
773            let mut plan = ptr::null_mut();
774            assert_eq!(
775                pamoja_lora_plan_for_region(PAMOJA_LORA_REGION_EU868, &mut plan),
776                PamojaStatus::Ok
777            );
778            let mut network = ptr::null_mut();
779            assert_eq!(
780                pamoja_gateway_network_open(
781                    plan,
782                    0x2A,
783                    pamoja_gateway_network_windows_default(),
784                    0x2601_0001,
785                    &mut network,
786                ),
787                PamojaStatus::Ok
788            );
789            let register = |version: u8| {
790                pamoja_gateway_network_register(
791                    network,
792                    dev_eui.as_ptr(),
793                    app_eui.as_ptr(),
794                    app_key.as_ptr(),
795                    version,
796                )
797            };
798            assert_eq!(register(PAMOJA_LORAWAN_VERSION_1_0_3), PamojaStatus::Ok);
799            assert_eq!(register(PAMOJA_LORAWAN_VERSION_1_0_4), PamojaStatus::Ok);
800            assert_eq!(register(2), PamojaStatus::InvalidArgument);
801            pamoja_gateway_network_free(network);
802            pamoja_lora_plan_free(plan);
803        }
804    }
805}