1use heapless::{Deque, Vec as HeaplessVec};
4use umsh_core::options::{OptionDecoder, OptionEncoder};
5use umsh_core::{
6 ChannelKey, EncodeError, NodeHint, PacketBuilder, PacketHeader, PacketType, SourceAddrRef,
7};
8use umsh_crypto::replay::{ReplayVerdict, ReplayWindow};
9use umsh_crypto::{AesProvider, CryptoEngine, PairwiseKeys, Sha256Provider};
10use umsh_ulcp::Status;
11use umsh_ulcp::airtime::lora_airtime_ms;
12use umsh_ulcp::alert::AlertState;
13use umsh_ulcp::battery::{self, BatteryStatus};
14use umsh_ulcp::frame::{self, Cmd, Frame, PropPayload, StreamPayload, TID_UNSOLICITED};
15use umsh_ulcp::gnss::{self, GnssSnapshot};
16use umsh_ulcp::ids::{
17 self, DEFAULT_ADVERT_INTERVAL_S, DEFAULT_BEACON_INTERVAL_S, MAX_AUTO_ANNOUNCE_INTERVAL_S,
18 MIN_AUTO_ANNOUNCE_INTERVAL_S, cap, prop, stream,
19};
20use umsh_ulcp::items::{self, Filter, ItemError, REGION_CODE_LEN};
21use umsh_ulcp::meta::{self, BufferedRxMeta, RX_FLAG_ACKED, RX_FLAG_BUFFERED, RxMeta, TxMeta};
22use umsh_ulcp::pui;
23
24use crate::duty::DutyLedger;
25
26pub const MAX_MTU: usize = 255;
28
29pub const MAX_DEVICE_NAME_LEN: usize = 64;
31
32const SCRATCH: usize = MAX_MTU + 24;
34
35const PROP_BUF: usize = MAX_PEER_KEYS * items::PUBLIC_KEY_LEN + 16;
38
39const SUPPORTED_BW_HZ: [u32; 10] = [
41 7_810, 10_420, 15_630, 20_830, 31_250, 41_670, 62_500, 125_000, 250_000, 500_000,
42];
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct RadioSettings {
48 pub enabled: bool,
49 pub freq_khz: u32,
50 pub bw_hz: u32,
51 pub sf: u8,
52 pub cr_denom: u8,
53 pub tx_power_dbm: i8,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub enum TxPower {
59 Default,
61 Max,
63 Dbm(i8),
65}
66
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
78pub struct BatteryFields {
79 pub voltage: bool,
80 pub level: bool,
81 pub charge_state: bool,
82}
83
84impl BatteryFields {
85 pub const NONE: Self = Self {
88 voltage: false,
89 level: false,
90 charge_state: false,
91 };
92
93 pub const fn any(self) -> bool {
95 self.voltage || self.level || self.charge_state
96 }
97
98 fn matches(self, snapshot: &BatteryStatus) -> bool {
100 (self.voltage || snapshot.voltage_mv.is_none())
101 && (self.level || snapshot.level_percent.is_none())
102 && (self.charge_state || snapshot.charge_state.is_none())
103 }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub struct AlertConfig {
109 pub timeout_ms: u32,
114}
115
116impl AlertConfig {
117 pub const DEFAULT: Self = Self {
119 timeout_ms: 5 * 60 * 1000,
120 };
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub struct TimeConfig;
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138pub struct GnssConfig {
139 pub default_enabled: bool,
152}
153
154impl GnssConfig {
155 pub const DEFAULT: Self = Self {
157 default_enabled: false,
158 };
159
160 pub const ALWAYS_ON: Self = Self {
162 default_enabled: true,
163 };
164}
165
166pub const DEFAULT_IDENT_PRECISION: u8 = 5;
170
171pub const MAX_IDENT_PRECISION: u8 = gnss::MAX_LOCATION_LEN as u8;
174
175#[derive(Clone, Copy, Debug)]
177pub struct SessionConfig {
178 pub dev_version: &'static str,
180 pub default_device_name: &'static str,
182 pub mtu: u16,
184 pub sync_word: u16,
187 pub min_tx_power_dbm: i8,
189 pub max_tx_power_dbm: i8,
191 pub freq_khz_min: u32,
193 pub freq_khz_max: u32,
194 pub defaults: RadioSettings,
197 pub default_duty_limit: u16,
199 pub duty: &'static DutyLedger,
206 pub battery: Option<BatteryFields>,
210 pub alert: Option<AlertConfig>,
214 pub time: Option<TimeConfig>,
217 pub gnss: Option<GnssConfig>,
221 pub illuminance: bool,
225}
226
227#[derive(Clone, Copy, Debug, PartialEq, Eq)]
230pub enum TxOutcome {
231 Sent,
233 ChannelBusy,
237 Failed,
239}
240
241#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243pub enum Effect {
244 ApplyRadio(RadioSettings),
246 StartTransmit,
250 SignIdentity { tid: u8 },
255 SampleRssi { tid: u8 },
260 SampleBattery { tid: u8 },
266 SampleIlluminance { tid: u8 },
271 SetPairingPin { tid: u8, pin: Option<u32> },
274 ReadTime { tid: u8 },
280 ApplyTime { epoch: Option<u32> },
289 SampleGnss { tid: u8, key: u32 },
294 DeviceNameChanged,
297 DrainQueue,
303 SaveSnapshot { tid: u8 },
308 ClearSaved { tid: u8 },
313 ProvisionIdentity { tid: u8 },
322 ApplyAlert(AlertState),
327 FactoryReset,
334}
335
336#[derive(Clone, Copy)]
339pub enum IdentitySource {
340 Install([u8; PRIVATE_KEY_LEN]),
342 Generate,
346}
347
348struct PendingTx {
349 data: HeaplessVec<u8, MAX_MTU>,
350 tid: u8,
351 airtime_ms: u32,
352 power: TxPower,
353 autonomous: bool,
357 ack_for: Option<u16>,
362 nocca: bool,
365}
366
367struct AckPlan {
369 trailer: [u8; 8],
372 flood_hops: Option<u8>,
377}
378
379enum SecureRx {
383 Plain,
387 New {
391 ack: Option<AckPlan>,
392 identity: Option<RxIdentity>,
393 },
394 Duplicate {
400 ack: Option<AckPlan>,
401 identity: Option<RxIdentity>,
402 },
403}
404
405enum PropValue {
407 Encoded(usize),
408 Unimplemented,
409 Unknown,
410}
411
412struct DeviceDomain {
417 settings: RadioSettings,
418 name: [u8; MAX_DEVICE_NAME_LEN],
419 name_len: usize,
420 channel_keys: ChannelKeyTable,
424 peers: DevPeerTable,
426 repeater_enabled: bool,
431 repeater_regions: RepeaterRegions,
435 repeater_default_region: Option<[u8; REGION_CODE_LEN]>,
442 repeater_min_rssi: Option<i16>,
445 repeater_min_snr: Option<i8>,
448 ident_role: Option<u8>,
457 ident_mobile: bool,
462 dev_discoverable: bool,
467 advert_interval_s: u32,
471 beacon_interval_s: u32,
477 startup_beacon: bool,
481 tz_offset_min: i16,
485 gnss_enabled: bool,
493 gnss_ident_update: bool,
497 gnss_ident_precision: u8,
500 gnss_time_trust: bool,
504}
505
506impl DeviceDomain {
507 fn post_reset(config: &SessionConfig) -> Self {
508 let mut settings = config.defaults;
509 settings.enabled = false;
510 let mut name = [0; MAX_DEVICE_NAME_LEN];
511 let name_len = config.default_device_name.len();
512 name[..name_len].copy_from_slice(config.default_device_name.as_bytes());
513 config.duty.reset_accounting();
516 config.duty.set_limit(config.default_duty_limit);
517 config
518 .duty
519 .set_phy(settings.sf, settings.bw_hz, settings.cr_denom);
520 Self {
521 settings,
522 name,
523 name_len,
524 channel_keys: ChannelKeyTable::default(),
525 peers: DevPeerTable::default(),
526 repeater_enabled: false,
527 repeater_regions: RepeaterRegions::default(),
528 repeater_default_region: None,
529 repeater_min_rssi: None,
530 repeater_min_snr: None,
531 ident_role: None,
532 ident_mobile: false,
533 dev_discoverable: true,
534 advert_interval_s: DEFAULT_ADVERT_INTERVAL_S,
535 beacon_interval_s: DEFAULT_BEACON_INTERVAL_S,
536 startup_beacon: true,
537 tz_offset_min: 0,
538 gnss_enabled: config.gnss.is_some_and(|gnss| gnss.default_enabled),
539 gnss_ident_update: false,
540 gnss_ident_precision: DEFAULT_IDENT_PRECISION,
541 gnss_time_trust: true,
542 }
543 }
544}
545
546pub const MAX_RX_FILTERS: usize = 16;
548
549#[derive(Clone, Copy)]
554struct FilterTable {
555 entries: [Filter; MAX_RX_FILTERS],
556 len: usize,
557}
558
559impl Default for FilterTable {
560 fn default() -> Self {
561 Self {
562 entries: [Filter::PktType(0); MAX_RX_FILTERS],
563 len: 0,
564 }
565 }
566}
567
568impl FilterTable {
569 fn iter(&self) -> impl Iterator<Item = &Filter> {
570 self.entries[..self.len].iter()
571 }
572
573 fn is_empty(&self) -> bool {
574 self.len == 0
575 }
576
577 fn insert(&mut self, filter: Filter) -> Result<(), Status> {
580 if self.iter().any(|existing| *existing == filter) {
581 return Err(Status::ALREADY);
582 }
583 if self.len == MAX_RX_FILTERS {
584 return Err(Status::NOMEM);
585 }
586 self.entries[self.len] = filter;
587 self.len += 1;
588 Ok(())
589 }
590
591 fn remove(&mut self, filter: Filter) -> Result<(), Status> {
594 let Some(index) = self.iter().position(|existing| *existing == filter) else {
595 return Err(Status::ITEM_NOT_FOUND);
596 };
597 self.len -= 1;
598 self.entries[index] = self.entries[self.len];
599 Ok(())
600 }
601
602 fn parse_table(value: &[u8]) -> Result<Self, Status> {
607 let mut table = Self::default();
608 for item in items::prefixed_items(value) {
609 let filter = decode_filter(item.map_err(table_error)?)?;
610 match table.insert(filter) {
611 Ok(()) | Err(Status::ALREADY) => {}
612 Err(status) => return Err(status),
613 }
614 }
615 Ok(table)
616 }
617}
618
619fn decode_filter(item: &[u8]) -> Result<Filter, Status> {
623 let filter = Filter::decode(item).map_err(|_| Status::INVALID_ARGUMENT)?;
624 if matches!(filter, Filter::PktType(pkt_type) if pkt_type > 7) {
625 return Err(Status::INVALID_ARGUMENT);
626 }
627 Ok(filter)
628}
629
630fn table_error(error: ItemError) -> Status {
635 match error {
636 ItemError::BadPrefix | ItemError::Truncated => Status::PARSE_ERROR,
637 _ => Status::INVALID_ARGUMENT,
638 }
639}
640
641pub const RX_QUEUE_CAPACITY: usize = 16;
643
644#[derive(Clone, Copy, PartialEq, Eq)]
650struct RxIdentity {
651 counter: u32,
652 mic: [u8; 16],
653 mic_len: u8,
654}
655
656impl RxIdentity {
657 fn new(counter: u32, mic: &[u8]) -> Option<Self> {
658 if mic.is_empty() || mic.len() > 16 {
659 return None;
660 }
661 let mut padded = [0u8; 16];
662 padded[..mic.len()].copy_from_slice(mic);
663 Some(Self {
664 counter,
665 mic: padded,
666 mic_len: mic.len() as u8,
667 })
668 }
669}
670
671#[derive(Clone, Copy)]
676struct QueueEntry {
677 data: [u8; MAX_MTU],
678 len: u16,
679 rssi_dbm: i16,
680 snr_cb: i16,
681 lqi: Option<core::num::NonZeroU8>,
682 rx_time_ms: u64,
683 acked: bool,
684 seq: u16,
688 identity: Option<RxIdentity>,
689}
690
691impl QueueEntry {
692 const EMPTY: Self = Self {
693 data: [0; MAX_MTU],
694 len: 0,
695 rssi_dbm: 0,
696 snr_cb: 0,
697 lqi: None,
698 rx_time_ms: 0,
699 acked: false,
700 seq: 0,
701 identity: None,
702 };
703
704 fn frame(&self) -> &[u8] {
705 &self.data[..usize::from(self.len)]
706 }
707}
708
709struct RxQueue {
714 entries: [QueueEntry; RX_QUEUE_CAPACITY],
715 head: usize,
717 len: usize,
718 dropped: u32,
719 next_seq: u16,
722}
723
724impl Default for RxQueue {
725 fn default() -> Self {
726 Self {
727 entries: [QueueEntry::EMPTY; RX_QUEUE_CAPACITY],
728 head: 0,
729 len: 0,
730 dropped: 0,
731 next_seq: 0,
732 }
733 }
734}
735
736impl RxQueue {
737 fn clear(&mut self) {
741 self.head = 0;
742 self.len = 0;
743 self.dropped = 0;
744 for entry in &mut self.entries {
745 entry.identity = None;
746 }
747 }
748
749 fn push(
752 &mut self,
753 data: &[u8],
754 rssi_dbm: i16,
755 snr_cb: i16,
756 lqi: Option<core::num::NonZeroU8>,
757 rx_time_ms: u64,
758 identity: Option<RxIdentity>,
759 ) -> u16 {
760 debug_assert!(data.len() <= MAX_MTU);
761 if self.len == RX_QUEUE_CAPACITY {
762 self.head = (self.head + 1) % RX_QUEUE_CAPACITY;
763 self.len -= 1;
764 self.dropped = self.dropped.wrapping_add(1);
765 }
766 let seq = self.next_seq;
767 self.next_seq = self.next_seq.wrapping_add(1);
768 let slot = (self.head + self.len) % RX_QUEUE_CAPACITY;
769 let entry = &mut self.entries[slot];
770 entry.data[..data.len()].copy_from_slice(data);
771 entry.len = data.len() as u16;
772 entry.rssi_dbm = rssi_dbm;
773 entry.snr_cb = snr_cb;
774 entry.lqi = lqi;
775 entry.rx_time_ms = rx_time_ms;
776 entry.acked = false;
777 entry.seq = seq;
778 entry.identity = identity;
779 self.len += 1;
780 seq
781 }
782
783 fn pop_front(&mut self) -> Option<QueueEntry> {
784 if self.len == 0 {
785 return None;
786 }
787 let entry = self.entries[self.head];
788 self.head = (self.head + 1) % RX_QUEUE_CAPACITY;
789 self.len -= 1;
790 Some(entry)
791 }
792
793 fn iter(&self) -> impl Iterator<Item = &QueueEntry> {
794 (0..self.len).map(|offset| &self.entries[(self.head + offset) % RX_QUEUE_CAPACITY])
795 }
796
797 fn seq_for_identity(&self, identity: &RxIdentity) -> Option<u16> {
800 self.iter()
801 .find(|entry| entry.identity.as_ref() == Some(identity))
802 .map(|entry| entry.seq)
803 }
804
805 fn mark_acked(&mut self, seq: u16) {
808 let Some(offset) = (0..self.len)
809 .find(|offset| self.entries[(self.head + offset) % RX_QUEUE_CAPACITY].seq == seq)
810 else {
811 return;
812 };
813 self.entries[(self.head + offset) % RX_QUEUE_CAPACITY].acked = true;
814 }
815}
816
817pub const MAX_CHANNEL_KEYS: usize = 8;
819pub const MAX_PEER_KEYS: usize = 8;
821
822#[derive(Clone, Copy)]
825struct ChannelKeyEntry {
826 key: [u8; items::CHANNEL_KEY_LEN],
827 id: [u8; items::CHANNEL_ID_LEN],
828}
829
830#[derive(Clone, Copy, Default)]
834struct ChannelKeyTable {
835 entries: [Option<ChannelKeyEntry>; MAX_CHANNEL_KEYS],
836 len: usize,
837}
838
839impl ChannelKeyTable {
840 fn iter(&self) -> impl Iterator<Item = &ChannelKeyEntry> {
841 self.entries[..self.len]
842 .iter()
843 .map(|entry| entry.as_ref().expect("entries below len are populated"))
844 }
845
846 fn insert(&mut self, entry: ChannelKeyEntry) -> Result<(), Status> {
847 if self.iter().any(|existing| existing.key == entry.key) {
848 return Err(Status::ALREADY);
849 }
850 if self.len == MAX_CHANNEL_KEYS {
851 return Err(Status::NOMEM);
852 }
853 self.entries[self.len] = Some(entry);
854 self.len += 1;
855 Ok(())
856 }
857
858 fn remove(&mut self, key: &[u8; items::CHANNEL_KEY_LEN]) -> Result<[u8; 2], Status> {
861 let Some(index) = self.iter().position(|existing| existing.key == *key) else {
862 return Err(Status::ITEM_NOT_FOUND);
863 };
864 let id = self.entries[index].expect("populated").id;
865 self.len -= 1;
866 self.entries[index] = self.entries[self.len];
867 self.entries[self.len] = None;
868 Ok(id)
869 }
870}
871
872struct PeerSlot {
877 entry: items::PeerKeyEntry,
878 window: ReplayWindow,
879}
880
881#[derive(Default)]
885struct PeerKeyTable {
886 entries: [Option<PeerSlot>; MAX_PEER_KEYS],
887 len: usize,
888}
889
890impl PeerKeyTable {
891 fn iter(&self) -> impl Iterator<Item = &PeerSlot> {
892 self.entries[..self.len]
893 .iter()
894 .map(|slot| slot.as_ref().expect("entries below len are populated"))
895 }
896
897 fn insert(&mut self, entry: items::PeerKeyEntry) -> Result<(), Status> {
901 if let Some(existing) = self.entries[..self.len]
902 .iter_mut()
903 .flatten()
904 .find(|existing| existing.entry.public_key == entry.public_key)
905 {
906 existing.entry = entry;
907 return Ok(());
908 }
909 if self.len == MAX_PEER_KEYS {
910 return Err(Status::NOMEM);
911 }
912 self.entries[self.len] = Some(PeerSlot {
913 entry,
914 window: ReplayWindow::new(),
915 });
916 self.len += 1;
917 Ok(())
918 }
919
920 fn reconcile(&mut self, desired: &[items::PeerKeyEntry]) {
935 let mut index = 0;
936 while index < self.len {
937 let public_key = self.entries[index]
938 .as_ref()
939 .expect("entries below len are populated")
940 .entry
941 .public_key;
942 if desired.iter().any(|entry| entry.public_key == public_key) {
943 index += 1;
944 continue;
945 }
946 self.len -= 1;
947 self.entries[index] = self.entries[self.len].take();
948 }
949 for entry in desired {
953 let _ = self.insert(*entry);
954 }
955 }
956
957 fn remove(&mut self, public_key: &[u8; items::PUBLIC_KEY_LEN]) -> Result<(), Status> {
960 let Some(index) = self
961 .iter()
962 .position(|existing| existing.entry.public_key == *public_key)
963 else {
964 return Err(Status::ITEM_NOT_FOUND);
965 };
966 self.len -= 1;
967 self.entries[index] = self.entries[self.len].take();
968 Ok(())
969 }
970
971 fn resolve_source(&self, source: &SourceAddrRef, frame: &[u8]) -> Option<usize> {
976 match source {
977 SourceAddrRef::FullKeyAt { offset } => {
978 let key = frame.get(*offset..*offset + items::PUBLIC_KEY_LEN)?;
979 self.iter().position(|slot| slot.entry.public_key == *key)
980 }
981 SourceAddrRef::Hint(hint) => {
982 let mut matches = self
983 .iter()
984 .enumerate()
985 .filter(|(_, slot)| slot.entry.public_key[..3] == hint.0);
986 let (index, _) = matches.next()?;
987 matches.next().is_none().then_some(index)
988 }
989 _ => None,
990 }
991 }
992}
993
994pub const PRIVATE_KEY_LEN: usize = 32;
996
997pub const MAX_DEV_PEERS: usize = 8;
999
1000#[derive(Clone, Copy, Default)]
1005struct DevPeerTable {
1006 entries: [[u8; items::PUBLIC_KEY_LEN]; MAX_DEV_PEERS],
1007 len: usize,
1008}
1009
1010impl DevPeerTable {
1011 fn iter(&self) -> impl Iterator<Item = &[u8; items::PUBLIC_KEY_LEN]> {
1012 self.entries[..self.len].iter()
1013 }
1014
1015 fn insert(&mut self, public_key: [u8; items::PUBLIC_KEY_LEN]) -> Result<(), Status> {
1018 if self.iter().any(|existing| *existing == public_key) {
1019 return Err(Status::ALREADY);
1020 }
1021 if self.len == MAX_DEV_PEERS {
1022 return Err(Status::NOMEM);
1023 }
1024 self.entries[self.len] = public_key;
1025 self.len += 1;
1026 Ok(())
1027 }
1028
1029 fn remove(&mut self, public_key: &[u8; items::PUBLIC_KEY_LEN]) -> Result<(), Status> {
1032 let Some(index) = self.iter().position(|existing| existing == public_key) else {
1033 return Err(Status::ITEM_NOT_FOUND);
1034 };
1035 self.len -= 1;
1036 self.entries[index] = self.entries[self.len];
1037 Ok(())
1038 }
1039
1040 fn parse_table(value: &[u8]) -> Result<Self, Status> {
1043 let mut table = Self::default();
1044 for item in items::fixed_items::<{ items::PUBLIC_KEY_LEN }>(value)
1045 .map_err(|_| Status::INVALID_ARGUMENT)?
1046 {
1047 match table.insert(*item) {
1048 Ok(()) | Err(Status::ALREADY) => {}
1049 Err(status) => return Err(status),
1050 }
1051 }
1052 Ok(table)
1053 }
1054}
1055
1056pub const MAX_REPEATER_REGIONS: usize = 8;
1060
1061#[derive(Clone, Copy, Default)]
1069struct RepeaterRegions {
1070 bytes: [u8; MAX_REPEATER_REGIONS * REGION_CODE_LEN],
1071 len: usize,
1072}
1073
1074impl RepeaterRegions {
1075 fn as_slice(&self) -> &[u8] {
1076 &self.bytes[..self.len]
1077 }
1078
1079 fn is_empty(&self) -> bool {
1080 self.len == 0
1081 }
1082
1083 fn parse(value: &[u8]) -> Result<Self, Status> {
1087 if !value.len().is_multiple_of(REGION_CODE_LEN) {
1088 return Err(Status::INVALID_ARGUMENT);
1089 }
1090 if value.len() > MAX_REPEATER_REGIONS * REGION_CODE_LEN {
1091 return Err(Status::NOMEM);
1092 }
1093 let mut regions = Self::default();
1094 regions.bytes[..value.len()].copy_from_slice(value);
1095 regions.len = value.len();
1096 Ok(regions)
1097 }
1098}
1099
1100fn parse_region_code(value: &[u8]) -> Result<Option<[u8; REGION_CODE_LEN]>, Status> {
1103 match value.len() {
1104 0 => Ok(None),
1105 REGION_CODE_LEN => Ok(Some([value[0], value[1]])),
1106 _ => Err(Status::INVALID_ARGUMENT),
1107 }
1108}
1109
1110const TRANSMITTED_MIC_SLOTS: usize = 16;
1115
1116#[derive(Default)]
1136struct TransmittedMics {
1137 slots: [[u8; 4]; TRANSMITTED_MIC_SLOTS],
1138 filled: usize,
1140 cursor: usize,
1142}
1143
1144impl TransmittedMics {
1145 fn note(&mut self, mic: [u8; 4]) {
1148 if self.contains(&mic) {
1149 return;
1150 }
1151 self.slots[self.cursor] = mic;
1152 self.cursor = (self.cursor + 1) % TRANSMITTED_MIC_SLOTS;
1153 if self.filled < TRANSMITTED_MIC_SLOTS {
1154 self.filled += 1;
1155 }
1156 }
1157
1158 fn contains(&self, mic: &[u8; 4]) -> bool {
1160 self.slots[..self.filled].iter().any(|slot| slot == mic)
1161 }
1162}
1163
1164#[derive(Default)]
1169struct HostDomain {
1170 key: Option<[u8; items::PUBLIC_KEY_LEN]>,
1172 filters: FilterTable,
1174 channel_keys: ChannelKeyTable,
1176 peer_keys: PeerKeyTable,
1178 auto_ack: bool,
1181 queue: RxQueue,
1183 transmitted_mics: TransmittedMics,
1187}
1188
1189impl HostDomain {
1190 fn reset(&mut self, key: Option<[u8; items::PUBLIC_KEY_LEN]>) {
1195 self.key = key;
1196 self.filters = FilterTable::default();
1197 self.channel_keys = ChannelKeyTable::default();
1198 self.peer_keys = PeerKeyTable::default();
1199 self.auto_ack = false;
1200 self.queue.clear();
1201 self.transmitted_mics = TransmittedMics::default();
1202 }
1203
1204 fn note_tx_mic(&mut self, frame: &[u8]) {
1209 let Ok(header) = PacketHeader::parse(frame) else {
1210 return;
1211 };
1212 if header.fcf.packet_type() == PacketType::MacAck {
1213 return;
1214 }
1215 if let Some(mic) = frame.get(header.mic_range.clone())
1216 && mic.len() >= 4
1217 {
1218 self.transmitted_mics.note([mic[0], mic[1], mic[2], mic[3]]);
1219 }
1220 }
1221
1222 fn filtering_configured(&self) -> bool {
1226 self.key.is_some() || !self.filters.is_empty() || self.channel_keys.len != 0
1227 }
1228
1229 fn accepts_live_frame(&self, data: &[u8]) -> bool {
1236 if PacketHeader::parse(data)
1237 .is_ok_and(|header| header.fcf.packet_type() == PacketType::Broadcast)
1238 {
1239 return true;
1240 }
1241 self.accepts_frame(data)
1242 }
1243
1244 fn accepts_frame(&self, data: &[u8]) -> bool {
1250 if !self.filtering_configured() {
1251 return true;
1252 }
1253 let Ok(header) = PacketHeader::parse(data) else {
1254 return false;
1255 };
1256 if let Some(mic) = data.get(header.mic_range.start..header.mic_range.start + 4)
1270 && self
1271 .transmitted_mics
1272 .contains(&[mic[0], mic[1], mic[2], mic[3]])
1273 {
1274 return true;
1275 }
1276 let dst = header.dst.map(|hint| hint.0);
1277 if let Some(key) = &self.key
1278 && dst == Some([key[0], key[1], key[2]])
1279 {
1280 return true;
1281 }
1282 let channel = header.channel.map(|channel| channel.0);
1283 if channel.is_some()
1286 && self
1287 .channel_keys
1288 .iter()
1289 .any(|entry| channel == Some(entry.id))
1290 {
1291 return true;
1292 }
1293 let pkt_type = header.fcf.packet_type() as u8;
1294 self.filters.iter().any(|filter| match filter {
1295 Filter::DestHint(hint) => dst == Some(*hint),
1296 Filter::ChannelId(id) => channel == Some(*id),
1297 Filter::PktType(filtered) => pkt_type == *filtered,
1298 })
1299 }
1300}
1301
1302pub const SNAPSHOT_MAX: usize = 1792;
1311
1312const SNAPSHOT_FORMAT: u8 = 4;
1324
1325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1333enum ApplyPhase {
1334 Keys,
1337 Config,
1340 Enable,
1342}
1343
1344impl ApplyPhase {
1345 const ORDER: [Self; 3] = [Self::Keys, Self::Config, Self::Enable];
1346}
1347
1348struct SavedProperty {
1351 number: u16,
1352 phase: ApplyPhase,
1353 repeatable: bool,
1356}
1357
1358const fn saved(number: u32, phase: ApplyPhase, repeatable: bool) -> SavedProperty {
1362 assert!(
1363 number <= u16::MAX as u32,
1364 "saved property number must fit u16"
1365 );
1366 SavedProperty {
1367 number: number as u16,
1368 phase,
1369 repeatable,
1370 }
1371}
1372
1373const SAVED_SCHEMA: &[SavedProperty] = &[
1394 saved(prop::PHY_ENABLED, ApplyPhase::Enable, false),
1395 saved(prop::PHY_FREQ, ApplyPhase::Config, false),
1396 saved(prop::PHY_TX_POWER, ApplyPhase::Config, false),
1397 saved(prop::PHY_LORA_BW, ApplyPhase::Config, false),
1398 saved(prop::PHY_LORA_SF, ApplyPhase::Config, false),
1399 saved(prop::PHY_LORA_CR, ApplyPhase::Config, false),
1400 saved(prop::DEV_KEY, ApplyPhase::Keys, false),
1401 saved(prop::DEV_CHANNEL_KEYS, ApplyPhase::Keys, true),
1402 saved(prop::DEV_PEERS, ApplyPhase::Keys, true),
1403 saved(prop::DEV_NAME, ApplyPhase::Config, false),
1404 saved(prop::MAC_REPEATER_ENABLED, ApplyPhase::Config, false),
1405 saved(prop::IDENT_ROLE, ApplyPhase::Config, false),
1406 saved(prop::IDENT_MOBILE, ApplyPhase::Config, false),
1407 saved(prop::MAC_REPEATER_REGIONS, ApplyPhase::Config, false),
1408 saved(prop::MAC_REPEATER_DEFAULT_REGION, ApplyPhase::Config, false),
1409 saved(prop::MAC_REPEATER_MIN_RSSI, ApplyPhase::Config, false),
1410 saved(prop::MAC_REPEATER_MIN_SNR, ApplyPhase::Config, false),
1411 saved(prop::DEV_DISCOVERABLE, ApplyPhase::Config, false),
1412 saved(prop::ADVERT_INTERVAL, ApplyPhase::Config, false),
1413 saved(prop::BEACON_INTERVAL, ApplyPhase::Config, false),
1414 saved(prop::STARTUP_BEACON, ApplyPhase::Config, false),
1415 saved(prop::GNSS_ENABLED, ApplyPhase::Config, false),
1416 saved(prop::PHY_DUTY_LIMIT, ApplyPhase::Config, false),
1417 saved(prop::TZ_OFFSET, ApplyPhase::Config, false),
1418 saved(prop::GNSS_IDENT_UPDATE, ApplyPhase::Config, false),
1419 saved(prop::GNSS_IDENT_PRECISION, ApplyPhase::Config, false),
1420 saved(prop::GNSS_TIME_TRUST, ApplyPhase::Config, false),
1421];
1422
1423const _: () = assert!(
1427 SAVED_SCHEMA.len() <= u32::BITS as usize,
1428 "SAVED_SCHEMA outgrew the duplicate-detection bitmask"
1429);
1430
1431#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1435pub enum SnapshotError {
1436 UnknownFormat,
1439 Malformed,
1442 InvalidValue,
1445}
1446
1447#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1449pub enum SavedStatus {
1450 #[default]
1452 None,
1453 Current,
1455 Fallback,
1458 Unreadable,
1461}
1462
1463impl SavedStatus {
1464 pub const fn as_octet(self) -> u8 {
1466 match self {
1467 Self::None => ids::saved::NONE,
1468 Self::Current => ids::saved::CURRENT,
1469 Self::Fallback => ids::saved::FALLBACK,
1470 Self::Unreadable => ids::saved::UNREADABLE,
1471 }
1472 }
1473}
1474
1475#[derive(Clone)]
1485struct SavedState {
1486 settings: RadioSettings,
1487 duty_limit: u16,
1488 name: [u8; MAX_DEVICE_NAME_LEN],
1489 name_len: usize,
1490 dev_key: Option<[u8; items::PUBLIC_KEY_LEN]>,
1500 dev_channel_keys: ChannelKeyTable,
1501 dev_peers: DevPeerTable,
1502 repeater_enabled: bool,
1503 repeater_regions: RepeaterRegions,
1504 repeater_default_region: Option<[u8; REGION_CODE_LEN]>,
1505 repeater_min_rssi: Option<i16>,
1506 repeater_min_snr: Option<i8>,
1507 ident_role: Option<u8>,
1508 ident_mobile: bool,
1509 dev_discoverable: bool,
1510 advert_interval_s: u32,
1511 beacon_interval_s: u32,
1512 startup_beacon: bool,
1513 tz_offset_min: i16,
1514 gnss_enabled: bool,
1515 gnss_ident_update: bool,
1516 gnss_ident_precision: u8,
1517 gnss_time_trust: bool,
1518}
1519
1520impl SavedState {
1521 fn capture(
1523 device: &DeviceDomain,
1524 duty_limit: u16,
1525 dev_key: Option<[u8; items::PUBLIC_KEY_LEN]>,
1526 ) -> Self {
1527 Self {
1528 settings: device.settings,
1529 duty_limit,
1530 name: device.name,
1531 name_len: device.name_len,
1532 dev_key,
1533 dev_channel_keys: device.channel_keys,
1534 dev_peers: device.peers,
1535 repeater_enabled: device.repeater_enabled,
1536 repeater_regions: device.repeater_regions,
1537 repeater_default_region: device.repeater_default_region,
1538 repeater_min_rssi: device.repeater_min_rssi,
1539 repeater_min_snr: device.repeater_min_snr,
1540 ident_role: device.ident_role,
1541 ident_mobile: device.ident_mobile,
1542 dev_discoverable: device.dev_discoverable,
1543 advert_interval_s: device.advert_interval_s,
1544 beacon_interval_s: device.beacon_interval_s,
1545 startup_beacon: device.startup_beacon,
1546 tz_offset_min: device.tz_offset_min,
1547 gnss_enabled: device.gnss_enabled,
1548 gnss_ident_update: device.gnss_ident_update,
1549 gnss_ident_precision: device.gnss_ident_precision,
1550 gnss_time_trust: device.gnss_time_trust,
1551 }
1552 }
1553
1554 fn defaults(config: &SessionConfig) -> Self {
1559 let mut settings = config.defaults;
1560 settings.enabled = false;
1561 let mut name = [0u8; MAX_DEVICE_NAME_LEN];
1562 let name_len = config.default_device_name.len();
1563 name[..name_len].copy_from_slice(config.default_device_name.as_bytes());
1564 Self {
1565 settings,
1566 duty_limit: config.default_duty_limit,
1567 name,
1568 name_len,
1569 dev_key: None,
1570 dev_channel_keys: ChannelKeyTable::default(),
1571 dev_peers: DevPeerTable::default(),
1572 repeater_enabled: false,
1573 repeater_regions: RepeaterRegions::default(),
1574 repeater_default_region: None,
1575 repeater_min_rssi: None,
1576 repeater_min_snr: None,
1577 ident_role: None,
1578 ident_mobile: false,
1579 dev_discoverable: true,
1580 advert_interval_s: DEFAULT_ADVERT_INTERVAL_S,
1581 beacon_interval_s: DEFAULT_BEACON_INTERVAL_S,
1582 startup_beacon: true,
1583 tz_offset_min: 0,
1584 gnss_enabled: config.gnss.is_some_and(|gnss| gnss.default_enabled),
1589 gnss_ident_update: false,
1590 gnss_ident_precision: DEFAULT_IDENT_PRECISION,
1591 gnss_time_trust: true,
1592 }
1593 }
1594
1595 fn encode(&self, out: &mut [u8]) -> Option<usize> {
1604 let (format, rest) = out.split_first_mut()?;
1605 *format = SNAPSHOT_FORMAT;
1606 let mut encoder = OptionEncoder::new(rest);
1607 for entry in SAVED_SCHEMA {
1608 self.encode_into(&mut encoder, entry.number).ok()?;
1609 }
1610 Some(1 + encoder.finish())
1611 }
1612
1613 fn encode_into(&self, encoder: &mut OptionEncoder<'_>, number: u16) -> Result<(), EncodeError> {
1616 match u32::from(number) {
1617 prop::PHY_ENABLED => encoder.put(number, &[self.settings.enabled as u8]),
1618 prop::PHY_FREQ => encoder.put(number, &self.settings.freq_khz.to_le_bytes()),
1619 prop::PHY_TX_POWER => encoder.put(number, &[self.settings.tx_power_dbm as u8]),
1620 prop::PHY_LORA_BW => encoder.put(number, &self.settings.bw_hz.to_le_bytes()),
1621 prop::PHY_LORA_SF => encoder.put(number, &[self.settings.sf]),
1622 prop::PHY_LORA_CR => encoder.put(number, &[self.settings.cr_denom]),
1623 prop::DEV_KEY => match &self.dev_key {
1627 Some(key) => encoder.put(number, key),
1628 None => Ok(()),
1629 },
1630 prop::DEV_CHANNEL_KEYS => {
1631 for entry in self.dev_channel_keys.iter() {
1632 encoder.put(number, &entry.key)?;
1633 }
1634 Ok(())
1635 }
1636 prop::DEV_PEERS => {
1637 for public_key in self.dev_peers.iter() {
1638 encoder.put(number, public_key)?;
1639 }
1640 Ok(())
1641 }
1642 prop::DEV_NAME => encoder.put(number, &self.name[..self.name_len]),
1643 prop::MAC_REPEATER_ENABLED => encoder.put(number, &[self.repeater_enabled as u8]),
1644 prop::IDENT_ROLE => match self.ident_role {
1645 Some(role) => encoder.put(number, &[role]),
1646 None => Ok(()),
1647 },
1648 prop::IDENT_MOBILE => encoder.put(number, &[self.ident_mobile as u8]),
1649 prop::MAC_REPEATER_REGIONS => match self.repeater_regions.is_empty() {
1653 true => Ok(()),
1654 false => encoder.put(number, self.repeater_regions.as_slice()),
1655 },
1656 prop::MAC_REPEATER_DEFAULT_REGION => match &self.repeater_default_region {
1657 Some(code) => encoder.put(number, code),
1658 None => Ok(()),
1659 },
1660 prop::MAC_REPEATER_MIN_RSSI => match self.repeater_min_rssi {
1661 Some(rssi) => encoder.put(number, &rssi.to_le_bytes()),
1662 None => Ok(()),
1663 },
1664 prop::MAC_REPEATER_MIN_SNR => match self.repeater_min_snr {
1665 Some(snr) => encoder.put(number, &[snr as u8]),
1666 None => Ok(()),
1667 },
1668 prop::DEV_DISCOVERABLE => encoder.put(number, &[self.dev_discoverable as u8]),
1669 prop::ADVERT_INTERVAL => encoder.put(number, &self.advert_interval_s.to_le_bytes()),
1670 prop::BEACON_INTERVAL => encoder.put(number, &self.beacon_interval_s.to_le_bytes()),
1671 prop::STARTUP_BEACON => encoder.put(number, &[self.startup_beacon as u8]),
1672 prop::GNSS_ENABLED => encoder.put(number, &[self.gnss_enabled as u8]),
1673 prop::PHY_DUTY_LIMIT => encoder.put(number, &self.duty_limit.to_le_bytes()),
1674 prop::TZ_OFFSET => encoder.put(number, &self.tz_offset_min.to_le_bytes()),
1675 prop::GNSS_IDENT_UPDATE => encoder.put(number, &[self.gnss_ident_update as u8]),
1676 prop::GNSS_IDENT_PRECISION => encoder.put(number, &[self.gnss_ident_precision]),
1677 prop::GNSS_TIME_TRUST => encoder.put(number, &[self.gnss_time_trust as u8]),
1678 _ => unreachable!("SAVED_SCHEMA row without an encoder arm"),
1679 }
1680 }
1681
1682 fn decode(config: &SessionConfig, bytes: &[u8]) -> Result<Self, SnapshotError> {
1701 let (format, options) = bytes.split_first().ok_or(SnapshotError::Malformed)?;
1702 if *format != SNAPSHOT_FORMAT {
1703 return Err(SnapshotError::UnknownFormat);
1704 }
1705 let mut state = Self::defaults(config);
1706 let mut seen: u32 = 0;
1707 for item in OptionDecoder::new(options) {
1708 let (number, value) = item.map_err(|_| SnapshotError::Malformed)?;
1709 let Some(index) = SAVED_SCHEMA.iter().position(|entry| entry.number == number) else {
1710 continue;
1711 };
1712 if !SAVED_SCHEMA[index].repeatable {
1713 let bit = 1u32 << index;
1714 if seen & bit != 0 {
1715 return Err(SnapshotError::Malformed);
1716 }
1717 seen |= bit;
1718 }
1719 state.absorb_option(config, number, value)?;
1720 }
1721 Ok(state)
1722 }
1723
1724 fn absorb_option(
1726 &mut self,
1727 config: &SessionConfig,
1728 number: u16,
1729 value: &[u8],
1730 ) -> Result<(), SnapshotError> {
1731 let invalid = |_| SnapshotError::InvalidValue;
1732 match u32::from(number) {
1733 prop::PHY_ENABLED => self.settings.enabled = parse_bool(value).map_err(invalid)?,
1734 prop::PHY_FREQ => {
1735 self.settings.freq_khz = validate_freq_khz(config, value).map_err(invalid)?
1736 }
1737 prop::PHY_TX_POWER => {
1741 self.settings.tx_power_dbm = clamp_tx_power(config, value).map_err(invalid)?
1742 }
1743 prop::PHY_LORA_BW => self.settings.bw_hz = validate_bw_hz(value).map_err(invalid)?,
1744 prop::PHY_LORA_SF => self.settings.sf = validate_sf(value).map_err(invalid)?,
1745 prop::PHY_LORA_CR => self.settings.cr_denom = validate_cr(value).map_err(invalid)?,
1746 prop::DEV_KEY => {
1747 self.dev_key = Some(value.try_into().map_err(|_| SnapshotError::InvalidValue)?);
1748 }
1749 prop::DEV_CHANNEL_KEYS => {
1750 let key = channel_key_item(value)?;
1751 self.dev_channel_keys
1752 .insert(ChannelKeyEntry {
1753 key,
1754 id: [0; items::CHANNEL_ID_LEN],
1755 })
1756 .map_err(invalid)?;
1757 }
1758 prop::DEV_PEERS => {
1759 let public_key: [u8; items::PUBLIC_KEY_LEN] =
1760 value.try_into().map_err(|_| SnapshotError::InvalidValue)?;
1761 self.dev_peers.insert(public_key).map_err(invalid)?;
1762 }
1763 prop::DEV_NAME => {
1764 if !valid_device_name(value) {
1765 return Err(SnapshotError::InvalidValue);
1766 }
1767 self.name = [0; MAX_DEVICE_NAME_LEN];
1768 self.name[..value.len()].copy_from_slice(value);
1769 self.name_len = value.len();
1770 }
1771 prop::MAC_REPEATER_ENABLED => {
1772 self.repeater_enabled = parse_bool(value).map_err(invalid)?
1773 }
1774 prop::IDENT_ROLE => self.ident_role = Some(parse_u8(value).map_err(invalid)?),
1775 prop::IDENT_MOBILE => self.ident_mobile = parse_bool(value).map_err(invalid)?,
1776 prop::MAC_REPEATER_REGIONS => {
1777 self.repeater_regions = RepeaterRegions::parse(value).map_err(invalid)?
1778 }
1779 prop::MAC_REPEATER_DEFAULT_REGION => {
1780 self.repeater_default_region = parse_region_code(value).map_err(invalid)?
1781 }
1782 prop::MAC_REPEATER_MIN_RSSI => {
1783 self.repeater_min_rssi = Some(parse_i16(value).map_err(invalid)?)
1784 }
1785 prop::MAC_REPEATER_MIN_SNR => {
1786 self.repeater_min_snr = Some(parse_i8(value).map_err(invalid)?)
1787 }
1788 prop::DEV_DISCOVERABLE => self.dev_discoverable = parse_bool(value).map_err(invalid)?,
1789 prop::ADVERT_INTERVAL => {
1790 self.advert_interval_s = validate_announce_interval(value).map_err(invalid)?
1791 }
1792 prop::BEACON_INTERVAL => {
1793 self.beacon_interval_s = validate_announce_interval(value).map_err(invalid)?
1794 }
1795 prop::STARTUP_BEACON => self.startup_beacon = parse_bool(value).map_err(invalid)?,
1796 prop::GNSS_ENABLED => self.gnss_enabled = parse_bool(value).map_err(invalid)?,
1797 prop::PHY_DUTY_LIMIT => self.duty_limit = parse_u16(value).map_err(invalid)?,
1798 prop::TZ_OFFSET => self.tz_offset_min = validate_tz_offset(value).map_err(invalid)?,
1799 prop::GNSS_IDENT_UPDATE => {
1800 self.gnss_ident_update = parse_bool(value).map_err(invalid)?
1801 }
1802 prop::GNSS_IDENT_PRECISION => {
1803 self.gnss_ident_precision = validate_ident_precision(value).map_err(invalid)?
1804 }
1805 prop::GNSS_TIME_TRUST => self.gnss_time_trust = parse_bool(value).map_err(invalid)?,
1806 _ => unreachable!("SAVED_SCHEMA row without a decoder arm"),
1807 }
1808 Ok(())
1809 }
1810
1811 fn derive_channel_ids<A: AesProvider, S: Sha256Provider>(
1816 &mut self,
1817 engine: &CryptoEngine<A, S>,
1818 ) {
1819 let table = &mut self.dev_channel_keys;
1820 for entry in table.entries[..table.len].iter_mut().flatten() {
1821 entry.id = engine.derive_channel_id(&ChannelKey(entry.key)).0;
1822 }
1823 }
1824}
1825
1826fn channel_key_item(value: &[u8]) -> Result<[u8; items::CHANNEL_KEY_LEN], SnapshotError> {
1828 value.try_into().map_err(|_| SnapshotError::InvalidValue)
1829}
1830
1831struct SessionState<const TX: usize> {
1835 promiscuous: bool,
1837 pending: Deque<PendingTx, TX>,
1841 drain: Option<DrainState>,
1845 pending_identity: Option<PendingIdentity>,
1850}
1851
1852impl<const TX: usize> Default for SessionState<TX> {
1853 fn default() -> Self {
1854 Self {
1855 promiscuous: false,
1856 pending: Deque::new(),
1857 drain: None,
1858 pending_identity: None,
1859 }
1860 }
1861}
1862
1863struct PendingIdentity {
1864 tid: u8,
1865 secret: Option<[u8; PRIVATE_KEY_LEN]>,
1867}
1868
1869struct DrainState {
1870 tid: u8,
1871 remaining: usize,
1872}
1873
1874pub struct Session<A: AesProvider, S: Sha256Provider, const TX: usize = 1> {
1875 config: SessionConfig,
1876 engine: CryptoEngine<A, S>,
1880 device: DeviceDomain,
1881 host: HostDomain,
1882 session: SessionState<TX>,
1883 attached: bool,
1887 link_secure: bool,
1891 saved: Option<SavedState>,
1896 snapshot_rejected: bool,
1901 dev_key: Option<[u8; items::PUBLIC_KEY_LEN]>,
1903 dev_key_persisted: Option<[u8; items::PUBLIC_KEY_LEN]>,
1910 last_status: Status,
1911 dev_domain_version: u32,
1919 alert: AlertState,
1929 alert_deadline_ms: Option<u64>,
1930 scratch: [u8; SCRATCH],
1931}
1932
1933impl<A: AesProvider, S: Sha256Provider, const TX: usize> Session<A, S, TX> {
1934 pub fn new(config: SessionConfig, boot_status: Status, engine: CryptoEngine<A, S>) -> Self {
1937 debug_assert!(usize::from(config.mtu) <= MAX_MTU);
1938 debug_assert!(valid_device_name(config.default_device_name.as_bytes()));
1939 Self {
1940 config,
1941 engine,
1942 device: DeviceDomain::post_reset(&config),
1943 host: HostDomain::default(),
1944 session: SessionState::default(),
1945 attached: false,
1946 link_secure: false,
1947 saved: None,
1948 snapshot_rejected: false,
1949 dev_key: None,
1950 dev_key_persisted: None,
1951 last_status: boot_status,
1952 dev_domain_version: 0,
1953 alert: AlertState::None,
1954 alert_deadline_ms: None,
1955 scratch: [0; SCRATCH],
1956 }
1957 }
1958
1959 pub fn settings(&self) -> RadioSettings {
1961 self.device.settings
1962 }
1963
1964 pub fn device_name(&self) -> &str {
1966 core::str::from_utf8(&self.device.name[..self.device.name_len])
1967 .expect("validated device name")
1968 }
1969
1970 pub fn tx_data(&self) -> &[u8] {
1972 self.session
1973 .pending
1974 .front()
1975 .map(|pending| pending.data.as_slice())
1976 .unwrap_or_default()
1977 }
1978
1979 pub fn tx_power(&self) -> TxPower {
1981 self.session
1982 .pending
1983 .front()
1984 .map(|pending| pending.power)
1985 .unwrap_or(TxPower::Default)
1986 }
1987
1988 pub fn max_tx_power_dbm(&self) -> i8 {
1991 self.config.max_tx_power_dbm
1992 }
1993
1994 pub fn tx_nocca(&self) -> bool {
1997 self.session
1998 .pending
1999 .front()
2000 .map(|pending| pending.nocca)
2001 .unwrap_or(false)
2002 }
2003
2004 pub fn has_pending_tx(&self) -> bool {
2006 !self.session.pending.is_empty()
2007 }
2008
2009 pub fn queued_frame_count(&self) -> usize {
2011 self.host.queue.len
2012 }
2013
2014 pub fn dev_domain_version(&self) -> u32 {
2025 self.dev_domain_version
2026 }
2027
2028 fn bump_dev_domain(&mut self) {
2031 self.dev_domain_version = self.dev_domain_version.wrapping_add(1);
2032 }
2033
2034 pub fn dev_channel_keys(&self) -> impl Iterator<Item = [u8; items::CHANNEL_KEY_LEN]> + '_ {
2038 self.device.channel_keys.iter().map(|entry| entry.key)
2039 }
2040
2041 pub fn dev_peers(&self) -> impl Iterator<Item = [u8; items::PUBLIC_KEY_LEN]> + '_ {
2044 self.device.peers.iter().copied()
2045 }
2046
2047 pub fn repeater_enabled(&self) -> bool {
2053 self.device.repeater_enabled
2054 }
2055
2056 pub fn repeater_regions(&self) -> &[u8] {
2062 self.device.repeater_regions.as_slice()
2063 }
2064
2065 pub fn repeater_default_region(&self) -> Option<[u8; REGION_CODE_LEN]> {
2068 self.device.repeater_default_region
2069 }
2070
2071 pub fn repeater_min_rssi(&self) -> Option<i16> {
2074 self.device.repeater_min_rssi
2075 }
2076
2077 pub fn repeater_min_snr(&self) -> Option<i8> {
2080 self.device.repeater_min_snr
2081 }
2082
2083 pub fn dev_key(&self) -> Option<&[u8; items::PUBLIC_KEY_LEN]> {
2087 self.dev_key.as_ref()
2088 }
2089
2090 pub fn reset(&mut self, reason: Status, emit: &mut impl FnMut(&[u8])) -> Effect {
2102 self.device = DeviceDomain::post_reset(&self.config);
2103 self.dev_key = self.dev_key_persisted;
2107 self.host.reset(None);
2108 if self.saved.is_some() {
2109 self.apply_saved_device();
2110 }
2111 self.session = SessionState::default();
2112 self.bump_dev_domain();
2115 self.send_status(TID_UNSOLICITED, reason, emit);
2116 self.apply_radio()
2117 }
2118
2119 fn apply_radio(&self) -> Effect {
2123 let settings = self.device.settings;
2124 self.config
2125 .duty
2126 .set_phy(settings.sf, settings.bw_hz, settings.cr_denom);
2127 Effect::ApplyRadio(settings)
2128 }
2129
2130 fn apply_saved_device(&mut self) {
2140 let saved = self.saved.as_ref().expect("caller checked saved");
2141 for phase in ApplyPhase::ORDER {
2142 for entry in SAVED_SCHEMA.iter().filter(|entry| entry.phase == phase) {
2143 match u32::from(entry.number) {
2144 prop::PHY_ENABLED => {
2153 let identity_matches =
2154 saved.dev_key.is_none_or(|key| Some(key) == self.dev_key);
2155 self.device.settings.enabled = saved.settings.enabled && identity_matches;
2156 }
2157 prop::DEV_KEY => {}
2160 prop::PHY_FREQ => self.device.settings.freq_khz = saved.settings.freq_khz,
2161 prop::PHY_TX_POWER => {
2162 self.device.settings.tx_power_dbm = saved.settings.tx_power_dbm
2163 }
2164 prop::PHY_LORA_BW => self.device.settings.bw_hz = saved.settings.bw_hz,
2165 prop::PHY_LORA_SF => self.device.settings.sf = saved.settings.sf,
2166 prop::PHY_LORA_CR => self.device.settings.cr_denom = saved.settings.cr_denom,
2167 prop::DEV_CHANNEL_KEYS => self.device.channel_keys = saved.dev_channel_keys,
2168 prop::DEV_PEERS => self.device.peers = saved.dev_peers,
2169 prop::DEV_NAME => {
2170 self.device.name = saved.name;
2171 self.device.name_len = saved.name_len;
2172 }
2173 prop::MAC_REPEATER_ENABLED => {
2174 self.device.repeater_enabled = saved.repeater_enabled
2175 }
2176 prop::IDENT_ROLE => self.device.ident_role = saved.ident_role,
2177 prop::IDENT_MOBILE => self.device.ident_mobile = saved.ident_mobile,
2178 prop::MAC_REPEATER_REGIONS => {
2179 self.device.repeater_regions = saved.repeater_regions
2180 }
2181 prop::MAC_REPEATER_DEFAULT_REGION => {
2182 self.device.repeater_default_region = saved.repeater_default_region
2183 }
2184 prop::MAC_REPEATER_MIN_RSSI => {
2185 self.device.repeater_min_rssi = saved.repeater_min_rssi
2186 }
2187 prop::MAC_REPEATER_MIN_SNR => {
2188 self.device.repeater_min_snr = saved.repeater_min_snr
2189 }
2190 prop::DEV_DISCOVERABLE => self.device.dev_discoverable = saved.dev_discoverable,
2191 prop::ADVERT_INTERVAL => {
2192 self.device.advert_interval_s = saved.advert_interval_s
2193 }
2194 prop::BEACON_INTERVAL => {
2195 self.device.beacon_interval_s = saved.beacon_interval_s
2196 }
2197 prop::STARTUP_BEACON => self.device.startup_beacon = saved.startup_beacon,
2198 prop::GNSS_ENABLED => self.device.gnss_enabled = saved.gnss_enabled,
2203 prop::PHY_DUTY_LIMIT => self.config.duty.set_limit(saved.duty_limit),
2204 prop::TZ_OFFSET => self.device.tz_offset_min = saved.tz_offset_min,
2205 prop::GNSS_IDENT_UPDATE => {
2206 self.device.gnss_ident_update = saved.gnss_ident_update
2207 }
2208 prop::GNSS_IDENT_PRECISION => {
2209 self.device.gnss_ident_precision = saved.gnss_ident_precision
2210 }
2211 prop::GNSS_TIME_TRUST => self.device.gnss_time_trust = saved.gnss_time_trust,
2212 _ => unreachable!("SAVED_SCHEMA row without an apply arm"),
2213 }
2214 }
2215 }
2216 self.bump_dev_domain();
2217 }
2218
2219 pub fn attach(&mut self, link_secure: bool) {
2231 self.session = SessionState::default();
2232 self.attached = true;
2233 self.link_secure = link_secure;
2234 }
2235
2236 pub fn detach(&mut self) {
2241 self.session = SessionState::default();
2242 self.attached = false;
2243 self.link_secure = false;
2244 }
2245
2246 pub fn handle_frame(
2248 &mut self,
2249 bytes: &[u8],
2250 now_ms: u64,
2251 emit: &mut impl FnMut(&[u8]),
2252 ) -> Option<Effect> {
2253 let received = Frame::parse(bytes).ok()?;
2256 let tid = received.header.tid();
2257 match received.command() {
2258 Some(Cmd::Nop) => {
2259 self.complete(tid, Status::OK, emit);
2260 None
2261 }
2262 Some(Cmd::Reset) => Some(self.reset(Status::RESET_SOFTWARE, emit)),
2263 Some(Cmd::PropGet) => match PropPayload::parse(received.payload) {
2264 Ok(payload) => self.prop_get(tid, payload.key, now_ms, emit),
2265 Err(_) => {
2266 self.complete(tid, Status::PARSE_ERROR, emit);
2267 None
2268 }
2269 },
2270 Some(Cmd::PropSet) => match PropPayload::parse(received.payload) {
2271 Ok(payload) => self.prop_set(tid, payload.key, payload.value, now_ms, emit),
2272 Err(_) => {
2273 self.complete(tid, Status::PARSE_ERROR, emit);
2274 None
2275 }
2276 },
2277 Some(Cmd::StrSend) => match StreamPayload::parse(received.payload) {
2278 Ok(payload) => self.str_send(tid, &payload, now_ms, emit),
2279 Err(_) => {
2280 self.complete(tid, Status::PARSE_ERROR, emit);
2281 None
2282 }
2283 },
2284 Some(Cmd::PropInsert) => {
2285 match PropPayload::parse(received.payload) {
2286 Ok(payload) => self.prop_insert(tid, payload.key, payload.value, emit),
2287 Err(_) => self.complete(tid, Status::PARSE_ERROR, emit),
2288 }
2289 None
2290 }
2291 Some(Cmd::PropRemove) => {
2292 match PropPayload::parse(received.payload) {
2293 Ok(payload) => self.prop_remove(tid, payload.key, payload.value, emit),
2294 Err(_) => self.complete(tid, Status::PARSE_ERROR, emit),
2295 }
2296 None
2297 }
2298 Some(Cmd::QueueDrain) => {
2302 if self.session.drain.is_some() {
2303 self.complete(tid, Status::BUSY, emit);
2304 return None;
2305 }
2306 if self.host.queue.len == 0 {
2307 self.complete(tid, Status::OK, emit);
2308 return None;
2309 }
2310 self.session.drain = Some(DrainState {
2311 tid,
2312 remaining: self.host.queue.len,
2313 });
2314 Some(Effect::DrainQueue)
2315 }
2316 Some(Cmd::Save) => Some(Effect::SaveSnapshot { tid }),
2320 Some(Cmd::Restore) => {
2330 if self.saved.is_none() {
2331 self.complete(tid, Status::INVALID_STATE, emit);
2332 return None;
2333 }
2334 self.apply_saved_device();
2335 self.session = SessionState::default();
2336 self.send_status(TID_UNSOLICITED, Status::RESET_RESTORED, emit);
2337 Some(self.apply_radio())
2338 }
2339 Some(Cmd::Clear) => Some(Effect::ClearSaved { tid }),
2344 Some(Cmd::FactoryReset) => Some(Effect::FactoryReset),
2351 Some(Cmd::PropIs | Cmd::StrRecv | Cmd::PropInserted | Cmd::PropRemoved) => {
2353 self.complete(tid, Status::INVALID_COMMAND, emit);
2354 None
2355 }
2356 None => {
2357 self.complete(tid, Status::INVALID_COMMAND, emit);
2358 None
2359 }
2360 }
2361 }
2362
2363 pub fn on_radio_rx(
2372 &mut self,
2373 data: &[u8],
2374 rssi_dbm: i16,
2375 snr_cb: i16,
2376 lqi: Option<core::num::NonZeroU8>,
2377 now_ms: u64,
2378 emit: &mut impl FnMut(&[u8]),
2379 ) -> Option<Effect> {
2380 if !self.device.settings.enabled || data.len() > usize::from(self.config.mtu) {
2381 return None;
2382 }
2383 if !self.attached {
2384 if !self.host.accepts_frame(data) {
2385 return None;
2386 }
2387 return match self.evaluate_detached_rx(data, now_ms) {
2388 SecureRx::Duplicate { ack, identity } => {
2389 let original =
2394 identity.and_then(|identity| self.host.queue.seq_for_identity(&identity));
2395 ack.and_then(|plan| self.stage_ack(plan, original, now_ms))
2396 }
2397 verdict => {
2398 let (ack, identity) = match verdict {
2399 SecureRx::New { ack, identity } => (ack, identity),
2400 _ => (None, None),
2401 };
2402 let seq = self
2408 .host
2409 .queue
2410 .push(data, rssi_dbm, snr_cb, lqi, now_ms, identity);
2411 ack.and_then(|plan| self.stage_ack(plan, Some(seq), now_ms))
2412 }
2413 };
2414 }
2415 if !self.session.promiscuous && !self.host.accepts_live_frame(data) {
2416 return None;
2417 }
2418 let mut rx_meta = [0u8; RxMeta::WIRE_LEN];
2419 let meta_len = RxMeta {
2420 rssi_dbm: Some(rssi_dbm),
2421 lqi,
2422 snr_cb: Some(snr_cb),
2423 }
2424 .encode(&mut rx_meta)
2425 .expect("buffer sized with WIRE_LEN");
2426 if let Ok(len) = frame::str_recv(
2427 &mut self.scratch,
2428 stream::PHY_RAW,
2429 data,
2430 &rx_meta[..meta_len],
2431 ) {
2432 emit(&self.scratch[..len]);
2433 }
2434 None
2435 }
2436
2437 fn evaluate_detached_rx(&mut self, data: &[u8], now_ms: u64) -> SecureRx {
2442 let Ok(header) = PacketHeader::parse(data) else {
2443 return SecureRx::Plain;
2444 };
2445 let Some(host_key) = &self.host.key else {
2446 return SecureRx::Plain;
2447 };
2448 let host_hint = NodeHint([host_key[0], host_key[1], host_key[2]]);
2449 let packet_type = header.fcf.packet_type();
2450 let wants_ack = packet_type.ack_requested();
2451
2452 let scratch = &mut self.scratch[..data.len()];
2453 scratch.copy_from_slice(data);
2454
2455 let (keys, peer_index) = match packet_type {
2457 PacketType::Unicast | PacketType::UnicastAckReq => {
2458 if header.dst != Some(host_hint) {
2459 return SecureRx::Plain;
2460 }
2461 let Some(index) = self.host.peer_keys.resolve_source(&header.source, data) else {
2462 return SecureRx::Plain;
2463 };
2464 let entry = &self.host.peer_keys.entries[index]
2465 .as_ref()
2466 .expect("resolved index is populated")
2467 .entry;
2468 (
2469 PairwiseKeys {
2470 k_enc: entry.k_enc,
2471 k_mic: entry.k_mic,
2472 },
2473 index,
2474 )
2475 }
2476 PacketType::BlindUnicast | PacketType::BlindUnicastAckReq => {
2477 let Some(channel) = header.channel else {
2481 return SecureRx::Plain;
2482 };
2483 let Some(channel_key) = self
2484 .host
2485 .channel_keys
2486 .iter()
2487 .find(|candidate| candidate.id == channel.0)
2488 .map(|candidate| candidate.key)
2489 else {
2490 return SecureRx::Plain;
2491 };
2492 let channel_keys = self.engine.derive_channel_keys(&ChannelKey(channel_key));
2493 let Ok((dst, source)) =
2494 self.engine
2495 .decrypt_blind_addr(scratch, &header, &channel_keys)
2496 else {
2497 return SecureRx::Plain;
2498 };
2499 if dst != host_hint {
2500 return SecureRx::Plain;
2501 }
2502 let Some(index) = self.host.peer_keys.resolve_source(&source, scratch) else {
2504 return SecureRx::Plain;
2505 };
2506 let entry = &self.host.peer_keys.entries[index]
2507 .as_ref()
2508 .expect("resolved index is populated")
2509 .entry;
2510 let pairwise = PairwiseKeys {
2511 k_enc: entry.k_enc,
2512 k_mic: entry.k_mic,
2513 };
2514 (
2515 self.engine.derive_blind_keys(&pairwise, &channel_keys),
2516 index,
2517 )
2518 }
2519 PacketType::Multicast => {
2525 let Some(channel) = header.channel else {
2526 return SecureRx::Plain;
2527 };
2528 let Some(channel_key) = self
2529 .host
2530 .channel_keys
2531 .iter()
2532 .find(|candidate| candidate.id == channel.0)
2533 .map(|candidate| candidate.key)
2534 else {
2535 return SecureRx::Plain;
2536 };
2537 let derived = self.engine.derive_channel_keys(&ChannelKey(channel_key));
2538 let channel_pairwise = PairwiseKeys {
2539 k_enc: derived.k_enc,
2540 k_mic: derived.k_mic,
2541 };
2542 if self
2543 .engine
2544 .open_packet(scratch, &header, &channel_pairwise)
2545 .is_err()
2546 {
2547 return SecureRx::Plain;
2548 }
2549 let Some(sec_info) = header.sec_info else {
2550 return SecureRx::Plain;
2551 };
2552 let identity =
2553 RxIdentity::new(sec_info.frame_counter, &data[header.mic_range.clone()]);
2554 let Some(identity) = identity else {
2555 return SecureRx::Plain;
2556 };
2557 return if self.host.queue.seq_for_identity(&identity).is_some() {
2562 SecureRx::Duplicate {
2563 ack: None,
2564 identity: Some(identity),
2565 }
2566 } else {
2567 SecureRx::New {
2568 ack: None,
2569 identity: Some(identity),
2570 }
2571 };
2572 }
2573 _ => return SecureRx::Plain,
2574 };
2575
2576 let Ok(body_range) = self.engine.open_packet(scratch, &header, &keys) else {
2578 return SecureRx::Plain;
2579 };
2580 let Some(sec_info) = header.sec_info else {
2581 return SecureRx::Plain;
2582 };
2583 let counter = sec_info.frame_counter;
2584 let mic = &data[header.mic_range.clone()];
2585
2586 let plan = wants_ack.then(|| {
2590 let mut cmac = self.engine.cmac_state(&keys.k_mic);
2591 umsh_core::feed_aad(&header, scratch, |chunk| cmac.update(chunk));
2592 cmac.update(&scratch[body_range.clone()]);
2593 let full_mac = cmac.finalize();
2594 AckPlan {
2595 trailer: self.engine.compute_ack_trailer(&full_mac, &keys.k_enc),
2596 flood_hops: header.flood_hops.map(|hops| hops.accumulated()),
2602 }
2603 });
2604 let identity = RxIdentity::new(counter, mic);
2605
2606 let window = &mut self.host.peer_keys.entries[peer_index]
2607 .as_mut()
2608 .expect("resolved index is populated")
2609 .window;
2610 match window.check(counter, mic, now_ms) {
2611 ReplayVerdict::Accept => {
2612 window.accept(counter, mic, now_ms);
2613 SecureRx::New {
2614 ack: plan,
2615 identity,
2616 }
2617 }
2618 ReplayVerdict::Replay => {
2619 let ack = window
2623 .is_acknowledgeable_duplicate(counter, mic, now_ms)
2624 .then_some(())
2625 .and(plan);
2626 SecureRx::Duplicate { ack, identity }
2627 }
2628 ReplayVerdict::OutOfWindow | ReplayVerdict::Stale => SecureRx::Plain,
2632 }
2633 }
2634
2635 fn stage_ack(&mut self, plan: AckPlan, ack_for: Option<u16>, now_ms: u64) -> Option<Effect> {
2642 if !self.host.auto_ack || !self.session.pending.is_empty() {
2643 return None;
2644 }
2645 let mut buf = [0u8; 24];
2646 let mut builder = PacketBuilder::new(&mut buf).mac_ack(plan.trailer);
2647 if let Some(hops) = plan.flood_hops {
2648 builder = builder.flood_hops(hops.clamp(1, 15));
2652 }
2653 let frame_len = builder.build().ok()?.len();
2654 let airtime_ms = lora_airtime_ms(
2655 self.device.settings.sf,
2656 self.device.settings.bw_hz,
2657 self.device.settings.cr_denom,
2658 frame_len,
2659 );
2660 if self.config.duty.would_exceed(now_ms, airtime_ms) {
2661 return None;
2662 }
2663 let mut data = HeaplessVec::new();
2664 data.extend_from_slice(&buf[..frame_len]).ok()?;
2665 self.session
2666 .pending
2667 .push_back(PendingTx {
2668 data,
2669 tid: TID_UNSOLICITED,
2670 airtime_ms,
2671 power: TxPower::Default,
2672 autonomous: true,
2673 ack_for,
2674 nocca: true,
2679 })
2680 .ok()?;
2681 Some(Effect::StartTransmit)
2682 }
2683
2684 pub fn on_tx_result(
2687 &mut self,
2688 outcome: TxOutcome,
2689 now_ms: u64,
2690 emit: &mut impl FnMut(&[u8]),
2691 ) -> Option<Effect> {
2692 let Some(pending) = self.session.pending.pop_front() else {
2693 return None;
2694 };
2695 match outcome {
2696 TxOutcome::Sent => {
2697 self.config.duty.record(now_ms, pending.airtime_ms);
2698 if pending.autonomous {
2699 if let Some(seq) = pending.ack_for {
2706 self.host.queue.mark_acked(seq);
2707 }
2708 } else {
2709 self.complete(pending.tid, Status::OK, emit);
2710 }
2711 }
2712 TxOutcome::ChannelBusy if !pending.autonomous => {
2716 self.complete(pending.tid, Status::CCA_FAILURE, emit);
2717 }
2718 TxOutcome::Failed if !pending.autonomous => {
2719 self.complete(pending.tid, Status::FAILURE, emit);
2720 }
2721 TxOutcome::ChannelBusy | TxOutcome::Failed => {}
2722 }
2723 (!self.session.pending.is_empty()).then_some(Effect::StartTransmit)
2724 }
2725
2726 fn prop_get(
2729 &mut self,
2730 tid: u8,
2731 key: u32,
2732 now_ms: u64,
2733 emit: &mut impl FnMut(&[u8]),
2734 ) -> Option<Effect> {
2735 if key == prop::BLE_PAIRING_PIN || key == prop::DEV_PRIVATE_KEY {
2743 self.complete(tid, Status::UNIMPLEMENTED, emit);
2744 return None;
2745 }
2746 if key == prop::PHY_RSSI {
2747 if self.device.settings.enabled {
2748 return Some(Effect::SampleRssi { tid });
2749 }
2750 self.complete(tid, Status::INVALID_STATE, emit);
2751 return None;
2752 }
2753 if key == prop::IDENT {
2762 return Some(Effect::SignIdentity { tid });
2763 }
2764 if key == prop::BATTERY
2769 && let Some(fields) = self.config.battery
2770 {
2771 if fields.any() {
2772 return Some(Effect::SampleBattery { tid });
2773 }
2774 self.send_prop_is(tid, prop::BATTERY, &[], emit);
2775 return None;
2776 }
2777 if key == prop::TIME && self.config.time.is_some() {
2783 return Some(Effect::ReadTime { tid });
2784 }
2785 if gnss::is_positioning_property(key) && self.config.gnss.is_some() {
2787 return Some(Effect::SampleGnss { tid, key });
2788 }
2789 if key == prop::ILLUMINANCE && self.config.illuminance {
2792 return Some(Effect::SampleIlluminance { tid });
2793 }
2794 let mut value = [0u8; PROP_BUF];
2795 match self.encode_prop(key, now_ms, &mut value) {
2796 PropValue::Encoded(len) => self.send_prop_is(tid, key, &value[..len], emit),
2797 PropValue::Unimplemented => self.complete(tid, Status::UNIMPLEMENTED, emit),
2798 PropValue::Unknown => self.complete(tid, Status::PROP_NOT_FOUND, emit),
2799 }
2800 None
2801 }
2802
2803 pub fn respond_rssi(&mut self, tid: u8, rssi: Result<i16, ()>, emit: &mut impl FnMut(&[u8])) {
2807 match rssi {
2808 Ok(dbm) => {
2809 let clamped = dbm.clamp(i16::from(i8::MIN), i16::from(i8::MAX)) as i8;
2810 self.send_prop_is(tid, prop::PHY_RSSI, &[clamped as u8], emit);
2811 }
2812 Err(()) => self.complete(tid, Status::FAILURE, emit),
2813 }
2814 }
2815
2816 pub fn respond_identity_blob(
2822 &mut self,
2823 tid: u8,
2824 blob: Result<&[u8], ()>,
2825 emit: &mut impl FnMut(&[u8]),
2826 ) {
2827 match blob {
2828 Ok(bytes) => self.send_prop_is(tid, prop::IDENT, bytes, emit),
2829 Err(()) => self.complete(tid, Status::FAILURE, emit),
2830 }
2831 }
2832
2833 pub fn ident_role(&self) -> Option<u8> {
2836 self.device.ident_role
2837 }
2838
2839 pub fn ident_mobile(&self) -> bool {
2842 self.device.ident_mobile
2843 }
2844
2845 pub fn dev_discoverable(&self) -> bool {
2848 self.device.dev_discoverable
2849 }
2850
2851 pub fn advert_interval_s(&self) -> u32 {
2854 self.device.advert_interval_s
2855 }
2856
2857 pub fn beacon_interval_s(&self) -> u32 {
2860 self.device.beacon_interval_s
2861 }
2862
2863 pub fn startup_beacon(&self) -> bool {
2865 self.device.startup_beacon
2866 }
2867
2868 pub fn tz_offset_min(&self) -> i16 {
2870 self.device.tz_offset_min
2871 }
2872
2873 pub fn gnss_enabled(&self) -> bool {
2878 self.config.gnss.is_some() && self.device.gnss_enabled
2879 }
2880
2881 pub fn gnss_ident_update(&self) -> bool {
2884 self.config.gnss.is_some() && self.device.gnss_ident_update
2885 }
2886
2887 pub fn gnss_ident_precision(&self) -> u8 {
2890 self.device.gnss_ident_precision
2891 }
2892
2893 pub fn gnss_time_trust(&self) -> bool {
2896 self.device.gnss_time_trust
2897 }
2898
2899 pub fn alert(&self) -> AlertState {
2902 self.alert
2903 }
2904
2905 pub fn alert_deadline_ms(&self) -> Option<u64> {
2915 self.alert_deadline_ms
2916 }
2917
2918 pub fn poll_alert(&mut self, now_ms: u64, emit: &mut impl FnMut(&[u8])) -> Option<Effect> {
2925 match self.alert_deadline_ms {
2926 Some(deadline) if now_ms >= deadline => self.clear_alert(emit),
2927 _ => None,
2928 }
2929 }
2930
2931 pub fn cancel_alert(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<Effect> {
2938 self.clear_alert(emit)
2939 }
2940
2941 pub fn toggle_gnss(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<bool> {
2955 if self.config.gnss.is_none() {
2956 return None;
2957 }
2958 let enabled = !self.device.gnss_enabled;
2959 self.device.gnss_enabled = enabled;
2960 self.bump_dev_domain();
2961 if self.attached {
2962 self.announce_prop_is(prop::GNSS_ENABLED, &[enabled as u8], emit);
2963 }
2964 Some(enabled)
2965 }
2966
2967 fn clear_alert(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<Effect> {
2970 if !self.alert.is_active() {
2971 return None;
2972 }
2973 self.alert = AlertState::None;
2974 self.alert_deadline_ms = None;
2975 if self.attached {
2979 let mut value = [0u8; pui::MAX_LEN];
2980 let len = pui::encode(AlertState::None.code(), &mut value).unwrap_or(0);
2981 self.announce_prop_is(prop::ALERT, &value[..len], emit);
2982 }
2983 Some(Effect::ApplyAlert(AlertState::None))
2984 }
2985
2986 pub fn publish_battery(&mut self, sample: BatteryStatus, emit: &mut impl FnMut(&[u8])) -> bool {
3003 if !self.attached {
3004 return false;
3005 }
3006 let Some(fields) = self.config.battery else {
3007 return false;
3008 };
3009 if !fields.matches(&sample) {
3010 return false;
3011 }
3012 let mut value = [0u8; battery::MAX_ENCODED_LEN];
3013 let Ok(len) = sample.encode(&mut value) else {
3014 return false;
3015 };
3016 self.announce_prop_is(prop::BATTERY, &value[..len], emit)
3017 }
3018
3019 pub fn respond_battery(
3029 &mut self,
3030 tid: u8,
3031 sample: Result<BatteryStatus, ()>,
3032 emit: &mut impl FnMut(&[u8]),
3033 ) {
3034 let fields = self.config.battery.unwrap_or_default();
3035 match sample {
3036 Ok(snapshot) if fields.matches(&snapshot) => {
3037 let mut value = [0u8; battery::MAX_ENCODED_LEN];
3038 match snapshot.encode(&mut value) {
3039 Ok(len) => self.send_prop_is(tid, prop::BATTERY, &value[..len], emit),
3040 Err(_) => self.complete(tid, Status::FAILURE, emit),
3041 }
3042 }
3043 Ok(_) | Err(()) => self.complete(tid, Status::FAILURE, emit),
3044 }
3045 }
3046
3047 pub fn respond_illuminance(
3056 &mut self,
3057 tid: u8,
3058 millilux: Option<u32>,
3059 emit: &mut impl FnMut(&[u8]),
3060 ) {
3061 match millilux {
3062 Some(value) => {
3063 self.send_prop_is(tid, prop::ILLUMINANCE, &value.to_le_bytes(), emit);
3064 }
3065 None => self.send_prop_is(tid, prop::ILLUMINANCE, &[], emit),
3066 }
3067 }
3068
3069 pub fn respond_time(&mut self, tid: u8, epoch: Option<u32>, emit: &mut impl FnMut(&[u8])) {
3078 match epoch {
3079 Some(seconds) => self.send_prop_is(tid, prop::TIME, &seconds.to_le_bytes(), emit),
3080 None => self.send_prop_is(tid, prop::TIME, &[], emit),
3081 }
3082 }
3083
3084 pub fn publish_time(&mut self, epoch: Option<u32>, emit: &mut impl FnMut(&[u8])) -> bool {
3095 if !self.attached || self.config.time.is_none() {
3096 return false;
3097 }
3098 match epoch {
3099 Some(seconds) => self.announce_prop_is(prop::TIME, &seconds.to_le_bytes(), emit),
3100 None => self.announce_prop_is(prop::TIME, &[], emit),
3101 }
3102 }
3103
3104 pub fn respond_gnss(
3113 &mut self,
3114 tid: u8,
3115 key: u32,
3116 sample: Result<GnssSnapshot, ()>,
3117 emit: &mut impl FnMut(&[u8]),
3118 ) {
3119 let mut value = [0u8; gnss::MAX_VALUE_LEN];
3120 match sample.and_then(|snapshot| snapshot.encode(key, &mut value).map_err(|_| ())) {
3121 Ok(len) => self.send_prop_is(tid, key, &value[..len], emit),
3122 Err(()) => self.complete(tid, Status::FAILURE, emit),
3123 }
3124 }
3125
3126 pub fn publish_gnss(
3138 &mut self,
3139 key: u32,
3140 snapshot: &GnssSnapshot,
3141 emit: &mut impl FnMut(&[u8]),
3142 ) -> bool {
3143 if !self.attached || self.config.gnss.is_none() {
3144 return false;
3145 }
3146 let mut value = [0u8; gnss::MAX_VALUE_LEN];
3147 let Ok(len) = snapshot.encode(key, &mut value) else {
3148 return false;
3149 };
3150 self.announce_prop_is(key, &value[..len], emit)
3151 }
3152
3153 pub fn drain_step(&mut self, now_ms: u64, emit: &mut impl FnMut(&[u8])) -> bool {
3159 let Some(drain) = &mut self.session.drain else {
3160 return false;
3161 };
3162 if drain.remaining == 0 {
3163 let tid = drain.tid;
3164 self.session.drain = None;
3165 self.complete(tid, Status::OK, emit);
3166 return false;
3167 }
3168 drain.remaining -= 1;
3169 let Some(entry) = self.host.queue.pop_front() else {
3170 let tid = drain.tid;
3173 self.session.drain = None;
3174 self.complete(tid, Status::OK, emit);
3175 return false;
3176 };
3177 let mut rx_meta = [0u8; BufferedRxMeta::WIRE_LEN];
3178 let meta_len = BufferedRxMeta {
3179 rx: RxMeta {
3180 rssi_dbm: Some(entry.rssi_dbm),
3181 lqi: entry.lqi,
3182 snr_cb: Some(entry.snr_cb),
3183 },
3184 flags: RX_FLAG_BUFFERED | if entry.acked { RX_FLAG_ACKED } else { 0 },
3185 age_s: u32::try_from(now_ms.saturating_sub(entry.rx_time_ms) / 1000)
3186 .unwrap_or(u32::MAX),
3187 }
3188 .encode(&mut rx_meta)
3189 .expect("buffer sized with WIRE_LEN");
3190 if let Ok(len) = frame::str_recv(
3191 &mut self.scratch,
3192 stream::PHY_RAW,
3193 entry.frame(),
3194 &rx_meta[..meta_len],
3195 ) {
3196 emit(&self.scratch[..len]);
3197 }
3198 true
3199 }
3200
3201 pub fn encode_snapshot(&self, out: &mut [u8]) -> Option<usize> {
3205 SavedState::capture(&self.device, self.config.duty.limit(), self.dev_key).encode(out)
3206 }
3207
3208 pub fn restore_at_boot(&mut self, bytes: &[u8]) -> Result<Effect, SnapshotError> {
3221 let mut saved = SavedState::decode(&self.config, bytes)?;
3222 saved.derive_channel_ids(&self.engine);
3223 self.saved = Some(saved);
3224 self.apply_saved_device();
3225 Ok(self.apply_radio())
3226 }
3227
3228 pub fn note_snapshot_rejected(&mut self) {
3237 self.snapshot_rejected = true;
3238 }
3239
3240 pub fn saved_status(&self) -> SavedStatus {
3242 match (self.saved.is_some(), self.snapshot_rejected) {
3243 (true, false) => SavedStatus::Current,
3244 (true, true) => SavedStatus::Fallback,
3245 (false, true) => SavedStatus::Unreadable,
3246 (false, false) => SavedStatus::None,
3247 }
3248 }
3249
3250 pub fn respond_save(&mut self, tid: u8, result: Result<(), ()>, emit: &mut impl FnMut(&[u8])) {
3256 match result {
3257 Ok(()) => {
3258 self.note_snapshot_saved();
3259 self.complete(tid, Status::OK, emit);
3260 }
3261 Err(()) => self.complete(tid, Status::FAILURE, emit),
3262 }
3263 }
3264
3265 pub fn note_snapshot_saved(&mut self) {
3275 self.saved = Some(SavedState::capture(
3276 &self.device,
3277 self.config.duty.limit(),
3278 self.dev_key,
3279 ));
3280 }
3281
3282 pub fn respond_clear(&mut self, tid: u8, result: Result<(), ()>, emit: &mut impl FnMut(&[u8])) {
3287 match result {
3288 Ok(()) => {
3289 self.saved = None;
3290 self.dev_key_persisted = None;
3291 self.complete(tid, Status::OK, emit);
3292 }
3293 Err(()) => self.complete(tid, Status::FAILURE, emit),
3294 }
3295 }
3296
3297 pub fn identity_request(&self) -> Option<IdentitySource> {
3300 self.session
3301 .pending_identity
3302 .as_ref()
3303 .map(|pending| match pending.secret {
3304 Some(secret) => IdentitySource::Install(secret),
3305 None => IdentitySource::Generate,
3306 })
3307 }
3308
3309 pub fn respond_identity(
3318 &mut self,
3319 tid: u8,
3320 result: Result<[u8; items::PUBLIC_KEY_LEN], ()>,
3321 emit: &mut impl FnMut(&[u8]),
3322 ) {
3323 let matched = self
3324 .session
3325 .pending_identity
3326 .take_if(|pending| pending.tid == tid)
3327 .is_some();
3328 match result {
3329 Ok(public_key) => {
3330 self.dev_key = Some(public_key);
3331 self.dev_key_persisted = Some(public_key);
3332 self.bump_dev_domain();
3338 if matched {
3339 self.send_prop_is(tid, prop::DEV_KEY, &public_key, emit);
3340 }
3341 }
3342 Err(()) if matched => self.complete(tid, Status::FAILURE, emit),
3343 Err(()) => {}
3344 }
3345 }
3346
3347 pub fn set_boot_identity(&mut self, public_key: [u8; items::PUBLIC_KEY_LEN]) {
3351 self.dev_key = Some(public_key);
3352 self.dev_key_persisted = Some(public_key);
3353 }
3354
3355 pub fn respond_pin_set(
3357 &mut self,
3358 tid: u8,
3359 result: Result<(), ()>,
3360 emit: &mut impl FnMut(&[u8]),
3361 ) {
3362 self.complete(
3363 tid,
3364 if result.is_ok() {
3365 Status::OK
3366 } else {
3367 Status::INTERNAL_ERROR
3368 },
3369 emit,
3370 );
3371 }
3372
3373 fn prop_set(
3374 &mut self,
3375 tid: u8,
3376 key: u32,
3377 value: &[u8],
3378 now_ms: u64,
3379 emit: &mut impl FnMut(&[u8]),
3380 ) -> Option<Effect> {
3381 if key == prop::BLE_PAIRING_PIN {
3382 let pin = if value.is_empty() {
3383 None
3384 } else {
3385 match parse_u32(value) {
3386 Ok(pin) if pin <= 999_999 => Some(pin),
3387 _ => {
3388 self.complete(tid, Status::INVALID_ARGUMENT, emit);
3389 return None;
3390 }
3391 }
3392 };
3393 return Some(Effect::SetPairingPin { tid, pin });
3394 }
3395 if key == prop::DEV_PRIVATE_KEY {
3396 if let Err(status) = self.require_secure_link() {
3401 self.complete(tid, status, emit);
3402 return None;
3403 }
3404 let secret = match value.len() {
3405 0 => None,
3406 PRIVATE_KEY_LEN => Some(value.try_into().expect("length checked")),
3407 _ => {
3408 self.complete(tid, Status::INVALID_ARGUMENT, emit);
3409 return None;
3410 }
3411 };
3412 if self.session.pending_identity.is_some() {
3413 self.complete(tid, Status::BUSY, emit);
3414 return None;
3415 }
3416 self.session.pending_identity = Some(PendingIdentity { tid, secret });
3417 return Some(Effect::ProvisionIdentity { tid });
3418 }
3419 if key == prop::HOST_KEY {
3420 let new_key = match value.len() {
3421 0 => None,
3422 items::PUBLIC_KEY_LEN => {
3423 let mut key = [0; items::PUBLIC_KEY_LEN];
3424 key.copy_from_slice(value);
3425 Some(key)
3426 }
3427 _ => {
3428 self.complete(tid, Status::INVALID_ARGUMENT, emit);
3429 return None;
3430 }
3431 };
3432 if new_key != self.host.key {
3441 self.host.reset(new_key);
3442 }
3443 self.send_prop_is(tid, key, value, emit);
3444 return None;
3445 }
3446 if key == prop::ALERT {
3450 let Some(config) = self.config.alert else {
3451 self.complete(tid, Status::PROP_NOT_FOUND, emit);
3452 return None;
3453 };
3454 let state = match pui::decode(value) {
3455 Ok((code, consumed)) if consumed == value.len() => AlertState::from_code(code),
3456 _ => None,
3457 };
3458 let Some(state) = state else {
3459 self.complete(tid, Status::INVALID_ARGUMENT, emit);
3460 return None;
3461 };
3462 self.alert = state;
3463 self.alert_deadline_ms = state
3467 .is_active()
3468 .then(|| now_ms.saturating_add(u64::from(config.timeout_ms)));
3469 let mut echo = [0u8; pui::MAX_LEN];
3470 let len = pui::encode(state.code(), &mut echo).unwrap_or(0);
3471 self.send_prop_is(tid, key, &echo[..len], emit);
3472 return Some(Effect::ApplyAlert(state));
3473 }
3474 if key == prop::TIME && self.config.time.is_some() {
3479 let epoch = match value {
3480 [] => None,
3481 _ => match parse_u32(value) {
3482 Ok(seconds) => Some(seconds),
3483 Err(status) => {
3484 self.complete(tid, status, emit);
3485 return None;
3486 }
3487 },
3488 };
3489 self.send_prop_is(tid, key, value, emit);
3490 return Some(Effect::ApplyTime { epoch });
3491 }
3492 if key == prop::DEV_NAME {
3493 if !valid_device_name(value) {
3494 self.complete(tid, Status::INVALID_ARGUMENT, emit);
3495 return None;
3496 }
3497 self.device.name[..value.len()].copy_from_slice(value);
3498 self.device.name_len = value.len();
3499 self.send_prop_is(tid, key, value, emit);
3500 return Some(Effect::DeviceNameChanged);
3501 }
3502 let radio_affecting = match self.apply_prop_set(key, value) {
3503 Ok(radio_affecting) => radio_affecting,
3504 Err(status) => {
3505 self.complete(tid, status, emit);
3506 return None;
3507 }
3508 };
3509 let mut encoded = [0u8; PROP_BUF];
3511 if let PropValue::Encoded(len) = self.encode_prop(key, now_ms, &mut encoded) {
3512 self.send_prop_is(tid, key, &encoded[..len], emit);
3513 }
3514 radio_affecting.then(|| self.apply_radio())
3515 }
3516
3517 fn apply_prop_set(&mut self, key: u32, value: &[u8]) -> Result<bool, Status> {
3520 match key {
3521 prop::PHY_ENABLED => {
3522 self.device.settings.enabled = parse_bool(value)?;
3523 Ok(true)
3524 }
3525 prop::PHY_FREQ => {
3526 self.device.settings.freq_khz = validate_freq_khz(&self.config, value)?;
3527 Ok(true)
3528 }
3529 prop::PHY_TX_POWER => {
3530 self.device.settings.tx_power_dbm = clamp_tx_power(&self.config, value)?;
3531 Ok(true)
3532 }
3533 prop::PHY_LORA_BW => {
3534 self.device.settings.bw_hz = validate_bw_hz(value)?;
3535 Ok(true)
3536 }
3537 prop::PHY_LORA_SF => {
3538 self.device.settings.sf = validate_sf(value)?;
3539 Ok(true)
3540 }
3541 prop::PHY_LORA_CR => {
3542 self.device.settings.cr_denom = validate_cr(value)?;
3543 Ok(true)
3544 }
3545 prop::PHY_LORA_SW => {
3546 if parse_u16(value)? != self.config.sync_word {
3549 return Err(Status::INVALID_ARGUMENT);
3550 }
3551 Ok(false)
3552 }
3553 prop::PHY_DUTY_LIMIT => {
3554 self.config.duty.set_limit(parse_u16(value)?);
3555 Ok(false)
3556 }
3557 prop::MAC_PROMISCUOUS => {
3558 self.session.promiscuous = parse_bool(value)?;
3560 Ok(false)
3561 }
3562 prop::HOST_AUTO_ACK => {
3563 self.host.auto_ack = parse_bool(value)?;
3564 Ok(false)
3565 }
3566 prop::HOST_RX_FILTERS => {
3570 self.host.filters = FilterTable::parse_table(value)?;
3571 Ok(false)
3572 }
3573 prop::HOST_CHANNEL_KEYS => {
3576 self.require_secure_link()?;
3577 let mut table = ChannelKeyTable::default();
3578 for key in items::fixed_items::<{ items::CHANNEL_KEY_LEN }>(value)
3579 .map_err(|_| Status::INVALID_ARGUMENT)?
3580 {
3581 match table.insert(self.channel_entry(key)) {
3583 Ok(()) | Err(Status::ALREADY) => {}
3584 Err(status) => return Err(status),
3585 }
3586 }
3587 self.host.channel_keys = table;
3588 Ok(false)
3589 }
3590 prop::HOST_PEER_KEYS => {
3596 self.require_secure_link()?;
3597 let mut desired: HeaplessVec<items::PeerKeyEntry, MAX_PEER_KEYS> =
3598 HeaplessVec::new();
3599 for item in items::fixed_items::<{ items::PeerKeyEntry::WIRE_LEN }>(value)
3600 .map_err(|_| Status::INVALID_ARGUMENT)?
3601 {
3602 let entry =
3603 items::PeerKeyEntry::decode(item).map_err(|_| Status::INVALID_ARGUMENT)?;
3604 match desired
3606 .iter_mut()
3607 .find(|existing| existing.public_key == entry.public_key)
3608 {
3609 Some(existing) => *existing = entry,
3610 None => desired.push(entry).map_err(|_| Status::NOMEM)?,
3611 }
3612 }
3613 self.host.peer_keys.reconcile(&desired);
3614 Ok(false)
3615 }
3616 prop::DEV_CHANNEL_KEYS => {
3617 self.require_secure_link()?;
3618 let mut table = ChannelKeyTable::default();
3619 for key in items::fixed_items::<{ items::CHANNEL_KEY_LEN }>(value)
3620 .map_err(|_| Status::INVALID_ARGUMENT)?
3621 {
3622 match table.insert(self.channel_entry(key)) {
3623 Ok(()) | Err(Status::ALREADY) => {}
3624 Err(status) => return Err(status),
3625 }
3626 }
3627 self.device.channel_keys = table;
3628 self.bump_dev_domain();
3629 Ok(false)
3630 }
3631 prop::DEV_PEERS => {
3634 self.device.peers = DevPeerTable::parse_table(value)?;
3635 self.bump_dev_domain();
3636 Ok(false)
3637 }
3638 prop::MAC_REPEATER_ENABLED => {
3644 self.device.repeater_enabled = parse_bool(value)?;
3645 self.bump_dev_domain();
3646 Ok(false)
3647 }
3648 prop::IDENT_ROLE => {
3654 self.device.ident_role = match value.len() {
3655 0 => None,
3656 1 => Some(value[0]),
3657 _ => return Err(Status::INVALID_ARGUMENT),
3658 };
3659 self.bump_dev_domain();
3660 Ok(false)
3661 }
3662 prop::IDENT_MOBILE => {
3663 self.device.ident_mobile = parse_bool(value)?;
3664 self.bump_dev_domain();
3665 Ok(false)
3666 }
3667 prop::DEV_DISCOVERABLE => {
3668 self.device.dev_discoverable = parse_bool(value)?;
3669 self.bump_dev_domain();
3670 Ok(false)
3671 }
3672 prop::ADVERT_INTERVAL => {
3676 self.device.advert_interval_s = validate_announce_interval(value)?;
3677 self.bump_dev_domain();
3678 Ok(false)
3679 }
3680 prop::BEACON_INTERVAL => {
3681 self.device.beacon_interval_s = validate_announce_interval(value)?;
3682 self.bump_dev_domain();
3683 Ok(false)
3684 }
3685 prop::STARTUP_BEACON => {
3686 self.device.startup_beacon = parse_bool(value)?;
3687 self.bump_dev_domain();
3688 Ok(false)
3689 }
3690 prop::MAC_REPEATER_REGIONS => {
3696 self.device.repeater_regions = RepeaterRegions::parse(value)?;
3697 self.bump_dev_domain();
3698 Ok(false)
3699 }
3700 prop::MAC_REPEATER_DEFAULT_REGION => {
3705 self.device.repeater_default_region = parse_region_code(value)?;
3706 self.bump_dev_domain();
3707 Ok(false)
3708 }
3709 prop::MAC_REPEATER_MIN_RSSI => {
3710 self.device.repeater_min_rssi = match value.is_empty() {
3711 true => None,
3712 false => Some(parse_i16(value)?),
3713 };
3714 self.bump_dev_domain();
3715 Ok(false)
3716 }
3717 prop::MAC_REPEATER_MIN_SNR => {
3718 self.device.repeater_min_snr = match value.is_empty() {
3719 true => None,
3720 false => Some(parse_i8(value)?),
3721 };
3722 self.bump_dev_domain();
3723 Ok(false)
3724 }
3725 prop::TZ_OFFSET if self.config.time.is_some() => {
3730 self.device.tz_offset_min = validate_tz_offset(value)?;
3731 self.bump_dev_domain();
3732 Ok(false)
3733 }
3734 prop::GNSS_ENABLED if self.config.gnss.is_some() => {
3735 self.device.gnss_enabled = parse_bool(value)?;
3736 self.bump_dev_domain();
3737 Ok(false)
3738 }
3739 prop::GNSS_IDENT_UPDATE if self.config.gnss.is_some() => {
3740 self.device.gnss_ident_update = parse_bool(value)?;
3741 self.bump_dev_domain();
3742 Ok(false)
3743 }
3744 prop::GNSS_IDENT_PRECISION if self.config.gnss.is_some() => {
3748 self.device.gnss_ident_precision = validate_ident_precision(value)?;
3749 self.bump_dev_domain();
3750 Ok(false)
3751 }
3752 prop::GNSS_TIME_TRUST if self.config.gnss.is_some() => {
3753 self.device.gnss_time_trust = parse_bool(value)?;
3754 self.bump_dev_domain();
3755 Ok(false)
3756 }
3757 prop::HOST_RX_QUEUE_CAPACITY => Err(Status::UNIMPLEMENTED),
3760 prop::LAST_STATUS
3763 | prop::PROTOCOL_VERSION
3764 | prop::DEV_VERSION
3765 | prop::INTERFACE_TYPE
3766 | prop::CAPS
3767 | prop::PHY_RSSI
3768 | prop::PHY_MTU
3769 | prop::PHY_DUTY_NOW
3770 | prop::DEV_KEY
3771 | prop::HOST_RX_QUEUE_COUNT
3772 | prop::HOST_RX_QUEUE_DROPPED
3773 | prop::SAVED => Err(Status::INVALID_ARGUMENT),
3774 prop::BATTERY if self.config.battery.is_some() => Err(Status::INVALID_ARGUMENT),
3775 key if gnss::is_positioning_property(key) && self.config.gnss.is_some() => {
3782 Err(Status::INVALID_ARGUMENT)
3783 }
3784 prop::ILLUMINANCE if self.config.illuminance => Err(Status::INVALID_ARGUMENT),
3785 _ => Err(Status::PROP_NOT_FOUND),
3786 }
3787 }
3788
3789 fn prop_insert(&mut self, tid: u8, key: u32, item: &[u8], emit: &mut impl FnMut(&[u8])) {
3792 match key {
3793 prop::HOST_RX_FILTERS => {
3794 let filter = match decode_filter(item) {
3795 Ok(filter) => filter,
3796 Err(status) => return self.complete(tid, status, emit),
3797 };
3798 match self.host.filters.insert(filter) {
3799 Ok(()) => self.send_prop_inserted(tid, key, item, emit),
3800 Err(status) => self.complete(tid, status, emit),
3801 }
3802 }
3803 prop::HOST_CHANNEL_KEYS => {
3806 let result = self.require_secure_link().and_then(|()| {
3807 let key: &[u8; items::CHANNEL_KEY_LEN] =
3808 item.try_into().map_err(|_| Status::INVALID_ARGUMENT)?;
3809 let entry = self.channel_entry(key);
3810 self.host.channel_keys.insert(entry).map(|()| entry.id)
3811 });
3812 match result {
3813 Ok(id) => self.send_prop_inserted(tid, key, &id, emit),
3814 Err(status) => self.complete(tid, status, emit),
3815 }
3816 }
3817 prop::HOST_PEER_KEYS => {
3818 let result = self.require_secure_link().and_then(|()| {
3819 let entry =
3820 items::PeerKeyEntry::decode(item).map_err(|_| Status::INVALID_ARGUMENT)?;
3821 self.host.peer_keys.insert(entry).map(|()| entry.public_key)
3824 });
3825 match result {
3826 Ok(public_key) => self.send_prop_inserted(tid, key, &public_key, emit),
3827 Err(status) => self.complete(tid, status, emit),
3828 }
3829 }
3830 prop::DEV_CHANNEL_KEYS => {
3831 let result = self.require_secure_link().and_then(|()| {
3832 let key: &[u8; items::CHANNEL_KEY_LEN] =
3833 item.try_into().map_err(|_| Status::INVALID_ARGUMENT)?;
3834 let entry = self.channel_entry(key);
3835 self.device.channel_keys.insert(entry).map(|()| entry.id)
3836 });
3837 match result {
3838 Ok(id) => {
3839 self.bump_dev_domain();
3840 self.send_prop_inserted(tid, key, &id, emit);
3841 }
3842 Err(status) => self.complete(tid, status, emit),
3843 }
3844 }
3845 prop::DEV_PEERS => {
3846 let result = item
3847 .try_into()
3848 .map_err(|_| Status::INVALID_ARGUMENT)
3849 .and_then(|public_key: &[u8; items::PUBLIC_KEY_LEN]| {
3850 self.device.peers.insert(*public_key)
3851 });
3852 match result {
3853 Ok(()) => {
3854 self.bump_dev_domain();
3855 self.send_prop_inserted(tid, key, item, emit);
3856 }
3857 Err(status) => self.complete(tid, status, emit),
3858 }
3859 }
3860 _ if self.known_prop(key) => self.complete(tid, Status::INVALID_ARGUMENT, emit),
3863 _ => self.complete(tid, Status::PROP_NOT_FOUND, emit),
3864 }
3865 }
3866
3867 fn prop_remove(&mut self, tid: u8, key: u32, selector: &[u8], emit: &mut impl FnMut(&[u8])) {
3870 match key {
3871 prop::HOST_RX_FILTERS => {
3872 let filter = match decode_filter(selector) {
3874 Ok(filter) => filter,
3875 Err(status) => return self.complete(tid, status, emit),
3876 };
3877 match self.host.filters.remove(filter) {
3878 Ok(()) => self.send_prop_removed(tid, key, selector, emit),
3879 Err(status) => self.complete(tid, status, emit),
3880 }
3881 }
3882 prop::HOST_CHANNEL_KEYS => {
3885 let result = selector
3886 .try_into()
3887 .map_err(|_| Status::INVALID_ARGUMENT)
3888 .and_then(|key: &[u8; items::CHANNEL_KEY_LEN]| {
3889 self.host.channel_keys.remove(key)
3890 });
3891 match result {
3892 Ok(id) => self.send_prop_removed(tid, key, &id, emit),
3893 Err(status) => self.complete(tid, status, emit),
3894 }
3895 }
3896 prop::HOST_PEER_KEYS => {
3899 let result = selector
3900 .try_into()
3901 .map_err(|_| Status::INVALID_ARGUMENT)
3902 .and_then(|public_key: &[u8; items::PUBLIC_KEY_LEN]| {
3903 self.host.peer_keys.remove(public_key)
3904 });
3905 match result {
3906 Ok(()) => self.send_prop_removed(tid, key, selector, emit),
3907 Err(status) => self.complete(tid, status, emit),
3908 }
3909 }
3910 prop::DEV_CHANNEL_KEYS => {
3911 let result = selector
3912 .try_into()
3913 .map_err(|_| Status::INVALID_ARGUMENT)
3914 .and_then(|key: &[u8; items::CHANNEL_KEY_LEN]| {
3915 self.device.channel_keys.remove(key)
3916 });
3917 match result {
3918 Ok(id) => {
3919 self.bump_dev_domain();
3920 self.send_prop_removed(tid, key, &id, emit);
3921 }
3922 Err(status) => self.complete(tid, status, emit),
3923 }
3924 }
3925 prop::DEV_PEERS => {
3926 let result = selector
3927 .try_into()
3928 .map_err(|_| Status::INVALID_ARGUMENT)
3929 .and_then(|public_key: &[u8; items::PUBLIC_KEY_LEN]| {
3930 self.device.peers.remove(public_key)
3931 });
3932 match result {
3933 Ok(()) => {
3934 self.bump_dev_domain();
3935 self.send_prop_removed(tid, key, selector, emit);
3936 }
3937 Err(status) => self.complete(tid, status, emit),
3938 }
3939 }
3940 _ if self.known_prop(key) => self.complete(tid, Status::INVALID_ARGUMENT, emit),
3941 _ => self.complete(tid, Status::PROP_NOT_FOUND, emit),
3942 }
3943 }
3944
3945 fn channel_entry(&self, key: &[u8; items::CHANNEL_KEY_LEN]) -> ChannelKeyEntry {
3948 ChannelKeyEntry {
3949 key: *key,
3950 id: self.engine.derive_channel_id(&ChannelKey(*key)).0,
3951 }
3952 }
3953
3954 fn require_secure_link(&self) -> Result<(), Status> {
3957 if self.link_secure {
3958 Ok(())
3959 } else {
3960 Err(Status::INVALID_STATE)
3961 }
3962 }
3963
3964 fn str_send(
3965 &mut self,
3966 tid: u8,
3967 payload: &StreamPayload<'_>,
3968 now_ms: u64,
3969 emit: &mut impl FnMut(&[u8]),
3970 ) -> Option<Effect> {
3971 if payload.stream != stream::PHY_RAW {
3972 self.complete(tid, Status::PROP_NOT_FOUND, emit);
3973 return None;
3974 }
3975 if !self.device.settings.enabled {
3976 self.complete(tid, Status::INVALID_STATE, emit);
3977 return None;
3978 }
3979 if payload.data.len() > usize::from(self.config.mtu) {
3980 self.complete(tid, Status::INVALID_ARGUMENT, emit);
3981 return None;
3982 }
3983 let Ok(tx_meta) = TxMeta::decode(payload.metadata) else {
3984 self.complete(tid, Status::PARSE_ERROR, emit);
3985 return None;
3986 };
3987 if self.session.pending.is_full() {
3988 self.complete(tid, Status::BUSY, emit);
3989 return None;
3990 }
3991
3992 let airtime_ms = lora_airtime_ms(
3993 self.device.settings.sf,
3994 self.device.settings.bw_hz,
3995 self.device.settings.cr_denom,
3996 payload.data.len(),
3997 );
3998 let projected_airtime_ms = self
3999 .session
4000 .pending
4001 .iter()
4002 .fold(airtime_ms, |total, pending| {
4003 total.saturating_add(pending.airtime_ms)
4004 });
4005 if tx_meta.flags & meta::TX_FLAG_NODUTY == 0
4006 && self.config.duty.would_exceed(now_ms, projected_airtime_ms)
4007 {
4008 self.complete(tid, Status::DUTY_LIMIT, emit);
4009 return None;
4010 }
4011 let was_empty = self.session.pending.is_empty();
4012 let mut data = HeaplessVec::new();
4013 data.extend_from_slice(payload.data)
4015 .expect("payload bounded by MAX_MTU");
4016 let queued = self.session.pending.push_back(PendingTx {
4017 data,
4018 tid,
4019 airtime_ms,
4020 power: match tx_meta.power {
4024 meta::TX_POWER_DEFAULT => TxPower::Default,
4025 meta::TX_POWER_MAX => TxPower::Max,
4026 dbm => TxPower::Dbm(
4027 dbm.clamp(self.config.min_tx_power_dbm, self.config.max_tx_power_dbm),
4028 ),
4029 },
4030 autonomous: false,
4031 ack_for: None,
4032 nocca: tx_meta.flags & meta::TX_FLAG_NOCCA != 0,
4033 });
4034 debug_assert!(queued.is_ok(), "queue fullness checked above");
4035 self.host.note_tx_mic(payload.data);
4039 was_empty.then_some(Effect::StartTransmit)
4040 }
4041
4042 fn known_prop(&self, key: u32) -> bool {
4048 if key == prop::BATTERY {
4049 return self.config.battery.is_some();
4050 }
4051 if key == prop::ALERT {
4052 return self.config.alert.is_some();
4053 }
4054 if key == prop::ILLUMINANCE {
4055 return self.config.illuminance;
4056 }
4057 if matches!(key, prop::TIME | prop::TZ_OFFSET) {
4058 return self.config.time.is_some();
4059 }
4060 if gnss::is_positioning_property(key)
4061 || matches!(
4062 key,
4063 prop::GNSS_ENABLED
4064 | prop::GNSS_IDENT_UPDATE
4065 | prop::GNSS_IDENT_PRECISION
4066 | prop::GNSS_TIME_TRUST
4067 )
4068 {
4069 return self.config.gnss.is_some();
4070 }
4071 matches!(
4072 key,
4073 prop::LAST_STATUS
4074 | prop::PROTOCOL_VERSION
4075 | prop::DEV_VERSION
4076 | prop::INTERFACE_TYPE
4077 | prop::CAPS
4078 | prop::PHY_ENABLED
4079 | prop::PHY_FREQ
4080 | prop::PHY_TX_POWER
4081 | prop::PHY_RSSI
4082 | prop::PHY_LORA_BW
4083 | prop::PHY_LORA_SF
4084 | prop::PHY_LORA_CR
4085 | prop::PHY_MTU
4086 | prop::PHY_LORA_SW
4087 | prop::DEV_NAME
4088 | prop::DEV_KEY
4089 | prop::DEV_PRIVATE_KEY
4090 | prop::DEV_CHANNEL_KEYS
4091 | prop::DEV_PEERS
4092 | prop::MAC_REPEATER_ENABLED
4093 | prop::MAC_REPEATER_REGIONS
4094 | prop::MAC_REPEATER_DEFAULT_REGION
4095 | prop::MAC_REPEATER_MIN_RSSI
4096 | prop::MAC_REPEATER_MIN_SNR
4097 | prop::IDENT
4098 | prop::IDENT_ROLE
4099 | prop::IDENT_MOBILE
4100 | prop::DEV_DISCOVERABLE
4101 | prop::ADVERT_INTERVAL
4102 | prop::BEACON_INTERVAL
4103 | prop::STARTUP_BEACON
4104 | prop::PHY_DUTY_NOW
4105 | prop::PHY_DUTY_LIMIT
4106 | prop::BLE_PAIRING_PIN
4107 | prop::MAC_PROMISCUOUS
4108 | prop::SAVED
4109 | prop::HOST_KEY
4110 | prop::HOST_RX_FILTERS
4111 | prop::HOST_CHANNEL_KEYS
4112 | prop::HOST_PEER_KEYS
4113 | prop::HOST_AUTO_ACK
4114 | prop::HOST_RX_QUEUE_COUNT
4115 | prop::HOST_RX_QUEUE_CAPACITY
4116 | prop::HOST_RX_QUEUE_DROPPED
4117 )
4118 }
4119
4120 fn encode_prop(&mut self, key: u32, now_ms: u64, out: &mut [u8; PROP_BUF]) -> PropValue {
4121 let len = match key {
4122 prop::LAST_STATUS => pui::encode(self.last_status.0, out).unwrap_or(0),
4123 prop::PROTOCOL_VERSION => {
4124 out[0] = ids::PROTOCOL_MAJOR_VERSION;
4125 out[1] = ids::PROTOCOL_MINOR_VERSION;
4126 2
4127 }
4128 prop::DEV_VERSION => {
4129 let bytes = self.config.dev_version.as_bytes();
4130 let len = bytes.len().min(out.len() - 1);
4131 out[..len].copy_from_slice(&bytes[..len]);
4132 out[len] = 0; len + 1
4134 }
4135 prop::INTERFACE_TYPE => pui::encode(ids::INTERFACE_TYPE, out).unwrap_or(0),
4136 prop::CAPS => {
4137 let mut len = 0;
4138 for capability in [
4139 cap::WRITABLE_RAW_STREAM,
4140 cap::PHY_DUTY_LIMIT,
4141 cap::DEV_NAME,
4142 cap::PHY_LORA,
4143 cap::HOST_FILTER,
4144 cap::HOST_RX_QUEUE,
4145 cap::HOST_KEYS,
4146 cap::HOST_AUTO_ACK,
4147 cap::SAVE,
4148 cap::DEV_IDENTITY,
4149 cap::REPEATER,
4150 cap::IDENT,
4151 cap::ADVERT,
4152 ] {
4153 len += pui::encode(capability, &mut out[len..]).unwrap_or(0);
4154 }
4155 if self.config.battery.is_some() {
4156 len += pui::encode(cap::BATTERY, &mut out[len..]).unwrap_or(0);
4157 }
4158 if self.config.alert.is_some() {
4159 len += pui::encode(cap::ALERT, &mut out[len..]).unwrap_or(0);
4160 }
4161 if self.config.time.is_some() {
4162 len += pui::encode(cap::TIME, &mut out[len..]).unwrap_or(0);
4163 }
4164 if self.config.gnss.is_some() {
4165 len += pui::encode(cap::GNSS, &mut out[len..]).unwrap_or(0);
4166 }
4167 if self.config.illuminance {
4168 len += pui::encode(cap::ILLUMINANCE, &mut out[len..]).unwrap_or(0);
4169 }
4170 len
4171 }
4172 prop::PHY_ENABLED => {
4173 out[0] = self.device.settings.enabled as u8;
4174 1
4175 }
4176 prop::PHY_FREQ => put(out, &self.device.settings.freq_khz.to_le_bytes()),
4177 prop::PHY_TX_POWER => {
4178 out[0] = self.device.settings.tx_power_dbm as u8;
4179 1
4180 }
4181 prop::PHY_RSSI => return PropValue::Unimplemented,
4182 prop::BATTERY if self.config.battery.is_some() => return PropValue::Unimplemented,
4185 prop::ILLUMINANCE if self.config.illuminance => return PropValue::Unimplemented,
4186 prop::PHY_LORA_BW => put(out, &self.device.settings.bw_hz.to_le_bytes()),
4187 prop::PHY_LORA_SF => {
4188 out[0] = self.device.settings.sf;
4189 1
4190 }
4191 prop::PHY_LORA_CR => {
4192 out[0] = self.device.settings.cr_denom;
4193 1
4194 }
4195 prop::PHY_MTU => put(out, &self.config.mtu.to_le_bytes()),
4196 prop::PHY_LORA_SW => put(out, &self.config.sync_word.to_le_bytes()),
4197 prop::DEV_NAME => put(out, &self.device.name[..self.device.name_len]),
4198 prop::DEV_KEY => match &self.dev_key {
4199 Some(key) => put(out, key),
4200 None => 0,
4201 },
4202 prop::DEV_CHANNEL_KEYS => {
4203 let mut len = 0;
4204 for entry in self.device.channel_keys.iter() {
4205 len += put(&mut out[len..], &entry.id);
4206 }
4207 len
4208 }
4209 prop::DEV_PEERS => {
4210 let mut len = 0;
4211 for public_key in self.device.peers.iter() {
4212 len += put(&mut out[len..], public_key);
4213 }
4214 len
4215 }
4216 prop::MAC_REPEATER_ENABLED => {
4217 out[0] = self.device.repeater_enabled as u8;
4218 1
4219 }
4220 prop::IDENT => return PropValue::Unimplemented,
4223 prop::IDENT_ROLE => match self.device.ident_role {
4224 Some(role) => {
4225 out[0] = role;
4226 1
4227 }
4228 None => 0,
4229 },
4230 prop::IDENT_MOBILE => {
4231 out[0] = self.device.ident_mobile as u8;
4232 1
4233 }
4234 prop::DEV_DISCOVERABLE => {
4235 out[0] = self.device.dev_discoverable as u8;
4236 1
4237 }
4238 prop::ADVERT_INTERVAL => put(out, &self.device.advert_interval_s.to_le_bytes()),
4239 prop::BEACON_INTERVAL => put(out, &self.device.beacon_interval_s.to_le_bytes()),
4240 prop::STARTUP_BEACON => {
4241 out[0] = self.device.startup_beacon as u8;
4242 1
4243 }
4244 prop::ALERT if self.config.alert.is_some() => {
4245 pui::encode(self.alert.code(), out).unwrap_or(0)
4246 }
4247 prop::TIME if self.config.time.is_some() => return PropValue::Unimplemented,
4250 key if gnss::is_positioning_property(key) && self.config.gnss.is_some() => {
4251 return PropValue::Unimplemented;
4252 }
4253 prop::TZ_OFFSET if self.config.time.is_some() => {
4254 put(out, &self.device.tz_offset_min.to_le_bytes())
4255 }
4256 prop::GNSS_ENABLED if self.config.gnss.is_some() => {
4257 out[0] = self.device.gnss_enabled as u8;
4258 1
4259 }
4260 prop::GNSS_IDENT_UPDATE if self.config.gnss.is_some() => {
4261 out[0] = self.device.gnss_ident_update as u8;
4262 1
4263 }
4264 prop::GNSS_IDENT_PRECISION if self.config.gnss.is_some() => {
4265 out[0] = self.device.gnss_ident_precision;
4266 1
4267 }
4268 prop::GNSS_TIME_TRUST if self.config.gnss.is_some() => {
4269 out[0] = self.device.gnss_time_trust as u8;
4270 1
4271 }
4272 prop::MAC_REPEATER_REGIONS => put(out, self.device.repeater_regions.as_slice()),
4273 prop::MAC_REPEATER_DEFAULT_REGION => match &self.device.repeater_default_region {
4274 Some(code) => put(out, code),
4275 None => 0,
4276 },
4277 prop::MAC_REPEATER_MIN_RSSI => match self.device.repeater_min_rssi {
4278 Some(rssi) => put(out, &rssi.to_le_bytes()),
4279 None => 0,
4280 },
4281 prop::MAC_REPEATER_MIN_SNR => match self.device.repeater_min_snr {
4282 Some(snr) => {
4283 out[0] = snr as u8;
4284 1
4285 }
4286 None => 0,
4287 },
4288 prop::PHY_DUTY_NOW => put(out, &self.config.duty.usage(now_ms).to_le_bytes()),
4289 prop::PHY_DUTY_LIMIT => put(out, &self.config.duty.limit().to_le_bytes()),
4290 prop::MAC_PROMISCUOUS => {
4291 out[0] = self.session.promiscuous as u8;
4292 1
4293 }
4294 prop::SAVED => {
4295 out[0] = self.saved_status().as_octet();
4296 1
4297 }
4298 prop::HOST_KEY => match &self.host.key {
4299 Some(key) => put(out, key),
4300 None => 0,
4301 },
4302 prop::HOST_CHANNEL_KEYS => {
4306 let mut len = 0;
4307 for entry in self.host.channel_keys.iter() {
4308 len += put(&mut out[len..], &entry.id);
4309 }
4310 len
4311 }
4312 prop::HOST_PEER_KEYS => {
4313 let mut len = 0;
4314 for slot in self.host.peer_keys.iter() {
4315 len += put(&mut out[len..], &slot.entry.public_key);
4316 }
4317 len
4318 }
4319 prop::HOST_AUTO_ACK => {
4320 out[0] = self.host.auto_ack as u8;
4321 1
4322 }
4323 prop::HOST_RX_QUEUE_COUNT => put(out, &(self.host.queue.len as u16).to_le_bytes()),
4324 prop::HOST_RX_QUEUE_CAPACITY => put(out, &(RX_QUEUE_CAPACITY as u16).to_le_bytes()),
4325 prop::HOST_RX_QUEUE_DROPPED => put(out, &self.host.queue.dropped.to_le_bytes()),
4326 prop::HOST_RX_FILTERS => {
4327 let mut len = 0;
4330 for filter in self.host.filters.iter() {
4331 let mut item = [0u8; Filter::MAX_WIRE_LEN];
4332 let item_len = filter.encode(&mut item).expect("MAX_WIRE_LEN sized");
4333 len += items::encode_prefixed_item(&item[..item_len], &mut out[len..])
4334 .expect("out sized for a full filter table");
4335 }
4336 len
4337 }
4338 _ => return PropValue::Unknown,
4339 };
4340 PropValue::Encoded(len)
4341 }
4342
4343 fn send_prop_is(&mut self, tid: u8, key: u32, value: &[u8], emit: &mut impl FnMut(&[u8])) {
4349 if tid == TID_UNSOLICITED {
4350 return;
4351 }
4352 let mut buf = [0u8; PROP_BUF + 16];
4353 if let Ok(len) = frame::prop_is(&mut buf, tid, key, value) {
4354 emit(&buf[..len]);
4355 }
4356 }
4357
4358 fn announce_prop_is(&mut self, key: u32, value: &[u8], emit: &mut impl FnMut(&[u8])) -> bool {
4366 let mut buf = [0u8; PROP_BUF + 16];
4367 match frame::prop_is(&mut buf, TID_UNSOLICITED, key, value) {
4368 Ok(len) => {
4369 emit(&buf[..len]);
4370 true
4371 }
4372 Err(_) => false,
4373 }
4374 }
4375
4376 fn send_prop_inserted(
4381 &mut self,
4382 tid: u8,
4383 key: u32,
4384 digest: &[u8],
4385 emit: &mut impl FnMut(&[u8]),
4386 ) {
4387 if tid == TID_UNSOLICITED {
4388 return;
4389 }
4390 let mut buf = [0u8; PROP_BUF + 16];
4391 if let Ok(len) = frame::prop_inserted(&mut buf, tid, key, digest) {
4392 emit(&buf[..len]);
4393 }
4394 }
4395
4396 fn send_prop_removed(
4399 &mut self,
4400 tid: u8,
4401 key: u32,
4402 digest: &[u8],
4403 emit: &mut impl FnMut(&[u8]),
4404 ) {
4405 if tid == TID_UNSOLICITED {
4406 return;
4407 }
4408 let mut buf = [0u8; PROP_BUF + 16];
4409 if let Ok(len) = frame::prop_removed(&mut buf, tid, key, digest) {
4410 emit(&buf[..len]);
4411 }
4412 }
4413
4414 fn send_status(&mut self, tid: u8, status: Status, emit: &mut impl FnMut(&[u8])) {
4417 self.last_status = status;
4418 let mut buf = [0u8; 16];
4419 if let Ok(len) = frame::last_status(&mut buf, tid, status) {
4420 emit(&buf[..len]);
4421 }
4422 }
4423
4424 fn complete(&mut self, tid: u8, status: Status, emit: &mut impl FnMut(&[u8])) {
4431 if tid == TID_UNSOLICITED {
4432 self.last_status = status;
4433 } else {
4434 self.send_status(tid, status, emit);
4435 }
4436 }
4437}
4438
4439fn put(out: &mut [u8], bytes: &[u8]) -> usize {
4440 out[..bytes.len()].copy_from_slice(bytes);
4441 bytes.len()
4442}
4443
4444fn parse_bool(value: &[u8]) -> Result<bool, Status> {
4445 match value {
4446 [0] => Ok(false),
4447 [1] => Ok(true),
4448 _ => Err(Status::INVALID_ARGUMENT),
4449 }
4450}
4451
4452fn parse_u8(value: &[u8]) -> Result<u8, Status> {
4453 match value {
4454 [byte] => Ok(*byte),
4455 _ => Err(Status::INVALID_ARGUMENT),
4456 }
4457}
4458
4459fn parse_i8(value: &[u8]) -> Result<i8, Status> {
4460 parse_u8(value).map(|byte| byte as i8)
4461}
4462
4463fn parse_i16(value: &[u8]) -> Result<i16, Status> {
4464 parse_u16(value).map(|half| half as i16)
4465}
4466
4467fn parse_u16(value: &[u8]) -> Result<u16, Status> {
4468 match value {
4469 [lo, hi] => Ok(u16::from_le_bytes([*lo, *hi])),
4470 _ => Err(Status::INVALID_ARGUMENT),
4471 }
4472}
4473
4474fn parse_u32(value: &[u8]) -> Result<u32, Status> {
4475 match value {
4476 [a, b, c, d] => Ok(u32::from_le_bytes([*a, *b, *c, *d])),
4477 _ => Err(Status::INVALID_ARGUMENT),
4478 }
4479}
4480
4481fn valid_device_name(value: &[u8]) -> bool {
4482 (1..=MAX_DEVICE_NAME_LEN).contains(&value.len())
4483 && !value.contains(&0)
4484 && core::str::from_utf8(value).is_ok()
4485}
4486
4487fn validate_freq_khz(config: &SessionConfig, value: &[u8]) -> Result<u32, Status> {
4502 let freq_khz = parse_u32(value)?;
4503 if !(config.freq_khz_min..=config.freq_khz_max).contains(&freq_khz) {
4504 return Err(Status::INVALID_ARGUMENT);
4505 }
4506 Ok(freq_khz)
4507}
4508
4509fn clamp_tx_power(config: &SessionConfig, value: &[u8]) -> Result<i8, Status> {
4515 Ok(parse_i8(value)?.clamp(config.min_tx_power_dbm, config.max_tx_power_dbm))
4516}
4517
4518fn validate_bw_hz(value: &[u8]) -> Result<u32, Status> {
4519 let bw_hz = parse_u32(value)?;
4520 if !SUPPORTED_BW_HZ.contains(&bw_hz) {
4521 return Err(Status::INVALID_ARGUMENT);
4522 }
4523 Ok(bw_hz)
4524}
4525
4526fn validate_sf(value: &[u8]) -> Result<u8, Status> {
4527 let sf = parse_u8(value)?;
4528 if !(5..=12).contains(&sf) {
4529 return Err(Status::INVALID_ARGUMENT);
4530 }
4531 Ok(sf)
4532}
4533
4534fn validate_cr(value: &[u8]) -> Result<u8, Status> {
4535 let cr = parse_u8(value)?;
4536 if !(5..=8).contains(&cr) {
4537 return Err(Status::INVALID_ARGUMENT);
4538 }
4539 Ok(cr)
4540}
4541
4542fn validate_tz_offset(value: &[u8]) -> Result<i16, Status> {
4549 let minutes = parse_i16(value)?;
4550 if !(-12 * 60..=14 * 60).contains(&minutes) {
4551 return Err(Status::INVALID_ARGUMENT);
4552 }
4553 Ok(minutes)
4554}
4555
4556fn validate_ident_precision(value: &[u8]) -> Result<u8, Status> {
4563 let precision = parse_u8(value)?;
4564 if !(1..=MAX_IDENT_PRECISION).contains(&precision) {
4565 return Err(Status::INVALID_ARGUMENT);
4566 }
4567 Ok(precision)
4568}
4569
4570fn validate_announce_interval(value: &[u8]) -> Result<u32, Status> {
4579 let seconds = parse_u32(value)?;
4580 if seconds != 0
4581 && !(MIN_AUTO_ANNOUNCE_INTERVAL_S..=MAX_AUTO_ANNOUNCE_INTERVAL_S).contains(&seconds)
4582 {
4583 return Err(Status::INVALID_ARGUMENT);
4584 }
4585 Ok(seconds)
4586}
4587
4588#[cfg(test)]
4589mod tests {
4590 use super::*;
4591 use umsh_crypto::software::{SoftwareAes, SoftwareSha256};
4592
4593 type TestSession = Session<SoftwareAes, SoftwareSha256>;
4594
4595 fn test_engine() -> CryptoEngine<SoftwareAes, SoftwareSha256> {
4596 CryptoEngine::new(SoftwareAes, SoftwareSha256)
4597 }
4598
4599 fn test_session() -> TestSession {
4603 let mut session = test_session_with_boot_status(Status::RESET_POWER_ON);
4604 session.attach(true);
4605 session
4606 }
4607
4608 fn test_session_with_boot_status(boot_status: Status) -> TestSession {
4609 let mut session = Session::new(test_config(), boot_status, test_engine());
4610 session.attach(true);
4611 session
4612 }
4613
4614 fn test_config() -> SessionConfig {
4615 SessionConfig {
4616 dev_version: "test-dev/0.1",
4617 default_device_name: "Test UMSH Device",
4618 mtu: 255,
4619 sync_word: 0x1424,
4620 min_tx_power_dbm: -9,
4621 max_tx_power_dbm: 22,
4622 freq_khz_min: 150_000,
4623 freq_khz_max: 960_000,
4624 defaults: RadioSettings {
4625 enabled: false,
4626 freq_khz: 910_525,
4627 bw_hz: 62_500,
4628 sf: 7,
4629 cr_denom: 5,
4630 tx_power_dbm: 14,
4631 },
4632 default_duty_limit: 0xFFFF,
4633 duty: Box::leak(Box::new(DutyLedger::new())),
4636 battery: Some(BatteryFields {
4639 voltage: true,
4640 level: false,
4641 charge_state: true,
4642 }),
4643 alert: Some(AlertConfig::DEFAULT),
4644 time: Some(TimeConfig),
4645 gnss: Some(GnssConfig::DEFAULT),
4646 illuminance: true,
4647 }
4648 }
4649
4650 fn timeless_config() -> SessionConfig {
4653 SessionConfig {
4654 time: None,
4655 gnss: None,
4656 ..test_config()
4657 }
4658 }
4659
4660 fn dispatch<const TX: usize>(
4662 session: &mut Session<SoftwareAes, SoftwareSha256, TX>,
4663 request: &[u8],
4664 now_ms: u64,
4665 ) -> (Vec<Vec<u8>>, Option<Effect>) {
4666 let mut emitted = Vec::new();
4667 let effect = session.handle_frame(request, now_ms, &mut |bytes: &[u8]| {
4668 emitted.push(bytes.to_vec())
4669 });
4670 (emitted, effect)
4671 }
4672
4673 fn parse_prop_is(bytes: &[u8]) -> (u8, u32, Vec<u8>) {
4675 let parsed = Frame::parse(bytes).unwrap();
4676 assert_eq!(parsed.command(), Some(Cmd::PropIs));
4677 let payload = PropPayload::parse(parsed.payload).unwrap();
4678 (parsed.header.tid(), payload.key, payload.value.to_vec())
4679 }
4680
4681 fn expect_status(bytes: &[u8], tid: u8, status: Status) {
4682 let (response_tid, key, value) = parse_prop_is(bytes);
4683 assert_eq!(response_tid, tid);
4684 assert_eq!(key, prop::LAST_STATUS);
4685 assert_eq!(pui::decode(&value).unwrap().0, status.0);
4686 }
4687
4688 fn get(session: &mut TestSession, key: u32) -> Vec<u8> {
4689 let mut buf = [0u8; 16];
4690 let len = frame::prop_get(&mut buf, 1, key).unwrap();
4691 let (emitted, effect) = dispatch(session, &buf[..len], 0);
4692 assert!(effect.is_none());
4693 let (_, response_key, value) = parse_prop_is(&emitted[0]);
4694 assert_eq!(response_key, key);
4695 value
4696 }
4697
4698 fn set(session: &mut TestSession, key: u32, value: &[u8]) -> (Vec<Vec<u8>>, Option<Effect>) {
4699 let mut buf = [0u8; 640];
4700 let len = frame::prop_set(&mut buf, 2, key, value).unwrap();
4701 dispatch(session, &buf[..len], 0)
4702 }
4703
4704 fn send_packet<const TX: usize>(
4705 session: &mut Session<SoftwareAes, SoftwareSha256, TX>,
4706 tid: u8,
4707 data: &[u8],
4708 meta: &[u8],
4709 now_ms: u64,
4710 ) -> (Vec<Vec<u8>>, Option<Effect>) {
4711 let mut buf = [0u8; 320];
4712 let len = frame::str_send(&mut buf, tid, stream::PHY_RAW, data, meta).unwrap();
4713 dispatch(session, &buf[..len], now_ms)
4714 }
4715
4716 fn enable(session: &mut TestSession) {
4717 let (_, effect) = set(session, prop::PHY_ENABLED, &[1]);
4718 assert!(matches!(effect, Some(Effect::ApplyRadio(settings)) if settings.enabled));
4719 }
4720
4721 #[test]
4722 fn nop_replies_ok() {
4723 let mut session = test_session();
4724 let mut buf = [0u8; 4];
4725 let len = frame::nop(&mut buf, 3).unwrap();
4726 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
4727 assert!(effect.is_none());
4728 expect_status(&emitted[0], 3, Status::OK);
4729 }
4730
4731 #[test]
4732 fn reset_returns_to_defaults() {
4733 let mut session = test_session();
4734 enable(&mut session);
4735 set(&mut session, prop::PHY_LORA_SF, &[12]);
4736
4737 let mut buf = [0u8; 4];
4738 let len = frame::reset(&mut buf, 0).unwrap();
4739 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
4740 expect_status(&emitted[0], TID_UNSOLICITED, Status::RESET_SOFTWARE);
4741 let Some(Effect::ApplyRadio(settings)) = effect else {
4742 panic!("expected ApplyRadio, got {effect:?}");
4743 };
4744 assert!(!settings.enabled);
4745 assert_eq!(settings.sf, 7);
4746 assert_eq!(get(&mut session, prop::PHY_ENABLED), [0]);
4747 }
4748
4749 #[test]
4750 fn identity_properties() {
4751 let mut session = test_session();
4752 assert_eq!(get(&mut session, prop::PROTOCOL_VERSION), [6, 0]);
4753 assert_eq!(get(&mut session, prop::DEV_VERSION), b"test-dev/0.1\0");
4754 assert_eq!(get(&mut session, prop::DEV_NAME), b"Test UMSH Device");
4755 assert_eq!(get(&mut session, prop::PHY_MTU), 255u16.to_le_bytes());
4756 assert_eq!(
4757 pui::decode(&get(&mut session, prop::INTERFACE_TYPE))
4758 .unwrap()
4759 .0,
4760 ids::INTERFACE_TYPE
4761 );
4762 assert_eq!(
4764 pui::decode(&get(&mut session, prop::LAST_STATUS))
4765 .unwrap()
4766 .0,
4767 Status::RESET_POWER_ON.0
4768 );
4769 }
4770
4771 #[test]
4772 fn caps_list_decodes() {
4773 let mut session = test_session();
4774 let raw = get(&mut session, prop::CAPS);
4775 let mut caps = Vec::new();
4776 let mut offset = 0;
4777 while offset < raw.len() {
4778 let (value, used) = pui::decode(&raw[offset..]).unwrap();
4779 caps.push(value);
4780 offset += used;
4781 }
4782 assert_eq!(
4783 caps,
4784 [
4785 cap::WRITABLE_RAW_STREAM,
4786 cap::PHY_DUTY_LIMIT,
4787 cap::DEV_NAME,
4788 cap::PHY_LORA,
4789 cap::HOST_FILTER,
4790 cap::HOST_RX_QUEUE,
4791 cap::HOST_KEYS,
4792 cap::HOST_AUTO_ACK,
4793 cap::SAVE,
4794 cap::DEV_IDENTITY,
4795 cap::REPEATER,
4796 cap::IDENT,
4797 cap::ADVERT,
4798 cap::BATTERY,
4799 cap::ALERT,
4800 cap::TIME,
4801 cap::GNSS,
4802 cap::ILLUMINANCE
4803 ]
4804 );
4805 }
4806
4807 #[test]
4810 fn caps_omit_time_and_gnss_when_unconfigured() {
4811 let mut session: TestSession =
4812 Session::new(timeless_config(), Status::RESET_POWER_ON, test_engine());
4813 session.attach(true);
4814 let raw = get(&mut session, prop::CAPS);
4815 let mut caps = Vec::new();
4816 let mut offset = 0;
4817 while offset < raw.len() {
4818 let (value, used) = pui::decode(&raw[offset..]).unwrap();
4819 caps.push(value);
4820 offset += used;
4821 }
4822 assert!(!caps.contains(&cap::TIME));
4823 assert!(!caps.contains(&cap::GNSS));
4824
4825 for key in [
4826 prop::TIME,
4827 prop::TZ_OFFSET,
4828 prop::GNSS_ENABLED,
4829 prop::GNSS_LOCATION,
4830 prop::GNSS_ALTITUDE,
4831 prop::GNSS_FIX,
4832 prop::GNSS_PRECISION,
4833 prop::GNSS_SATELLITES,
4834 prop::GNSS_IDENT_UPDATE,
4835 prop::GNSS_IDENT_PRECISION,
4836 prop::GNSS_TIME_TRUST,
4837 ] {
4838 let mut buf = [0u8; 16];
4839 let len = frame::prop_get(&mut buf, 6, key).unwrap();
4840 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
4841 assert_eq!(effect, None, "prop {key} produced an effect");
4842 let (_, status_key, value) = parse_prop_is(&emitted[0]);
4843 assert_eq!(status_key, prop::LAST_STATUS);
4844 assert_eq!(
4845 pui::decode(&value).unwrap().0,
4846 Status::PROP_NOT_FOUND.0,
4847 "prop {key} is visible without its capability"
4848 );
4849 }
4850 }
4851
4852 #[test]
4856 fn time_reads_and_writes_defer_to_the_platform() {
4857 let mut session = test_session();
4858
4859 let mut buf = [0u8; 16];
4860 let len = frame::prop_get(&mut buf, 7, prop::TIME).unwrap();
4861 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
4862 assert!(emitted.is_empty(), "no response until the clock is read");
4863 assert_eq!(effect, Some(Effect::ReadTime { tid: 7 }));
4864
4865 let mut out = Vec::new();
4866 session.respond_time(7, Some(1_780_000_000), &mut |bytes: &[u8]| {
4867 out.push(bytes.to_vec())
4868 });
4869 let (tid, key, value) = parse_prop_is(&out[0]);
4870 assert_eq!((tid, key), (7, prop::TIME));
4871 assert_eq!(value, 1_780_000_000u32.to_le_bytes());
4872
4873 let mut out = Vec::new();
4876 session.respond_time(4, None, &mut |bytes: &[u8]| out.push(bytes.to_vec()));
4877 let (_, _, value) = parse_prop_is(&out[0]);
4878 assert_eq!(value, Vec::<u8>::new());
4879
4880 let (_, effect) = set(&mut session, prop::TIME, &1_780_000_042u32.to_le_bytes());
4881 assert_eq!(
4882 effect,
4883 Some(Effect::ApplyTime {
4884 epoch: Some(1_780_000_042)
4885 })
4886 );
4887 let (_, effect) = set(&mut session, prop::TIME, &[]);
4890 assert_eq!(effect, Some(Effect::ApplyTime { epoch: None }));
4891 let (emitted, effect) = set(&mut session, prop::TIME, &[0, 0]);
4893 assert_eq!(effect, None);
4894 let (_, _, value) = parse_prop_is(&emitted[0]);
4895 assert_eq!(pui::decode(&value).unwrap().0, Status::INVALID_ARGUMENT.0);
4896 }
4897
4898 #[test]
4900 fn time_publishes_only_while_attached() {
4901 let mut session = test_session();
4902 let mut out = Vec::new();
4903 assert!(
4904 session.publish_time(Some(1_780_000_000), &mut |bytes: &[u8]| {
4905 out.push(bytes.to_vec())
4906 })
4907 );
4908 let (tid, key, value) = parse_prop_is(&out[0]);
4909 assert_eq!((tid, key), (TID_UNSOLICITED, prop::TIME));
4910 assert_eq!(value, 1_780_000_000u32.to_le_bytes());
4911
4912 session.detach();
4913 let mut out = Vec::new();
4914 assert!(
4915 !session.publish_time(Some(1_780_000_000), &mut |bytes: &[u8]| {
4916 out.push(bytes.to_vec())
4917 })
4918 );
4919 assert!(out.is_empty());
4920 }
4921
4922 #[test]
4925 fn timezone_is_always_known_and_bounded() {
4926 let mut session = test_session();
4927 assert_eq!(get(&mut session, prop::TZ_OFFSET), [0, 0]);
4928 assert_eq!(session.tz_offset_min(), 0);
4929
4930 set(&mut session, prop::TZ_OFFSET, &(-480i16).to_le_bytes());
4932 assert_eq!(get(&mut session, prop::TZ_OFFSET), (-480i16).to_le_bytes());
4933 assert_eq!(session.tz_offset_min(), -480);
4934
4935 for minutes in [-12 * 60i16, 14 * 60] {
4938 let (_, effect) = set(&mut session, prop::TZ_OFFSET, &minutes.to_le_bytes());
4939 assert_eq!(effect, None);
4940 assert_eq!(session.tz_offset_min(), minutes);
4941 }
4942 for minutes in [-12 * 60i16 - 1, 14 * 60 + 1] {
4943 let (emitted, _) = set(&mut session, prop::TZ_OFFSET, &minutes.to_le_bytes());
4944 let (_, _, value) = parse_prop_is(&emitted[0]);
4945 assert_eq!(pui::decode(&value).unwrap().0, Status::INVALID_ARGUMENT.0);
4946 }
4947 }
4948
4949 #[test]
4952 fn positioning_gets_sample_and_are_never_writable() {
4953 let mut session = test_session();
4954 let mut buf = [0u8; 16];
4955 let len = frame::prop_get(&mut buf, 5, prop::GNSS_LOCATION).unwrap();
4956 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
4957 assert!(emitted.is_empty());
4958 assert_eq!(
4959 effect,
4960 Some(Effect::SampleGnss {
4961 tid: 5,
4962 key: prop::GNSS_LOCATION
4963 })
4964 );
4965
4966 let mut snapshot = GnssSnapshot::SEARCHING;
4967 snapshot.fix = umsh_ulcp::gnss::FixKind::ThreeD;
4968 snapshot.altitude_m = Some(112);
4969 snapshot.accuracy_dm = Some(45);
4970 snapshot.sats_used = 8;
4971 snapshot.sats_in_view = Some(11);
4972 snapshot.set_location(&[0x8a, 0x1f, 0x4c, 0x00, 0xd3]);
4973 let mut out = Vec::new();
4974 session.respond_gnss(
4975 5,
4976 prop::GNSS_LOCATION,
4977 Ok(snapshot),
4978 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
4979 );
4980 let (tid, key, value) = parse_prop_is(&out[0]);
4981 assert_eq!((tid, key), (5, prop::GNSS_LOCATION));
4982 assert_eq!(value, [0x8a, 0x1f, 0x4c, 0x00, 0xd3]);
4983
4984 for key in [
4986 prop::GNSS_LOCATION,
4987 prop::GNSS_ALTITUDE,
4988 prop::GNSS_FIX,
4989 prop::GNSS_PRECISION,
4990 prop::GNSS_SATELLITES,
4991 ] {
4992 let (emitted, _) = set(&mut session, key, &[0]);
4993 let (_, _, value) = parse_prop_is(&emitted[0]);
4994 assert_eq!(
4995 pui::decode(&value).unwrap().0,
4996 Status::INVALID_ARGUMENT.0,
4997 "prop {key} accepted a write"
4998 );
4999 }
5000 }
5001
5002 #[test]
5005 fn a_searching_receiver_reports_zero_rather_than_nothing() {
5006 let mut session = test_session();
5007 for (key, expected) in [
5008 (prop::GNSS_FIX, vec![0u8]),
5009 (prop::GNSS_SATELLITES, vec![0u8]),
5010 (prop::GNSS_LOCATION, vec![]),
5011 (prop::GNSS_ALTITUDE, vec![]),
5012 (prop::GNSS_PRECISION, vec![]),
5013 ] {
5014 let mut out = Vec::new();
5015 session.respond_gnss(
5016 3,
5017 key,
5018 Ok(GnssSnapshot::SEARCHING),
5019 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
5020 );
5021 let (_, answered, value) = parse_prop_is(&out[0]);
5022 assert_eq!(answered, key);
5023 assert_eq!(value, expected, "prop {key}");
5024 }
5025 }
5026
5027 #[test]
5030 fn gnss_settings_round_trip_and_are_bounded() {
5031 let mut session = test_session();
5032 assert_eq!(get(&mut session, prop::GNSS_ENABLED), [0]);
5033 assert_eq!(get(&mut session, prop::GNSS_IDENT_UPDATE), [0]);
5034 assert_eq!(
5035 get(&mut session, prop::GNSS_IDENT_PRECISION),
5036 [DEFAULT_IDENT_PRECISION]
5037 );
5038 assert_eq!(get(&mut session, prop::GNSS_TIME_TRUST), [1]);
5039 assert!(!session.gnss_enabled());
5040 assert!(session.gnss_time_trust());
5041
5042 set(&mut session, prop::GNSS_ENABLED, &[1]);
5043 set(&mut session, prop::GNSS_IDENT_UPDATE, &[1]);
5044 set(&mut session, prop::GNSS_IDENT_PRECISION, &[3]);
5045 set(&mut session, prop::GNSS_TIME_TRUST, &[0]);
5046 assert!(session.gnss_enabled());
5047 assert!(session.gnss_ident_update());
5048 assert_eq!(session.gnss_ident_precision(), 3);
5049 assert!(!session.gnss_time_trust());
5050
5051 for precision in [0u8, MAX_IDENT_PRECISION + 1] {
5055 let (emitted, _) = set(&mut session, prop::GNSS_IDENT_PRECISION, &[precision]);
5056 let (_, _, value) = parse_prop_is(&emitted[0]);
5057 assert_eq!(pui::decode(&value).unwrap().0, Status::INVALID_ARGUMENT.0);
5058 }
5059 assert_eq!(session.gnss_ident_precision(), 3);
5060 }
5061
5062 #[test]
5071 fn a_board_can_boot_its_receiver_on() {
5072 let config = SessionConfig {
5073 gnss: Some(GnssConfig::ALWAYS_ON),
5074 ..test_config()
5075 };
5076 let always_on = || {
5077 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
5078 session.attach(true);
5079 session
5080 };
5081
5082 let mut session = always_on();
5083 assert_eq!(get(&mut session, prop::GNSS_ENABLED), [1]);
5084 assert!(session.gnss_enabled());
5085
5086 set(&mut session, prop::GNSS_ENABLED, &[0]);
5089 let mut buf = [0u8; 512];
5090 let len = session.encode_snapshot(&mut buf).expect("snapshot");
5091 let saved = SavedState::decode(&config, &buf[..len]).expect("decode");
5092 assert!(!saved.gnss_enabled);
5093
5094 let mut fresh = always_on();
5096 set(&mut fresh, prop::GNSS_ENABLED, &[0]);
5097 assert!(!fresh.gnss_enabled());
5098 fresh.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
5099 assert!(fresh.gnss_enabled(), "reset dropped the board default");
5100 }
5101
5102 #[test]
5105 fn a_local_toggle_flips_the_switch_and_announces_it() {
5106 let mut session = test_session();
5107 assert!(!session.gnss_enabled());
5108 let before = session.dev_domain_version();
5109
5110 let mut emitted = Vec::new();
5111 let enabled = session.toggle_gnss(&mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
5112 assert_eq!(enabled, Some(true));
5113 assert!(session.gnss_enabled());
5114 assert_eq!(get(&mut session, prop::GNSS_ENABLED), [1]);
5115 assert_ne!(
5116 session.dev_domain_version(),
5117 before,
5118 "the receiver never heard about it"
5119 );
5120 let (tid, key, value) = parse_prop_is(&emitted[0]);
5121 assert_eq!(
5122 (tid, key, value),
5123 (TID_UNSOLICITED, prop::GNSS_ENABLED, vec![1])
5124 );
5125
5126 emitted.clear();
5128 assert_eq!(
5129 session.toggle_gnss(&mut |bytes: &[u8]| emitted.push(bytes.to_vec())),
5130 Some(false)
5131 );
5132 assert!(!session.gnss_enabled());
5133 }
5134
5135 #[test]
5138 fn a_local_toggle_without_the_capability_is_inert() {
5139 let mut session: TestSession =
5140 Session::new(timeless_config(), Status::RESET_POWER_ON, test_engine());
5141 let mut emitted = Vec::new();
5142 assert_eq!(
5143 session.toggle_gnss(&mut |bytes: &[u8]| emitted.push(bytes.to_vec())),
5144 None
5145 );
5146 assert!(emitted.is_empty());
5147 }
5148
5149 #[test]
5152 fn gnss_accessors_are_false_without_the_capability() {
5153 let session: TestSession =
5154 Session::new(timeless_config(), Status::RESET_POWER_ON, test_engine());
5155 assert!(!session.gnss_enabled());
5156 assert!(!session.gnss_ident_update());
5157 }
5158
5159 #[test]
5160 fn advertisement_policy_round_trips_and_survives_a_reboot() {
5161 let mut session = test_session();
5162 assert_eq!(
5163 get(&mut session, prop::ADVERT_INTERVAL),
5164 DEFAULT_ADVERT_INTERVAL_S.to_le_bytes()
5165 );
5166 assert_eq!(
5167 get(&mut session, prop::BEACON_INTERVAL),
5168 DEFAULT_BEACON_INTERVAL_S.to_le_bytes()
5169 );
5170 assert_eq!(get(&mut session, prop::STARTUP_BEACON), [1]);
5171
5172 let (emitted, effect) = set(&mut session, prop::ADVERT_INTERVAL, &7200u32.to_le_bytes());
5173 assert!(effect.is_none());
5174 let (_, key, value) = parse_prop_is(&emitted[0]);
5175 assert_eq!(key, prop::ADVERT_INTERVAL);
5176 assert_eq!(value, 7200u32.to_le_bytes());
5177 set(&mut session, prop::BEACON_INTERVAL, &0u32.to_le_bytes());
5179 set(&mut session, prop::STARTUP_BEACON, &[0]);
5180 assert_eq!(session.advert_interval_s(), 7200);
5181 assert_eq!(session.beacon_interval_s(), 0);
5182 assert!(!session.startup_beacon());
5183
5184 save(&mut session);
5185 let mut bytes = [0u8; SNAPSHOT_MAX];
5186 let len = session.encode_snapshot(&mut bytes).unwrap();
5187 let mut booted: TestSession =
5188 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
5189 booted.restore_at_boot(&bytes[..len]).unwrap();
5190 assert_eq!(booted.advert_interval_s(), 7200);
5191 assert_eq!(booted.beacon_interval_s(), 0);
5192 assert!(!booted.startup_beacon());
5193 }
5194
5195 #[test]
5199 fn announce_interval_holds_to_its_bounds_but_accepts_zero() {
5200 let mut session = test_session();
5201 for &key in &[prop::ADVERT_INTERVAL, prop::BEACON_INTERVAL] {
5202 for rejected in [
5203 MIN_AUTO_ANNOUNCE_INTERVAL_S - 1,
5204 MAX_AUTO_ANNOUNCE_INTERVAL_S + 1,
5205 u32::MAX,
5206 ] {
5207 let (emitted, _) = set(&mut session, key, &rejected.to_le_bytes());
5208 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5209 }
5210
5211 for accepted in [
5212 MIN_AUTO_ANNOUNCE_INTERVAL_S,
5213 MAX_AUTO_ANNOUNCE_INTERVAL_S,
5214 0,
5216 ] {
5217 let (emitted, _) = set(&mut session, key, &accepted.to_le_bytes());
5218 let (_, response_key, value) = parse_prop_is(&emitted[0]);
5219 assert_eq!(response_key, key);
5220 assert_eq!(value, accepted.to_le_bytes());
5221 }
5222 }
5223 }
5224
5225 #[test]
5230 fn a_snapshot_without_advertisement_options_restores_the_defaults() {
5231 let mut session = test_session();
5232 set(&mut session, prop::ADVERT_INTERVAL, &0u32.to_le_bytes());
5233 set(&mut session, prop::STARTUP_BEACON, &[0]);
5234 save(&mut session);
5235 let mut bytes = [0u8; SNAPSHOT_MAX];
5236 let len = session.encode_snapshot(&mut bytes).unwrap();
5237
5238 let stripped = strip_snapshot_options(
5241 &bytes[..len],
5242 &[
5243 prop::ADVERT_INTERVAL,
5244 prop::BEACON_INTERVAL,
5245 prop::STARTUP_BEACON,
5246 ],
5247 );
5248
5249 let mut booted: TestSession =
5250 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
5251 booted.restore_at_boot(&stripped).unwrap();
5252 assert_eq!(booted.advert_interval_s(), DEFAULT_ADVERT_INTERVAL_S);
5253 assert_eq!(booted.beacon_interval_s(), DEFAULT_BEACON_INTERVAL_S);
5254 assert!(booted.startup_beacon());
5255 }
5256
5257 #[test]
5262 fn identity_role_and_mobility_are_independent_of_forwarding() {
5263 let mut session = test_session();
5264 assert_eq!(get(&mut session, prop::IDENT_ROLE), Vec::<u8>::new());
5266 assert_eq!(get(&mut session, prop::IDENT_MOBILE), [0]);
5267
5268 set(&mut session, prop::MAC_REPEATER_ENABLED, &[1]);
5272 assert_eq!(get(&mut session, prop::IDENT_ROLE), Vec::<u8>::new());
5273
5274 let (emitted, effect) = set(&mut session, prop::IDENT_ROLE, &[1]);
5277 assert!(effect.is_none());
5278 let (_, key, value) = parse_prop_is(&emitted[0]);
5279 assert_eq!(key, prop::IDENT_ROLE);
5280 assert_eq!(value, [1]);
5281 set(&mut session, prop::IDENT_MOBILE, &[1]);
5282 assert_eq!(get(&mut session, prop::IDENT_ROLE), [1]);
5283 assert_eq!(get(&mut session, prop::IDENT_MOBILE), [1]);
5284 assert_eq!(get(&mut session, prop::MAC_REPEATER_ENABLED), [1]);
5285
5286 set(&mut session, prop::IDENT_ROLE, &[]);
5288 assert_eq!(get(&mut session, prop::IDENT_ROLE), Vec::<u8>::new());
5289
5290 set(&mut session, prop::IDENT_ROLE, &[3]);
5292 save(&mut session);
5293 let mut bytes = [0u8; SNAPSHOT_MAX];
5294 let len = session.encode_snapshot(&mut bytes).unwrap();
5295 let mut booted: TestSession =
5296 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
5297 booted.restore_at_boot(&bytes[..len]).unwrap();
5298 assert_eq!(booted.ident_role(), Some(3));
5299 assert!(booted.ident_mobile());
5300 booted.attach(true);
5301 assert_eq!(get(&mut booted, prop::IDENT_ROLE), [3]);
5302 assert_eq!(get(&mut booted, prop::IDENT_MOBILE), [1]);
5303
5304 let (emitted, _) = set(&mut booted, prop::IDENT_ROLE, &[1, 2]);
5306 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5307 let (emitted, _) = set(&mut booted, prop::IDENT_MOBILE, &[2]);
5308 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5309 }
5310
5311 #[test]
5314 fn dev_discoverable_defaults_on_and_the_opt_out_survives_reboot() {
5315 let mut session = test_session();
5316 assert_eq!(get(&mut session, prop::DEV_DISCOVERABLE), [1]);
5317 assert!(session.dev_discoverable());
5318
5319 let (emitted, effect) = set(&mut session, prop::DEV_DISCOVERABLE, &[0]);
5320 assert!(effect.is_none());
5321 let (_, key, value) = parse_prop_is(&emitted[0]);
5322 assert_eq!(key, prop::DEV_DISCOVERABLE);
5323 assert_eq!(value, [0]);
5324 assert!(!session.dev_discoverable());
5325
5326 save(&mut session);
5327 let mut bytes = [0u8; SNAPSHOT_MAX];
5328 let len = session.encode_snapshot(&mut bytes).unwrap();
5329 let mut booted: TestSession =
5330 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
5331 booted.restore_at_boot(&bytes[..len]).unwrap();
5332 assert!(!booted.dev_discoverable());
5333 booted.attach(true);
5334 assert_eq!(get(&mut booted, prop::DEV_DISCOVERABLE), [0]);
5335
5336 let (emitted, _) = set(&mut booted, prop::DEV_DISCOVERABLE, &[2]);
5338 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5339 }
5340
5341 #[test]
5345 fn prop_ident_defers_to_the_platform_for_signing() {
5346 let mut session = test_session();
5347 let mut buf = [0u8; 16];
5348 let len = frame::prop_get(&mut buf, 7, prop::IDENT).unwrap();
5349 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
5350 assert!(emitted.is_empty(), "nothing before the signature exists");
5351 assert_eq!(effect, Some(Effect::SignIdentity { tid: 7 }));
5352
5353 let blob = [0xAB; 96];
5354 let mut out = Vec::new();
5355 session.respond_identity_blob(7, Ok(&blob), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
5356 let (tid, key, value) = parse_prop_is(&out[0]);
5357 assert_eq!((tid, key), (7, prop::IDENT));
5358 assert_eq!(value, blob);
5359
5360 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
5363 assert_eq!(effect, Some(Effect::SignIdentity { tid: 7 }));
5364 let mut out = Vec::new();
5365 session.respond_identity_blob(7, Err(()), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
5366 expect_status(&out[0], 7, Status::FAILURE);
5367 }
5368
5369 #[test]
5370 fn repeater_enable_round_trips_and_persists() {
5371 let mut session = test_session();
5372 assert_eq!(get(&mut session, prop::MAC_REPEATER_ENABLED), [0]);
5375 let (emitted, effect) = set(&mut session, prop::MAC_REPEATER_ENABLED, &[1]);
5376 let (_, key, value) = parse_prop_is(&emitted[0]);
5377 assert_eq!(key, prop::MAC_REPEATER_ENABLED);
5378 assert_eq!(value, [1]);
5379 assert_eq!(effect, None);
5381 assert_eq!(get(&mut session, prop::MAC_REPEATER_ENABLED), [1]);
5382 assert!(session.repeater_enabled());
5383 let (emitted, _) = set(&mut session, prop::MAC_REPEATER_ENABLED, &[2]);
5385 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5386
5387 save(&mut session);
5389 let mut bytes = [0u8; SNAPSHOT_MAX];
5390 let len = session.encode_snapshot(&mut bytes).unwrap();
5391 let mut booted: TestSession =
5392 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
5393 booted.restore_at_boot(&bytes[..len]).unwrap();
5394 assert!(booted.repeater_enabled());
5395 booted.attach(true);
5396 assert_eq!(get(&mut booted, prop::MAC_REPEATER_ENABLED), [1]);
5397
5398 let fresh: TestSession = Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
5400 assert!(!fresh.repeater_enabled());
5401 }
5402
5403 #[test]
5407 fn repeater_policy_round_trips_and_persists() {
5408 let mut session = test_session();
5409
5410 assert_eq!(
5412 get(&mut session, prop::MAC_REPEATER_REGIONS),
5413 Vec::<u8>::new()
5414 );
5415 assert_eq!(
5416 get(&mut session, prop::MAC_REPEATER_DEFAULT_REGION),
5417 Vec::<u8>::new()
5418 );
5419 assert_eq!(
5420 get(&mut session, prop::MAC_REPEATER_MIN_RSSI),
5421 Vec::<u8>::new()
5422 );
5423 assert_eq!(
5424 get(&mut session, prop::MAC_REPEATER_MIN_SNR),
5425 Vec::<u8>::new()
5426 );
5427
5428 assert_eq!(get(&mut session, prop::MAC_REPEATER_ENABLED), [0]);
5430 let (emitted, effect) = set(
5431 &mut session,
5432 prop::MAC_REPEATER_REGIONS,
5433 &[0x78, 0x53, 0x31, 0xD9],
5434 );
5435 assert_eq!(effect, None, "policy is not radio-affecting");
5436 let (_, key, value) = parse_prop_is(&emitted[0]);
5437 assert_eq!(key, prop::MAC_REPEATER_REGIONS);
5438 assert_eq!(value, [0x78, 0x53, 0x31, 0xD9]);
5439 set(
5440 &mut session,
5441 prop::MAC_REPEATER_DEFAULT_REGION,
5442 &[0x78, 0x53],
5443 );
5444 set(&mut session, prop::MAC_REPEATER_MIN_RSSI, &[0x8D, 0xFF]);
5446 set(&mut session, prop::MAC_REPEATER_MIN_SNR, &[0xF9]);
5448
5449 assert_eq!(session.repeater_regions(), [0x78, 0x53, 0x31, 0xD9]);
5450 assert_eq!(session.repeater_default_region(), Some([0x78, 0x53]));
5451 assert_eq!(session.repeater_min_rssi(), Some(-115));
5452 assert_eq!(session.repeater_min_snr(), Some(-7));
5453
5454 save(&mut session);
5456 let mut bytes = [0u8; SNAPSHOT_MAX];
5457 let len = session.encode_snapshot(&mut bytes).unwrap();
5458 let mut booted: TestSession =
5459 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
5460 booted.restore_at_boot(&bytes[..len]).unwrap();
5461 assert_eq!(booted.repeater_regions(), [0x78, 0x53, 0x31, 0xD9]);
5462 assert_eq!(booted.repeater_default_region(), Some([0x78, 0x53]));
5463 assert_eq!(booted.repeater_min_rssi(), Some(-115));
5464 assert_eq!(booted.repeater_min_snr(), Some(-7));
5465 booted.attach(true);
5466 assert_eq!(
5467 get(&mut booted, prop::MAC_REPEATER_MIN_RSSI),
5468 [0x8D, 0xFF],
5469 "the reported value is the little-endian INT16 that was written"
5470 );
5471 assert_eq!(get(&mut booted, prop::MAC_REPEATER_MIN_SNR), [0xF9]);
5472 }
5473
5474 #[test]
5478 fn repeater_policy_clears_back_to_unset() {
5479 let mut session = test_session();
5480 set(&mut session, prop::MAC_REPEATER_REGIONS, &[0x78, 0x53]);
5481 set(
5482 &mut session,
5483 prop::MAC_REPEATER_DEFAULT_REGION,
5484 &[0x78, 0x53],
5485 );
5486 set(&mut session, prop::MAC_REPEATER_MIN_RSSI, &[0x8D, 0xFF]);
5487 set(&mut session, prop::MAC_REPEATER_MIN_SNR, &[0xF9]);
5488
5489 set(&mut session, prop::MAC_REPEATER_REGIONS, &[]);
5490 set(&mut session, prop::MAC_REPEATER_DEFAULT_REGION, &[]);
5491 set(&mut session, prop::MAC_REPEATER_MIN_RSSI, &[]);
5492 set(&mut session, prop::MAC_REPEATER_MIN_SNR, &[]);
5493
5494 assert_eq!(session.repeater_regions(), Vec::<u8>::new());
5495 assert_eq!(session.repeater_default_region(), None);
5496 assert_eq!(session.repeater_min_rssi(), None);
5497 assert_eq!(session.repeater_min_snr(), None);
5498
5499 save(&mut session);
5500 let mut bytes = [0u8; SNAPSHOT_MAX];
5501 let len = session.encode_snapshot(&mut bytes).unwrap();
5502 let mut booted: TestSession =
5503 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
5504 booted.restore_at_boot(&bytes[..len]).unwrap();
5505 assert_eq!(booted.repeater_regions(), Vec::<u8>::new());
5506 assert_eq!(booted.repeater_default_region(), None);
5507 assert_eq!(booted.repeater_min_rssi(), None);
5508 assert_eq!(booted.repeater_min_snr(), None);
5509 }
5510
5511 #[test]
5515 fn repeater_policy_rejects_malformed_values() {
5516 let mut session = test_session();
5517
5518 let (emitted, _) = set(
5521 &mut session,
5522 prop::MAC_REPEATER_REGIONS,
5523 &[0x78, 0x53, 0x31],
5524 );
5525 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5526 let over_capacity = [0xAAu8; (MAX_REPEATER_REGIONS + 1) * REGION_CODE_LEN];
5528 let (emitted, _) = set(&mut session, prop::MAC_REPEATER_REGIONS, &over_capacity);
5529 expect_status(&emitted[0], 2, Status::NOMEM);
5530
5531 let (emitted, _) = set(
5533 &mut session,
5534 prop::MAC_REPEATER_DEFAULT_REGION,
5535 &[0x78, 0x53, 0x31],
5536 );
5537 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5538
5539 let (emitted, _) = set(&mut session, prop::MAC_REPEATER_MIN_RSSI, &[0x8D]);
5541 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5542 let (emitted, _) = set(&mut session, prop::MAC_REPEATER_MIN_SNR, &[0xF9, 0xFF]);
5543 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5544
5545 assert_eq!(session.repeater_regions(), Vec::<u8>::new());
5547 assert_eq!(session.repeater_default_region(), None);
5548 assert_eq!(session.repeater_min_rssi(), None);
5549 assert_eq!(session.repeater_min_snr(), None);
5550 }
5551
5552 #[test]
5557 fn repeater_default_region_is_not_cross_checked_against_the_region_list() {
5558 let mut session = test_session();
5559 set(
5561 &mut session,
5562 prop::MAC_REPEATER_DEFAULT_REGION,
5563 &[0x78, 0x53],
5564 );
5565 assert_eq!(session.repeater_default_region(), Some([0x78, 0x53]));
5566 set(&mut session, prop::MAC_REPEATER_REGIONS, &[0x31, 0xD9]);
5567 assert_eq!(session.repeater_regions(), [0x31, 0xD9]);
5568 assert_eq!(
5569 session.repeater_default_region(),
5570 Some([0x78, 0x53]),
5571 "a later region-list write must not silently drop the default"
5572 );
5573
5574 let mut session = test_session();
5577 set(
5578 &mut session,
5579 prop::MAC_REPEATER_DEFAULT_REGION,
5580 &[0xAB, 0xCD],
5581 );
5582 assert_eq!(session.repeater_regions(), Vec::<u8>::new());
5583 assert_eq!(session.repeater_default_region(), Some([0xAB, 0xCD]));
5584 }
5585
5586 #[test]
5587 fn device_name_round_trips_survives_attach_and_resets_to_default() {
5588 let mut session = test_session();
5589 let configured = "Field Radio 📻";
5590 let (emitted, effect) = set(&mut session, prop::DEV_NAME, configured.as_bytes());
5591 let (_, key, value) = parse_prop_is(&emitted[0]);
5592 assert_eq!(key, prop::DEV_NAME);
5593 assert_eq!(value, configured.as_bytes());
5594 assert_eq!(effect, Some(Effect::DeviceNameChanged));
5595 assert_eq!(session.device_name(), configured);
5596
5597 session.attach(true);
5598 assert_eq!(get(&mut session, prop::DEV_NAME), configured.as_bytes());
5599
5600 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_| {});
5601 assert_eq!(get(&mut session, prop::DEV_NAME), b"Test UMSH Device");
5602 }
5603
5604 #[test]
5605 fn attach_preserves_device_domain_and_emits_nothing() {
5606 let mut session = test_session_with_boot_status(Status::RESET_WATCHDOG);
5607
5608 set(&mut session, prop::PHY_FREQ, &906_875u32.to_le_bytes());
5611 set(&mut session, prop::PHY_LORA_SF, &[9]);
5612 set(&mut session, prop::PHY_DUTY_LIMIT, &100u16.to_le_bytes());
5613 enable(&mut session);
5614 let settings_before = session.settings();
5615 assert!(settings_before.enabled);
5616 let (_, effect) = send_packet(&mut session, 1, &[0xAB; 8], &[], 0);
5617 assert_eq!(effect, Some(Effect::StartTransmit));
5618 let mut emitted = Vec::new();
5619 session.on_tx_result(TxOutcome::Sent, 0, &mut |bytes: &[u8]| {
5620 emitted.push(bytes.to_vec())
5621 });
5622 let duty_before = get(&mut session, prop::PHY_DUTY_NOW);
5623 assert_ne!(duty_before, 0u16.to_le_bytes());
5624
5625 session.attach(true);
5628 assert_eq!(session.settings(), settings_before);
5629 assert_eq!(get(&mut session, prop::PHY_ENABLED), [1]);
5630 assert_eq!(get(&mut session, prop::PHY_FREQ), 906_875u32.to_le_bytes());
5631 assert_eq!(
5632 get(&mut session, prop::PHY_DUTY_LIMIT),
5633 100u16.to_le_bytes()
5634 );
5635 assert_eq!(get(&mut session, prop::PHY_DUTY_NOW), duty_before);
5636 }
5637
5638 #[test]
5639 fn attach_retains_boot_status_for_first_query() {
5640 let mut session = test_session_with_boot_status(Status::RESET_WATCHDOG);
5641 session.attach(true);
5642 let raw = get(&mut session, prop::LAST_STATUS);
5643 assert_eq!(pui::decode(&raw).unwrap().0, Status::RESET_WATCHDOG.0);
5644 }
5645
5646 #[test]
5647 fn attach_resets_promiscuous_mode() {
5648 let mut session = test_session();
5649 set(&mut session, prop::MAC_PROMISCUOUS, &[1]);
5650 assert_eq!(get(&mut session, prop::MAC_PROMISCUOUS), [1]);
5651 session.attach(true);
5652 assert_eq!(get(&mut session, prop::MAC_PROMISCUOUS), [0]);
5653
5654 set(&mut session, prop::MAC_PROMISCUOUS, &[1]);
5656 session.detach();
5657 session.attach(true);
5658 assert_eq!(get(&mut session, prop::MAC_PROMISCUOUS), [0]);
5659
5660 let (emitted, _) = set(&mut session, prop::MAC_PROMISCUOUS, &[2]);
5662 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5663 }
5664
5665 #[test]
5666 fn attach_clears_pending_transmit_correlation() {
5667 let mut session = test_session();
5668 enable(&mut session);
5669 let (_, effect) = send_packet(&mut session, 3, &[0x01; 4], &[], 0);
5670 assert_eq!(effect, Some(Effect::StartTransmit));
5671 assert!(session.has_pending_tx());
5672
5673 session.attach(true);
5676 assert!(!session.has_pending_tx());
5677 let mut emitted = Vec::new();
5678 session.on_tx_result(TxOutcome::Sent, 0, &mut |bytes: &[u8]| {
5679 emitted.push(bytes.to_vec())
5680 });
5681 assert!(emitted.is_empty());
5682
5683 let (_, effect) = send_packet(&mut session, 4, &[0x02; 4], &[], 0);
5685 assert_eq!(effect, Some(Effect::StartTransmit));
5686 }
5687
5688 #[test]
5689 fn reset_restores_post_reset_values_and_announces() {
5690 let mut session = test_session();
5691 set(&mut session, prop::PHY_FREQ, &906_875u32.to_le_bytes());
5692 set(&mut session, prop::MAC_PROMISCUOUS, &[1]);
5693 enable(&mut session);
5694
5695 let mut emitted = Vec::new();
5696 let effect = session.reset(Status::RESET_SOFTWARE, &mut |bytes: &[u8]| {
5697 emitted.push(bytes.to_vec())
5698 });
5699 expect_status(&emitted[0], TID_UNSOLICITED, Status::RESET_SOFTWARE);
5700 assert!(matches!(effect, Effect::ApplyRadio(settings) if !settings.enabled));
5701 assert_eq!(get(&mut session, prop::PHY_FREQ), 910_525u32.to_le_bytes());
5702 assert_eq!(get(&mut session, prop::MAC_PROMISCUOUS), [0]);
5703 }
5704
5705 #[test]
5706 fn device_name_rejects_empty_invalid_nul_and_oversize_values() {
5707 let mut session = test_session();
5708 let oversize = [b'x'; MAX_DEVICE_NAME_LEN + 1];
5709 for bad in [&[][..], &[0xff][..], b"bad\0name", &oversize[..]] {
5710 let (emitted, effect) = set(&mut session, prop::DEV_NAME, bad);
5711 assert!(effect.is_none());
5712 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5713 }
5714 assert_eq!(session.device_name(), "Test UMSH Device");
5715 }
5716
5717 #[test]
5718 fn rf_property_round_trip() {
5719 let mut session = test_session();
5720 let (emitted, effect) = set(&mut session, prop::PHY_FREQ, &906_875u32.to_le_bytes());
5721 let (_, key, value) = parse_prop_is(&emitted[0]);
5722 assert_eq!(key, prop::PHY_FREQ);
5723 assert_eq!(value, 906_875u32.to_le_bytes());
5724 assert!(matches!(effect, Some(Effect::ApplyRadio(s)) if s.freq_khz == 906_875));
5725 assert_eq!(get(&mut session, prop::PHY_FREQ), 906_875u32.to_le_bytes());
5726 }
5727
5728 #[test]
5732 fn tx_power_clamps_to_radio_range() {
5733 let mut session = test_session();
5734 for (requested, expected) in [(-20i8, -9i8), (40, 22), (-9, -9), (22, 22), (14, 14)] {
5735 let (emitted, effect) = set(&mut session, prop::PHY_TX_POWER, &[requested as u8]);
5736 let (_, key, value) = parse_prop_is(&emitted[0]);
5737 assert_eq!(key, prop::PHY_TX_POWER);
5738 assert_eq!(value, [expected as u8], "set {requested} dBm");
5739 assert!(
5740 matches!(effect, Some(Effect::ApplyRadio(s)) if s.tx_power_dbm == expected),
5741 "set {requested} dBm"
5742 );
5743 assert_eq!(get(&mut session, prop::PHY_TX_POWER), [expected as u8]);
5744 }
5745 }
5746
5747 #[test]
5750 fn tx_power_override_clamps_to_radio_range() {
5751 for (requested, expected) in [(-20i8, -9i8), (40, 22)] {
5752 let mut session = test_session();
5753 enable(&mut session);
5754 let meta = [requested as u8, 0x00];
5755 let (_, effect) = send_packet(&mut session, 4, &[0u8; 8], &meta, 0);
5756 assert_eq!(effect, Some(Effect::StartTransmit));
5757 assert_eq!(session.tx_power(), TxPower::Dbm(expected), "tx {requested}");
5758 }
5759 }
5760
5761 #[test]
5762 fn invalid_values_rejected() {
5763 let mut session = test_session();
5764 for (key, bad) in [
5765 (prop::PHY_LORA_SF, &[4][..]),
5766 (prop::PHY_LORA_SF, &[13][..]),
5767 (prop::PHY_LORA_CR, &[9][..]),
5768 (prop::PHY_LORA_BW, &123_456u32.to_le_bytes()[..]),
5769 (prop::PHY_FREQ, &10_000u32.to_le_bytes()[..]),
5770 (prop::PHY_TX_POWER, &[14, 0][..]),
5773 (prop::PHY_ENABLED, &[2][..]),
5774 (prop::PHY_LORA_SW, &0xBEEFu16.to_le_bytes()[..]),
5775 (prop::PHY_FREQ, &[1, 2][..]),
5777 ] {
5778 let (emitted, effect) = set(&mut session, key, bad);
5779 assert!(effect.is_none(), "key {key} accepted {bad:?}");
5780 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5781 }
5782 }
5783
5784 #[test]
5785 fn read_only_and_unknown_props() {
5786 let mut session = test_session();
5787 let (emitted, _) = set(&mut session, prop::PHY_MTU, &[0, 1]);
5788 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5789
5790 let (emitted, _) = set(&mut session, 9_999, &[0]);
5791 expect_status(&emitted[0], 2, Status::PROP_NOT_FOUND);
5792
5793 let mut buf = [0u8; 16];
5794 let len = frame::prop_get(&mut buf, 1, 9_999).unwrap();
5795 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
5796 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
5797
5798 let len = frame::prop_get(&mut buf, 1, prop::PHY_RSSI).unwrap();
5800 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
5801 assert!(effect.is_none());
5802 expect_status(&emitted[0], 1, Status::INVALID_STATE);
5803 }
5804
5805 #[test]
5806 fn phy_rssi_defers_to_radio_when_enabled() {
5807 let mut session = test_session();
5808 enable(&mut session);
5809
5810 let mut buf = [0u8; 16];
5812 let len = frame::prop_get(&mut buf, 3, prop::PHY_RSSI).unwrap();
5813 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
5814 assert!(emitted.is_empty(), "no response until the radio is sampled");
5815 assert_eq!(effect, Some(Effect::SampleRssi { tid: 3 }));
5816
5817 let mut out = Vec::new();
5819 session.respond_rssi(3, Ok(-91), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
5820 let (tid, key, value) = parse_prop_is(&out[0]);
5821 assert_eq!(tid, 3);
5822 assert_eq!(key, prop::PHY_RSSI);
5823 assert_eq!(value, [(-91i8) as u8]);
5824
5825 let mut out = Vec::new();
5827 session.respond_rssi(4, Err(()), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
5828 expect_status(&out[0], 4, Status::FAILURE);
5829 }
5830
5831 #[test]
5832 fn battery_get_samples_on_request() {
5833 let mut session = test_session();
5834
5835 let mut buf = [0u8; 16];
5837 let len = frame::prop_get(&mut buf, 7, prop::BATTERY).unwrap();
5838 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
5839 assert!(
5840 emitted.is_empty(),
5841 "no response until the battery is sampled"
5842 );
5843 assert_eq!(effect, Some(Effect::SampleBattery { tid: 7 }));
5844
5845 let mut out = Vec::new();
5848 session.respond_battery(
5849 7,
5850 Ok(BatteryStatus {
5851 voltage_mv: Some(3987),
5852 level_percent: None,
5853 charge_state: Some(battery::BatteryChargeState::Charging),
5854 }),
5855 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
5856 );
5857 let (tid, key, value) = parse_prop_is(&out[0]);
5858 assert_eq!(tid, 7);
5859 assert_eq!(key, prop::BATTERY);
5860 assert_eq!(value, [0b101, 0x93, 0x0F, 1]);
5861
5862 let mut out = Vec::new();
5865 session.respond_battery(6, Err(()), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
5866 expect_status(&out[0], 6, Status::FAILURE);
5867 }
5868
5869 #[test]
5870 fn battery_snapshot_must_match_configured_fields() {
5871 let mut session = test_session();
5872 let mut out = Vec::new();
5876 session.respond_battery(
5877 5,
5878 Ok(BatteryStatus {
5879 voltage_mv: Some(4200),
5880 level_percent: Some(80),
5881 charge_state: Some(battery::BatteryChargeState::Charged),
5882 }),
5883 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
5884 );
5885 expect_status(&out[0], 5, Status::FAILURE);
5886
5887 let mut out = Vec::new();
5891 session.respond_battery(
5892 6,
5893 Ok(BatteryStatus {
5894 voltage_mv: Some(4200),
5895 level_percent: None,
5896 charge_state: None,
5897 }),
5898 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
5899 );
5900 let (tid, key, value) = parse_prop_is(&out[0]);
5901 assert_eq!((tid, key), (6, prop::BATTERY));
5902 let decoded = BatteryStatus::decode(&value).unwrap();
5903 assert_eq!(decoded.voltage_mv, Some(4200));
5904 assert_eq!(decoded.charge_state, None);
5905 }
5906
5907 #[test]
5908 fn battery_without_capability_is_unknown() {
5909 let mut config = test_config();
5910 config.battery = None;
5911 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
5912 session.attach(true);
5913
5914 let raw = get(&mut session, prop::CAPS);
5915 let mut offset = 0;
5916 while offset < raw.len() {
5917 let (value, used) = pui::decode(&raw[offset..]).unwrap();
5918 assert_ne!(value, cap::BATTERY);
5919 offset += used;
5920 }
5921
5922 let mut buf = [0u8; 16];
5923 let len = frame::prop_get(&mut buf, 1, prop::BATTERY).unwrap();
5924 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
5925 assert!(effect.is_none());
5926 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
5927
5928 let (emitted, _) = set(&mut session, prop::BATTERY, &[0b001, 0, 0]);
5929 expect_status(&emitted[0], 2, Status::PROP_NOT_FOUND);
5930 }
5931
5932 #[test]
5933 fn battery_with_no_fields_answers_empty_without_sampling() {
5934 let mut config = test_config();
5935 config.battery = Some(BatteryFields::NONE);
5936 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
5937 session.attach(true);
5938
5939 assert_eq!(get(&mut session, prop::BATTERY), Vec::<u8>::new());
5941 }
5942
5943 #[test]
5944 fn battery_rejects_mutation() {
5945 let mut session = test_session();
5946 let (emitted, effect) = set(&mut session, prop::BATTERY, &[0b001, 0, 0]);
5947 assert!(effect.is_none());
5948 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
5949
5950 let mut buf = [0u8; 16];
5951 let len = frame::prop_insert(&mut buf, 3, prop::BATTERY, &[0]).unwrap();
5952 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
5953 expect_status(&emitted[0], 3, Status::INVALID_ARGUMENT);
5954
5955 let len = frame::prop_remove(&mut buf, 4, prop::BATTERY, &[0]).unwrap();
5956 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
5957 expect_status(&emitted[0], 4, Status::INVALID_ARGUMENT);
5958 }
5959
5960 #[test]
5961 fn illuminance_get_samples_on_request() {
5962 let mut session = test_session();
5963 let mut buf = [0u8; 16];
5964 let len = frame::prop_get(&mut buf, 5, prop::ILLUMINANCE).unwrap();
5965 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
5966 assert!(emitted.is_empty(), "no response until the sensor is read");
5967 assert_eq!(effect, Some(Effect::SampleIlluminance { tid: 5 }));
5968
5969 let mut out = Vec::new();
5970 session.respond_illuminance(5, Some(12_345), &mut |bytes: &[u8]| {
5971 out.push(bytes.to_vec())
5972 });
5973 let (tid, key, value) = parse_prop_is(&out[0]);
5974 assert_eq!((tid, key), (5, prop::ILLUMINANCE));
5975 assert_eq!(value, 12_345u32.to_le_bytes());
5976 }
5977
5978 #[test]
5981 fn illuminance_reports_a_failed_read_as_empty() {
5982 let mut session = test_session();
5983 let mut out = Vec::new();
5984 session.respond_illuminance(4, None, &mut |bytes: &[u8]| out.push(bytes.to_vec()));
5985 let (tid, key, value) = parse_prop_is(&out[0]);
5986 assert_eq!((tid, key, value), (4, prop::ILLUMINANCE, Vec::new()));
5987 }
5988
5989 #[test]
5990 fn illuminance_without_the_sensor_is_unknown() {
5991 let mut config = test_config();
5992 config.illuminance = false;
5993 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
5994 session.attach(true);
5995
5996 let raw = get(&mut session, prop::CAPS);
5997 let mut offset = 0;
5998 while offset < raw.len() {
5999 let (value, used) = pui::decode(&raw[offset..]).unwrap();
6000 assert_ne!(value, cap::ILLUMINANCE);
6001 offset += used;
6002 }
6003
6004 let mut buf = [0u8; 16];
6005 let len = frame::prop_get(&mut buf, 1, prop::ILLUMINANCE).unwrap();
6006 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6007 assert!(effect.is_none());
6008 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
6009 }
6010
6011 #[test]
6012 fn illuminance_rejects_mutation() {
6013 let mut session = test_session();
6014 let (emitted, effect) = set(&mut session, prop::ILLUMINANCE, &0u32.to_le_bytes());
6015 assert!(effect.is_none());
6016 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
6017 }
6018
6019 fn set_alert_at(
6021 session: &mut TestSession,
6022 state: AlertState,
6023 now_ms: u64,
6024 ) -> (Vec<Vec<u8>>, Option<Effect>) {
6025 let mut value = [0u8; pui::MAX_LEN];
6026 let value_len = pui::encode(state.code(), &mut value).unwrap();
6027 let mut buf = [0u8; 16];
6028 let len = frame::prop_set(&mut buf, 2, prop::ALERT, &value[..value_len]).unwrap();
6029 dispatch(session, &buf[..len], now_ms)
6030 }
6031
6032 #[test]
6033 fn alert_starts_and_reports_the_new_state() {
6034 let mut session = test_session();
6035 assert_eq!(get(&mut session, prop::ALERT), vec![0]);
6036
6037 let (emitted, effect) = set_alert_at(&mut session, AlertState::Locate, 1_000);
6038 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::Locate)));
6039 let (tid, key, value) = parse_prop_is(&emitted[0]);
6040 assert_eq!((tid, key, value), (2, prop::ALERT, vec![1]));
6041 assert_eq!(session.alert(), AlertState::Locate);
6042 assert_eq!(get(&mut session, prop::ALERT), vec![1]);
6043 }
6044
6045 #[test]
6046 fn alert_rejects_unknown_states() {
6047 let mut session = test_session();
6048 let (emitted, effect) = set(&mut session, prop::ALERT, &[2]);
6049 assert!(effect.is_none());
6050 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
6051 let (emitted, effect) = set(&mut session, prop::ALERT, &[]);
6053 assert!(effect.is_none());
6054 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
6055 assert_eq!(session.alert(), AlertState::None);
6056 }
6057
6058 #[test]
6059 fn alert_expires_at_its_deadline() {
6060 let mut session = test_session();
6061 let started = 10_000;
6062 set_alert_at(&mut session, AlertState::Locate, started);
6063 let deadline = started + u64::from(AlertConfig::DEFAULT.timeout_ms);
6064 assert_eq!(session.alert_deadline_ms(), Some(deadline));
6065
6066 let mut emitted = Vec::new();
6068 let effect = session.poll_alert(deadline - 1, &mut |bytes: &[u8]| {
6069 emitted.push(bytes.to_vec())
6070 });
6071 assert!(effect.is_none());
6072 assert!(emitted.is_empty());
6073 assert_eq!(session.alert(), AlertState::Locate);
6074
6075 let effect = session.poll_alert(deadline, &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
6076 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::None)));
6077 assert_eq!(session.alert(), AlertState::None);
6078 assert_eq!(session.alert_deadline_ms(), None);
6079 let (tid, key, value) = parse_prop_is(&emitted[0]);
6081 assert_eq!((tid, key, value), (TID_UNSOLICITED, prop::ALERT, vec![0]));
6082 }
6083
6084 #[test]
6085 fn re_arming_an_alert_restarts_the_deadline() {
6086 let mut session = test_session();
6087 set_alert_at(&mut session, AlertState::Locate, 1_000);
6088 let first = session.alert_deadline_ms().unwrap();
6089
6090 let (_, effect) = set_alert_at(&mut session, AlertState::Locate, 60_000);
6092 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::Locate)));
6093 assert_eq!(
6094 session.alert_deadline_ms(),
6095 Some(60_000 + u64::from(AlertConfig::DEFAULT.timeout_ms))
6096 );
6097 assert!(session.alert_deadline_ms().unwrap() > first);
6098 let mut emitted = Vec::new();
6100 assert!(
6101 session
6102 .poll_alert(first, &mut |bytes: &[u8]| emitted.push(bytes.to_vec()))
6103 .is_none()
6104 );
6105 assert_eq!(session.alert(), AlertState::Locate);
6106 }
6107
6108 #[test]
6109 fn local_cancel_clears_and_announces_once() {
6110 let mut session = test_session();
6111 set_alert_at(&mut session, AlertState::Locate, 0);
6112
6113 let mut emitted = Vec::new();
6114 let effect = session.cancel_alert(&mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
6115 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::None)));
6116 let (tid, key, value) = parse_prop_is(&emitted[0]);
6117 assert_eq!((tid, key, value), (TID_UNSOLICITED, prop::ALERT, vec![0]));
6118
6119 emitted.clear();
6121 let effect = session.cancel_alert(&mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
6122 assert!(effect.is_none());
6123 assert!(emitted.is_empty());
6124 }
6125
6126 #[test]
6127 fn alert_survives_detach_and_reset() {
6128 let mut session = test_session();
6129 set_alert_at(&mut session, AlertState::Locate, 0);
6130
6131 session.detach();
6134 assert_eq!(session.alert(), AlertState::Locate);
6135 assert!(session.alert_deadline_ms().is_some());
6136
6137 session.attach(true);
6140 let mut buf = [0u8; 16];
6141 let len = frame::reset(&mut buf, 7).unwrap();
6142 dispatch(&mut session, &buf[..len], 0);
6143 assert_eq!(session.alert(), AlertState::Locate);
6144 assert_eq!(get(&mut session, prop::ALERT), vec![1]);
6145 }
6146
6147 #[test]
6148 fn cancelling_while_detached_emits_nothing() {
6149 let mut session = test_session();
6150 set_alert_at(&mut session, AlertState::Locate, 0);
6151 session.detach();
6152
6153 let mut emitted = Vec::new();
6154 let effect = session.cancel_alert(&mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
6155 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::None)));
6157 assert!(emitted.is_empty());
6158 assert_eq!(session.alert(), AlertState::None);
6159 }
6160
6161 #[test]
6162 fn alert_is_absent_without_the_capability() {
6163 let mut config = test_config();
6164 config.alert = None;
6165 let mut session: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
6166 session.attach(true);
6167
6168 let mut buf = [0u8; 16];
6169 let len = frame::prop_get(&mut buf, 1, prop::ALERT).unwrap();
6170 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
6171 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
6172
6173 let (emitted, effect) = set(&mut session, prop::ALERT, &[1]);
6174 assert!(effect.is_none());
6175 expect_status(&emitted[0], 2, Status::PROP_NOT_FOUND);
6176
6177 let raw = get(&mut session, prop::CAPS);
6178 let mut offset = 0;
6179 while offset < raw.len() {
6180 let (value, used) = pui::decode(&raw[offset..]).unwrap();
6181 assert_ne!(value, cap::ALERT);
6182 offset += used;
6183 }
6184 }
6185
6186 #[test]
6187 fn alert_is_not_saved() {
6188 let mut session = test_session();
6189 set_alert_at(&mut session, AlertState::Locate, 0);
6190 let mut buf = [0u8; SNAPSHOT_MAX];
6191 let len = session.encode_snapshot(&mut buf).unwrap();
6192
6193 let mut fresh: TestSession =
6196 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
6197 fresh.attach(true);
6198 fresh.restore_at_boot(&buf[..len]).unwrap();
6199 assert_eq!(fresh.alert(), AlertState::None);
6200 assert_eq!(fresh.alert_deadline_ms(), None);
6201 }
6202
6203 #[test]
6204 fn battery_reads_still_sample_after_reset() {
6205 let mut session = test_session();
6206 let mut buf = [0u8; 16];
6207 let len = frame::reset(&mut buf, 0).unwrap();
6208 dispatch(&mut session, &buf[..len], 0);
6209
6210 let len = frame::prop_get(&mut buf, 5, prop::BATTERY).unwrap();
6213 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6214 assert!(emitted.is_empty());
6215 assert_eq!(effect, Some(Effect::SampleBattery { tid: 5 }));
6216 }
6217
6218 fn publish(session: &mut TestSession, sample: BatteryStatus) -> (bool, Vec<Vec<u8>>) {
6220 let mut out = Vec::new();
6221 let published =
6222 session.publish_battery(sample, &mut |bytes: &[u8]| out.push(bytes.to_vec()));
6223 (published, out)
6224 }
6225
6226 fn matching_sample() -> BatteryStatus {
6227 BatteryStatus {
6228 voltage_mv: Some(3987),
6229 level_percent: None,
6230 charge_state: Some(battery::BatteryChargeState::Charging),
6231 }
6232 }
6233
6234 #[test]
6235 fn battery_publishes_unsolicited_snapshot() {
6236 let mut session = test_session();
6237 let (published, out) = publish(&mut session, matching_sample());
6238 assert!(published);
6239 let (tid, key, value) = parse_prop_is(&out[0]);
6240 assert_eq!(tid, TID_UNSOLICITED);
6242 assert_eq!(key, prop::BATTERY);
6243 assert_eq!(value, [0b101, 0x93, 0x0F, 1]);
6245 }
6246
6247 #[test]
6248 fn battery_publish_needs_an_attached_host() {
6249 let mut session = test_session();
6250 session.detach();
6251 let (published, out) = publish(&mut session, matching_sample());
6252 assert!(!published, "nobody to notify while detached");
6253 assert!(out.is_empty());
6254
6255 session.attach(true);
6257 assert!(publish(&mut session, matching_sample()).0);
6258 }
6259
6260 #[test]
6261 fn battery_publish_enforces_configured_fields() {
6262 let mut session = test_session();
6263 let (published, out) = publish(
6267 &mut session,
6268 BatteryStatus {
6269 voltage_mv: Some(4200),
6270 level_percent: Some(80),
6271 charge_state: Some(battery::BatteryChargeState::Charged),
6272 },
6273 );
6274 assert!(!published);
6275 assert!(out.is_empty());
6276
6277 let (published, out) = publish(
6282 &mut session,
6283 BatteryStatus {
6284 voltage_mv: Some(4200),
6285 level_percent: None,
6286 charge_state: None,
6287 },
6288 );
6289 assert!(published);
6290 let (_, key, value) = parse_prop_is(&out[0]);
6291 assert_eq!(key, prop::BATTERY);
6292 assert_eq!(
6293 BatteryStatus::decode(&value).unwrap().voltage_mv,
6294 Some(4200)
6295 );
6296 }
6297
6298 #[test]
6299 fn battery_publish_is_silent_without_the_capability() {
6300 let mut config = test_config();
6301 config.battery = None;
6302 let mut session: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
6303 session.attach(true);
6304
6305 let (published, out) = publish(&mut session, matching_sample());
6306 assert!(!published);
6307 assert!(out.is_empty());
6308 }
6309
6310 #[test]
6311 fn battery_publish_does_not_disturb_last_status_or_reads() {
6312 let mut session = test_session();
6313 publish(&mut session, matching_sample());
6316 let status = get(&mut session, prop::LAST_STATUS);
6317 assert_eq!(pui::decode(&status).unwrap().0, Status::RESET_POWER_ON.0);
6318
6319 let mut buf = [0u8; 16];
6321 let len = frame::prop_get(&mut buf, 3, prop::BATTERY).unwrap();
6322 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6323 assert!(emitted.is_empty());
6324 assert_eq!(effect, Some(Effect::SampleBattery { tid: 3 }));
6325 }
6326
6327 #[test]
6328 fn pairing_pin_set_clear_validate_and_defer() {
6329 let mut session = test_session();
6330
6331 let (emitted, effect) = set(
6332 &mut session,
6333 prop::BLE_PAIRING_PIN,
6334 &123_456u32.to_le_bytes(),
6335 );
6336 assert!(
6337 emitted.is_empty(),
6338 "PIN must not be acknowledged before apply"
6339 );
6340 assert_eq!(
6341 effect,
6342 Some(Effect::SetPairingPin {
6343 tid: 2,
6344 pin: Some(123_456)
6345 })
6346 );
6347
6348 let (emitted, effect) = set(&mut session, prop::BLE_PAIRING_PIN, &[]);
6349 assert!(emitted.is_empty());
6350 assert_eq!(effect, Some(Effect::SetPairingPin { tid: 2, pin: None }));
6351
6352 for bad in [&1_000_000u32.to_le_bytes()[..], &[1, 2, 3][..]] {
6353 let (emitted, effect) = set(&mut session, prop::BLE_PAIRING_PIN, bad);
6354 assert!(effect.is_none());
6355 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
6356 }
6357 }
6358
6359 #[test]
6360 fn pairing_pin_completion_and_get_refusal() {
6361 let mut session = test_session();
6362 let mut emitted = Vec::new();
6363 session.respond_pin_set(7, Ok(()), &mut |frame| emitted.push(frame.to_vec()));
6364 expect_status(&emitted[0], 7, Status::OK);
6365 emitted.clear();
6366 session.respond_pin_set(6, Err(()), &mut |frame| emitted.push(frame.to_vec()));
6367 expect_status(&emitted[0], 6, Status::INTERNAL_ERROR);
6368
6369 let mut request = [0; 16];
6370 let len = frame::prop_get(&mut request, 5, prop::BLE_PAIRING_PIN).unwrap();
6371 let (emitted, effect) = dispatch(&mut session, &request[..len], 0);
6372 assert!(effect.is_none());
6373 expect_status(&emitted[0], 5, Status::UNIMPLEMENTED);
6374 }
6375
6376 #[test]
6377 fn reset_has_no_pairing_pin_effect() {
6378 let mut session = test_session();
6379 let mut request = [0; 4];
6380 let len = frame::reset(&mut request, 1).unwrap();
6381 let (_, effect) = dispatch(&mut session, &request[..len], 0);
6382 assert!(matches!(effect, Some(Effect::ApplyRadio(_))));
6383 }
6384
6385 #[test]
6386 fn transmit_lifecycle() {
6387 let mut session = test_session();
6388 enable(&mut session);
6389
6390 let packet = [0xAAu8; 32];
6391 let (emitted, effect) = send_packet(&mut session, 4, &packet, &[], 0);
6392 assert!(emitted.is_empty(), "no response until TX completes");
6393 assert_eq!(effect, Some(Effect::StartTransmit));
6394 assert_eq!(session.tx_data(), &packet);
6395 assert_eq!(session.tx_power(), TxPower::Default);
6396
6397 let (emitted, effect) = send_packet(&mut session, 5, &packet, &[], 0);
6399 assert!(effect.is_none());
6400 expect_status(&emitted[0], 5, Status::BUSY);
6401
6402 let mut emitted = Vec::new();
6404 session.on_tx_result(TxOutcome::Sent, 0, &mut |bytes: &[u8]| {
6405 emitted.push(bytes.to_vec())
6406 });
6407 expect_status(&emitted[0], 4, Status::OK);
6408 assert!(!session.has_pending_tx());
6409 let duty = get(&mut session, prop::PHY_DUTY_NOW);
6410 assert!(u16::from_le_bytes([duty[0], duty[1]]) > 0);
6411 }
6412
6413 #[test]
6414 fn target_selected_transmit_queue_pipelines_frames() {
6415 type PipelinedSession = Session<SoftwareAes, SoftwareSha256, 3>;
6416 let mut session: PipelinedSession =
6417 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
6418 session.attach(true);
6419 let mut request = [0u8; 16];
6420 let len = frame::prop_set(&mut request, 1, prop::PHY_ENABLED, &[1]).unwrap();
6421 let (_, effect) = dispatch(&mut session, &request[..len], 0);
6422 assert!(matches!(effect, Some(Effect::ApplyRadio(settings)) if settings.enabled));
6423
6424 for tid in 2..=4 {
6425 let (emitted, effect) = send_packet(&mut session, tid, &[tid; 8], &[], 0);
6426 assert!(emitted.is_empty());
6427 assert_eq!(effect, (tid == 2).then_some(Effect::StartTransmit));
6428 }
6429 let (emitted, effect) = send_packet(&mut session, 5, &[5; 8], &[], 0);
6430 assert!(effect.is_none());
6431 expect_status(&emitted[0], 5, Status::BUSY);
6432
6433 for tid in 2..=4 {
6434 assert_eq!(session.tx_data(), &[tid; 8]);
6435 let mut emitted = Vec::new();
6436 let effect = session.on_tx_result(TxOutcome::Sent, 0, &mut |bytes| {
6437 emitted.push(bytes.to_vec())
6438 });
6439 expect_status(&emitted[0], tid, Status::OK);
6440 assert_eq!(effect, (tid != 4).then_some(Effect::StartTransmit));
6441 }
6442 assert!(!session.has_pending_tx());
6443 }
6444
6445 #[test]
6446 fn transmit_requires_enabled_phy() {
6447 let mut session = test_session();
6448 let (emitted, effect) = send_packet(&mut session, 4, &[0u8; 8], &[], 0);
6449 assert!(effect.is_none());
6450 expect_status(&emitted[0], 4, Status::INVALID_STATE);
6451 }
6452
6453 #[test]
6454 fn transmit_power_override() {
6455 let mut session = test_session();
6456 enable(&mut session);
6457 let meta = [22u8, 0x00];
6458 let (_, effect) = send_packet(&mut session, 4, &[0u8; 8], &meta, 0);
6459 assert_eq!(effect, Some(Effect::StartTransmit));
6460 assert_eq!(session.tx_power(), TxPower::Dbm(22));
6461 }
6462
6463 #[test]
6464 fn duty_limit_blocks_and_noduty_bypasses() {
6465 let mut session = test_session();
6466 enable(&mut session);
6467 set(&mut session, prop::PHY_LORA_SF, &[12]);
6469 set(&mut session, prop::PHY_LORA_BW, &7_810u32.to_le_bytes());
6470 set(&mut session, prop::PHY_DUTY_LIMIT, &65u16.to_le_bytes());
6472
6473 let packet = [0u8; 255];
6474 let (emitted, effect) = send_packet(&mut session, 3, &packet, &[], 0);
6475 assert!(effect.is_none());
6476 expect_status(&emitted[0], 3, Status::DUTY_LIMIT);
6477
6478 let meta = [meta::TX_POWER_DEFAULT as u8, meta::TX_FLAG_NODUTY];
6480 let (_, effect) = send_packet(&mut session, 3, &packet, &meta, 0);
6481 assert_eq!(effect, Some(Effect::StartTransmit));
6482 }
6483
6484 #[test]
6485 fn nocca_flag_controls_channel_sensing() {
6486 let mut session = test_session();
6487 enable(&mut session);
6488
6489 let (_, effect) = send_packet(&mut session, 3, &[0u8; 8], &[], 0);
6491 assert_eq!(effect, Some(Effect::StartTransmit));
6492 assert!(!session.tx_nocca());
6493 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
6494
6495 let meta = [meta::TX_POWER_DEFAULT as u8, meta::TX_FLAG_NOCCA];
6497 let (_, effect) = send_packet(&mut session, 4, &[0u8; 8], &meta, 0);
6498 assert_eq!(effect, Some(Effect::StartTransmit));
6499 assert!(session.tx_nocca());
6500 }
6501
6502 #[test]
6503 fn channel_busy_completes_host_send_with_cca_failure() {
6504 let mut session = test_session();
6505 enable(&mut session);
6506
6507 let (_, effect) = send_packet(&mut session, 7, &[0u8; 8], &[], 0);
6508 assert_eq!(effect, Some(Effect::StartTransmit));
6509
6510 let mut emitted = Vec::new();
6513 session.on_tx_result(TxOutcome::ChannelBusy, 0, &mut |bytes: &[u8]| {
6514 emitted.push(bytes.to_vec())
6515 });
6516 expect_status(&emitted[0], 7, Status::CCA_FAILURE);
6517
6518 assert_eq!(get(&mut session, prop::PHY_DUTY_NOW), 0u16.to_le_bytes());
6520 }
6521
6522 #[test]
6523 fn delegated_ack_transmits_without_channel_sensing() {
6524 let mut session = auto_ack_session();
6528 let keys = test_pairwise();
6529 let effect = rx_effect(&mut session, &sealed_unar(5, &keys, false), 0);
6530 assert_eq!(effect, Some(Effect::StartTransmit));
6531 assert!(
6532 session.tx_nocca(),
6533 "delegated ack must skip the channel-activity check"
6534 );
6535 }
6536
6537 #[test]
6543 fn foreign_client_airtime_counts_against_the_session() {
6544 let config = test_config();
6545 let ledger = config.duty;
6546 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
6547 session.attach(true);
6548 enable(&mut session);
6549 set(&mut session, prop::PHY_DUTY_LIMIT, &655u16.to_le_bytes());
6550
6551 assert_eq!(get(&mut session, prop::PHY_DUTY_NOW), 0u16.to_le_bytes());
6552 for _ in 0..36 {
6554 ledger.record(0, 1_000);
6555 }
6556 let duty_now = get(&mut session, prop::PHY_DUTY_NOW);
6557 assert!(u16::from_le_bytes([duty_now[0], duty_now[1]]) >= 655);
6558
6559 let (emitted, effect) = send_packet(&mut session, 3, &[0u8; 32], &[], 0);
6560 assert!(effect.is_none());
6561 expect_status(&emitted[0], 3, Status::DUTY_LIMIT);
6562
6563 set(&mut session, prop::PHY_LORA_SF, &[12]);
6566 set(&mut session, prop::PHY_LORA_BW, &7_810u32.to_le_bytes());
6567 assert_eq!(
6568 ledger.airtime_ms(32),
6569 umsh_ulcp::airtime::lora_airtime_ms(12, 7_810, 5, 32)
6570 );
6571 }
6572
6573 #[test]
6574 fn fire_and_forget_failures_are_silent() {
6575 let mut session = test_session();
6576 let (emitted, effect) = send_packet(&mut session, 0, &[0u8; 4], &[], 0);
6578 assert!(effect.is_none());
6579 assert!(emitted.is_empty());
6580 assert_eq!(
6582 pui::decode(&get(&mut session, prop::LAST_STATUS))
6583 .unwrap()
6584 .0,
6585 Status::INVALID_STATE.0
6586 );
6587 }
6588
6589 #[test]
6590 fn radio_rx_emits_str_recv() {
6591 let mut session = test_session();
6592 enable(&mut session);
6593 let mut emitted = Vec::new();
6594 session.on_radio_rx(&[1, 2, 3], -91, -53, None, 0, &mut |bytes: &[u8]| {
6595 emitted.push(bytes.to_vec())
6596 });
6597 let parsed = Frame::parse(&emitted[0]).unwrap();
6598 assert_eq!(parsed.command(), Some(Cmd::StrRecv));
6599 assert_eq!(parsed.header.tid(), TID_UNSOLICITED);
6600 let payload = StreamPayload::parse(parsed.payload).unwrap();
6601 assert_eq!(payload.data, &[1, 2, 3]);
6602 let rx_meta = RxMeta::decode(payload.metadata).unwrap();
6603 assert_eq!(rx_meta.rssi_dbm, Some(-91));
6604 assert_eq!(rx_meta.snr_cb, Some(-53));
6605 }
6606
6607 #[test]
6608 fn radio_rx_suppressed_while_disabled() {
6609 let mut session = test_session();
6610 let mut emitted = Vec::new();
6611 session.on_radio_rx(&[1, 2, 3], -91, -53, None, 0, &mut |bytes: &[u8]| {
6612 emitted.push(bytes.to_vec())
6613 });
6614 assert!(emitted.is_empty());
6615 session.detach();
6617 session.on_radio_rx(&[1, 2, 3], -91, -53, None, 0, &mut |_: &[u8]| {});
6618 session.attach(true);
6619 assert_eq!(
6620 get(&mut session, prop::HOST_RX_QUEUE_COUNT),
6621 0u16.to_le_bytes()
6622 );
6623 }
6624
6625 #[test]
6626 fn unknown_command_rejected() {
6627 let mut session = test_session();
6628 let (emitted, _) = dispatch(&mut session, &[0x81, 42], 0);
6629 expect_status(&emitted[0], 1, Status::INVALID_COMMAND);
6630 }
6631
6632 #[test]
6633 fn insert_remove_reject_per_property_knowledge() {
6634 let mut session = test_session();
6635 let mut buf = [0u8; 80];
6636
6637 for known in [
6639 prop::PHY_FREQ,
6640 prop::BLE_PAIRING_PIN,
6641 prop::CAPS,
6642 prop::MAC_REPEATER_ENABLED,
6643 prop::IDENT_ROLE,
6644 prop::IDENT_MOBILE,
6645 prop::DEV_DISCOVERABLE,
6646 prop::MAC_REPEATER_REGIONS,
6649 prop::MAC_REPEATER_DEFAULT_REGION,
6650 prop::MAC_REPEATER_MIN_RSSI,
6651 prop::MAC_REPEATER_MIN_SNR,
6652 prop::ADVERT_INTERVAL,
6654 prop::BEACON_INTERVAL,
6655 prop::STARTUP_BEACON,
6656 ] {
6657 let len = frame::prop_insert(&mut buf, 1, known, &[0; 4]).unwrap();
6658 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6659 assert!(effect.is_none());
6660 expect_status(&emitted[0], 1, Status::INVALID_ARGUMENT);
6661 }
6662 for unknown in [83, 1_234] {
6665 let len = frame::prop_remove(&mut buf, 2, unknown, &[0; 4]).unwrap();
6666 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6667 assert!(effect.is_none());
6668 expect_status(&emitted[0], 2, Status::PROP_NOT_FOUND);
6669 }
6670 let (emitted, _) = dispatch(&mut session, &[0x81, Cmd::PropInsert as u8], 0);
6672 expect_status(&emitted[0], 1, Status::PARSE_ERROR);
6673 }
6674
6675 #[test]
6676 fn clear_defers_and_leaves_live_state_alone() {
6677 let mut session = test_session();
6678 let mut buf = [0u8; 8];
6679 set(&mut session, prop::DEV_NAME, b"kept name");
6683 let len = frame::clear(&mut buf, 4).unwrap();
6684 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6685 assert!(emitted.is_empty(), "no response before the erase commits");
6686 assert_eq!(effect, Some(Effect::ClearSaved { tid: 4 }));
6687 let mut emitted = Vec::new();
6688 session.respond_clear(4, Ok(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
6689 expect_status(&emitted[0], 4, Status::OK);
6690 assert_eq!(session.device_name(), "kept name");
6691
6692 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
6694 assert_eq!(effect, Some(Effect::ClearSaved { tid: 4 }));
6695 let mut emitted = Vec::new();
6696 session.respond_clear(4, Err(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
6697 expect_status(&emitted[0], 4, Status::FAILURE);
6698 }
6699
6700 #[test]
6701 fn device_only_notifications_rejected_from_host() {
6702 let mut session = test_session();
6703 for cmd in [
6704 Cmd::PropInserted,
6705 Cmd::PropRemoved,
6706 Cmd::PropIs,
6707 Cmd::StrRecv,
6708 ] {
6709 let (emitted, effect) = dispatch(&mut session, &[0x81, cmd as u8], 0);
6710 assert!(effect.is_none());
6711 expect_status(&emitted[0], 1, Status::INVALID_COMMAND);
6712 }
6713 }
6714
6715 #[test]
6716 fn malformed_frames_ignored() {
6717 let mut session = test_session();
6718 for bad in [&[][..], &[0x81][..], &[0x00, 0x00][..], &[0xB8, 0x00][..]] {
6719 let (emitted, effect) = dispatch(&mut session, bad, 0);
6720 assert!(emitted.is_empty());
6721 assert!(effect.is_none());
6722 }
6723 }
6724
6725 use umsh_core::{ChannelId, NodeHint, PacketBuilder};
6728
6729 fn unicast_to(dst: [u8; 3]) -> Vec<u8> {
6730 let mut buf = [0u8; 64];
6731 PacketBuilder::new(&mut buf)
6732 .unicast(NodeHint(dst))
6733 .source_hint(NodeHint([9, 9, 9]))
6734 .frame_counter(1)
6735 .payload(&[1, 2, 3])
6736 .build()
6737 .unwrap()
6738 .as_bytes()
6739 .to_vec()
6740 }
6741
6742 fn multicast_on(channel: [u8; 2]) -> Vec<u8> {
6743 let mut buf = [0u8; 64];
6744 PacketBuilder::new(&mut buf)
6745 .multicast(ChannelId(channel))
6746 .source_hint(NodeHint([9, 9, 9]))
6747 .frame_counter(1)
6748 .payload(&[1, 2, 3])
6749 .build()
6750 .unwrap()
6751 .as_bytes()
6752 .to_vec()
6753 }
6754
6755 fn blind_unicast_on(channel: [u8; 2]) -> Vec<u8> {
6756 let mut buf = [0u8; 96];
6757 PacketBuilder::new(&mut buf)
6758 .blind_unicast(ChannelId(channel), NodeHint([7, 7, 7]))
6759 .source_hint(NodeHint([9, 9, 9]))
6760 .frame_counter(1)
6761 .payload(&[1, 2, 3])
6762 .build()
6763 .unwrap()
6764 .as_bytes()
6765 .to_vec()
6766 }
6767
6768 fn broadcast_frame() -> Vec<u8> {
6769 let mut buf = [0u8; 64];
6770 PacketBuilder::new(&mut buf)
6771 .broadcast()
6772 .source_hint(NodeHint([9, 9, 9]))
6773 .payload(&[1, 2, 3])
6774 .build()
6775 .unwrap()
6776 .to_vec()
6777 }
6778
6779 fn mac_ack_with_mic(ack_mic: [u8; 4]) -> Vec<u8> {
6780 let mut trailer = [0u8; 8];
6781 trailer[..4].copy_from_slice(&ack_mic);
6782 trailer[4..].copy_from_slice(&[0x5A; 4]); let mut buf = [0u8; 32];
6784 PacketBuilder::new(&mut buf)
6785 .mac_ack(trailer)
6786 .build()
6787 .unwrap()
6788 .to_vec()
6789 }
6790
6791 fn delivered(session: &mut TestSession, frame: &[u8]) -> bool {
6793 delivered_at(session, frame, 0)
6794 }
6795
6796 fn delivered_at(session: &mut TestSession, frame: &[u8], now_ms: u64) -> bool {
6797 let mut emitted = Vec::new();
6798 session.on_radio_rx(frame, -80, 40, None, now_ms, &mut |bytes: &[u8]| {
6799 emitted.push(bytes.to_vec())
6800 });
6801 !emitted.is_empty()
6802 }
6803
6804 fn insert_item(
6805 session: &mut TestSession,
6806 key: u32,
6807 item: &[u8],
6808 ) -> (Vec<Vec<u8>>, Option<Effect>) {
6809 let mut buf = [0u8; 96];
6810 let len = frame::prop_insert(&mut buf, 5, key, item).unwrap();
6811 dispatch(session, &buf[..len], 0)
6812 }
6813
6814 fn remove_item(
6815 session: &mut TestSession,
6816 key: u32,
6817 item: &[u8],
6818 ) -> (Vec<Vec<u8>>, Option<Effect>) {
6819 let mut buf = [0u8; 96];
6820 let len = frame::prop_remove(&mut buf, 6, key, item).unwrap();
6821 dispatch(session, &buf[..len], 0)
6822 }
6823
6824 fn install_host_key(session: &mut TestSession, key: &[u8; 32]) {
6826 let (emitted, effect) = set(session, prop::HOST_KEY, key);
6827 assert!(effect.is_none(), "host replacement needs no durable step");
6828 let (_, response_key, value) = parse_prop_is(&emitted[0]);
6829 assert_eq!(response_key, prop::HOST_KEY);
6830 assert_eq!(value, key);
6831 }
6832
6833 fn parse_table_notice(bytes: &[u8], expected: Cmd, tid: u8) -> (u32, Vec<u8>) {
6835 let parsed = Frame::parse(bytes).unwrap();
6836 assert_eq!(parsed.command(), Some(expected));
6837 assert_eq!(parsed.header.tid(), tid);
6838 let payload = PropPayload::parse(parsed.payload).unwrap();
6839 (payload.key, payload.value.to_vec())
6840 }
6841
6842 #[test]
6843 fn factory_state_accepts_everything() {
6844 let mut session = test_session();
6845 enable(&mut session);
6846 assert!(delivered(&mut session, &unicast_to([1, 2, 3])));
6849 assert!(delivered(&mut session, &broadcast_frame()));
6850 assert!(delivered(&mut session, &[0x00, 0x01, 0x02]));
6851 }
6852
6853 #[test]
6854 fn host_key_round_trip_and_implicit_dest_filter() {
6855 let mut session = test_session();
6856 enable(&mut session);
6857 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
6858
6859 let key = [0xC4; 32];
6860 install_host_key(&mut session, &key);
6861 assert_eq!(get(&mut session, prop::HOST_KEY), key);
6862
6863 assert!(delivered(&mut session, &unicast_to([0xC4, 0xC4, 0xC4])));
6869 assert!(!delivered(
6870 &mut session,
6871 &mac_ack_with_mic([0xC4, 0xC4, 0xC4, 0xC4])
6872 ));
6873 assert!(!delivered(&mut session, &unicast_to([1, 2, 3])));
6874 assert!(delivered(&mut session, &broadcast_frame()));
6876 assert!(!delivered(&mut session, &[0x00, 0x01, 0x02]));
6877 }
6878
6879 #[test]
6880 fn mac_ack_accepted_only_when_expected() {
6881 let mut session = test_session();
6882 enable(&mut session);
6883 install_host_key(&mut session, &[0xC4; 32]); assert!(!delivered(
6887 &mut session,
6888 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
6889 ));
6890
6891 let frame = sealed_unar(7, &test_pairwise(), false);
6893 let header = PacketHeader::parse(&frame).unwrap();
6894 let mic = &frame[header.mic_range.clone()];
6895 let ack_mic = [mic[0], mic[1], mic[2], mic[3]];
6896 let (_emitted, effect) = send_packet(&mut session, 4, &frame, &[], 0);
6897 assert_eq!(effect, Some(Effect::StartTransmit));
6898 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
6899
6900 assert!(delivered(&mut session, &mac_ack_with_mic(ack_mic)));
6902 assert!(delivered(&mut session, &mac_ack_with_mic(ack_mic)));
6905 assert!(!delivered(
6907 &mut session,
6908 &mac_ack_with_mic([0x99, 0x88, 0x77, 0x66])
6909 ));
6910 }
6911
6912 #[test]
6919 fn repeat_of_a_transmitted_frame_passes_the_filter() {
6920 let mut session = test_session();
6921 enable(&mut session);
6922 install_host_key(&mut session, &HOST_PUB);
6923
6924 let mut buf = [0u8; 96];
6926 let mut packet = PacketBuilder::new(&mut buf)
6927 .unicast(NodeHint([PEER_PUB[0], PEER_PUB[1], PEER_PUB[2]]))
6928 .source_hint(NodeHint([HOST_PUB[0], HOST_PUB[1], HOST_PUB[2]]))
6929 .frame_counter(9)
6930 .flood_hops(5)
6931 .mic_size(MicSize::Mic8)
6932 .payload(&[4, 5, 6])
6933 .build()
6934 .unwrap();
6935 test_engine()
6936 .seal_packet(&mut packet, &test_pairwise())
6937 .unwrap();
6938 let frame = packet.as_bytes().to_vec();
6939
6940 let header = PacketHeader::parse(&frame).unwrap();
6943 let mut repeat = frame.clone();
6944 repeat[1] = header.flood_hops.unwrap().decremented().0;
6945 assert_ne!(repeat, frame);
6946
6947 assert!(!delivered(&mut session, &repeat));
6950
6951 let (_emitted, effect) = send_packet(&mut session, 4, &frame, &[], 0);
6952 assert_eq!(effect, Some(Effect::StartTransmit));
6953 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
6954
6955 assert!(delivered(&mut session, &repeat));
6959 assert!(delivered(&mut session, &repeat));
6960 }
6961
6962 #[test]
6963 fn explicit_pkt_type_filter_accepts_unexpected_mac_ack() {
6964 let mut session = test_session();
6968 enable(&mut session);
6969 install_host_key(&mut session, &[0xC4; 32]); assert!(!delivered(
6973 &mut session,
6974 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
6975 ));
6976
6977 insert_item(
6979 &mut session,
6980 prop::HOST_RX_FILTERS,
6981 &[items::FILTER_PKT_TYPE, PacketType::MacAck as u8],
6982 );
6983
6984 assert!(delivered(
6986 &mut session,
6987 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
6988 ));
6989 }
6990
6991 #[test]
6992 fn host_key_rejects_bad_lengths() {
6993 let mut session = test_session();
6994 for bad in [&[0u8; 31][..], &[0u8; 33][..], &[1u8][..]] {
6995 let (emitted, effect) = set(&mut session, prop::HOST_KEY, bad);
6996 assert!(effect.is_none());
6997 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
6998 }
6999 }
7000
7001 #[test]
7002 fn host_key_set_is_idempotent_for_current_value() {
7003 let mut session = test_session();
7004 let (emitted, effect) = set(&mut session, prop::HOST_KEY, &[]);
7006 assert!(effect.is_none());
7007 let (_, key, value) = parse_prop_is(&emitted[0]);
7008 assert_eq!(key, prop::HOST_KEY);
7009 assert!(value.is_empty());
7010
7011 let host_key = [0xC4; 32];
7012 install_host_key(&mut session, &host_key);
7013 insert_item(
7014 &mut session,
7015 prop::HOST_RX_FILTERS,
7016 &[items::FILTER_PKT_TYPE, 0],
7017 );
7018
7019 let (emitted, effect) = set(&mut session, prop::HOST_KEY, &host_key);
7021 assert!(effect.is_none());
7022 let (_, key, value) = parse_prop_is(&emitted[0]);
7023 assert_eq!(key, prop::HOST_KEY);
7024 assert_eq!(value, host_key);
7025 assert!(!get(&mut session, prop::HOST_RX_FILTERS).is_empty());
7026 }
7027
7028 #[test]
7032 fn host_replacement_clears_the_host_domain_immediately() {
7033 let mut session = test_session();
7034 install_host_key(&mut session, &[0xAA; 32]);
7035 insert_item(
7036 &mut session,
7037 prop::HOST_RX_FILTERS,
7038 &[items::FILTER_PKT_TYPE, 0],
7039 );
7040
7041 install_host_key(&mut session, &[0xBB; 32]);
7042 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
7043
7044 insert_item(
7046 &mut session,
7047 prop::HOST_RX_FILTERS,
7048 &[items::FILTER_PKT_TYPE, 0],
7049 );
7050 let (emitted, effect) = set(&mut session, prop::HOST_KEY, &[]);
7051 assert!(effect.is_none());
7052 let (_, key, value) = parse_prop_is(&emitted[0]);
7053 assert_eq!(key, prop::HOST_KEY);
7054 assert!(value.is_empty());
7055 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
7056 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
7057 }
7058
7059 #[test]
7060 fn cmd_rst_clears_host_domain() {
7061 let mut session = test_session();
7062 install_host_key(&mut session, &[0xAA; 32]);
7063 insert_item(
7064 &mut session,
7065 prop::HOST_RX_FILTERS,
7066 &[items::FILTER_PKT_TYPE, 0],
7067 );
7068 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_| {});
7069 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
7070 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
7071 }
7072
7073 #[test]
7074 fn filter_insert_remove_lifecycle() {
7075 let mut session = test_session();
7076 let item = [items::FILTER_DEST_HINT, 0x11, 0x22, 0x33];
7077
7078 let (emitted, effect) = insert_item(&mut session, prop::HOST_RX_FILTERS, &item);
7079 assert!(effect.is_none());
7080 let (key, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
7081 assert_eq!(key, prop::HOST_RX_FILTERS);
7082 assert_eq!(digest, item);
7083
7084 let (emitted, _) = insert_item(&mut session, prop::HOST_RX_FILTERS, &item);
7086 expect_status(&emitted[0], 5, Status::ALREADY);
7087
7088 let table = get(&mut session, prop::HOST_RX_FILTERS);
7090 assert_eq!(table, [&[4u8][..], &item[..]].concat());
7091
7092 let (emitted, _) = remove_item(&mut session, prop::HOST_RX_FILTERS, &item);
7093 let (key, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
7094 assert_eq!(key, prop::HOST_RX_FILTERS);
7095 assert_eq!(digest, item);
7096 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
7097
7098 let (emitted, _) = remove_item(&mut session, prop::HOST_RX_FILTERS, &item);
7100 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
7101 }
7102
7103 #[test]
7104 fn filter_insert_rejects_invalid_entries() {
7105 let mut session = test_session();
7106 for bad in [
7107 &[][..], &[3, 0][..], &[items::FILTER_DEST_HINT, 1, 2][..], &[items::FILTER_CHANNEL_ID, 1, 2, 3][..], &[items::FILTER_PKT_TYPE, 8][..], ] {
7113 let (emitted, effect) = insert_item(&mut session, prop::HOST_RX_FILTERS, bad);
7114 assert!(effect.is_none());
7115 expect_status(&emitted[0], 5, Status::INVALID_ARGUMENT);
7116 }
7117 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
7118 }
7119
7120 #[test]
7121 fn filter_table_capacity_is_bounded() {
7122 let mut session = test_session();
7123 for index in 0..MAX_RX_FILTERS as u8 {
7124 let (emitted, _) = insert_item(
7125 &mut session,
7126 prop::HOST_RX_FILTERS,
7127 &[items::FILTER_DEST_HINT, index, 0, 0],
7128 );
7129 parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
7130 }
7131 let (emitted, _) = insert_item(
7132 &mut session,
7133 prop::HOST_RX_FILTERS,
7134 &[items::FILTER_DEST_HINT, 0xFF, 0, 0],
7135 );
7136 expect_status(&emitted[0], 5, Status::NOMEM);
7137 }
7138
7139 #[test]
7140 fn whole_table_set_is_atomic() {
7141 let mut session = test_session();
7142 let good_a = [items::FILTER_DEST_HINT, 1, 2, 3];
7143 let good_b = [items::FILTER_PKT_TYPE, 0];
7144
7145 let mut table = Vec::new();
7146 for item in [&good_a[..], &good_b[..]] {
7147 table.push(item.len() as u8);
7148 table.extend_from_slice(item);
7149 }
7150 let (emitted, effect) = set(&mut session, prop::HOST_RX_FILTERS, &table);
7151 assert!(effect.is_none());
7152 let (_, key, value) = parse_prop_is(&emitted[0]);
7153 assert_eq!(key, prop::HOST_RX_FILTERS);
7154 assert_eq!(value, table);
7155
7156 let mut bad_table = table.clone();
7159 bad_table.extend_from_slice(&[2, 3, 0]); let (emitted, _) = set(&mut session, prop::HOST_RX_FILTERS, &bad_table);
7161 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7162 assert_eq!(get(&mut session, prop::HOST_RX_FILTERS), table);
7163
7164 let (emitted, _) = set(&mut session, prop::HOST_RX_FILTERS, &[9, 1]);
7166 expect_status(&emitted[0], 2, Status::PARSE_ERROR);
7167 assert_eq!(get(&mut session, prop::HOST_RX_FILTERS), table);
7168
7169 let mut doubled = table.clone();
7171 doubled.extend_from_slice(&table);
7172 let (emitted, _) = set(&mut session, prop::HOST_RX_FILTERS, &doubled);
7173 let (_, _, value) = parse_prop_is(&emitted[0]);
7174 assert_eq!(value, table);
7175
7176 let (emitted, _) = set(&mut session, prop::HOST_RX_FILTERS, &[]);
7178 let (_, _, value) = parse_prop_is(&emitted[0]);
7179 assert!(value.is_empty());
7180 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
7181 }
7182
7183 #[test]
7184 fn explicit_filters_match_each_type() {
7185 let mut session = test_session();
7186 enable(&mut session);
7187
7188 insert_item(
7190 &mut session,
7191 prop::HOST_RX_FILTERS,
7192 &[items::FILTER_DEST_HINT, 0x11, 0x22, 0x33],
7193 );
7194 assert!(delivered(&mut session, &unicast_to([0x11, 0x22, 0x33])));
7195 assert!(!delivered(
7197 &mut session,
7198 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
7199 ));
7200 assert!(!delivered(&mut session, &unicast_to([4, 5, 6])));
7201 assert!(delivered(&mut session, &broadcast_frame()));
7204
7205 insert_item(
7208 &mut session,
7209 prop::HOST_RX_FILTERS,
7210 &[items::FILTER_CHANNEL_ID, 0xAB, 0xCD],
7211 );
7212 assert!(delivered(&mut session, &multicast_on([0xAB, 0xCD])));
7213 assert!(delivered(&mut session, &blind_unicast_on([0xAB, 0xCD])));
7214 assert!(!delivered(&mut session, &multicast_on([0x00, 0x01])));
7215
7216 insert_item(
7219 &mut session,
7220 prop::HOST_RX_FILTERS,
7221 &[items::FILTER_PKT_TYPE, PacketType::MacAck as u8],
7222 );
7223 assert!(delivered(
7224 &mut session,
7225 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
7226 ));
7227 assert!(!delivered(&mut session, &unicast_to([4, 5, 6])));
7229 assert!(!delivered(&mut session, &[0x00, 0x01, 0x02]));
7230 }
7231
7232 #[test]
7233 fn promiscuous_bypasses_filtering_for_live_delivery() {
7234 let mut session = test_session();
7235 enable(&mut session);
7236 insert_item(
7237 &mut session,
7238 prop::HOST_RX_FILTERS,
7239 &[items::FILTER_DEST_HINT, 0x11, 0x22, 0x33],
7240 );
7241 assert!(!delivered(&mut session, &unicast_to([4, 5, 6])));
7242
7243 set(&mut session, prop::MAC_PROMISCUOUS, &[1]);
7244 assert!(delivered(&mut session, &unicast_to([4, 5, 6])));
7245 assert!(delivered(&mut session, &[0x00, 0x01, 0x02]));
7246
7247 session.attach(true);
7249 assert!(!delivered(&mut session, &unicast_to([4, 5, 6])));
7250 }
7251
7252 #[test]
7253 fn filters_survive_attach() {
7254 let mut session = test_session();
7255 enable(&mut session);
7256 install_host_key(&mut session, &[0xC4; 32]);
7257 insert_item(
7258 &mut session,
7259 prop::HOST_RX_FILTERS,
7260 &[items::FILTER_PKT_TYPE, 0],
7261 );
7262 session.attach(true);
7263 assert_eq!(get(&mut session, prop::HOST_KEY), [0xC4; 32]);
7264 assert!(delivered(&mut session, &broadcast_frame()));
7265 assert!(delivered(&mut session, &unicast_to([0xC4, 0xC4, 0xC4])));
7266 assert!(!delivered(&mut session, &unicast_to([1, 2, 3])));
7267 }
7268
7269 #[test]
7270 fn host_key_insert_remove_is_invalid_argument() {
7271 let mut session = test_session();
7272 let (emitted, _) = insert_item(&mut session, prop::HOST_KEY, &[0; 32]);
7273 expect_status(&emitted[0], 5, Status::INVALID_ARGUMENT);
7274 let (emitted, _) = remove_item(&mut session, prop::HOST_KEY, &[0; 32]);
7275 expect_status(&emitted[0], 6, Status::INVALID_ARGUMENT);
7276 }
7277
7278 fn receive_detached(session: &mut TestSession, frame: &[u8], now_ms: u64) {
7283 assert!(!delivered_at(session, frame, now_ms));
7284 }
7285
7286 fn queue_count(session: &mut TestSession) -> u16 {
7287 let raw = get(session, prop::HOST_RX_QUEUE_COUNT);
7288 u16::from_le_bytes([raw[0], raw[1]])
7289 }
7290
7291 fn drain(session: &mut TestSession, now_ms: u64) -> Vec<(Vec<u8>, BufferedRxMeta)> {
7294 let mut buf = [0u8; 4];
7295 let len = frame::queue_drain(&mut buf, 7).unwrap();
7296 let (emitted, effect) = dispatch(session, &buf[..len], now_ms);
7297 if effect.is_none() {
7298 expect_status(&emitted[0], 7, Status::OK);
7300 return Vec::new();
7301 }
7302 assert_eq!(effect, Some(Effect::DrainQueue));
7303 assert!(emitted.is_empty());
7304 let mut steps = Vec::new();
7305 loop {
7306 let mut emitted = Vec::new();
7307 let more = session.drain_step(now_ms, &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
7308 assert_eq!(emitted.len(), 1, "each step emits exactly one frame");
7309 if !more {
7310 expect_status(&emitted[0], 7, Status::OK);
7311 return steps;
7312 }
7313 let parsed = Frame::parse(&emitted[0]).unwrap();
7314 assert_eq!(parsed.command(), Some(Cmd::StrRecv));
7315 let payload = StreamPayload::parse(parsed.payload).unwrap();
7316 steps.push((
7317 payload.data.to_vec(),
7318 BufferedRxMeta::decode(payload.metadata).unwrap(),
7319 ));
7320 }
7321 }
7322
7323 #[test]
7324 fn detached_receive_then_attach_count_drain() {
7325 let mut session = test_session();
7326 enable(&mut session);
7327 session.detach();
7328 receive_detached(&mut session, &unicast_to([1, 2, 3]), 1_000);
7329 receive_detached(&mut session, &broadcast_frame(), 3_000);
7330
7331 session.attach(true);
7334 assert_eq!(queue_count(&mut session), 2);
7335 assert!(delivered(&mut session, &unicast_to([7, 7, 7])));
7336 assert_eq!(queue_count(&mut session), 2);
7337
7338 let drained = drain(&mut session, 8_000);
7339 assert_eq!(drained.len(), 2);
7340 assert_eq!(drained[0].0, unicast_to([1, 2, 3]));
7343 assert_eq!(drained[1].0, broadcast_frame());
7344 for (_, meta) in &drained {
7345 assert_eq!(meta.flags, RX_FLAG_BUFFERED);
7346 assert_eq!(meta.rx.rssi_dbm, Some(-80));
7347 assert_eq!(meta.rx.snr_cb, Some(40));
7348 }
7349 assert_eq!((drained[0].1.age_s, drained[1].1.age_s), (7, 5));
7350
7351 assert_eq!(queue_count(&mut session), 0);
7352 assert!(drain(&mut session, 9_000).is_empty());
7354 }
7355
7356 #[test]
7357 fn queue_overflow_evicts_oldest_and_counts_dropped() {
7358 let mut session = test_session();
7359 enable(&mut session);
7360 session.detach();
7361 for index in 0..(RX_QUEUE_CAPACITY + 3) as u8 {
7363 receive_detached(&mut session, &unicast_to([index, 0, 0]), 0);
7364 }
7365 session.attach(true);
7366 assert_eq!(queue_count(&mut session), RX_QUEUE_CAPACITY as u16);
7367 assert_eq!(
7368 get(&mut session, prop::HOST_RX_QUEUE_DROPPED),
7369 3u32.to_le_bytes()
7370 );
7371 let drained = drain(&mut session, 0);
7372 assert_eq!(drained[0].0, unicast_to([3, 0, 0]));
7373 assert_eq!(
7374 drained.last().unwrap().0,
7375 unicast_to([(RX_QUEUE_CAPACITY + 2) as u8, 0, 0])
7376 );
7377 }
7378
7379 #[test]
7380 fn queue_respects_receive_filtering() {
7381 let mut session = test_session();
7382 enable(&mut session);
7383 insert_item(
7384 &mut session,
7385 prop::HOST_RX_FILTERS,
7386 &[items::FILTER_DEST_HINT, 0x11, 0x22, 0x33],
7387 );
7388 session.detach();
7389 receive_detached(&mut session, &unicast_to([0x11, 0x22, 0x33]), 0);
7390 receive_detached(&mut session, &unicast_to([4, 5, 6]), 0); receive_detached(&mut session, &[0xFF, 0xFE], 0); session.attach(true);
7393 assert_eq!(queue_count(&mut session), 1);
7394 }
7395
7396 #[test]
7397 fn broadcasts_are_implicit_live_but_follow_filters_when_queued() {
7398 let mut session = test_session();
7399 enable(&mut session);
7400 install_host_key(&mut session, &[0xC4; 32]);
7404 assert!(delivered(&mut session, &broadcast_frame()));
7405 session.detach();
7408 receive_detached(&mut session, &broadcast_frame(), 0);
7409 session.attach(true);
7410 assert_eq!(queue_count(&mut session), 0);
7411 }
7412
7413 #[test]
7414 fn unauthenticated_duplicates_occupy_separate_entries() {
7415 let mut session = test_session();
7418 enable(&mut session);
7419 session.detach();
7420 let frame = unicast_to([1, 2, 3]);
7421 receive_detached(&mut session, &frame, 0);
7422 receive_detached(&mut session, &frame, 0);
7423 session.attach(true);
7424 assert_eq!(queue_count(&mut session), 2);
7425 }
7426
7427 #[test]
7428 fn live_arrivals_interleave_with_a_drain() {
7429 let mut session = test_session();
7430 enable(&mut session);
7431 session.detach();
7432 receive_detached(&mut session, &unicast_to([1, 0, 0]), 0);
7433 receive_detached(&mut session, &unicast_to([2, 0, 0]), 0);
7434 session.attach(true);
7435
7436 let mut buf = [0u8; 4];
7437 let len = frame::queue_drain(&mut buf, 7).unwrap();
7438 let (_, effect) = dispatch(&mut session, &buf[..len], 10_000);
7439 assert_eq!(effect, Some(Effect::DrainQueue));
7440
7441 let mut emitted = Vec::new();
7443 assert!(session.drain_step(10_000, &mut |bytes: &[u8]| emitted.push(bytes.to_vec())));
7444
7445 assert!(delivered_at(&mut session, &unicast_to([3, 0, 0]), 10_000));
7448
7449 let mut frames = 0;
7451 loop {
7452 let mut emitted = Vec::new();
7453 let more = session.drain_step(10_000, &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
7454 if !more {
7455 expect_status(&emitted[0], 7, Status::OK);
7456 break;
7457 }
7458 frames += 1;
7459 }
7460 assert_eq!(frames, 1);
7461 assert_eq!(queue_count(&mut session), 0);
7462 }
7463
7464 #[test]
7465 fn second_drain_while_in_progress_is_busy() {
7466 let mut session = test_session();
7467 enable(&mut session);
7468 session.detach();
7469 receive_detached(&mut session, &unicast_to([1, 0, 0]), 0);
7470 session.attach(true);
7471
7472 let mut buf = [0u8; 4];
7473 let len = frame::queue_drain(&mut buf, 7).unwrap();
7474 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
7475 assert_eq!(effect, Some(Effect::DrainQueue));
7476
7477 let len = frame::queue_drain(&mut buf, 6).unwrap();
7478 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
7479 assert!(effect.is_none());
7480 expect_status(&emitted[0], 6, Status::BUSY);
7481 }
7482
7483 #[test]
7484 fn reset_and_host_replacement_discard_the_queue() {
7485 let mut session = test_session();
7486 enable(&mut session);
7487 session.detach();
7488 for _ in 0..(RX_QUEUE_CAPACITY + 1) {
7489 receive_detached(&mut session, &unicast_to([1, 2, 3]), 0);
7490 }
7491 session.attach(true);
7492 assert_ne!(queue_count(&mut session), 0);
7493
7494 install_host_key(&mut session, &[0xAA; 32]);
7497 assert_eq!(queue_count(&mut session), 0);
7498 assert_eq!(
7499 get(&mut session, prop::HOST_RX_QUEUE_DROPPED),
7500 0u32.to_le_bytes()
7501 );
7502
7503 enable(&mut session);
7506 session.detach();
7507 receive_detached(&mut session, &unicast_to([0xAA, 0xAA, 0xAA]), 0);
7508 session.attach(true);
7509 assert_eq!(queue_count(&mut session), 1);
7510 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_| {});
7511 assert_eq!(queue_count(&mut session), 0);
7512 }
7513
7514 fn install_channel_key(session: &mut TestSession, key: &[u8; 32]) -> [u8; 2] {
7518 let (emitted, effect) = insert_item(session, prop::HOST_CHANNEL_KEYS, key);
7519 assert!(effect.is_none());
7520 let (prop_key, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
7521 assert_eq!(prop_key, prop::HOST_CHANNEL_KEYS);
7522 digest.try_into().expect("channel digest is 2 bytes")
7523 }
7524
7525 fn peer_entry(seed: u8) -> [u8; 64] {
7526 let mut item = [0u8; 64];
7527 item[..32].fill(seed);
7528 item[32..48].fill(0xE0 | (seed & 0x0F));
7529 item[48..].fill(0x50 | (seed & 0x0F));
7530 item
7531 }
7532
7533 #[test]
7534 fn channel_key_lifecycle_and_digest_is_derived_id() {
7535 let mut session = test_session();
7536 let key = [0x42; 32];
7537 let expected_id = test_engine().derive_channel_id(&ChannelKey(key)).0;
7538
7539 let digest = install_channel_key(&mut session, &key);
7540 assert_eq!(digest, expected_id);
7541 assert_eq!(get(&mut session, prop::HOST_CHANNEL_KEYS), expected_id);
7542
7543 let (emitted, _) = insert_item(&mut session, prop::HOST_CHANNEL_KEYS, &key);
7545 expect_status(&emitted[0], 5, Status::ALREADY);
7546
7547 let (emitted, _) = remove_item(&mut session, prop::HOST_CHANNEL_KEYS, &key);
7549 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
7550 assert_eq!(digest, expected_id);
7551 assert!(get(&mut session, prop::HOST_CHANNEL_KEYS).is_empty());
7552
7553 let (emitted, _) = remove_item(&mut session, prop::HOST_CHANNEL_KEYS, &key);
7554 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
7555
7556 for bad in [&[0u8; 31][..], &[0u8; 33][..], &[][..]] {
7558 let (emitted, _) = insert_item(&mut session, prop::HOST_CHANNEL_KEYS, bad);
7559 expect_status(&emitted[0], 5, Status::INVALID_ARGUMENT);
7560 }
7561 }
7562
7563 #[test]
7564 fn channel_key_capacity_is_bounded() {
7565 let mut session = test_session();
7566 for seed in 0..MAX_CHANNEL_KEYS as u8 {
7567 install_channel_key(&mut session, &[seed; 32]);
7568 }
7569 let (emitted, _) = insert_item(&mut session, prop::HOST_CHANNEL_KEYS, &[0xFF; 32]);
7570 expect_status(&emitted[0], 5, Status::NOMEM);
7571 }
7572
7573 #[test]
7574 fn peer_key_lifecycle_replacement_and_secret_free_digests() {
7575 let mut session = test_session();
7576 let entry = peer_entry(0xA1);
7577
7578 let (emitted, _) = insert_item(&mut session, prop::HOST_PEER_KEYS, &entry);
7579 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
7580 assert_eq!(digest, entry[..32]);
7581 for frame in &emitted {
7583 assert!(!frame.windows(16).any(|window| window == &entry[32..48]));
7584 assert!(!frame.windows(16).any(|window| window == &entry[48..]));
7585 }
7586
7587 assert_eq!(get(&mut session, prop::HOST_PEER_KEYS), entry[..32]);
7589
7590 let mut replacement = entry;
7593 replacement[32..].fill(0x77);
7594 let (emitted, _) = insert_item(&mut session, prop::HOST_PEER_KEYS, &replacement);
7595 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
7596 assert_eq!(digest, entry[..32]);
7597 assert_eq!(get(&mut session, prop::HOST_PEER_KEYS), entry[..32]);
7598
7599 let (emitted, _) = remove_item(&mut session, prop::HOST_PEER_KEYS, &entry[..32]);
7601 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
7602 assert_eq!(digest, entry[..32]);
7603 assert!(get(&mut session, prop::HOST_PEER_KEYS).is_empty());
7604
7605 let (emitted, _) = remove_item(&mut session, prop::HOST_PEER_KEYS, &entry[..32]);
7606 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
7607
7608 let (emitted, _) = insert_item(&mut session, prop::HOST_PEER_KEYS, &entry[..63]);
7610 expect_status(&emitted[0], 5, Status::INVALID_ARGUMENT);
7611 }
7612
7613 #[test]
7614 fn key_table_whole_set_is_atomic_and_collapses_duplicates() {
7615 let mut session = test_session();
7616
7617 let key_a = [0xA0; 32];
7620 let key_b = [0xB0; 32];
7621 let mut table = Vec::new();
7622 table.extend_from_slice(&key_a);
7623 table.extend_from_slice(&key_b);
7624 table.extend_from_slice(&key_a);
7625 let (emitted, _) = set(&mut session, prop::HOST_CHANNEL_KEYS, &table);
7626 let (_, key, value) = parse_prop_is(&emitted[0]);
7627 assert_eq!(key, prop::HOST_CHANNEL_KEYS);
7628 assert_eq!(value.len(), 4, "two unique channels, 2-byte ids");
7629
7630 let (emitted, _) = set(&mut session, prop::HOST_CHANNEL_KEYS, &table[..40]);
7631 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7632 assert_eq!(get(&mut session, prop::HOST_CHANNEL_KEYS).len(), 4);
7633
7634 let mut peers = Vec::new();
7636 peers.extend_from_slice(&peer_entry(0x01));
7637 let mut updated = peer_entry(0x01);
7638 updated[32..].fill(0x99);
7639 peers.extend_from_slice(&updated);
7640 let (emitted, _) = set(&mut session, prop::HOST_PEER_KEYS, &peers);
7641 let (_, _, value) = parse_prop_is(&emitted[0]);
7642 assert_eq!(value, peer_entry(0x01)[..32], "one entry, digest form");
7643
7644 let (emitted, _) = set(&mut session, prop::HOST_PEER_KEYS, &[]);
7646 let (_, _, value) = parse_prop_is(&emitted[0]);
7647 assert!(value.is_empty());
7648 let mut oversized = Vec::new();
7649 for seed in 0..(MAX_PEER_KEYS + 1) as u8 {
7650 oversized.extend_from_slice(&peer_entry(seed));
7651 }
7652 let (emitted, _) = set(&mut session, prop::HOST_PEER_KEYS, &oversized);
7653 expect_status(&emitted[0], 2, Status::NOMEM);
7654 assert!(get(&mut session, prop::HOST_PEER_KEYS).is_empty());
7655 }
7656
7657 #[test]
7658 fn insecure_transport_refuses_key_writes() {
7659 let mut session = test_session();
7660 session.attach(false); for (key, item) in [
7663 (prop::HOST_CHANNEL_KEYS, &[0x42u8; 32][..]),
7664 (prop::HOST_PEER_KEYS, &peer_entry(0x01)[..]),
7665 ] {
7666 let (emitted, effect) = set(&mut session, key, item);
7667 assert!(effect.is_none());
7668 expect_status(&emitted[0], 2, Status::INVALID_STATE);
7669 let (emitted, _) = insert_item(&mut session, key, item);
7670 expect_status(&emitted[0], 5, Status::INVALID_STATE);
7671 assert!(get(&mut session, key).is_empty(), "table must stay empty");
7672 }
7673
7674 let (emitted, _) = set(&mut session, prop::PHY_DUTY_LIMIT, &100u16.to_le_bytes());
7676 let (_, key, _) = parse_prop_is(&emitted[0]);
7677 assert_eq!(key, prop::PHY_DUTY_LIMIT);
7678
7679 session.attach(true);
7681 install_channel_key(&mut session, &[0x42; 32]);
7682 }
7683
7684 #[test]
7685 fn provisioned_channel_id_is_an_implicit_filter() {
7686 let mut session = test_session();
7687 enable(&mut session);
7688 let id = install_channel_key(&mut session, &[0x42; 32]);
7692 assert!(delivered(&mut session, &multicast_on(id)));
7693 assert!(delivered(&mut session, &blind_unicast_on(id)));
7694 let other = [id[0] ^ 0xFF, id[1]];
7695 assert!(!delivered(&mut session, &multicast_on(other)));
7696 assert!(delivered(&mut session, &broadcast_frame()));
7698 assert!(!delivered(&mut session, &[0x00, 0x01, 0x02]));
7699
7700 session.detach();
7702 receive_detached(&mut session, &multicast_on(id), 0);
7703 receive_detached(&mut session, &multicast_on(other), 0);
7704 session.attach(true);
7705 assert_eq!(queue_count(&mut session), 1);
7706 }
7707
7708 #[test]
7709 fn host_replacement_clears_key_tables() {
7710 let mut session = test_session();
7711 install_channel_key(&mut session, &[0x42; 32]);
7712 insert_item(&mut session, prop::HOST_PEER_KEYS, &peer_entry(0x01));
7713
7714 install_host_key(&mut session, &[0xAA; 32]);
7715 assert!(get(&mut session, prop::HOST_CHANNEL_KEYS).is_empty());
7716 assert!(get(&mut session, prop::HOST_PEER_KEYS).is_empty());
7717
7718 install_channel_key(&mut session, &[0x42; 32]);
7720 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_| {});
7721 assert!(get(&mut session, prop::HOST_CHANNEL_KEYS).is_empty());
7722 }
7723
7724 use umsh_core::{MicSize, PublicKey};
7727
7728 const HOST_PUB: [u8; 32] = [0xC4; 32];
7729 const PEER_PUB: [u8; 32] = [0x0A; 32];
7730
7731 fn test_pairwise() -> PairwiseKeys {
7732 PairwiseKeys {
7733 k_enc: [0x5E; 16],
7734 k_mic: [0x5F; 16],
7735 }
7736 }
7737
7738 fn peer_item(public_key: &[u8; 32], keys: &PairwiseKeys) -> [u8; 64] {
7739 let mut item = [0u8; 64];
7740 item[..32].copy_from_slice(public_key);
7741 item[32..48].copy_from_slice(&keys.k_enc);
7742 item[48..].copy_from_slice(&keys.k_mic);
7743 item
7744 }
7745
7746 fn auto_ack_session() -> TestSession {
7749 let mut session = test_session();
7750 enable(&mut session);
7751 install_host_key(&mut session, &HOST_PUB);
7752 insert_item(
7753 &mut session,
7754 prop::HOST_PEER_KEYS,
7755 &peer_item(&PEER_PUB, &test_pairwise()),
7756 );
7757 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
7758 session.detach();
7759 session
7760 }
7761
7762 fn sealed_unar(counter: u32, keys: &PairwiseKeys, full_source: bool) -> Vec<u8> {
7765 let mut buf = [0u8; 96];
7766 let builder = PacketBuilder::new(&mut buf).unicast(NodeHint([0xC4, 0xC4, 0xC4]));
7767 let builder = if full_source {
7768 builder.source_full(&PublicKey(PEER_PUB))
7769 } else {
7770 builder.source_hint(NodeHint([0x0A, 0x0A, 0x0A]))
7771 };
7772 let mut packet = builder
7773 .frame_counter(counter)
7774 .ack_requested()
7775 .mic_size(MicSize::Mic8)
7776 .payload(&[3, 1, 2])
7777 .build()
7778 .unwrap();
7779 test_engine().seal_packet(&mut packet, keys).unwrap();
7780 packet.as_bytes().to_vec()
7781 }
7782
7783 fn sealed_buar(counter: u32, channel_key: &[u8; 32]) -> Vec<u8> {
7785 let engine = test_engine();
7786 let channel_keys = engine.derive_channel_keys(&ChannelKey(*channel_key));
7787 let mut buf = [0u8; 96];
7788 let mut packet = PacketBuilder::new(&mut buf)
7789 .blind_unicast(channel_keys.channel_id, NodeHint([0xC4, 0xC4, 0xC4]))
7790 .source_hint(NodeHint([0x0A, 0x0A, 0x0A]))
7791 .frame_counter(counter)
7792 .ack_requested()
7793 .encrypted()
7794 .mic_size(MicSize::Mic8)
7795 .payload(&[3, 9, 9])
7796 .build()
7797 .unwrap();
7798 let blind = engine.derive_blind_keys(&test_pairwise(), &channel_keys);
7799 engine
7800 .seal_blind_packet(&mut packet, &blind, &channel_keys)
7801 .unwrap();
7802 packet.as_bytes().to_vec()
7803 }
7804
7805 fn rx_effect(session: &mut TestSession, frame: &[u8], now_ms: u64) -> Option<Effect> {
7807 session.on_radio_rx(frame, -80, 40, None, now_ms, &mut |_: &[u8]| {
7808 panic!("detached receive must not emit")
7809 })
7810 }
7811
7812 fn expected_ack_trailer(frame: &[u8], keys: &PairwiseKeys) -> [u8; 8] {
7815 let engine = test_engine();
7816 let header = PacketHeader::parse(frame).unwrap();
7817 let mut cmac = engine.cmac_state(&keys.k_mic);
7818 umsh_core::feed_aad(&header, frame, |chunk| cmac.update(chunk));
7819 cmac.update(&frame[header.body_range.clone()]);
7820 engine.compute_ack_trailer(&cmac.finalize(), &keys.k_enc)
7821 }
7822
7823 fn expect_ack_transmit(
7826 session: &mut TestSession,
7827 effect: Option<Effect>,
7828 trailer: Option<[u8; 8]>,
7829 ) {
7830 assert_eq!(effect, Some(Effect::StartTransmit));
7831 let header = PacketHeader::parse(session.tx_data()).unwrap();
7832 assert_eq!(header.fcf.packet_type(), PacketType::MacAck);
7833 if let Some(trailer) = trailer {
7834 assert_eq!(session.tx_data()[header.mic_range.clone()], trailer);
7835 }
7836 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {
7837 panic!("autonomous ack must be silent")
7838 });
7839 }
7840
7841 #[test]
7842 fn unar_success_acks_queues_and_reports_acked() {
7843 let mut session = auto_ack_session();
7844 let frame = sealed_unar(100, &test_pairwise(), false);
7845 let effect = rx_effect(&mut session, &frame, 1_000);
7846 expect_ack_transmit(
7847 &mut session,
7848 effect,
7849 Some(expected_ack_trailer(&frame, &test_pairwise())),
7850 );
7851
7852 session.attach(true);
7855 assert_eq!(
7856 pui::decode(&get(&mut session, prop::LAST_STATUS))
7857 .unwrap()
7858 .0,
7859 Status::RESET_POWER_ON.0
7860 );
7861 assert_eq!(queue_count(&mut session), 1);
7862 let drained = drain(&mut session, 1_000);
7863 assert_eq!(
7864 drained[0].0, frame,
7865 "the queue holds the original wire bytes"
7866 );
7867 assert_eq!(drained[0].1.flags, RX_FLAG_BUFFERED | RX_FLAG_ACKED);
7868 }
7869
7870 #[test]
7871 fn buar_success_and_missing_channel_key() {
7872 let channel_key = [0x42; 32];
7873 let mut session = auto_ack_session();
7874 let frame = sealed_buar(7, &channel_key);
7877 assert!(rx_effect(&mut session, &frame, 0).is_none());
7878 session.attach(true);
7879 assert_eq!(queue_count(&mut session), 0);
7880
7881 install_channel_key(&mut session, &channel_key);
7884 session.detach();
7885 let effect = rx_effect(&mut session, &frame, 0);
7886 expect_ack_transmit(&mut session, effect, None);
7887 session.attach(true);
7888 assert_eq!(queue_count(&mut session), 1);
7889 let drained = drain(&mut session, 0);
7890 assert_eq!(drained[0].1.flags, RX_FLAG_BUFFERED | RX_FLAG_ACKED);
7891 }
7892
7893 #[test]
7894 fn unprovisioned_source_and_bad_mic_queue_unacked() {
7895 let mut session = auto_ack_session();
7896
7897 let wrong_keys = PairwiseKeys {
7901 k_enc: [1; 16],
7902 k_mic: [2; 16],
7903 };
7904 assert!(rx_effect(&mut session, &sealed_unar(5, &wrong_keys, false), 0).is_none());
7905
7906 let mut corrupted = sealed_unar(6, &test_pairwise(), false);
7909 let last = corrupted.len() - 1;
7910 corrupted[last] ^= 0xFF;
7911 assert!(rx_effect(&mut session, &corrupted, 0).is_none());
7912
7913 let effect = rx_effect(&mut session, &sealed_unar(1, &test_pairwise(), false), 0);
7916 expect_ack_transmit(&mut session, effect, None);
7917
7918 session.attach(true);
7919 assert_eq!(queue_count(&mut session), 3);
7920 let drained = drain(&mut session, 0);
7921 assert_eq!(drained[0].1.flags, RX_FLAG_BUFFERED);
7922 assert_eq!(drained[1].1.flags, RX_FLAG_BUFFERED);
7923 assert_eq!(drained[2].1.flags, RX_FLAG_BUFFERED | RX_FLAG_ACKED);
7924 }
7925
7926 #[test]
7927 fn ambiguous_source_hint_is_never_acked_but_full_key_resolves() {
7928 let mut session = auto_ack_session();
7929 let mut twin = PEER_PUB;
7932 twin[31] ^= 0xFF;
7933 session.attach(true);
7934 insert_item(
7935 &mut session,
7936 prop::HOST_PEER_KEYS,
7937 &peer_item(&twin, &test_pairwise()),
7938 );
7939 session.detach();
7940
7941 assert!(rx_effect(&mut session, &sealed_unar(4, &test_pairwise(), false), 0).is_none());
7943
7944 let effect = rx_effect(&mut session, &sealed_unar(4, &test_pairwise(), true), 0);
7946 expect_ack_transmit(&mut session, effect, None);
7947 }
7948
7949 #[test]
7950 fn duplicates_coalesce_and_reack_only_within_window() {
7951 let mut session = auto_ack_session();
7952 let keys = test_pairwise();
7953
7954 let first = sealed_unar(5, &keys, false);
7955 let effect = rx_effect(&mut session, &first, 0);
7956 expect_ack_transmit(
7957 &mut session,
7958 effect,
7959 Some(expected_ack_trailer(&first, &keys)),
7960 );
7961
7962 let effect = rx_effect(&mut session, &first, 10);
7964 expect_ack_transmit(
7965 &mut session,
7966 effect,
7967 Some(expected_ack_trailer(&first, &keys)),
7968 );
7969
7970 for counter in 6..=14 {
7972 let effect = rx_effect(&mut session, &sealed_unar(counter, &keys, false), 20);
7973 expect_ack_transmit(&mut session, effect, None);
7974 }
7975 assert!(rx_effect(&mut session, &first, 30).is_none());
7977
7978 let effect = rx_effect(&mut session, &sealed_unar(15, &keys, false), 40);
7981 expect_ack_transmit(&mut session, effect, None);
7982
7983 session.attach(true);
7984 assert_eq!(queue_count(&mut session), 12);
7987 }
7988
7989 #[test]
7990 fn attached_host_suppresses_delegation() {
7991 let mut session = auto_ack_session();
7992 session.attach(true);
7993 let frame = sealed_unar(5, &test_pairwise(), false);
7994 let mut emitted = Vec::new();
7995 let effect = session.on_radio_rx(&frame, -80, 40, None, 0, &mut |bytes: &[u8]| {
7996 emitted.push(bytes.to_vec())
7997 });
7998 assert!(effect.is_none());
8000 assert_eq!(emitted.len(), 1);
8001 }
8002
8003 #[test]
8004 fn auto_ack_disabled_and_duty_limit_leave_frames_unacked() {
8005 let mut session = auto_ack_session();
8006 session.attach(true);
8007 set(&mut session, prop::HOST_AUTO_ACK, &[0]);
8008 session.detach();
8009 assert!(rx_effect(&mut session, &sealed_unar(5, &test_pairwise(), false), 0).is_none());
8010
8011 session.attach(true);
8014 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
8015 set(&mut session, prop::PHY_DUTY_LIMIT, &0u16.to_le_bytes());
8016 session.detach();
8017 assert!(rx_effect(&mut session, &sealed_unar(6, &test_pairwise(), false), 0).is_none());
8018
8019 session.attach(true);
8020 assert_eq!(queue_count(&mut session), 2);
8021 for (_, meta) in drain(&mut session, 0) {
8022 assert_eq!(meta.flags, RX_FLAG_BUFFERED);
8023 }
8024 }
8025
8026 #[test]
8027 fn auto_ack_property_round_trips_and_resets() {
8028 let mut session = test_session();
8029 assert_eq!(get(&mut session, prop::HOST_AUTO_ACK), [0]);
8030 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
8031 assert_eq!(get(&mut session, prop::HOST_AUTO_ACK), [1]);
8032
8033 session.attach(true);
8035 assert_eq!(get(&mut session, prop::HOST_AUTO_ACK), [1]);
8036 install_host_key(&mut session, &[0xBB; 32]);
8037 assert_eq!(get(&mut session, prop::HOST_AUTO_ACK), [0]);
8038
8039 let (emitted, _) = set(&mut session, prop::HOST_AUTO_ACK, &[2]);
8040 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8041 }
8042
8043 #[test]
8044 fn peer_key_replacement_preserves_replay_baseline() {
8045 let mut session = auto_ack_session();
8046 let old_keys = test_pairwise();
8047 let effect = rx_effect(&mut session, &sealed_unar(5, &old_keys, false), 0);
8048 expect_ack_transmit(&mut session, effect, None);
8049
8050 session.attach(true);
8052 let new_keys = PairwiseKeys {
8053 k_enc: [0x77; 16],
8054 k_mic: [0x78; 16],
8055 };
8056 insert_item(
8057 &mut session,
8058 prop::HOST_PEER_KEYS,
8059 &peer_item(&PEER_PUB, &new_keys),
8060 );
8061 session.detach();
8062
8063 assert!(rx_effect(&mut session, &sealed_unar(5, &new_keys, false), 10).is_none());
8067 let effect = rx_effect(&mut session, &sealed_unar(6, &new_keys, false), 20);
8068 expect_ack_transmit(&mut session, effect, None);
8069 }
8070
8071 fn strip_snapshot_options(bytes: &[u8], drop: &[u32]) -> Vec<u8> {
8076 let (format, options) = bytes.split_first().unwrap();
8077 let mut out = vec![0u8; bytes.len()];
8078 out[0] = *format;
8079 let mut encoder = OptionEncoder::new(&mut out[1..]);
8080 for item in OptionDecoder::new(options) {
8081 let (number, value) = item.unwrap();
8082 if drop.contains(&u32::from(number)) {
8083 continue;
8084 }
8085 encoder.put(number, value).unwrap();
8086 }
8087 let len = 1 + encoder.finish();
8088 out.truncate(len);
8089 out
8090 }
8091
8092 fn save(session: &mut TestSession) {
8094 let mut buf = [0u8; 4];
8095 let len = frame::save(&mut buf, 3).unwrap();
8096 let (emitted, effect) = dispatch(session, &buf[..len], 0);
8097 assert!(emitted.is_empty(), "no response before the write commits");
8098 assert_eq!(effect, Some(Effect::SaveSnapshot { tid: 3 }));
8099 let mut emitted = Vec::new();
8100 session.respond_save(3, Ok(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
8101 expect_status(&emitted[0], 3, Status::OK);
8102 }
8103
8104 fn restore(session: &mut TestSession) -> Option<Effect> {
8106 let mut buf = [0u8; 4];
8107 let len = frame::restore(&mut buf, 5).unwrap();
8108 let (emitted, effect) = dispatch(session, &buf[..len], 0);
8109 expect_status(&emitted[0], TID_UNSOLICITED, Status::RESET_RESTORED);
8110 effect
8111 }
8112
8113 fn provisioned_session() -> TestSession {
8116 let mut session = test_session();
8117 set(&mut session, prop::PHY_FREQ, &906_875u32.to_le_bytes());
8118 enable(&mut session);
8119 set(&mut session, prop::DEV_NAME, b"saved name");
8120 install_host_key(&mut session, &HOST_PUB);
8121 insert_item(
8122 &mut session,
8123 prop::HOST_RX_FILTERS,
8124 &[items::FILTER_PKT_TYPE, 0],
8125 );
8126 install_channel_key(&mut session, &[0x42; 32]);
8127 insert_item(
8128 &mut session,
8129 prop::HOST_PEER_KEYS,
8130 &peer_item(&PEER_PUB, &test_pairwise()),
8131 );
8132 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
8133 session
8134 }
8135
8136 #[test]
8137 fn save_sets_prop_saved_and_failure_rolls_back() {
8138 let mut session = test_session();
8139 assert_eq!(get(&mut session, prop::SAVED), [0]);
8140
8141 let mut buf = [0u8; 4];
8143 let len = frame::save(&mut buf, 3).unwrap();
8144 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
8145 assert_eq!(effect, Some(Effect::SaveSnapshot { tid: 3 }));
8146 let mut emitted = Vec::new();
8147 session.respond_save(3, Err(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
8148 expect_status(&emitted[0], 3, Status::FAILURE);
8149 assert_eq!(get(&mut session, prop::SAVED), [0]);
8150
8151 save(&mut session);
8152 assert_eq!(get(&mut session, prop::SAVED), [1]);
8153
8154 let (emitted, _) = set(&mut session, prop::SAVED, &[0]);
8156 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8157 }
8158
8159 #[test]
8160 fn restore_without_snapshot_is_invalid_state() {
8161 let mut session = test_session();
8162 let mut buf = [0u8; 4];
8163 let len = frame::restore(&mut buf, 5).unwrap();
8164 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
8165 assert!(effect.is_none());
8166 expect_status(&emitted[0], 5, Status::INVALID_STATE);
8167 }
8168
8169 #[test]
8170 fn snapshot_round_trips_through_the_wire_encoding() {
8171 let session = provisioned_session();
8172 let mut bytes = [0u8; SNAPSHOT_MAX];
8173 let len = session.encode_snapshot(&mut bytes).unwrap();
8174
8175 let mut booted: TestSession =
8179 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8180 let effect = booted.restore_at_boot(&bytes[..len]).unwrap();
8181 assert!(matches!(effect, Effect::ApplyRadio(s) if s.enabled && s.freq_khz == 906_875));
8182 assert_eq!(booted.device_name(), "saved name");
8183 let id = test_engine().derive_channel_id(&ChannelKey([0x42; 32])).0;
8187 booted.on_radio_rx(&multicast_on(id), -80, 40, None, 0, &mut |_: &[u8]| {
8188 panic!("detached boot must not emit")
8189 });
8190 booted.attach(true);
8191 assert_eq!(get(&mut booted, prop::SAVED), [ids::saved::CURRENT]);
8192 assert_eq!(get(&mut booted, prop::HOST_KEY), Vec::<u8>::new());
8193 assert_eq!(get(&mut booted, prop::HOST_AUTO_ACK), [0]);
8194 assert_eq!(queue_count(&mut booted), 1);
8198
8199 let mut fresh: TestSession =
8203 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8204 assert_eq!(
8205 fresh.restore_at_boot(&bytes[..len - 1]),
8206 Err(SnapshotError::Malformed)
8207 );
8208 let mut wrong_format = bytes;
8209 wrong_format[0] = SNAPSHOT_FORMAT + 1;
8210 assert_eq!(
8211 fresh.restore_at_boot(&wrong_format[..len]),
8212 Err(SnapshotError::UnknownFormat)
8213 );
8214 let mut legacy = bytes;
8217 legacy[0] = 3;
8218 assert_eq!(
8219 fresh.restore_at_boot(&legacy[..len]),
8220 Err(SnapshotError::UnknownFormat)
8221 );
8222 fresh.attach(true);
8223 assert_eq!(get(&mut fresh, prop::SAVED), [ids::saved::NONE]);
8224 fresh.note_snapshot_rejected();
8225 assert_eq!(get(&mut fresh, prop::SAVED), [ids::saved::UNREADABLE]);
8226 }
8227
8228 #[test]
8234 fn positioning_settings_survive_a_save_and_the_clock_does_not() {
8235 let mut session = test_session();
8236 set(&mut session, prop::GNSS_ENABLED, &[1]);
8237 set(&mut session, prop::TZ_OFFSET, &(-300i16).to_le_bytes());
8238 set(&mut session, prop::GNSS_IDENT_UPDATE, &[1]);
8239 set(&mut session, prop::GNSS_IDENT_PRECISION, &[6]);
8240 set(&mut session, prop::GNSS_TIME_TRUST, &[0]);
8241 set(&mut session, prop::TIME, &1_780_000_000u32.to_le_bytes());
8242
8243 let mut bytes = [0u8; SNAPSHOT_MAX];
8244 let len = session.encode_snapshot(&mut bytes).unwrap();
8245
8246 let mut booted: TestSession =
8247 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8248 booted.restore_at_boot(&bytes[..len]).unwrap();
8249 assert!(booted.gnss_enabled());
8250 assert_eq!(booted.tz_offset_min(), -300);
8251 assert!(booted.gnss_ident_update());
8252 assert_eq!(booted.gnss_ident_precision(), 6);
8253 assert!(!booted.gnss_time_trust());
8254 booted.attach(true);
8255 assert_eq!(get(&mut booted, prop::GNSS_ENABLED), [1]);
8256 assert_eq!(get(&mut booted, prop::GNSS_TIME_TRUST), [0]);
8257 let mut buf = [0u8; 16];
8260 let get_len = frame::prop_get(&mut buf, 3, prop::TIME).unwrap();
8261 let (_, effect) = dispatch(&mut booted, &buf[..get_len], 0);
8262 assert_eq!(effect, Some(Effect::ReadTime { tid: 3 }));
8263
8264 let mut out = Vec::new();
8267 booted.reset(Status::RESET_SOFTWARE, &mut |bytes: &[u8]| {
8268 out.push(bytes.to_vec())
8269 });
8270 assert!(booted.gnss_enabled());
8271 assert_eq!(booted.tz_offset_min(), -300);
8272 }
8273
8274 #[test]
8279 fn restore_under_a_different_identity_leaves_the_phy_disabled() {
8280 let mut session = provisioned_session();
8281 session.set_boot_identity([0xAA; 32]);
8282 let mut bytes = [0u8; SNAPSHOT_MAX];
8283 let len = session.encode_snapshot(&mut bytes).unwrap();
8284
8285 let mut replacement: TestSession =
8287 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8288 replacement.set_boot_identity([0xBB; 32]);
8289 let effect = replacement.restore_at_boot(&bytes[..len]).unwrap();
8290 assert!(matches!(effect, Effect::ApplyRadio(s) if !s.enabled));
8291 assert_eq!(replacement.device_name(), "saved name");
8293 assert_eq!(replacement.settings().freq_khz, 906_875);
8294
8295 let mut same: TestSession =
8298 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8299 same.set_boot_identity([0xAA; 32]);
8300 let effect = same.restore_at_boot(&bytes[..len]).unwrap();
8301 assert!(matches!(effect, Effect::ApplyRadio(s) if s.enabled));
8302 }
8303
8304 #[test]
8309 fn saved_schema_orders_by_identifier_and_applies_by_phase() {
8310 assert!(
8311 SAVED_SCHEMA
8312 .windows(2)
8313 .all(|pair| pair[0].number < pair[1].number),
8314 "the option encoder rejects a number below the last one written"
8315 );
8316 let enable = SAVED_SCHEMA
8317 .iter()
8318 .find(|entry| u32::from(entry.number) == prop::PHY_ENABLED)
8319 .expect("PHY_ENABLED is saved");
8320 assert_eq!(enable.phase, ApplyPhase::Enable);
8321 assert_eq!(*ApplyPhase::ORDER.last().unwrap(), ApplyPhase::Enable);
8322 assert!(
8323 SAVED_SCHEMA
8324 .iter()
8325 .filter(|entry| entry.phase == ApplyPhase::Enable)
8326 .count()
8327 == 1,
8328 "only the PHY enable belongs in the last phase"
8329 );
8330 assert!(u32::from(enable.number) < prop::PHY_FREQ);
8332 }
8333
8334 #[test]
8338 fn absent_options_decode_to_post_reset_defaults() {
8339 let mut bare = [0u8; SNAPSHOT_MAX];
8340 bare[0] = SNAPSHOT_FORMAT;
8341 let mut booted: TestSession =
8342 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8343 let effect = booted.restore_at_boot(&bare[..1]).unwrap();
8344 let defaults = test_config().defaults;
8345 assert!(matches!(
8346 effect,
8347 Effect::ApplyRadio(s) if !s.enabled
8348 && s.freq_khz == defaults.freq_khz
8349 && s.sf == defaults.sf
8350 ));
8351 assert_eq!(booted.device_name(), test_config().default_device_name);
8352 booted.attach(true);
8353 assert_eq!(get(&mut booted, prop::SAVED), [ids::saved::CURRENT]);
8354 assert_eq!(get(&mut booted, prop::MAC_REPEATER_ENABLED), [0]);
8355 assert_eq!(get(&mut booted, prop::HOST_KEY), [] as [u8; 0]);
8356 }
8357
8358 #[test]
8362 fn unknown_options_are_skipped() {
8363 let session = provisioned_session();
8364 let mut bytes = [0u8; SNAPSHOT_MAX];
8365 let len = session.encode_snapshot(&mut bytes).unwrap();
8366 let mut extended = bytes;
8368 let mut encoder =
8369 OptionEncoder::with_last_number(&mut extended[len..], prop::PHY_DUTY_LIMIT as u16);
8370 encoder.put(60000, &[1, 2, 3]).unwrap();
8371 let extra = encoder.finish();
8372
8373 let mut booted: TestSession =
8374 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8375 booted.restore_at_boot(&extended[..len + extra]).unwrap();
8376 assert_eq!(booted.device_name(), "saved name");
8377 }
8378
8379 #[test]
8382 fn a_repeated_single_valued_property_is_rejected() {
8383 let mut bytes = [0u8; SNAPSHOT_MAX];
8384 bytes[0] = SNAPSHOT_FORMAT;
8385 let mut encoder = OptionEncoder::new(&mut bytes[1..]);
8386 encoder.put(prop::PHY_LORA_SF as u16, &[9]).unwrap();
8387 encoder.put(prop::PHY_LORA_SF as u16, &[10]).unwrap();
8388 let len = 1 + encoder.finish();
8389
8390 let mut booted: TestSession =
8391 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8392 assert_eq!(
8393 booted.restore_at_boot(&bytes[..len]),
8394 Err(SnapshotError::Malformed)
8395 );
8396 }
8397
8398 #[test]
8402 fn out_of_range_snapshot_values_are_rejected() {
8403 for (number, value) in [
8404 (prop::PHY_LORA_SF, &[13u8][..]),
8405 (prop::PHY_LORA_CR, &[4][..]),
8406 (prop::PHY_LORA_BW, &7_000u32.to_le_bytes()[..]),
8407 (prop::PHY_ENABLED, &[2][..]),
8408 (prop::DEV_NAME, &[][..]),
8409 ] {
8410 let mut bytes = [0u8; SNAPSHOT_MAX];
8411 bytes[0] = SNAPSHOT_FORMAT;
8412 let mut encoder = OptionEncoder::new(&mut bytes[1..]);
8413 encoder.put(number as u16, value).unwrap();
8414 let len = 1 + encoder.finish();
8415
8416 let mut booted: TestSession =
8417 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8418 assert_eq!(
8419 booted.restore_at_boot(&bytes[..len]),
8420 Err(SnapshotError::InvalidValue),
8421 "property {number} accepted an out-of-range value"
8422 );
8423 }
8424 }
8425
8426 #[test]
8430 fn snapshot_at_capacity_fits_the_buffer() {
8431 let mut session = provisioned_session();
8432 for seed in 0..MAX_CHANNEL_KEYS as u8 {
8433 let _ = session.device.channel_keys.insert(ChannelKeyEntry {
8434 key: [seed; items::CHANNEL_KEY_LEN],
8435 id: [seed, seed],
8436 });
8437 }
8438 for seed in 0..MAX_DEV_PEERS as u8 {
8439 let _ = session.device.peers.insert([seed; items::PUBLIC_KEY_LEN]);
8440 }
8441 session.device.name = [b'n'; MAX_DEVICE_NAME_LEN];
8442 session.device.name_len = MAX_DEVICE_NAME_LEN;
8443 session.set_boot_identity([0xEE; 32]);
8444
8445 let mut bytes = [0u8; SNAPSHOT_MAX];
8446 let len = session
8447 .encode_snapshot(&mut bytes)
8448 .expect("a full snapshot must fit SNAPSHOT_MAX");
8449 assert!(len <= SNAPSHOT_MAX);
8450
8451 let mut booted: TestSession =
8452 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8453 booted.set_boot_identity([0xEE; 32]);
8454 booted.restore_at_boot(&bytes[..len]).unwrap();
8455 assert_eq!(booted.device.channel_keys.len, MAX_CHANNEL_KEYS);
8456 assert_eq!(booted.device.peers.len, MAX_DEV_PEERS);
8457 }
8458
8459 #[test]
8460 fn reset_post_reset_values_come_from_the_snapshot() {
8461 let mut session = provisioned_session();
8462 save(&mut session);
8463
8464 set(&mut session, prop::PHY_FREQ, &915_000u32.to_le_bytes());
8466 set(&mut session, prop::DEV_NAME, b"diverged");
8467 let mut emitted = Vec::new();
8468 let effect = session.reset(Status::RESET_SOFTWARE, &mut |bytes: &[u8]| {
8469 emitted.push(bytes.to_vec())
8470 });
8471 assert!(matches!(effect, Effect::ApplyRadio(s) if s.enabled && s.freq_khz == 906_875));
8474 assert_eq!(session.device_name(), "saved name");
8475 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
8478
8479 let mut buf = [0u8; 4];
8481 let len = frame::clear(&mut buf, 4).unwrap();
8482 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
8483 assert_eq!(effect, Some(Effect::ClearSaved { tid: 4 }));
8484 session.respond_clear(4, Ok(()), &mut |_: &[u8]| {});
8485 let effect = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
8486 assert!(matches!(effect, Effect::ApplyRadio(s) if !s.enabled && s.freq_khz == 910_525));
8487 assert_eq!(session.device_name(), "Test UMSH Device");
8488 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
8489 }
8490
8491 #[test]
8492 fn restore_reverts_config_but_preserves_queue_and_baselines() {
8493 let mut session = provisioned_session();
8494 save(&mut session);
8495
8496 session.detach();
8499 let first = sealed_unar(5, &test_pairwise(), false);
8500 let effect = rx_effect(&mut session, &first, 0);
8501 expect_ack_transmit(&mut session, effect, None);
8502 session.attach(true);
8503 assert_eq!(queue_count(&mut session), 1);
8504
8505 set(&mut session, prop::PHY_DUTY_LIMIT, &77u16.to_le_bytes());
8507 insert_item(
8508 &mut session,
8509 prop::HOST_RX_FILTERS,
8510 &[items::FILTER_DEST_HINT, 9, 9, 9],
8511 );
8512
8513 let effect = restore(&mut session);
8516 assert!(matches!(effect, Some(Effect::ApplyRadio(s)) if s.enabled));
8517 assert_eq!(
8518 get(&mut session, prop::PHY_DUTY_LIMIT),
8519 0xFFFFu16.to_le_bytes()
8520 );
8521 let filters = get(&mut session, prop::HOST_RX_FILTERS);
8524 assert_eq!(
8525 filters,
8526 [2, items::FILTER_PKT_TYPE, 0, 4, 0, 9, 9, 9],
8527 "host domain untouched by a restore"
8528 );
8529 assert_eq!(queue_count(&mut session), 1);
8530
8531 session.detach();
8535 let effect = rx_effect(&mut session, &first, 10);
8536 expect_ack_transmit(&mut session, effect, None);
8537 session.attach(true);
8538 assert_eq!(queue_count(&mut session), 1);
8539 }
8540
8541 #[test]
8545 fn restore_leaves_the_host_domain_untouched() {
8546 let mut session = provisioned_session();
8547 save(&mut session);
8548
8549 install_host_key(&mut session, &[0xBB; 32]);
8552 assert_eq!(get(&mut session, prop::SAVED), [ids::saved::CURRENT]);
8553 session.detach();
8554 assert!(!delivered_at(
8555 &mut session,
8556 &unicast_to([0xBB, 0xBB, 0xBB]),
8557 0
8558 ));
8559 session.attach(true);
8560 assert_eq!(queue_count(&mut session), 1);
8561
8562 let effect = restore(&mut session);
8563 assert!(matches!(effect, Some(Effect::ApplyRadio(_))));
8564 assert_eq!(session.device_name(), "saved name");
8566 assert_eq!(get(&mut session, prop::HOST_KEY), [0xBB; 32]);
8568 assert_eq!(queue_count(&mut session), 1);
8569 }
8570
8571 #[test]
8575 fn a_saved_snapshot_carries_no_host_domain_across_a_reboot() {
8576 let mut session = provisioned_session();
8577 save(&mut session);
8578 let mut bytes = [0u8; SNAPSHOT_MAX];
8579 let len = session.encode_snapshot(&mut bytes).unwrap();
8580
8581 let mut booted: TestSession =
8582 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8583 booted.restore_at_boot(&bytes[..len]).unwrap();
8584 booted.attach(true);
8585 assert_eq!(booted.device_name(), "saved name");
8586 assert_eq!(get(&mut booted, prop::HOST_KEY), Vec::<u8>::new());
8587 assert!(get(&mut booted, prop::HOST_CHANNEL_KEYS).is_empty());
8588 assert!(get(&mut booted, prop::HOST_PEER_KEYS).is_empty());
8589 assert!(get(&mut booted, prop::HOST_RX_FILTERS).is_empty());
8590 assert_eq!(get(&mut booted, prop::HOST_AUTO_ACK), [0]);
8591 }
8592
8593 #[test]
8598 fn a_whole_table_peer_set_reconciles_rather_than_rebuilding() {
8599 let mut session = test_session();
8600 enable(&mut session);
8601 install_host_key(&mut session, &HOST_PUB);
8602 install_channel_key(&mut session, &[0x42; 32]);
8603 set(
8604 &mut session,
8605 prop::HOST_PEER_KEYS,
8606 &peer_item(&PEER_PUB, &test_pairwise()),
8607 );
8608 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
8609
8610 session.detach();
8612 let frame = sealed_unar(9, &test_pairwise(), false);
8613 let effect = rx_effect(&mut session, &frame, 0);
8614 expect_ack_transmit(&mut session, effect, None);
8615 session.attach(true);
8616 assert_eq!(queue_count(&mut session), 1);
8617 let _ = drain(&mut session, 0);
8618
8619 let mut table = peer_item(&PEER_PUB, &test_pairwise()).to_vec();
8623 table.extend_from_slice(&peer_item(&[0x77; 32], &test_pairwise()));
8624 set(&mut session, prop::HOST_PEER_KEYS, &table);
8625 assert_eq!(get(&mut session, prop::HOST_PEER_KEYS).len(), 64);
8626
8627 session.detach();
8628 let effect = rx_effect(&mut session, &frame, 10);
8629 expect_ack_transmit(&mut session, effect, None);
8630 session.attach(true);
8631 assert_eq!(
8632 queue_count(&mut session),
8633 0,
8634 "a preserved baseline coalesces the replay instead of queueing it again"
8635 );
8636
8637 set(
8640 &mut session,
8641 prop::HOST_PEER_KEYS,
8642 &peer_item(&[0x77; 32], &test_pairwise()),
8643 );
8644 assert_eq!(get(&mut session, prop::HOST_PEER_KEYS), [0x77; 32]);
8645 }
8646
8647 fn drained_flags(session: &mut TestSession) -> Vec<u8> {
8652 drain(session, 0)
8653 .into_iter()
8654 .map(|(_, meta)| meta.flags)
8655 .collect()
8656 }
8657
8658 #[test]
8659 fn ack_flag_requires_confirmed_transmission() {
8660 let mut session = auto_ack_session();
8661 let keys = test_pairwise();
8662 let frame = sealed_unar(5, &keys, false);
8663
8664 let effect = rx_effect(&mut session, &frame, 0);
8667 assert_eq!(effect, Some(Effect::StartTransmit));
8668 session.on_tx_result(TxOutcome::Failed, 0, &mut |_: &[u8]| {
8669 panic!("autonomous ack is silent")
8670 });
8671
8672 let effect = rx_effect(&mut session, &frame, 10);
8675 assert_eq!(
8676 effect,
8677 Some(Effect::StartTransmit),
8678 "re-ack after failed TX"
8679 );
8680 session.on_tx_result(TxOutcome::Sent, 10, &mut |_: &[u8]| {
8681 panic!("autonomous ack is silent")
8682 });
8683
8684 session.attach(true);
8685 assert_eq!(queue_count(&mut session), 1, "duplicate coalesced");
8686 assert_eq!(
8687 drained_flags(&mut session),
8688 [RX_FLAG_BUFFERED | RX_FLAG_ACKED]
8689 );
8690 }
8691
8692 #[test]
8693 fn failed_ack_leaves_flag_clear_and_eviction_is_handle_safe() {
8694 let mut session = auto_ack_session();
8695 let keys = test_pairwise();
8696
8697 let effect = rx_effect(&mut session, &sealed_unar(1, &keys, false), 0);
8699 assert_eq!(effect, Some(Effect::StartTransmit));
8700 session.on_tx_result(TxOutcome::Failed, 0, &mut |_: &[u8]| {});
8701
8702 let effect = rx_effect(&mut session, &sealed_unar(2, &keys, false), 0);
8708 assert_eq!(effect, Some(Effect::StartTransmit));
8709 for counter in 3..(2 + RX_QUEUE_CAPACITY as u32) {
8710 assert!(rx_effect(&mut session, &sealed_unar(counter, &keys, false), 0).is_none());
8712 }
8713 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
8714
8715 session.attach(true);
8716 assert_eq!(queue_count(&mut session), RX_QUEUE_CAPACITY as u16);
8717 let flags = drained_flags(&mut session);
8718 assert_eq!(flags[0], RX_FLAG_BUFFERED | RX_FLAG_ACKED);
8721 assert!(
8722 flags[1..].iter().all(|flags| *flags == RX_FLAG_BUFFERED),
8723 "no other entry may borrow the confirmation"
8724 );
8725 }
8726
8727 fn sealed_flooded_unar(counter: u32, keys: &PairwiseKeys, accumulated: u8) -> Vec<u8> {
8732 let mut buf = [0u8; 96];
8733 let mut packet = PacketBuilder::new(&mut buf)
8734 .unicast(NodeHint([0xC4, 0xC4, 0xC4]))
8735 .source_hint(NodeHint([0x0A, 0x0A, 0x0A]))
8736 .frame_counter(counter)
8737 .ack_requested()
8738 .mic_size(MicSize::Mic8)
8739 .flood_hops(15)
8740 .payload(&[3, 1, 2])
8741 .build()
8742 .unwrap();
8743 test_engine().seal_packet(&mut packet, keys).unwrap();
8744 let mut frame = packet.as_bytes().to_vec();
8745 frame[1] = umsh_core::FloodHops::new(15 - accumulated, accumulated)
8746 .unwrap()
8747 .0;
8748 frame
8749 }
8750
8751 #[test]
8752 fn flooded_traffic_gets_flood_return_acks() {
8753 let mut session = auto_ack_session();
8754 let keys = test_pairwise();
8755
8756 let effect = rx_effect(&mut session, &sealed_unar(1, &keys, false), 0);
8758 assert_eq!(effect, Some(Effect::StartTransmit));
8759 let header = PacketHeader::parse(session.tx_data()).unwrap();
8760 assert_eq!(header.flood_hops, None);
8761 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
8762
8763 for (accumulated, expected_remaining) in [(3u8, 3u8), (0, 1), (15, 15)] {
8766 let frame = sealed_flooded_unar(u32::from(accumulated) + 10, &keys, accumulated);
8767 let effect = rx_effect(&mut session, &frame, 0);
8768 assert_eq!(
8769 effect,
8770 Some(Effect::StartTransmit),
8771 "accumulated={accumulated}"
8772 );
8773 let header = PacketHeader::parse(session.tx_data()).unwrap();
8774 assert_eq!(header.fcf.packet_type(), PacketType::MacAck);
8775 let hops = header.flood_hops.expect("flood-return ack");
8776 assert_eq!(
8777 hops.remaining(),
8778 expected_remaining,
8779 "accumulated={accumulated}"
8780 );
8781 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
8782 }
8783
8784 let retransmission = sealed_flooded_unar(25, &keys, 7);
8789 let effect = rx_effect(&mut session, &retransmission, 5);
8790 assert_eq!(effect, Some(Effect::StartTransmit));
8791 let header = PacketHeader::parse(session.tx_data()).unwrap();
8792 assert_eq!(
8793 header.flood_hops.expect("flood-return re-ack").remaining(),
8794 7
8795 );
8796 }
8797
8798 fn sealed_multicast(counter: u32, channel_key: &[u8; 32], fill: u8) -> Vec<u8> {
8801 let engine = test_engine();
8802 let derived = engine.derive_channel_keys(&ChannelKey(*channel_key));
8803 let mut buf = [0u8; 96];
8804 let mut packet = PacketBuilder::new(&mut buf)
8805 .multicast(derived.channel_id)
8806 .source_hint(NodeHint([0x0A, 0x0A, 0x0A]))
8807 .frame_counter(counter)
8808 .mic_size(MicSize::Mic8)
8809 .payload(&[3, fill])
8810 .build()
8811 .unwrap();
8812 let keys = PairwiseKeys {
8813 k_enc: derived.k_enc,
8814 k_mic: derived.k_mic,
8815 };
8816 engine.seal_packet(&mut packet, &keys).unwrap();
8817 packet.as_bytes().to_vec()
8818 }
8819
8820 #[test]
8821 fn authenticated_multicast_duplicates_coalesce_queue_locally() {
8822 let channel_key = [0x42u8; 32];
8823 let mut session = auto_ack_session();
8824 session.attach(true);
8825 install_channel_key(&mut session, &channel_key);
8826 session.detach();
8827
8828 let frame = sealed_multicast(9, &channel_key, 0x11);
8831 assert!(rx_effect(&mut session, &frame, 0).is_none());
8832 assert!(rx_effect(&mut session, &frame, 5).is_none());
8833 assert!(rx_effect(&mut session, &sealed_multicast(10, &channel_key, 0x11), 0).is_none());
8835 assert!(rx_effect(&mut session, &sealed_multicast(11, &channel_key, 0x22), 0).is_none());
8836
8837 session.attach(true);
8838 assert_eq!(queue_count(&mut session), 3);
8839 for flags in drained_flags(&mut session) {
8840 assert_eq!(flags, RX_FLAG_BUFFERED, "multicast is never acked");
8841 }
8842 }
8843
8844 #[test]
8845 fn unauthenticated_multicast_duplicates_occupy_separate_entries() {
8846 let mut session = test_session();
8850 enable(&mut session);
8851 insert_item(
8852 &mut session,
8853 prop::HOST_RX_FILTERS,
8854 &[items::FILTER_PKT_TYPE, PacketType::Multicast as u8],
8855 );
8856 session.detach();
8857 let frame = sealed_multicast(9, &[0x42; 32], 0x11);
8858 receive_detached(&mut session, &frame, 0);
8859 receive_detached(&mut session, &frame, 0);
8860 session.attach(true);
8861 assert_eq!(queue_count(&mut session), 2);
8862 }
8863
8864 fn public_of(secret: &[u8; 32]) -> [u8; 32] {
8868 use umsh_crypto::NodeIdentity;
8869 umsh_crypto::software::SoftwareIdentity::from_secret_bytes(secret)
8870 .public_key()
8871 .0
8872 }
8873
8874 fn provision_identity(session: &mut TestSession, tid: u8, secret: &[u8; 32]) -> [u8; 32] {
8878 let mut buf = [0u8; 64];
8879 let len = frame::prop_set(&mut buf, tid, prop::DEV_PRIVATE_KEY, secret).unwrap();
8880 let (emitted, effect) = dispatch(session, &buf[..len], 0);
8881 assert!(
8882 emitted.is_empty(),
8883 "no response before the identity is stored"
8884 );
8885 assert_eq!(effect, Some(Effect::ProvisionIdentity { tid }));
8886 let Some(IdentitySource::Install(staged)) = session.identity_request() else {
8887 panic!("staged request must carry the installed secret");
8888 };
8889 assert_eq!(staged, *secret);
8890 let public_key = public_of(&staged);
8891 let mut emitted = Vec::new();
8892 session.respond_identity(tid, Ok(public_key), &mut |bytes: &[u8]| {
8893 emitted.push(bytes.to_vec())
8894 });
8895 let (response_tid, key, value) = parse_prop_is(&emitted[0]);
8896 assert_eq!(response_tid, tid);
8897 assert_eq!(key, prop::DEV_KEY, "success is announced as the public key");
8898 assert_eq!(value, public_key);
8899 public_key
8900 }
8901
8902 #[test]
8903 fn device_identity_provisioning_lifecycle() {
8904 let mut session = test_session();
8905 assert!(get(&mut session, prop::DEV_KEY).is_empty());
8908 let mut buf = [0u8; 16];
8909 let len = frame::prop_get(&mut buf, 4, prop::DEV_PRIVATE_KEY).unwrap();
8910 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
8911 expect_status(&emitted[0], 4, Status::UNIMPLEMENTED);
8912
8913 let (emitted, _) = set(&mut session, prop::DEV_KEY, &[0x55; 32]);
8915 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8916
8917 let (emitted, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, &[0x11; 31]);
8919 assert!(effect.is_none());
8920 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8921
8922 let public_key = provision_identity(&mut session, 7, &[0x11; 32]);
8923 assert_eq!(get(&mut session, prop::DEV_KEY), public_key);
8924
8925 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
8928 assert_eq!(get(&mut session, prop::DEV_KEY), public_key);
8929
8930 insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
8933 let replaced = provision_identity(&mut session, 3, &[0x22; 32]);
8934 assert_ne!(replaced, public_key);
8935 assert_eq!(get(&mut session, prop::DEV_KEY), replaced);
8936 assert_eq!(get(&mut session, prop::DEV_PEERS), [0xD0; 32]);
8937 }
8938
8939 #[test]
8940 fn identity_generation_stages_and_concurrent_writes_are_busy() {
8941 let mut session = test_session();
8942 let (emitted, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, &[]);
8944 assert!(emitted.is_empty());
8945 assert_eq!(effect, Some(Effect::ProvisionIdentity { tid: 2 }));
8946 assert!(matches!(
8947 session.identity_request(),
8948 Some(IdentitySource::Generate)
8949 ));
8950
8951 let (emitted, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, &[0x33; 32]);
8953 assert!(effect.is_none());
8954 expect_status(&emitted[0], 2, Status::BUSY);
8955
8956 let generated = public_of(&[0x5A; 32]);
8959 let mut emitted = Vec::new();
8960 session.respond_identity(2, Ok(generated), &mut |bytes: &[u8]| {
8961 emitted.push(bytes.to_vec())
8962 });
8963 let (_, key, value) = parse_prop_is(&emitted[0]);
8964 assert_eq!(key, prop::DEV_KEY);
8965 assert_eq!(value, generated);
8966 assert!(session.identity_request().is_none());
8967 }
8968
8969 #[test]
8970 fn identity_provisioning_failure_leaves_the_identity_unchanged() {
8971 let mut session = test_session();
8972 let original = provision_identity(&mut session, 7, &[0x11; 32]);
8973
8974 let (_, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, &[0x22; 32]);
8975 assert_eq!(effect, Some(Effect::ProvisionIdentity { tid: 2 }));
8976 let mut emitted = Vec::new();
8977 session.respond_identity(2, Err(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
8978 expect_status(&emitted[0], 2, Status::FAILURE);
8979 assert_eq!(get(&mut session, prop::DEV_KEY), original);
8980 assert!(session.identity_request().is_none());
8981 }
8982
8983 #[test]
8984 fn identity_and_dev_channel_writes_require_a_secure_link() {
8985 let mut session = test_session();
8986 session.attach(false);
8987
8988 for value in [&[0x11u8; 32][..], &[][..]] {
8990 let (emitted, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, value);
8991 assert!(effect.is_none());
8992 expect_status(&emitted[0], 2, Status::INVALID_STATE);
8993 }
8994 let (emitted, _) = insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &[0x42; 32]);
8995 expect_status(&emitted[0], 5, Status::INVALID_STATE);
8996 let (emitted, _) = set(&mut session, prop::DEV_CHANNEL_KEYS, &[0x42; 32]);
8997 expect_status(&emitted[0], 2, Status::INVALID_STATE);
8998
8999 let (emitted, _) = insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
9002 let (key, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
9003 assert_eq!(key, prop::DEV_PEERS);
9004 assert_eq!(digest, [0xD0; 32]);
9005 }
9006
9007 #[test]
9008 fn dev_channel_keys_and_peers_lifecycle() {
9009 let mut session = test_session();
9010 let dev_channel = [0x66u8; 32];
9011 let expected_id = test_engine().derive_channel_id(&ChannelKey(dev_channel)).0;
9012
9013 let (emitted, _) = insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
9016 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
9017 assert_eq!(digest, expected_id);
9018 assert_eq!(get(&mut session, prop::DEV_CHANNEL_KEYS), expected_id);
9019 let (emitted, _) = insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
9020 expect_status(&emitted[0], 5, Status::ALREADY);
9021
9022 let (emitted, _) = insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
9025 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
9026 assert_eq!(digest, [0xD0; 32]);
9027 let (emitted, _) = insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
9028 expect_status(&emitted[0], 5, Status::ALREADY);
9029 let mut two = Vec::new();
9030 two.extend_from_slice(&[0xD1; 32]);
9031 two.extend_from_slice(&[0xD1; 32]);
9032 let (emitted, _) = set(&mut session, prop::DEV_PEERS, &two);
9033 let (_, key, value) = parse_prop_is(&emitted[0]);
9034 assert_eq!(key, prop::DEV_PEERS);
9035 assert_eq!(value, [0xD1; 32], "duplicate items collapse");
9036
9037 let (emitted, _) = remove_item(&mut session, prop::DEV_PEERS, &[0xD1; 32]);
9039 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
9040 assert_eq!(digest, [0xD1; 32]);
9041 let (emitted, _) = remove_item(&mut session, prop::DEV_PEERS, &[0xD1; 32]);
9042 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
9043 let (emitted, _) = remove_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
9044 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
9045 assert_eq!(digest, expected_id);
9046
9047 for seed in 0..MAX_DEV_PEERS as u8 {
9049 insert_item(&mut session, prop::DEV_PEERS, &[seed; 32]);
9050 }
9051 let (emitted, _) = insert_item(&mut session, prop::DEV_PEERS, &[0xFF; 32]);
9052 expect_status(&emitted[0], 5, Status::NOMEM);
9053 }
9054
9055 #[test]
9056 fn dev_domain_version_tracks_node_table_changes() {
9057 let mut session = test_session();
9058 assert_eq!(session.dev_domain_version(), 0);
9059 assert_eq!(session.dev_channel_keys().count(), 0);
9060 assert_eq!(session.dev_peers().count(), 0);
9061 assert!(session.dev_key().is_none());
9062
9063 let dev_channel = [0x66u8; 32];
9066 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
9067 assert_eq!(session.dev_domain_version(), 1);
9068 assert_eq!(
9069 session.dev_channel_keys().collect::<Vec<_>>(),
9070 [dev_channel]
9071 );
9072 insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
9073 assert_eq!(session.dev_domain_version(), 2);
9074 assert_eq!(session.dev_peers().collect::<Vec<_>>(), [[0xD0; 32]]);
9075
9076 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
9078 remove_item(&mut session, prop::DEV_PEERS, &[0xEE; 32]);
9079 assert_eq!(session.dev_domain_version(), 2);
9080
9081 insert_item(&mut session, prop::HOST_CHANNEL_KEYS, &[0x42; 32]);
9084 assert_eq!(session.dev_domain_version(), 2);
9085
9086 set(&mut session, prop::DEV_PEERS, &[0xD1; 32]);
9088 assert_eq!(session.dev_domain_version(), 3);
9089 remove_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
9090 assert_eq!(session.dev_domain_version(), 4);
9091
9092 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
9095 assert_eq!(session.dev_domain_version(), 5);
9096 assert_eq!(session.dev_channel_keys().count(), 0);
9097 assert_eq!(session.dev_peers().count(), 0);
9098
9099 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
9103 save(&mut session);
9104 let mut bytes = [0u8; SNAPSHOT_MAX];
9105 let len = session.encode_snapshot(&mut bytes).unwrap();
9106 let mut booted: TestSession =
9107 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
9108 assert_eq!(booted.dev_domain_version(), 0);
9109 booted.restore_at_boot(&bytes[..len]).unwrap();
9110 assert_ne!(booted.dev_domain_version(), 0);
9111 assert_eq!(booted.dev_channel_keys().collect::<Vec<_>>(), [dev_channel]);
9112 }
9113
9114 #[test]
9115 fn dev_channel_keys_do_not_create_host_receive_filters() {
9116 let mut session = test_session();
9117 enable(&mut session);
9118 install_host_key(&mut session, &HOST_PUB);
9121 let dev_channel = [0x66u8; 32];
9122 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
9123 let dev_id = test_engine().derive_channel_id(&ChannelKey(dev_channel)).0;
9124
9125 session.detach();
9128 receive_detached(&mut session, &multicast_on(dev_id), 0);
9129 session.attach(true);
9130 assert_eq!(queue_count(&mut session), 0);
9131
9132 install_channel_key(&mut session, &dev_channel);
9134 session.detach();
9135 receive_detached(&mut session, &multicast_on(dev_id), 0);
9136 session.attach(true);
9137 assert_eq!(queue_count(&mut session), 1);
9138 }
9139
9140 #[test]
9141 fn snapshot_carries_device_tables_but_never_the_identity() {
9142 let mut session = test_session();
9143 let dev_channel = [0x66u8; 32];
9144 let dev_id = test_engine().derive_channel_id(&ChannelKey(dev_channel)).0;
9145 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
9146 insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
9147 let public_key = provision_identity(&mut session, 7, &[0x11; 32]);
9148 save(&mut session);
9149
9150 remove_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
9153 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
9154 assert_eq!(get(&mut session, prop::DEV_PEERS), [0xD0; 32]);
9155
9156 let mut bytes = [0u8; SNAPSHOT_MAX];
9159 let len = session.encode_snapshot(&mut bytes).unwrap();
9160 let mut booted: TestSession =
9161 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
9162 booted.restore_at_boot(&bytes[..len]).unwrap();
9163 booted.attach(true);
9164 assert_eq!(get(&mut booted, prop::DEV_CHANNEL_KEYS), dev_id);
9165 assert_eq!(get(&mut booted, prop::DEV_PEERS), [0xD0; 32]);
9166 assert!(get(&mut booted, prop::DEV_KEY).is_empty());
9167 booted.set_boot_identity(public_key);
9168 assert_eq!(get(&mut booted, prop::DEV_KEY), public_key);
9169
9170 install_host_key(&mut booted, &[0xBB; 32]);
9172 assert_eq!(get(&mut booted, prop::DEV_CHANNEL_KEYS), dev_id);
9173 assert_eq!(get(&mut booted, prop::DEV_PEERS), [0xD0; 32]);
9174 assert_eq!(get(&mut booted, prop::DEV_KEY), public_key);
9175 }
9176
9177 #[test]
9178 fn restore_never_reverts_the_identity_and_clear_plus_reset_erases_it() {
9179 let mut session = test_session();
9180 let first = provision_identity(&mut session, 7, &[0x11; 32]);
9181 save(&mut session);
9182
9183 let second = provision_identity(&mut session, 3, &[0x22; 32]);
9186 assert_ne!(second, first);
9187 let _ = restore(&mut session);
9188 assert_eq!(get(&mut session, prop::DEV_KEY), second);
9189
9190 let mut buf = [0u8; 4];
9193 let len = frame::clear(&mut buf, 4).unwrap();
9194 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
9195 assert_eq!(effect, Some(Effect::ClearSaved { tid: 4 }));
9196 session.respond_clear(4, Ok(()), &mut |_: &[u8]| {});
9197 assert_eq!(get(&mut session, prop::DEV_KEY), second);
9198 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
9199 assert!(get(&mut session, prop::DEV_KEY).is_empty());
9200 }
9201
9202 #[test]
9203 fn tid_zero_identity_provisioning_is_silent_but_applies() {
9204 let mut session = test_session();
9205 let mut buf = [0u8; 64];
9206 let len = frame::prop_set(&mut buf, 0, prop::DEV_PRIVATE_KEY, &[0x11; 32]).unwrap();
9207 let effect = dispatch_tid0_silent(&mut session, &buf[..len].to_vec());
9208 assert_eq!(effect, Some(Effect::ProvisionIdentity { tid: 0 }));
9209 let public_key = public_of(&[0x11; 32]);
9210 session.respond_identity(0, Ok(public_key), &mut |_: &[u8]| {
9211 panic!("tid-0 provisioning must be silent")
9212 });
9213 assert_eq!(get(&mut session, prop::DEV_KEY), public_key);
9214 }
9215
9216 fn dispatch_tid0_silent(session: &mut TestSession, bytes: &[u8]) -> Option<Effect> {
9218 let (emitted, effect) = dispatch(session, bytes, 0);
9219 assert!(
9220 emitted.is_empty(),
9221 "fire-and-forget commands receive no correlated response"
9222 );
9223 effect
9224 }
9225
9226 fn last_status_of(session: &mut TestSession) -> u32 {
9227 pui::decode(&get(session, prop::LAST_STATUS)).unwrap().0
9228 }
9229
9230 #[test]
9231 fn tid_zero_commands_are_fire_and_forget() {
9232 let mut session = test_session();
9233 let mut buf = [0u8; 640];
9234
9235 let len = frame::nop(&mut buf, 0).unwrap();
9238 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
9239 assert_eq!(last_status_of(&mut session), Status::OK.0);
9240
9241 let len = frame::queue_drain(&mut buf, 0).unwrap();
9242 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
9243
9244 let len = frame::save(&mut buf, 0).unwrap();
9245 let effect = dispatch_tid0_silent(&mut session, &buf[..len].to_vec());
9246 assert_eq!(effect, Some(Effect::SaveSnapshot { tid: 0 }));
9247 session.respond_save(0, Ok(()), &mut |_: &[u8]| {
9248 panic!("tid-0 save must be silent")
9249 });
9250 assert_eq!(get(&mut session, prop::SAVED), [1]);
9251
9252 let len = frame::clear(&mut buf, 0).unwrap();
9254 let effect = dispatch_tid0_silent(&mut session, &buf[..len].to_vec());
9255 assert_eq!(effect, Some(Effect::ClearSaved { tid: 0 }));
9256 session.respond_clear(0, Err(()), &mut |_: &[u8]| {
9257 panic!("tid-0 clear must be silent")
9258 });
9259 assert_eq!(last_status_of(&mut session), Status::FAILURE.0);
9260 assert_eq!(
9261 get(&mut session, prop::SAVED),
9262 [1],
9263 "failed clear rolls back nothing"
9264 );
9265
9266 let len = frame::prop_set(&mut buf, 0, prop::PHY_DUTY_LIMIT, &99u16.to_le_bytes()).unwrap();
9269 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
9270 assert_eq!(get(&mut session, prop::PHY_DUTY_LIMIT), 99u16.to_le_bytes());
9271
9272 let item = [items::FILTER_PKT_TYPE, 0];
9273 let len = frame::prop_insert(&mut buf, 0, prop::HOST_RX_FILTERS, &item).unwrap();
9274 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
9275 assert_eq!(
9276 get(&mut session, prop::HOST_RX_FILTERS),
9277 [2, items::FILTER_PKT_TYPE, 0]
9278 );
9279
9280 let len = frame::prop_remove(&mut buf, 0, prop::HOST_RX_FILTERS, &item).unwrap();
9281 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
9282 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
9283
9284 let len = frame::prop_get(&mut buf, 0, prop::PHY_MTU).unwrap();
9286 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
9287 }
9288
9289 #[test]
9290 fn tid_zero_drain_delivers_frames_but_no_completion() {
9291 let mut session = test_session();
9292 enable(&mut session);
9293 session.detach();
9294 receive_detached(&mut session, &unicast_to([1, 2, 3]), 0);
9295 session.attach(true);
9296
9297 let mut buf = [0u8; 4];
9298 let len = frame::queue_drain(&mut buf, 0).unwrap();
9299 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
9300 assert!(emitted.is_empty());
9301 assert_eq!(effect, Some(Effect::DrainQueue));
9302
9303 let mut emitted = Vec::new();
9305 assert!(session.drain_step(0, &mut |bytes: &[u8]| emitted.push(bytes.to_vec())));
9306 assert_eq!(emitted.len(), 1);
9307 assert_eq!(
9308 Frame::parse(&emitted[0]).unwrap().command(),
9309 Some(Cmd::StrRecv)
9310 );
9311 let mut emitted = Vec::new();
9312 assert!(!session.drain_step(0, &mut |bytes: &[u8]| emitted.push(bytes.to_vec())));
9313 assert!(
9314 emitted.is_empty(),
9315 "TID-zero drain has no completion response"
9316 );
9317 assert_eq!(last_status_of(&mut session), Status::OK.0);
9318 }
9319
9320 #[test]
9321 fn queue_properties_are_read_only_and_capacity_fixed() {
9322 let mut session = test_session();
9323 assert_eq!(
9324 get(&mut session, prop::HOST_RX_QUEUE_CAPACITY),
9325 (RX_QUEUE_CAPACITY as u16).to_le_bytes()
9326 );
9327 for (key, status) in [
9328 (prop::HOST_RX_QUEUE_COUNT, Status::INVALID_ARGUMENT),
9329 (prop::HOST_RX_QUEUE_DROPPED, Status::INVALID_ARGUMENT),
9330 (prop::HOST_RX_QUEUE_CAPACITY, Status::UNIMPLEMENTED),
9331 ] {
9332 let (emitted, effect) = set(&mut session, key, &0u16.to_le_bytes());
9333 assert!(effect.is_none());
9334 expect_status(&emitted[0], 2, status);
9335 }
9336 }
9337}