Skip to main content

pamoja_ffi/
lorawan_mac.rs

1//! The C ABI for the commands a LoRaWAN network and device configure each other with.
2//!
3//! A frame carries these either in its options field, at most
4//! [`PAMOJA_LORAWAN_FOPTS_MAX`] bytes of them, or as a whole payload on port zero. A caller
5//! that has either in hand walks it here.
6//!
7//! Every command crosses as one [`PamojaLorawanMacCommand`], a flat record whose `cid` says
8//! which command it is and whose remaining fields carry whatever that command holds. The
9//! rest read as zero. A command means a different thing in each direction, so the direction
10//! goes in and there is no default.
11//!
12//! A command does not carry its own length, so a reader cannot step over one it does not
13//! know. [`pamoja_lorawan_mac_count`] reports how many were read before that happened, and
14//! the bytes after it are simply not readable.
15
16use pamoja_lorawan::mac::{MacCommand, MacCommands};
17use pamoja_lorawan::Direction;
18
19use crate::lorawan::PamojaLorawanDirection;
20use crate::{read_bytes, set_last_error, PamojaStatus};
21
22/// How many bytes of commands a frame can carry beside a payload.
23pub const PAMOJA_LORAWAN_FOPTS_MAX: usize = 15;
24
25/// The longest single command, in bytes.
26pub const PAMOJA_LORAWAN_MAC_MAX: usize = 27;
27
28// The header carries these as literals, because cbindgen drops a constant whose value
29// names another crate's. These hold them to what those crates say.
30const _: () = assert!(PAMOJA_LORAWAN_FOPTS_MAX == pamoja_lorawan::mac::FOPTS_MAX);
31const _: () = assert!(PAMOJA_LORAWAN_MAC_MAX == pamoja_lorawan::mac::MAX_COMMAND);
32
33/// One command, with the fields of whichever command it is.
34///
35/// `cid` names the command and `direction` says which way it travels; together they decide
36/// which of the other fields carry anything. The rest are zero.
37#[repr(C)]
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct PamojaLorawanMacCommand {
40    /// Which command this is.
41    pub cid: u8,
42    /// Which way it travels.
43    pub direction: PamojaLorawanDirection,
44    /// How far above the floor a link check arrived, in dB.
45    pub margin: u8,
46    /// How many gateways heard it.
47    pub gateways: u8,
48    /// The data rate a network asks a device to use.
49    pub data_rate: u8,
50    /// The transmit power it may use, as a ceiling.
51    pub tx_power: u8,
52    /// Which channels may carry an uplink.
53    pub channel_mask: u16,
54    /// Which block of sixteen channels that mask applies to.
55    pub mask_control: u8,
56    /// How many times to send an unconfirmed uplink.
57    pub transmissions: u8,
58    /// Whether the power was set, `1` for yes.
59    pub power_ack: u8,
60    /// Whether the data rate was set.
61    pub data_rate_ack: u8,
62    /// Whether the channel mask was usable.
63    pub channel_mask_ack: u8,
64    /// The share of the air a device is held to, as one over two to this.
65    pub max_duty_cycle: u8,
66    /// How far the first receive window sits below the uplink rate.
67    pub rx1_offset: u8,
68    /// The rate of the second receive window.
69    pub rx2_data_rate: u8,
70    /// A frequency in hertz, for the windows and the channel commands.
71    pub frequency_hz: u32,
72    /// Whether the window offset was in range.
73    pub rx1_offset_ack: u8,
74    /// Whether the window rate was known.
75    pub rx2_data_rate_ack: u8,
76    /// Whether the frequency was usable.
77    pub channel_ack: u8,
78    /// A device battery level: `0` on external power, `255` when it cannot tell.
79    pub battery: u8,
80    /// The signal-to-noise ratio of the last request, in dB.
81    pub snr_margin: i8,
82    /// Which channel a channel command names.
83    pub index: u8,
84    /// The fastest rate allowed on it.
85    pub max_data_rate: u8,
86    /// The slowest rate allowed on it.
87    pub min_data_rate: u8,
88    /// Whether the device can run that range of rates.
89    pub data_rate_range_ok: u8,
90    /// Whether its radio can reach that frequency.
91    pub frequency_ok: u8,
92    /// How long a device waits before its first receive window, as the command codes it.
93    pub delay: u8,
94    /// The coded transmit power ceiling a region imposes.
95    pub max_eirp: u8,
96    /// Whether an uplink is held to 400 ms of air time.
97    pub uplink_dwell: u8,
98    /// Whether a downlink is.
99    pub downlink_dwell: u8,
100    /// Whether the channel already had an uplink frequency to pair a downlink with.
101    pub uplink_frequency_exists: u8,
102    /// Seconds since the GPS epoch.
103    pub seconds: u32,
104    /// The fraction of that second, in steps of one part in 256.
105    pub fraction: u8,
106    /// Whether a relay runs.
107    pub enabled: u8,
108    /// How often a relay scans, as TS011-1.0.1 table 18 codes it.
109    pub cad_periodicity: u8,
110    /// Which of the region's relay channels is a relay's default one.
111    pub default_channel_index: u8,
112    /// Whether a relay configuration sets a second channel, 1 for yes.
113    pub second_channel_index: u8,
114    /// The second channel's data rate; its frequency is the frequency field.
115    pub second_channel_data_rate: u8,
116    /// How far above its frequency the second channel is acknowledged, as table 35 codes it.
117    pub second_channel_ack_offset: u8,
118    /// Whether the scan period was valid.
119    pub cad_periodicity_ack: u8,
120    /// Whether the default channel was valid.
121    pub default_channel_index_ack: u8,
122    /// Whether the second channel index was valid.
123    pub second_channel_index_ack: u8,
124    /// Whether the second channel's data rate was valid.
125    pub second_channel_data_rate_ack: u8,
126    /// Whether its acknowledgment offset was valid.
127    pub second_channel_ack_offset_ack: u8,
128    /// Whether its frequency was valid.
129    pub second_channel_frequency_ack: u8,
130    /// How an end device uses a relay, as TS011-1.0.1 table 40 codes it.
131    pub relay_mode: u8,
132    /// How many unanswered uplinks turn relaying on, as table 41 codes it.
133    pub smart_enable_level: u8,
134    /// How many WOR frames without an acknowledgment before an uplink goes anyway.
135    pub back_off: u8,
136    /// What a join filter rule does, or whether a trusted end device is read or removed.
137    pub action: u8,
138    /// How many leading bytes of JoinEUI and DevEUI a join filter rule matches.
139    pub eui_len: u8,
140    /// Those bytes, most significant first, with the rest zero.
141    pub eui: [u8; 16],
142    /// Whether a join filter rule was one to create, change or remove.
143    pub combined_rules_ack: u8,
144    /// Whether its length was valid.
145    pub eui_len_ack: u8,
146    /// Whether its action was valid.
147    pub action_ack: u8,
148    /// Tokens a trusted end device earns an hour, 63 for no limit.
149    pub reload_rate: u8,
150    /// Its bucket size multiplier, as TS011-1.0.1 table 55 codes it.
151    pub bucket_size: u8,
152    /// An end device address a relay command names.
153    pub dev_addr: u32,
154    /// A wake-on-radio frame counter.
155    pub wfcnt: u32,
156    /// An end device's root relay session key.
157    pub root_wor_s_key: [u8; 16],
158    /// Whether a trusted list entry was in use.
159    pub index_ack: u8,
160    /// What a forwarding limit command does to a relay's token counters, as table 63 codes it.
161    pub reset_limit_counters: u8,
162    /// Join requests a relay forwards an hour, 127 for no limit.
163    pub join_request_reload_rate: u8,
164    /// New end device notifications a relay sends an hour.
165    pub notify_reload_rate: u8,
166    /// Uplinks a relay forwards an hour across every trusted end device.
167    pub global_uplink_reload_rate: u8,
168    /// Every message a relay sends an hour.
169    pub overall_reload_rate: u8,
170    /// The join request bucket size multiplier.
171    pub join_request_bucket_size: u8,
172    /// The notification bucket size multiplier.
173    pub notify_bucket_size: u8,
174    /// The global uplink bucket size multiplier.
175    pub global_uplink_bucket_size: u8,
176    /// The overall bucket size multiplier.
177    pub overall_bucket_size: u8,
178    /// The signal strength of a WOR frame a relay could not verify, in dBm.
179    pub rssi_dbm: i16,
180    /// Its signal-to-noise ratio, in dB.
181    pub snr_db: i8,
182}
183
184impl PamojaLorawanMacCommand {
185    const fn blank(cid: u8, direction: PamojaLorawanDirection) -> PamojaLorawanMacCommand {
186        PamojaLorawanMacCommand {
187            cid,
188            direction,
189            margin: 0,
190            gateways: 0,
191            data_rate: 0,
192            tx_power: 0,
193            channel_mask: 0,
194            mask_control: 0,
195            transmissions: 0,
196            power_ack: 0,
197            data_rate_ack: 0,
198            channel_mask_ack: 0,
199            max_duty_cycle: 0,
200            rx1_offset: 0,
201            rx2_data_rate: 0,
202            frequency_hz: 0,
203            rx1_offset_ack: 0,
204            rx2_data_rate_ack: 0,
205            channel_ack: 0,
206            battery: 0,
207            snr_margin: 0,
208            index: 0,
209            max_data_rate: 0,
210            min_data_rate: 0,
211            data_rate_range_ok: 0,
212            frequency_ok: 0,
213            delay: 0,
214            max_eirp: 0,
215            uplink_dwell: 0,
216            downlink_dwell: 0,
217            uplink_frequency_exists: 0,
218            seconds: 0,
219            fraction: 0,
220            enabled: 0,
221            cad_periodicity: 0,
222            default_channel_index: 0,
223            second_channel_index: 0,
224            second_channel_data_rate: 0,
225            second_channel_ack_offset: 0,
226            cad_periodicity_ack: 0,
227            default_channel_index_ack: 0,
228            second_channel_index_ack: 0,
229            second_channel_data_rate_ack: 0,
230            second_channel_ack_offset_ack: 0,
231            second_channel_frequency_ack: 0,
232            relay_mode: 0,
233            smart_enable_level: 0,
234            back_off: 0,
235            action: 0,
236            eui_len: 0,
237            eui: [0; 16],
238            combined_rules_ack: 0,
239            eui_len_ack: 0,
240            action_ack: 0,
241            reload_rate: 0,
242            bucket_size: 0,
243            dev_addr: 0,
244            wfcnt: 0,
245            root_wor_s_key: [0; 16],
246            index_ack: 0,
247            reset_limit_counters: 0,
248            join_request_reload_rate: 0,
249            notify_reload_rate: 0,
250            global_uplink_reload_rate: 0,
251            overall_reload_rate: 0,
252            join_request_bucket_size: 0,
253            notify_bucket_size: 0,
254            global_uplink_bucket_size: 0,
255            overall_bucket_size: 0,
256            rssi_dbm: 0,
257            snr_db: 0,
258        }
259    }
260}
261
262fn crossing(direction: PamojaLorawanDirection) -> Direction {
263    match direction {
264        PamojaLorawanDirection::Uplink => Direction::Uplink,
265        PamojaLorawanDirection::Downlink => Direction::Downlink,
266    }
267}
268
269fn traveling(direction: Direction) -> PamojaLorawanDirection {
270    match direction {
271        Direction::Uplink => PamojaLorawanDirection::Uplink,
272        Direction::Downlink => PamojaLorawanDirection::Downlink,
273    }
274}
275
276// One command, flattened into the record that crosses the boundary.
277pub(crate) fn write_command(command: MacCommand) -> PamojaLorawanMacCommand {
278    let mut flat = PamojaLorawanMacCommand::blank(command.cid(), traveling(command.direction()));
279    match command {
280        MacCommand::LinkCheckReq
281        | MacCommand::DutyCycleAns
282        | MacCommand::DevStatusReq
283        | MacCommand::RxTimingSetupAns
284        | MacCommand::TxParamSetupAns
285        | MacCommand::DeviceTimeReq => {}
286        MacCommand::LinkCheckAns { margin, gateways } => {
287            flat.margin = margin;
288            flat.gateways = gateways;
289        }
290        MacCommand::LinkAdrReq {
291            data_rate,
292            tx_power,
293            channel_mask,
294            mask_control,
295            transmissions,
296        } => {
297            flat.data_rate = data_rate;
298            flat.tx_power = tx_power;
299            flat.channel_mask = channel_mask;
300            flat.mask_control = mask_control;
301            flat.transmissions = transmissions;
302        }
303        MacCommand::LinkAdrAns {
304            power_ack,
305            data_rate_ack,
306            channel_mask_ack,
307        } => {
308            flat.power_ack = u8::from(power_ack);
309            flat.data_rate_ack = u8::from(data_rate_ack);
310            flat.channel_mask_ack = u8::from(channel_mask_ack);
311        }
312        MacCommand::DutyCycleReq { max_duty_cycle } => flat.max_duty_cycle = max_duty_cycle,
313        MacCommand::RxParamSetupReq {
314            rx1_offset,
315            rx2_data_rate,
316            frequency_hz,
317        } => {
318            flat.rx1_offset = rx1_offset;
319            flat.rx2_data_rate = rx2_data_rate;
320            flat.frequency_hz = frequency_hz;
321        }
322        MacCommand::RxParamSetupAns {
323            rx1_offset_ack,
324            rx2_data_rate_ack,
325            channel_ack,
326        } => {
327            flat.rx1_offset_ack = u8::from(rx1_offset_ack);
328            flat.rx2_data_rate_ack = u8::from(rx2_data_rate_ack);
329            flat.channel_ack = u8::from(channel_ack);
330        }
331        MacCommand::DevStatusAns { battery, margin } => {
332            flat.battery = battery;
333            flat.snr_margin = margin;
334        }
335        MacCommand::NewChannelReq {
336            index,
337            frequency_hz,
338            max_data_rate,
339            min_data_rate,
340        } => {
341            flat.index = index;
342            flat.frequency_hz = frequency_hz;
343            flat.max_data_rate = max_data_rate;
344            flat.min_data_rate = min_data_rate;
345        }
346        MacCommand::NewChannelAns {
347            data_rate_range_ok,
348            frequency_ok,
349        } => {
350            flat.data_rate_range_ok = u8::from(data_rate_range_ok);
351            flat.frequency_ok = u8::from(frequency_ok);
352        }
353        MacCommand::RxTimingSetupReq { delay } => flat.delay = delay,
354        MacCommand::TxParamSetupReq {
355            max_eirp,
356            uplink_dwell,
357            downlink_dwell,
358        } => {
359            flat.max_eirp = max_eirp;
360            flat.uplink_dwell = u8::from(uplink_dwell);
361            flat.downlink_dwell = u8::from(downlink_dwell);
362        }
363        MacCommand::DlChannelReq {
364            index,
365            frequency_hz,
366        } => {
367            flat.index = index;
368            flat.frequency_hz = frequency_hz;
369        }
370        MacCommand::DlChannelAns {
371            uplink_frequency_exists,
372            frequency_ok,
373        } => {
374            flat.uplink_frequency_exists = u8::from(uplink_frequency_exists);
375            flat.frequency_ok = u8::from(frequency_ok);
376        }
377        MacCommand::DeviceTimeAns { seconds, fraction } => {
378            flat.seconds = seconds;
379            flat.fraction = fraction;
380        }
381        MacCommand::RelayConfReq {
382            enabled,
383            cad_periodicity,
384            default_channel_index,
385            second_channel_index,
386            second_channel_data_rate,
387            second_channel_ack_offset,
388            second_channel_frequency_hz,
389        } => {
390            flat.enabled = u8::from(enabled);
391            flat.cad_periodicity = cad_periodicity;
392            flat.default_channel_index = default_channel_index;
393            flat.second_channel_index = second_channel_index;
394            flat.second_channel_data_rate = second_channel_data_rate;
395            flat.second_channel_ack_offset = second_channel_ack_offset;
396            flat.frequency_hz = second_channel_frequency_hz;
397        }
398        MacCommand::RelayConfAns {
399            cad_periodicity_ack,
400            default_channel_index_ack,
401            second_channel_index_ack,
402            second_channel_data_rate_ack,
403            second_channel_ack_offset_ack,
404            second_channel_frequency_ack,
405        } => {
406            flat.cad_periodicity_ack = u8::from(cad_periodicity_ack);
407            flat.default_channel_index_ack = u8::from(default_channel_index_ack);
408            flat.second_channel_index_ack = u8::from(second_channel_index_ack);
409            flat.second_channel_data_rate_ack = u8::from(second_channel_data_rate_ack);
410            flat.second_channel_ack_offset_ack = u8::from(second_channel_ack_offset_ack);
411            flat.second_channel_frequency_ack = u8::from(second_channel_frequency_ack);
412        }
413        MacCommand::EndDeviceConfReq {
414            relay_mode,
415            smart_enable_level,
416            back_off,
417            second_channel_index,
418            second_channel_data_rate,
419            second_channel_ack_offset,
420            second_channel_frequency_hz,
421        } => {
422            flat.relay_mode = relay_mode;
423            flat.smart_enable_level = smart_enable_level;
424            flat.back_off = back_off;
425            flat.second_channel_index = second_channel_index;
426            flat.second_channel_data_rate = second_channel_data_rate;
427            flat.second_channel_ack_offset = second_channel_ack_offset;
428            flat.frequency_hz = second_channel_frequency_hz;
429        }
430        MacCommand::EndDeviceConfAns {
431            second_channel_ack_offset_ack,
432            second_channel_index_ack,
433            second_channel_data_rate_ack,
434            second_channel_frequency_ack,
435        } => {
436            flat.second_channel_ack_offset_ack = u8::from(second_channel_ack_offset_ack);
437            flat.second_channel_index_ack = u8::from(second_channel_index_ack);
438            flat.second_channel_data_rate_ack = u8::from(second_channel_data_rate_ack);
439            flat.second_channel_frequency_ack = u8::from(second_channel_frequency_ack);
440        }
441        MacCommand::FilterListReq {
442            index,
443            action,
444            eui_len,
445            eui,
446        } => {
447            flat.index = index;
448            flat.action = action;
449            flat.eui_len = eui_len;
450            flat.eui = eui;
451        }
452        MacCommand::FilterListAns {
453            combined_rules_ack,
454            eui_len_ack,
455            action_ack,
456        } => {
457            flat.combined_rules_ack = u8::from(combined_rules_ack);
458            flat.eui_len_ack = u8::from(eui_len_ack);
459            flat.action_ack = u8::from(action_ack);
460        }
461        MacCommand::UpdateUplinkListReq {
462            index,
463            reload_rate,
464            bucket_size,
465            dev_addr,
466            wfcnt,
467            root_wor_s_key,
468        } => {
469            flat.index = index;
470            flat.reload_rate = reload_rate;
471            flat.bucket_size = bucket_size;
472            flat.dev_addr = dev_addr;
473            flat.wfcnt = wfcnt;
474            flat.root_wor_s_key = root_wor_s_key;
475        }
476        MacCommand::UpdateUplinkListAns => {}
477        MacCommand::CtrlUplinkListReq { index, action } => {
478            flat.index = index;
479            flat.action = action;
480        }
481        MacCommand::CtrlUplinkListAns { index_ack, wfcnt } => {
482            flat.index_ack = u8::from(index_ack);
483            flat.wfcnt = wfcnt;
484        }
485        MacCommand::ConfigureFwdLimitReq {
486            reset_limit_counters,
487            join_request_reload_rate,
488            notify_reload_rate,
489            global_uplink_reload_rate,
490            overall_reload_rate,
491            join_request_bucket_size,
492            notify_bucket_size,
493            global_uplink_bucket_size,
494            overall_bucket_size,
495        } => {
496            flat.reset_limit_counters = reset_limit_counters;
497            flat.join_request_reload_rate = join_request_reload_rate;
498            flat.notify_reload_rate = notify_reload_rate;
499            flat.global_uplink_reload_rate = global_uplink_reload_rate;
500            flat.overall_reload_rate = overall_reload_rate;
501            flat.join_request_bucket_size = join_request_bucket_size;
502            flat.notify_bucket_size = notify_bucket_size;
503            flat.global_uplink_bucket_size = global_uplink_bucket_size;
504            flat.overall_bucket_size = overall_bucket_size;
505        }
506        MacCommand::ConfigureFwdLimitAns => {}
507        MacCommand::NotifyNewEndDeviceReq {
508            dev_addr,
509            rssi_dbm,
510            snr_db,
511        } => {
512            flat.dev_addr = dev_addr;
513            flat.rssi_dbm = rssi_dbm;
514            flat.snr_db = snr_db;
515        }
516    }
517    flat
518}
519
520// The command a record stands for, or nothing when the identifier and the direction do not
521// name one.
522pub(crate) fn read_command(flat: &PamojaLorawanMacCommand) -> Option<MacCommand> {
523    use pamoja_lorawan::mac;
524
525    let down = matches!(flat.direction, PamojaLorawanDirection::Downlink);
526    let on = |value: u8| value != 0;
527
528    let command = match (flat.cid, down) {
529        (mac::CID_LINK_CHECK, false) => MacCommand::LinkCheckReq,
530        (mac::CID_LINK_CHECK, true) => MacCommand::LinkCheckAns {
531            margin: flat.margin,
532            gateways: flat.gateways,
533        },
534        (mac::CID_LINK_ADR, true) => MacCommand::LinkAdrReq {
535            data_rate: flat.data_rate,
536            tx_power: flat.tx_power,
537            channel_mask: flat.channel_mask,
538            mask_control: flat.mask_control,
539            transmissions: flat.transmissions,
540        },
541        (mac::CID_LINK_ADR, false) => MacCommand::LinkAdrAns {
542            power_ack: on(flat.power_ack),
543            data_rate_ack: on(flat.data_rate_ack),
544            channel_mask_ack: on(flat.channel_mask_ack),
545        },
546        (mac::CID_DUTY_CYCLE, true) => MacCommand::DutyCycleReq {
547            max_duty_cycle: flat.max_duty_cycle,
548        },
549        (mac::CID_DUTY_CYCLE, false) => MacCommand::DutyCycleAns,
550        (mac::CID_RX_PARAM_SETUP, true) => MacCommand::RxParamSetupReq {
551            rx1_offset: flat.rx1_offset,
552            rx2_data_rate: flat.rx2_data_rate,
553            frequency_hz: flat.frequency_hz,
554        },
555        (mac::CID_RX_PARAM_SETUP, false) => MacCommand::RxParamSetupAns {
556            rx1_offset_ack: on(flat.rx1_offset_ack),
557            rx2_data_rate_ack: on(flat.rx2_data_rate_ack),
558            channel_ack: on(flat.channel_ack),
559        },
560        (mac::CID_DEV_STATUS, true) => MacCommand::DevStatusReq,
561        (mac::CID_DEV_STATUS, false) => MacCommand::DevStatusAns {
562            battery: flat.battery,
563            margin: flat.snr_margin,
564        },
565        (mac::CID_NEW_CHANNEL, true) => MacCommand::NewChannelReq {
566            index: flat.index,
567            frequency_hz: flat.frequency_hz,
568            max_data_rate: flat.max_data_rate,
569            min_data_rate: flat.min_data_rate,
570        },
571        (mac::CID_NEW_CHANNEL, false) => MacCommand::NewChannelAns {
572            data_rate_range_ok: on(flat.data_rate_range_ok),
573            frequency_ok: on(flat.frequency_ok),
574        },
575        (mac::CID_RX_TIMING_SETUP, true) => MacCommand::RxTimingSetupReq { delay: flat.delay },
576        (mac::CID_RX_TIMING_SETUP, false) => MacCommand::RxTimingSetupAns,
577        (mac::CID_TX_PARAM_SETUP, true) => MacCommand::TxParamSetupReq {
578            max_eirp: flat.max_eirp,
579            uplink_dwell: on(flat.uplink_dwell),
580            downlink_dwell: on(flat.downlink_dwell),
581        },
582        (mac::CID_TX_PARAM_SETUP, false) => MacCommand::TxParamSetupAns,
583        (mac::CID_DL_CHANNEL, true) => MacCommand::DlChannelReq {
584            index: flat.index,
585            frequency_hz: flat.frequency_hz,
586        },
587        (mac::CID_DL_CHANNEL, false) => MacCommand::DlChannelAns {
588            uplink_frequency_exists: on(flat.uplink_frequency_exists),
589            frequency_ok: on(flat.frequency_ok),
590        },
591        (mac::CID_DEVICE_TIME, false) => MacCommand::DeviceTimeReq,
592        (mac::CID_DEVICE_TIME, true) => MacCommand::DeviceTimeAns {
593            seconds: flat.seconds,
594            fraction: flat.fraction,
595        },
596        (mac::CID_RELAY_CONF, true) => MacCommand::RelayConfReq {
597            enabled: on(flat.enabled),
598            cad_periodicity: flat.cad_periodicity,
599            default_channel_index: flat.default_channel_index,
600            second_channel_index: flat.second_channel_index,
601            second_channel_data_rate: flat.second_channel_data_rate,
602            second_channel_ack_offset: flat.second_channel_ack_offset,
603            second_channel_frequency_hz: flat.frequency_hz,
604        },
605        (mac::CID_RELAY_CONF, false) => MacCommand::RelayConfAns {
606            cad_periodicity_ack: on(flat.cad_periodicity_ack),
607            default_channel_index_ack: on(flat.default_channel_index_ack),
608            second_channel_index_ack: on(flat.second_channel_index_ack),
609            second_channel_data_rate_ack: on(flat.second_channel_data_rate_ack),
610            second_channel_ack_offset_ack: on(flat.second_channel_ack_offset_ack),
611            second_channel_frequency_ack: on(flat.second_channel_frequency_ack),
612        },
613        (mac::CID_END_DEVICE_CONF, true) => MacCommand::EndDeviceConfReq {
614            relay_mode: flat.relay_mode,
615            smart_enable_level: flat.smart_enable_level,
616            back_off: flat.back_off,
617            second_channel_index: flat.second_channel_index,
618            second_channel_data_rate: flat.second_channel_data_rate,
619            second_channel_ack_offset: flat.second_channel_ack_offset,
620            second_channel_frequency_hz: flat.frequency_hz,
621        },
622        (mac::CID_END_DEVICE_CONF, false) => MacCommand::EndDeviceConfAns {
623            second_channel_ack_offset_ack: on(flat.second_channel_ack_offset_ack),
624            second_channel_index_ack: on(flat.second_channel_index_ack),
625            second_channel_data_rate_ack: on(flat.second_channel_data_rate_ack),
626            second_channel_frequency_ack: on(flat.second_channel_frequency_ack),
627        },
628        (mac::CID_FILTER_LIST, true) => MacCommand::FilterListReq {
629            index: flat.index,
630            action: flat.action,
631            eui_len: flat.eui_len,
632            eui: flat.eui,
633        },
634        (mac::CID_FILTER_LIST, false) => MacCommand::FilterListAns {
635            combined_rules_ack: on(flat.combined_rules_ack),
636            eui_len_ack: on(flat.eui_len_ack),
637            action_ack: on(flat.action_ack),
638        },
639        (mac::CID_UPDATE_UPLINK_LIST, true) => MacCommand::UpdateUplinkListReq {
640            index: flat.index,
641            reload_rate: flat.reload_rate,
642            bucket_size: flat.bucket_size,
643            dev_addr: flat.dev_addr,
644            wfcnt: flat.wfcnt,
645            root_wor_s_key: flat.root_wor_s_key,
646        },
647        (mac::CID_UPDATE_UPLINK_LIST, false) => MacCommand::UpdateUplinkListAns,
648        (mac::CID_CTRL_UPLINK_LIST, true) => MacCommand::CtrlUplinkListReq {
649            index: flat.index,
650            action: flat.action,
651        },
652        (mac::CID_CTRL_UPLINK_LIST, false) => MacCommand::CtrlUplinkListAns {
653            index_ack: on(flat.index_ack),
654            wfcnt: flat.wfcnt,
655        },
656        (mac::CID_CONFIGURE_FWD_LIMIT, true) => MacCommand::ConfigureFwdLimitReq {
657            reset_limit_counters: flat.reset_limit_counters,
658            join_request_reload_rate: flat.join_request_reload_rate,
659            notify_reload_rate: flat.notify_reload_rate,
660            global_uplink_reload_rate: flat.global_uplink_reload_rate,
661            overall_reload_rate: flat.overall_reload_rate,
662            join_request_bucket_size: flat.join_request_bucket_size,
663            notify_bucket_size: flat.notify_bucket_size,
664            global_uplink_bucket_size: flat.global_uplink_bucket_size,
665            overall_bucket_size: flat.overall_bucket_size,
666        },
667        (mac::CID_CONFIGURE_FWD_LIMIT, false) => MacCommand::ConfigureFwdLimitAns,
668        (mac::CID_NOTIFY_NEW_END_DEVICE, false) => MacCommand::NotifyNewEndDeviceReq {
669            dev_addr: flat.dev_addr,
670            rssi_dbm: flat.rssi_dbm,
671            snr_db: flat.snr_db,
672        },
673        _ => return None,
674    };
675    Some(command)
676}
677
678/// Counts the commands packed into a field.
679///
680/// # Arguments
681///
682/// * `direction` - which way the frame carrying them travels.
683/// * `bytes` - the options field, or a payload sent on port zero.
684/// * `len` - how many bytes that is.
685/// * `out_count` - where to put the count.
686///
687/// # Returns
688///
689/// [`PamojaStatus::Ok`], with the count written. Counting stops at an identifier this build
690/// does not know, because nothing says how long it is, so the count is what was readable.
691///
692/// # Safety
693///
694/// `bytes` must point to `len` readable bytes or be null when `len` is zero, and `out_count`
695/// must be writable.
696#[no_mangle]
697pub unsafe extern "C" fn pamoja_lorawan_mac_count(
698    direction: PamojaLorawanDirection,
699    bytes: *const u8,
700    len: usize,
701    out_count: *mut usize,
702) -> PamojaStatus {
703    if out_count.is_null() {
704        set_last_error("the count must not be null".to_owned());
705        return PamojaStatus::InvalidArgument;
706    }
707    let field = match read_bytes(bytes, len) {
708        Ok(field) => field,
709        Err(status) => return status,
710    };
711    let read = MacCommands::new(crossing(direction), &field)
712        .take_while(Result::is_ok)
713        .count();
714    *out_count = read;
715    PamojaStatus::Ok
716}
717
718/// Reads one of the commands packed into a field.
719///
720/// # Arguments
721///
722/// * `direction` - which way the frame carrying them travels.
723/// * `bytes` - the options field, or a payload sent on port zero.
724/// * `len` - how many bytes that is.
725/// * `index` - which command, counting from zero.
726/// * `out_command` - where to put it.
727///
728/// # Returns
729///
730/// [`PamojaStatus::Ok`] with the command written, or
731/// [`PamojaStatus::InvalidArgument`] when there is no command at that position, which
732/// includes one cut short and one this build does not know.
733///
734/// # Safety
735///
736/// `bytes` must point to `len` readable bytes or be null when `len` is zero, and
737/// `out_command` must be writable.
738#[no_mangle]
739pub unsafe extern "C" fn pamoja_lorawan_mac_at(
740    direction: PamojaLorawanDirection,
741    bytes: *const u8,
742    len: usize,
743    index: usize,
744    out_command: *mut PamojaLorawanMacCommand,
745) -> PamojaStatus {
746    if out_command.is_null() {
747        set_last_error("the command must not be null".to_owned());
748        return PamojaStatus::InvalidArgument;
749    }
750    let field = match read_bytes(bytes, len) {
751        Ok(field) => field,
752        Err(status) => return status,
753    };
754    let found = MacCommands::new(crossing(direction), &field)
755        .take_while(Result::is_ok)
756        .nth(index)
757        .and_then(Result::ok);
758    match found {
759        Some(command) => {
760            *out_command = write_command(command);
761            PamojaStatus::Ok
762        }
763        None => {
764            set_last_error(format!("there is no readable command at position {index}"));
765            PamojaStatus::InvalidArgument
766        }
767    }
768}
769
770/// Writes one command out.
771///
772/// # Arguments
773///
774/// * `command` - the command to write.
775/// * `out` - where to write it.
776/// * `capacity` - how much room that is, at least [`PAMOJA_LORAWAN_MAC_MAX`] for any
777///   command.
778/// * `out_written` - where to put how many bytes were written.
779///
780/// # Returns
781///
782/// [`PamojaStatus::Ok`] with the bytes written, or [`PamojaStatus::InvalidArgument`] when
783/// the identifier and direction name no command, a field will not fit what it is carried in,
784/// or there is not enough room.
785///
786/// # Safety
787///
788/// `command` must be readable, `out` must point to `capacity` writable bytes, and
789/// `out_written` must be writable.
790#[no_mangle]
791pub unsafe extern "C" fn pamoja_lorawan_mac_encode(
792    command: *const PamojaLorawanMacCommand,
793    out: *mut u8,
794    capacity: usize,
795    out_written: *mut usize,
796) -> PamojaStatus {
797    if command.is_null() || out.is_null() || out_written.is_null() {
798        set_last_error("the command, the buffer, and the count must not be null".to_owned());
799        return PamojaStatus::InvalidArgument;
800    }
801    let Some(command) = read_command(&*command) else {
802        set_last_error(format!(
803            "identifier {:#04x} names no command in that direction",
804            (*command).cid
805        ));
806        return PamojaStatus::InvalidArgument;
807    };
808
809    let mut buffer = [0u8; PAMOJA_LORAWAN_MAC_MAX];
810    let written = match command.encode(&mut buffer) {
811        Ok(written) => written,
812        Err(error) => {
813            set_last_error(error.to_string());
814            return PamojaStatus::InvalidArgument;
815        }
816    };
817    if written > capacity {
818        set_last_error(format!(
819            "the command takes {written} bytes and there is room for {capacity}"
820        ));
821        return PamojaStatus::InvalidArgument;
822    }
823
824    std::ptr::copy_nonoverlapping(buffer.as_ptr(), out, written);
825    *out_written = written;
826    PamojaStatus::Ok
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    fn at(
834        direction: PamojaLorawanDirection,
835        bytes: &[u8],
836        index: usize,
837    ) -> PamojaLorawanMacCommand {
838        let mut command = PamojaLorawanMacCommand::blank(0, PamojaLorawanDirection::Uplink);
839        let status = unsafe {
840            pamoja_lorawan_mac_at(direction, bytes.as_ptr(), bytes.len(), index, &mut command)
841        };
842        assert_eq!(status, PamojaStatus::Ok);
843        command
844    }
845
846    #[test]
847    fn the_same_bytes_read_as_different_commands_in_each_direction() {
848        let bytes = [0x03, 0x52, 0xff, 0x00, 0x01];
849
850        let down = at(PamojaLorawanDirection::Downlink, &bytes, 0);
851        assert_eq!(down.data_rate, 5);
852        assert_eq!(down.tx_power, 2);
853        assert_eq!(down.channel_mask, 0x00ff);
854
855        let up = at(PamojaLorawanDirection::Uplink, &bytes, 0);
856        assert_eq!(up.cid, down.cid, "the same identifier");
857        assert_eq!(up.data_rate_ack, 1, "read as a status byte instead");
858        assert_eq!(up.data_rate, 0, "and the request fields carry nothing");
859    }
860
861    #[test]
862    fn a_field_of_commands_is_counted_and_read_one_by_one() {
863        let bytes = [0x02, 0x0d];
864        let mut count = 0usize;
865        let status = unsafe {
866            pamoja_lorawan_mac_count(
867                PamojaLorawanDirection::Uplink,
868                bytes.as_ptr(),
869                bytes.len(),
870                &mut count,
871            )
872        };
873
874        assert_eq!(status, PamojaStatus::Ok);
875        assert_eq!(count, 2);
876        assert_eq!(at(PamojaLorawanDirection::Uplink, &bytes, 0).cid, 0x02);
877        assert_eq!(at(PamojaLorawanDirection::Uplink, &bytes, 1).cid, 0x0d);
878    }
879
880    #[test]
881    fn counting_stops_where_reading_does() {
882        // Nothing says how long an unknown command is, so neither the count nor a read goes
883        // past it.
884        let bytes = [0x02, 0x7f, 0x11];
885        let mut count = 0usize;
886        unsafe {
887            pamoja_lorawan_mac_count(
888                PamojaLorawanDirection::Uplink,
889                bytes.as_ptr(),
890                bytes.len(),
891                &mut count,
892            );
893        }
894        assert_eq!(count, 1);
895
896        let mut command = PamojaLorawanMacCommand::blank(0, PamojaLorawanDirection::Uplink);
897        let status = unsafe {
898            pamoja_lorawan_mac_at(
899                PamojaLorawanDirection::Uplink,
900                bytes.as_ptr(),
901                bytes.len(),
902                1,
903                &mut command,
904            )
905        };
906        assert_eq!(status, PamojaStatus::InvalidArgument);
907    }
908
909    #[test]
910    fn a_command_written_out_reads_back_as_itself() {
911        let mut command = PamojaLorawanMacCommand::blank(0x07, PamojaLorawanDirection::Downlink);
912        command.index = 3;
913        command.frequency_hz = 867_100_000;
914        command.max_data_rate = 5;
915        command.min_data_rate = 0;
916
917        let mut out = [0u8; PAMOJA_LORAWAN_MAC_MAX];
918        let mut written = 0usize;
919        let status = unsafe {
920            pamoja_lorawan_mac_encode(&command, out.as_mut_ptr(), out.len(), &mut written)
921        };
922
923        assert_eq!(status, PamojaStatus::Ok);
924        assert_eq!(&out[..written], &[0x07, 0x03, 0x18, 0x4f, 0x84, 0x50]);
925        assert_eq!(
926            at(PamojaLorawanDirection::Downlink, &out[..written], 0),
927            command
928        );
929    }
930
931    #[test]
932    fn a_relay_command_carries_its_key_and_eui_prefix_across() {
933        let mut command = PamojaLorawanMacCommand::blank(0x43, PamojaLorawanDirection::Downlink);
934        command.index = 3;
935        command.reload_rate = 10;
936        command.bucket_size = 2;
937        command.dev_addr = 0x2601_1bda;
938        command.wfcnt = 7;
939        command.root_wor_s_key = core::array::from_fn(|at| at as u8);
940
941        let mut out = [0u8; PAMOJA_LORAWAN_MAC_MAX];
942        let mut written = 0usize;
943        let status = unsafe {
944            pamoja_lorawan_mac_encode(&command, out.as_mut_ptr(), out.len(), &mut written)
945        };
946        assert_eq!(status, PamojaStatus::Ok);
947        assert_eq!(written, PAMOJA_LORAWAN_MAC_MAX);
948        assert_eq!(
949            at(PamojaLorawanDirection::Downlink, &out[..written], 0),
950            command
951        );
952
953        let mut filter = PamojaLorawanMacCommand::blank(0x42, PamojaLorawanDirection::Downlink);
954        filter.index = 1;
955        filter.action = 1;
956        filter.eui_len = 3;
957        filter.eui[..3].copy_from_slice(&[0xab, 0xcd, 0xef]);
958        let status = unsafe {
959            pamoja_lorawan_mac_encode(&filter, out.as_mut_ptr(), out.len(), &mut written)
960        };
961        assert_eq!(status, PamojaStatus::Ok);
962        assert_eq!(&out[..written], &[0x42, 0xa3, 0x00, 0xef, 0xcd, 0xab]);
963        assert_eq!(
964            at(PamojaLorawanDirection::Downlink, &out[..written], 0),
965            filter
966        );
967
968        let mut notice = PamojaLorawanMacCommand::blank(0x46, PamojaLorawanDirection::Uplink);
969        notice.dev_addr = 0x2601_1bda;
970        notice.rssi_dbm = -100;
971        notice.snr_db = 5;
972        let status = unsafe {
973            pamoja_lorawan_mac_encode(&notice, out.as_mut_ptr(), out.len(), &mut written)
974        };
975        assert_eq!(status, PamojaStatus::Ok);
976        assert_eq!(&out[..written], &[0x46, 0xda, 0x1b, 0x01, 0x26, 0xb9, 0x0a]);
977    }
978
979    #[test]
980    fn an_identifier_that_names_no_command_is_refused() {
981        let command = PamojaLorawanMacCommand::blank(0x7f, PamojaLorawanDirection::Downlink);
982        let mut out = [0u8; PAMOJA_LORAWAN_MAC_MAX];
983        let mut written = 0usize;
984        let status = unsafe {
985            pamoja_lorawan_mac_encode(&command, out.as_mut_ptr(), out.len(), &mut written)
986        };
987
988        assert_eq!(status, PamojaStatus::InvalidArgument);
989    }
990
991    #[test]
992    fn a_buffer_too_small_for_the_command_is_refused() {
993        let mut command = PamojaLorawanMacCommand::blank(0x02, PamojaLorawanDirection::Downlink);
994        command.margin = 20;
995        command.gateways = 3;
996
997        let mut out = [0u8; 2];
998        let mut written = 0usize;
999        let status = unsafe {
1000            pamoja_lorawan_mac_encode(&command, out.as_mut_ptr(), out.len(), &mut written)
1001        };
1002
1003        assert_eq!(status, PamojaStatus::InvalidArgument);
1004        assert_eq!(written, 0, "and nothing was written");
1005    }
1006}