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}