Skip to main content

pamoja_lorawan/
mac.rs

1//! The commands a network and a device configure each other with.
2//!
3//! A frame carries a payload for the application, and it can carry commands for the device
4//! itself: change to this data rate, listen on this frequency, tell me how your battery is.
5//! They ride either in the frame options, unencrypted and at most [`FOPTS_MAX`] bytes of
6//! them, or as a whole payload on port zero, which a session encrypts with the network key.
7//! A frame that carries them in both places is one a device is told to ignore.
8//!
9//! Two things here are easy to get wrong, and both are quiet when you do:
10//!
11//! - A command identifier means different things in each direction. `0x03` going down is a
12//!   request to change rate, and the same byte coming up is the answer to one. Nothing in
13//!   the bytes says which, so [`MacCommand::parse`] is told the direction and there is no
14//!   default.
15//! - A command does not carry its own length. A reader is expected to know it, which means
16//!   an identifier it does not know cannot be stepped over: everything after it is
17//!   unreadable. [`MacCommands`] stops there rather than guessing, and leaves the rest in
18//!   [`remaining`](MacCommands::remaining) so a caller can see what was not read.
19//!
20//! Every layout here is from the LoRaWAN 1.0.3 specification, section 5, and the relay
21//! commands from TS011-1.0.1 section 10. Multi-byte fields go out low byte first as the rest
22//! of the protocol does.
23
24use pamoja_lora::region::RelayChannel;
25
26use crate::{Direction, LorawanError};
27
28/// How many bytes of commands a frame can carry beside a payload.
29///
30/// Past this they have to go in a payload of their own, on port zero.
31pub const FOPTS_MAX: usize = 15;
32
33/// The longest single command, which is the one that adds an end device to a relay's
34/// trusted list.
35pub const MAX_COMMAND: usize = 27;
36
37/// The longest join filter a relay rule carries: a JoinEUI and a DevEUI.
38pub const FILTER_EUI_MAX: usize = 16;
39
40/// Checks the link, and asks how well it was heard.
41pub const CID_LINK_CHECK: u8 = 0x02;
42
43/// Sets the data rate, the power, and which channels may be used.
44pub const CID_LINK_ADR: u8 = 0x03;
45
46/// Holds a device to a share of the air.
47pub const CID_DUTY_CYCLE: u8 = 0x04;
48
49/// Sets where and how fast the second receive window listens.
50pub const CID_RX_PARAM_SETUP: u8 = 0x05;
51
52/// Asks a device for its battery and how well it is hearing.
53pub const CID_DEV_STATUS: u8 = 0x06;
54
55/// Creates or changes a channel.
56pub const CID_NEW_CHANNEL: u8 = 0x07;
57
58/// Sets how long after sending a device starts listening.
59pub const CID_RX_TIMING_SETUP: u8 = 0x08;
60
61/// Sets the power and the dwell time a region asks for.
62pub const CID_TX_PARAM_SETUP: u8 = 0x09;
63
64/// Moves the downlink frequency of a channel away from its uplink one.
65pub const CID_DL_CHANNEL: u8 = 0x0a;
66
67/// Asks the network what time it is.
68pub const CID_DEVICE_TIME: u8 = 0x0d;
69
70/// Configures a relay's wake-on-radio channels, TS011-1.0.1 section 10.1.
71pub const CID_RELAY_CONF: u8 = 0x40;
72
73/// Configures how an end device uses a relay, TS011-1.0.1 section 10.2.
74pub const CID_END_DEVICE_CONF: u8 = 0x41;
75
76/// Sets a rule of a relay's join request filter, TS011-1.0.1 section 10.3.
77pub const CID_FILTER_LIST: u8 = 0x42;
78
79/// Adds an end device to a relay's trusted list, TS011-1.0.1 section 10.4.
80pub const CID_UPDATE_UPLINK_LIST: u8 = 0x43;
81
82/// Reads or removes an end device of a relay's trusted list, TS011-1.0.1 section 10.5.
83pub const CID_CTRL_UPLINK_LIST: u8 = 0x44;
84
85/// Sets a relay's forwarding limits, TS011-1.0.1 section 10.6.
86pub const CID_CONFIGURE_FWD_LIMIT: u8 = 0x45;
87
88/// A relay telling the network of an end device it could not verify, TS011-1.0.1
89/// section 10.7.
90pub const CID_NOTIFY_NEW_END_DEVICE: u8 = 0x46;
91
92/// The transmit powers the dwell time command can name, in dBm, by their coded value.
93///
94/// The steps are uneven because they are chosen to land on the ceilings different regions
95/// impose rather than to divide a range evenly.
96pub const EIRP_DBM: [u8; 16] = [
97    8, 10, 12, 13, 14, 16, 18, 20, 21, 24, 26, 27, 29, 30, 33, 36,
98];
99
100/// What a coded transmit power means in dBm.
101///
102/// # Arguments
103///
104/// * `code` - the coded value, 0 through 15.
105///
106/// # Returns
107///
108/// The power, or `None` for a value outside the table.
109///
110/// # Examples
111///
112/// ```
113/// use pamoja_lorawan::mac::eirp_dbm;
114///
115/// assert_eq!(eirp_dbm(5), Some(16));
116/// assert_eq!(eirp_dbm(15), Some(36));
117/// assert_eq!(eirp_dbm(16), None);
118/// ```
119#[must_use]
120pub const fn eirp_dbm(code: u8) -> Option<u8> {
121    if (code as usize) < EIRP_DBM.len() {
122        Some(EIRP_DBM[code as usize])
123    } else {
124        None
125    }
126}
127
128/// The code for a transmit power, when the table has one for it exactly.
129///
130/// # Arguments
131///
132/// * `dbm` - the power.
133///
134/// # Returns
135///
136/// The coded value, or `None` when the table holds no such power. It is not rounded,
137/// because rounding up would put a device over a limit a region set.
138///
139/// # Examples
140///
141/// ```
142/// use pamoja_lorawan::mac::eirp_code;
143///
144/// assert_eq!(eirp_code(16), Some(5));
145/// assert_eq!(eirp_code(17), None, "not a power the command can name");
146/// ```
147#[must_use]
148pub const fn eirp_code(dbm: u8) -> Option<u8> {
149    let mut at = 0;
150    while at < EIRP_DBM.len() {
151        if EIRP_DBM[at] == dbm {
152            return Some(at as u8);
153        }
154        at += 1;
155    }
156    None
157}
158
159/// How long a device waits before its first receive window, in seconds.
160///
161/// # Arguments
162///
163/// * `coded` - the value the command carries, 0 through 15.
164///
165/// # Returns
166///
167/// The delay. Zero and one both mean one second, which is the one place the coding is not
168/// the number itself.
169///
170/// # Examples
171///
172/// ```
173/// use pamoja_lorawan::mac::receive_delay_s;
174///
175/// assert_eq!(receive_delay_s(0), 1, "zero is one second, not none");
176/// assert_eq!(receive_delay_s(1), 1);
177/// assert_eq!(receive_delay_s(5), 5);
178/// ```
179#[must_use]
180pub const fn receive_delay_s(coded: u8) -> u8 {
181    if coded == 0 {
182        1
183    } else {
184        coded
185    }
186}
187
188/// One command, in either direction.
189///
190/// The name says which way it travels: a request goes to a device, an answer comes back.
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192pub enum MacCommand {
193    /// A device asking whether the network can hear it.
194    LinkCheckReq,
195    /// How well it was heard, and by how many gateways.
196    LinkCheckAns {
197        /// How far above the floor the last check arrived, in dB. 255 is reserved.
198        margin: u8,
199        /// How many gateways heard it.
200        gateways: u8,
201    },
202    /// Change rate, power, and which channels may be used.
203    LinkAdrReq {
204        /// The rate, which each region codes for itself.
205        data_rate: u8,
206        /// The power, as a ceiling rather than an instruction.
207        tx_power: u8,
208        /// Which channels may carry an uplink, lowest channel in the lowest bit.
209        channel_mask: u16,
210        /// Which block of sixteen channels the mask applies to.
211        mask_control: u8,
212        /// How many times to send an unconfirmed uplink. Zero means the device keeps one.
213        transmissions: u8,
214    },
215    /// Which parts of the request a device took.
216    LinkAdrAns {
217        /// Whether the power was set.
218        power_ack: bool,
219        /// Whether the rate was set.
220        data_rate_ack: bool,
221        /// Whether the channel mask was usable.
222        channel_mask_ack: bool,
223    },
224    /// Hold the device to a share of the air.
225    DutyCycleReq {
226        /// The share is one over two to this, so 0 is no limit beyond the regional one.
227        max_duty_cycle: u8,
228    },
229    /// The device took it.
230    DutyCycleAns,
231    /// Set where and how fast the second receive window listens.
232    RxParamSetupReq {
233        /// How far the first window's rate sits below the uplink rate.
234        rx1_offset: u8,
235        /// The rate of the second window.
236        rx2_data_rate: u8,
237        /// The frequency of the second window, in hertz.
238        frequency_hz: u32,
239    },
240    /// Which parts of that the device took.
241    RxParamSetupAns {
242        /// Whether the offset was in range.
243        rx1_offset_ack: bool,
244        /// Whether the rate was known.
245        rx2_data_rate_ack: bool,
246        /// Whether the frequency was usable.
247        channel_ack: bool,
248    },
249    /// Ask a device how it is.
250    DevStatusReq,
251    /// How it is.
252    DevStatusAns {
253        /// 0 on external power, 1 through 254 from empty to full, 255 when it cannot tell.
254        battery: u8,
255        /// The signal-to-noise ratio of the last request, in dB, from -32 to 31.
256        margin: i8,
257    },
258    /// Create a channel, or change one.
259    NewChannelReq {
260        /// Which channel. The ones a region fixes cannot be changed.
261        index: u8,
262        /// The center frequency in hertz. Zero switches the channel off.
263        frequency_hz: u32,
264        /// The fastest rate allowed on it.
265        max_data_rate: u8,
266        /// The slowest rate allowed on it.
267        min_data_rate: u8,
268    },
269    /// Whether the channel could be made.
270    NewChannelAns {
271        /// Whether the device can run that range of rates.
272        data_rate_range_ok: bool,
273        /// Whether its radio can reach that frequency.
274        frequency_ok: bool,
275    },
276    /// Set how long after sending the device starts listening.
277    RxTimingSetupReq {
278        /// The delay in seconds as the command codes it, which
279        /// [`receive_delay_s`] reads.
280        delay: u8,
281    },
282    /// The device took it.
283    RxTimingSetupAns,
284    /// Set the power and dwell time a region asks for.
285    TxParamSetupReq {
286        /// The coded power, which [`eirp_dbm`] reads.
287        max_eirp: u8,
288        /// Whether an uplink is held to 400 ms of air time.
289        uplink_dwell: bool,
290        /// Whether a downlink is.
291        downlink_dwell: bool,
292    },
293    /// The device took it.
294    TxParamSetupAns,
295    /// Move a channel's downlink frequency away from its uplink one.
296    DlChannelReq {
297        /// Which channel.
298        index: u8,
299        /// The downlink frequency in hertz.
300        frequency_hz: u32,
301    },
302    /// Whether that could be done.
303    DlChannelAns {
304        /// Whether the channel already had an uplink frequency to pair with.
305        uplink_frequency_exists: bool,
306        /// Whether the radio can reach the frequency.
307        frequency_ok: bool,
308    },
309    /// A device asking the network what time it is.
310    DeviceTimeReq,
311    /// The time, taken as the uplink that asked finished arriving.
312    DeviceTimeAns {
313        /// Seconds since the GPS epoch, which leap seconds separate from UTC.
314        seconds: u32,
315        /// The fraction of that second, in steps of one part in 256.
316        fraction: u8,
317    },
318    /// Start or stop a relay, and set the channels it scans.
319    RelayConfReq {
320        /// Whether the relay runs. With it stopped, every other field is ignored.
321        enabled: bool,
322        /// How often the relay scans its default channel, as TS011-1.0.1 table 18 codes it.
323        cad_periodicity: u8,
324        /// Which of the region's relay channels is the default one, 0 or 1.
325        default_channel_index: u8,
326        /// Whether a second channel is set, as table 34 codes it: 0 for none, 1 for the one
327        /// the next fields describe.
328        second_channel_index: u8,
329        /// The second channel's data rate.
330        second_channel_data_rate: u8,
331        /// How far above its frequency the second channel is acknowledged, as table 35
332        /// codes it.
333        second_channel_ack_offset: u8,
334        /// The second channel's frequency in hertz.
335        second_channel_frequency_hz: u32,
336    },
337    /// Which parts of the relay configuration were valid.
338    RelayConfAns {
339        /// Whether the scan period was.
340        cad_periodicity_ack: bool,
341        /// Whether the default channel was.
342        default_channel_index_ack: bool,
343        /// Whether the second channel index was.
344        second_channel_index_ack: bool,
345        /// Whether the second channel's data rate was.
346        second_channel_data_rate_ack: bool,
347        /// Whether its acknowledgment offset was.
348        second_channel_ack_offset_ack: bool,
349        /// Whether its frequency was.
350        second_channel_frequency_ack: bool,
351    },
352    /// Set how an end device uses a relay.
353    EndDeviceConfReq {
354        /// Whether relaying is off (0), on (1), turned on after uplinks go unanswered (2), or
355        /// left to the device (3), TS011-1.0.1 table 40.
356        relay_mode: u8,
357        /// How many unanswered uplinks turn relaying on in mode 2, as table 41 codes it.
358        smart_enable_level: u8,
359        /// How many wake-on-radio frames without an acknowledgment before the uplink goes
360        /// anyway, 0 meaning every time, table 43.
361        back_off: u8,
362        /// Whether a second channel is set, 0 for none and 1 for the one described.
363        second_channel_index: u8,
364        /// The second channel's data rate.
365        second_channel_data_rate: u8,
366        /// How far above its frequency the second channel is acknowledged, table 35.
367        second_channel_ack_offset: u8,
368        /// The second channel's frequency in hertz.
369        second_channel_frequency_hz: u32,
370    },
371    /// Which parts of the end device configuration were valid, TS011-1.0.1 table 45.
372    EndDeviceConfAns {
373        /// Whether the second channel's acknowledgment offset was.
374        second_channel_ack_offset_ack: bool,
375        /// Whether the second channel index was.
376        second_channel_index_ack: bool,
377        /// Whether its data rate was.
378        second_channel_data_rate_ack: bool,
379        /// Whether its frequency was.
380        second_channel_frequency_ack: bool,
381    },
382    /// Set one rule of a relay's join request filter.
383    FilterListReq {
384        /// The rule, 0 being the action when no other rule matches.
385        index: u8,
386        /// No rule (0), forward (1) or filter (2), TS011-1.0.1 table 48.
387        action: u8,
388        /// How many leading bytes of JoinEUI and DevEUI the rule matches. A length past
389        /// [`FILTER_EUI_MAX`] is kept, so a relay can refuse it, but not its bytes past the
390        /// sixteenth.
391        eui_len: u8,
392        /// The leading bytes of JoinEUI then DevEUI, most significant first, as an EUI is
393        /// written.
394        eui: [u8; FILTER_EUI_MAX],
395    },
396    /// Which parts of the filter rule were valid.
397    FilterListAns {
398        /// Whether the fields together made a rule to create, change or remove.
399        combined_rules_ack: bool,
400        /// Whether the length was.
401        eui_len_ack: bool,
402        /// Whether the action was.
403        action_ack: bool,
404    },
405    /// Trust an end device, so a relay verifies its wake-on-radio frames and forwards it.
406    UpdateUplinkListReq {
407        /// The entry of the trusted list, 0 to 15.
408        index: u8,
409        /// Tokens earned an hour, 63 meaning no limit, TS011-1.0.1 table 54.
410        reload_rate: u8,
411        /// The bucket size multiplier, as table 55 codes it.
412        bucket_size: u8,
413        /// The end device's address.
414        dev_addr: u32,
415        /// The next wake-on-radio frame counter the network expects from it.
416        wfcnt: u32,
417        /// The root relay session key the device's wake-on-radio keys come from.
418        root_wor_s_key: [u8; 16],
419    },
420    /// The relay took it.
421    UpdateUplinkListAns,
422    /// Read the counter of a trusted end device, or remove it.
423    CtrlUplinkListReq {
424        /// The entry of the trusted list.
425        index: u8,
426        /// Read (0) or remove (1), TS011-1.0.1 table 58.
427        action: u8,
428    },
429    /// The entry's last valid counter.
430    CtrlUplinkListAns {
431        /// Whether the entry was in use.
432        index_ack: bool,
433        /// The last wake-on-radio frame counter the relay accepted from it.
434        wfcnt: u32,
435    },
436    /// Set a relay's forwarding limits, TS011-1.0.1 section 10.6.
437    ConfigureFwdLimitReq {
438        /// What happens to the token counters, as table 63 codes it.
439        reset_limit_counters: u8,
440        /// Join requests forwarded an hour, 127 meaning no limit.
441        join_request_reload_rate: u8,
442        /// New end device notifications an hour.
443        notify_reload_rate: u8,
444        /// Uplinks forwarded an hour across every trusted end device.
445        global_uplink_reload_rate: u8,
446        /// Every message the relay sends an hour.
447        overall_reload_rate: u8,
448        /// The join request bucket size multiplier, table 55.
449        join_request_bucket_size: u8,
450        /// The notification bucket size multiplier.
451        notify_bucket_size: u8,
452        /// The global uplink bucket size multiplier.
453        global_uplink_bucket_size: u8,
454        /// The overall bucket size multiplier.
455        overall_bucket_size: u8,
456    },
457    /// The relay took it.
458    ConfigureFwdLimitAns,
459    /// A relay heard a wake-on-radio frame it could not verify.
460    NotifyNewEndDeviceReq {
461        /// The address the frame named.
462        dev_addr: u32,
463        /// The frame's signal strength in dBm, carried from -142 to -15.
464        rssi_dbm: i16,
465        /// Its signal-to-noise ratio in dB, carried from -20 to 11.
466        snr_db: i8,
467    },
468}
469
470/// The frequency offsets a relay's second channel can be acknowledged at, TS011-1.0.1
471/// table 35, by their coded value.
472pub const RELAY_ACK_OFFSET_HZ: [u32; 6] = [0, 200_000, 400_000, 800_000, 1_600_000, 3_200_000];
473
474/// What a coded bucket size multiplies a reload rate by, TS011-1.0.1 table 55.
475pub const RELAY_BUCKET_MULTIPLIER: [u16; 4] = [1, 2, 4, 12];
476
477/// The second channel a relay configuration describes, where it names one.
478///
479/// # Arguments
480///
481/// * `second_channel_index` - the coded index, 1 for a second channel.
482/// * `data_rate` - its data rate.
483/// * `ack_offset` - the coded acknowledgment offset.
484/// * `frequency_hz` - its frequency.
485///
486/// # Returns
487///
488/// The channel, with the acknowledgment frequency worked out, or `None` when the index or
489/// offset is not one TS011-1.0.1 defines.
490///
491/// # Examples
492///
493/// ```
494/// use pamoja_lora::region::RelayChannel;
495/// use pamoja_lorawan::mac::relay_second_channel;
496///
497/// assert_eq!(
498///     relay_second_channel(1, 3, 1, 868_100_000),
499///     Some(RelayChannel::new(868_100_000, 868_300_000, 3)),
500///     "offset 1 is 200 kHz"
501/// );
502/// assert_eq!(relay_second_channel(0, 3, 1, 868_100_000), None, "no second channel");
503/// assert_eq!(relay_second_channel(1, 3, 6, 868_100_000), None, "offset 6 is reserved");
504/// ```
505#[must_use]
506pub fn relay_second_channel(
507    second_channel_index: u8,
508    data_rate: u8,
509    ack_offset: u8,
510    frequency_hz: u32,
511) -> Option<RelayChannel> {
512    if second_channel_index != 1 {
513        return None;
514    }
515    let offset = *RELAY_ACK_OFFSET_HZ.get(usize::from(ack_offset))?;
516    Some(RelayChannel::new(
517        frequency_hz,
518        frequency_hz.checked_add(offset)?,
519        data_rate,
520    ))
521}
522
523impl MacCommand {
524    /// The identifier this command goes out under.
525    ///
526    /// # Returns
527    ///
528    /// The byte, which is shared with the command answering it in the other direction.
529    ///
530    /// # Examples
531    ///
532    /// ```
533    /// use pamoja_lorawan::mac::{MacCommand, CID_LINK_ADR};
534    ///
535    /// let answer = MacCommand::LinkAdrAns {
536    ///     power_ack: true,
537    ///     data_rate_ack: true,
538    ///     channel_mask_ack: true,
539    /// };
540    /// assert_eq!(answer.cid(), CID_LINK_ADR, "the same byte as the request");
541    /// ```
542    #[must_use]
543    pub const fn cid(&self) -> u8 {
544        match self {
545            MacCommand::LinkCheckReq | MacCommand::LinkCheckAns { .. } => CID_LINK_CHECK,
546            MacCommand::LinkAdrReq { .. } | MacCommand::LinkAdrAns { .. } => CID_LINK_ADR,
547            MacCommand::DutyCycleReq { .. } | MacCommand::DutyCycleAns => CID_DUTY_CYCLE,
548            MacCommand::RxParamSetupReq { .. } | MacCommand::RxParamSetupAns { .. } => {
549                CID_RX_PARAM_SETUP
550            }
551            MacCommand::DevStatusReq | MacCommand::DevStatusAns { .. } => CID_DEV_STATUS,
552            MacCommand::NewChannelReq { .. } | MacCommand::NewChannelAns { .. } => CID_NEW_CHANNEL,
553            MacCommand::RxTimingSetupReq { .. } | MacCommand::RxTimingSetupAns => {
554                CID_RX_TIMING_SETUP
555            }
556            MacCommand::TxParamSetupReq { .. } | MacCommand::TxParamSetupAns => CID_TX_PARAM_SETUP,
557            MacCommand::DlChannelReq { .. } | MacCommand::DlChannelAns { .. } => CID_DL_CHANNEL,
558            MacCommand::DeviceTimeReq | MacCommand::DeviceTimeAns { .. } => CID_DEVICE_TIME,
559            MacCommand::RelayConfReq { .. } | MacCommand::RelayConfAns { .. } => CID_RELAY_CONF,
560            MacCommand::EndDeviceConfReq { .. } | MacCommand::EndDeviceConfAns { .. } => {
561                CID_END_DEVICE_CONF
562            }
563            MacCommand::FilterListReq { .. } | MacCommand::FilterListAns { .. } => CID_FILTER_LIST,
564            MacCommand::UpdateUplinkListReq { .. } | MacCommand::UpdateUplinkListAns => {
565                CID_UPDATE_UPLINK_LIST
566            }
567            MacCommand::CtrlUplinkListReq { .. } | MacCommand::CtrlUplinkListAns { .. } => {
568                CID_CTRL_UPLINK_LIST
569            }
570            MacCommand::ConfigureFwdLimitReq { .. } | MacCommand::ConfigureFwdLimitAns => {
571                CID_CONFIGURE_FWD_LIMIT
572            }
573            MacCommand::NotifyNewEndDeviceReq { .. } => CID_NOTIFY_NEW_END_DEVICE,
574        }
575    }
576
577    /// Which way this command travels.
578    ///
579    /// # Returns
580    ///
581    /// [`Direction::Downlink`] for what a network sends, [`Direction::Uplink`] for what a
582    /// device answers with.
583    ///
584    /// # Examples
585    ///
586    /// ```
587    /// use pamoja_lorawan::{Direction, mac::MacCommand};
588    ///
589    /// assert_eq!(MacCommand::DevStatusReq.direction(), Direction::Downlink);
590    /// assert_eq!(MacCommand::LinkCheckReq.direction(), Direction::Uplink);
591    /// ```
592    #[must_use]
593    pub const fn direction(&self) -> Direction {
594        match self {
595            MacCommand::LinkCheckReq
596            | MacCommand::LinkAdrAns { .. }
597            | MacCommand::DutyCycleAns
598            | MacCommand::RxParamSetupAns { .. }
599            | MacCommand::DevStatusAns { .. }
600            | MacCommand::NewChannelAns { .. }
601            | MacCommand::RxTimingSetupAns
602            | MacCommand::TxParamSetupAns
603            | MacCommand::DlChannelAns { .. }
604            | MacCommand::DeviceTimeReq
605            | MacCommand::RelayConfAns { .. }
606            | MacCommand::EndDeviceConfAns { .. }
607            | MacCommand::FilterListAns { .. }
608            | MacCommand::UpdateUplinkListAns
609            | MacCommand::CtrlUplinkListAns { .. }
610            | MacCommand::ConfigureFwdLimitAns
611            | MacCommand::NotifyNewEndDeviceReq { .. } => Direction::Uplink,
612            _ => Direction::Downlink,
613        }
614    }
615
616    /// How many bytes this command takes, its identifier included.
617    ///
618    /// # Returns
619    ///
620    /// The length, which a reader has to know because the bytes do not carry it.
621    ///
622    /// # Examples
623    ///
624    /// ```
625    /// use pamoja_lorawan::mac::MacCommand;
626    ///
627    /// assert_eq!(MacCommand::LinkCheckReq.len(), 1, "the identifier and nothing else");
628    /// assert_eq!(MacCommand::DevStatusAns { battery: 200, margin: -3 }.len(), 3);
629    /// ```
630    #[must_use]
631    pub const fn len(&self) -> usize {
632        1 + match self {
633            MacCommand::LinkCheckReq
634            | MacCommand::DutyCycleAns
635            | MacCommand::DevStatusReq
636            | MacCommand::RxTimingSetupAns
637            | MacCommand::TxParamSetupAns
638            | MacCommand::DeviceTimeReq
639            | MacCommand::UpdateUplinkListAns
640            | MacCommand::ConfigureFwdLimitAns => 0,
641            MacCommand::LinkAdrAns { .. }
642            | MacCommand::RelayConfAns { .. }
643            | MacCommand::EndDeviceConfAns { .. }
644            | MacCommand::FilterListAns { .. }
645            | MacCommand::CtrlUplinkListReq { .. }
646            | MacCommand::DutyCycleReq { .. }
647            | MacCommand::RxParamSetupAns { .. }
648            | MacCommand::NewChannelAns { .. }
649            | MacCommand::RxTimingSetupReq { .. }
650            | MacCommand::TxParamSetupReq { .. }
651            | MacCommand::DlChannelAns { .. } => 1,
652            MacCommand::LinkCheckAns { .. } | MacCommand::DevStatusAns { .. } => 2,
653            MacCommand::LinkAdrReq { .. }
654            | MacCommand::RxParamSetupReq { .. }
655            | MacCommand::DlChannelReq { .. } => 4,
656            MacCommand::NewChannelReq { .. }
657            | MacCommand::DeviceTimeAns { .. }
658            | MacCommand::RelayConfReq { .. }
659            | MacCommand::CtrlUplinkListAns { .. }
660            | MacCommand::ConfigureFwdLimitReq { .. } => 5,
661            MacCommand::EndDeviceConfReq { .. } | MacCommand::NotifyNewEndDeviceReq { .. } => 6,
662            MacCommand::FilterListReq { eui_len, .. } => 2 + *eui_len as usize,
663            MacCommand::UpdateUplinkListReq { .. } => 26,
664        }
665    }
666
667    /// Whether this command is only its identifier.
668    ///
669    /// # Returns
670    ///
671    /// Whether it carries no fields.
672    #[must_use]
673    pub const fn is_empty(&self) -> bool {
674        self.len() == 1
675    }
676
677    /// Writes this command out.
678    ///
679    /// # Arguments
680    ///
681    /// * `out` - where to write it.
682    ///
683    /// # Returns
684    ///
685    /// How many bytes were written.
686    ///
687    /// # Errors
688    ///
689    /// [`LorawanError::PayloadTooLong`] when `out` is shorter than the command, and
690    /// [`LorawanError::MalformedFrame`] for a frequency the field cannot hold, which is one
691    /// above 1.67 GHz or one that is not a whole number of hundreds of hertz, and for a join
692    /// filter longer than [`FILTER_EUI_MAX`] bytes.
693    ///
694    /// # Examples
695    ///
696    /// ```
697    /// use pamoja_lorawan::mac::MacCommand;
698    /// use pamoja_lorawan::Direction;
699    ///
700    /// // The network's answer to a link check: heard 20 dB above the floor by three gateways.
701    /// let mut out = [0u8; 8];
702    /// let command = MacCommand::LinkCheckAns { margin: 20, gateways: 3 };
703    /// let written = command.encode(&mut out).unwrap();
704    ///
705    /// // The device reads it back from the downlink's frame options.
706    /// let (heard, taken) = MacCommand::parse(Direction::Downlink, &out[..written]).unwrap();
707    /// assert_eq!((heard, taken), (command, written));
708    /// ```
709    pub fn encode(&self, out: &mut [u8]) -> Result<usize, LorawanError> {
710        if let MacCommand::FilterListReq { eui_len, .. } = *self {
711            if usize::from(eui_len) > FILTER_EUI_MAX {
712                return Err(LorawanError::MalformedFrame);
713            }
714        }
715        let len = self.len();
716        if out.len() < len {
717            return Err(LorawanError::PayloadTooLong);
718        }
719        out[0] = self.cid();
720
721        match *self {
722            MacCommand::LinkCheckReq
723            | MacCommand::DutyCycleAns
724            | MacCommand::DevStatusReq
725            | MacCommand::RxTimingSetupAns
726            | MacCommand::TxParamSetupAns
727            | MacCommand::DeviceTimeReq
728            | MacCommand::UpdateUplinkListAns
729            | MacCommand::ConfigureFwdLimitAns => {}
730            MacCommand::LinkCheckAns { margin, gateways } => {
731                out[1] = margin;
732                out[2] = gateways;
733            }
734            MacCommand::RelayConfReq {
735                enabled,
736                cad_periodicity,
737                default_channel_index,
738                second_channel_index,
739                second_channel_data_rate,
740                second_channel_ack_offset,
741                second_channel_frequency_hz,
742            } => {
743                let settings = (u16::from(enabled) << 13)
744                    | (u16::from(cad_periodicity & 0x07) << 10)
745                    | (u16::from(default_channel_index & 0x01) << 9)
746                    | second_channel_bits(
747                        second_channel_index,
748                        second_channel_data_rate,
749                        second_channel_ack_offset,
750                    );
751                out[1..3].copy_from_slice(&settings.to_le_bytes());
752                write_frequency(second_channel_frequency_hz, &mut out[3..6])?;
753            }
754            MacCommand::RelayConfAns {
755                cad_periodicity_ack,
756                default_channel_index_ack,
757                second_channel_index_ack,
758                second_channel_data_rate_ack,
759                second_channel_ack_offset_ack,
760                second_channel_frequency_ack,
761            } => {
762                out[1] = (u8::from(cad_periodicity_ack) << 5)
763                    | (u8::from(default_channel_index_ack) << 4)
764                    | (u8::from(second_channel_index_ack) << 3)
765                    | (u8::from(second_channel_data_rate_ack) << 2)
766                    | (u8::from(second_channel_ack_offset_ack) << 1)
767                    | u8::from(second_channel_frequency_ack);
768            }
769            MacCommand::EndDeviceConfReq {
770                relay_mode,
771                smart_enable_level,
772                back_off,
773                second_channel_index,
774                second_channel_data_rate,
775                second_channel_ack_offset,
776                second_channel_frequency_hz,
777            } => {
778                out[1] = ((relay_mode & 0x03) << 2) | (smart_enable_level & 0x03);
779                let settings = (u16::from(back_off & 0x3f) << 9)
780                    | second_channel_bits(
781                        second_channel_index,
782                        second_channel_data_rate,
783                        second_channel_ack_offset,
784                    );
785                out[2..4].copy_from_slice(&settings.to_le_bytes());
786                write_frequency(second_channel_frequency_hz, &mut out[4..7])?;
787            }
788            MacCommand::EndDeviceConfAns {
789                second_channel_ack_offset_ack,
790                second_channel_index_ack,
791                second_channel_data_rate_ack,
792                second_channel_frequency_ack,
793            } => {
794                out[1] = (u8::from(second_channel_ack_offset_ack) << 3)
795                    | (u8::from(second_channel_index_ack) << 2)
796                    | (u8::from(second_channel_data_rate_ack) << 1)
797                    | u8::from(second_channel_frequency_ack);
798            }
799            MacCommand::FilterListReq {
800                index,
801                action,
802                eui_len,
803                eui,
804            } => {
805                let param = (u16::from(index & 0x0f) << 7)
806                    | (u16::from(action & 0x03) << 5)
807                    | u16::from(eui_len & 0x1f);
808                out[1..3].copy_from_slice(&param.to_le_bytes());
809                let len = usize::from(eui_len);
810                for (at, byte) in eui[..len].iter().rev().enumerate() {
811                    out[3 + at] = *byte;
812                }
813            }
814            MacCommand::FilterListAns {
815                combined_rules_ack,
816                eui_len_ack,
817                action_ack,
818            } => {
819                out[1] = (u8::from(combined_rules_ack) << 2)
820                    | (u8::from(eui_len_ack) << 1)
821                    | u8::from(action_ack);
822            }
823            MacCommand::UpdateUplinkListReq {
824                index,
825                reload_rate,
826                bucket_size,
827                dev_addr,
828                wfcnt,
829                root_wor_s_key,
830            } => {
831                out[1] = index & 0x0f;
832                out[2] = ((bucket_size & 0x03) << 6) | (reload_rate & 0x3f);
833                out[3..7].copy_from_slice(&dev_addr.to_le_bytes());
834                out[7..11].copy_from_slice(&wfcnt.to_le_bytes());
835                out[11..27].copy_from_slice(&root_wor_s_key);
836            }
837            MacCommand::CtrlUplinkListReq { index, action } => {
838                out[1] = ((action & 0x01) << 4) | (index & 0x0f);
839            }
840            MacCommand::CtrlUplinkListAns { index_ack, wfcnt } => {
841                out[1] = u8::from(index_ack);
842                out[2..6].copy_from_slice(&wfcnt.to_le_bytes());
843            }
844            MacCommand::ConfigureFwdLimitReq {
845                reset_limit_counters,
846                join_request_reload_rate,
847                notify_reload_rate,
848                global_uplink_reload_rate,
849                overall_reload_rate,
850                join_request_bucket_size,
851                notify_bucket_size,
852                global_uplink_bucket_size,
853                overall_bucket_size,
854            } => {
855                let rates = (u32::from(reset_limit_counters & 0x03) << 28)
856                    | (u32::from(join_request_reload_rate & 0x7f) << 21)
857                    | (u32::from(notify_reload_rate & 0x7f) << 14)
858                    | (u32::from(global_uplink_reload_rate & 0x7f) << 7)
859                    | u32::from(overall_reload_rate & 0x7f);
860                out[1..5].copy_from_slice(&rates.to_le_bytes());
861                out[5] = ((join_request_bucket_size & 0x03) << 6)
862                    | ((notify_bucket_size & 0x03) << 4)
863                    | ((global_uplink_bucket_size & 0x03) << 2)
864                    | (overall_bucket_size & 0x03);
865            }
866            MacCommand::NotifyNewEndDeviceReq {
867                dev_addr,
868                rssi_dbm,
869                snr_db,
870            } => {
871                out[1..5].copy_from_slice(&dev_addr.to_le_bytes());
872                let power = (relay_rssi_code(rssi_dbm) << 5) | relay_snr_code(snr_db);
873                out[5..7].copy_from_slice(&power.to_le_bytes());
874            }
875            MacCommand::LinkAdrReq {
876                data_rate,
877                tx_power,
878                channel_mask,
879                mask_control,
880                transmissions,
881            } => {
882                out[1] = (data_rate << 4) | (tx_power & 0x0f);
883                out[2..4].copy_from_slice(&channel_mask.to_le_bytes());
884                out[4] = ((mask_control & 0x07) << 4) | (transmissions & 0x0f);
885            }
886            MacCommand::LinkAdrAns {
887                power_ack,
888                data_rate_ack,
889                channel_mask_ack,
890            } => {
891                out[1] = (u8::from(power_ack) << 2)
892                    | (u8::from(data_rate_ack) << 1)
893                    | u8::from(channel_mask_ack);
894            }
895            MacCommand::DutyCycleReq { max_duty_cycle } => out[1] = max_duty_cycle & 0x0f,
896            MacCommand::RxParamSetupReq {
897                rx1_offset,
898                rx2_data_rate,
899                frequency_hz,
900            } => {
901                out[1] = ((rx1_offset & 0x07) << 4) | (rx2_data_rate & 0x0f);
902                write_frequency(frequency_hz, &mut out[2..5])?;
903            }
904            MacCommand::RxParamSetupAns {
905                rx1_offset_ack,
906                rx2_data_rate_ack,
907                channel_ack,
908            } => {
909                out[1] = (u8::from(rx1_offset_ack) << 2)
910                    | (u8::from(rx2_data_rate_ack) << 1)
911                    | u8::from(channel_ack);
912            }
913            MacCommand::DevStatusAns { battery, margin } => {
914                out[1] = battery;
915                out[2] = (margin as u8) & 0x3f;
916            }
917            MacCommand::NewChannelReq {
918                index,
919                frequency_hz,
920                max_data_rate,
921                min_data_rate,
922            } => {
923                out[1] = index;
924                write_frequency(frequency_hz, &mut out[2..5])?;
925                out[5] = (max_data_rate << 4) | (min_data_rate & 0x0f);
926            }
927            MacCommand::NewChannelAns {
928                data_rate_range_ok,
929                frequency_ok,
930            } => {
931                out[1] = (u8::from(data_rate_range_ok) << 1) | u8::from(frequency_ok);
932            }
933            MacCommand::RxTimingSetupReq { delay } => out[1] = delay & 0x0f,
934            MacCommand::TxParamSetupReq {
935                max_eirp,
936                uplink_dwell,
937                downlink_dwell,
938            } => {
939                out[1] = (u8::from(downlink_dwell) << 5)
940                    | (u8::from(uplink_dwell) << 4)
941                    | (max_eirp & 0x0f);
942            }
943            MacCommand::DlChannelReq {
944                index,
945                frequency_hz,
946            } => {
947                out[1] = index;
948                write_frequency(frequency_hz, &mut out[2..5])?;
949            }
950            MacCommand::DlChannelAns {
951                uplink_frequency_exists,
952                frequency_ok,
953            } => {
954                out[1] = (u8::from(uplink_frequency_exists) << 1) | u8::from(frequency_ok);
955            }
956            MacCommand::DeviceTimeAns { seconds, fraction } => {
957                out[1..5].copy_from_slice(&seconds.to_le_bytes());
958                out[5] = fraction;
959            }
960        }
961
962        Ok(len)
963    }
964
965    /// Reads one command.
966    ///
967    /// # Arguments
968    ///
969    /// * `direction` - which way the frame carrying it travels. The same identifier is a
970    ///   different command each way, and nothing in the bytes says which, so this is not
971    ///   guessed.
972    /// * `bytes` - the command and whatever follows it.
973    ///
974    /// # Returns
975    ///
976    /// The command and how many bytes it took.
977    ///
978    /// # Errors
979    ///
980    /// [`LorawanError::FrameTooShort`] when the bytes stop inside the command, and
981    /// [`LorawanError::UnknownCommand`] carrying the identifier when it is one this
982    /// version does not know, which a caller cannot step over.
983    ///
984    /// # Examples
985    ///
986    /// ```
987    /// use pamoja_lorawan::{Direction, mac::MacCommand};
988    ///
989    /// // A request and its answer share an identifier, so the direction decides which one
990    /// // the same bytes are.
991    /// let mut bytes = [0u8; 2];
992    /// let request = MacCommand::DutyCycleReq { max_duty_cycle: 10 };
993    /// request.encode(&mut bytes).unwrap();
994    ///
995    /// let (down, _) = MacCommand::parse(Direction::Downlink, &bytes).unwrap();
996    /// assert_eq!(down, request);
997    ///
998    /// let (up, taken) = MacCommand::parse(Direction::Uplink, &bytes).unwrap();
999    /// assert_eq!(up, MacCommand::DutyCycleAns);
1000    /// assert_eq!(taken, 1, "the answer has no payload, so the second byte is not its own");
1001    /// ```
1002    pub fn parse(direction: Direction, bytes: &[u8]) -> Result<(MacCommand, usize), LorawanError> {
1003        let Some(&cid) = bytes.first() else {
1004            return Err(LorawanError::FrameTooShort);
1005        };
1006        let rest = &bytes[1..];
1007        let down = matches!(direction, Direction::Downlink);
1008
1009        let command = match (cid, down) {
1010            (CID_LINK_CHECK, false) => MacCommand::LinkCheckReq,
1011            (CID_LINK_CHECK, true) => MacCommand::LinkCheckAns {
1012                margin: byte(rest, 0)?,
1013                gateways: byte(rest, 1)?,
1014            },
1015            (CID_LINK_ADR, true) => {
1016                let rate_power = byte(rest, 0)?;
1017                let redundancy = byte(rest, 3)?;
1018                MacCommand::LinkAdrReq {
1019                    data_rate: rate_power >> 4,
1020                    tx_power: rate_power & 0x0f,
1021                    channel_mask: u16::from_le_bytes([byte(rest, 1)?, byte(rest, 2)?]),
1022                    mask_control: (redundancy >> 4) & 0x07,
1023                    transmissions: redundancy & 0x0f,
1024                }
1025            }
1026            (CID_LINK_ADR, false) => {
1027                let status = byte(rest, 0)?;
1028                MacCommand::LinkAdrAns {
1029                    power_ack: status & 0x04 != 0,
1030                    data_rate_ack: status & 0x02 != 0,
1031                    channel_mask_ack: status & 0x01 != 0,
1032                }
1033            }
1034            (CID_DUTY_CYCLE, true) => MacCommand::DutyCycleReq {
1035                max_duty_cycle: byte(rest, 0)? & 0x0f,
1036            },
1037            (CID_DUTY_CYCLE, false) => MacCommand::DutyCycleAns,
1038            (CID_RX_PARAM_SETUP, true) => {
1039                let settings = byte(rest, 0)?;
1040                MacCommand::RxParamSetupReq {
1041                    rx1_offset: (settings >> 4) & 0x07,
1042                    rx2_data_rate: settings & 0x0f,
1043                    frequency_hz: read_frequency(rest, 1)?,
1044                }
1045            }
1046            (CID_RX_PARAM_SETUP, false) => {
1047                let status = byte(rest, 0)?;
1048                MacCommand::RxParamSetupAns {
1049                    rx1_offset_ack: status & 0x04 != 0,
1050                    rx2_data_rate_ack: status & 0x02 != 0,
1051                    channel_ack: status & 0x01 != 0,
1052                }
1053            }
1054            (CID_DEV_STATUS, true) => MacCommand::DevStatusReq,
1055            (CID_DEV_STATUS, false) => MacCommand::DevStatusAns {
1056                battery: byte(rest, 0)?,
1057                margin: sign_extend_6(byte(rest, 1)?),
1058            },
1059            (CID_NEW_CHANNEL, true) => {
1060                let range = byte(rest, 4)?;
1061                MacCommand::NewChannelReq {
1062                    index: byte(rest, 0)?,
1063                    frequency_hz: read_frequency(rest, 1)?,
1064                    max_data_rate: range >> 4,
1065                    min_data_rate: range & 0x0f,
1066                }
1067            }
1068            (CID_NEW_CHANNEL, false) => {
1069                let status = byte(rest, 0)?;
1070                MacCommand::NewChannelAns {
1071                    data_rate_range_ok: status & 0x02 != 0,
1072                    frequency_ok: status & 0x01 != 0,
1073                }
1074            }
1075            (CID_RX_TIMING_SETUP, true) => MacCommand::RxTimingSetupReq {
1076                delay: byte(rest, 0)? & 0x0f,
1077            },
1078            (CID_RX_TIMING_SETUP, false) => MacCommand::RxTimingSetupAns,
1079            (CID_TX_PARAM_SETUP, true) => {
1080                let field = byte(rest, 0)?;
1081                MacCommand::TxParamSetupReq {
1082                    max_eirp: field & 0x0f,
1083                    uplink_dwell: field & 0x10 != 0,
1084                    downlink_dwell: field & 0x20 != 0,
1085                }
1086            }
1087            (CID_TX_PARAM_SETUP, false) => MacCommand::TxParamSetupAns,
1088            (CID_DL_CHANNEL, true) => MacCommand::DlChannelReq {
1089                index: byte(rest, 0)?,
1090                frequency_hz: read_frequency(rest, 1)?,
1091            },
1092            (CID_DL_CHANNEL, false) => {
1093                let status = byte(rest, 0)?;
1094                MacCommand::DlChannelAns {
1095                    uplink_frequency_exists: status & 0x02 != 0,
1096                    frequency_ok: status & 0x01 != 0,
1097                }
1098            }
1099            (CID_DEVICE_TIME, false) => MacCommand::DeviceTimeReq,
1100            (CID_DEVICE_TIME, true) => MacCommand::DeviceTimeAns {
1101                seconds: u32::from_le_bytes([
1102                    byte(rest, 0)?,
1103                    byte(rest, 1)?,
1104                    byte(rest, 2)?,
1105                    byte(rest, 3)?,
1106                ]),
1107                fraction: byte(rest, 4)?,
1108            },
1109            (CID_RELAY_CONF, true) => {
1110                let settings = u16::from_le_bytes([byte(rest, 0)?, byte(rest, 1)?]);
1111                MacCommand::RelayConfReq {
1112                    enabled: settings & (1 << 13) != 0,
1113                    cad_periodicity: ((settings >> 10) & 0x07) as u8,
1114                    default_channel_index: ((settings >> 9) & 0x01) as u8,
1115                    second_channel_index: ((settings >> 7) & 0x03) as u8,
1116                    second_channel_data_rate: ((settings >> 3) & 0x0f) as u8,
1117                    second_channel_ack_offset: (settings & 0x07) as u8,
1118                    second_channel_frequency_hz: read_frequency(rest, 2)?,
1119                }
1120            }
1121            (CID_RELAY_CONF, false) => {
1122                let status = byte(rest, 0)?;
1123                MacCommand::RelayConfAns {
1124                    cad_periodicity_ack: status & 0x20 != 0,
1125                    default_channel_index_ack: status & 0x10 != 0,
1126                    second_channel_index_ack: status & 0x08 != 0,
1127                    second_channel_data_rate_ack: status & 0x04 != 0,
1128                    second_channel_ack_offset_ack: status & 0x02 != 0,
1129                    second_channel_frequency_ack: status & 0x01 != 0,
1130                }
1131            }
1132            (CID_END_DEVICE_CONF, true) => {
1133                let mode = byte(rest, 0)?;
1134                let settings = u16::from_le_bytes([byte(rest, 1)?, byte(rest, 2)?]);
1135                MacCommand::EndDeviceConfReq {
1136                    relay_mode: (mode >> 2) & 0x03,
1137                    smart_enable_level: mode & 0x03,
1138                    back_off: ((settings >> 9) & 0x3f) as u8,
1139                    second_channel_index: ((settings >> 7) & 0x03) as u8,
1140                    second_channel_data_rate: ((settings >> 3) & 0x0f) as u8,
1141                    second_channel_ack_offset: (settings & 0x07) as u8,
1142                    second_channel_frequency_hz: read_frequency(rest, 3)?,
1143                }
1144            }
1145            (CID_END_DEVICE_CONF, false) => {
1146                let status = byte(rest, 0)?;
1147                MacCommand::EndDeviceConfAns {
1148                    second_channel_ack_offset_ack: status & 0x08 != 0,
1149                    second_channel_index_ack: status & 0x04 != 0,
1150                    second_channel_data_rate_ack: status & 0x02 != 0,
1151                    second_channel_frequency_ack: status & 0x01 != 0,
1152                }
1153            }
1154            (CID_FILTER_LIST, true) => {
1155                let param = u16::from_le_bytes([byte(rest, 0)?, byte(rest, 1)?]);
1156                let eui_len = (param & 0x1f) as u8;
1157                let len = usize::from(eui_len);
1158                let wire = rest.get(2..2 + len).ok_or(LorawanError::FrameTooShort)?;
1159                let mut eui = [0u8; FILTER_EUI_MAX];
1160                for (at, byte) in wire.iter().rev().take(FILTER_EUI_MAX).enumerate() {
1161                    eui[at] = *byte;
1162                }
1163                MacCommand::FilterListReq {
1164                    index: ((param >> 7) & 0x0f) as u8,
1165                    action: ((param >> 5) & 0x03) as u8,
1166                    eui_len,
1167                    eui,
1168                }
1169            }
1170            (CID_FILTER_LIST, false) => {
1171                let status = byte(rest, 0)?;
1172                MacCommand::FilterListAns {
1173                    combined_rules_ack: status & 0x04 != 0,
1174                    eui_len_ack: status & 0x02 != 0,
1175                    action_ack: status & 0x01 != 0,
1176                }
1177            }
1178            (CID_UPDATE_UPLINK_LIST, true) => {
1179                let key = rest.get(10..26).ok_or(LorawanError::FrameTooShort)?;
1180                let limit = byte(rest, 1)?;
1181                let mut root_wor_s_key = [0u8; 16];
1182                root_wor_s_key.copy_from_slice(key);
1183                MacCommand::UpdateUplinkListReq {
1184                    index: byte(rest, 0)? & 0x0f,
1185                    reload_rate: limit & 0x3f,
1186                    bucket_size: limit >> 6,
1187                    dev_addr: u32::from_le_bytes([
1188                        byte(rest, 2)?,
1189                        byte(rest, 3)?,
1190                        byte(rest, 4)?,
1191                        byte(rest, 5)?,
1192                    ]),
1193                    wfcnt: u32::from_le_bytes([
1194                        byte(rest, 6)?,
1195                        byte(rest, 7)?,
1196                        byte(rest, 8)?,
1197                        byte(rest, 9)?,
1198                    ]),
1199                    root_wor_s_key,
1200                }
1201            }
1202            (CID_UPDATE_UPLINK_LIST, false) => MacCommand::UpdateUplinkListAns,
1203            (CID_CTRL_UPLINK_LIST, true) => {
1204                let field = byte(rest, 0)?;
1205                MacCommand::CtrlUplinkListReq {
1206                    index: field & 0x0f,
1207                    action: (field >> 4) & 0x01,
1208                }
1209            }
1210            (CID_CTRL_UPLINK_LIST, false) => MacCommand::CtrlUplinkListAns {
1211                index_ack: byte(rest, 0)? & 0x01 != 0,
1212                wfcnt: u32::from_le_bytes([
1213                    byte(rest, 1)?,
1214                    byte(rest, 2)?,
1215                    byte(rest, 3)?,
1216                    byte(rest, 4)?,
1217                ]),
1218            },
1219            (CID_CONFIGURE_FWD_LIMIT, true) => {
1220                let rates = u32::from_le_bytes([
1221                    byte(rest, 0)?,
1222                    byte(rest, 1)?,
1223                    byte(rest, 2)?,
1224                    byte(rest, 3)?,
1225                ]);
1226                let sizes = byte(rest, 4)?;
1227                MacCommand::ConfigureFwdLimitReq {
1228                    reset_limit_counters: ((rates >> 28) & 0x03) as u8,
1229                    join_request_reload_rate: ((rates >> 21) & 0x7f) as u8,
1230                    notify_reload_rate: ((rates >> 14) & 0x7f) as u8,
1231                    global_uplink_reload_rate: ((rates >> 7) & 0x7f) as u8,
1232                    overall_reload_rate: (rates & 0x7f) as u8,
1233                    join_request_bucket_size: sizes >> 6,
1234                    notify_bucket_size: (sizes >> 4) & 0x03,
1235                    global_uplink_bucket_size: (sizes >> 2) & 0x03,
1236                    overall_bucket_size: sizes & 0x03,
1237                }
1238            }
1239            (CID_CONFIGURE_FWD_LIMIT, false) => MacCommand::ConfigureFwdLimitAns,
1240            (CID_NOTIFY_NEW_END_DEVICE, false) => {
1241                let power = u16::from_le_bytes([byte(rest, 4)?, byte(rest, 5)?]);
1242                MacCommand::NotifyNewEndDeviceReq {
1243                    dev_addr: u32::from_le_bytes([
1244                        byte(rest, 0)?,
1245                        byte(rest, 1)?,
1246                        byte(rest, 2)?,
1247                        byte(rest, 3)?,
1248                    ]),
1249                    rssi_dbm: -i16::from(((power >> 5) & 0x7f) as u8) - 15,
1250                    snr_db: ((power & 0x1f) as i8) - 20,
1251                }
1252            }
1253            _ => return Err(LorawanError::UnknownCommand(cid)),
1254        };
1255
1256        Ok((command, command.len()))
1257    }
1258}
1259
1260/// Writes a run of commands into one buffer.
1261///
1262/// # Arguments
1263///
1264/// * `commands` - what to write, in the order they go out.
1265/// * `out` - where to write them.
1266///
1267/// # Returns
1268///
1269/// How many bytes were written.
1270///
1271/// # Errors
1272///
1273/// [`LorawanError::PayloadTooLong`] when they do not fit, and whatever
1274/// [`MacCommand::encode`] refuses.
1275///
1276/// # Examples
1277///
1278/// ```
1279/// use pamoja_lorawan::mac::{encode_all, MacCommand, MacCommands, FOPTS_MAX};
1280/// use pamoja_lorawan::Direction;
1281///
1282/// // A device asks how well it is heard and what time it is, in one frame's options.
1283/// let asked = [MacCommand::LinkCheckReq, MacCommand::DeviceTimeReq];
1284/// let mut out = [0u8; FOPTS_MAX];
1285/// let written = encode_all(&asked, &mut out).unwrap();
1286///
1287/// let read: Vec<MacCommand> = MacCommands::new(Direction::Uplink, &out[..written])
1288///     .map(|command| command.unwrap())
1289///     .collect();
1290/// assert_eq!(read, asked);
1291/// ```
1292pub fn encode_all(commands: &[MacCommand], out: &mut [u8]) -> Result<usize, LorawanError> {
1293    let mut at = 0;
1294    for command in commands {
1295        at += command.encode(out.get_mut(at..).ok_or(LorawanError::PayloadTooLong)?)?;
1296    }
1297    Ok(at)
1298}
1299
1300/// Walks the commands packed into one field.
1301///
1302/// Iteration ends at an identifier this version does not know, because a command does not
1303/// carry its own length and so cannot be stepped over. What was not read is left in
1304/// [`remaining`](Self::remaining).
1305pub struct MacCommands<'a> {
1306    direction: Direction,
1307    bytes: &'a [u8],
1308    at: usize,
1309    stopped: bool,
1310}
1311
1312impl<'a> MacCommands<'a> {
1313    /// Reads the commands in a field.
1314    ///
1315    /// # Arguments
1316    ///
1317    /// * `direction` - which way the frame carrying them travels.
1318    /// * `bytes` - the frame options, or a payload sent on port zero.
1319    ///
1320    /// # Returns
1321    ///
1322    /// The walk.
1323    ///
1324    /// # Examples
1325    ///
1326    /// ```
1327    /// use pamoja_lorawan::{Direction, mac::{encode_all, MacCommand, MacCommands}};
1328    ///
1329    /// // The frame options of an uplink that asks for a link check and the time.
1330    /// let mut field = [0u8; 2];
1331    /// encode_all(&[MacCommand::LinkCheckReq, MacCommand::DeviceTimeReq], &mut field).unwrap();
1332    ///
1333    /// let read: Vec<MacCommand> = MacCommands::new(Direction::Uplink, &field)
1334    ///     .map(|command| command.unwrap())
1335    ///     .collect();
1336    /// assert_eq!(read, [MacCommand::LinkCheckReq, MacCommand::DeviceTimeReq]);
1337    /// ```
1338    #[must_use]
1339    pub const fn new(direction: Direction, bytes: &'a [u8]) -> MacCommands<'a> {
1340        MacCommands {
1341            direction,
1342            bytes,
1343            at: 0,
1344            stopped: false,
1345        }
1346    }
1347
1348    /// What was not read.
1349    ///
1350    /// # Returns
1351    ///
1352    /// The bytes left, which is empty once every command has been read and is whatever
1353    /// followed an identifier this version does not know.
1354    ///
1355    /// # Examples
1356    ///
1357    /// ```
1358    /// use pamoja_lorawan::{Direction, mac::{MacCommand, MacCommands}};
1359    ///
1360    /// // A link check request, then a command from a later version of the specification,
1361    /// // identifier 0x7F with two bytes of its own, which this one cannot step over.
1362    /// let later = [0x7F, 0x11, 0x22];
1363    /// let mut field = [0u8; 4];
1364    /// let written = MacCommand::LinkCheckReq.encode(&mut field).unwrap();
1365    /// field[written..].copy_from_slice(&later);
1366    ///
1367    /// let mut walk = MacCommands::new(Direction::Uplink, &field);
1368    /// assert!(walk.next().is_some(), "the first one is known");
1369    /// assert!(walk.next().is_none(), "the later one is not, so the walk stops");
1370    /// assert_eq!(walk.remaining(), later);
1371    /// ```
1372    #[must_use]
1373    pub fn remaining(&self) -> &'a [u8] {
1374        &self.bytes[self.at.min(self.bytes.len())..]
1375    }
1376}
1377
1378impl Iterator for MacCommands<'_> {
1379    type Item = Result<MacCommand, LorawanError>;
1380
1381    fn next(&mut self) -> Option<Self::Item> {
1382        if self.stopped || self.at >= self.bytes.len() {
1383            return None;
1384        }
1385        match MacCommand::parse(self.direction, &self.bytes[self.at..]) {
1386            Ok((command, taken)) => {
1387                self.at += taken;
1388                Some(Ok(command))
1389            }
1390            Err(LorawanError::UnknownCommand(_)) => {
1391                self.stopped = true;
1392                None
1393            }
1394            Err(error) => {
1395                self.stopped = true;
1396                Some(Err(error))
1397            }
1398        }
1399    }
1400}
1401
1402// One byte of a command's fields, or a refusal when the command is cut short.
1403fn byte(rest: &[u8], at: usize) -> Result<u8, LorawanError> {
1404    rest.get(at).copied().ok_or(LorawanError::FrameTooShort)
1405}
1406
1407// A frequency field: three bytes, low byte first, counting hundreds of hertz.
1408fn read_frequency(rest: &[u8], at: usize) -> Result<u32, LorawanError> {
1409    let raw = u32::from_le_bytes([byte(rest, at)?, byte(rest, at + 1)?, byte(rest, at + 2)?, 0]);
1410    Ok(raw * 100)
1411}
1412
1413// The second channel bits a relay and an end device configuration share: index, data rate
1414// and acknowledgment offset.
1415fn second_channel_bits(index: u8, data_rate: u8, ack_offset: u8) -> u16 {
1416    (u16::from(index & 0x03) << 7)
1417        | (u16::from(data_rate & 0x0f) << 3)
1418        | u16::from(ack_offset & 0x07)
1419}
1420
1421/// The coded signal strength a relay reports, TS011-1.0.1 sections 9.1 and 10.7: `-15 - code`
1422/// dBm, clamped to what seven bits carry.
1423///
1424/// # Arguments
1425///
1426/// * `rssi_dbm` - the strength in dBm.
1427///
1428/// # Returns
1429///
1430/// The code, 0 for -15 dBm or stronger and 127 for -142 dBm or weaker.
1431///
1432/// # Examples
1433///
1434/// ```
1435/// use pamoja_lorawan::mac::relay_rssi_code;
1436///
1437/// assert_eq!(relay_rssi_code(-15), 0);
1438/// assert_eq!(relay_rssi_code(-100), 85);
1439/// assert_eq!(relay_rssi_code(-160), 127, "clamped to the weakest");
1440/// assert_eq!(relay_rssi_code(0), 0, "clamped to the strongest");
1441/// ```
1442#[must_use]
1443pub fn relay_rssi_code(rssi_dbm: i16) -> u16 {
1444    (-15 - rssi_dbm.clamp(-142, -15)) as u16
1445}
1446
1447/// The coded signal-to-noise ratio a relay reports, TS011-1.0.1 sections 9.1 and 10.7:
1448/// `code - 20` dB, clamped to what five bits carry.
1449///
1450/// # Arguments
1451///
1452/// * `snr_db` - the ratio in dB.
1453///
1454/// # Returns
1455///
1456/// The code, 0 for -20 dB or worse and 31 for 11 dB or better.
1457///
1458/// # Examples
1459///
1460/// ```
1461/// use pamoja_lorawan::mac::relay_snr_code;
1462///
1463/// assert_eq!(relay_snr_code(-20), 0);
1464/// assert_eq!(relay_snr_code(0), 20);
1465/// assert_eq!(relay_snr_code(15), 31, "clamped to the best");
1466/// ```
1467#[must_use]
1468pub fn relay_snr_code(snr_db: i8) -> u16 {
1469    (snr_db.clamp(-20, 11) + 20) as u16
1470}
1471
1472fn write_frequency(hertz: u32, out: &mut [u8]) -> Result<(), LorawanError> {
1473    if !hertz.is_multiple_of(100) {
1474        return Err(LorawanError::MalformedFrame);
1475    }
1476    let raw = hertz / 100;
1477    if raw > 0x00ff_ffff {
1478        return Err(LorawanError::MalformedFrame);
1479    }
1480    let bytes = raw.to_le_bytes();
1481    out[..3].copy_from_slice(&bytes[..3]);
1482    Ok(())
1483}
1484
1485// A six-bit signed field, widened to the number it stands for.
1486const fn sign_extend_6(byte: u8) -> i8 {
1487    let value = byte & 0x3f;
1488    if value & 0x20 != 0 {
1489        (value as i8) - 64
1490    } else {
1491        value as i8
1492    }
1493}
1494
1495#[cfg(test)]
1496mod tests {
1497    use super::*;
1498
1499    /// Encodes a command and hands back the bytes.
1500    fn encoded(command: MacCommand) -> [u8; MAX_COMMAND] {
1501        let mut out = [0u8; MAX_COMMAND];
1502        let len = command.encode(&mut out).expect("it encodes");
1503        assert_eq!(
1504            len,
1505            command.len(),
1506            "the length it reports is the length it writes"
1507        );
1508        out
1509    }
1510
1511    #[test]
1512    fn the_same_identifier_is_a_different_command_in_each_direction() {
1513        let bytes = [0x03, 0x52, 0xff, 0x00, 0x01];
1514
1515        let (down, taken) = MacCommand::parse(Direction::Downlink, &bytes).expect("it parses");
1516        assert_eq!(
1517            down,
1518            MacCommand::LinkAdrReq {
1519                data_rate: 5,
1520                tx_power: 2,
1521                channel_mask: 0x00ff,
1522                mask_control: 0,
1523                transmissions: 1,
1524            }
1525        );
1526        assert_eq!(taken, 5);
1527
1528        let (up, taken) = MacCommand::parse(Direction::Uplink, &bytes).expect("it parses");
1529        assert_eq!(
1530            up,
1531            MacCommand::LinkAdrAns {
1532                power_ack: false,
1533                data_rate_ack: true,
1534                channel_mask_ack: false,
1535            },
1536            "0x52 read as a status byte, not as a rate"
1537        );
1538        assert_eq!(taken, 2, "the answer is shorter than the request");
1539    }
1540
1541    #[test]
1542    fn every_layout_matches_the_specification() {
1543        assert_eq!(
1544            &encoded(MacCommand::LinkCheckAns {
1545                margin: 20,
1546                gateways: 3
1547            })[..3],
1548            &[0x02, 20, 3]
1549        );
1550        assert_eq!(
1551            &encoded(MacCommand::LinkAdrReq {
1552                data_rate: 5,
1553                tx_power: 2,
1554                channel_mask: 0x00ff,
1555                mask_control: 0,
1556                transmissions: 1,
1557            })[..5],
1558            &[0x03, 0x52, 0xff, 0x00, 0x01]
1559        );
1560        assert_eq!(
1561            &encoded(MacCommand::DutyCycleReq { max_duty_cycle: 4 })[..2],
1562            &[0x04, 0x04]
1563        );
1564        assert_eq!(
1565            &encoded(MacCommand::RxParamSetupReq {
1566                rx1_offset: 2,
1567                rx2_data_rate: 3,
1568                frequency_hz: 869_525_000,
1569            })[..5],
1570            &[0x05, 0x23, 0xd2, 0xad, 0x84]
1571        );
1572        assert_eq!(
1573            &encoded(MacCommand::NewChannelReq {
1574                index: 3,
1575                frequency_hz: 867_100_000,
1576                max_data_rate: 5,
1577                min_data_rate: 0,
1578            })[..6],
1579            &[0x07, 0x03, 0x18, 0x4f, 0x84, 0x50]
1580        );
1581        assert_eq!(
1582            &encoded(MacCommand::RxTimingSetupReq { delay: 5 })[..2],
1583            &[0x08, 0x05]
1584        );
1585        assert_eq!(
1586            &encoded(MacCommand::DlChannelReq {
1587                index: 1,
1588                frequency_hz: 869_525_000,
1589            })[..5],
1590            &[0x0a, 0x01, 0xd2, 0xad, 0x84]
1591        );
1592    }
1593
1594    #[test]
1595    fn a_frequency_is_carried_in_hundreds_of_hertz_low_byte_first() {
1596        let command = MacCommand::DlChannelReq {
1597            index: 0,
1598            frequency_hz: 868_100_000,
1599        };
1600        let bytes = encoded(command);
1601        assert_eq!(
1602            u32::from_le_bytes([bytes[2], bytes[3], bytes[4], 0]) * 100,
1603            868_100_000
1604        );
1605
1606        let (read, _) = MacCommand::parse(Direction::Downlink, &bytes).expect("it parses");
1607        assert_eq!(read, command, "what goes out reads back the same");
1608
1609        // A frequency the field cannot hold is refused rather than truncated.
1610        let mut out = [0u8; MAX_COMMAND];
1611        assert_eq!(
1612            MacCommand::DlChannelReq {
1613                index: 0,
1614                frequency_hz: 2_000_000_000,
1615            }
1616            .encode(&mut out),
1617            Err(LorawanError::MalformedFrame)
1618        );
1619        assert_eq!(
1620            MacCommand::DlChannelReq {
1621                index: 0,
1622                frequency_hz: 868_100_050,
1623            }
1624            .encode(&mut out),
1625            Err(LorawanError::MalformedFrame),
1626            "the field counts hundreds, so this is not representable"
1627        );
1628    }
1629
1630    #[test]
1631    fn a_margin_is_a_six_bit_signed_number() {
1632        for margin in [-32i8, -10, -1, 0, 1, 31] {
1633            let command = MacCommand::DevStatusAns {
1634                battery: 200,
1635                margin,
1636            };
1637            let bytes = encoded(command);
1638            let (read, _) = MacCommand::parse(Direction::Uplink, &bytes).expect("it parses");
1639            assert_eq!(read, command, "at {margin}");
1640        }
1641
1642        assert_eq!(
1643            &encoded(MacCommand::DevStatusAns {
1644                battery: 0x80,
1645                margin: -10
1646            })[..3],
1647            &[0x06, 0x80, 0x36]
1648        );
1649    }
1650
1651    #[test]
1652    fn a_command_this_version_does_not_know_stops_the_walk() {
1653        // Nothing carries its own length, so the reader cannot step over what it does not
1654        // know. It stops, and says what it did not read.
1655        let field = [0x02, 0x7f, 0x11, 0x22];
1656        let mut walk = MacCommands::new(Direction::Uplink, &field);
1657
1658        assert_eq!(walk.next(), Some(Ok(MacCommand::LinkCheckReq)));
1659        assert_eq!(walk.next(), None);
1660        assert_eq!(walk.remaining(), &[0x7f, 0x11, 0x22]);
1661    }
1662
1663    #[test]
1664    fn a_command_cut_short_is_refused_rather_than_guessed() {
1665        assert_eq!(
1666            MacCommand::parse(Direction::Downlink, &[0x03, 0x52]),
1667            Err(LorawanError::FrameTooShort)
1668        );
1669        assert_eq!(
1670            MacCommand::parse(Direction::Uplink, &[]),
1671            Err(LorawanError::FrameTooShort)
1672        );
1673
1674        let field = [0x02, 0x06, 0xc8];
1675        let mut walk = MacCommands::new(Direction::Uplink, &field);
1676        assert_eq!(walk.next(), Some(Ok(MacCommand::LinkCheckReq)));
1677        assert_eq!(
1678            walk.next(),
1679            Some(Err(LorawanError::FrameTooShort)),
1680            "the status answer wants two bytes and only one is left"
1681        );
1682        assert_eq!(walk.next(), None, "and it does not carry on afterwards");
1683    }
1684
1685    #[test]
1686    fn a_run_of_commands_reads_back_as_it_was_written() {
1687        let commands = [
1688            MacCommand::DevStatusAns {
1689                battery: 255,
1690                margin: -3,
1691            },
1692            MacCommand::LinkCheckReq,
1693            MacCommand::RxTimingSetupAns,
1694        ];
1695
1696        let mut out = [0u8; FOPTS_MAX];
1697        let written = encode_all(&commands, &mut out).expect("they fit");
1698
1699        let read: Vec<MacCommand> = MacCommands::new(Direction::Uplink, &out[..written])
1700            .map(|command| command.expect("each one parses"))
1701            .collect();
1702        assert_eq!(read.as_slice(), &commands[..]);
1703    }
1704
1705    #[test]
1706    fn commands_that_do_not_fit_the_options_field_are_refused() {
1707        // Sixteen bytes of commands do not fit beside a payload; they need a frame of their
1708        // own, on port zero.
1709        let long = [MacCommand::NewChannelReq {
1710            index: 0,
1711            frequency_hz: 868_100_000,
1712            max_data_rate: 5,
1713            min_data_rate: 0,
1714        }; 3];
1715
1716        let mut out = [0u8; FOPTS_MAX];
1717        assert_eq!(
1718            encode_all(&long, &mut out),
1719            Err(LorawanError::PayloadTooLong)
1720        );
1721
1722        let mut roomy = [0u8; 32];
1723        assert_eq!(encode_all(&long, &mut roomy), Ok(18));
1724    }
1725
1726    #[test]
1727    fn the_transmit_power_table_is_the_one_the_specification_prints() {
1728        assert_eq!(eirp_dbm(0), Some(8));
1729        assert_eq!(eirp_dbm(5), Some(16));
1730        assert_eq!(eirp_dbm(15), Some(36));
1731        assert_eq!(eirp_dbm(16), None);
1732
1733        assert_eq!(eirp_code(16), Some(5));
1734        assert_eq!(eirp_code(36), Some(15));
1735        assert_eq!(
1736            eirp_code(17),
1737            None,
1738            "a power between two steps is not rounded"
1739        );
1740
1741        let command = MacCommand::TxParamSetupReq {
1742            max_eirp: 5,
1743            uplink_dwell: true,
1744            downlink_dwell: false,
1745        };
1746        assert_eq!(&encoded(command)[..2], &[0x09, 0x15]);
1747
1748        let (read, _) =
1749            MacCommand::parse(Direction::Downlink, &encoded(command)).expect("it parses");
1750        assert_eq!(read, command);
1751    }
1752
1753    #[test]
1754    fn a_receive_delay_of_zero_is_one_second() {
1755        assert_eq!(receive_delay_s(0), 1);
1756        assert_eq!(receive_delay_s(1), 1);
1757        assert_eq!(receive_delay_s(15), 15);
1758    }
1759
1760    #[test]
1761    fn every_command_reports_the_direction_it_travels() {
1762        assert_eq!(MacCommand::LinkCheckReq.direction(), Direction::Uplink);
1763        assert_eq!(
1764            MacCommand::LinkCheckAns {
1765                margin: 0,
1766                gateways: 0
1767            }
1768            .direction(),
1769            Direction::Downlink
1770        );
1771        assert_eq!(MacCommand::DeviceTimeReq.direction(), Direction::Uplink);
1772        assert_eq!(
1773            MacCommand::DeviceTimeAns {
1774                seconds: 0,
1775                fraction: 0
1776            }
1777            .direction(),
1778            Direction::Downlink
1779        );
1780    }
1781
1782    /// Encodes a command, checks it reads back the same in its direction, and hands back
1783    /// the bytes.
1784    fn round_trip(command: MacCommand) -> Vec<u8> {
1785        let mut out = [0u8; MAX_COMMAND];
1786        let len = command.encode(&mut out).expect("it encodes");
1787        assert_eq!(len, command.len());
1788        let (read, taken) = MacCommand::parse(command.direction(), &out[..len]).expect("it parses");
1789        assert_eq!(read, command, "what goes out reads back the same");
1790        assert_eq!(taken, len);
1791        out[..len].to_vec()
1792    }
1793
1794    #[test]
1795    fn a_relay_configuration_packs_its_channel_settings_as_table_32_lays_them_out() {
1796        // StartStop bit 13, CADPeriodicity 12:10, DefaultChIdx 9, SecondChIdx 8:7,
1797        // SecondChDr 6:3, SecondChAckOffset 2:0, then the second frequency.
1798        let started = MacCommand::RelayConfReq {
1799            enabled: true,
1800            cad_periodicity: 2,
1801            default_channel_index: 1,
1802            second_channel_index: 1,
1803            second_channel_data_rate: 5,
1804            second_channel_ack_offset: 1,
1805            second_channel_frequency_hz: 868_100_000,
1806        };
1807        assert_eq!(round_trip(started), [0x40, 0xa9, 0x2a, 0x28, 0x76, 0x84]);
1808
1809        let stopped = MacCommand::RelayConfReq {
1810            enabled: false,
1811            cad_periodicity: 0,
1812            default_channel_index: 0,
1813            second_channel_index: 0,
1814            second_channel_data_rate: 0,
1815            second_channel_ack_offset: 0,
1816            second_channel_frequency_hz: 0,
1817        };
1818        assert_eq!(round_trip(stopped), [0x40, 0, 0, 0, 0, 0]);
1819
1820        let answer = MacCommand::RelayConfAns {
1821            cad_periodicity_ack: true,
1822            default_channel_index_ack: false,
1823            second_channel_index_ack: true,
1824            second_channel_data_rate_ack: true,
1825            second_channel_ack_offset_ack: false,
1826            second_channel_frequency_ack: true,
1827        };
1828        assert_eq!(round_trip(answer), [0x40, 0b0010_1101]);
1829    }
1830
1831    #[test]
1832    fn an_end_device_configuration_and_its_answer_follow_tables_39_42_and_45() {
1833        let request = MacCommand::EndDeviceConfReq {
1834            relay_mode: 2,
1835            smart_enable_level: 1,
1836            back_off: 8,
1837            second_channel_index: 1,
1838            second_channel_data_rate: 3,
1839            second_channel_ack_offset: 2,
1840            second_channel_frequency_hz: 869_525_000,
1841        };
1842        assert_eq!(
1843            round_trip(request),
1844            [0x41, 0x09, 0x9a, 0x10, 0xd2, 0xad, 0x84]
1845        );
1846
1847        // TS011-1.0.1 moved bit 3 from BackOffACK to SecondChAckOffsetACK.
1848        let answer = MacCommand::EndDeviceConfAns {
1849            second_channel_ack_offset_ack: true,
1850            second_channel_index_ack: false,
1851            second_channel_data_rate_ack: false,
1852            second_channel_frequency_ack: true,
1853        };
1854        assert_eq!(round_trip(answer), [0x41, 0b0000_1001]);
1855    }
1856
1857    #[test]
1858    fn a_join_filter_carries_its_eui_prefix_low_byte_first() {
1859        // The rules of TS011-1.0.1 appendix 3.
1860        let oui = MacCommand::FilterListReq {
1861            index: 1,
1862            action: 1,
1863            eui_len: 3,
1864            eui: [0xab, 0xcd, 0xef, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
1865        };
1866        assert_eq!(round_trip(oui), [0x42, 0xa3, 0x00, 0xef, 0xcd, 0xab]);
1867
1868        let range = MacCommand::FilterListReq {
1869            index: 3,
1870            action: 1,
1871            eui_len: 15,
1872            eui: [
1873                0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, 0x12, 0x34, 0x56, 0x78, 0x28, 0x37,
1874                0x46, 0,
1875            ],
1876        };
1877        assert_eq!(
1878            round_trip(range),
1879            [
1880                0x42, 0xaf, 0x01, 0x46, 0x37, 0x28, 0x78, 0x56, 0x34, 0x12, 0xcd, 0xab, 0xef, 0xcd,
1881                0xab, 0xef, 0xcd, 0xab
1882            ]
1883        );
1884
1885        let default_rule = MacCommand::FilterListReq {
1886            index: 0,
1887            action: 2,
1888            eui_len: 0,
1889            eui: [0; FILTER_EUI_MAX],
1890        };
1891        assert_eq!(round_trip(default_rule), [0x42, 0x40, 0x00]);
1892
1893        let answer = MacCommand::FilterListAns {
1894            combined_rules_ack: true,
1895            eui_len_ack: true,
1896            action_ack: false,
1897        };
1898        assert_eq!(round_trip(answer), [0x42, 0b0000_0110]);
1899    }
1900
1901    #[test]
1902    fn a_join_filter_longer_than_two_euis_is_read_so_it_can_be_refused_but_not_written() {
1903        let mut bytes = vec![0x42, 0x94, 0x01];
1904        bytes.extend(1..=20u8);
1905        let (read, taken) = MacCommand::parse(Direction::Downlink, &bytes).expect("it parses");
1906        assert_eq!(taken, 23, "every byte of the rule is stepped over");
1907        let MacCommand::FilterListReq { eui_len, eui, .. } = read else {
1908            panic!("a filter rule");
1909        };
1910        assert_eq!(eui_len, 20);
1911        assert_eq!(
1912            eui[0], 20,
1913            "the last byte on the air is the most significant"
1914        );
1915
1916        let mut out = [0u8; MAX_COMMAND];
1917        assert_eq!(read.encode(&mut out), Err(LorawanError::MalformedFrame));
1918
1919        assert_eq!(
1920            MacCommand::parse(Direction::Downlink, &[0x42, 0xa3, 0x00, 0xef]),
1921            Err(LorawanError::FrameTooShort),
1922            "three bytes promised, one given"
1923        );
1924    }
1925
1926    #[test]
1927    fn a_trusted_end_device_carries_its_limit_address_counter_and_root_key() {
1928        let key: [u8; 16] = core::array::from_fn(|at| at as u8);
1929        let request = MacCommand::UpdateUplinkListReq {
1930            index: 3,
1931            reload_rate: 10,
1932            bucket_size: 2,
1933            dev_addr: 0x2601_1bda,
1934            wfcnt: 7,
1935            root_wor_s_key: key,
1936        };
1937        let mut want = vec![
1938            0x43, 0x03, 0x8a, 0xda, 0x1b, 0x01, 0x26, 0x07, 0x00, 0x00, 0x00,
1939        ];
1940        want.extend_from_slice(&key);
1941        assert_eq!(round_trip(request), want);
1942        assert_eq!(request.len(), MAX_COMMAND, "the longest command there is");
1943        assert_eq!(round_trip(MacCommand::UpdateUplinkListAns), [0x43]);
1944
1945        assert_eq!(
1946            round_trip(MacCommand::CtrlUplinkListReq {
1947                index: 3,
1948                action: 1
1949            }),
1950            [0x44, 0x13]
1951        );
1952        assert_eq!(
1953            round_trip(MacCommand::CtrlUplinkListAns {
1954                index_ack: true,
1955                wfcnt: 0x0102_0304
1956            }),
1957            [0x44, 0x01, 0x04, 0x03, 0x02, 0x01]
1958        );
1959    }
1960
1961    #[test]
1962    fn forwarding_limits_pack_four_reload_rates_and_four_bucket_sizes() {
1963        // Tables 62 and 65: ResetLimitCounters 29:28, JoinReq 27:21, Notify 20:14,
1964        // GlobalUplink 13:7, Overall 6:0; then JoinReq 7:6, Notify 5:4, GlobalUplink 3:2,
1965        // Overall 1:0.
1966        let request = MacCommand::ConfigureFwdLimitReq {
1967            reset_limit_counters: 3,
1968            join_request_reload_rate: 4,
1969            notify_reload_rate: 4,
1970            global_uplink_reload_rate: 8,
1971            overall_reload_rate: 127,
1972            join_request_bucket_size: 1,
1973            notify_bucket_size: 1,
1974            global_uplink_bucket_size: 2,
1975            overall_bucket_size: 0,
1976        };
1977        assert_eq!(round_trip(request), [0x45, 0x7f, 0x04, 0x81, 0x30, 0x58]);
1978        assert_eq!(round_trip(MacCommand::ConfigureFwdLimitAns), [0x45]);
1979    }
1980
1981    #[test]
1982    fn a_new_end_device_notice_codes_strength_and_ratio_as_table_67_says() {
1983        let notice = MacCommand::NotifyNewEndDeviceReq {
1984            dev_addr: 0x2601_1bda,
1985            rssi_dbm: -100,
1986            snr_db: 5,
1987        };
1988        assert_eq!(
1989            round_trip(notice),
1990            [0x46, 0xda, 0x1b, 0x01, 0x26, 0xb9, 0x0a]
1991        );
1992
1993        // Past what the fields carry, the closest value goes out.
1994        let mut out = [0u8; MAX_COMMAND];
1995        let len = MacCommand::NotifyNewEndDeviceReq {
1996            dev_addr: 1,
1997            rssi_dbm: -160,
1998            snr_db: 30,
1999        }
2000        .encode(&mut out)
2001        .expect("it encodes");
2002        let (read, _) = MacCommand::parse(Direction::Uplink, &out[..len]).expect("it parses");
2003        assert_eq!(
2004            read,
2005            MacCommand::NotifyNewEndDeviceReq {
2006                dev_addr: 1,
2007                rssi_dbm: -142,
2008                snr_db: 11
2009            }
2010        );
2011
2012        assert_eq!(
2013            MacCommand::parse(Direction::Downlink, &[0x46, 0, 0, 0, 0, 0, 0]),
2014            Err(LorawanError::UnknownCommand(0x46)),
2015            "only a relay sends it"
2016        );
2017    }
2018
2019    #[test]
2020    fn relay_commands_no_longer_stop_a_walk() {
2021        // A device that does not relay still reads past them to the commands after.
2022        let mut field = [0u8; 32];
2023        let len = encode_all(
2024            &[
2025                MacCommand::CtrlUplinkListReq {
2026                    index: 0,
2027                    action: 0,
2028                },
2029                MacCommand::DevStatusReq,
2030            ],
2031            &mut field,
2032        )
2033        .expect("they fit");
2034        let read: Vec<MacCommand> = MacCommands::new(Direction::Downlink, &field[..len])
2035            .map(|command| command.expect("each one parses"))
2036            .collect();
2037        assert_eq!(read.len(), 2);
2038        assert_eq!(read[1], MacCommand::DevStatusReq);
2039    }
2040
2041    #[test]
2042    fn the_network_time_is_seconds_since_the_gps_epoch() {
2043        // The worked example the specification gives.
2044        let command = MacCommand::DeviceTimeAns {
2045            seconds: 1_139_322_288,
2046            fraction: 0,
2047        };
2048        let bytes = encoded(command);
2049        assert_eq!(bytes[0], CID_DEVICE_TIME);
2050        assert_eq!(
2051            u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]),
2052            1_139_322_288
2053        );
2054
2055        let (read, taken) = MacCommand::parse(Direction::Downlink, &bytes).expect("it parses");
2056        assert_eq!(read, command);
2057        assert_eq!(taken, 6);
2058    }
2059}