1use pamoja_lora::region::RelayChannel;
25
26use crate::{Direction, LorawanError};
27
28pub const FOPTS_MAX: usize = 15;
32
33pub const MAX_COMMAND: usize = 27;
36
37pub const FILTER_EUI_MAX: usize = 16;
39
40pub const CID_LINK_CHECK: u8 = 0x02;
42
43pub const CID_LINK_ADR: u8 = 0x03;
45
46pub const CID_DUTY_CYCLE: u8 = 0x04;
48
49pub const CID_RX_PARAM_SETUP: u8 = 0x05;
51
52pub const CID_DEV_STATUS: u8 = 0x06;
54
55pub const CID_NEW_CHANNEL: u8 = 0x07;
57
58pub const CID_RX_TIMING_SETUP: u8 = 0x08;
60
61pub const CID_TX_PARAM_SETUP: u8 = 0x09;
63
64pub const CID_DL_CHANNEL: u8 = 0x0a;
66
67pub const CID_DEVICE_TIME: u8 = 0x0d;
69
70pub const CID_RELAY_CONF: u8 = 0x40;
72
73pub const CID_END_DEVICE_CONF: u8 = 0x41;
75
76pub const CID_FILTER_LIST: u8 = 0x42;
78
79pub const CID_UPDATE_UPLINK_LIST: u8 = 0x43;
81
82pub const CID_CTRL_UPLINK_LIST: u8 = 0x44;
84
85pub const CID_CONFIGURE_FWD_LIMIT: u8 = 0x45;
87
88pub const CID_NOTIFY_NEW_END_DEVICE: u8 = 0x46;
91
92pub 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#[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#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192pub enum MacCommand {
193 LinkCheckReq,
195 LinkCheckAns {
197 margin: u8,
199 gateways: u8,
201 },
202 LinkAdrReq {
204 data_rate: u8,
206 tx_power: u8,
208 channel_mask: u16,
210 mask_control: u8,
212 transmissions: u8,
214 },
215 LinkAdrAns {
217 power_ack: bool,
219 data_rate_ack: bool,
221 channel_mask_ack: bool,
223 },
224 DutyCycleReq {
226 max_duty_cycle: u8,
228 },
229 DutyCycleAns,
231 RxParamSetupReq {
233 rx1_offset: u8,
235 rx2_data_rate: u8,
237 frequency_hz: u32,
239 },
240 RxParamSetupAns {
242 rx1_offset_ack: bool,
244 rx2_data_rate_ack: bool,
246 channel_ack: bool,
248 },
249 DevStatusReq,
251 DevStatusAns {
253 battery: u8,
255 margin: i8,
257 },
258 NewChannelReq {
260 index: u8,
262 frequency_hz: u32,
264 max_data_rate: u8,
266 min_data_rate: u8,
268 },
269 NewChannelAns {
271 data_rate_range_ok: bool,
273 frequency_ok: bool,
275 },
276 RxTimingSetupReq {
278 delay: u8,
281 },
282 RxTimingSetupAns,
284 TxParamSetupReq {
286 max_eirp: u8,
288 uplink_dwell: bool,
290 downlink_dwell: bool,
292 },
293 TxParamSetupAns,
295 DlChannelReq {
297 index: u8,
299 frequency_hz: u32,
301 },
302 DlChannelAns {
304 uplink_frequency_exists: bool,
306 frequency_ok: bool,
308 },
309 DeviceTimeReq,
311 DeviceTimeAns {
313 seconds: u32,
315 fraction: u8,
317 },
318 RelayConfReq {
320 enabled: bool,
322 cad_periodicity: u8,
324 default_channel_index: u8,
326 second_channel_index: u8,
329 second_channel_data_rate: u8,
331 second_channel_ack_offset: u8,
334 second_channel_frequency_hz: u32,
336 },
337 RelayConfAns {
339 cad_periodicity_ack: bool,
341 default_channel_index_ack: bool,
343 second_channel_index_ack: bool,
345 second_channel_data_rate_ack: bool,
347 second_channel_ack_offset_ack: bool,
349 second_channel_frequency_ack: bool,
351 },
352 EndDeviceConfReq {
354 relay_mode: u8,
357 smart_enable_level: u8,
359 back_off: u8,
362 second_channel_index: u8,
364 second_channel_data_rate: u8,
366 second_channel_ack_offset: u8,
368 second_channel_frequency_hz: u32,
370 },
371 EndDeviceConfAns {
373 second_channel_ack_offset_ack: bool,
375 second_channel_index_ack: bool,
377 second_channel_data_rate_ack: bool,
379 second_channel_frequency_ack: bool,
381 },
382 FilterListReq {
384 index: u8,
386 action: u8,
388 eui_len: u8,
392 eui: [u8; FILTER_EUI_MAX],
395 },
396 FilterListAns {
398 combined_rules_ack: bool,
400 eui_len_ack: bool,
402 action_ack: bool,
404 },
405 UpdateUplinkListReq {
407 index: u8,
409 reload_rate: u8,
411 bucket_size: u8,
413 dev_addr: u32,
415 wfcnt: u32,
417 root_wor_s_key: [u8; 16],
419 },
420 UpdateUplinkListAns,
422 CtrlUplinkListReq {
424 index: u8,
426 action: u8,
428 },
429 CtrlUplinkListAns {
431 index_ack: bool,
433 wfcnt: u32,
435 },
436 ConfigureFwdLimitReq {
438 reset_limit_counters: u8,
440 join_request_reload_rate: u8,
442 notify_reload_rate: u8,
444 global_uplink_reload_rate: u8,
446 overall_reload_rate: u8,
448 join_request_bucket_size: u8,
450 notify_bucket_size: u8,
452 global_uplink_bucket_size: u8,
454 overall_bucket_size: u8,
456 },
457 ConfigureFwdLimitAns,
459 NotifyNewEndDeviceReq {
461 dev_addr: u32,
463 rssi_dbm: i16,
465 snr_db: i8,
467 },
468}
469
470pub const RELAY_ACK_OFFSET_HZ: [u32; 6] = [0, 200_000, 400_000, 800_000, 1_600_000, 3_200_000];
473
474pub const RELAY_BUCKET_MULTIPLIER: [u16; 4] = [1, 2, 4, 12];
476
477#[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 #[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 #[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 #[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 #[must_use]
673 pub const fn is_empty(&self) -> bool {
674 self.len() == 1
675 }
676
677 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(¶m.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 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
1260pub 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
1300pub struct MacCommands<'a> {
1306 direction: Direction,
1307 bytes: &'a [u8],
1308 at: usize,
1309 stopped: bool,
1310}
1311
1312impl<'a> MacCommands<'a> {
1313 #[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 #[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
1402fn byte(rest: &[u8], at: usize) -> Result<u8, LorawanError> {
1404 rest.get(at).copied().ok_or(LorawanError::FrameTooShort)
1405}
1406
1407fn 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
1413fn 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#[must_use]
1443pub fn relay_rssi_code(rssi_dbm: i16) -> u16 {
1444 (-15 - rssi_dbm.clamp(-142, -15)) as u16
1445}
1446
1447#[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
1485const 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 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 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 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 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 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 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 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 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 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 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 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 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}