1use core::str::FromStr;
4
5use heapless::{Deque, Vec as HeaplessVec};
6use umsh_core::options::{OptionDecoder, OptionEncoder};
7use umsh_core::{
8 ChannelKey, EncodeError, NodeHint, PacketBuilder, PacketHeader, PacketType, RegionCode,
9 SourceAddrRef,
10};
11use umsh_crypto::replay::{ReplayVerdict, ReplayWindow};
12use umsh_crypto::{AesProvider, CryptoEngine, PairwiseKeys, Sha256Provider};
13use umsh_ulcp::Status;
14use umsh_ulcp::airtime::lora_airtime_ms;
15use umsh_ulcp::alert::AlertState;
16use umsh_ulcp::battery::{self, BatteryStatus};
17use umsh_ulcp::ble::BleLinkState;
18use umsh_ulcp::frame::{
19 self, Cmd, Frame, Header, MultiEntries, PropPayload, StreamPayload, TID_UNSOLICITED,
20};
21use umsh_ulcp::gnss::{self, GnssSnapshot};
22use umsh_ulcp::ids::{
23 self, DEFAULT_ADVERT_INTERVAL_S, DEFAULT_BEACON_INTERVAL_S, MAX_AUTO_ANNOUNCE_INTERVAL_S,
24 MIN_AUTO_ANNOUNCE_INTERVAL_S, admin_reachable, cap, prop, stream,
25};
26pub use umsh_ulcp::items::REGION_STRING_MAX_LEN;
27use umsh_ulcp::items::{self, Filter, ItemError, REGION_CODE_LEN};
28use umsh_ulcp::meta::{
29 self, BufferedRxMeta, RX_FLAG_ACKED, RX_FLAG_BUFFERED, RX_FLAG_SELF_TX, RxMeta, TxMeta,
30};
31use umsh_ulcp::pui;
32use umsh_ulcp::sint;
33use umsh_ulcp::stats::{Counter, StatsLedger};
34
35use crate::duty::DutyLedger;
36
37pub const MAX_MTU: usize = 255;
39
40pub const MAX_DEVICE_NAME_LEN: usize = 64;
42
43const SCRATCH: usize = MAX_MTU + 24;
45
46const PROP_BUF: usize = MAX_PEER_KEYS * items::PUBLIC_KEY_LEN + 16;
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct RadioSettings {
54 pub enabled: bool,
55 pub freq_khz: u32,
56 pub bw_hz: u32,
57 pub sf: u8,
58 pub cr_denom: u8,
59 pub tx_power_dbm: i8,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum TxPower {
65 Default,
67 Max,
69 Dbm(i8),
71}
72
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
84pub struct BatteryFields {
85 pub voltage: bool,
86 pub level: bool,
87 pub charge_state: bool,
88}
89
90impl BatteryFields {
91 pub const NONE: Self = Self {
94 voltage: false,
95 level: false,
96 charge_state: false,
97 };
98
99 pub const fn any(self) -> bool {
101 self.voltage || self.level || self.charge_state
102 }
103
104 fn matches(self, snapshot: &BatteryStatus) -> bool {
106 (self.voltage || snapshot.voltage_mv.is_none())
107 && (self.level || snapshot.level_percent.is_none())
108 && (self.charge_state || snapshot.charge_state.is_none())
109 }
110}
111
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct AlertConfig {
115 pub timeout_ms: u32,
120}
121
122impl AlertConfig {
123 pub const DEFAULT: Self = Self {
125 timeout_ms: 5 * 60 * 1000,
126 };
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub struct TimeConfig;
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub struct GnssConfig {
145 pub default_enabled: bool,
158}
159
160impl GnssConfig {
161 pub const DEFAULT: Self = Self {
163 default_enabled: false,
164 };
165
166 pub const ALWAYS_ON: Self = Self {
168 default_enabled: true,
169 };
170}
171
172pub const DEFAULT_IDENT_PRECISION: u8 = 5;
176
177pub const MAX_IDENT_PRECISION: u8 = gnss::MAX_LOCATION_LEN as u8;
180
181#[derive(Clone, Copy, Debug)]
183pub struct SessionConfig {
184 pub dev_version: &'static str,
186 pub dev_model: Option<&'static str>,
191 pub default_device_name: &'static str,
193 pub mtu: u16,
195 pub sync_word: u16,
198 pub min_tx_power_dbm: i8,
200 pub max_tx_power_dbm: i8,
202 pub freq_khz_min: u32,
204 pub freq_khz_max: u32,
205 pub defaults: RadioSettings,
208 pub default_duty_limit: u16,
210 pub duty: &'static DutyLedger,
217 pub battery: Option<BatteryFields>,
221 pub alert: Option<AlertConfig>,
225 pub time: Option<TimeConfig>,
228 pub gnss: Option<GnssConfig>,
232 pub illuminance: bool,
236 pub ble: bool,
241 pub ble_pairing: bool,
253 pub reboot: bool,
259 pub stats: Option<&'static StatsLedger>,
275 pub mac_node: bool,
284}
285
286#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub enum TxOutcome {
290 Sent,
292 ChannelBusy,
296 Failed,
298}
299
300#[derive(Clone, Copy, Debug, PartialEq, Eq)]
302pub enum Effect {
303 ApplyRadio(RadioSettings),
305 StartTransmit,
309 SignIdentity { tid: u8 },
314 SampleRssi { tid: u8 },
319 SampleBattery { tid: u8 },
325 SampleIlluminance { tid: u8 },
330 SetPairingPin { tid: u8, pin: Option<u32> },
333 ReadTime { tid: u8 },
339 ApplyTime { epoch: Option<u32> },
348 SampleGnss { tid: u8, key: u32 },
353 ApplyBackhaul { enabled: bool },
359 DeviceNameChanged,
362 DrainQueue,
368 SaveSnapshot { tid: u8 },
373 ClearSaved { tid: u8 },
378 ProvisionIdentity { tid: u8 },
387 ApplyAlert(AlertState),
392 FactoryReset,
399 Reboot,
405 BleClearBonds { tid: u8 },
412 SetBlePairing { tid: u8, open: bool },
419}
420
421#[derive(Clone, Copy)]
424pub enum IdentitySource {
425 Install([u8; PRIVATE_KEY_LEN]),
427 Generate,
431}
432
433struct PendingTx {
434 data: HeaplessVec<u8, MAX_MTU>,
435 tid: u8,
436 airtime_ms: u32,
437 power: TxPower,
438 autonomous: bool,
442 ack_for: Option<u16>,
447 nocca: bool,
450}
451
452struct AckPlan {
454 trailer: [u8; 8],
457 flood_hops: Option<u8>,
462}
463
464enum SecureRx {
468 Plain,
472 New {
476 ack: Option<AckPlan>,
477 identity: Option<RxIdentity>,
478 },
479 Duplicate {
485 ack: Option<AckPlan>,
486 identity: Option<RxIdentity>,
487 },
488}
489
490enum PropValue {
492 Encoded(usize),
493 Unimplemented,
494 Unknown,
495}
496
497struct DeviceDomain {
502 settings: RadioSettings,
503 name: [u8; MAX_DEVICE_NAME_LEN],
504 name_len: usize,
505 channel_keys: ChannelKeyTable,
509 peers: DevPeerTable,
511 admins: DevAdminTable,
515 repeater_enabled: bool,
520 repeater_regions: RepeaterRegions,
524 repeater_default_region: Option<[u8; REGION_CODE_LEN]>,
531 repeater_min_rssi: Option<i16>,
534 repeater_min_snr: Option<i8>,
537 ident_role: Option<u8>,
546 ident_mobile: bool,
551 ident_location: HeaplessVec<u8, { gnss::MAX_LOCATION_LEN }>,
559 ident_altitude_m: Option<i32>,
563 dev_discoverable: bool,
568 advert_interval_s: u32,
572 beacon_interval_s: u32,
578 startup_beacon: bool,
582 tz_offset_min: i16,
586 gnss_enabled: bool,
594 gnss_ident_update: bool,
598 gnss_ident_precision: u8,
601 gnss_time_trust: bool,
605 ble_enabled: bool,
610}
611
612impl DeviceDomain {
613 fn post_reset(config: &SessionConfig) -> Self {
614 let mut settings = config.defaults;
615 settings.enabled = false;
616 let mut name = [0; MAX_DEVICE_NAME_LEN];
617 let name_len = config.default_device_name.len();
618 name[..name_len].copy_from_slice(config.default_device_name.as_bytes());
619 config.duty.reset_accounting();
622 config.duty.set_limit(config.default_duty_limit);
623 config
624 .duty
625 .set_phy(settings.sf, settings.bw_hz, settings.cr_denom);
626 Self {
627 settings,
628 name,
629 name_len,
630 channel_keys: ChannelKeyTable::default(),
631 peers: DevPeerTable::default(),
632 admins: DevAdminTable::default(),
633 repeater_enabled: false,
634 repeater_regions: RepeaterRegions::default(),
635 repeater_default_region: None,
636 repeater_min_rssi: None,
637 repeater_min_snr: None,
638 ident_role: None,
639 ident_mobile: false,
640 ident_location: HeaplessVec::new(),
641 ident_altitude_m: None,
642 dev_discoverable: true,
643 advert_interval_s: DEFAULT_ADVERT_INTERVAL_S,
644 beacon_interval_s: DEFAULT_BEACON_INTERVAL_S,
645 startup_beacon: true,
646 tz_offset_min: 0,
647 gnss_enabled: config.gnss.is_some_and(|gnss| gnss.default_enabled),
648 gnss_ident_update: false,
649 gnss_ident_precision: DEFAULT_IDENT_PRECISION,
650 gnss_time_trust: true,
651 ble_enabled: true,
652 }
653 }
654}
655
656pub const MAX_RX_FILTERS: usize = 16;
658
659#[derive(Clone, Copy)]
664struct FilterTable {
665 entries: [Filter; MAX_RX_FILTERS],
666 len: usize,
667}
668
669impl Default for FilterTable {
670 fn default() -> Self {
671 Self {
672 entries: [Filter::PktType(0); MAX_RX_FILTERS],
673 len: 0,
674 }
675 }
676}
677
678impl FilterTable {
679 fn iter(&self) -> impl Iterator<Item = &Filter> {
680 self.entries[..self.len].iter()
681 }
682
683 fn is_empty(&self) -> bool {
684 self.len == 0
685 }
686
687 fn insert(&mut self, filter: Filter) -> Result<(), Status> {
690 if self.iter().any(|existing| *existing == filter) {
691 return Err(Status::ALREADY);
692 }
693 if self.len == MAX_RX_FILTERS {
694 return Err(Status::NOMEM);
695 }
696 self.entries[self.len] = filter;
697 self.len += 1;
698 Ok(())
699 }
700
701 fn remove(&mut self, filter: Filter) -> Result<(), Status> {
704 let Some(index) = self.iter().position(|existing| *existing == filter) else {
705 return Err(Status::ITEM_NOT_FOUND);
706 };
707 self.len -= 1;
708 self.entries[index] = self.entries[self.len];
709 Ok(())
710 }
711
712 fn parse_table(value: &[u8]) -> Result<Self, Status> {
717 let mut table = Self::default();
718 for item in items::prefixed_items(value) {
719 let filter = decode_filter(item.map_err(table_error)?)?;
720 match table.insert(filter) {
721 Ok(()) | Err(Status::ALREADY) => {}
722 Err(status) => return Err(status),
723 }
724 }
725 Ok(table)
726 }
727}
728
729fn decode_filter(item: &[u8]) -> Result<Filter, Status> {
733 let filter = Filter::decode(item).map_err(|_| Status::INVALID_ARGUMENT)?;
734 if matches!(filter, Filter::PktType(pkt_type) if pkt_type > 7) {
735 return Err(Status::INVALID_ARGUMENT);
736 }
737 Ok(filter)
738}
739
740fn table_error(error: ItemError) -> Status {
745 match error {
746 ItemError::BadPrefix | ItemError::Truncated => Status::PARSE_ERROR,
747 _ => Status::INVALID_ARGUMENT,
748 }
749}
750
751pub const RX_QUEUE_CAPACITY: usize = 16;
753
754#[derive(Clone, Copy, PartialEq, Eq)]
760struct RxIdentity {
761 counter: u32,
762 mic: [u8; 16],
763 mic_len: u8,
764}
765
766impl RxIdentity {
767 fn new(counter: u32, mic: &[u8]) -> Option<Self> {
768 if mic.is_empty() || mic.len() > 16 {
769 return None;
770 }
771 let mut padded = [0u8; 16];
772 padded[..mic.len()].copy_from_slice(mic);
773 Some(Self {
774 counter,
775 mic: padded,
776 mic_len: mic.len() as u8,
777 })
778 }
779}
780
781#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
788pub struct RadioRxInfo {
789 pub rssi_dbm: Option<i16>,
790 pub snr_cb: Option<i16>,
791 pub lqi: Option<core::num::NonZeroU8>,
792 pub self_tx: bool,
794}
795
796impl RadioRxInfo {
797 pub fn measured(rssi_dbm: i16, snr_cb: i16, lqi: Option<core::num::NonZeroU8>) -> Self {
799 Self {
800 rssi_dbm: Some(rssi_dbm),
801 snr_cb: Some(snr_cb),
802 lqi,
803 self_tx: false,
804 }
805 }
806
807 pub fn self_transmitted() -> Self {
809 Self {
810 self_tx: true,
811 ..Self::default()
812 }
813 }
814}
815
816#[derive(Clone, Copy)]
821struct QueueEntry {
822 data: [u8; MAX_MTU],
823 len: u16,
824 rssi_dbm: Option<i16>,
825 snr_cb: Option<i16>,
826 lqi: Option<core::num::NonZeroU8>,
827 self_tx: bool,
828 rx_time_ms: u64,
829 acked: bool,
830 seq: u16,
834 identity: Option<RxIdentity>,
835}
836
837impl QueueEntry {
838 const EMPTY: Self = Self {
839 data: [0; MAX_MTU],
840 len: 0,
841 rssi_dbm: None,
842 snr_cb: None,
843 lqi: None,
844 self_tx: false,
845 rx_time_ms: 0,
846 acked: false,
847 seq: 0,
848 identity: None,
849 };
850
851 fn frame(&self) -> &[u8] {
852 &self.data[..usize::from(self.len)]
853 }
854}
855
856struct RxQueue {
861 entries: [QueueEntry; RX_QUEUE_CAPACITY],
862 head: usize,
864 len: usize,
865 dropped: u32,
866 next_seq: u16,
869}
870
871impl Default for RxQueue {
872 fn default() -> Self {
873 Self {
874 entries: [QueueEntry::EMPTY; RX_QUEUE_CAPACITY],
875 head: 0,
876 len: 0,
877 dropped: 0,
878 next_seq: 0,
879 }
880 }
881}
882
883impl RxQueue {
884 fn clear(&mut self) {
888 self.head = 0;
889 self.len = 0;
890 self.dropped = 0;
891 for entry in &mut self.entries {
892 entry.identity = None;
893 }
894 }
895
896 fn push(
899 &mut self,
900 data: &[u8],
901 info: &RadioRxInfo,
902 rx_time_ms: u64,
903 identity: Option<RxIdentity>,
904 ) -> u16 {
905 debug_assert!(data.len() <= MAX_MTU);
906 if self.len == RX_QUEUE_CAPACITY {
907 self.head = (self.head + 1) % RX_QUEUE_CAPACITY;
908 self.len -= 1;
909 self.dropped = self.dropped.wrapping_add(1);
910 }
911 let seq = self.next_seq;
912 self.next_seq = self.next_seq.wrapping_add(1);
913 let slot = (self.head + self.len) % RX_QUEUE_CAPACITY;
914 let entry = &mut self.entries[slot];
915 entry.data[..data.len()].copy_from_slice(data);
916 entry.len = data.len() as u16;
917 entry.rssi_dbm = info.rssi_dbm;
918 entry.snr_cb = info.snr_cb;
919 entry.lqi = info.lqi;
920 entry.self_tx = info.self_tx;
921 entry.rx_time_ms = rx_time_ms;
922 entry.acked = false;
923 entry.seq = seq;
924 entry.identity = identity;
925 self.len += 1;
926 seq
927 }
928
929 fn pop_front(&mut self) -> Option<QueueEntry> {
930 if self.len == 0 {
931 return None;
932 }
933 let entry = self.entries[self.head];
934 self.head = (self.head + 1) % RX_QUEUE_CAPACITY;
935 self.len -= 1;
936 Some(entry)
937 }
938
939 fn iter(&self) -> impl Iterator<Item = &QueueEntry> {
940 (0..self.len).map(|offset| &self.entries[(self.head + offset) % RX_QUEUE_CAPACITY])
941 }
942
943 fn seq_for_identity(&self, identity: &RxIdentity) -> Option<u16> {
946 self.iter()
947 .find(|entry| entry.identity.as_ref() == Some(identity))
948 .map(|entry| entry.seq)
949 }
950
951 fn mark_acked(&mut self, seq: u16) {
954 let Some(offset) = (0..self.len)
955 .find(|offset| self.entries[(self.head + offset) % RX_QUEUE_CAPACITY].seq == seq)
956 else {
957 return;
958 };
959 self.entries[(self.head + offset) % RX_QUEUE_CAPACITY].acked = true;
960 }
961}
962
963pub const MAX_CHANNEL_KEYS: usize = 8;
965pub const MAX_PEER_KEYS: usize = 8;
967
968#[derive(Clone, Copy)]
971struct ChannelKeyEntry {
972 key: [u8; items::CHANNEL_KEY_LEN],
973 id: [u8; items::CHANNEL_ID_LEN],
974}
975
976#[derive(Clone, Copy, Default)]
980struct ChannelKeyTable {
981 entries: [Option<ChannelKeyEntry>; MAX_CHANNEL_KEYS],
982 len: usize,
983}
984
985impl ChannelKeyTable {
986 fn iter(&self) -> impl Iterator<Item = &ChannelKeyEntry> {
987 self.entries[..self.len]
988 .iter()
989 .map(|entry| entry.as_ref().expect("entries below len are populated"))
990 }
991
992 fn insert(&mut self, entry: ChannelKeyEntry) -> Result<(), Status> {
993 if self.iter().any(|existing| existing.key == entry.key) {
994 return Err(Status::ALREADY);
995 }
996 if self.len == MAX_CHANNEL_KEYS {
997 return Err(Status::NOMEM);
998 }
999 self.entries[self.len] = Some(entry);
1000 self.len += 1;
1001 Ok(())
1002 }
1003
1004 fn remove(&mut self, key: &[u8; items::CHANNEL_KEY_LEN]) -> Result<[u8; 2], Status> {
1007 let Some(index) = self.iter().position(|existing| existing.key == *key) else {
1008 return Err(Status::ITEM_NOT_FOUND);
1009 };
1010 let id = self.entries[index].expect("populated").id;
1011 self.len -= 1;
1012 self.entries[index] = self.entries[self.len];
1013 self.entries[self.len] = None;
1014 Ok(id)
1015 }
1016}
1017
1018struct PeerSlot {
1023 entry: items::PeerKeyEntry,
1024 window: ReplayWindow,
1025}
1026
1027#[derive(Default)]
1031struct PeerKeyTable {
1032 entries: [Option<PeerSlot>; MAX_PEER_KEYS],
1033 len: usize,
1034}
1035
1036impl PeerKeyTable {
1037 fn iter(&self) -> impl Iterator<Item = &PeerSlot> {
1038 self.entries[..self.len]
1039 .iter()
1040 .map(|slot| slot.as_ref().expect("entries below len are populated"))
1041 }
1042
1043 fn insert(&mut self, entry: items::PeerKeyEntry) -> Result<(), Status> {
1047 if let Some(existing) = self.entries[..self.len]
1048 .iter_mut()
1049 .flatten()
1050 .find(|existing| existing.entry.public_key == entry.public_key)
1051 {
1052 existing.entry = entry;
1053 return Ok(());
1054 }
1055 if self.len == MAX_PEER_KEYS {
1056 return Err(Status::NOMEM);
1057 }
1058 self.entries[self.len] = Some(PeerSlot {
1059 entry,
1060 window: ReplayWindow::new(),
1061 });
1062 self.len += 1;
1063 Ok(())
1064 }
1065
1066 fn reconcile(&mut self, desired: &[items::PeerKeyEntry]) {
1081 let mut index = 0;
1082 while index < self.len {
1083 let public_key = self.entries[index]
1084 .as_ref()
1085 .expect("entries below len are populated")
1086 .entry
1087 .public_key;
1088 if desired.iter().any(|entry| entry.public_key == public_key) {
1089 index += 1;
1090 continue;
1091 }
1092 self.len -= 1;
1093 self.entries[index] = self.entries[self.len].take();
1094 }
1095 for entry in desired {
1099 let _ = self.insert(*entry);
1100 }
1101 }
1102
1103 fn remove(&mut self, public_key: &[u8; items::PUBLIC_KEY_LEN]) -> Result<(), Status> {
1106 let Some(index) = self
1107 .iter()
1108 .position(|existing| existing.entry.public_key == *public_key)
1109 else {
1110 return Err(Status::ITEM_NOT_FOUND);
1111 };
1112 self.len -= 1;
1113 self.entries[index] = self.entries[self.len].take();
1114 Ok(())
1115 }
1116
1117 fn resolve_source(&self, source: &SourceAddrRef, frame: &[u8]) -> Option<usize> {
1122 match source {
1123 SourceAddrRef::FullKeyAt { offset } => {
1124 let key = frame.get(*offset..*offset + items::PUBLIC_KEY_LEN)?;
1125 self.iter().position(|slot| slot.entry.public_key == *key)
1126 }
1127 SourceAddrRef::Hint(hint) => {
1128 let mut matches = self
1129 .iter()
1130 .enumerate()
1131 .filter(|(_, slot)| slot.entry.public_key[..3] == hint.0);
1132 let (index, _) = matches.next()?;
1133 matches.next().is_none().then_some(index)
1134 }
1135 _ => None,
1136 }
1137 }
1138}
1139
1140pub const PRIVATE_KEY_LEN: usize = 32;
1142
1143pub const MAX_DEV_PEERS: usize = 8;
1145
1146pub const MAX_DEV_ADMINS: usize = 8;
1148
1149#[derive(Clone, Copy)]
1159struct PublicKeyTable<const N: usize> {
1160 entries: [[u8; items::PUBLIC_KEY_LEN]; N],
1161 len: usize,
1162}
1163
1164type DevPeerTable = PublicKeyTable<MAX_DEV_PEERS>;
1166type DevAdminTable = PublicKeyTable<MAX_DEV_ADMINS>;
1168
1169impl<const N: usize> Default for PublicKeyTable<N> {
1171 fn default() -> Self {
1172 Self {
1173 entries: [[0; items::PUBLIC_KEY_LEN]; N],
1174 len: 0,
1175 }
1176 }
1177}
1178
1179impl<const N: usize> PublicKeyTable<N> {
1180 fn iter(&self) -> impl Iterator<Item = &[u8; items::PUBLIC_KEY_LEN]> {
1181 self.entries[..self.len].iter()
1182 }
1183
1184 fn insert(&mut self, public_key: [u8; items::PUBLIC_KEY_LEN]) -> Result<(), Status> {
1187 if self.iter().any(|existing| *existing == public_key) {
1188 return Err(Status::ALREADY);
1189 }
1190 if self.len == N {
1191 return Err(Status::NOMEM);
1192 }
1193 self.entries[self.len] = public_key;
1194 self.len += 1;
1195 Ok(())
1196 }
1197
1198 fn remove(&mut self, public_key: &[u8; items::PUBLIC_KEY_LEN]) -> Result<(), Status> {
1201 let Some(index) = self.iter().position(|existing| existing == public_key) else {
1202 return Err(Status::ITEM_NOT_FOUND);
1203 };
1204 self.len -= 1;
1205 self.entries[index] = self.entries[self.len];
1206 Ok(())
1207 }
1208
1209 fn parse_table(value: &[u8]) -> Result<Self, Status> {
1212 let mut table = Self::default();
1213 for item in items::fixed_items::<{ items::PUBLIC_KEY_LEN }>(value)
1214 .map_err(|_| Status::INVALID_ARGUMENT)?
1215 {
1216 match table.insert(*item) {
1217 Ok(()) | Err(Status::ALREADY) => {}
1218 Err(status) => return Err(status),
1219 }
1220 }
1221 Ok(table)
1222 }
1223}
1224
1225pub const MAX_REPEATER_REGIONS: usize = 8;
1229
1230#[derive(Clone, Copy)]
1239struct RegionEntry {
1240 text: [u8; REGION_STRING_MAX_LEN],
1241 text_len: usize,
1242 code: [u8; REGION_CODE_LEN],
1243}
1244
1245impl RegionEntry {
1246 fn text(&self) -> &[u8] {
1247 &self.text[..self.text_len]
1248 }
1249}
1250
1251#[derive(Clone, Copy, Default)]
1254struct RepeaterRegions {
1255 entries: [Option<RegionEntry>; MAX_REPEATER_REGIONS],
1256 len: usize,
1257}
1258
1259impl RepeaterRegions {
1260 fn iter(&self) -> impl Iterator<Item = &RegionEntry> {
1261 self.entries[..self.len].iter().filter_map(Option::as_ref)
1262 }
1263
1264 fn position(&self, text: &[u8]) -> Option<usize> {
1270 self.iter()
1271 .position(|entry| entry.text().eq_ignore_ascii_case(text))
1272 }
1273
1274 fn push(&mut self, entry: RegionEntry) -> Result<(), Status> {
1276 let Some(index) = self.position(entry.text()) else {
1277 if self.len == MAX_REPEATER_REGIONS {
1278 return Err(Status::NOMEM);
1279 }
1280 self.entries[self.len] = Some(entry);
1281 self.len += 1;
1282 return Ok(());
1283 };
1284 if self.entries[index].is_some_and(|held| held.text() == entry.text()) {
1289 return Err(Status::ALREADY);
1290 }
1291 self.entries[index] = Some(entry);
1292 Ok(())
1293 }
1294
1295 fn remove(&mut self, text: &[u8]) -> Result<(), Status> {
1296 let index = self.position(text).ok_or(Status::ITEM_NOT_FOUND)?;
1297 self.len -= 1;
1299 self.entries[index] = self.entries[self.len];
1300 self.entries[self.len] = None;
1301 Ok(())
1302 }
1303
1304 fn parse_table(value: &[u8]) -> Result<Self, Status> {
1311 let mut regions = Self::default();
1312 for item in items::prefixed_items(value) {
1313 let entry = region_entry(item.map_err(|_| Status::INVALID_ARGUMENT)?)?;
1314 match regions.push(entry) {
1315 Ok(()) | Err(Status::ALREADY) => {}
1316 Err(status) => return Err(status),
1317 }
1318 }
1319 Ok(regions)
1320 }
1321}
1322
1323fn region_entry(item: &[u8]) -> Result<RegionEntry, Status> {
1330 if !(1..=REGION_STRING_MAX_LEN).contains(&item.len()) {
1331 return Err(Status::INVALID_ARGUMENT);
1332 }
1333 let text = core::str::from_utf8(item).map_err(|_| Status::INVALID_ARGUMENT)?;
1334 let code = RegionCode::from_str(text).map_err(|_| Status::INVALID_ARGUMENT)?;
1335 let mut entry = RegionEntry {
1336 text: [0; REGION_STRING_MAX_LEN],
1337 text_len: item.len(),
1338 code: code.to_bytes(),
1339 };
1340 entry.text[..item.len()].copy_from_slice(item);
1341 Ok(entry)
1342}
1343
1344fn parse_region_code(value: &[u8]) -> Result<Option<[u8; REGION_CODE_LEN]>, Status> {
1347 match value.len() {
1348 0 => Ok(None),
1349 REGION_CODE_LEN => Ok(Some([value[0], value[1]])),
1350 _ => Err(Status::INVALID_ARGUMENT),
1351 }
1352}
1353
1354const TRANSMITTED_MIC_SLOTS: usize = 16;
1359
1360#[derive(Default)]
1380struct TransmittedMics {
1381 slots: [[u8; 4]; TRANSMITTED_MIC_SLOTS],
1382 filled: usize,
1384 cursor: usize,
1386}
1387
1388impl TransmittedMics {
1389 fn note(&mut self, mic: [u8; 4]) {
1392 if self.contains(&mic) {
1393 return;
1394 }
1395 self.slots[self.cursor] = mic;
1396 self.cursor = (self.cursor + 1) % TRANSMITTED_MIC_SLOTS;
1397 if self.filled < TRANSMITTED_MIC_SLOTS {
1398 self.filled += 1;
1399 }
1400 }
1401
1402 fn contains(&self, mic: &[u8; 4]) -> bool {
1404 self.slots[..self.filled].iter().any(|slot| slot == mic)
1405 }
1406}
1407
1408#[derive(Default)]
1413struct HostDomain {
1414 key: Option<[u8; items::PUBLIC_KEY_LEN]>,
1416 filters: FilterTable,
1418 channel_keys: ChannelKeyTable,
1420 peer_keys: PeerKeyTable,
1422 auto_ack: bool,
1425 queue: RxQueue,
1427 transmitted_mics: TransmittedMics,
1431}
1432
1433impl HostDomain {
1434 fn reset(&mut self, key: Option<[u8; items::PUBLIC_KEY_LEN]>) {
1439 self.key = key;
1440 self.filters = FilterTable::default();
1441 self.channel_keys = ChannelKeyTable::default();
1442 self.peer_keys = PeerKeyTable::default();
1443 self.auto_ack = false;
1444 self.queue.clear();
1445 self.transmitted_mics = TransmittedMics::default();
1446 }
1447
1448 fn note_tx_mic(&mut self, frame: &[u8]) {
1453 let Ok(header) = PacketHeader::parse(frame) else {
1454 return;
1455 };
1456 if header.fcf.packet_type() == PacketType::MacAck {
1457 return;
1458 }
1459 if let Some(mic) = frame.get(header.mic_range.clone())
1460 && mic.len() >= 4
1461 {
1462 self.transmitted_mics.note([mic[0], mic[1], mic[2], mic[3]]);
1463 }
1464 }
1465
1466 fn filtering_configured(&self) -> bool {
1470 self.key.is_some() || !self.filters.is_empty() || self.channel_keys.len != 0
1471 }
1472
1473 fn accepts_live_frame(&self, data: &[u8]) -> bool {
1480 if PacketHeader::parse(data)
1481 .is_ok_and(|header| header.fcf.packet_type() == PacketType::Broadcast)
1482 {
1483 return true;
1484 }
1485 self.accepts_frame(data)
1486 }
1487
1488 fn accepts_frame(&self, data: &[u8]) -> bool {
1494 if !self.filtering_configured() {
1495 return true;
1496 }
1497 let Ok(header) = PacketHeader::parse(data) else {
1498 return false;
1499 };
1500 if let Some(mic) = data.get(header.mic_range.start..header.mic_range.start + 4)
1514 && self
1515 .transmitted_mics
1516 .contains(&[mic[0], mic[1], mic[2], mic[3]])
1517 {
1518 return true;
1519 }
1520 let dst = header.dst.map(|hint| hint.0);
1521 if let Some(key) = &self.key
1522 && dst == Some([key[0], key[1], key[2]])
1523 {
1524 return true;
1525 }
1526 let channel = header.channel.map(|channel| channel.0);
1527 if channel.is_some()
1530 && self
1531 .channel_keys
1532 .iter()
1533 .any(|entry| channel == Some(entry.id))
1534 {
1535 return true;
1536 }
1537 let pkt_type = header.fcf.packet_type() as u8;
1538 self.filters.iter().any(|filter| match filter {
1539 Filter::DestHint(hint) => dst == Some(*hint),
1540 Filter::ChannelId(id) => channel == Some(*id),
1541 Filter::PktType(filtered) => pkt_type == *filtered,
1542 })
1543 }
1544}
1545
1546pub const SNAPSHOT_MAX: usize = 1792;
1558
1559const SNAPSHOT_FORMAT: u8 = 4;
1571
1572#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1580enum ApplyPhase {
1581 Keys,
1584 Config,
1587 Enable,
1589}
1590
1591impl ApplyPhase {
1592 const ORDER: [Self; 3] = [Self::Keys, Self::Config, Self::Enable];
1593}
1594
1595struct SavedProperty {
1598 number: u16,
1599 phase: ApplyPhase,
1600 repeatable: bool,
1603}
1604
1605const fn saved(number: u32, phase: ApplyPhase, repeatable: bool) -> SavedProperty {
1609 assert!(
1610 number <= u16::MAX as u32,
1611 "saved property number must fit u16"
1612 );
1613 SavedProperty {
1614 number: number as u16,
1615 phase,
1616 repeatable,
1617 }
1618}
1619
1620const SAVED_SCHEMA: &[SavedProperty] = &[
1641 saved(prop::PHY_ENABLED, ApplyPhase::Enable, false),
1642 saved(prop::PHY_FREQ, ApplyPhase::Config, false),
1643 saved(prop::PHY_TX_POWER, ApplyPhase::Config, false),
1644 saved(prop::PHY_LORA_BW, ApplyPhase::Config, false),
1645 saved(prop::PHY_LORA_SF, ApplyPhase::Config, false),
1646 saved(prop::PHY_LORA_CR, ApplyPhase::Config, false),
1647 saved(prop::DEV_KEY, ApplyPhase::Keys, false),
1648 saved(prop::DEV_CHANNEL_KEYS, ApplyPhase::Keys, true),
1649 saved(prop::DEV_PEERS, ApplyPhase::Keys, true),
1650 saved(prop::DEV_NAME, ApplyPhase::Config, false),
1651 saved(prop::MAC_REPEATER_ENABLED, ApplyPhase::Config, false),
1652 saved(prop::IDENT_ROLE, ApplyPhase::Config, false),
1653 saved(prop::IDENT_MOBILE, ApplyPhase::Config, false),
1654 saved(prop::MAC_REPEATER_REGIONS, ApplyPhase::Config, true),
1655 saved(prop::MAC_REPEATER_DEFAULT_REGION, ApplyPhase::Config, false),
1656 saved(prop::MAC_REPEATER_MIN_RSSI, ApplyPhase::Config, false),
1657 saved(prop::MAC_REPEATER_MIN_SNR, ApplyPhase::Config, false),
1658 saved(prop::DEV_DISCOVERABLE, ApplyPhase::Config, false),
1659 saved(prop::ADVERT_INTERVAL, ApplyPhase::Config, false),
1660 saved(prop::BEACON_INTERVAL, ApplyPhase::Config, false),
1661 saved(prop::STARTUP_BEACON, ApplyPhase::Config, false),
1662 saved(prop::IDENT_LOCATION, ApplyPhase::Config, false),
1663 saved(prop::IDENT_ALTITUDE, ApplyPhase::Config, false),
1664 saved(prop::GNSS_ENABLED, ApplyPhase::Config, false),
1665 saved(prop::PHY_DUTY_LIMIT, ApplyPhase::Config, false),
1666 saved(prop::DEV_ADMINS, ApplyPhase::Keys, true),
1667 saved(prop::TZ_OFFSET, ApplyPhase::Config, false),
1668 saved(prop::GNSS_IDENT_UPDATE, ApplyPhase::Config, false),
1669 saved(prop::GNSS_IDENT_PRECISION, ApplyPhase::Config, false),
1670 saved(prop::GNSS_TIME_TRUST, ApplyPhase::Config, false),
1671 saved(prop::BLE_ENABLED, ApplyPhase::Config, false),
1672];
1673
1674const _: () = assert!(
1678 SAVED_SCHEMA.len() <= u32::BITS as usize,
1679 "SAVED_SCHEMA outgrew the duplicate-detection bitmask"
1680);
1681
1682const _: () = {
1688 let mut index = 1;
1689 while index < SAVED_SCHEMA.len() {
1690 assert!(
1691 SAVED_SCHEMA[index - 1].number < SAVED_SCHEMA[index].number,
1692 "SAVED_SCHEMA is not in ascending property order"
1693 );
1694 index += 1;
1695 }
1696};
1697
1698#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1702pub enum SnapshotError {
1703 UnknownFormat,
1706 Malformed,
1709 InvalidValue,
1712}
1713
1714#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1716pub enum SavedStatus {
1717 #[default]
1719 None,
1720 Current,
1722 Fallback,
1725 Unreadable,
1728}
1729
1730impl SavedStatus {
1731 pub const fn as_octet(self) -> u8 {
1733 match self {
1734 Self::None => ids::saved::NONE,
1735 Self::Current => ids::saved::CURRENT,
1736 Self::Fallback => ids::saved::FALLBACK,
1737 Self::Unreadable => ids::saved::UNREADABLE,
1738 }
1739 }
1740}
1741
1742#[derive(Clone)]
1752struct SavedState {
1753 settings: RadioSettings,
1754 duty_limit: u16,
1755 name: [u8; MAX_DEVICE_NAME_LEN],
1756 name_len: usize,
1757 dev_key: Option<[u8; items::PUBLIC_KEY_LEN]>,
1767 dev_channel_keys: ChannelKeyTable,
1768 dev_peers: DevPeerTable,
1769 dev_admins: DevAdminTable,
1770 repeater_enabled: bool,
1771 repeater_regions: RepeaterRegions,
1772 repeater_default_region: Option<[u8; REGION_CODE_LEN]>,
1773 repeater_min_rssi: Option<i16>,
1774 repeater_min_snr: Option<i8>,
1775 ident_role: Option<u8>,
1776 ident_mobile: bool,
1777 ident_location: HeaplessVec<u8, { gnss::MAX_LOCATION_LEN }>,
1778 ident_altitude_m: Option<i32>,
1779 dev_discoverable: bool,
1780 advert_interval_s: u32,
1781 beacon_interval_s: u32,
1782 startup_beacon: bool,
1783 tz_offset_min: i16,
1784 gnss_enabled: bool,
1785 gnss_ident_update: bool,
1786 gnss_ident_precision: u8,
1787 gnss_time_trust: bool,
1788 ble_enabled: bool,
1789}
1790
1791impl SavedState {
1792 fn capture(
1794 device: &DeviceDomain,
1795 duty_limit: u16,
1796 dev_key: Option<[u8; items::PUBLIC_KEY_LEN]>,
1797 ) -> Self {
1798 Self {
1799 settings: device.settings,
1800 duty_limit,
1801 name: device.name,
1802 name_len: device.name_len,
1803 dev_key,
1804 dev_channel_keys: device.channel_keys,
1805 dev_peers: device.peers,
1806 dev_admins: device.admins,
1807 repeater_enabled: device.repeater_enabled,
1808 repeater_regions: device.repeater_regions,
1809 repeater_default_region: device.repeater_default_region,
1810 repeater_min_rssi: device.repeater_min_rssi,
1811 repeater_min_snr: device.repeater_min_snr,
1812 ident_role: device.ident_role,
1813 ident_mobile: device.ident_mobile,
1814 ident_location: device.ident_location.clone(),
1815 ident_altitude_m: device.ident_altitude_m,
1816 dev_discoverable: device.dev_discoverable,
1817 advert_interval_s: device.advert_interval_s,
1818 beacon_interval_s: device.beacon_interval_s,
1819 startup_beacon: device.startup_beacon,
1820 tz_offset_min: device.tz_offset_min,
1821 gnss_enabled: device.gnss_enabled,
1822 gnss_ident_update: device.gnss_ident_update,
1823 gnss_ident_precision: device.gnss_ident_precision,
1824 gnss_time_trust: device.gnss_time_trust,
1825 ble_enabled: device.ble_enabled,
1826 }
1827 }
1828
1829 fn defaults(config: &SessionConfig) -> Self {
1834 let mut settings = config.defaults;
1835 settings.enabled = false;
1836 let mut name = [0u8; MAX_DEVICE_NAME_LEN];
1837 let name_len = config.default_device_name.len();
1838 name[..name_len].copy_from_slice(config.default_device_name.as_bytes());
1839 Self {
1840 settings,
1841 duty_limit: config.default_duty_limit,
1842 name,
1843 name_len,
1844 dev_key: None,
1845 dev_channel_keys: ChannelKeyTable::default(),
1846 dev_peers: DevPeerTable::default(),
1847 dev_admins: DevAdminTable::default(),
1848 repeater_enabled: false,
1849 repeater_regions: RepeaterRegions::default(),
1850 repeater_default_region: None,
1851 repeater_min_rssi: None,
1852 repeater_min_snr: None,
1853 ident_role: None,
1854 ident_mobile: false,
1855 ident_location: HeaplessVec::new(),
1856 ident_altitude_m: None,
1857 dev_discoverable: true,
1858 advert_interval_s: DEFAULT_ADVERT_INTERVAL_S,
1859 beacon_interval_s: DEFAULT_BEACON_INTERVAL_S,
1860 startup_beacon: true,
1861 tz_offset_min: 0,
1862 gnss_enabled: config.gnss.is_some_and(|gnss| gnss.default_enabled),
1867 gnss_ident_update: false,
1868 gnss_ident_precision: DEFAULT_IDENT_PRECISION,
1869 gnss_time_trust: true,
1870 ble_enabled: true,
1871 }
1872 }
1873
1874 fn encode(&self, out: &mut [u8]) -> Option<usize> {
1883 let (format, rest) = out.split_first_mut()?;
1884 *format = SNAPSHOT_FORMAT;
1885 let mut encoder = OptionEncoder::new(rest);
1886 for entry in SAVED_SCHEMA {
1887 self.encode_into(&mut encoder, entry.number).ok()?;
1888 }
1889 Some(1 + encoder.finish())
1890 }
1891
1892 fn encode_into(&self, encoder: &mut OptionEncoder<'_>, number: u16) -> Result<(), EncodeError> {
1895 match u32::from(number) {
1896 prop::PHY_ENABLED => encoder.put(number, &[self.settings.enabled as u8]),
1897 prop::PHY_FREQ => encoder.put(number, &self.settings.freq_khz.to_le_bytes()),
1898 prop::PHY_TX_POWER => encoder.put(number, &[self.settings.tx_power_dbm as u8]),
1899 prop::PHY_LORA_BW => encoder.put(number, &self.settings.bw_hz.to_le_bytes()),
1900 prop::PHY_LORA_SF => encoder.put(number, &[self.settings.sf]),
1901 prop::PHY_LORA_CR => encoder.put(number, &[self.settings.cr_denom]),
1902 prop::DEV_KEY => match &self.dev_key {
1906 Some(key) => encoder.put(number, key),
1907 None => Ok(()),
1908 },
1909 prop::DEV_CHANNEL_KEYS => {
1910 for entry in self.dev_channel_keys.iter() {
1911 encoder.put(number, &entry.key)?;
1912 }
1913 Ok(())
1914 }
1915 prop::DEV_PEERS => {
1916 for public_key in self.dev_peers.iter() {
1917 encoder.put(number, public_key)?;
1918 }
1919 Ok(())
1920 }
1921 prop::DEV_NAME => encoder.put(number, &self.name[..self.name_len]),
1922 prop::MAC_REPEATER_ENABLED => encoder.put(number, &[self.repeater_enabled as u8]),
1923 prop::IDENT_ROLE => match self.ident_role {
1924 Some(role) => encoder.put(number, &[role]),
1925 None => Ok(()),
1926 },
1927 prop::IDENT_MOBILE => encoder.put(number, &[self.ident_mobile as u8]),
1928 prop::MAC_REPEATER_REGIONS => {
1935 for entry in self.repeater_regions.iter() {
1936 encoder.put(number, entry.text())?;
1937 }
1938 Ok(())
1939 }
1940 prop::MAC_REPEATER_DEFAULT_REGION => match &self.repeater_default_region {
1941 Some(code) => encoder.put(number, code),
1942 None => Ok(()),
1943 },
1944 prop::MAC_REPEATER_MIN_RSSI => match self.repeater_min_rssi {
1945 Some(rssi) => encoder.put(number, &rssi.to_le_bytes()),
1946 None => Ok(()),
1947 },
1948 prop::MAC_REPEATER_MIN_SNR => match self.repeater_min_snr {
1949 Some(snr) => encoder.put(number, &[snr as u8]),
1950 None => Ok(()),
1951 },
1952 prop::DEV_DISCOVERABLE => encoder.put(number, &[self.dev_discoverable as u8]),
1953 prop::ADVERT_INTERVAL => encoder.put(number, &self.advert_interval_s.to_le_bytes()),
1954 prop::BEACON_INTERVAL => encoder.put(number, &self.beacon_interval_s.to_le_bytes()),
1955 prop::STARTUP_BEACON => encoder.put(number, &[self.startup_beacon as u8]),
1956 prop::IDENT_LOCATION => match self.ident_location.is_empty() {
1959 true => Ok(()),
1960 false => encoder.put(number, &self.ident_location),
1961 },
1962 prop::IDENT_ALTITUDE => match self.ident_altitude_m {
1963 Some(meters) => {
1964 let mut buf = [0u8; sint::MAX_LEN];
1965 let len = sint::encode(meters, &mut buf).expect("buffer holds any i32");
1966 encoder.put(number, &buf[..len])
1967 }
1968 None => Ok(()),
1969 },
1970 prop::GNSS_ENABLED => encoder.put(number, &[self.gnss_enabled as u8]),
1971 prop::PHY_DUTY_LIMIT => encoder.put(number, &self.duty_limit.to_le_bytes()),
1972 prop::DEV_ADMINS => {
1973 for public_key in self.dev_admins.iter() {
1974 encoder.put(number, public_key)?;
1975 }
1976 Ok(())
1977 }
1978 prop::TZ_OFFSET => encoder.put(number, &self.tz_offset_min.to_le_bytes()),
1979 prop::GNSS_IDENT_UPDATE => encoder.put(number, &[self.gnss_ident_update as u8]),
1980 prop::GNSS_IDENT_PRECISION => encoder.put(number, &[self.gnss_ident_precision]),
1981 prop::GNSS_TIME_TRUST => encoder.put(number, &[self.gnss_time_trust as u8]),
1982 prop::BLE_ENABLED => encoder.put(number, &[self.ble_enabled as u8]),
1983 _ => unreachable!("SAVED_SCHEMA row without an encoder arm"),
1984 }
1985 }
1986
1987 fn decode(config: &SessionConfig, bytes: &[u8]) -> Result<Self, SnapshotError> {
2006 let (format, options) = bytes.split_first().ok_or(SnapshotError::Malformed)?;
2007 if *format != SNAPSHOT_FORMAT {
2008 return Err(SnapshotError::UnknownFormat);
2009 }
2010 let mut state = Self::defaults(config);
2011 let mut seen: u32 = 0;
2012 for item in OptionDecoder::new(options) {
2013 let (number, value) = item.map_err(|_| SnapshotError::Malformed)?;
2014 let Some(index) = SAVED_SCHEMA.iter().position(|entry| entry.number == number) else {
2015 continue;
2016 };
2017 if !SAVED_SCHEMA[index].repeatable {
2018 let bit = 1u32 << index;
2019 if seen & bit != 0 {
2020 return Err(SnapshotError::Malformed);
2021 }
2022 seen |= bit;
2023 }
2024 state.absorb_option(config, number, value)?;
2025 }
2026 Ok(state)
2027 }
2028
2029 fn absorb_option(
2031 &mut self,
2032 config: &SessionConfig,
2033 number: u16,
2034 value: &[u8],
2035 ) -> Result<(), SnapshotError> {
2036 let invalid = |_| SnapshotError::InvalidValue;
2037 match u32::from(number) {
2038 prop::PHY_ENABLED => self.settings.enabled = parse_bool(value).map_err(invalid)?,
2039 prop::PHY_FREQ => {
2040 self.settings.freq_khz = validate_freq_khz(config, value).map_err(invalid)?
2041 }
2042 prop::PHY_TX_POWER => {
2046 self.settings.tx_power_dbm = clamp_tx_power(config, value).map_err(invalid)?
2047 }
2048 prop::PHY_LORA_BW => self.settings.bw_hz = validate_bw_hz(value).map_err(invalid)?,
2049 prop::PHY_LORA_SF => self.settings.sf = validate_sf(value).map_err(invalid)?,
2050 prop::PHY_LORA_CR => self.settings.cr_denom = validate_cr(value).map_err(invalid)?,
2051 prop::DEV_KEY => {
2052 self.dev_key = Some(value.try_into().map_err(|_| SnapshotError::InvalidValue)?);
2053 }
2054 prop::DEV_CHANNEL_KEYS => {
2055 let key = channel_key_item(value)?;
2056 self.dev_channel_keys
2057 .insert(ChannelKeyEntry {
2058 key,
2059 id: [0; items::CHANNEL_ID_LEN],
2060 })
2061 .map_err(invalid)?;
2062 }
2063 prop::DEV_PEERS => {
2064 let public_key: [u8; items::PUBLIC_KEY_LEN] =
2065 value.try_into().map_err(|_| SnapshotError::InvalidValue)?;
2066 self.dev_peers.insert(public_key).map_err(invalid)?;
2067 }
2068 prop::DEV_NAME => {
2069 if !valid_device_name(value) {
2070 return Err(SnapshotError::InvalidValue);
2071 }
2072 self.name = [0; MAX_DEVICE_NAME_LEN];
2073 self.name[..value.len()].copy_from_slice(value);
2074 self.name_len = value.len();
2075 }
2076 prop::MAC_REPEATER_ENABLED => {
2077 self.repeater_enabled = parse_bool(value).map_err(invalid)?
2078 }
2079 prop::IDENT_ROLE => self.ident_role = Some(parse_u8(value).map_err(invalid)?),
2080 prop::IDENT_MOBILE => self.ident_mobile = parse_bool(value).map_err(invalid)?,
2081 prop::MAC_REPEATER_REGIONS => {
2082 if let Ok(entry) = region_entry(value) {
2086 let _ = self.repeater_regions.push(entry);
2087 }
2088 }
2089 prop::MAC_REPEATER_DEFAULT_REGION => {
2090 self.repeater_default_region = parse_region_code(value).map_err(invalid)?
2091 }
2092 prop::MAC_REPEATER_MIN_RSSI => {
2093 self.repeater_min_rssi = Some(parse_i16(value).map_err(invalid)?)
2094 }
2095 prop::MAC_REPEATER_MIN_SNR => {
2096 self.repeater_min_snr = Some(parse_i8(value).map_err(invalid)?)
2097 }
2098 prop::DEV_DISCOVERABLE => self.dev_discoverable = parse_bool(value).map_err(invalid)?,
2099 prop::ADVERT_INTERVAL => {
2100 self.advert_interval_s = validate_announce_interval(value).map_err(invalid)?
2101 }
2102 prop::BEACON_INTERVAL => {
2103 self.beacon_interval_s = validate_announce_interval(value).map_err(invalid)?
2104 }
2105 prop::STARTUP_BEACON => self.startup_beacon = parse_bool(value).map_err(invalid)?,
2106 prop::IDENT_LOCATION => {
2107 self.ident_location = validate_ident_location(value).map_err(invalid)?
2108 }
2109 prop::IDENT_ALTITUDE => {
2110 self.ident_altitude_m = validate_ident_altitude(value).map_err(invalid)?
2111 }
2112 prop::GNSS_ENABLED => self.gnss_enabled = parse_bool(value).map_err(invalid)?,
2113 prop::PHY_DUTY_LIMIT => self.duty_limit = parse_u16(value).map_err(invalid)?,
2114 prop::DEV_ADMINS => {
2115 let public_key: [u8; items::PUBLIC_KEY_LEN] =
2116 value.try_into().map_err(|_| SnapshotError::InvalidValue)?;
2117 self.dev_admins.insert(public_key).map_err(invalid)?;
2118 }
2119 prop::TZ_OFFSET => self.tz_offset_min = validate_tz_offset(value).map_err(invalid)?,
2120 prop::GNSS_IDENT_UPDATE => {
2121 self.gnss_ident_update = parse_bool(value).map_err(invalid)?
2122 }
2123 prop::GNSS_IDENT_PRECISION => {
2124 self.gnss_ident_precision = validate_ident_precision(value).map_err(invalid)?
2125 }
2126 prop::GNSS_TIME_TRUST => self.gnss_time_trust = parse_bool(value).map_err(invalid)?,
2127 prop::BLE_ENABLED => self.ble_enabled = parse_bool(value).map_err(invalid)?,
2128 _ => unreachable!("SAVED_SCHEMA row without a decoder arm"),
2129 }
2130 Ok(())
2131 }
2132
2133 fn derive_channel_ids<A: AesProvider, S: Sha256Provider>(
2138 &mut self,
2139 engine: &CryptoEngine<A, S>,
2140 ) {
2141 let table = &mut self.dev_channel_keys;
2142 for entry in table.entries[..table.len].iter_mut().flatten() {
2143 entry.id = engine.derive_channel_id(&ChannelKey(entry.key)).0;
2144 }
2145 }
2146}
2147
2148fn channel_key_item(value: &[u8]) -> Result<[u8; items::CHANNEL_KEY_LEN], SnapshotError> {
2150 value.try_into().map_err(|_| SnapshotError::InvalidValue)
2151}
2152
2153struct SessionState<const TX: usize> {
2157 promiscuous: bool,
2159 backhaul: bool,
2162 pending: Deque<PendingTx, TX>,
2166 drain: Option<DrainState>,
2170 pending_identity: Option<PendingIdentity>,
2175}
2176
2177impl<const TX: usize> Default for SessionState<TX> {
2178 fn default() -> Self {
2179 Self {
2180 promiscuous: false,
2181 backhaul: false,
2182 pending: Deque::new(),
2183 drain: None,
2184 pending_identity: None,
2185 }
2186 }
2187}
2188
2189struct PendingIdentity {
2190 tid: u8,
2191 secret: Option<[u8; PRIVATE_KEY_LEN]>,
2193}
2194
2195struct DrainState {
2196 tid: u8,
2197 remaining: usize,
2198}
2199
2200pub struct Session<A: AesProvider, S: Sha256Provider, const TX: usize = 1> {
2201 config: SessionConfig,
2202 engine: CryptoEngine<A, S>,
2206 device: DeviceDomain,
2207 host: HostDomain,
2208 session: SessionState<TX>,
2209 attached: bool,
2213 link_secure: bool,
2217 saved: Option<SavedState>,
2222 snapshot_rejected: bool,
2227 dev_key: Option<[u8; items::PUBLIC_KEY_LEN]>,
2229 dev_key_persisted: Option<[u8; items::PUBLIC_KEY_LEN]>,
2236 last_status: Status,
2237 dev_domain_version: u32,
2245 alert: AlertState,
2255 alert_deadline_ms: Option<u64>,
2256 ble_bond_count: u8,
2268 ble_link: BleLinkState,
2269 ble_pairing_open: bool,
2274 multi: Option<MultiState>,
2279 binding: Binding,
2284 scratch: [u8; SCRATCH],
2285}
2286
2287#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
2295pub enum Binding {
2296 #[default]
2298 Local,
2299 Admin,
2302}
2303
2304pub const MULTI_MAX: usize = 300;
2308
2309struct MultiState {
2316 tid: u8,
2317 writes: bool,
2320 request: HeaplessVec<u8, MULTI_MAX>,
2321 offset: usize,
2323 reply: HeaplessVec<u8, MULTI_MAX>,
2325 reply_budget: usize,
2330 failed: bool,
2332 truncated: bool,
2335}
2336
2337impl MultiState {
2338 fn new(tid: u8, writes: bool, payload: &[u8], reply_budget: usize) -> Option<Self> {
2339 let mut request = HeaplessVec::new();
2340 request.extend_from_slice(payload).ok()?;
2341 let mut reply = HeaplessVec::new();
2342 let header = Header::new(tid)?;
2343 reply.push(header.to_byte()).ok()?;
2344 reply.push(Cmd::PropAre as u8).ok()?;
2345 Some(Self {
2346 tid,
2347 writes,
2348 request,
2349 offset: 0,
2350 reply,
2351 reply_budget: reply_budget.min(MULTI_MAX),
2352 failed: false,
2353 truncated: false,
2354 })
2355 }
2356
2357 fn room(&self) -> usize {
2358 self.reply_budget.saturating_sub(self.reply.len())
2359 }
2360
2361 fn push_entry(&mut self, key: u32, value: &[u8]) -> bool {
2363 let Some(needed) = frame::entry_len(key, value.len()) else {
2364 return false;
2365 };
2366 if needed > self.room() {
2367 return false;
2368 }
2369 let mut header = [0u8; pui::MAX_LEN * 2];
2370 let body = pui::encoded_len(key) + value.len();
2371 let mut written = pui::encode(body as u32, &mut header).unwrap_or(0);
2372 written += pui::encode(key, &mut header[written..]).unwrap_or(0);
2373 self.reply.extend_from_slice(&header[..written]).is_ok()
2374 && self.reply.extend_from_slice(value).is_ok()
2375 }
2376
2377 fn push_status(&mut self, status: Status) -> bool {
2378 let mut value = [0u8; pui::MAX_LEN];
2379 let len = pui::encode(status.0, &mut value).unwrap_or(0);
2380 self.push_entry(prop::LAST_STATUS, &value[..len])
2381 }
2382}
2383
2384impl<A: AesProvider, S: Sha256Provider, const TX: usize> Session<A, S, TX> {
2385 pub fn new(config: SessionConfig, boot_status: Status, engine: CryptoEngine<A, S>) -> Self {
2388 debug_assert!(usize::from(config.mtu) <= MAX_MTU);
2389 debug_assert!(valid_device_name(config.default_device_name.as_bytes()));
2390 Self {
2391 config,
2392 engine,
2393 device: DeviceDomain::post_reset(&config),
2394 host: HostDomain::default(),
2395 session: SessionState::default(),
2396 attached: false,
2397 link_secure: false,
2398 saved: None,
2399 snapshot_rejected: false,
2400 dev_key: None,
2401 dev_key_persisted: None,
2402 last_status: boot_status,
2403 dev_domain_version: 0,
2404 alert: AlertState::None,
2405 alert_deadline_ms: None,
2406 ble_bond_count: 0,
2407 ble_link: BleLinkState::None,
2408 ble_pairing_open: false,
2409 multi: None,
2410 binding: Binding::Local,
2411 scratch: [0; SCRATCH],
2412 }
2413 }
2414
2415 pub fn settings(&self) -> RadioSettings {
2417 self.device.settings
2418 }
2419
2420 pub fn device_name(&self) -> &str {
2422 core::str::from_utf8(&self.device.name[..self.device.name_len])
2423 .expect("validated device name")
2424 }
2425
2426 pub fn tx_data(&self) -> &[u8] {
2428 self.session
2429 .pending
2430 .front()
2431 .map(|pending| pending.data.as_slice())
2432 .unwrap_or_default()
2433 }
2434
2435 pub fn tx_power(&self) -> TxPower {
2437 self.session
2438 .pending
2439 .front()
2440 .map(|pending| pending.power)
2441 .unwrap_or(TxPower::Default)
2442 }
2443
2444 pub fn max_tx_power_dbm(&self) -> i8 {
2447 self.config.max_tx_power_dbm
2448 }
2449
2450 pub fn tx_nocca(&self) -> bool {
2453 self.session
2454 .pending
2455 .front()
2456 .map(|pending| pending.nocca)
2457 .unwrap_or(false)
2458 }
2459
2460 pub fn has_pending_tx(&self) -> bool {
2462 !self.session.pending.is_empty()
2463 }
2464
2465 pub fn queued_frame_count(&self) -> usize {
2467 self.host.queue.len
2468 }
2469
2470 pub fn dev_domain_version(&self) -> u32 {
2481 self.dev_domain_version
2482 }
2483
2484 fn bump_dev_domain(&mut self) {
2487 self.dev_domain_version = self.dev_domain_version.wrapping_add(1);
2488 }
2489
2490 pub fn dev_channel_keys(&self) -> impl Iterator<Item = [u8; items::CHANNEL_KEY_LEN]> + '_ {
2494 self.device.channel_keys.iter().map(|entry| entry.key)
2495 }
2496
2497 pub fn dev_peers(&self) -> impl Iterator<Item = [u8; items::PUBLIC_KEY_LEN]> + '_ {
2500 self.device.peers.iter().copied()
2501 }
2502
2503 pub fn dev_admins(&self) -> impl Iterator<Item = [u8; items::PUBLIC_KEY_LEN]> + '_ {
2507 self.device.admins.iter().copied()
2508 }
2509
2510 pub fn repeater_enabled(&self) -> bool {
2516 self.device.repeater_enabled
2517 }
2518
2519 pub fn repeater_region_codes(&self) -> impl Iterator<Item = [u8; REGION_CODE_LEN]> + '_ {
2523 self.device.repeater_regions.iter().map(|entry| entry.code)
2524 }
2525
2526 pub fn repeater_region_names(&self) -> impl Iterator<Item = &str> + '_ {
2529 self.device
2530 .repeater_regions
2531 .iter()
2532 .filter_map(|entry| core::str::from_utf8(entry.text()).ok())
2534 }
2535
2536 pub fn repeater_default_region(&self) -> Option<[u8; REGION_CODE_LEN]> {
2539 self.device.repeater_default_region
2540 }
2541
2542 pub fn repeater_min_rssi(&self) -> Option<i16> {
2545 self.device.repeater_min_rssi
2546 }
2547
2548 pub fn repeater_min_snr(&self) -> Option<i8> {
2551 self.device.repeater_min_snr
2552 }
2553
2554 pub fn dev_key(&self) -> Option<&[u8; items::PUBLIC_KEY_LEN]> {
2558 self.dev_key.as_ref()
2559 }
2560
2561 pub fn reset(&mut self, reason: Status, emit: &mut impl FnMut(&[u8])) -> Effect {
2573 self.device = DeviceDomain::post_reset(&self.config);
2574 self.dev_key = self.dev_key_persisted;
2578 self.host.reset(None);
2579 if self.saved.is_some() {
2580 self.apply_saved_device();
2581 }
2582 self.session = SessionState::default();
2583 self.bump_dev_domain();
2586 self.announce_status(reason, emit);
2587 self.apply_radio()
2588 }
2589
2590 fn apply_radio(&self) -> Effect {
2594 let settings = self.device.settings;
2595 self.config
2596 .duty
2597 .set_phy(settings.sf, settings.bw_hz, settings.cr_denom);
2598 Effect::ApplyRadio(settings)
2599 }
2600
2601 fn apply_saved_device(&mut self) {
2611 let saved = self.saved.as_ref().expect("caller checked saved");
2612 for phase in ApplyPhase::ORDER {
2613 for entry in SAVED_SCHEMA.iter().filter(|entry| entry.phase == phase) {
2614 match u32::from(entry.number) {
2615 prop::PHY_ENABLED => {
2624 let identity_matches =
2625 saved.dev_key.is_none_or(|key| Some(key) == self.dev_key);
2626 self.device.settings.enabled = saved.settings.enabled && identity_matches;
2627 }
2628 prop::DEV_KEY => {}
2631 prop::PHY_FREQ => self.device.settings.freq_khz = saved.settings.freq_khz,
2632 prop::PHY_TX_POWER => {
2633 self.device.settings.tx_power_dbm = saved.settings.tx_power_dbm
2634 }
2635 prop::PHY_LORA_BW => self.device.settings.bw_hz = saved.settings.bw_hz,
2636 prop::PHY_LORA_SF => self.device.settings.sf = saved.settings.sf,
2637 prop::PHY_LORA_CR => self.device.settings.cr_denom = saved.settings.cr_denom,
2638 prop::DEV_CHANNEL_KEYS => self.device.channel_keys = saved.dev_channel_keys,
2639 prop::DEV_PEERS => self.device.peers = saved.dev_peers,
2640 prop::DEV_ADMINS => self.device.admins = saved.dev_admins,
2641 prop::DEV_NAME => {
2642 self.device.name = saved.name;
2643 self.device.name_len = saved.name_len;
2644 }
2645 prop::MAC_REPEATER_ENABLED => {
2646 self.device.repeater_enabled = saved.repeater_enabled
2647 }
2648 prop::IDENT_ROLE => self.device.ident_role = saved.ident_role,
2649 prop::IDENT_MOBILE => self.device.ident_mobile = saved.ident_mobile,
2650 prop::MAC_REPEATER_REGIONS => {
2651 self.device.repeater_regions = saved.repeater_regions
2652 }
2653 prop::MAC_REPEATER_DEFAULT_REGION => {
2654 self.device.repeater_default_region = saved.repeater_default_region
2655 }
2656 prop::MAC_REPEATER_MIN_RSSI => {
2657 self.device.repeater_min_rssi = saved.repeater_min_rssi
2658 }
2659 prop::MAC_REPEATER_MIN_SNR => {
2660 self.device.repeater_min_snr = saved.repeater_min_snr
2661 }
2662 prop::DEV_DISCOVERABLE => self.device.dev_discoverable = saved.dev_discoverable,
2663 prop::ADVERT_INTERVAL => {
2664 self.device.advert_interval_s = saved.advert_interval_s
2665 }
2666 prop::BEACON_INTERVAL => {
2667 self.device.beacon_interval_s = saved.beacon_interval_s
2668 }
2669 prop::STARTUP_BEACON => self.device.startup_beacon = saved.startup_beacon,
2670 prop::IDENT_LOCATION => {
2675 self.device.ident_location = saved.ident_location.clone()
2676 }
2677 prop::IDENT_ALTITUDE => self.device.ident_altitude_m = saved.ident_altitude_m,
2678 prop::GNSS_ENABLED => self.device.gnss_enabled = saved.gnss_enabled,
2683 prop::PHY_DUTY_LIMIT => self.config.duty.set_limit(saved.duty_limit),
2684 prop::TZ_OFFSET => self.device.tz_offset_min = saved.tz_offset_min,
2685 prop::GNSS_IDENT_UPDATE => {
2686 self.device.gnss_ident_update = saved.gnss_ident_update
2687 }
2688 prop::GNSS_IDENT_PRECISION => {
2689 self.device.gnss_ident_precision = saved.gnss_ident_precision
2690 }
2691 prop::GNSS_TIME_TRUST => self.device.gnss_time_trust = saved.gnss_time_trust,
2692 prop::BLE_ENABLED => self.device.ble_enabled = saved.ble_enabled,
2693 _ => unreachable!("SAVED_SCHEMA row without an apply arm"),
2694 }
2695 }
2696 }
2697 self.bump_dev_domain();
2698 }
2699
2700 pub fn attach(&mut self, link_secure: bool) {
2712 self.session = SessionState::default();
2713 self.attached = true;
2714 self.link_secure = link_secure;
2715 }
2716
2717 pub fn detach(&mut self) {
2722 self.session = SessionState::default();
2723 self.attached = false;
2724 self.link_secure = false;
2725 }
2726
2727 pub fn handle_frame(
2729 &mut self,
2730 bytes: &[u8],
2731 now_ms: u64,
2732 emit: &mut impl FnMut(&[u8]),
2733 ) -> Option<Effect> {
2734 self.handle_frame_budgeted(bytes, now_ms, MULTI_MAX, emit)
2735 }
2736
2737 pub fn handle_frame_budgeted(
2744 &mut self,
2745 bytes: &[u8],
2746 now_ms: u64,
2747 reply_budget: usize,
2748 emit: &mut impl FnMut(&[u8]),
2749 ) -> Option<Effect> {
2750 self.binding = Binding::Local;
2751 self.dispatch_frame(bytes, now_ms, reply_budget, emit)
2752 }
2753
2754 pub fn handle_admin_frame(
2772 &mut self,
2773 bytes: &[u8],
2774 now_ms: u64,
2775 reply_budget: usize,
2776 emit: &mut impl FnMut(&[u8]),
2777 ) -> Option<Effect> {
2778 self.binding = Binding::Admin;
2779 if Frame::parse(bytes).is_err() {
2785 self.send_status(TID_UNSOLICITED, Status::PARSE_ERROR, emit);
2786 return None;
2787 }
2788 self.dispatch_frame(bytes, now_ms, reply_budget, emit)
2789 }
2790
2791 pub fn end_admin_exchange(&mut self) {
2794 self.binding = Binding::Local;
2795 }
2796
2797 fn is_admin(&self) -> bool {
2800 self.binding == Binding::Admin
2801 }
2802
2803 fn dispatch_frame(
2804 &mut self,
2805 bytes: &[u8],
2806 now_ms: u64,
2807 reply_budget: usize,
2808 emit: &mut impl FnMut(&[u8]),
2809 ) -> Option<Effect> {
2810 let received = Frame::parse(bytes).ok()?;
2813 let tid = received.header.tid();
2814 match received.command() {
2815 Some(Cmd::Nop) => {
2816 self.complete(tid, Status::OK, emit);
2817 None
2818 }
2819 Some(Cmd::Reset) => Some(self.reset(Status::RESET_SOFTWARE, emit)),
2820 Some(Cmd::PropGet) => match PropPayload::parse(received.payload) {
2821 Ok(payload) => self.prop_get(tid, payload.key, now_ms, emit),
2822 Err(_) => {
2823 self.complete(tid, Status::PARSE_ERROR, emit);
2824 None
2825 }
2826 },
2827 Some(Cmd::PropSet) => match PropPayload::parse(received.payload) {
2828 Ok(payload) => self.prop_set(tid, payload.key, payload.value, now_ms, emit),
2829 Err(_) => {
2830 self.complete(tid, Status::PARSE_ERROR, emit);
2831 None
2832 }
2833 },
2834 Some(Cmd::StrSend | Cmd::QueueDrain) if self.is_admin() => {
2839 self.complete(tid, Status::INVALID_COMMAND, emit);
2840 None
2841 }
2842 Some(Cmd::StrSend) => match StreamPayload::parse(received.payload) {
2843 Ok(payload) => self.str_send(tid, &payload, now_ms, emit),
2844 Err(_) => {
2845 self.complete(tid, Status::PARSE_ERROR, emit);
2846 None
2847 }
2848 },
2849 Some(Cmd::PropInsert) => {
2850 match PropPayload::parse(received.payload) {
2851 Ok(payload) => self.prop_insert(tid, payload.key, payload.value, emit),
2852 Err(_) => self.complete(tid, Status::PARSE_ERROR, emit),
2853 }
2854 None
2855 }
2856 Some(Cmd::PropRemove) => {
2857 match PropPayload::parse(received.payload) {
2858 Ok(payload) => self.prop_remove(tid, payload.key, payload.value, emit),
2859 Err(_) => self.complete(tid, Status::PARSE_ERROR, emit),
2860 }
2861 None
2862 }
2863 Some(Cmd::QueueDrain) => {
2867 if self.session.drain.is_some() {
2868 self.complete(tid, Status::BUSY, emit);
2869 return None;
2870 }
2871 if self.host.queue.len == 0 {
2872 self.complete(tid, Status::OK, emit);
2873 return None;
2874 }
2875 self.session.drain = Some(DrainState {
2876 tid,
2877 remaining: self.host.queue.len,
2878 });
2879 Some(Effect::DrainQueue)
2880 }
2881 Some(Cmd::Save) => Some(Effect::SaveSnapshot { tid }),
2885 Some(Cmd::Restore) => {
2895 if self.saved.is_none() {
2896 self.complete(tid, Status::INVALID_STATE, emit);
2897 return None;
2898 }
2899 self.apply_saved_device();
2900 self.session = SessionState::default();
2901 self.announce_status(Status::RESET_RESTORED, emit);
2902 Some(self.apply_radio())
2903 }
2904 Some(Cmd::Clear) => Some(Effect::ClearSaved { tid }),
2909 Some(Cmd::FactoryReset) => Some(Effect::FactoryReset),
2916 Some(Cmd::Reboot) => {
2922 if !self.config.reboot {
2923 self.complete(tid, Status::UNIMPLEMENTED, emit);
2924 return None;
2925 }
2926 Some(Effect::Reboot)
2927 }
2928 Some(Cmd::BleClearBonds) => {
2934 if !self.config.ble_pairing {
2935 self.complete(tid, Status::UNIMPLEMENTED, emit);
2936 return None;
2937 }
2938 Some(Effect::BleClearBonds { tid })
2939 }
2940 Some(Cmd::PropMultiGet) => {
2946 self.begin_multi(tid, false, received.payload, now_ms, reply_budget, emit)
2947 }
2948 Some(Cmd::PropMultiSet) => {
2949 self.begin_multi(tid, true, received.payload, now_ms, reply_budget, emit)
2950 }
2951 Some(
2953 Cmd::PropIs | Cmd::StrRecv | Cmd::PropInserted | Cmd::PropRemoved | Cmd::PropAre,
2954 ) => {
2955 self.complete(tid, Status::INVALID_COMMAND, emit);
2956 None
2957 }
2958 None => {
2959 self.complete(tid, Status::INVALID_COMMAND, emit);
2960 None
2961 }
2962 }
2963 }
2964
2965 pub fn on_radio_rx(
2974 &mut self,
2975 data: &[u8],
2976 info: &RadioRxInfo,
2977 now_ms: u64,
2978 emit: &mut impl FnMut(&[u8]),
2979 ) -> Option<Effect> {
2980 if !self.device.settings.enabled || data.len() > usize::from(self.config.mtu) {
2981 return None;
2982 }
2983 if !self.attached {
2984 if !self.host.accepts_frame(data) {
2985 return None;
2986 }
2987 return match self.evaluate_detached_rx(data, now_ms) {
2988 SecureRx::Duplicate { ack, identity } => {
2989 let original =
2994 identity.and_then(|identity| self.host.queue.seq_for_identity(&identity));
2995 ack.and_then(|plan| self.stage_ack(plan, original, now_ms))
2996 }
2997 verdict => {
2998 let (ack, identity) = match verdict {
2999 SecureRx::New { ack, identity } => (ack, identity),
3000 _ => (None, None),
3001 };
3002 let seq = self.host.queue.push(data, info, now_ms, identity);
3008 ack.and_then(|plan| self.stage_ack(plan, Some(seq), now_ms))
3009 }
3010 };
3011 }
3012 if !self.session.promiscuous && !self.host.accepts_live_frame(data) {
3013 return None;
3014 }
3015 let rx = RxMeta {
3016 rssi_dbm: info.rssi_dbm,
3017 lqi: info.lqi,
3018 snr_cb: info.snr_cb,
3019 };
3020 let mut rx_meta = [0u8; BufferedRxMeta::WIRE_LEN];
3024 let meta_len = if info.self_tx {
3025 BufferedRxMeta {
3026 rx,
3027 flags: RX_FLAG_SELF_TX,
3028 age_s: 0,
3029 }
3030 .encode(&mut rx_meta)
3031 } else {
3032 rx.encode(&mut rx_meta)
3033 }
3034 .expect("buffer sized with WIRE_LEN");
3035 if let Ok(len) = frame::str_recv(
3036 &mut self.scratch,
3037 stream::PHY_RAW,
3038 data,
3039 &rx_meta[..meta_len],
3040 ) {
3041 emit(&self.scratch[..len]);
3042 }
3043 None
3044 }
3045
3046 fn evaluate_detached_rx(&mut self, data: &[u8], now_ms: u64) -> SecureRx {
3051 let Ok(header) = PacketHeader::parse(data) else {
3052 return SecureRx::Plain;
3053 };
3054 let Some(host_key) = &self.host.key else {
3055 return SecureRx::Plain;
3056 };
3057 let host_hint = NodeHint([host_key[0], host_key[1], host_key[2]]);
3058 let packet_type = header.fcf.packet_type();
3059 let wants_ack = packet_type.ack_requested();
3060
3061 let scratch = &mut self.scratch[..data.len()];
3062 scratch.copy_from_slice(data);
3063
3064 let (keys, peer_index) = match packet_type {
3066 PacketType::Unicast | PacketType::UnicastAckReq => {
3067 if header.dst != Some(host_hint) {
3068 return SecureRx::Plain;
3069 }
3070 let Some(index) = self.host.peer_keys.resolve_source(&header.source, data) else {
3071 return SecureRx::Plain;
3072 };
3073 let entry = &self.host.peer_keys.entries[index]
3074 .as_ref()
3075 .expect("resolved index is populated")
3076 .entry;
3077 (
3078 PairwiseKeys {
3079 k_enc: entry.k_enc,
3080 k_mic: entry.k_mic,
3081 },
3082 index,
3083 )
3084 }
3085 PacketType::BlindUnicast | PacketType::BlindUnicastAckReq => {
3086 let Some(channel) = header.channel else {
3090 return SecureRx::Plain;
3091 };
3092 let Some(channel_key) = self
3093 .host
3094 .channel_keys
3095 .iter()
3096 .find(|candidate| candidate.id == channel.0)
3097 .map(|candidate| candidate.key)
3098 else {
3099 return SecureRx::Plain;
3100 };
3101 let channel_keys = self.engine.derive_channel_keys(&ChannelKey(channel_key));
3102 let Ok((dst, source)) =
3103 self.engine
3104 .decrypt_blind_addr(scratch, &header, &channel_keys)
3105 else {
3106 return SecureRx::Plain;
3107 };
3108 if dst != host_hint {
3109 return SecureRx::Plain;
3110 }
3111 let Some(index) = self.host.peer_keys.resolve_source(&source, scratch) else {
3113 return SecureRx::Plain;
3114 };
3115 let entry = &self.host.peer_keys.entries[index]
3116 .as_ref()
3117 .expect("resolved index is populated")
3118 .entry;
3119 let pairwise = PairwiseKeys {
3120 k_enc: entry.k_enc,
3121 k_mic: entry.k_mic,
3122 };
3123 (
3124 self.engine.derive_blind_keys(&pairwise, &channel_keys),
3125 index,
3126 )
3127 }
3128 PacketType::Multicast => {
3134 let Some(channel) = header.channel else {
3135 return SecureRx::Plain;
3136 };
3137 let Some(channel_key) = self
3138 .host
3139 .channel_keys
3140 .iter()
3141 .find(|candidate| candidate.id == channel.0)
3142 .map(|candidate| candidate.key)
3143 else {
3144 return SecureRx::Plain;
3145 };
3146 let derived = self.engine.derive_channel_keys(&ChannelKey(channel_key));
3147 let channel_pairwise = PairwiseKeys {
3148 k_enc: derived.k_enc,
3149 k_mic: derived.k_mic,
3150 };
3151 if self
3152 .engine
3153 .open_packet(scratch, &header, &channel_pairwise)
3154 .is_err()
3155 {
3156 return SecureRx::Plain;
3157 }
3158 let Some(sec_info) = header.sec_info else {
3159 return SecureRx::Plain;
3160 };
3161 let identity =
3162 RxIdentity::new(sec_info.frame_counter, &data[header.mic_range.clone()]);
3163 let Some(identity) = identity else {
3164 return SecureRx::Plain;
3165 };
3166 return if self.host.queue.seq_for_identity(&identity).is_some() {
3171 SecureRx::Duplicate {
3172 ack: None,
3173 identity: Some(identity),
3174 }
3175 } else {
3176 SecureRx::New {
3177 ack: None,
3178 identity: Some(identity),
3179 }
3180 };
3181 }
3182 _ => return SecureRx::Plain,
3183 };
3184
3185 let Ok(body_range) = self.engine.open_packet(scratch, &header, &keys) else {
3187 return SecureRx::Plain;
3188 };
3189 let Some(sec_info) = header.sec_info else {
3190 return SecureRx::Plain;
3191 };
3192 let counter = sec_info.frame_counter;
3193 let mic = &data[header.mic_range.clone()];
3194
3195 let plan = wants_ack.then(|| {
3199 let full_mac = self.engine.s2v_tag(
3200 &keys.k_mic,
3201 |cmac| umsh_core::feed_aad(&header, scratch, |chunk| cmac.update(chunk)),
3202 &scratch[body_range.clone()],
3203 );
3204 AckPlan {
3205 trailer: self.engine.compute_ack_trailer(&full_mac, &keys.k_enc),
3206 flood_hops: header.flood_hops.map(|hops| hops.accumulated()),
3212 }
3213 });
3214 let identity = RxIdentity::new(counter, mic);
3215
3216 let window = &mut self.host.peer_keys.entries[peer_index]
3217 .as_mut()
3218 .expect("resolved index is populated")
3219 .window;
3220 match window.check(counter, mic, now_ms) {
3221 ReplayVerdict::Accept => {
3222 window.accept(counter, mic, now_ms);
3223 SecureRx::New {
3224 ack: plan,
3225 identity,
3226 }
3227 }
3228 ReplayVerdict::Replay => {
3229 let holdoff_ms = u64::from(lora_airtime_ms(
3237 self.device.settings.sf,
3238 self.device.settings.bw_hz,
3239 self.device.settings.cr_denom,
3240 data.len(),
3241 )) * 285
3242 / 100;
3243 let ack = window
3244 .note_acknowledgeable_duplicate(counter, mic, now_ms, holdoff_ms)
3245 .then_some(())
3246 .and(plan);
3247 SecureRx::Duplicate { ack, identity }
3248 }
3249 ReplayVerdict::OutOfWindow | ReplayVerdict::Stale => SecureRx::Plain,
3253 }
3254 }
3255
3256 fn stage_ack(&mut self, plan: AckPlan, ack_for: Option<u16>, now_ms: u64) -> Option<Effect> {
3263 if !self.host.auto_ack || !self.session.pending.is_empty() {
3264 return None;
3265 }
3266 let mut buf = [0u8; 24];
3267 let mut builder = PacketBuilder::new(&mut buf).mac_ack(plan.trailer);
3268 if let Some(hops) = plan.flood_hops {
3269 builder = builder.flood_hops(hops.clamp(1, 15));
3273 }
3274 let frame_len = builder.build().ok()?.len();
3275 let airtime_ms = lora_airtime_ms(
3276 self.device.settings.sf,
3277 self.device.settings.bw_hz,
3278 self.device.settings.cr_denom,
3279 frame_len,
3280 );
3281 if self.config.duty.would_exceed(now_ms, airtime_ms) {
3282 return None;
3283 }
3284 let mut data = HeaplessVec::new();
3285 data.extend_from_slice(&buf[..frame_len]).ok()?;
3286 self.session
3287 .pending
3288 .push_back(PendingTx {
3289 data,
3290 tid: TID_UNSOLICITED,
3291 airtime_ms,
3292 power: TxPower::Default,
3293 autonomous: true,
3294 ack_for,
3295 nocca: true,
3300 })
3301 .ok()?;
3302 Some(Effect::StartTransmit)
3303 }
3304
3305 pub fn on_tx_result(
3308 &mut self,
3309 outcome: TxOutcome,
3310 now_ms: u64,
3311 emit: &mut impl FnMut(&[u8]),
3312 ) -> Option<Effect> {
3313 let Some(pending) = self.session.pending.pop_front() else {
3314 return None;
3315 };
3316 match outcome {
3317 TxOutcome::Sent => {
3318 self.config.duty.record(now_ms, pending.airtime_ms);
3319 if pending.autonomous {
3320 if let Some(seq) = pending.ack_for {
3327 self.host.queue.mark_acked(seq);
3328 }
3329 } else {
3330 self.complete(pending.tid, Status::OK, emit);
3331 }
3332 }
3333 TxOutcome::ChannelBusy if !pending.autonomous => {
3337 self.complete(pending.tid, Status::CCA_FAILURE, emit);
3338 }
3339 TxOutcome::Failed if !pending.autonomous => {
3340 self.complete(pending.tid, Status::FAILURE, emit);
3341 }
3342 TxOutcome::ChannelBusy | TxOutcome::Failed => {}
3343 }
3344 (!self.session.pending.is_empty()).then_some(Effect::StartTransmit)
3345 }
3346
3347 fn begin_multi(
3352 &mut self,
3353 tid: u8,
3354 writes: bool,
3355 payload: &[u8],
3356 now_ms: u64,
3357 reply_budget: usize,
3358 emit: &mut impl FnMut(&[u8]),
3359 ) -> Option<Effect> {
3360 debug_assert!(self.multi.is_none());
3361 let Some(state) = MultiState::new(tid, writes, payload, reply_budget) else {
3362 self.complete(tid, Status::NOMEM, emit);
3365 return None;
3366 };
3367 self.multi = Some(state);
3368 self.advance_multi(now_ms, emit)
3369 }
3370
3371 fn advance_multi(&mut self, now_ms: u64, emit: &mut impl FnMut(&[u8])) -> Option<Effect> {
3378 loop {
3379 let mut value = [0u8; MULTI_MAX];
3380 let (tid, writes) = {
3381 let state = self.multi.as_ref()?;
3382 (state.tid, state.writes)
3383 };
3384 let next = {
3385 let state = self.multi.as_mut()?;
3386 if state.truncated || (state.writes && state.failed) {
3389 None
3390 } else {
3391 let rest = &state.request[state.offset..];
3392 if rest.is_empty() {
3393 None
3394 } else if state.writes {
3395 let mut entries = MultiEntries::new(rest);
3396 match entries.next() {
3397 Some(Ok(entry)) => {
3398 let consumed = rest.len() - entries.remainder().len();
3399 state.offset += consumed;
3400 match frame::entry_len(entry.key, entry.value.len()) {
3406 Some(needed) if needed <= state.room() => {
3407 value[..entry.value.len()].copy_from_slice(entry.value);
3408 Some((entry.key, entry.value.len()))
3409 }
3410 _ => {
3411 state.truncated = true;
3412 None
3413 }
3414 }
3415 }
3416 Some(Err(_)) => {
3417 state.offset = state.request.len();
3418 state.failed = true;
3419 state.truncated |= !state.push_status(Status::PARSE_ERROR);
3420 None
3421 }
3422 None => None,
3423 }
3424 } else {
3425 match pui::decode(rest) {
3426 Ok((key, consumed)) => {
3427 state.offset += consumed;
3428 Some((key, 0))
3429 }
3430 Err(_) => {
3431 state.offset = state.request.len();
3432 state.truncated |= !state.push_status(Status::PARSE_ERROR);
3433 None
3434 }
3435 }
3436 }
3437 }
3438 };
3439 let Some((key, value_len)) = next else {
3440 return self.finish_multi(emit);
3441 };
3442 let effect = if writes {
3443 self.prop_set(tid, key, &value[..value_len], now_ms, emit)
3444 } else {
3445 self.prop_get(tid, key, now_ms, emit)
3446 };
3447 if effect.is_some() {
3451 return effect;
3452 }
3453 }
3454 }
3455
3456 fn finish_multi(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<Effect> {
3462 let state = self.multi.take()?;
3463 emit(&state.reply);
3464 None
3465 }
3466
3467 pub fn resume_multi(&mut self, now_ms: u64, emit: &mut impl FnMut(&[u8])) -> Option<Effect> {
3473 self.multi.as_ref()?;
3474 self.advance_multi(now_ms, emit)
3475 }
3476
3477 fn prop_get(
3478 &mut self,
3479 tid: u8,
3480 key: u32,
3481 now_ms: u64,
3482 emit: &mut impl FnMut(&[u8]),
3483 ) -> Option<Effect> {
3484 if self.is_admin() && !admin_reachable(key) {
3485 self.complete(tid, Status::PROP_NOT_FOUND, emit);
3486 return None;
3487 }
3488 if key == prop::BLE_PAIRING_PIN || key == prop::DEV_PRIVATE_KEY {
3496 self.complete(tid, Status::UNIMPLEMENTED, emit);
3497 return None;
3498 }
3499 if key == prop::PHY_RSSI {
3500 if self.device.settings.enabled {
3501 return Some(Effect::SampleRssi { tid });
3502 }
3503 self.complete(tid, Status::INVALID_STATE, emit);
3504 return None;
3505 }
3506 if key == prop::IDENT {
3515 return Some(Effect::SignIdentity { tid });
3516 }
3517 if key == prop::BATTERY
3522 && let Some(fields) = self.config.battery
3523 {
3524 if fields.any() {
3525 return Some(Effect::SampleBattery { tid });
3526 }
3527 self.send_prop_is(tid, prop::BATTERY, &[], emit);
3528 return None;
3529 }
3530 if key == prop::TIME && self.config.time.is_some() {
3536 return Some(Effect::ReadTime { tid });
3537 }
3538 if gnss::is_positioning_property(key) && self.config.gnss.is_some() {
3540 return Some(Effect::SampleGnss { tid, key });
3541 }
3542 if key == prop::ILLUMINANCE && self.config.illuminance {
3545 return Some(Effect::SampleIlluminance { tid });
3546 }
3547 let mut value = [0u8; PROP_BUF];
3548 match self.encode_prop(key, now_ms, &mut value) {
3549 PropValue::Encoded(len) => self.send_prop_is(tid, key, &value[..len], emit),
3550 PropValue::Unimplemented => self.complete(tid, Status::UNIMPLEMENTED, emit),
3551 PropValue::Unknown => self.complete(tid, Status::PROP_NOT_FOUND, emit),
3552 }
3553 None
3554 }
3555
3556 pub fn respond_rssi(&mut self, tid: u8, rssi: Result<i16, ()>, emit: &mut impl FnMut(&[u8])) {
3560 match rssi {
3561 Ok(dbm) => {
3562 let clamped = dbm.clamp(i16::from(i8::MIN), i16::from(i8::MAX)) as i8;
3563 self.send_prop_is(tid, prop::PHY_RSSI, &[clamped as u8], emit);
3564 }
3565 Err(()) => self.complete(tid, Status::FAILURE, emit),
3566 }
3567 }
3568
3569 pub fn respond_identity_blob(
3575 &mut self,
3576 tid: u8,
3577 blob: Result<&[u8], ()>,
3578 emit: &mut impl FnMut(&[u8]),
3579 ) {
3580 match blob {
3581 Ok(bytes) => self.send_prop_is(tid, prop::IDENT, bytes, emit),
3582 Err(()) => self.complete(tid, Status::FAILURE, emit),
3583 }
3584 }
3585
3586 pub fn ident_role(&self) -> Option<u8> {
3589 self.device.ident_role
3590 }
3591
3592 pub fn ident_mobile(&self) -> bool {
3595 self.device.ident_mobile
3596 }
3597
3598 pub fn ident_location(&self) -> &[u8] {
3601 &self.device.ident_location
3602 }
3603
3604 pub fn ident_altitude_m(&self) -> Option<i32> {
3607 self.device.ident_altitude_m
3608 }
3609
3610 pub fn absorb_ident_fix(&mut self, location: &[u8], altitude_m: Option<i32>) -> bool {
3621 if !self.gnss_ident_update() {
3622 return false;
3623 }
3624 let clamped = &location[..location.len().min(self.gnss_ident_precision() as usize)];
3625 if self.device.ident_location == clamped && self.device.ident_altitude_m == altitude_m {
3626 return false;
3627 }
3628 self.device.ident_location = HeaplessVec::from_slice(clamped).unwrap_or_default();
3629 self.device.ident_altitude_m = altitude_m;
3630 self.bump_dev_domain();
3631 true
3632 }
3633
3634 pub fn dev_discoverable(&self) -> bool {
3637 self.device.dev_discoverable
3638 }
3639
3640 pub fn advert_interval_s(&self) -> u32 {
3643 self.device.advert_interval_s
3644 }
3645
3646 pub fn beacon_interval_s(&self) -> u32 {
3649 self.device.beacon_interval_s
3650 }
3651
3652 pub fn startup_beacon(&self) -> bool {
3654 self.device.startup_beacon
3655 }
3656
3657 pub fn tz_offset_min(&self) -> i16 {
3659 self.device.tz_offset_min
3660 }
3661
3662 pub fn gnss_enabled(&self) -> bool {
3667 self.config.gnss.is_some() && self.device.gnss_enabled
3668 }
3669
3670 pub fn gnss_ident_update(&self) -> bool {
3673 self.config.gnss.is_some() && self.device.gnss_ident_update
3674 }
3675
3676 pub fn ble_enabled(&self) -> bool {
3682 self.config.ble && self.device.ble_enabled
3683 }
3684
3685 pub fn ble_bond_count(&self) -> u8 {
3687 if self.config.ble_pairing {
3688 self.ble_bond_count
3689 } else {
3690 0
3691 }
3692 }
3693
3694 pub fn ble_link(&self) -> BleLinkState {
3700 if self.config.ble {
3701 self.ble_link
3702 } else {
3703 BleLinkState::None
3704 }
3705 }
3706
3707 pub fn ble_pairing(&self) -> bool {
3709 self.config.ble_pairing && self.ble_pairing_open
3710 }
3711
3712 pub fn gnss_ident_precision(&self) -> u8 {
3715 self.device.gnss_ident_precision
3716 }
3717
3718 pub fn gnss_time_trust(&self) -> bool {
3721 self.device.gnss_time_trust
3722 }
3723
3724 pub fn alert(&self) -> AlertState {
3727 self.alert
3728 }
3729
3730 pub fn alert_deadline_ms(&self) -> Option<u64> {
3740 self.alert_deadline_ms
3741 }
3742
3743 pub fn poll_alert(&mut self, now_ms: u64, emit: &mut impl FnMut(&[u8])) -> Option<Effect> {
3750 match self.alert_deadline_ms {
3751 Some(deadline) if now_ms >= deadline => self.clear_alert(emit),
3752 _ => None,
3753 }
3754 }
3755
3756 pub fn cancel_alert(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<Effect> {
3763 self.clear_alert(emit)
3764 }
3765
3766 pub fn toggle_gnss(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<bool> {
3772 self.toggle_device_flag(
3773 self.config.gnss.is_some(),
3774 prop::GNSS_ENABLED,
3775 |device| &mut device.gnss_enabled,
3776 emit,
3777 )
3778 }
3779
3780 pub fn toggle_gnss_ident_update(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<bool> {
3787 self.toggle_device_flag(
3788 self.config.gnss.is_some(),
3789 prop::GNSS_IDENT_UPDATE,
3790 |device| &mut device.gnss_ident_update,
3791 emit,
3792 )
3793 }
3794
3795 pub fn toggle_repeater(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<bool> {
3800 self.toggle_device_flag(
3801 true,
3802 prop::MAC_REPEATER_ENABLED,
3803 |device| &mut device.repeater_enabled,
3804 emit,
3805 )
3806 }
3807
3808 pub fn toggle_ble(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<bool> {
3814 self.toggle_device_flag(
3815 self.config.ble,
3816 prop::BLE_ENABLED,
3817 |device| &mut device.ble_enabled,
3818 emit,
3819 )
3820 }
3821
3822 pub fn force_ble_on(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<bool> {
3831 if !self.config.ble || self.device.ble_enabled {
3832 return None;
3833 }
3834 self.device.ble_enabled = true;
3835 self.bump_dev_domain();
3836 if self.attached {
3837 self.announce_prop_is(prop::BLE_ENABLED, &[1], emit);
3838 }
3839 Some(true)
3840 }
3841
3842 fn toggle_device_flag(
3852 &mut self,
3853 available: bool,
3854 key: u32,
3855 pick: fn(&mut DeviceDomain) -> &mut bool,
3856 emit: &mut impl FnMut(&[u8]),
3857 ) -> Option<bool> {
3858 if !available {
3859 return None;
3860 }
3861 let flag = pick(&mut self.device);
3862 *flag = !*flag;
3863 let enabled = *flag;
3864 self.bump_dev_domain();
3865 if self.attached {
3866 self.announce_prop_is(key, &[enabled as u8], emit);
3867 }
3868 Some(enabled)
3869 }
3870
3871 fn clear_alert(&mut self, emit: &mut impl FnMut(&[u8])) -> Option<Effect> {
3874 if !self.alert.is_active() {
3875 return None;
3876 }
3877 self.alert = AlertState::None;
3878 self.alert_deadline_ms = None;
3879 if self.attached {
3883 let mut value = [0u8; pui::MAX_LEN];
3884 let len = pui::encode(AlertState::None.code(), &mut value).unwrap_or(0);
3885 self.announce_prop_is(prop::ALERT, &value[..len], emit);
3886 }
3887 Some(Effect::ApplyAlert(AlertState::None))
3888 }
3889
3890 pub fn publish_battery(&mut self, sample: BatteryStatus, emit: &mut impl FnMut(&[u8])) -> bool {
3907 if !self.attached {
3908 return false;
3909 }
3910 let Some(fields) = self.config.battery else {
3911 return false;
3912 };
3913 if !fields.matches(&sample) {
3914 return false;
3915 }
3916 let mut value = [0u8; battery::MAX_ENCODED_LEN];
3917 let Ok(len) = sample.encode(&mut value) else {
3918 return false;
3919 };
3920 self.announce_prop_is(prop::BATTERY, &value[..len], emit)
3921 }
3922
3923 pub fn respond_battery(
3933 &mut self,
3934 tid: u8,
3935 sample: Result<BatteryStatus, ()>,
3936 emit: &mut impl FnMut(&[u8]),
3937 ) {
3938 let fields = self.config.battery.unwrap_or_default();
3939 match sample {
3940 Ok(snapshot) if fields.matches(&snapshot) => {
3941 let mut value = [0u8; battery::MAX_ENCODED_LEN];
3942 match snapshot.encode(&mut value) {
3943 Ok(len) => self.send_prop_is(tid, prop::BATTERY, &value[..len], emit),
3944 Err(_) => self.complete(tid, Status::FAILURE, emit),
3945 }
3946 }
3947 Ok(_) | Err(()) => self.complete(tid, Status::FAILURE, emit),
3948 }
3949 }
3950
3951 pub fn respond_illuminance(
3960 &mut self,
3961 tid: u8,
3962 millilux: Option<u32>,
3963 emit: &mut impl FnMut(&[u8]),
3964 ) {
3965 match millilux {
3966 Some(value) => {
3967 self.send_prop_is(tid, prop::ILLUMINANCE, &value.to_le_bytes(), emit);
3968 }
3969 None => self.send_prop_is(tid, prop::ILLUMINANCE, &[], emit),
3970 }
3971 }
3972
3973 pub fn respond_time(&mut self, tid: u8, epoch: Option<u32>, emit: &mut impl FnMut(&[u8])) {
3982 match epoch {
3983 Some(seconds) => self.send_prop_is(tid, prop::TIME, &seconds.to_le_bytes(), emit),
3984 None => self.send_prop_is(tid, prop::TIME, &[], emit),
3985 }
3986 }
3987
3988 pub fn publish_time(&mut self, epoch: Option<u32>, emit: &mut impl FnMut(&[u8])) -> bool {
3999 if !self.attached || self.config.time.is_none() {
4000 return false;
4001 }
4002 match epoch {
4003 Some(seconds) => self.announce_prop_is(prop::TIME, &seconds.to_le_bytes(), emit),
4004 None => self.announce_prop_is(prop::TIME, &[], emit),
4005 }
4006 }
4007
4008 pub fn respond_gnss(
4017 &mut self,
4018 tid: u8,
4019 key: u32,
4020 sample: Result<GnssSnapshot, ()>,
4021 emit: &mut impl FnMut(&[u8]),
4022 ) {
4023 let mut value = [0u8; gnss::MAX_VALUE_LEN];
4024 match sample.and_then(|snapshot| snapshot.encode(key, &mut value).map_err(|_| ())) {
4025 Ok(len) => self.send_prop_is(tid, key, &value[..len], emit),
4026 Err(()) => self.complete(tid, Status::FAILURE, emit),
4027 }
4028 }
4029
4030 pub fn publish_gnss(
4042 &mut self,
4043 key: u32,
4044 snapshot: &GnssSnapshot,
4045 emit: &mut impl FnMut(&[u8]),
4046 ) -> bool {
4047 if !self.attached || self.config.gnss.is_none() {
4048 return false;
4049 }
4050 let mut value = [0u8; gnss::MAX_VALUE_LEN];
4051 let Ok(len) = snapshot.encode(key, &mut value) else {
4052 return false;
4053 };
4054 self.announce_prop_is(key, &value[..len], emit)
4055 }
4056
4057 pub fn drain_step(&mut self, now_ms: u64, emit: &mut impl FnMut(&[u8])) -> bool {
4063 let Some(drain) = &mut self.session.drain else {
4064 return false;
4065 };
4066 if drain.remaining == 0 {
4067 let tid = drain.tid;
4068 self.session.drain = None;
4069 self.complete(tid, Status::OK, emit);
4070 return false;
4071 }
4072 drain.remaining -= 1;
4073 let Some(entry) = self.host.queue.pop_front() else {
4074 let tid = drain.tid;
4077 self.session.drain = None;
4078 self.complete(tid, Status::OK, emit);
4079 return false;
4080 };
4081 let mut rx_meta = [0u8; BufferedRxMeta::WIRE_LEN];
4082 let meta_len = BufferedRxMeta {
4083 rx: RxMeta {
4084 rssi_dbm: entry.rssi_dbm,
4085 lqi: entry.lqi,
4086 snr_cb: entry.snr_cb,
4087 },
4088 flags: RX_FLAG_BUFFERED
4089 | if entry.acked { RX_FLAG_ACKED } else { 0 }
4090 | if entry.self_tx { RX_FLAG_SELF_TX } else { 0 },
4091 age_s: u32::try_from(now_ms.saturating_sub(entry.rx_time_ms) / 1000)
4092 .unwrap_or(u32::MAX),
4093 }
4094 .encode(&mut rx_meta)
4095 .expect("buffer sized with WIRE_LEN");
4096 if let Ok(len) = frame::str_recv(
4097 &mut self.scratch,
4098 stream::PHY_RAW,
4099 entry.frame(),
4100 &rx_meta[..meta_len],
4101 ) {
4102 emit(&self.scratch[..len]);
4103 }
4104 true
4105 }
4106
4107 pub fn encode_snapshot(&self, out: &mut [u8]) -> Option<usize> {
4111 SavedState::capture(&self.device, self.config.duty.limit(), self.dev_key).encode(out)
4112 }
4113
4114 pub fn restore_at_boot(&mut self, bytes: &[u8]) -> Result<Effect, SnapshotError> {
4127 let mut saved = SavedState::decode(&self.config, bytes)?;
4128 saved.derive_channel_ids(&self.engine);
4129 self.saved = Some(saved);
4130 self.apply_saved_device();
4131 Ok(self.apply_radio())
4132 }
4133
4134 pub fn note_snapshot_rejected(&mut self) {
4143 self.snapshot_rejected = true;
4144 }
4145
4146 pub fn saved_status(&self) -> SavedStatus {
4148 match (self.saved.is_some(), self.snapshot_rejected) {
4149 (true, false) => SavedStatus::Current,
4150 (true, true) => SavedStatus::Fallback,
4151 (false, true) => SavedStatus::Unreadable,
4152 (false, false) => SavedStatus::None,
4153 }
4154 }
4155
4156 pub fn respond_save(&mut self, tid: u8, result: Result<(), ()>, emit: &mut impl FnMut(&[u8])) {
4162 match result {
4163 Ok(()) => {
4164 self.note_snapshot_saved();
4165 self.complete(tid, Status::OK, emit);
4166 }
4167 Err(()) => self.complete(tid, Status::FAILURE, emit),
4168 }
4169 }
4170
4171 pub fn note_snapshot_saved(&mut self) {
4181 self.saved = Some(SavedState::capture(
4182 &self.device,
4183 self.config.duty.limit(),
4184 self.dev_key,
4185 ));
4186 }
4187
4188 pub fn respond_clear(&mut self, tid: u8, result: Result<(), ()>, emit: &mut impl FnMut(&[u8])) {
4193 match result {
4194 Ok(()) => {
4195 self.saved = None;
4196 self.dev_key_persisted = None;
4197 self.complete(tid, Status::OK, emit);
4198 }
4199 Err(()) => self.complete(tid, Status::FAILURE, emit),
4200 }
4201 }
4202
4203 pub fn identity_request(&self) -> Option<IdentitySource> {
4206 self.session
4207 .pending_identity
4208 .as_ref()
4209 .map(|pending| match pending.secret {
4210 Some(secret) => IdentitySource::Install(secret),
4211 None => IdentitySource::Generate,
4212 })
4213 }
4214
4215 pub fn respond_identity(
4224 &mut self,
4225 tid: u8,
4226 result: Result<[u8; items::PUBLIC_KEY_LEN], ()>,
4227 emit: &mut impl FnMut(&[u8]),
4228 ) {
4229 let matched = self
4230 .session
4231 .pending_identity
4232 .take_if(|pending| pending.tid == tid)
4233 .is_some();
4234 match result {
4235 Ok(public_key) => {
4236 self.dev_key = Some(public_key);
4237 self.dev_key_persisted = Some(public_key);
4238 self.bump_dev_domain();
4244 if matched {
4245 self.send_prop_is(tid, prop::DEV_KEY, &public_key, emit);
4246 }
4247 }
4248 Err(()) if matched => self.complete(tid, Status::FAILURE, emit),
4249 Err(()) => {}
4250 }
4251 }
4252
4253 pub fn set_boot_identity(&mut self, public_key: [u8; items::PUBLIC_KEY_LEN]) {
4257 self.dev_key = Some(public_key);
4258 self.dev_key_persisted = Some(public_key);
4259 }
4260
4261 pub fn respond_pin_set(
4263 &mut self,
4264 tid: u8,
4265 result: Result<(), ()>,
4266 emit: &mut impl FnMut(&[u8]),
4267 ) {
4268 self.complete(
4269 tid,
4270 if result.is_ok() {
4271 Status::OK
4272 } else {
4273 Status::INTERNAL_ERROR
4274 },
4275 emit,
4276 );
4277 }
4278
4279 pub fn respond_ble_clear_bonds(
4281 &mut self,
4282 tid: u8,
4283 result: Result<(), ()>,
4284 emit: &mut impl FnMut(&[u8]),
4285 ) {
4286 self.complete(
4287 tid,
4288 if result.is_ok() {
4289 Status::OK
4290 } else {
4291 Status::INTERNAL_ERROR
4292 },
4293 emit,
4294 );
4295 }
4296
4297 pub fn set_ble_bond_count(&mut self, count: u8, emit: &mut impl FnMut(&[u8])) {
4305 if !self.config.ble_pairing || self.ble_bond_count == count {
4306 return;
4307 }
4308 self.ble_bond_count = count;
4309 if self.attached {
4310 self.announce_prop_is(prop::BLE_BOND_COUNT, &[count], emit);
4311 }
4312 }
4313
4314 pub fn set_ble_link(&mut self, state: BleLinkState, emit: &mut impl FnMut(&[u8])) {
4324 if !self.config.ble || self.ble_link == state {
4325 return;
4326 }
4327 self.ble_link = state;
4328 if self.attached {
4329 self.announce_prop_is(prop::BLE_LINK, &[state.code()], emit);
4330 }
4331 }
4332
4333 pub fn set_ble_pairing(&mut self, open: bool, emit: &mut impl FnMut(&[u8])) {
4341 if !self.config.ble_pairing || self.ble_pairing_open == open {
4342 return;
4343 }
4344 self.ble_pairing_open = open;
4345 if self.attached {
4346 self.announce_prop_is(prop::BLE_PAIRING, &[open as u8], emit);
4347 }
4348 }
4349
4350 pub fn respond_ble_pairing(
4358 &mut self,
4359 tid: u8,
4360 result: Result<bool, ()>,
4361 emit: &mut impl FnMut(&[u8]),
4362 ) {
4363 match result {
4364 Ok(open) => {
4365 self.ble_pairing_open = open;
4366 self.send_prop_is(tid, prop::BLE_PAIRING, &[open as u8], emit);
4367 }
4368 Err(()) => self.complete(tid, Status::INVALID_STATE, emit),
4369 }
4370 }
4371
4372 fn prop_set(
4373 &mut self,
4374 tid: u8,
4375 key: u32,
4376 value: &[u8],
4377 now_ms: u64,
4378 emit: &mut impl FnMut(&[u8]),
4379 ) -> Option<Effect> {
4380 if self.is_admin() && !admin_reachable(key) {
4381 self.complete(tid, Status::PROP_NOT_FOUND, emit);
4382 return None;
4383 }
4384 if key == prop::BLE_PAIRING_PIN {
4385 let pin = if value.is_empty() {
4386 None
4387 } else {
4388 match parse_u32(value) {
4389 Ok(pin) if pin <= 999_999 => Some(pin),
4390 _ => {
4391 self.complete(tid, Status::INVALID_ARGUMENT, emit);
4392 return None;
4393 }
4394 }
4395 };
4396 return Some(Effect::SetPairingPin { tid, pin });
4397 }
4398 if key == prop::BLE_PAIRING {
4403 if !self.config.ble_pairing {
4404 self.complete(tid, Status::PROP_NOT_FOUND, emit);
4405 return None;
4406 }
4407 let open = match parse_bool(value) {
4408 Ok(open) => open,
4409 Err(status) => {
4410 self.complete(tid, status, emit);
4411 return None;
4412 }
4413 };
4414 return Some(Effect::SetBlePairing { tid, open });
4415 }
4416 if key == prop::DEV_PRIVATE_KEY {
4417 if let Err(status) = self.require_secure_link() {
4422 self.complete(tid, status, emit);
4423 return None;
4424 }
4425 let secret = match value.len() {
4426 0 => None,
4427 PRIVATE_KEY_LEN => Some(value.try_into().expect("length checked")),
4428 _ => {
4429 self.complete(tid, Status::INVALID_ARGUMENT, emit);
4430 return None;
4431 }
4432 };
4433 if self.session.pending_identity.is_some() {
4434 self.complete(tid, Status::BUSY, emit);
4435 return None;
4436 }
4437 self.session.pending_identity = Some(PendingIdentity { tid, secret });
4438 return Some(Effect::ProvisionIdentity { tid });
4439 }
4440 if key == prop::HOST_KEY {
4441 let new_key = match value.len() {
4442 0 => None,
4443 items::PUBLIC_KEY_LEN => {
4444 let mut key = [0; items::PUBLIC_KEY_LEN];
4445 key.copy_from_slice(value);
4446 Some(key)
4447 }
4448 _ => {
4449 self.complete(tid, Status::INVALID_ARGUMENT, emit);
4450 return None;
4451 }
4452 };
4453 if new_key != self.host.key {
4462 self.host.reset(new_key);
4463 }
4464 self.send_prop_is(tid, key, value, emit);
4465 return None;
4466 }
4467 if key == prop::ALERT {
4471 let Some(config) = self.config.alert else {
4472 self.complete(tid, Status::PROP_NOT_FOUND, emit);
4473 return None;
4474 };
4475 let state = match pui::decode(value) {
4476 Ok((code, consumed)) if consumed == value.len() => AlertState::from_code(code),
4477 _ => None,
4478 };
4479 let Some(state) = state else {
4480 self.complete(tid, Status::INVALID_ARGUMENT, emit);
4481 return None;
4482 };
4483 self.alert = state;
4484 self.alert_deadline_ms = state
4488 .is_active()
4489 .then(|| now_ms.saturating_add(u64::from(config.timeout_ms)));
4490 let mut echo = [0u8; pui::MAX_LEN];
4491 let len = pui::encode(state.code(), &mut echo).unwrap_or(0);
4492 self.send_prop_is(tid, key, &echo[..len], emit);
4493 return Some(Effect::ApplyAlert(state));
4494 }
4495 if key == prop::TIME && self.config.time.is_some() {
4500 let epoch = match value {
4501 [] => None,
4502 _ => match parse_u32(value) {
4503 Ok(seconds) => Some(seconds),
4504 Err(status) => {
4505 self.complete(tid, status, emit);
4506 return None;
4507 }
4508 },
4509 };
4510 self.send_prop_is(tid, key, value, emit);
4511 return Some(Effect::ApplyTime { epoch });
4512 }
4513 if key == prop::MAC_BACKHAUL && self.config.mac_node {
4518 let enabled = match parse_bool(value) {
4519 Ok(enabled) => enabled,
4520 Err(status) => {
4521 self.complete(tid, status, emit);
4522 return None;
4523 }
4524 };
4525 self.session.backhaul = enabled;
4527 self.send_prop_is(tid, key, &[enabled as u8], emit);
4528 return Some(Effect::ApplyBackhaul { enabled });
4529 }
4530 if key == prop::DEV_NAME {
4531 if !valid_device_name(value) {
4532 self.complete(tid, Status::INVALID_ARGUMENT, emit);
4533 return None;
4534 }
4535 self.device.name[..value.len()].copy_from_slice(value);
4536 self.device.name_len = value.len();
4537 self.send_prop_is(tid, key, value, emit);
4538 return Some(Effect::DeviceNameChanged);
4539 }
4540 let radio_affecting = match self.apply_prop_set(key, value) {
4541 Ok(radio_affecting) => radio_affecting,
4542 Err(status) => {
4543 self.complete(tid, status, emit);
4544 return None;
4545 }
4546 };
4547 let mut encoded = [0u8; PROP_BUF];
4549 if let PropValue::Encoded(len) = self.encode_prop(key, now_ms, &mut encoded) {
4550 self.send_prop_is(tid, key, &encoded[..len], emit);
4551 }
4552 radio_affecting.then(|| self.apply_radio())
4553 }
4554
4555 fn apply_prop_set(&mut self, key: u32, value: &[u8]) -> Result<bool, Status> {
4558 if let Some((counter, ledger)) = self.stats_counter(key) {
4564 if parse_u32(value)? != 0 {
4565 return Err(Status::INVALID_ARGUMENT);
4566 }
4567 ledger.reset(counter);
4568 return Ok(false);
4569 }
4570 match key {
4571 prop::PHY_ENABLED => {
4572 self.device.settings.enabled = parse_bool(value)?;
4573 Ok(true)
4574 }
4575 prop::PHY_FREQ => {
4576 self.device.settings.freq_khz = validate_freq_khz(&self.config, value)?;
4577 Ok(true)
4578 }
4579 prop::PHY_TX_POWER => {
4580 self.device.settings.tx_power_dbm = clamp_tx_power(&self.config, value)?;
4581 Ok(true)
4582 }
4583 prop::PHY_LORA_BW => {
4584 self.device.settings.bw_hz = validate_bw_hz(value)?;
4585 Ok(true)
4586 }
4587 prop::PHY_LORA_SF => {
4588 self.device.settings.sf = validate_sf(value)?;
4589 Ok(true)
4590 }
4591 prop::PHY_LORA_CR => {
4592 self.device.settings.cr_denom = validate_cr(value)?;
4593 Ok(true)
4594 }
4595 prop::PHY_LORA_SW => {
4596 if parse_u16(value)? != self.config.sync_word {
4599 return Err(Status::INVALID_ARGUMENT);
4600 }
4601 Ok(false)
4602 }
4603 prop::PHY_DUTY_LIMIT => {
4604 self.config.duty.set_limit(parse_u16(value)?);
4605 Ok(false)
4606 }
4607 prop::MAC_PROMISCUOUS => {
4608 self.session.promiscuous = parse_bool(value)?;
4610 Ok(false)
4611 }
4612 prop::HOST_AUTO_ACK => {
4613 self.host.auto_ack = parse_bool(value)?;
4614 Ok(false)
4615 }
4616 prop::HOST_RX_FILTERS => {
4620 self.host.filters = FilterTable::parse_table(value)?;
4621 Ok(false)
4622 }
4623 prop::HOST_CHANNEL_KEYS => {
4626 self.require_secure_link()?;
4627 let mut table = ChannelKeyTable::default();
4628 for key in items::fixed_items::<{ items::CHANNEL_KEY_LEN }>(value)
4629 .map_err(|_| Status::INVALID_ARGUMENT)?
4630 {
4631 match table.insert(self.channel_entry(key)) {
4633 Ok(()) | Err(Status::ALREADY) => {}
4634 Err(status) => return Err(status),
4635 }
4636 }
4637 self.host.channel_keys = table;
4638 Ok(false)
4639 }
4640 prop::HOST_PEER_KEYS => {
4646 self.require_secure_link()?;
4647 let mut desired: HeaplessVec<items::PeerKeyEntry, MAX_PEER_KEYS> =
4648 HeaplessVec::new();
4649 for item in items::fixed_items::<{ items::PeerKeyEntry::WIRE_LEN }>(value)
4650 .map_err(|_| Status::INVALID_ARGUMENT)?
4651 {
4652 let entry =
4653 items::PeerKeyEntry::decode(item).map_err(|_| Status::INVALID_ARGUMENT)?;
4654 match desired
4656 .iter_mut()
4657 .find(|existing| existing.public_key == entry.public_key)
4658 {
4659 Some(existing) => *existing = entry,
4660 None => desired.push(entry).map_err(|_| Status::NOMEM)?,
4661 }
4662 }
4663 self.host.peer_keys.reconcile(&desired);
4664 Ok(false)
4665 }
4666 prop::DEV_CHANNEL_KEYS => {
4667 self.require_secure_link()?;
4668 let mut table = ChannelKeyTable::default();
4669 for key in items::fixed_items::<{ items::CHANNEL_KEY_LEN }>(value)
4670 .map_err(|_| Status::INVALID_ARGUMENT)?
4671 {
4672 match table.insert(self.channel_entry(key)) {
4673 Ok(()) | Err(Status::ALREADY) => {}
4674 Err(status) => return Err(status),
4675 }
4676 }
4677 self.device.channel_keys = table;
4678 self.bump_dev_domain();
4679 Ok(false)
4680 }
4681 prop::DEV_PEERS => {
4684 self.device.peers = DevPeerTable::parse_table(value)?;
4685 self.bump_dev_domain();
4686 Ok(false)
4687 }
4688 prop::DEV_ADMINS => {
4691 self.device.admins = DevAdminTable::parse_table(value)?;
4692 self.bump_dev_domain();
4693 Ok(false)
4694 }
4695 prop::MAC_REPEATER_ENABLED if self.config.mac_node => {
4701 self.device.repeater_enabled = parse_bool(value)?;
4702 self.bump_dev_domain();
4703 Ok(false)
4704 }
4705 prop::IDENT_ROLE => {
4711 self.device.ident_role = match value.len() {
4712 0 => None,
4713 1 => Some(value[0]),
4714 _ => return Err(Status::INVALID_ARGUMENT),
4715 };
4716 self.bump_dev_domain();
4717 Ok(false)
4718 }
4719 prop::IDENT_MOBILE => {
4720 self.device.ident_mobile = parse_bool(value)?;
4721 self.bump_dev_domain();
4722 Ok(false)
4723 }
4724 prop::IDENT_LOCATION => {
4729 if self.gnss_ident_update() {
4730 return Err(Status::INVALID_STATE);
4731 }
4732 self.device.ident_location = validate_ident_location(value)?;
4733 self.bump_dev_domain();
4734 Ok(false)
4735 }
4736 prop::IDENT_ALTITUDE => {
4737 if self.gnss_ident_update() {
4738 return Err(Status::INVALID_STATE);
4739 }
4740 self.device.ident_altitude_m = validate_ident_altitude(value)?;
4741 self.bump_dev_domain();
4742 Ok(false)
4743 }
4744 prop::DEV_DISCOVERABLE => {
4745 self.device.dev_discoverable = parse_bool(value)?;
4746 self.bump_dev_domain();
4747 Ok(false)
4748 }
4749 prop::ADVERT_INTERVAL => {
4753 self.device.advert_interval_s = validate_announce_interval(value)?;
4754 self.bump_dev_domain();
4755 Ok(false)
4756 }
4757 prop::BEACON_INTERVAL => {
4758 self.device.beacon_interval_s = validate_announce_interval(value)?;
4759 self.bump_dev_domain();
4760 Ok(false)
4761 }
4762 prop::STARTUP_BEACON => {
4763 self.device.startup_beacon = parse_bool(value)?;
4764 self.bump_dev_domain();
4765 Ok(false)
4766 }
4767 prop::MAC_REPEATER_REGIONS if self.config.mac_node => {
4773 self.device.repeater_regions = RepeaterRegions::parse_table(value)?;
4774 self.bump_dev_domain();
4775 Ok(false)
4776 }
4777 prop::MAC_REPEATER_DEFAULT_REGION if self.config.mac_node => {
4782 self.device.repeater_default_region = parse_region_code(value)?;
4783 self.bump_dev_domain();
4784 Ok(false)
4785 }
4786 prop::MAC_REPEATER_MIN_RSSI if self.config.mac_node => {
4787 self.device.repeater_min_rssi = match value.is_empty() {
4788 true => None,
4789 false => Some(parse_i16(value)?),
4790 };
4791 self.bump_dev_domain();
4792 Ok(false)
4793 }
4794 prop::MAC_REPEATER_MIN_SNR if self.config.mac_node => {
4795 self.device.repeater_min_snr = match value.is_empty() {
4796 true => None,
4797 false => Some(parse_i8(value)?),
4798 };
4799 self.bump_dev_domain();
4800 Ok(false)
4801 }
4802 prop::TZ_OFFSET if self.config.time.is_some() => {
4807 self.device.tz_offset_min = validate_tz_offset(value)?;
4808 self.bump_dev_domain();
4809 Ok(false)
4810 }
4811 prop::GNSS_ENABLED if self.config.gnss.is_some() => {
4812 self.device.gnss_enabled = parse_bool(value)?;
4813 self.bump_dev_domain();
4814 Ok(false)
4815 }
4816 prop::GNSS_IDENT_UPDATE if self.config.gnss.is_some() => {
4817 self.device.gnss_ident_update = parse_bool(value)?;
4818 self.bump_dev_domain();
4819 Ok(false)
4820 }
4821 prop::GNSS_IDENT_PRECISION if self.config.gnss.is_some() => {
4825 self.device.gnss_ident_precision = validate_ident_precision(value)?;
4826 self.bump_dev_domain();
4827 Ok(false)
4828 }
4829 prop::BLE_ENABLED if self.config.ble => {
4833 self.device.ble_enabled = parse_bool(value)?;
4834 self.bump_dev_domain();
4835 Ok(false)
4836 }
4837 prop::GNSS_TIME_TRUST if self.config.gnss.is_some() => {
4838 self.device.gnss_time_trust = parse_bool(value)?;
4839 self.bump_dev_domain();
4840 Ok(false)
4841 }
4842 prop::HOST_RX_QUEUE_CAPACITY => Err(Status::UNIMPLEMENTED),
4845 prop::LAST_STATUS
4848 | prop::PROTOCOL_VERSION
4849 | prop::DEV_VERSION
4850 | prop::DEV_MODEL
4851 | prop::INTERFACE_TYPE
4852 | prop::CAPS
4853 | prop::UPTIME
4854 | prop::PHY_RSSI
4855 | prop::PHY_MTU
4856 | prop::PHY_DUTY_NOW
4857 | prop::DEV_KEY
4858 | prop::HOST_RX_QUEUE_COUNT
4859 | prop::HOST_RX_QUEUE_DROPPED
4860 | prop::SAVED => Err(Status::INVALID_ARGUMENT),
4861 prop::BATTERY if self.config.battery.is_some() => Err(Status::INVALID_ARGUMENT),
4862 key if gnss::is_positioning_property(key) && self.config.gnss.is_some() => {
4869 Err(Status::INVALID_ARGUMENT)
4870 }
4871 prop::ILLUMINANCE if self.config.illuminance => Err(Status::INVALID_ARGUMENT),
4872 prop::BLE_BOND_COUNT if self.config.ble_pairing => Err(Status::INVALID_ARGUMENT),
4876 prop::BLE_LINK if self.config.ble => Err(Status::INVALID_ARGUMENT),
4880 _ => Err(Status::PROP_NOT_FOUND),
4881 }
4882 }
4883
4884 fn prop_insert(&mut self, tid: u8, key: u32, item: &[u8], emit: &mut impl FnMut(&[u8])) {
4887 if self.is_admin() && !admin_reachable(key) {
4888 return self.complete(tid, Status::PROP_NOT_FOUND, emit);
4889 }
4890 match key {
4891 prop::HOST_RX_FILTERS => {
4892 let filter = match decode_filter(item) {
4893 Ok(filter) => filter,
4894 Err(status) => return self.complete(tid, status, emit),
4895 };
4896 match self.host.filters.insert(filter) {
4897 Ok(()) => self.send_prop_inserted(tid, key, item, emit),
4898 Err(status) => self.complete(tid, status, emit),
4899 }
4900 }
4901 prop::MAC_REPEATER_REGIONS if self.config.mac_node => {
4903 let entry = match region_entry(item) {
4904 Ok(entry) => entry,
4905 Err(status) => return self.complete(tid, status, emit),
4906 };
4907 match self.device.repeater_regions.push(entry) {
4908 Ok(()) => {
4909 self.bump_dev_domain();
4910 self.send_prop_inserted(tid, key, item, emit)
4911 }
4912 Err(status) => self.complete(tid, status, emit),
4913 }
4914 }
4915 prop::HOST_CHANNEL_KEYS => {
4918 let result = self.require_secure_link().and_then(|()| {
4919 let key: &[u8; items::CHANNEL_KEY_LEN] =
4920 item.try_into().map_err(|_| Status::INVALID_ARGUMENT)?;
4921 let entry = self.channel_entry(key);
4922 self.host.channel_keys.insert(entry).map(|()| entry.id)
4923 });
4924 match result {
4925 Ok(id) => self.send_prop_inserted(tid, key, &id, emit),
4926 Err(status) => self.complete(tid, status, emit),
4927 }
4928 }
4929 prop::HOST_PEER_KEYS => {
4930 let result = self.require_secure_link().and_then(|()| {
4931 let entry =
4932 items::PeerKeyEntry::decode(item).map_err(|_| Status::INVALID_ARGUMENT)?;
4933 self.host.peer_keys.insert(entry).map(|()| entry.public_key)
4936 });
4937 match result {
4938 Ok(public_key) => self.send_prop_inserted(tid, key, &public_key, emit),
4939 Err(status) => self.complete(tid, status, emit),
4940 }
4941 }
4942 prop::DEV_CHANNEL_KEYS => {
4943 let result = self.require_secure_link().and_then(|()| {
4944 let key: &[u8; items::CHANNEL_KEY_LEN] =
4945 item.try_into().map_err(|_| Status::INVALID_ARGUMENT)?;
4946 let entry = self.channel_entry(key);
4947 self.device.channel_keys.insert(entry).map(|()| entry.id)
4948 });
4949 match result {
4950 Ok(id) => {
4951 self.bump_dev_domain();
4952 self.send_prop_inserted(tid, key, &id, emit);
4953 }
4954 Err(status) => self.complete(tid, status, emit),
4955 }
4956 }
4957 prop::DEV_PEERS => {
4958 let result = item
4959 .try_into()
4960 .map_err(|_| Status::INVALID_ARGUMENT)
4961 .and_then(|public_key: &[u8; items::PUBLIC_KEY_LEN]| {
4962 self.device.peers.insert(*public_key)
4963 });
4964 match result {
4965 Ok(()) => {
4966 self.bump_dev_domain();
4967 self.send_prop_inserted(tid, key, item, emit);
4968 }
4969 Err(status) => self.complete(tid, status, emit),
4970 }
4971 }
4972 prop::DEV_ADMINS => {
4973 let result = item
4974 .try_into()
4975 .map_err(|_| Status::INVALID_ARGUMENT)
4976 .and_then(|public_key: &[u8; items::PUBLIC_KEY_LEN]| {
4977 self.device.admins.insert(*public_key)
4978 });
4979 match result {
4980 Ok(()) => {
4981 self.bump_dev_domain();
4982 self.send_prop_inserted(tid, key, item, emit);
4983 }
4984 Err(status) => self.complete(tid, status, emit),
4985 }
4986 }
4987 _ if self.known_prop(key) => self.complete(tid, Status::INVALID_ARGUMENT, emit),
4990 _ => self.complete(tid, Status::PROP_NOT_FOUND, emit),
4991 }
4992 }
4993
4994 fn prop_remove(&mut self, tid: u8, key: u32, selector: &[u8], emit: &mut impl FnMut(&[u8])) {
4997 if self.is_admin() && !admin_reachable(key) {
4998 return self.complete(tid, Status::PROP_NOT_FOUND, emit);
4999 }
5000 match key {
5001 prop::HOST_RX_FILTERS => {
5002 let filter = match decode_filter(selector) {
5004 Ok(filter) => filter,
5005 Err(status) => return self.complete(tid, status, emit),
5006 };
5007 match self.host.filters.remove(filter) {
5008 Ok(()) => self.send_prop_removed(tid, key, selector, emit),
5009 Err(status) => self.complete(tid, status, emit),
5010 }
5011 }
5012 prop::MAC_REPEATER_REGIONS if self.config.mac_node => {
5016 if let Err(status) = region_entry(selector) {
5017 return self.complete(tid, status, emit);
5018 }
5019 match self.device.repeater_regions.remove(selector) {
5020 Ok(()) => {
5021 self.bump_dev_domain();
5022 self.send_prop_removed(tid, key, selector, emit)
5023 }
5024 Err(status) => self.complete(tid, status, emit),
5025 }
5026 }
5027 prop::HOST_CHANNEL_KEYS => {
5030 let result = selector
5031 .try_into()
5032 .map_err(|_| Status::INVALID_ARGUMENT)
5033 .and_then(|key: &[u8; items::CHANNEL_KEY_LEN]| {
5034 self.host.channel_keys.remove(key)
5035 });
5036 match result {
5037 Ok(id) => self.send_prop_removed(tid, key, &id, emit),
5038 Err(status) => self.complete(tid, status, emit),
5039 }
5040 }
5041 prop::HOST_PEER_KEYS => {
5044 let result = selector
5045 .try_into()
5046 .map_err(|_| Status::INVALID_ARGUMENT)
5047 .and_then(|public_key: &[u8; items::PUBLIC_KEY_LEN]| {
5048 self.host.peer_keys.remove(public_key)
5049 });
5050 match result {
5051 Ok(()) => self.send_prop_removed(tid, key, selector, emit),
5052 Err(status) => self.complete(tid, status, emit),
5053 }
5054 }
5055 prop::DEV_CHANNEL_KEYS => {
5056 let result = selector
5057 .try_into()
5058 .map_err(|_| Status::INVALID_ARGUMENT)
5059 .and_then(|key: &[u8; items::CHANNEL_KEY_LEN]| {
5060 self.device.channel_keys.remove(key)
5061 });
5062 match result {
5063 Ok(id) => {
5064 self.bump_dev_domain();
5065 self.send_prop_removed(tid, key, &id, emit);
5066 }
5067 Err(status) => self.complete(tid, status, emit),
5068 }
5069 }
5070 prop::DEV_PEERS => {
5071 let result = selector
5072 .try_into()
5073 .map_err(|_| Status::INVALID_ARGUMENT)
5074 .and_then(|public_key: &[u8; items::PUBLIC_KEY_LEN]| {
5075 self.device.peers.remove(public_key)
5076 });
5077 match result {
5078 Ok(()) => {
5079 self.bump_dev_domain();
5080 self.send_prop_removed(tid, key, selector, emit);
5081 }
5082 Err(status) => self.complete(tid, status, emit),
5083 }
5084 }
5085 prop::DEV_ADMINS => {
5086 let result = selector
5087 .try_into()
5088 .map_err(|_| Status::INVALID_ARGUMENT)
5089 .and_then(|public_key: &[u8; items::PUBLIC_KEY_LEN]| {
5090 self.device.admins.remove(public_key)
5091 });
5092 match result {
5093 Ok(()) => {
5094 self.bump_dev_domain();
5095 self.send_prop_removed(tid, key, selector, emit);
5096 }
5097 Err(status) => self.complete(tid, status, emit),
5098 }
5099 }
5100 _ if self.known_prop(key) => self.complete(tid, Status::INVALID_ARGUMENT, emit),
5101 _ => self.complete(tid, Status::PROP_NOT_FOUND, emit),
5102 }
5103 }
5104
5105 fn channel_entry(&self, key: &[u8; items::CHANNEL_KEY_LEN]) -> ChannelKeyEntry {
5108 ChannelKeyEntry {
5109 key: *key,
5110 id: self.engine.derive_channel_id(&ChannelKey(*key)).0,
5111 }
5112 }
5113
5114 fn require_secure_link(&self) -> Result<(), Status> {
5122 if self.link_secure || self.is_admin() {
5123 Ok(())
5124 } else {
5125 Err(Status::INVALID_STATE)
5126 }
5127 }
5128
5129 fn str_send(
5130 &mut self,
5131 tid: u8,
5132 payload: &StreamPayload<'_>,
5133 now_ms: u64,
5134 emit: &mut impl FnMut(&[u8]),
5135 ) -> Option<Effect> {
5136 if payload.stream != stream::PHY_RAW {
5137 self.complete(tid, Status::PROP_NOT_FOUND, emit);
5138 return None;
5139 }
5140 if !self.device.settings.enabled {
5141 self.complete(tid, Status::INVALID_STATE, emit);
5142 return None;
5143 }
5144 if payload.data.len() > usize::from(self.config.mtu) {
5145 self.complete(tid, Status::INVALID_ARGUMENT, emit);
5146 return None;
5147 }
5148 let Ok(tx_meta) = TxMeta::decode(payload.metadata) else {
5149 self.complete(tid, Status::PARSE_ERROR, emit);
5150 return None;
5151 };
5152 if self.session.pending.is_full() {
5153 self.complete(tid, Status::BUSY, emit);
5154 return None;
5155 }
5156
5157 let airtime_ms = if self.session.backhaul {
5163 0
5164 } else {
5165 lora_airtime_ms(
5166 self.device.settings.sf,
5167 self.device.settings.bw_hz,
5168 self.device.settings.cr_denom,
5169 payload.data.len(),
5170 )
5171 };
5172 let projected_airtime_ms = self
5173 .session
5174 .pending
5175 .iter()
5176 .fold(airtime_ms, |total, pending| {
5177 total.saturating_add(pending.airtime_ms)
5178 });
5179 if tx_meta.flags & meta::TX_FLAG_NODUTY == 0
5180 && self.config.duty.would_exceed(now_ms, projected_airtime_ms)
5181 {
5182 self.complete(tid, Status::DUTY_LIMIT, emit);
5183 return None;
5184 }
5185 let was_empty = self.session.pending.is_empty();
5186 let mut data = HeaplessVec::new();
5187 data.extend_from_slice(payload.data)
5189 .expect("payload bounded by MAX_MTU");
5190 let queued = self.session.pending.push_back(PendingTx {
5191 data,
5192 tid,
5193 airtime_ms,
5194 power: match tx_meta.power {
5198 meta::TX_POWER_DEFAULT => TxPower::Default,
5199 meta::TX_POWER_MAX => TxPower::Max,
5200 dbm => TxPower::Dbm(
5201 dbm.clamp(self.config.min_tx_power_dbm, self.config.max_tx_power_dbm),
5202 ),
5203 },
5204 autonomous: false,
5205 ack_for: None,
5206 nocca: tx_meta.flags & meta::TX_FLAG_NOCCA != 0,
5207 });
5208 debug_assert!(queued.is_ok(), "queue fullness checked above");
5209 self.host.note_tx_mic(payload.data);
5213 was_empty.then_some(Effect::StartTransmit)
5214 }
5215
5216 fn stats_counter(&self, key: u32) -> Option<(Counter, &'static StatsLedger)> {
5227 let counter = Counter::from_property(key)?;
5228 let ledger = self.config.stats?;
5229 (!counter.needs_node() || self.config.mac_node).then_some((counter, ledger))
5233 }
5234
5235 fn known_prop(&self, key: u32) -> bool {
5236 if key == prop::DEV_MODEL {
5237 return self.config.dev_model.is_some();
5238 }
5239 if key == prop::BATTERY {
5240 return self.config.battery.is_some();
5241 }
5242 if key == prop::ALERT {
5243 return self.config.alert.is_some();
5244 }
5245 if key == prop::ILLUMINANCE {
5246 return self.config.illuminance;
5247 }
5248 if key == prop::BLE_ENABLED {
5249 return self.config.ble;
5250 }
5251 if key == prop::BLE_BOND_COUNT {
5252 return self.config.ble_pairing;
5253 }
5254 if key == prop::BLE_LINK {
5255 return self.config.ble;
5256 }
5257 if key == prop::BLE_PAIRING {
5258 return self.config.ble_pairing;
5259 }
5260 if matches!(key, prop::TIME | prop::TZ_OFFSET) {
5261 return self.config.time.is_some();
5262 }
5263 if Counter::from_property(key).is_some() {
5264 return self.stats_counter(key).is_some();
5265 }
5266 if matches!(
5267 key,
5268 prop::MAC_REPEATER_ENABLED
5269 | prop::MAC_REPEATER_REGIONS
5270 | prop::MAC_REPEATER_DEFAULT_REGION
5271 | prop::MAC_REPEATER_MIN_RSSI
5272 | prop::MAC_REPEATER_MIN_SNR
5273 | prop::MAC_BACKHAUL
5274 ) {
5275 return self.config.mac_node;
5276 }
5277 if gnss::is_positioning_property(key)
5278 || matches!(
5279 key,
5280 prop::GNSS_ENABLED
5281 | prop::GNSS_IDENT_UPDATE
5282 | prop::GNSS_IDENT_PRECISION
5283 | prop::GNSS_TIME_TRUST
5284 )
5285 {
5286 return self.config.gnss.is_some();
5287 }
5288 matches!(
5289 key,
5290 prop::LAST_STATUS
5291 | prop::PROTOCOL_VERSION
5292 | prop::DEV_VERSION
5293 | prop::INTERFACE_TYPE
5294 | prop::CAPS
5295 | prop::UPTIME
5296 | prop::PHY_ENABLED
5297 | prop::PHY_FREQ
5298 | prop::PHY_TX_POWER
5299 | prop::PHY_RSSI
5300 | prop::PHY_LORA_BW
5301 | prop::PHY_LORA_SF
5302 | prop::PHY_LORA_CR
5303 | prop::PHY_MTU
5304 | prop::PHY_LORA_SW
5305 | prop::DEV_NAME
5306 | prop::DEV_KEY
5307 | prop::DEV_PRIVATE_KEY
5308 | prop::DEV_CHANNEL_KEYS
5309 | prop::DEV_PEERS
5310 | prop::DEV_ADMINS
5311 | prop::IDENT
5312 | prop::IDENT_ROLE
5313 | prop::IDENT_MOBILE
5314 | prop::IDENT_LOCATION
5315 | prop::IDENT_ALTITUDE
5316 | prop::DEV_DISCOVERABLE
5317 | prop::ADVERT_INTERVAL
5318 | prop::BEACON_INTERVAL
5319 | prop::STARTUP_BEACON
5320 | prop::PHY_DUTY_NOW
5321 | prop::PHY_DUTY_LIMIT
5322 | prop::BLE_PAIRING_PIN
5323 | prop::MAC_PROMISCUOUS
5324 | prop::SAVED
5325 | prop::HOST_KEY
5326 | prop::HOST_RX_FILTERS
5327 | prop::HOST_CHANNEL_KEYS
5328 | prop::HOST_PEER_KEYS
5329 | prop::HOST_AUTO_ACK
5330 | prop::HOST_RX_QUEUE_COUNT
5331 | prop::HOST_RX_QUEUE_CAPACITY
5332 | prop::HOST_RX_QUEUE_DROPPED
5333 )
5334 }
5335
5336 fn encode_prop(&mut self, key: u32, now_ms: u64, out: &mut [u8; PROP_BUF]) -> PropValue {
5337 if let Some((counter, ledger)) = self.stats_counter(key) {
5343 return PropValue::Encoded(put(out, &ledger.get(counter).to_le_bytes()));
5344 }
5345 let len = match key {
5346 prop::LAST_STATUS => pui::encode(self.last_status.0, out).unwrap_or(0),
5347 prop::PROTOCOL_VERSION => {
5348 out[0] = ids::PROTOCOL_MAJOR_VERSION;
5349 out[1] = ids::PROTOCOL_MINOR_VERSION;
5350 2
5351 }
5352 prop::DEV_VERSION => put_str(out, self.config.dev_version),
5353 prop::DEV_MODEL => put_str(out, self.config.dev_model.unwrap_or_default()),
5356 prop::INTERFACE_TYPE => pui::encode(ids::INTERFACE_TYPE, out).unwrap_or(0),
5357 prop::UPTIME => put(
5363 out,
5364 &u32::try_from(now_ms / 1000)
5365 .unwrap_or(u32::MAX)
5366 .to_le_bytes(),
5367 ),
5368 prop::CAPS => {
5369 let mut len = 0;
5370 let mac_node = self.config.mac_node;
5371 for (capability, advertised) in [
5372 (cap::WRITABLE_RAW_STREAM, true),
5373 (cap::PHY_DUTY_LIMIT, true),
5374 (cap::DEV_NAME, true),
5375 (cap::PHY_LORA, true),
5376 (cap::HOST_FILTER, true),
5377 (cap::HOST_RX_QUEUE, true),
5378 (cap::HOST_KEYS, true),
5379 (cap::HOST_AUTO_ACK, true),
5380 (cap::SAVE, true),
5381 (cap::DEV_IDENTITY, true),
5382 (cap::REPEATER, mac_node),
5383 (cap::IDENT, true),
5384 (cap::ADMIN, true),
5385 (cap::ADVERT, true),
5386 (cap::MAC_BACKHAUL, mac_node),
5387 (cap::CMD_MULTI, true),
5388 ] {
5389 if !advertised {
5390 continue;
5391 }
5392 len += pui::encode(capability, &mut out[len..]).unwrap_or(0);
5393 }
5394 if self.config.battery.is_some() {
5395 len += pui::encode(cap::BATTERY, &mut out[len..]).unwrap_or(0);
5396 }
5397 if self.config.alert.is_some() {
5398 len += pui::encode(cap::ALERT, &mut out[len..]).unwrap_or(0);
5399 }
5400 if self.config.time.is_some() {
5401 len += pui::encode(cap::TIME, &mut out[len..]).unwrap_or(0);
5402 }
5403 if self.config.gnss.is_some() {
5404 len += pui::encode(cap::GNSS, &mut out[len..]).unwrap_or(0);
5405 }
5406 if self.config.illuminance {
5407 len += pui::encode(cap::ILLUMINANCE, &mut out[len..]).unwrap_or(0);
5408 }
5409 if self.config.ble {
5410 len += pui::encode(cap::BLE, &mut out[len..]).unwrap_or(0);
5411 }
5412 if self.config.reboot {
5413 len += pui::encode(cap::REBOOT, &mut out[len..]).unwrap_or(0);
5414 }
5415 if self.config.stats.is_some() {
5416 len += pui::encode(cap::STATS, &mut out[len..]).unwrap_or(0);
5417 }
5418 len
5419 }
5420 prop::PHY_ENABLED => {
5421 out[0] = self.device.settings.enabled as u8;
5422 1
5423 }
5424 prop::PHY_FREQ => put(out, &self.device.settings.freq_khz.to_le_bytes()),
5425 prop::PHY_TX_POWER => {
5426 out[0] = self.device.settings.tx_power_dbm as u8;
5427 1
5428 }
5429 prop::PHY_RSSI => return PropValue::Unimplemented,
5430 prop::BATTERY if self.config.battery.is_some() => return PropValue::Unimplemented,
5433 prop::ILLUMINANCE if self.config.illuminance => return PropValue::Unimplemented,
5434 prop::PHY_LORA_BW => put(out, &self.device.settings.bw_hz.to_le_bytes()),
5435 prop::PHY_LORA_SF => {
5436 out[0] = self.device.settings.sf;
5437 1
5438 }
5439 prop::PHY_LORA_CR => {
5440 out[0] = self.device.settings.cr_denom;
5441 1
5442 }
5443 prop::PHY_MTU => put(out, &self.config.mtu.to_le_bytes()),
5444 prop::PHY_LORA_SW => put(out, &self.config.sync_word.to_le_bytes()),
5445 prop::DEV_NAME => put(out, &self.device.name[..self.device.name_len]),
5446 prop::DEV_KEY => match &self.dev_key {
5447 Some(key) => put(out, key),
5448 None => 0,
5449 },
5450 prop::DEV_CHANNEL_KEYS => {
5451 let mut len = 0;
5452 for entry in self.device.channel_keys.iter() {
5453 len += put(&mut out[len..], &entry.id);
5454 }
5455 len
5456 }
5457 prop::DEV_PEERS => {
5458 let mut len = 0;
5459 for public_key in self.device.peers.iter() {
5460 len += put(&mut out[len..], public_key);
5461 }
5462 len
5463 }
5464 prop::DEV_ADMINS => {
5465 let mut len = 0;
5466 for public_key in self.device.admins.iter() {
5467 len += put(&mut out[len..], public_key);
5468 }
5469 len
5470 }
5471 prop::MAC_REPEATER_ENABLED if self.config.mac_node => {
5472 out[0] = self.device.repeater_enabled as u8;
5473 1
5474 }
5475 prop::IDENT => return PropValue::Unimplemented,
5478 prop::IDENT_ROLE => match self.device.ident_role {
5479 Some(role) => {
5480 out[0] = role;
5481 1
5482 }
5483 None => 0,
5484 },
5485 prop::IDENT_MOBILE => {
5486 out[0] = self.device.ident_mobile as u8;
5487 1
5488 }
5489 prop::IDENT_LOCATION => put(out, &self.device.ident_location),
5490 prop::IDENT_ALTITUDE => match self.device.ident_altitude_m {
5493 Some(meters) => sint::encode(meters, out).unwrap_or(0),
5494 None => 0,
5495 },
5496 prop::DEV_DISCOVERABLE => {
5497 out[0] = self.device.dev_discoverable as u8;
5498 1
5499 }
5500 prop::ADVERT_INTERVAL => put(out, &self.device.advert_interval_s.to_le_bytes()),
5501 prop::BEACON_INTERVAL => put(out, &self.device.beacon_interval_s.to_le_bytes()),
5502 prop::STARTUP_BEACON => {
5503 out[0] = self.device.startup_beacon as u8;
5504 1
5505 }
5506 prop::ALERT if self.config.alert.is_some() => {
5507 pui::encode(self.alert.code(), out).unwrap_or(0)
5508 }
5509 prop::TIME if self.config.time.is_some() => return PropValue::Unimplemented,
5512 key if gnss::is_positioning_property(key) && self.config.gnss.is_some() => {
5513 return PropValue::Unimplemented;
5514 }
5515 prop::TZ_OFFSET if self.config.time.is_some() => {
5516 put(out, &self.device.tz_offset_min.to_le_bytes())
5517 }
5518 prop::GNSS_ENABLED if self.config.gnss.is_some() => {
5519 out[0] = self.device.gnss_enabled as u8;
5520 1
5521 }
5522 prop::GNSS_IDENT_UPDATE if self.config.gnss.is_some() => {
5523 out[0] = self.device.gnss_ident_update as u8;
5524 1
5525 }
5526 prop::GNSS_IDENT_PRECISION if self.config.gnss.is_some() => {
5527 out[0] = self.device.gnss_ident_precision;
5528 1
5529 }
5530 prop::GNSS_TIME_TRUST if self.config.gnss.is_some() => {
5531 out[0] = self.device.gnss_time_trust as u8;
5532 1
5533 }
5534 prop::BLE_ENABLED if self.config.ble => {
5535 out[0] = self.device.ble_enabled as u8;
5536 1
5537 }
5538 prop::BLE_BOND_COUNT if self.config.ble_pairing => {
5539 out[0] = self.ble_bond_count;
5540 1
5541 }
5542 prop::BLE_LINK if self.config.ble => {
5543 out[0] = self.ble_link.code();
5544 1
5545 }
5546 prop::BLE_PAIRING if self.config.ble_pairing => {
5547 out[0] = self.ble_pairing_open as u8;
5548 1
5549 }
5550 prop::MAC_REPEATER_REGIONS if self.config.mac_node => {
5551 let mut len = 0;
5554 for entry in self.device.repeater_regions.iter() {
5555 len += items::encode_prefixed_item(entry.text(), &mut out[len..])
5556 .expect("out sized for a full region table");
5557 }
5558 len
5559 }
5560 prop::MAC_REPEATER_DEFAULT_REGION if self.config.mac_node => {
5561 match &self.device.repeater_default_region {
5562 Some(code) => put(out, code),
5563 None => 0,
5564 }
5565 }
5566 prop::MAC_REPEATER_MIN_RSSI if self.config.mac_node => {
5567 match self.device.repeater_min_rssi {
5568 Some(rssi) => put(out, &rssi.to_le_bytes()),
5569 None => 0,
5570 }
5571 }
5572 prop::MAC_REPEATER_MIN_SNR if self.config.mac_node => {
5573 match self.device.repeater_min_snr {
5574 Some(snr) => {
5575 out[0] = snr as u8;
5576 1
5577 }
5578 None => 0,
5579 }
5580 }
5581 prop::PHY_DUTY_NOW => put(out, &self.config.duty.usage(now_ms).to_le_bytes()),
5582 prop::PHY_DUTY_LIMIT => put(out, &self.config.duty.limit().to_le_bytes()),
5583
5584 prop::MAC_PROMISCUOUS => {
5585 out[0] = self.session.promiscuous as u8;
5586 1
5587 }
5588 prop::MAC_BACKHAUL if self.config.mac_node => {
5589 out[0] = self.session.backhaul as u8;
5590 1
5591 }
5592 prop::SAVED => {
5593 out[0] = self.saved_status().as_octet();
5594 1
5595 }
5596 prop::HOST_KEY => match &self.host.key {
5597 Some(key) => put(out, key),
5598 None => 0,
5599 },
5600 prop::HOST_CHANNEL_KEYS => {
5604 let mut len = 0;
5605 for entry in self.host.channel_keys.iter() {
5606 len += put(&mut out[len..], &entry.id);
5607 }
5608 len
5609 }
5610 prop::HOST_PEER_KEYS => {
5611 let mut len = 0;
5612 for slot in self.host.peer_keys.iter() {
5613 len += put(&mut out[len..], &slot.entry.public_key);
5614 }
5615 len
5616 }
5617 prop::HOST_AUTO_ACK => {
5618 out[0] = self.host.auto_ack as u8;
5619 1
5620 }
5621 prop::HOST_RX_QUEUE_COUNT => put(out, &(self.host.queue.len as u16).to_le_bytes()),
5622 prop::HOST_RX_QUEUE_CAPACITY => put(out, &(RX_QUEUE_CAPACITY as u16).to_le_bytes()),
5623 prop::HOST_RX_QUEUE_DROPPED => put(out, &self.host.queue.dropped.to_le_bytes()),
5624 prop::HOST_RX_FILTERS => {
5625 let mut len = 0;
5628 for filter in self.host.filters.iter() {
5629 let mut item = [0u8; Filter::MAX_WIRE_LEN];
5630 let item_len = filter.encode(&mut item).expect("MAX_WIRE_LEN sized");
5631 len += items::encode_prefixed_item(&item[..item_len], &mut out[len..])
5632 .expect("out sized for a full filter table");
5633 }
5634 len
5635 }
5636 _ => return PropValue::Unknown,
5637 };
5638 PropValue::Encoded(len)
5639 }
5640
5641 fn send_prop_is(&mut self, tid: u8, key: u32, value: &[u8], emit: &mut impl FnMut(&[u8])) {
5647 if let Some(state) = self.multi.as_mut() {
5654 state.truncated |= !state.push_entry(key, value);
5655 return;
5656 }
5657 if self.suppress_response(tid) {
5658 return;
5659 }
5660 let mut buf = [0u8; PROP_BUF + 16];
5661 if let Ok(len) = frame::prop_is(&mut buf, tid, key, value) {
5662 emit(&buf[..len]);
5663 }
5664 }
5665
5666 fn suppress_response(&self, tid: u8) -> bool {
5673 tid == TID_UNSOLICITED && !self.is_admin()
5674 }
5675
5676 fn announce_prop_is(&mut self, key: u32, value: &[u8], emit: &mut impl FnMut(&[u8])) -> bool {
5684 let mut buf = [0u8; PROP_BUF + 16];
5685 match frame::prop_is(&mut buf, TID_UNSOLICITED, key, value) {
5686 Ok(len) => {
5687 emit(&buf[..len]);
5688 true
5689 }
5690 Err(_) => false,
5691 }
5692 }
5693
5694 fn send_prop_inserted(
5699 &mut self,
5700 tid: u8,
5701 key: u32,
5702 digest: &[u8],
5703 emit: &mut impl FnMut(&[u8]),
5704 ) {
5705 if self.suppress_response(tid) {
5706 return;
5707 }
5708 let mut buf = [0u8; PROP_BUF + 16];
5709 if let Ok(len) = frame::prop_inserted(&mut buf, tid, key, digest) {
5710 emit(&buf[..len]);
5711 }
5712 }
5713
5714 fn send_prop_removed(
5717 &mut self,
5718 tid: u8,
5719 key: u32,
5720 digest: &[u8],
5721 emit: &mut impl FnMut(&[u8]),
5722 ) {
5723 if self.suppress_response(tid) {
5724 return;
5725 }
5726 let mut buf = [0u8; PROP_BUF + 16];
5727 if let Ok(len) = frame::prop_removed(&mut buf, tid, key, digest) {
5728 emit(&buf[..len]);
5729 }
5730 }
5731
5732 fn send_status(&mut self, tid: u8, status: Status, emit: &mut impl FnMut(&[u8])) {
5735 self.last_status = status;
5736 let mut buf = [0u8; 16];
5737 if let Ok(len) = frame::last_status(&mut buf, tid, status) {
5738 emit(&buf[..len]);
5739 }
5740 }
5741
5742 fn complete(&mut self, tid: u8, status: Status, emit: &mut impl FnMut(&[u8])) {
5749 if let Some(state) = self.multi.as_mut() {
5753 self.last_status = status;
5754 state.failed |= status != Status::OK;
5755 state.truncated |= !state.push_status(status);
5756 return;
5757 }
5758 if self.suppress_response(tid) {
5759 self.last_status = status;
5760 } else {
5761 self.send_status(tid, status, emit);
5762 }
5763 }
5764
5765 fn announce_status(&mut self, status: Status, emit: &mut impl FnMut(&[u8])) {
5776 if self.is_admin() {
5777 self.last_status = status;
5778 return;
5779 }
5780 self.send_status(TID_UNSOLICITED, status, emit);
5781 }
5782}
5783
5784fn put(out: &mut [u8], bytes: &[u8]) -> usize {
5785 out[..bytes.len()].copy_from_slice(bytes);
5786 bytes.len()
5787}
5788
5789fn put_str(out: &mut [u8], value: &str) -> usize {
5794 let bytes = value.as_bytes();
5795 let len = bytes.len().min(out.len() - 1);
5796 out[..len].copy_from_slice(&bytes[..len]);
5797 out[len] = 0;
5798 len + 1
5799}
5800
5801fn parse_bool(value: &[u8]) -> Result<bool, Status> {
5802 match value {
5803 [0] => Ok(false),
5804 [1] => Ok(true),
5805 _ => Err(Status::INVALID_ARGUMENT),
5806 }
5807}
5808
5809fn parse_u8(value: &[u8]) -> Result<u8, Status> {
5810 match value {
5811 [byte] => Ok(*byte),
5812 _ => Err(Status::INVALID_ARGUMENT),
5813 }
5814}
5815
5816fn parse_i8(value: &[u8]) -> Result<i8, Status> {
5817 parse_u8(value).map(|byte| byte as i8)
5818}
5819
5820fn parse_i16(value: &[u8]) -> Result<i16, Status> {
5821 parse_u16(value).map(|half| half as i16)
5822}
5823
5824fn parse_u16(value: &[u8]) -> Result<u16, Status> {
5825 match value {
5826 [lo, hi] => Ok(u16::from_le_bytes([*lo, *hi])),
5827 _ => Err(Status::INVALID_ARGUMENT),
5828 }
5829}
5830
5831fn parse_u32(value: &[u8]) -> Result<u32, Status> {
5832 match value {
5833 [a, b, c, d] => Ok(u32::from_le_bytes([*a, *b, *c, *d])),
5834 _ => Err(Status::INVALID_ARGUMENT),
5835 }
5836}
5837
5838fn valid_device_name(value: &[u8]) -> bool {
5839 (1..=MAX_DEVICE_NAME_LEN).contains(&value.len())
5840 && !value.contains(&0)
5841 && core::str::from_utf8(value).is_ok()
5842}
5843
5844fn validate_freq_khz(config: &SessionConfig, value: &[u8]) -> Result<u32, Status> {
5859 let freq_khz = parse_u32(value)?;
5860 if !(config.freq_khz_min..=config.freq_khz_max).contains(&freq_khz) {
5861 return Err(Status::INVALID_ARGUMENT);
5862 }
5863 Ok(freq_khz)
5864}
5865
5866fn clamp_tx_power(config: &SessionConfig, value: &[u8]) -> Result<i8, Status> {
5872 Ok(parse_i8(value)?.clamp(config.min_tx_power_dbm, config.max_tx_power_dbm))
5873}
5874
5875fn validate_bw_hz(value: &[u8]) -> Result<u32, Status> {
5876 let bw_hz = parse_u32(value)?;
5877 if !umsh_ulcp::profiles::SUPPORTED_BANDWIDTHS_HZ.contains(&bw_hz) {
5878 return Err(Status::INVALID_ARGUMENT);
5879 }
5880 Ok(bw_hz)
5881}
5882
5883fn validate_sf(value: &[u8]) -> Result<u8, Status> {
5884 let sf = parse_u8(value)?;
5885 if !(5..=12).contains(&sf) {
5886 return Err(Status::INVALID_ARGUMENT);
5887 }
5888 Ok(sf)
5889}
5890
5891fn validate_cr(value: &[u8]) -> Result<u8, Status> {
5892 let cr = parse_u8(value)?;
5893 if !(5..=8).contains(&cr) {
5894 return Err(Status::INVALID_ARGUMENT);
5895 }
5896 Ok(cr)
5897}
5898
5899fn validate_tz_offset(value: &[u8]) -> Result<i16, Status> {
5906 let minutes = parse_i16(value)?;
5907 if !(-12 * 60..=14 * 60).contains(&minutes) {
5908 return Err(Status::INVALID_ARGUMENT);
5909 }
5910 Ok(minutes)
5911}
5912
5913fn validate_ident_precision(value: &[u8]) -> Result<u8, Status> {
5920 let precision = parse_u8(value)?;
5921 if !(1..=MAX_IDENT_PRECISION).contains(&precision) {
5922 return Err(Status::INVALID_ARGUMENT);
5923 }
5924 Ok(precision)
5925}
5926
5927fn validate_ident_location(
5933 value: &[u8],
5934) -> Result<HeaplessVec<u8, { gnss::MAX_LOCATION_LEN }>, Status> {
5935 HeaplessVec::from_slice(value).map_err(|_| Status::INVALID_ARGUMENT)
5936}
5937
5938fn validate_ident_altitude(value: &[u8]) -> Result<Option<i32>, Status> {
5944 match value {
5945 [] => Ok(None),
5946 bytes => sint::decode(bytes)
5947 .map(Some)
5948 .map_err(|_| Status::INVALID_ARGUMENT),
5949 }
5950}
5951
5952fn validate_announce_interval(value: &[u8]) -> Result<u32, Status> {
5961 let seconds = parse_u32(value)?;
5962 if seconds != 0
5963 && !(MIN_AUTO_ANNOUNCE_INTERVAL_S..=MAX_AUTO_ANNOUNCE_INTERVAL_S).contains(&seconds)
5964 {
5965 return Err(Status::INVALID_ARGUMENT);
5966 }
5967 Ok(seconds)
5968}
5969
5970#[cfg(test)]
5971mod tests {
5972 use super::*;
5973 use umsh_crypto::software::{SoftwareAes, SoftwareSha256};
5974
5975 type TestSession = Session<SoftwareAes, SoftwareSha256>;
5976
5977 fn test_engine() -> CryptoEngine<SoftwareAes, SoftwareSha256> {
5978 CryptoEngine::new(SoftwareAes, SoftwareSha256)
5979 }
5980
5981 fn test_session() -> TestSession {
5985 let mut session = test_session_with_boot_status(Status::RESET_POWER_ON);
5986 session.attach(true);
5987 session
5988 }
5989
5990 fn test_session_with_boot_status(boot_status: Status) -> TestSession {
5991 let mut session = Session::new(test_config(), boot_status, test_engine());
5992 session.attach(true);
5993 session
5994 }
5995
5996 fn test_config() -> SessionConfig {
5997 SessionConfig {
5998 dev_version: "test-dev/0.1",
5999 dev_model: Some("Test Board"),
6000 default_device_name: "Test UMSH Device",
6001 mtu: 255,
6002 sync_word: 0x1424,
6003 min_tx_power_dbm: -9,
6004 max_tx_power_dbm: 22,
6005 freq_khz_min: 150_000,
6006 freq_khz_max: 960_000,
6007 defaults: RadioSettings {
6008 enabled: false,
6009 freq_khz: 910_525,
6010 bw_hz: 62_500,
6011 sf: 7,
6012 cr_denom: 5,
6013 tx_power_dbm: 14,
6014 },
6015 default_duty_limit: 0xFFFF,
6016 duty: Box::leak(Box::new(DutyLedger::new())),
6019 stats: Some(Box::leak(Box::new(StatsLedger::new()))),
6021 battery: Some(BatteryFields {
6024 voltage: true,
6025 level: false,
6026 charge_state: true,
6027 }),
6028 alert: Some(AlertConfig::DEFAULT),
6029 time: Some(TimeConfig),
6030 gnss: Some(GnssConfig::DEFAULT),
6031 illuminance: true,
6032 ble: true,
6033 ble_pairing: true,
6034 reboot: true,
6035 mac_node: true,
6036 }
6037 }
6038
6039 fn timeless_config() -> SessionConfig {
6042 SessionConfig {
6043 time: None,
6044 gnss: None,
6045 ..test_config()
6046 }
6047 }
6048
6049 fn dispatch<const TX: usize>(
6051 session: &mut Session<SoftwareAes, SoftwareSha256, TX>,
6052 request: &[u8],
6053 now_ms: u64,
6054 ) -> (Vec<Vec<u8>>, Option<Effect>) {
6055 let mut emitted = Vec::new();
6056 let effect = session.handle_frame(request, now_ms, &mut |bytes: &[u8]| {
6057 emitted.push(bytes.to_vec())
6058 });
6059 (emitted, effect)
6060 }
6061
6062 fn parse_prop_is(bytes: &[u8]) -> (u8, u32, Vec<u8>) {
6064 let parsed = Frame::parse(bytes).unwrap();
6065 assert_eq!(parsed.command(), Some(Cmd::PropIs));
6066 let payload = PropPayload::parse(parsed.payload).unwrap();
6067 (parsed.header.tid(), payload.key, payload.value.to_vec())
6068 }
6069
6070 fn expect_status(bytes: &[u8], tid: u8, status: Status) {
6071 let (response_tid, key, value) = parse_prop_is(bytes);
6072 assert_eq!(response_tid, tid);
6073 assert_eq!(key, prop::LAST_STATUS);
6074 assert_eq!(pui::decode(&value).unwrap().0, status.0);
6075 }
6076
6077 fn get(session: &mut TestSession, key: u32) -> Vec<u8> {
6078 let mut buf = [0u8; 16];
6079 let len = frame::prop_get(&mut buf, 1, key).unwrap();
6080 let (emitted, effect) = dispatch(session, &buf[..len], 0);
6081 assert!(effect.is_none());
6082 let (_, response_key, value) = parse_prop_is(&emitted[0]);
6083 assert_eq!(response_key, key);
6084 value
6085 }
6086
6087 fn capabilities(session: &mut TestSession) -> Vec<u32> {
6089 let raw = get(session, prop::CAPS);
6090 let mut caps = Vec::new();
6091 let mut offset = 0;
6092 while offset < raw.len() {
6093 let (value, used) = pui::decode(&raw[offset..]).unwrap();
6094 caps.push(value);
6095 offset += used;
6096 }
6097 caps
6098 }
6099
6100 fn set(session: &mut TestSession, key: u32, value: &[u8]) -> (Vec<Vec<u8>>, Option<Effect>) {
6101 let mut buf = [0u8; 1024];
6102 let len = frame::prop_set(&mut buf, 2, key, value).unwrap();
6103 dispatch(session, &buf[..len], 0)
6104 }
6105
6106 fn region_table(regions: &[&str]) -> Vec<u8> {
6109 region_table_raw(
6110 ®ions
6111 .iter()
6112 .map(|text| text.as_bytes())
6113 .collect::<Vec<_>>(),
6114 )
6115 }
6116
6117 fn region_table_raw(items: &[&[u8]]) -> Vec<u8> {
6119 let mut table = Vec::new();
6120 for item in items {
6121 let mut encoded = [0u8; 64];
6122 let len = items::encode_prefixed_item(item, &mut encoded).unwrap();
6123 table.extend_from_slice(&encoded[..len]);
6124 }
6125 table
6126 }
6127
6128 fn region_codes(session: &TestSession) -> Vec<[u8; REGION_CODE_LEN]> {
6129 session.repeater_region_codes().collect()
6130 }
6131
6132 fn region_names(session: &TestSession) -> Vec<String> {
6133 session.repeater_region_names().map(str::to_owned).collect()
6134 }
6135
6136 fn send_packet<const TX: usize>(
6137 session: &mut Session<SoftwareAes, SoftwareSha256, TX>,
6138 tid: u8,
6139 data: &[u8],
6140 meta: &[u8],
6141 now_ms: u64,
6142 ) -> (Vec<Vec<u8>>, Option<Effect>) {
6143 let mut buf = [0u8; 320];
6144 let len = frame::str_send(&mut buf, tid, stream::PHY_RAW, data, meta).unwrap();
6145 dispatch(session, &buf[..len], now_ms)
6146 }
6147
6148 fn enable(session: &mut TestSession) {
6149 let (_, effect) = set(session, prop::PHY_ENABLED, &[1]);
6150 assert!(matches!(effect, Some(Effect::ApplyRadio(settings)) if settings.enabled));
6151 }
6152
6153 fn multi(
6157 session: &mut TestSession,
6158 request: &[u8],
6159 now_ms: u64,
6160 ) -> (u8, Vec<(u32, Vec<u8>)>, Vec<Effect>) {
6161 multi_budgeted(session, request, now_ms, MULTI_MAX)
6162 }
6163
6164 fn multi_budgeted(
6165 session: &mut TestSession,
6166 request: &[u8],
6167 now_ms: u64,
6168 reply_budget: usize,
6169 ) -> (u8, Vec<(u32, Vec<u8>)>, Vec<Effect>) {
6170 let mut emitted = Vec::new();
6171 let mut served = Vec::new();
6172 let mut pending =
6173 session.handle_frame_budgeted(request, now_ms, reply_budget, &mut |bytes: &[u8]| {
6174 emitted.push(bytes.to_vec())
6175 });
6176 while let Some(effect) = pending.take() {
6177 match effect {
6179 Effect::SampleBattery { tid } => session.respond_battery(
6180 tid,
6181 Ok(BatteryStatus::default()),
6182 &mut |bytes: &[u8]| emitted.push(bytes.to_vec()),
6183 ),
6184 Effect::ReadTime { tid } => {
6185 session.respond_time(tid, Some(1_700_000_000), &mut |bytes: &[u8]| {
6186 emitted.push(bytes.to_vec())
6187 })
6188 }
6189 Effect::SampleIlluminance { tid } => {
6190 session.respond_illuminance(tid, Some(1234), &mut |bytes: &[u8]| {
6191 emitted.push(bytes.to_vec())
6192 })
6193 }
6194 other => served.push(other),
6195 }
6196 pending =
6197 session.resume_multi(now_ms, &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
6198 }
6199 assert_eq!(emitted.len(), 1, "a multi command answers with one frame");
6200 let parsed = Frame::parse(&emitted[0]).unwrap();
6201 assert_eq!(parsed.command(), Some(Cmd::PropAre));
6202 let entries = MultiEntries::new(parsed.payload)
6203 .map(|entry| entry.unwrap())
6204 .map(|entry| (entry.key, entry.value.to_vec()))
6205 .collect();
6206 (parsed.header.tid(), entries, served)
6207 }
6208
6209 fn entry_status(value: &[u8]) -> Status {
6210 Status(pui::decode(value).unwrap().0)
6211 }
6212
6213 #[test]
6214 fn multi_get_answers_every_slot_in_order() {
6215 let mut session = test_session();
6216 let mut buf = [0u8; 64];
6217 let len = frame::prop_multi_get(
6220 &mut buf,
6221 3,
6222 &[
6223 prop::PHY_TX_POWER,
6224 60_000,
6225 prop::DEV_PRIVATE_KEY,
6226 prop::PHY_LORA_SF,
6227 ],
6228 )
6229 .unwrap();
6230 let (tid, entries, _) = multi(&mut session, &buf[..len], 0);
6231
6232 assert_eq!(tid, 3);
6233 assert_eq!(entries.len(), 4);
6234 assert_eq!(entries[0], (prop::PHY_TX_POWER, vec![14]));
6235 assert_eq!(entries[1].0, prop::LAST_STATUS);
6236 assert_eq!(entry_status(&entries[1].1), Status::PROP_NOT_FOUND);
6237 assert_eq!(entries[2].0, prop::LAST_STATUS);
6238 assert_eq!(entry_status(&entries[2].1), Status::UNIMPLEMENTED);
6239 assert_eq!(entries[3], (prop::PHY_LORA_SF, vec![7]));
6240 }
6241
6242 #[test]
6246 fn multi_get_completes_deferred_values_in_place() {
6247 let mut session = test_session();
6248 let mut buf = [0u8; 64];
6249 let len = frame::prop_multi_get(
6250 &mut buf,
6251 1,
6252 &[prop::DEV_NAME, prop::TIME, prop::ILLUMINANCE, prop::PHY_MTU],
6253 )
6254 .unwrap();
6255 let (_, entries, _) = multi(&mut session, &buf[..len], 0);
6256
6257 assert_eq!(entries.len(), 4);
6258 assert_eq!(entries[0].0, prop::DEV_NAME);
6259 assert_eq!(entries[1].0, prop::TIME);
6260 assert_eq!(entries[1].1, 1_700_000_000u32.to_le_bytes());
6261 assert_eq!(entries[2].0, prop::ILLUMINANCE);
6262 assert_eq!(entries[3].0, prop::PHY_MTU);
6263 }
6264
6265 #[test]
6266 fn multi_set_applies_in_order_and_echoes_values() {
6267 let mut session = test_session();
6268 let mut buf = [0u8; 128];
6269 let entries: [(u32, &[u8]); 2] = [(prop::PHY_TX_POWER, &[20]), (prop::PHY_LORA_SF, &[9])];
6270 let len = frame::prop_multi_set(&mut buf, 4, &entries).unwrap();
6271 let (tid, reply, _) = multi(&mut session, &buf[..len], 0);
6272
6273 assert_eq!(tid, 4);
6274 assert_eq!(reply.len(), 2);
6275 assert_eq!(reply[0], (prop::PHY_TX_POWER, vec![20]));
6276 assert_eq!(reply[1], (prop::PHY_LORA_SF, vec![9]));
6277 assert_eq!(get(&mut session, prop::PHY_TX_POWER), vec![20]);
6278 assert_eq!(get(&mut session, prop::PHY_LORA_SF), vec![9]);
6279 }
6280
6281 #[test]
6282 fn multi_set_stops_at_the_first_failure() {
6283 let mut session = test_session();
6284 let mut buf = [0u8; 128];
6285 let entries: [(u32, &[u8]); 3] = [
6288 (prop::PHY_TX_POWER, &[20]),
6289 (prop::PHY_LORA_SF, &[99]),
6290 (prop::PHY_LORA_CR, &[8]),
6291 ];
6292 let len = frame::prop_multi_set(&mut buf, 5, &entries).unwrap();
6293 let (_, reply, _) = multi(&mut session, &buf[..len], 0);
6294
6295 assert_eq!(reply.len(), 2);
6296 assert_eq!(reply[0], (prop::PHY_TX_POWER, vec![20]));
6297 assert_eq!(reply[1].0, prop::LAST_STATUS);
6298 assert_eq!(entry_status(&reply[1].1), Status::INVALID_ARGUMENT);
6299 assert_eq!(get(&mut session, prop::PHY_LORA_CR), vec![5]);
6300 }
6301
6302 #[test]
6305 fn multi_set_stops_before_an_entry_it_cannot_report() {
6306 let mut session = test_session();
6307 let mut buf = [0u8; 256];
6308 let name = [0x41u8; 40];
6309 let entries: [(u32, &[u8]); 3] = [
6310 (prop::DEV_NAME, &name),
6311 (prop::PHY_TX_POWER, &[17]),
6312 (prop::PHY_LORA_CR, &[8]),
6313 ];
6314 let len = frame::prop_multi_set(&mut buf, 6, &entries).unwrap();
6315 let budget = 2 + frame::entry_len(prop::DEV_NAME, name.len()).unwrap() + 1;
6317 let (_, reply, _) = multi_budgeted(&mut session, &buf[..len], 0, budget);
6318
6319 assert_eq!(reply.len(), 1);
6320 assert_eq!(reply[0].0, prop::DEV_NAME);
6321 assert_eq!(get(&mut session, prop::PHY_TX_POWER), vec![14]);
6324 assert_eq!(get(&mut session, prop::PHY_LORA_CR), vec![5]);
6325 }
6326
6327 #[test]
6331 fn multi_get_returns_what_fits() {
6332 let mut session = test_session();
6333 let mut buf = [0u8; 64];
6334 let keys = [prop::DEV_NAME, prop::PHY_TX_POWER, prop::PHY_LORA_SF];
6335 let len = frame::prop_multi_get(&mut buf, 7, &keys).unwrap();
6336 let full = multi(&mut session, &buf[..len], 0).1;
6337 assert_eq!(full.len(), 3);
6338
6339 let budget = 2 + frame::entry_len(prop::DEV_NAME, full[0].1.len()).unwrap() + 1;
6340 let (tid, short, _) = multi_budgeted(&mut session, &buf[..len], 0, budget);
6341 assert_eq!(tid, 7);
6342 assert_eq!(short.len(), 1);
6343 assert_eq!(short[0], full[0]);
6344 }
6345
6346 #[test]
6347 fn multi_commands_reject_malformed_and_oversized_requests() {
6348 let mut session = test_session();
6349
6350 let request = [0x81, Cmd::PropMultiGet as u8, 0x80];
6352 let (_, entries, _) = multi(&mut session, &request, 0);
6353 assert_eq!(entries.len(), 1);
6354 assert_eq!(entry_status(&entries[0].1), Status::PARSE_ERROR);
6355
6356 let mut buf = [0u8; 1024];
6359 let keys = [prop::PHY_TX_POWER; 400];
6360 let len = frame::prop_multi_get(&mut buf, 2, &keys).unwrap();
6361 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6362 assert!(effect.is_none());
6363 expect_status(&emitted[0], 2, Status::NOMEM);
6364 }
6365
6366 #[test]
6367 fn prop_are_from_the_host_is_an_invalid_command() {
6368 let mut session = test_session();
6369 let request = [0x83, Cmd::PropAre as u8];
6370 let (emitted, effect) = dispatch(&mut session, &request, 0);
6371 assert!(effect.is_none());
6372 expect_status(&emitted[0], 3, Status::INVALID_COMMAND);
6373 }
6374
6375 #[test]
6376 fn nop_replies_ok() {
6377 let mut session = test_session();
6378 let mut buf = [0u8; 4];
6379 let len = frame::nop(&mut buf, 3).unwrap();
6380 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6381 assert!(effect.is_none());
6382 expect_status(&emitted[0], 3, Status::OK);
6383 }
6384
6385 #[test]
6386 fn reset_returns_to_defaults() {
6387 let mut session = test_session();
6388 enable(&mut session);
6389 set(&mut session, prop::PHY_LORA_SF, &[12]);
6390
6391 let mut buf = [0u8; 4];
6392 let len = frame::reset(&mut buf, 0).unwrap();
6393 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6394 expect_status(&emitted[0], TID_UNSOLICITED, Status::RESET_SOFTWARE);
6395 let Some(Effect::ApplyRadio(settings)) = effect else {
6396 panic!("expected ApplyRadio, got {effect:?}");
6397 };
6398 assert!(!settings.enabled);
6399 assert_eq!(settings.sf, 7);
6400 assert_eq!(get(&mut session, prop::PHY_ENABLED), [0]);
6401 }
6402
6403 #[test]
6404 fn identity_properties() {
6405 let mut session = test_session();
6406 assert_eq!(get(&mut session, prop::PROTOCOL_VERSION), [6, 0]);
6407 assert_eq!(get(&mut session, prop::DEV_VERSION), b"test-dev/0.1\0");
6408 assert_eq!(get(&mut session, prop::DEV_MODEL), b"Test Board\0");
6409 assert_eq!(get(&mut session, prop::DEV_NAME), b"Test UMSH Device");
6410 assert_eq!(get(&mut session, prop::PHY_MTU), 255u16.to_le_bytes());
6411 assert_eq!(
6412 pui::decode(&get(&mut session, prop::INTERFACE_TYPE))
6413 .unwrap()
6414 .0,
6415 ids::INTERFACE_TYPE
6416 );
6417 assert_eq!(
6419 pui::decode(&get(&mut session, prop::LAST_STATUS))
6420 .unwrap()
6421 .0,
6422 Status::RESET_POWER_ON.0
6423 );
6424 }
6425
6426 #[test]
6427 fn caps_list_decodes() {
6428 let mut session = test_session();
6429 assert_eq!(
6430 capabilities(&mut session),
6431 [
6432 cap::WRITABLE_RAW_STREAM,
6433 cap::PHY_DUTY_LIMIT,
6434 cap::DEV_NAME,
6435 cap::PHY_LORA,
6436 cap::HOST_FILTER,
6437 cap::HOST_RX_QUEUE,
6438 cap::HOST_KEYS,
6439 cap::HOST_AUTO_ACK,
6440 cap::SAVE,
6441 cap::DEV_IDENTITY,
6442 cap::REPEATER,
6443 cap::IDENT,
6444 cap::ADMIN,
6445 cap::ADVERT,
6446 cap::MAC_BACKHAUL,
6447 cap::CMD_MULTI,
6448 cap::BATTERY,
6449 cap::ALERT,
6450 cap::TIME,
6451 cap::GNSS,
6452 cap::ILLUMINANCE,
6453 cap::BLE,
6454 cap::REBOOT,
6455 cap::STATS,
6456 ]
6457 );
6458 }
6459
6460 #[test]
6463 fn caps_omit_time_and_gnss_when_unconfigured() {
6464 let mut session: TestSession =
6465 Session::new(timeless_config(), Status::RESET_POWER_ON, test_engine());
6466 session.attach(true);
6467 let raw = get(&mut session, prop::CAPS);
6468 let mut caps = Vec::new();
6469 let mut offset = 0;
6470 while offset < raw.len() {
6471 let (value, used) = pui::decode(&raw[offset..]).unwrap();
6472 caps.push(value);
6473 offset += used;
6474 }
6475 assert!(!caps.contains(&cap::TIME));
6476 assert!(!caps.contains(&cap::GNSS));
6477
6478 for key in [
6479 prop::TIME,
6480 prop::TZ_OFFSET,
6481 prop::GNSS_ENABLED,
6482 prop::GNSS_LOCATION,
6483 prop::GNSS_ALTITUDE,
6484 prop::GNSS_FIX,
6485 prop::GNSS_PRECISION,
6486 prop::GNSS_SATELLITES,
6487 prop::GNSS_IDENT_UPDATE,
6488 prop::GNSS_IDENT_PRECISION,
6489 prop::GNSS_TIME_TRUST,
6490 ] {
6491 let mut buf = [0u8; 16];
6492 let len = frame::prop_get(&mut buf, 6, key).unwrap();
6493 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6494 assert_eq!(effect, None, "prop {key} produced an effect");
6495 let (_, status_key, value) = parse_prop_is(&emitted[0]);
6496 assert_eq!(status_key, prop::LAST_STATUS);
6497 assert_eq!(
6498 pui::decode(&value).unwrap().0,
6499 Status::PROP_NOT_FOUND.0,
6500 "prop {key} is visible without its capability"
6501 );
6502 }
6503 }
6504
6505 #[test]
6509 fn traffic_counters_report_the_ledger_and_clear_to_zero() {
6510 let stats: &'static StatsLedger = Box::leak(Box::new(StatsLedger::new()));
6511 let config = SessionConfig {
6512 stats: Some(stats),
6513 ..test_config()
6514 };
6515 let mut session: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
6516 session.attach(true);
6517
6518 assert!(capabilities(&mut session).contains(&cap::STATS));
6519
6520 stats.add(Counter::TxPackets, 7);
6521 stats.add(Counter::RxBadCrc, 3);
6522 assert_eq!(
6523 get(&mut session, prop::STAT_TX_PACKETS),
6524 7u32.to_le_bytes().to_vec()
6525 );
6526 assert_eq!(
6527 get(&mut session, prop::STAT_RX_BAD_CRC),
6528 3u32.to_le_bytes().to_vec()
6529 );
6530 assert_eq!(
6531 get(&mut session, prop::STAT_RX_PACKETS),
6532 0u32.to_le_bytes().to_vec()
6533 );
6534
6535 let (emitted, effect) = set(&mut session, prop::STAT_TX_PACKETS, &0u32.to_le_bytes());
6538 assert!(effect.is_none());
6539 let (_, key, value) = parse_prop_is(&emitted[0]);
6540 assert_eq!(key, prop::STAT_TX_PACKETS);
6541 assert_eq!(value, 0u32.to_le_bytes().to_vec());
6542 assert_eq!(stats.get(Counter::TxPackets), 0);
6543 assert_eq!(stats.get(Counter::RxBadCrc), 3);
6545
6546 stats.add(Counter::TxPackets, 2);
6548 assert_eq!(
6549 get(&mut session, prop::STAT_TX_PACKETS),
6550 2u32.to_le_bytes().to_vec()
6551 );
6552
6553 let (emitted, _) = set(&mut session, prop::STAT_TX_PACKETS, &1u32.to_le_bytes());
6554 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
6555 assert_eq!(
6556 stats.get(Counter::TxPackets),
6557 2,
6558 "a refused write cleared it"
6559 );
6560
6561 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_| {});
6565 session.attach(true);
6566 assert_eq!(
6567 get(&mut session, prop::STAT_TX_PACKETS),
6568 2u32.to_le_bytes().to_vec()
6569 );
6570 assert_eq!(
6571 get(&mut session, prop::STAT_RX_BAD_CRC),
6572 3u32.to_le_bytes().to_vec()
6573 );
6574 }
6575
6576 #[test]
6581 fn traffic_counters_follow_the_capability_and_the_node() {
6582 let mut countless: TestSession = Session::new(
6583 SessionConfig {
6584 stats: None,
6585 ..test_config()
6586 },
6587 Status::RESET_POWER_ON,
6588 test_engine(),
6589 );
6590 countless.attach(true);
6591 assert!(!capabilities(&mut countless).contains(&cap::STATS));
6592 for counter in Counter::ALL {
6593 let mut buf = [0u8; 16];
6594 let len = frame::prop_get(&mut buf, 1, counter.property()).unwrap();
6595 let (emitted, _) = dispatch(&mut countless, &buf[..len], 0);
6596 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
6597 }
6598
6599 let mut headless: TestSession = Session::new(
6600 SessionConfig {
6601 mac_node: false,
6602 ..test_config()
6603 },
6604 Status::RESET_POWER_ON,
6605 test_engine(),
6606 );
6607 headless.attach(true);
6608 assert!(capabilities(&mut headless).contains(&cap::STATS));
6609 for counter in Counter::ALL {
6610 let mut buf = [0u8; 16];
6611 let len = frame::prop_get(&mut buf, 1, counter.property()).unwrap();
6612 let (emitted, _) = dispatch(&mut headless, &buf[..len], 0);
6613 if counter.needs_node() {
6614 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
6615 } else {
6616 let (_, key, value) = parse_prop_is(&emitted[0]);
6617 assert_eq!(key, counter.property());
6618 assert_eq!(value, 0u32.to_le_bytes().to_vec());
6619 }
6620 }
6621 }
6622
6623 #[test]
6629 fn caps_omit_the_node_when_none_runs_behind_the_session() {
6630 let config = SessionConfig {
6631 mac_node: false,
6632 ..test_config()
6633 };
6634 let mut session: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
6635 session.attach(true);
6636 let caps = capabilities(&mut session);
6637 assert!(!caps.contains(&cap::REPEATER));
6638 assert!(!caps.contains(&cap::MAC_BACKHAUL));
6639
6640 for key in [
6641 prop::MAC_REPEATER_ENABLED,
6642 prop::MAC_REPEATER_REGIONS,
6643 prop::MAC_REPEATER_DEFAULT_REGION,
6644 prop::MAC_REPEATER_MIN_RSSI,
6645 prop::MAC_REPEATER_MIN_SNR,
6646 prop::MAC_BACKHAUL,
6647 ] {
6648 let mut buf = [0u8; 16];
6649 let len = frame::prop_get(&mut buf, 6, key).unwrap();
6650 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6651 assert_eq!(effect, None, "prop {key} produced an effect");
6652 let (_, status_key, value) = parse_prop_is(&emitted[0]);
6653 assert_eq!(status_key, prop::LAST_STATUS);
6654 assert_eq!(
6655 pui::decode(&value).unwrap().0,
6656 Status::PROP_NOT_FOUND.0,
6657 "prop {key} is visible without a node"
6658 );
6659
6660 let len = frame::prop_set(&mut buf, 7, key, &[1]).unwrap();
6661 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6662 assert_eq!(effect, None, "setting prop {key} produced an effect");
6663 let (_, status_key, value) = parse_prop_is(&emitted[0]);
6664 assert_eq!(status_key, prop::LAST_STATUS);
6665 assert_eq!(
6666 pui::decode(&value).unwrap().0,
6667 Status::PROP_NOT_FOUND.0,
6668 "prop {key} is settable without a node"
6669 );
6670 }
6671 }
6672
6673 #[test]
6678 fn reboot_defers_to_the_platform_or_says_it_cannot() {
6679 let mut buf = [0u8; 8];
6680 let len = frame::reboot(&mut buf, 3).unwrap();
6681
6682 let mut session = test_session();
6683 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6684 assert_eq!(effect, Some(Effect::Reboot));
6685 assert!(emitted.is_empty(), "a reboot answers nothing");
6686 assert!(capabilities(&mut session).contains(&cap::REBOOT));
6687
6688 let config = SessionConfig {
6689 reboot: false,
6690 ..test_config()
6691 };
6692 let mut fixed: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
6693 fixed.attach(true);
6694 let (emitted, effect) = dispatch(&mut fixed, &buf[..len], 0);
6695 assert_eq!(effect, None);
6696 let (_, status_key, value) = parse_prop_is(&emitted[0]);
6697 assert_eq!(status_key, prop::LAST_STATUS);
6698 assert_eq!(pui::decode(&value).unwrap().0, Status::UNIMPLEMENTED.0);
6699 assert!(!capabilities(&mut fixed).contains(&cap::REBOOT));
6700 }
6701
6702 #[test]
6706 fn bond_commands_defer_to_the_platform_or_say_they_cannot() {
6707 let mut buf = [0u8; 8];
6708 let clear = frame::ble_clear_bonds(&mut buf, 3).unwrap();
6709 let clear = buf[..clear].to_vec();
6710
6711 let mut session = test_session();
6712 let (emitted, effect) = dispatch(&mut session, &clear, 0);
6713 assert_eq!(effect, Some(Effect::BleClearBonds { tid: 3 }));
6714 assert!(emitted.is_empty(), "the answer waits for the platform");
6715
6716 let mut emitted = Vec::new();
6717 session.respond_ble_clear_bonds(3, Ok(()), &mut |frame| emitted.push(frame.to_vec()));
6718 let (_, key, value) = parse_prop_is(&emitted[0]);
6719 assert_eq!(key, prop::LAST_STATUS);
6720 assert_eq!(pui::decode(&value).unwrap().0, Status::OK.0);
6721
6722 let config = SessionConfig {
6723 ble_pairing: false,
6724 ..test_config()
6725 };
6726 let mut fixed: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
6727 fixed.attach(true);
6728 let (emitted, effect) = dispatch(&mut fixed, &clear, 0);
6729 assert_eq!(effect, None);
6730 let (_, key, value) = parse_prop_is(&emitted[0]);
6731 assert_eq!(key, prop::LAST_STATUS);
6732 assert_eq!(pui::decode(&value).unwrap().0, Status::UNIMPLEMENTED.0);
6733 assert!(capabilities(&mut fixed).contains(&cap::BLE));
6738 let mut buf = [0u8; 16];
6739 let len = frame::prop_get(&mut buf, 6, prop::BLE_BOND_COUNT).unwrap();
6740 let (emitted, _) = dispatch(&mut fixed, &buf[..len], 0);
6741 expect_status(&emitted[0], 6, Status::PROP_NOT_FOUND);
6742 }
6743
6744 #[test]
6748 fn the_pairing_window_is_a_toggle_the_transport_answers_for() {
6749 let mut session = test_session();
6750 assert_eq!(get(&mut session, prop::BLE_PAIRING), [0]);
6751
6752 let mut buf = [0u8; 16];
6755 let len = frame::prop_set(&mut buf, 4, prop::BLE_PAIRING, &[1]).unwrap();
6756 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6757 assert_eq!(effect, Some(Effect::SetBlePairing { tid: 4, open: true }));
6758 assert!(emitted.is_empty(), "the answer waits for the platform");
6759 let mut emitted = Vec::new();
6760 session.respond_ble_pairing(4, Ok(true), &mut |frame| emitted.push(frame.to_vec()));
6761 let (_, key, value) = parse_prop_is(&emitted[0]);
6762 assert_eq!(key, prop::BLE_PAIRING);
6763 assert_eq!(value, [1]);
6764 assert_eq!(get(&mut session, prop::BLE_PAIRING), [1]);
6765
6766 let len = frame::prop_set(&mut buf, 5, prop::BLE_PAIRING, &[1]).unwrap();
6769 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
6770 assert_eq!(effect, Some(Effect::SetBlePairing { tid: 5, open: true }));
6771 let mut emitted = Vec::new();
6772 session.respond_ble_pairing(5, Err(()), &mut |frame| emitted.push(frame.to_vec()));
6773 expect_status(&emitted[0], 5, Status::INVALID_STATE);
6774
6775 let mut announced = Vec::new();
6779 session.set_ble_pairing(false, &mut |frame| announced.push(frame.to_vec()));
6780 let (_, key, value) = parse_prop_is(&announced[0]);
6781 assert_eq!(key, prop::BLE_PAIRING);
6782 assert_eq!(value, [0]);
6783 assert_eq!(get(&mut session, prop::BLE_PAIRING), [0]);
6784
6785 let config = SessionConfig {
6788 ble_pairing: false,
6789 ..test_config()
6790 };
6791 let mut fixed: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
6792 fixed.attach(true);
6793 let len = frame::prop_set(&mut buf, 6, prop::BLE_PAIRING, &[1]).unwrap();
6794 let (emitted, effect) = dispatch(&mut fixed, &buf[..len], 0);
6795 assert_eq!(effect, None);
6796 expect_status(&emitted[0], 6, Status::PROP_NOT_FOUND);
6797 }
6798
6799 #[test]
6804 fn bond_count_reports_the_transport_and_survives_a_reset() {
6805 let mut session = test_session();
6806 assert_eq!(get(&mut session, prop::BLE_BOND_COUNT), [0]);
6807 let mut announced = Vec::new();
6808 session.set_ble_bond_count(2, &mut |frame| announced.push(frame.to_vec()));
6809 assert_eq!(get(&mut session, prop::BLE_BOND_COUNT), [2]);
6810 let (_, key, value) = parse_prop_is(&announced[0]);
6811 assert_eq!(key, prop::BLE_BOND_COUNT);
6812 assert_eq!(value, [2]);
6813
6814 let mut buf = [0u8; 16];
6815 let len = frame::prop_set(&mut buf, 5, prop::BLE_BOND_COUNT, &[1]).unwrap();
6816 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
6817 let (_, key, value) = parse_prop_is(&emitted[0]);
6818 assert_eq!(key, prop::LAST_STATUS);
6819 assert_eq!(pui::decode(&value).unwrap().0, Status::INVALID_ARGUMENT.0);
6820
6821 session.reset(Status::RESET_OTHER, &mut |_| {});
6822 assert_eq!(get(&mut session, prop::BLE_BOND_COUNT), [2]);
6823 }
6824
6825 #[test]
6830 fn link_state_reports_the_transport_and_survives_a_reset() {
6831 let mut session = test_session();
6832 assert_eq!(
6833 get(&mut session, prop::BLE_LINK),
6834 [BleLinkState::None.code()]
6835 );
6836
6837 let mut announced = Vec::new();
6841 let mut emit = |frame: &[u8]| announced.push(frame.to_vec());
6842 session.set_ble_link(BleLinkState::Connected, &mut emit);
6843 session.set_ble_link(BleLinkState::Connected, &mut emit);
6844 session.set_ble_link(BleLinkState::Attached, &mut emit);
6845 assert_eq!(announced.len(), 2, "an unchanged state announces nothing");
6846 let (_, key, value) = parse_prop_is(&announced[1]);
6847 assert_eq!(key, prop::BLE_LINK);
6848 assert_eq!(value, [BleLinkState::Attached.code()]);
6849 assert!(session.ble_link().is_attached());
6850
6851 let mut buf = [0u8; 16];
6852 let len = frame::prop_set(&mut buf, 5, prop::BLE_LINK, &[0]).unwrap();
6853 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
6854 expect_status(&emitted[0], 5, Status::INVALID_ARGUMENT);
6855
6856 session.reset(Status::RESET_OTHER, &mut |_| {});
6857 assert_eq!(
6858 get(&mut session, prop::BLE_LINK),
6859 [BleLinkState::Attached.code()]
6860 );
6861 }
6862
6863 #[test]
6866 fn a_board_without_bluetooth_has_no_link_to_report() {
6867 let config = SessionConfig {
6868 ble: false,
6869 ble_pairing: false,
6870 ..test_config()
6871 };
6872 let mut session: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
6873 session.attach(true);
6874 assert!(!capabilities(&mut session).contains(&cap::BLE));
6875
6876 let mut buf = [0u8; 16];
6877 let len = frame::prop_get(&mut buf, 2, prop::BLE_LINK).unwrap();
6878 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
6879 expect_status(&emitted[0], 2, Status::PROP_NOT_FOUND);
6880
6881 let mut announced = Vec::new();
6884 session.set_ble_link(BleLinkState::Attached, &mut |frame| {
6885 announced.push(frame.to_vec())
6886 });
6887 assert!(announced.is_empty());
6888 assert_eq!(session.ble_link(), BleLinkState::None);
6889 }
6890
6891 #[test]
6895 fn time_reads_and_writes_defer_to_the_platform() {
6896 let mut session = test_session();
6897
6898 let mut buf = [0u8; 16];
6899 let len = frame::prop_get(&mut buf, 7, prop::TIME).unwrap();
6900 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6901 assert!(emitted.is_empty(), "no response until the clock is read");
6902 assert_eq!(effect, Some(Effect::ReadTime { tid: 7 }));
6903
6904 let mut out = Vec::new();
6905 session.respond_time(7, Some(1_780_000_000), &mut |bytes: &[u8]| {
6906 out.push(bytes.to_vec())
6907 });
6908 let (tid, key, value) = parse_prop_is(&out[0]);
6909 assert_eq!((tid, key), (7, prop::TIME));
6910 assert_eq!(value, 1_780_000_000u32.to_le_bytes());
6911
6912 let mut out = Vec::new();
6915 session.respond_time(4, None, &mut |bytes: &[u8]| out.push(bytes.to_vec()));
6916 let (_, _, value) = parse_prop_is(&out[0]);
6917 assert_eq!(value, Vec::<u8>::new());
6918
6919 let (_, effect) = set(&mut session, prop::TIME, &1_780_000_042u32.to_le_bytes());
6920 assert_eq!(
6921 effect,
6922 Some(Effect::ApplyTime {
6923 epoch: Some(1_780_000_042)
6924 })
6925 );
6926 let (_, effect) = set(&mut session, prop::TIME, &[]);
6929 assert_eq!(effect, Some(Effect::ApplyTime { epoch: None }));
6930 let (emitted, effect) = set(&mut session, prop::TIME, &[0, 0]);
6932 assert_eq!(effect, None);
6933 let (_, _, value) = parse_prop_is(&emitted[0]);
6934 assert_eq!(pui::decode(&value).unwrap().0, Status::INVALID_ARGUMENT.0);
6935 }
6936
6937 #[test]
6939 fn time_publishes_only_while_attached() {
6940 let mut session = test_session();
6941 let mut out = Vec::new();
6942 assert!(
6943 session.publish_time(Some(1_780_000_000), &mut |bytes: &[u8]| {
6944 out.push(bytes.to_vec())
6945 })
6946 );
6947 let (tid, key, value) = parse_prop_is(&out[0]);
6948 assert_eq!((tid, key), (TID_UNSOLICITED, prop::TIME));
6949 assert_eq!(value, 1_780_000_000u32.to_le_bytes());
6950
6951 session.detach();
6952 let mut out = Vec::new();
6953 assert!(
6954 !session.publish_time(Some(1_780_000_000), &mut |bytes: &[u8]| {
6955 out.push(bytes.to_vec())
6956 })
6957 );
6958 assert!(out.is_empty());
6959 }
6960
6961 #[test]
6964 fn timezone_is_always_known_and_bounded() {
6965 let mut session = test_session();
6966 assert_eq!(get(&mut session, prop::TZ_OFFSET), [0, 0]);
6967 assert_eq!(session.tz_offset_min(), 0);
6968
6969 set(&mut session, prop::TZ_OFFSET, &(-480i16).to_le_bytes());
6971 assert_eq!(get(&mut session, prop::TZ_OFFSET), (-480i16).to_le_bytes());
6972 assert_eq!(session.tz_offset_min(), -480);
6973
6974 for minutes in [-12 * 60i16, 14 * 60] {
6977 let (_, effect) = set(&mut session, prop::TZ_OFFSET, &minutes.to_le_bytes());
6978 assert_eq!(effect, None);
6979 assert_eq!(session.tz_offset_min(), minutes);
6980 }
6981 for minutes in [-12 * 60i16 - 1, 14 * 60 + 1] {
6982 let (emitted, _) = set(&mut session, prop::TZ_OFFSET, &minutes.to_le_bytes());
6983 let (_, _, value) = parse_prop_is(&emitted[0]);
6984 assert_eq!(pui::decode(&value).unwrap().0, Status::INVALID_ARGUMENT.0);
6985 }
6986 }
6987
6988 #[test]
6991 fn positioning_gets_sample_and_are_never_writable() {
6992 let mut session = test_session();
6993 let mut buf = [0u8; 16];
6994 let len = frame::prop_get(&mut buf, 5, prop::GNSS_LOCATION).unwrap();
6995 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
6996 assert!(emitted.is_empty());
6997 assert_eq!(
6998 effect,
6999 Some(Effect::SampleGnss {
7000 tid: 5,
7001 key: prop::GNSS_LOCATION
7002 })
7003 );
7004
7005 let mut snapshot = GnssSnapshot::SEARCHING;
7006 snapshot.fix = umsh_ulcp::gnss::FixKind::ThreeD;
7007 snapshot.altitude_m = Some(112);
7008 snapshot.accuracy_dm = Some(45);
7009 snapshot.sats_used = 8;
7010 snapshot.sats_in_view = Some(11);
7011 snapshot.set_location(&[0x8a, 0x1f, 0x4c, 0x00, 0xd3]);
7012 let mut out = Vec::new();
7013 session.respond_gnss(
7014 5,
7015 prop::GNSS_LOCATION,
7016 Ok(snapshot),
7017 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
7018 );
7019 let (tid, key, value) = parse_prop_is(&out[0]);
7020 assert_eq!((tid, key), (5, prop::GNSS_LOCATION));
7021 assert_eq!(value, [0x8a, 0x1f, 0x4c, 0x00, 0xd3]);
7022
7023 for key in [
7025 prop::GNSS_LOCATION,
7026 prop::GNSS_ALTITUDE,
7027 prop::GNSS_FIX,
7028 prop::GNSS_PRECISION,
7029 prop::GNSS_SATELLITES,
7030 ] {
7031 let (emitted, _) = set(&mut session, key, &[0]);
7032 let (_, _, value) = parse_prop_is(&emitted[0]);
7033 assert_eq!(
7034 pui::decode(&value).unwrap().0,
7035 Status::INVALID_ARGUMENT.0,
7036 "prop {key} accepted a write"
7037 );
7038 }
7039 }
7040
7041 #[test]
7044 fn a_searching_receiver_reports_zero_rather_than_nothing() {
7045 let mut session = test_session();
7046 for (key, expected) in [
7047 (prop::GNSS_FIX, vec![0u8]),
7048 (prop::GNSS_SATELLITES, vec![0u8]),
7049 (prop::GNSS_LOCATION, vec![]),
7050 (prop::GNSS_ALTITUDE, vec![]),
7051 (prop::GNSS_PRECISION, vec![]),
7052 ] {
7053 let mut out = Vec::new();
7054 session.respond_gnss(
7055 3,
7056 key,
7057 Ok(GnssSnapshot::SEARCHING),
7058 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
7059 );
7060 let (_, answered, value) = parse_prop_is(&out[0]);
7061 assert_eq!(answered, key);
7062 assert_eq!(value, expected, "prop {key}");
7063 }
7064 }
7065
7066 #[test]
7069 fn gnss_settings_round_trip_and_are_bounded() {
7070 let mut session = test_session();
7071 assert_eq!(get(&mut session, prop::GNSS_ENABLED), [0]);
7072 assert_eq!(get(&mut session, prop::GNSS_IDENT_UPDATE), [0]);
7073 assert_eq!(
7074 get(&mut session, prop::GNSS_IDENT_PRECISION),
7075 [DEFAULT_IDENT_PRECISION]
7076 );
7077 assert_eq!(get(&mut session, prop::GNSS_TIME_TRUST), [1]);
7078 assert!(!session.gnss_enabled());
7079 assert!(session.gnss_time_trust());
7080
7081 set(&mut session, prop::GNSS_ENABLED, &[1]);
7082 set(&mut session, prop::GNSS_IDENT_UPDATE, &[1]);
7083 set(&mut session, prop::GNSS_IDENT_PRECISION, &[3]);
7084 set(&mut session, prop::GNSS_TIME_TRUST, &[0]);
7085 assert!(session.gnss_enabled());
7086 assert!(session.gnss_ident_update());
7087 assert_eq!(session.gnss_ident_precision(), 3);
7088 assert!(!session.gnss_time_trust());
7089
7090 for precision in [0u8, MAX_IDENT_PRECISION + 1] {
7094 let (emitted, _) = set(&mut session, prop::GNSS_IDENT_PRECISION, &[precision]);
7095 let (_, _, value) = parse_prop_is(&emitted[0]);
7096 assert_eq!(pui::decode(&value).unwrap().0, Status::INVALID_ARGUMENT.0);
7097 }
7098 assert_eq!(session.gnss_ident_precision(), 3);
7099 }
7100
7101 #[test]
7110 fn a_board_can_boot_its_receiver_on() {
7111 let config = SessionConfig {
7112 gnss: Some(GnssConfig::ALWAYS_ON),
7113 ..test_config()
7114 };
7115 let always_on = || {
7116 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
7117 session.attach(true);
7118 session
7119 };
7120
7121 let mut session = always_on();
7122 assert_eq!(get(&mut session, prop::GNSS_ENABLED), [1]);
7123 assert!(session.gnss_enabled());
7124
7125 set(&mut session, prop::GNSS_ENABLED, &[0]);
7128 let mut buf = [0u8; 512];
7129 let len = session.encode_snapshot(&mut buf).expect("snapshot");
7130 let saved = SavedState::decode(&config, &buf[..len]).expect("decode");
7131 assert!(!saved.gnss_enabled);
7132
7133 let mut fresh = always_on();
7135 set(&mut fresh, prop::GNSS_ENABLED, &[0]);
7136 assert!(!fresh.gnss_enabled());
7137 fresh.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
7138 assert!(fresh.gnss_enabled(), "reset dropped the board default");
7139 }
7140
7141 #[test]
7144 fn a_local_toggle_flips_the_switch_and_announces_it() {
7145 let mut session = test_session();
7146 assert!(!session.gnss_enabled());
7147 let before = session.dev_domain_version();
7148
7149 let mut emitted = Vec::new();
7150 let enabled = session.toggle_gnss(&mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
7151 assert_eq!(enabled, Some(true));
7152 assert!(session.gnss_enabled());
7153 assert_eq!(get(&mut session, prop::GNSS_ENABLED), [1]);
7154 assert_ne!(
7155 session.dev_domain_version(),
7156 before,
7157 "the receiver never heard about it"
7158 );
7159 let (tid, key, value) = parse_prop_is(&emitted[0]);
7160 assert_eq!(
7161 (tid, key, value),
7162 (TID_UNSOLICITED, prop::GNSS_ENABLED, vec![1])
7163 );
7164
7165 emitted.clear();
7167 assert_eq!(
7168 session.toggle_gnss(&mut |bytes: &[u8]| emitted.push(bytes.to_vec())),
7169 Some(false)
7170 );
7171 assert!(!session.gnss_enabled());
7172 }
7173
7174 #[test]
7177 fn every_local_toggle_flips_its_own_property() {
7178 let cases: [(
7180 bool,
7181 fn(&mut TestSession, &mut Vec<Vec<u8>>) -> Option<bool>,
7182 u32,
7183 ); 3] = [
7184 (
7185 false,
7186 |session, emitted| {
7187 session.toggle_gnss_ident_update(&mut |b: &[u8]| emitted.push(b.to_vec()))
7188 },
7189 prop::GNSS_IDENT_UPDATE,
7190 ),
7191 (
7192 false,
7193 |session, emitted| {
7194 session.toggle_repeater(&mut |b: &[u8]| emitted.push(b.to_vec()))
7195 },
7196 prop::MAC_REPEATER_ENABLED,
7197 ),
7198 (
7199 true,
7200 |session, emitted| session.toggle_ble(&mut |b: &[u8]| emitted.push(b.to_vec())),
7201 prop::BLE_ENABLED,
7202 ),
7203 ];
7204
7205 for (start, toggle, key) in cases {
7206 let mut session = test_session();
7207 assert_eq!(get(&mut session, key), [start as u8], "prop {key} start");
7208 let before = session.dev_domain_version();
7209
7210 let mut emitted = Vec::new();
7211 assert_eq!(toggle(&mut session, &mut emitted), Some(!start));
7212 assert_eq!(get(&mut session, key), [!start as u8]);
7213 assert_ne!(
7214 session.dev_domain_version(),
7215 before,
7216 "prop {key} never reached the mirror"
7217 );
7218 assert_eq!(
7219 parse_prop_is(&emitted[0]),
7220 (TID_UNSOLICITED, key, vec![!start as u8]),
7221 "prop {key} was not announced"
7222 );
7223
7224 emitted.clear();
7226 assert_eq!(toggle(&mut session, &mut emitted), Some(start));
7227 }
7228 }
7229
7230 #[test]
7233 fn a_board_without_bluetooth_has_neither_the_property_nor_the_switch() {
7234 let config = SessionConfig {
7235 ble: false,
7236 ..test_config()
7237 };
7238 let mut session: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
7239 session.attach(true);
7240
7241 assert!(!session.ble_enabled());
7242 let mut emitted = Vec::new();
7243 assert_eq!(
7244 session.toggle_ble(&mut |bytes: &[u8]| emitted.push(bytes.to_vec())),
7245 None
7246 );
7247 assert!(emitted.is_empty());
7248
7249 let raw = get(&mut session, prop::CAPS);
7250 let mut offset = 0;
7251 while offset < raw.len() {
7252 let (value, used) = pui::decode(&raw[offset..]).unwrap();
7253 assert_ne!(value, cap::BLE);
7254 offset += used;
7255 }
7256
7257 let mut buf = [0u8; 16];
7258 let len = frame::prop_get(&mut buf, 6, prop::BLE_ENABLED).unwrap();
7259 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
7260 let (_, status_key, value) = parse_prop_is(&emitted[0]);
7261 assert_eq!(status_key, prop::LAST_STATUS);
7262 assert_eq!(pui::decode(&value).unwrap().0, Status::PROP_NOT_FOUND.0);
7263 }
7264
7265 #[test]
7268 fn a_local_toggle_without_the_capability_is_inert() {
7269 let mut session: TestSession =
7270 Session::new(timeless_config(), Status::RESET_POWER_ON, test_engine());
7271 let mut emitted = Vec::new();
7272 assert_eq!(
7273 session.toggle_gnss(&mut |bytes: &[u8]| emitted.push(bytes.to_vec())),
7274 None
7275 );
7276 assert!(emitted.is_empty());
7277 }
7278
7279 #[test]
7282 fn gnss_accessors_are_false_without_the_capability() {
7283 let session: TestSession =
7284 Session::new(timeless_config(), Status::RESET_POWER_ON, test_engine());
7285 assert!(!session.gnss_enabled());
7286 assert!(!session.gnss_ident_update());
7287 }
7288
7289 #[test]
7290 fn advertisement_policy_round_trips_and_survives_a_reboot() {
7291 let mut session = test_session();
7292 assert_eq!(
7293 get(&mut session, prop::ADVERT_INTERVAL),
7294 DEFAULT_ADVERT_INTERVAL_S.to_le_bytes()
7295 );
7296 assert_eq!(
7297 get(&mut session, prop::BEACON_INTERVAL),
7298 DEFAULT_BEACON_INTERVAL_S.to_le_bytes()
7299 );
7300 assert_eq!(get(&mut session, prop::STARTUP_BEACON), [1]);
7301
7302 let (emitted, effect) = set(&mut session, prop::ADVERT_INTERVAL, &7200u32.to_le_bytes());
7303 assert!(effect.is_none());
7304 let (_, key, value) = parse_prop_is(&emitted[0]);
7305 assert_eq!(key, prop::ADVERT_INTERVAL);
7306 assert_eq!(value, 7200u32.to_le_bytes());
7307 set(&mut session, prop::BEACON_INTERVAL, &0u32.to_le_bytes());
7309 set(&mut session, prop::STARTUP_BEACON, &[0]);
7310 assert_eq!(session.advert_interval_s(), 7200);
7311 assert_eq!(session.beacon_interval_s(), 0);
7312 assert!(!session.startup_beacon());
7313
7314 save(&mut session);
7315 let mut bytes = [0u8; SNAPSHOT_MAX];
7316 let len = session.encode_snapshot(&mut bytes).unwrap();
7317 let mut booted: TestSession =
7318 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
7319 booted.restore_at_boot(&bytes[..len]).unwrap();
7320 assert_eq!(booted.advert_interval_s(), 7200);
7321 assert_eq!(booted.beacon_interval_s(), 0);
7322 assert!(!booted.startup_beacon());
7323 }
7324
7325 #[test]
7329 fn announce_interval_holds_to_its_bounds_but_accepts_zero() {
7330 let mut session = test_session();
7331 for &key in &[prop::ADVERT_INTERVAL, prop::BEACON_INTERVAL] {
7332 for rejected in [
7333 MIN_AUTO_ANNOUNCE_INTERVAL_S - 1,
7334 MAX_AUTO_ANNOUNCE_INTERVAL_S + 1,
7335 u32::MAX,
7336 ] {
7337 let (emitted, _) = set(&mut session, key, &rejected.to_le_bytes());
7338 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7339 }
7340
7341 for accepted in [
7342 MIN_AUTO_ANNOUNCE_INTERVAL_S,
7343 MAX_AUTO_ANNOUNCE_INTERVAL_S,
7344 0,
7346 ] {
7347 let (emitted, _) = set(&mut session, key, &accepted.to_le_bytes());
7348 let (_, response_key, value) = parse_prop_is(&emitted[0]);
7349 assert_eq!(response_key, key);
7350 assert_eq!(value, accepted.to_le_bytes());
7351 }
7352 }
7353 }
7354
7355 #[test]
7360 fn a_snapshot_without_advertisement_options_restores_the_defaults() {
7361 let mut session = test_session();
7362 set(&mut session, prop::ADVERT_INTERVAL, &0u32.to_le_bytes());
7363 set(&mut session, prop::STARTUP_BEACON, &[0]);
7364 save(&mut session);
7365 let mut bytes = [0u8; SNAPSHOT_MAX];
7366 let len = session.encode_snapshot(&mut bytes).unwrap();
7367
7368 let stripped = strip_snapshot_options(
7371 &bytes[..len],
7372 &[
7373 prop::ADVERT_INTERVAL,
7374 prop::BEACON_INTERVAL,
7375 prop::STARTUP_BEACON,
7376 ],
7377 );
7378
7379 let mut booted: TestSession =
7380 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
7381 booted.restore_at_boot(&stripped).unwrap();
7382 assert_eq!(booted.advert_interval_s(), DEFAULT_ADVERT_INTERVAL_S);
7383 assert_eq!(booted.beacon_interval_s(), DEFAULT_BEACON_INTERVAL_S);
7384 assert!(booted.startup_beacon());
7385 }
7386
7387 #[test]
7392 fn identity_role_and_mobility_are_independent_of_forwarding() {
7393 let mut session = test_session();
7394 assert_eq!(get(&mut session, prop::IDENT_ROLE), Vec::<u8>::new());
7396 assert_eq!(get(&mut session, prop::IDENT_MOBILE), [0]);
7397
7398 set(&mut session, prop::MAC_REPEATER_ENABLED, &[1]);
7402 assert_eq!(get(&mut session, prop::IDENT_ROLE), Vec::<u8>::new());
7403
7404 let (emitted, effect) = set(&mut session, prop::IDENT_ROLE, &[1]);
7407 assert!(effect.is_none());
7408 let (_, key, value) = parse_prop_is(&emitted[0]);
7409 assert_eq!(key, prop::IDENT_ROLE);
7410 assert_eq!(value, [1]);
7411 set(&mut session, prop::IDENT_MOBILE, &[1]);
7412 assert_eq!(get(&mut session, prop::IDENT_ROLE), [1]);
7413 assert_eq!(get(&mut session, prop::IDENT_MOBILE), [1]);
7414 assert_eq!(get(&mut session, prop::MAC_REPEATER_ENABLED), [1]);
7415
7416 set(&mut session, prop::IDENT_ROLE, &[]);
7418 assert_eq!(get(&mut session, prop::IDENT_ROLE), Vec::<u8>::new());
7419
7420 set(&mut session, prop::IDENT_ROLE, &[3]);
7422 save(&mut session);
7423 let mut bytes = [0u8; SNAPSHOT_MAX];
7424 let len = session.encode_snapshot(&mut bytes).unwrap();
7425 let mut booted: TestSession =
7426 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
7427 booted.restore_at_boot(&bytes[..len]).unwrap();
7428 assert_eq!(booted.ident_role(), Some(3));
7429 assert!(booted.ident_mobile());
7430 booted.attach(true);
7431 assert_eq!(get(&mut booted, prop::IDENT_ROLE), [3]);
7432 assert_eq!(get(&mut booted, prop::IDENT_MOBILE), [1]);
7433
7434 let (emitted, _) = set(&mut booted, prop::IDENT_ROLE, &[1, 2]);
7436 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7437 let (emitted, _) = set(&mut booted, prop::IDENT_MOBILE, &[2]);
7438 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7439 }
7440
7441 #[test]
7444 fn advertised_position_is_written_and_saved() {
7445 let mut session = test_session();
7446 assert_eq!(get(&mut session, prop::IDENT_LOCATION), Vec::<u8>::new());
7447 assert_eq!(get(&mut session, prop::IDENT_ALTITUDE), Vec::<u8>::new());
7448
7449 set(&mut session, prop::IDENT_LOCATION, &[1, 2, 3, 4, 5]);
7450 set(&mut session, prop::IDENT_ALTITUDE, &[100]);
7451 assert_eq!(get(&mut session, prop::IDENT_LOCATION), [1, 2, 3, 4, 5]);
7452 assert_eq!(get(&mut session, prop::IDENT_ALTITUDE), [100]);
7453
7454 set(&mut session, prop::IDENT_ALTITUDE, &[100, 0, 0, 0]);
7456 assert_eq!(get(&mut session, prop::IDENT_ALTITUDE), [100]);
7457 set(&mut session, prop::IDENT_ALTITUDE, &[0x9C, 0xFF]);
7459 assert_eq!(session.ident_altitude_m(), Some(-100));
7460 assert_eq!(get(&mut session, prop::IDENT_ALTITUDE), [0x9C]);
7461 set(&mut session, prop::IDENT_ALTITUDE, &[0xC8, 0x00]);
7463 assert_eq!(get(&mut session, prop::IDENT_ALTITUDE), [0xC8, 0x00]);
7464
7465 let (emitted, _) = set(&mut session, prop::IDENT_ALTITUDE, &[0; 5]);
7467 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7468 let (emitted, _) = set(&mut session, prop::IDENT_LOCATION, &[0; 8]);
7469 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7470
7471 save(&mut session);
7472 let mut bytes = [0u8; SNAPSHOT_MAX];
7473 let len = session.encode_snapshot(&mut bytes).unwrap();
7474 let mut booted: TestSession =
7475 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
7476 booted.restore_at_boot(&bytes[..len]).unwrap();
7477 assert_eq!(booted.ident_location(), [1, 2, 3, 4, 5]);
7478 assert_eq!(booted.ident_altitude_m(), Some(200));
7479
7480 booted.attach(true);
7482 set(&mut booted, prop::IDENT_LOCATION, &[]);
7483 assert_eq!(get(&mut booted, prop::IDENT_LOCATION), Vec::<u8>::new());
7484 }
7485
7486 #[test]
7490 fn a_board_without_a_receiver_still_advertises_a_position() {
7491 let mut session: TestSession =
7492 Session::new(timeless_config(), Status::RESET_POWER_ON, test_engine());
7493 session.attach(true);
7494 set(&mut session, prop::IDENT_LOCATION, &[9, 8, 7]);
7495 assert_eq!(get(&mut session, prop::IDENT_LOCATION), [9, 8, 7]);
7496 let mut buf = [0u8; 16];
7498 let len = frame::prop_get(&mut buf, 1, prop::GNSS_LOCATION).unwrap();
7499 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
7500 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
7501 }
7502
7503 #[test]
7508 fn auto_update_owns_the_position_until_it_is_switched_off() {
7509 let mut session = test_session();
7510 set(&mut session, prop::GNSS_IDENT_UPDATE, &[1]);
7511
7512 let (emitted, _) = set(&mut session, prop::IDENT_LOCATION, &[1, 2, 3]);
7513 expect_status(&emitted[0], 2, Status::INVALID_STATE);
7514 let (emitted, _) = set(&mut session, prop::IDENT_ALTITUDE, &[10]);
7515 expect_status(&emitted[0], 2, Status::INVALID_STATE);
7516
7517 set(&mut session, prop::GNSS_IDENT_PRECISION, &[3]);
7519 assert!(session.absorb_ident_fix(&[1, 2, 3, 4, 5, 6, 7], Some(120)));
7520 assert_eq!(session.ident_location(), [1, 2, 3]);
7521 assert_eq!(session.ident_altitude_m(), Some(120));
7522
7523 assert!(!session.absorb_ident_fix(&[1, 2, 3, 9, 9, 9, 9], Some(120)));
7526 assert!(session.absorb_ident_fix(&[1, 2, 4, 0, 0, 0, 0], Some(120)));
7527
7528 set(&mut session, prop::GNSS_IDENT_UPDATE, &[0]);
7531 assert_eq!(session.ident_location(), [1, 2, 4]);
7532 assert_eq!(session.ident_altitude_m(), Some(120));
7533 assert!(!session.absorb_ident_fix(&[7, 7, 7], Some(0)));
7534 set(&mut session, prop::IDENT_LOCATION, &[5, 5]);
7535 assert_eq!(get(&mut session, prop::IDENT_LOCATION), [5, 5]);
7536 }
7537
7538 #[test]
7541 fn dev_discoverable_defaults_on_and_the_opt_out_survives_reboot() {
7542 let mut session = test_session();
7543 assert_eq!(get(&mut session, prop::DEV_DISCOVERABLE), [1]);
7544 assert!(session.dev_discoverable());
7545
7546 let (emitted, effect) = set(&mut session, prop::DEV_DISCOVERABLE, &[0]);
7547 assert!(effect.is_none());
7548 let (_, key, value) = parse_prop_is(&emitted[0]);
7549 assert_eq!(key, prop::DEV_DISCOVERABLE);
7550 assert_eq!(value, [0]);
7551 assert!(!session.dev_discoverable());
7552
7553 save(&mut session);
7554 let mut bytes = [0u8; SNAPSHOT_MAX];
7555 let len = session.encode_snapshot(&mut bytes).unwrap();
7556 let mut booted: TestSession =
7557 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
7558 booted.restore_at_boot(&bytes[..len]).unwrap();
7559 assert!(!booted.dev_discoverable());
7560 booted.attach(true);
7561 assert_eq!(get(&mut booted, prop::DEV_DISCOVERABLE), [0]);
7562
7563 let (emitted, _) = set(&mut booted, prop::DEV_DISCOVERABLE, &[2]);
7565 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7566 }
7567
7568 #[test]
7572 fn prop_ident_defers_to_the_platform_for_signing() {
7573 let mut session = test_session();
7574 let mut buf = [0u8; 16];
7575 let len = frame::prop_get(&mut buf, 7, prop::IDENT).unwrap();
7576 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
7577 assert!(emitted.is_empty(), "nothing before the signature exists");
7578 assert_eq!(effect, Some(Effect::SignIdentity { tid: 7 }));
7579
7580 let blob = [0xAB; 96];
7581 let mut out = Vec::new();
7582 session.respond_identity_blob(7, Ok(&blob), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
7583 let (tid, key, value) = parse_prop_is(&out[0]);
7584 assert_eq!((tid, key), (7, prop::IDENT));
7585 assert_eq!(value, blob);
7586
7587 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
7590 assert_eq!(effect, Some(Effect::SignIdentity { tid: 7 }));
7591 let mut out = Vec::new();
7592 session.respond_identity_blob(7, Err(()), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
7593 expect_status(&out[0], 7, Status::FAILURE);
7594 }
7595
7596 #[test]
7597 fn repeater_enable_round_trips_and_persists() {
7598 let mut session = test_session();
7599 assert_eq!(get(&mut session, prop::MAC_REPEATER_ENABLED), [0]);
7602 let (emitted, effect) = set(&mut session, prop::MAC_REPEATER_ENABLED, &[1]);
7603 let (_, key, value) = parse_prop_is(&emitted[0]);
7604 assert_eq!(key, prop::MAC_REPEATER_ENABLED);
7605 assert_eq!(value, [1]);
7606 assert_eq!(effect, None);
7608 assert_eq!(get(&mut session, prop::MAC_REPEATER_ENABLED), [1]);
7609 assert!(session.repeater_enabled());
7610 let (emitted, _) = set(&mut session, prop::MAC_REPEATER_ENABLED, &[2]);
7612 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7613
7614 save(&mut session);
7616 let mut bytes = [0u8; SNAPSHOT_MAX];
7617 let len = session.encode_snapshot(&mut bytes).unwrap();
7618 let mut booted: TestSession =
7619 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
7620 booted.restore_at_boot(&bytes[..len]).unwrap();
7621 assert!(booted.repeater_enabled());
7622 booted.attach(true);
7623 assert_eq!(get(&mut booted, prop::MAC_REPEATER_ENABLED), [1]);
7624
7625 let fresh: TestSession = Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
7627 assert!(!fresh.repeater_enabled());
7628 }
7629
7630 #[test]
7634 fn repeater_policy_round_trips_and_persists() {
7635 let mut session = test_session();
7636
7637 assert_eq!(
7639 get(&mut session, prop::MAC_REPEATER_REGIONS),
7640 Vec::<u8>::new()
7641 );
7642 assert_eq!(
7643 get(&mut session, prop::MAC_REPEATER_DEFAULT_REGION),
7644 Vec::<u8>::new()
7645 );
7646 assert_eq!(
7647 get(&mut session, prop::MAC_REPEATER_MIN_RSSI),
7648 Vec::<u8>::new()
7649 );
7650 assert_eq!(
7651 get(&mut session, prop::MAC_REPEATER_MIN_SNR),
7652 Vec::<u8>::new()
7653 );
7654
7655 assert_eq!(get(&mut session, prop::MAC_REPEATER_ENABLED), [0]);
7657 let table = region_table(&["SJC", "Rogue Valley"]);
7658 let (emitted, effect) = set(&mut session, prop::MAC_REPEATER_REGIONS, &table);
7659 assert_eq!(effect, None, "policy is not radio-affecting");
7660 let (_, key, value) = parse_prop_is(&emitted[0]);
7661 assert_eq!(key, prop::MAC_REPEATER_REGIONS);
7662 assert_eq!(value, table, "the strings read back as they were written");
7663 set(
7664 &mut session,
7665 prop::MAC_REPEATER_DEFAULT_REGION,
7666 &[0x78, 0x53],
7667 );
7668 set(&mut session, prop::MAC_REPEATER_MIN_RSSI, &[0x8D, 0xFF]);
7670 set(&mut session, prop::MAC_REPEATER_MIN_SNR, &[0xF9]);
7672
7673 assert_eq!(region_names(&session), ["SJC", "Rogue Valley"]);
7674 assert_eq!(
7675 region_codes(&session),
7676 [[0x78, 0x53], [0xC0, 0xF9]],
7677 "an airport code and a hashed name, derived once at the write"
7678 );
7679 assert_eq!(session.repeater_default_region(), Some([0x78, 0x53]));
7680 assert_eq!(session.repeater_min_rssi(), Some(-115));
7681 assert_eq!(session.repeater_min_snr(), Some(-7));
7682
7683 save(&mut session);
7685 let mut bytes = [0u8; SNAPSHOT_MAX];
7686 let len = session.encode_snapshot(&mut bytes).unwrap();
7687 let mut booted: TestSession =
7688 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
7689 booted.restore_at_boot(&bytes[..len]).unwrap();
7690 assert_eq!(region_names(&booted), ["SJC", "Rogue Valley"]);
7691 assert_eq!(
7692 region_codes(&booted),
7693 [[0x78, 0x53], [0xC0, 0xF9]],
7694 "the snapshot carries the strings; the codes re-derive on restore"
7695 );
7696 assert_eq!(booted.repeater_default_region(), Some([0x78, 0x53]));
7697 assert_eq!(booted.repeater_min_rssi(), Some(-115));
7698 assert_eq!(booted.repeater_min_snr(), Some(-7));
7699 booted.attach(true);
7700 assert_eq!(
7701 get(&mut booted, prop::MAC_REPEATER_MIN_RSSI),
7702 [0x8D, 0xFF],
7703 "the reported value is the little-endian INT16 that was written"
7704 );
7705 assert_eq!(get(&mut booted, prop::MAC_REPEATER_MIN_SNR), [0xF9]);
7706 }
7707
7708 #[test]
7712 fn repeater_policy_clears_back_to_unset() {
7713 let mut session = test_session();
7714 set(
7715 &mut session,
7716 prop::MAC_REPEATER_REGIONS,
7717 ®ion_table(&["SJC"]),
7718 );
7719 set(
7720 &mut session,
7721 prop::MAC_REPEATER_DEFAULT_REGION,
7722 &[0x78, 0x53],
7723 );
7724 set(&mut session, prop::MAC_REPEATER_MIN_RSSI, &[0x8D, 0xFF]);
7725 set(&mut session, prop::MAC_REPEATER_MIN_SNR, &[0xF9]);
7726
7727 set(&mut session, prop::MAC_REPEATER_REGIONS, &[]);
7728 set(&mut session, prop::MAC_REPEATER_DEFAULT_REGION, &[]);
7729 set(&mut session, prop::MAC_REPEATER_MIN_RSSI, &[]);
7730 set(&mut session, prop::MAC_REPEATER_MIN_SNR, &[]);
7731
7732 assert_eq!(region_names(&session), Vec::<String>::new());
7733 assert_eq!(session.repeater_default_region(), None);
7734 assert_eq!(session.repeater_min_rssi(), None);
7735 assert_eq!(session.repeater_min_snr(), None);
7736
7737 save(&mut session);
7738 let mut bytes = [0u8; SNAPSHOT_MAX];
7739 let len = session.encode_snapshot(&mut bytes).unwrap();
7740 let mut booted: TestSession =
7741 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
7742 booted.restore_at_boot(&bytes[..len]).unwrap();
7743 assert_eq!(region_names(&booted), Vec::<String>::new());
7744 assert_eq!(booted.repeater_default_region(), None);
7745 assert_eq!(booted.repeater_min_rssi(), None);
7746 assert_eq!(booted.repeater_min_snr(), None);
7747 }
7748
7749 #[test]
7753 fn repeater_policy_rejects_malformed_values() {
7754 let mut session = test_session();
7755
7756 for bad in [
7760 b"".as_slice(),
7761 b"AAAAAAAAAAAAAAAAAAAAAAAAA",
7763 b"\xFF\xFE not text",
7764 ] {
7765 let (emitted, _) = set(
7766 &mut session,
7767 prop::MAC_REPEATER_REGIONS,
7768 ®ion_table_raw(&[bad]),
7769 );
7770 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7771 }
7772 let (emitted, _) = set(&mut session, prop::MAC_REPEATER_REGIONS, &[9, b'S', b'J']);
7774 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7775 let names: Vec<String> = (0..=MAX_REPEATER_REGIONS)
7777 .map(|index| format!("Region {index}"))
7778 .collect();
7779 let over_capacity = region_table(&names.iter().map(String::as_str).collect::<Vec<_>>());
7780 let (emitted, _) = set(&mut session, prop::MAC_REPEATER_REGIONS, &over_capacity);
7781 expect_status(&emitted[0], 2, Status::NOMEM);
7782
7783 let (emitted, _) = set(
7785 &mut session,
7786 prop::MAC_REPEATER_DEFAULT_REGION,
7787 &[0x78, 0x53, 0x31],
7788 );
7789 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7790
7791 let (emitted, _) = set(&mut session, prop::MAC_REPEATER_MIN_RSSI, &[0x8D]);
7793 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7794 let (emitted, _) = set(&mut session, prop::MAC_REPEATER_MIN_SNR, &[0xF9, 0xFF]);
7795 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
7796
7797 assert_eq!(region_names(&session), Vec::<String>::new());
7799 assert_eq!(session.repeater_default_region(), None);
7800 assert_eq!(session.repeater_min_rssi(), None);
7801 assert_eq!(session.repeater_min_snr(), None);
7802 }
7803
7804 #[test]
7809 fn repeater_default_region_is_not_cross_checked_against_the_region_list() {
7810 let mut session = test_session();
7811 set(
7813 &mut session,
7814 prop::MAC_REPEATER_DEFAULT_REGION,
7815 &[0x78, 0x53],
7816 );
7817 assert_eq!(session.repeater_default_region(), Some([0x78, 0x53]));
7818 set(
7819 &mut session,
7820 prop::MAC_REPEATER_REGIONS,
7821 ®ion_table(&["SF Bay Area"]),
7822 );
7823 assert_eq!(region_codes(&session), [[0xD8, 0xB7]]);
7824 assert_eq!(
7825 session.repeater_default_region(),
7826 Some([0x78, 0x53]),
7827 "a later region-list write must not silently drop the default"
7828 );
7829
7830 let mut session = test_session();
7833 set(
7834 &mut session,
7835 prop::MAC_REPEATER_DEFAULT_REGION,
7836 &[0xAB, 0xCD],
7837 );
7838 assert_eq!(region_names(&session), Vec::<String>::new());
7839 assert_eq!(session.repeater_default_region(), Some([0xAB, 0xCD]));
7840 }
7841
7842 #[test]
7846 fn repeater_regions_insert_and_remove_one_entry_at_a_time() {
7847 let mut session = test_session();
7848
7849 let (emitted, effect) = insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"SJC");
7850 assert!(effect.is_none());
7851 let (key, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
7852 assert_eq!(key, prop::MAC_REPEATER_REGIONS);
7853 assert_eq!(digest, b"SJC", "the inserted item is echoed as written");
7854 insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"Rogue Valley");
7855 assert_eq!(region_names(&session), ["SJC", "Rogue Valley"]);
7856 assert_eq!(region_codes(&session), [[0x78, 0x53], [0xC0, 0xF9]]);
7857 assert_eq!(
7858 get(&mut session, prop::MAC_REPEATER_REGIONS),
7859 region_table(&["SJC", "Rogue Valley"])
7860 );
7861
7862 insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"0x1234");
7864 assert_eq!(region_codes(&session)[2], [0x12, 0x34]);
7865
7866 let (emitted, _) = insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"SJC");
7869 expect_status(&emitted[0], 5, Status::ALREADY);
7870 assert_eq!(region_names(&session), ["SJC", "Rogue Valley", "0x1234"]);
7871
7872 let (emitted, _) = remove_item(&mut session, prop::MAC_REPEATER_REGIONS, b"SJC");
7874 let (key, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
7875 assert_eq!(key, prop::MAC_REPEATER_REGIONS);
7876 assert_eq!(digest, b"SJC");
7877 assert_eq!(region_names(&session), ["0x1234", "Rogue Valley"]);
7878
7879 let (emitted, _) = remove_item(&mut session, prop::MAC_REPEATER_REGIONS, b"SJC");
7880 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
7881 let (emitted, _) = remove_item(&mut session, prop::MAC_REPEATER_REGIONS, b"");
7884 expect_status(&emitted[0], 6, Status::INVALID_ARGUMENT);
7885
7886 for index in 0..MAX_REPEATER_REGIONS - 2 {
7888 let name = format!("Region {index}");
7889 insert_item(&mut session, prop::MAC_REPEATER_REGIONS, name.as_bytes());
7890 }
7891 let (emitted, _) = insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"One too many");
7892 expect_status(&emitted[0], 5, Status::NOMEM);
7893 }
7894
7895 #[test]
7900 fn repeater_regions_hold_one_entry_per_region_whatever_its_case() {
7901 let mut session = test_session();
7902
7903 insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"MFD");
7904 let (emitted, _) = insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"mfd");
7905 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
7906 assert_eq!(digest, b"mfd", "the respelling is what the host is told");
7907 assert_eq!(region_names(&session), ["mfd"]);
7908 assert_eq!(
7909 region_codes(&session),
7910 [[0x52, 0x34]],
7911 "the code never moved: it derives from the folded string"
7912 );
7913
7914 let (emitted, _) = insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"mfd");
7916 expect_status(&emitted[0], 5, Status::ALREADY);
7917
7918 insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"Rogue Valley");
7921 insert_item(&mut session, prop::MAC_REPEATER_REGIONS, b"ROGUE VALLEY");
7922 assert_eq!(region_names(&session), ["mfd", "ROGUE VALLEY"]);
7923 remove_item(&mut session, prop::MAC_REPEATER_REGIONS, b"rogue valley");
7924 assert_eq!(region_names(&session), ["mfd"]);
7925
7926 set(
7929 &mut session,
7930 prop::MAC_REPEATER_REGIONS,
7931 ®ion_table(&["SJC", "sjc", "Sjc"]),
7932 );
7933 assert_eq!(region_names(&session), ["Sjc"]);
7934 assert_eq!(region_codes(&session), [[0x78, 0x53]]);
7935 }
7936
7937 #[test]
7938 fn device_name_round_trips_survives_attach_and_resets_to_default() {
7939 let mut session = test_session();
7940 let configured = "Field Radio 📻";
7941 let (emitted, effect) = set(&mut session, prop::DEV_NAME, configured.as_bytes());
7942 let (_, key, value) = parse_prop_is(&emitted[0]);
7943 assert_eq!(key, prop::DEV_NAME);
7944 assert_eq!(value, configured.as_bytes());
7945 assert_eq!(effect, Some(Effect::DeviceNameChanged));
7946 assert_eq!(session.device_name(), configured);
7947
7948 session.attach(true);
7949 assert_eq!(get(&mut session, prop::DEV_NAME), configured.as_bytes());
7950
7951 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_| {});
7952 assert_eq!(get(&mut session, prop::DEV_NAME), b"Test UMSH Device");
7953 }
7954
7955 #[test]
7956 fn attach_preserves_device_domain_and_emits_nothing() {
7957 let mut session = test_session_with_boot_status(Status::RESET_WATCHDOG);
7958
7959 set(&mut session, prop::PHY_FREQ, &906_875u32.to_le_bytes());
7962 set(&mut session, prop::PHY_LORA_SF, &[9]);
7963 set(&mut session, prop::PHY_DUTY_LIMIT, &100u16.to_le_bytes());
7964 enable(&mut session);
7965 let settings_before = session.settings();
7966 assert!(settings_before.enabled);
7967 let (_, effect) = send_packet(&mut session, 1, &[0xAB; 8], &[], 0);
7968 assert_eq!(effect, Some(Effect::StartTransmit));
7969 let mut emitted = Vec::new();
7970 session.on_tx_result(TxOutcome::Sent, 0, &mut |bytes: &[u8]| {
7971 emitted.push(bytes.to_vec())
7972 });
7973 let duty_before = get(&mut session, prop::PHY_DUTY_NOW);
7974 assert_ne!(duty_before, 0u16.to_le_bytes());
7975
7976 session.attach(true);
7979 assert_eq!(session.settings(), settings_before);
7980 assert_eq!(get(&mut session, prop::PHY_ENABLED), [1]);
7981 assert_eq!(get(&mut session, prop::PHY_FREQ), 906_875u32.to_le_bytes());
7982 assert_eq!(
7983 get(&mut session, prop::PHY_DUTY_LIMIT),
7984 100u16.to_le_bytes()
7985 );
7986 assert_eq!(get(&mut session, prop::PHY_DUTY_NOW), duty_before);
7987 }
7988
7989 #[test]
7990 fn attach_retains_boot_status_for_first_query() {
7991 let mut session = test_session_with_boot_status(Status::RESET_WATCHDOG);
7992 session.attach(true);
7993 let raw = get(&mut session, prop::LAST_STATUS);
7994 assert_eq!(pui::decode(&raw).unwrap().0, Status::RESET_WATCHDOG.0);
7995 }
7996
7997 #[test]
7998 fn attach_resets_promiscuous_mode() {
7999 let mut session = test_session();
8000 set(&mut session, prop::MAC_PROMISCUOUS, &[1]);
8001 assert_eq!(get(&mut session, prop::MAC_PROMISCUOUS), [1]);
8002 session.attach(true);
8003 assert_eq!(get(&mut session, prop::MAC_PROMISCUOUS), [0]);
8004
8005 set(&mut session, prop::MAC_PROMISCUOUS, &[1]);
8007 session.detach();
8008 session.attach(true);
8009 assert_eq!(get(&mut session, prop::MAC_PROMISCUOUS), [0]);
8010
8011 let (emitted, _) = set(&mut session, prop::MAC_PROMISCUOUS, &[2]);
8013 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8014 }
8015
8016 #[test]
8017 fn attach_resets_backhaul_mode() {
8018 let mut session = test_session();
8019 let (_, effect) = set(&mut session, prop::MAC_BACKHAUL, &[1]);
8020 assert_eq!(effect, Some(Effect::ApplyBackhaul { enabled: true }));
8021 assert_eq!(get(&mut session, prop::MAC_BACKHAUL), [1]);
8022
8023 session.attach(true);
8024 assert_eq!(get(&mut session, prop::MAC_BACKHAUL), [0]);
8025
8026 let (emitted, _) = set(&mut session, prop::MAC_BACKHAUL, &[2]);
8027 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8028 }
8029
8030 #[test]
8035 fn a_backhauled_send_spends_no_airtime() {
8036 let mut session = test_session();
8037 enable(&mut session);
8038 set(&mut session, prop::MAC_BACKHAUL, &[1]);
8039
8040 let (_, effect) = send_packet(&mut session, 1, &[0xAB; 32], &[], 0);
8041 assert_eq!(effect, Some(Effect::StartTransmit));
8042 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
8043 assert_eq!(get(&mut session, prop::PHY_DUTY_NOW), 0u16.to_le_bytes());
8044
8045 set(&mut session, prop::MAC_BACKHAUL, &[0]);
8047 let (_, effect) = send_packet(&mut session, 2, &[0xAB; 32], &[], 0);
8048 assert_eq!(effect, Some(Effect::StartTransmit));
8049 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
8050 assert_ne!(get(&mut session, prop::PHY_DUTY_NOW), 0u16.to_le_bytes());
8051 }
8052
8053 #[test]
8057 fn a_self_transmitted_frame_is_delivered_as_unmeasured() {
8058 let mut session = test_session();
8059 enable(&mut session);
8060 set(&mut session, prop::MAC_PROMISCUOUS, &[1]);
8061
8062 let mut emitted = Vec::new();
8063 session.on_radio_rx(
8064 &[1, 2, 3],
8065 &RadioRxInfo::self_transmitted(),
8066 0,
8067 &mut |bytes: &[u8]| emitted.push(bytes.to_vec()),
8068 );
8069 let parsed = Frame::parse(&emitted[0]).unwrap();
8070 let payload = StreamPayload::parse(parsed.payload).unwrap();
8071 assert_eq!(payload.data, &[1, 2, 3]);
8072 let meta = BufferedRxMeta::decode(payload.metadata).unwrap();
8073 assert_eq!(meta.flags, RX_FLAG_SELF_TX);
8074 assert_eq!(meta.age_s, 0);
8075 assert_eq!(meta.rx.rssi_dbm, None);
8076 assert_eq!(meta.rx.snr_cb, None);
8077 }
8078
8079 #[test]
8082 fn an_ordinary_reception_carries_no_flags_byte() {
8083 let mut session = test_session();
8084 enable(&mut session);
8085
8086 let mut emitted = Vec::new();
8087 session.on_radio_rx(
8088 &[1, 2, 3],
8089 &RadioRxInfo::measured(-91, -53, None),
8090 0,
8091 &mut |bytes: &[u8]| emitted.push(bytes.to_vec()),
8092 );
8093 let parsed = Frame::parse(&emitted[0]).unwrap();
8094 let payload = StreamPayload::parse(parsed.payload).unwrap();
8095 assert_eq!(payload.metadata.len(), RxMeta::WIRE_LEN);
8096 }
8097
8098 #[test]
8099 fn attach_clears_pending_transmit_correlation() {
8100 let mut session = test_session();
8101 enable(&mut session);
8102 let (_, effect) = send_packet(&mut session, 3, &[0x01; 4], &[], 0);
8103 assert_eq!(effect, Some(Effect::StartTransmit));
8104 assert!(session.has_pending_tx());
8105
8106 session.attach(true);
8109 assert!(!session.has_pending_tx());
8110 let mut emitted = Vec::new();
8111 session.on_tx_result(TxOutcome::Sent, 0, &mut |bytes: &[u8]| {
8112 emitted.push(bytes.to_vec())
8113 });
8114 assert!(emitted.is_empty());
8115
8116 let (_, effect) = send_packet(&mut session, 4, &[0x02; 4], &[], 0);
8118 assert_eq!(effect, Some(Effect::StartTransmit));
8119 }
8120
8121 #[test]
8122 fn reset_restores_post_reset_values_and_announces() {
8123 let mut session = test_session();
8124 set(&mut session, prop::PHY_FREQ, &906_875u32.to_le_bytes());
8125 set(&mut session, prop::MAC_PROMISCUOUS, &[1]);
8126 enable(&mut session);
8127
8128 let mut emitted = Vec::new();
8129 let effect = session.reset(Status::RESET_SOFTWARE, &mut |bytes: &[u8]| {
8130 emitted.push(bytes.to_vec())
8131 });
8132 expect_status(&emitted[0], TID_UNSOLICITED, Status::RESET_SOFTWARE);
8133 assert!(matches!(effect, Effect::ApplyRadio(settings) if !settings.enabled));
8134 assert_eq!(get(&mut session, prop::PHY_FREQ), 910_525u32.to_le_bytes());
8135 assert_eq!(get(&mut session, prop::MAC_PROMISCUOUS), [0]);
8136 }
8137
8138 #[test]
8139 fn device_name_rejects_empty_invalid_nul_and_oversize_values() {
8140 let mut session = test_session();
8141 let oversize = [b'x'; MAX_DEVICE_NAME_LEN + 1];
8142 for bad in [&[][..], &[0xff][..], b"bad\0name", &oversize[..]] {
8143 let (emitted, effect) = set(&mut session, prop::DEV_NAME, bad);
8144 assert!(effect.is_none());
8145 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8146 }
8147 assert_eq!(session.device_name(), "Test UMSH Device");
8148 }
8149
8150 #[test]
8151 fn rf_property_round_trip() {
8152 let mut session = test_session();
8153 let (emitted, effect) = set(&mut session, prop::PHY_FREQ, &906_875u32.to_le_bytes());
8154 let (_, key, value) = parse_prop_is(&emitted[0]);
8155 assert_eq!(key, prop::PHY_FREQ);
8156 assert_eq!(value, 906_875u32.to_le_bytes());
8157 assert!(matches!(effect, Some(Effect::ApplyRadio(s)) if s.freq_khz == 906_875));
8158 assert_eq!(get(&mut session, prop::PHY_FREQ), 906_875u32.to_le_bytes());
8159 }
8160
8161 #[test]
8165 fn tx_power_clamps_to_radio_range() {
8166 let mut session = test_session();
8167 for (requested, expected) in [(-20i8, -9i8), (40, 22), (-9, -9), (22, 22), (14, 14)] {
8168 let (emitted, effect) = set(&mut session, prop::PHY_TX_POWER, &[requested as u8]);
8169 let (_, key, value) = parse_prop_is(&emitted[0]);
8170 assert_eq!(key, prop::PHY_TX_POWER);
8171 assert_eq!(value, [expected as u8], "set {requested} dBm");
8172 assert!(
8173 matches!(effect, Some(Effect::ApplyRadio(s)) if s.tx_power_dbm == expected),
8174 "set {requested} dBm"
8175 );
8176 assert_eq!(get(&mut session, prop::PHY_TX_POWER), [expected as u8]);
8177 }
8178 }
8179
8180 #[test]
8183 fn tx_power_override_clamps_to_radio_range() {
8184 for (requested, expected) in [(-20i8, -9i8), (40, 22)] {
8185 let mut session = test_session();
8186 enable(&mut session);
8187 let meta = [requested as u8, 0x00];
8188 let (_, effect) = send_packet(&mut session, 4, &[0u8; 8], &meta, 0);
8189 assert_eq!(effect, Some(Effect::StartTransmit));
8190 assert_eq!(session.tx_power(), TxPower::Dbm(expected), "tx {requested}");
8191 }
8192 }
8193
8194 #[test]
8195 fn invalid_values_rejected() {
8196 let mut session = test_session();
8197 for (key, bad) in [
8198 (prop::PHY_LORA_SF, &[4][..]),
8199 (prop::PHY_LORA_SF, &[13][..]),
8200 (prop::PHY_LORA_CR, &[9][..]),
8201 (prop::PHY_LORA_BW, &123_456u32.to_le_bytes()[..]),
8202 (prop::PHY_FREQ, &10_000u32.to_le_bytes()[..]),
8203 (prop::PHY_TX_POWER, &[14, 0][..]),
8206 (prop::PHY_ENABLED, &[2][..]),
8207 (prop::PHY_LORA_SW, &0xBEEFu16.to_le_bytes()[..]),
8208 (prop::PHY_FREQ, &[1, 2][..]),
8210 ] {
8211 let (emitted, effect) = set(&mut session, key, bad);
8212 assert!(effect.is_none(), "key {key} accepted {bad:?}");
8213 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8214 }
8215 }
8216
8217 #[test]
8218 fn uptime_reads_the_callers_clock() {
8219 let mut session = test_session();
8220
8221 let read_at = |session: &mut TestSession, now_ms: u64| -> u32 {
8224 let mut buf = [0u8; 16];
8225 let len = frame::prop_get(&mut buf, 1, prop::UPTIME).unwrap();
8226 let (emitted, effect) = dispatch(session, &buf[..len], now_ms);
8227 assert!(effect.is_none());
8228 let (_, key, value) = parse_prop_is(&emitted[0]);
8229 assert_eq!(key, prop::UPTIME);
8230 u32::from_le_bytes(value.try_into().expect("UINT32"))
8231 };
8232
8233 assert_eq!(read_at(&mut session, 0), 0);
8234 assert_eq!(read_at(&mut session, 999), 0);
8235 assert_eq!(read_at(&mut session, 3_600_500), 3600);
8236
8237 let mut buf = [0u8; 4];
8241 let len = frame::reset(&mut buf, 0).unwrap();
8242 dispatch(&mut session, &buf[..len], 3_600_500);
8243 assert_eq!(read_at(&mut session, 3_601_500), 3601);
8244
8245 let (emitted, _) = set(&mut session, prop::UPTIME, &7u32.to_le_bytes());
8247 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8248 }
8249
8250 #[test]
8251 fn read_only_and_unknown_props() {
8252 let mut session = test_session();
8253 let (emitted, _) = set(&mut session, prop::PHY_MTU, &[0, 1]);
8254 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8255
8256 let (emitted, _) = set(&mut session, 9_999, &[0]);
8257 expect_status(&emitted[0], 2, Status::PROP_NOT_FOUND);
8258
8259 let mut buf = [0u8; 16];
8260 let len = frame::prop_get(&mut buf, 1, 9_999).unwrap();
8261 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
8262 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
8263
8264 let len = frame::prop_get(&mut buf, 1, prop::PHY_RSSI).unwrap();
8266 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
8267 assert!(effect.is_none());
8268 expect_status(&emitted[0], 1, Status::INVALID_STATE);
8269 }
8270
8271 #[test]
8272 fn phy_rssi_defers_to_radio_when_enabled() {
8273 let mut session = test_session();
8274 enable(&mut session);
8275
8276 let mut buf = [0u8; 16];
8278 let len = frame::prop_get(&mut buf, 3, prop::PHY_RSSI).unwrap();
8279 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
8280 assert!(emitted.is_empty(), "no response until the radio is sampled");
8281 assert_eq!(effect, Some(Effect::SampleRssi { tid: 3 }));
8282
8283 let mut out = Vec::new();
8285 session.respond_rssi(3, Ok(-91), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
8286 let (tid, key, value) = parse_prop_is(&out[0]);
8287 assert_eq!(tid, 3);
8288 assert_eq!(key, prop::PHY_RSSI);
8289 assert_eq!(value, [(-91i8) as u8]);
8290
8291 let mut out = Vec::new();
8293 session.respond_rssi(4, Err(()), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
8294 expect_status(&out[0], 4, Status::FAILURE);
8295 }
8296
8297 #[test]
8298 fn battery_get_samples_on_request() {
8299 let mut session = test_session();
8300
8301 let mut buf = [0u8; 16];
8303 let len = frame::prop_get(&mut buf, 7, prop::BATTERY).unwrap();
8304 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
8305 assert!(
8306 emitted.is_empty(),
8307 "no response until the battery is sampled"
8308 );
8309 assert_eq!(effect, Some(Effect::SampleBattery { tid: 7 }));
8310
8311 let mut out = Vec::new();
8314 session.respond_battery(
8315 7,
8316 Ok(BatteryStatus {
8317 voltage_mv: Some(3987),
8318 level_percent: None,
8319 charge_state: Some(battery::BatteryChargeState::Charging),
8320 }),
8321 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
8322 );
8323 let (tid, key, value) = parse_prop_is(&out[0]);
8324 assert_eq!(tid, 7);
8325 assert_eq!(key, prop::BATTERY);
8326 assert_eq!(value, [0b101, 0x93, 0x0F, 1]);
8327
8328 let mut out = Vec::new();
8331 session.respond_battery(6, Err(()), &mut |bytes: &[u8]| out.push(bytes.to_vec()));
8332 expect_status(&out[0], 6, Status::FAILURE);
8333 }
8334
8335 #[test]
8336 fn battery_snapshot_must_match_configured_fields() {
8337 let mut session = test_session();
8338 let mut out = Vec::new();
8342 session.respond_battery(
8343 5,
8344 Ok(BatteryStatus {
8345 voltage_mv: Some(4200),
8346 level_percent: Some(80),
8347 charge_state: Some(battery::BatteryChargeState::Charged),
8348 }),
8349 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
8350 );
8351 expect_status(&out[0], 5, Status::FAILURE);
8352
8353 let mut out = Vec::new();
8357 session.respond_battery(
8358 6,
8359 Ok(BatteryStatus {
8360 voltage_mv: Some(4200),
8361 level_percent: None,
8362 charge_state: None,
8363 }),
8364 &mut |bytes: &[u8]| out.push(bytes.to_vec()),
8365 );
8366 let (tid, key, value) = parse_prop_is(&out[0]);
8367 assert_eq!((tid, key), (6, prop::BATTERY));
8368 let decoded = BatteryStatus::decode(&value).unwrap();
8369 assert_eq!(decoded.voltage_mv, Some(4200));
8370 assert_eq!(decoded.charge_state, None);
8371 }
8372
8373 #[test]
8374 fn battery_without_capability_is_unknown() {
8375 let mut config = test_config();
8376 config.battery = None;
8377 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
8378 session.attach(true);
8379
8380 let raw = get(&mut session, prop::CAPS);
8381 let mut offset = 0;
8382 while offset < raw.len() {
8383 let (value, used) = pui::decode(&raw[offset..]).unwrap();
8384 assert_ne!(value, cap::BATTERY);
8385 offset += used;
8386 }
8387
8388 let mut buf = [0u8; 16];
8389 let len = frame::prop_get(&mut buf, 1, prop::BATTERY).unwrap();
8390 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
8391 assert!(effect.is_none());
8392 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
8393
8394 let (emitted, _) = set(&mut session, prop::BATTERY, &[0b001, 0, 0]);
8395 expect_status(&emitted[0], 2, Status::PROP_NOT_FOUND);
8396 }
8397
8398 #[test]
8399 fn battery_with_no_fields_answers_empty_without_sampling() {
8400 let mut config = test_config();
8401 config.battery = Some(BatteryFields::NONE);
8402 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
8403 session.attach(true);
8404
8405 assert_eq!(get(&mut session, prop::BATTERY), Vec::<u8>::new());
8407 }
8408
8409 #[test]
8410 fn battery_rejects_mutation() {
8411 let mut session = test_session();
8412 let (emitted, effect) = set(&mut session, prop::BATTERY, &[0b001, 0, 0]);
8413 assert!(effect.is_none());
8414 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8415
8416 let mut buf = [0u8; 16];
8417 let len = frame::prop_insert(&mut buf, 3, prop::BATTERY, &[0]).unwrap();
8418 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
8419 expect_status(&emitted[0], 3, Status::INVALID_ARGUMENT);
8420
8421 let len = frame::prop_remove(&mut buf, 4, prop::BATTERY, &[0]).unwrap();
8422 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
8423 expect_status(&emitted[0], 4, Status::INVALID_ARGUMENT);
8424 }
8425
8426 #[test]
8427 fn illuminance_get_samples_on_request() {
8428 let mut session = test_session();
8429 let mut buf = [0u8; 16];
8430 let len = frame::prop_get(&mut buf, 5, prop::ILLUMINANCE).unwrap();
8431 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
8432 assert!(emitted.is_empty(), "no response until the sensor is read");
8433 assert_eq!(effect, Some(Effect::SampleIlluminance { tid: 5 }));
8434
8435 let mut out = Vec::new();
8436 session.respond_illuminance(5, Some(12_345), &mut |bytes: &[u8]| {
8437 out.push(bytes.to_vec())
8438 });
8439 let (tid, key, value) = parse_prop_is(&out[0]);
8440 assert_eq!((tid, key), (5, prop::ILLUMINANCE));
8441 assert_eq!(value, 12_345u32.to_le_bytes());
8442 }
8443
8444 #[test]
8447 fn illuminance_reports_a_failed_read_as_empty() {
8448 let mut session = test_session();
8449 let mut out = Vec::new();
8450 session.respond_illuminance(4, None, &mut |bytes: &[u8]| out.push(bytes.to_vec()));
8451 let (tid, key, value) = parse_prop_is(&out[0]);
8452 assert_eq!((tid, key, value), (4, prop::ILLUMINANCE, Vec::new()));
8453 }
8454
8455 #[test]
8456 fn illuminance_without_the_sensor_is_unknown() {
8457 let mut config = test_config();
8458 config.illuminance = false;
8459 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
8460 session.attach(true);
8461
8462 let raw = get(&mut session, prop::CAPS);
8463 let mut offset = 0;
8464 while offset < raw.len() {
8465 let (value, used) = pui::decode(&raw[offset..]).unwrap();
8466 assert_ne!(value, cap::ILLUMINANCE);
8467 offset += used;
8468 }
8469
8470 let mut buf = [0u8; 16];
8471 let len = frame::prop_get(&mut buf, 1, prop::ILLUMINANCE).unwrap();
8472 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
8473 assert!(effect.is_none());
8474 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
8475 }
8476
8477 #[test]
8478 fn illuminance_rejects_mutation() {
8479 let mut session = test_session();
8480 let (emitted, effect) = set(&mut session, prop::ILLUMINANCE, &0u32.to_le_bytes());
8481 assert!(effect.is_none());
8482 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8483 }
8484
8485 fn set_alert_at(
8487 session: &mut TestSession,
8488 state: AlertState,
8489 now_ms: u64,
8490 ) -> (Vec<Vec<u8>>, Option<Effect>) {
8491 let mut value = [0u8; pui::MAX_LEN];
8492 let value_len = pui::encode(state.code(), &mut value).unwrap();
8493 let mut buf = [0u8; 16];
8494 let len = frame::prop_set(&mut buf, 2, prop::ALERT, &value[..value_len]).unwrap();
8495 dispatch(session, &buf[..len], now_ms)
8496 }
8497
8498 #[test]
8499 fn alert_starts_and_reports_the_new_state() {
8500 let mut session = test_session();
8501 assert_eq!(get(&mut session, prop::ALERT), vec![0]);
8502
8503 let (emitted, effect) = set_alert_at(&mut session, AlertState::Locate, 1_000);
8504 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::Locate)));
8505 let (tid, key, value) = parse_prop_is(&emitted[0]);
8506 assert_eq!((tid, key, value), (2, prop::ALERT, vec![1]));
8507 assert_eq!(session.alert(), AlertState::Locate);
8508 assert_eq!(get(&mut session, prop::ALERT), vec![1]);
8509 }
8510
8511 #[test]
8512 fn alert_rejects_unknown_states() {
8513 let mut session = test_session();
8514 let (emitted, effect) = set(&mut session, prop::ALERT, &[2]);
8515 assert!(effect.is_none());
8516 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8517 let (emitted, effect) = set(&mut session, prop::ALERT, &[]);
8519 assert!(effect.is_none());
8520 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8521 assert_eq!(session.alert(), AlertState::None);
8522 }
8523
8524 #[test]
8525 fn alert_expires_at_its_deadline() {
8526 let mut session = test_session();
8527 let started = 10_000;
8528 set_alert_at(&mut session, AlertState::Locate, started);
8529 let deadline = started + u64::from(AlertConfig::DEFAULT.timeout_ms);
8530 assert_eq!(session.alert_deadline_ms(), Some(deadline));
8531
8532 let mut emitted = Vec::new();
8534 let effect = session.poll_alert(deadline - 1, &mut |bytes: &[u8]| {
8535 emitted.push(bytes.to_vec())
8536 });
8537 assert!(effect.is_none());
8538 assert!(emitted.is_empty());
8539 assert_eq!(session.alert(), AlertState::Locate);
8540
8541 let effect = session.poll_alert(deadline, &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
8542 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::None)));
8543 assert_eq!(session.alert(), AlertState::None);
8544 assert_eq!(session.alert_deadline_ms(), None);
8545 let (tid, key, value) = parse_prop_is(&emitted[0]);
8547 assert_eq!((tid, key, value), (TID_UNSOLICITED, prop::ALERT, vec![0]));
8548 }
8549
8550 #[test]
8551 fn re_arming_an_alert_restarts_the_deadline() {
8552 let mut session = test_session();
8553 set_alert_at(&mut session, AlertState::Locate, 1_000);
8554 let first = session.alert_deadline_ms().unwrap();
8555
8556 let (_, effect) = set_alert_at(&mut session, AlertState::Locate, 60_000);
8558 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::Locate)));
8559 assert_eq!(
8560 session.alert_deadline_ms(),
8561 Some(60_000 + u64::from(AlertConfig::DEFAULT.timeout_ms))
8562 );
8563 assert!(session.alert_deadline_ms().unwrap() > first);
8564 let mut emitted = Vec::new();
8566 assert!(
8567 session
8568 .poll_alert(first, &mut |bytes: &[u8]| emitted.push(bytes.to_vec()))
8569 .is_none()
8570 );
8571 assert_eq!(session.alert(), AlertState::Locate);
8572 }
8573
8574 #[test]
8575 fn local_cancel_clears_and_announces_once() {
8576 let mut session = test_session();
8577 set_alert_at(&mut session, AlertState::Locate, 0);
8578
8579 let mut emitted = Vec::new();
8580 let effect = session.cancel_alert(&mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
8581 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::None)));
8582 let (tid, key, value) = parse_prop_is(&emitted[0]);
8583 assert_eq!((tid, key, value), (TID_UNSOLICITED, prop::ALERT, vec![0]));
8584
8585 emitted.clear();
8587 let effect = session.cancel_alert(&mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
8588 assert!(effect.is_none());
8589 assert!(emitted.is_empty());
8590 }
8591
8592 #[test]
8593 fn alert_survives_detach_and_reset() {
8594 let mut session = test_session();
8595 set_alert_at(&mut session, AlertState::Locate, 0);
8596
8597 session.detach();
8600 assert_eq!(session.alert(), AlertState::Locate);
8601 assert!(session.alert_deadline_ms().is_some());
8602
8603 session.attach(true);
8606 let mut buf = [0u8; 16];
8607 let len = frame::reset(&mut buf, 7).unwrap();
8608 dispatch(&mut session, &buf[..len], 0);
8609 assert_eq!(session.alert(), AlertState::Locate);
8610 assert_eq!(get(&mut session, prop::ALERT), vec![1]);
8611 }
8612
8613 #[test]
8614 fn cancelling_while_detached_emits_nothing() {
8615 let mut session = test_session();
8616 set_alert_at(&mut session, AlertState::Locate, 0);
8617 session.detach();
8618
8619 let mut emitted = Vec::new();
8620 let effect = session.cancel_alert(&mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
8621 assert_eq!(effect, Some(Effect::ApplyAlert(AlertState::None)));
8623 assert!(emitted.is_empty());
8624 assert_eq!(session.alert(), AlertState::None);
8625 }
8626
8627 #[test]
8628 fn alert_is_absent_without_the_capability() {
8629 let mut config = test_config();
8630 config.alert = None;
8631 let mut session: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
8632 session.attach(true);
8633
8634 let mut buf = [0u8; 16];
8635 let len = frame::prop_get(&mut buf, 1, prop::ALERT).unwrap();
8636 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
8637 expect_status(&emitted[0], 1, Status::PROP_NOT_FOUND);
8638
8639 let (emitted, effect) = set(&mut session, prop::ALERT, &[1]);
8640 assert!(effect.is_none());
8641 expect_status(&emitted[0], 2, Status::PROP_NOT_FOUND);
8642
8643 let raw = get(&mut session, prop::CAPS);
8644 let mut offset = 0;
8645 while offset < raw.len() {
8646 let (value, used) = pui::decode(&raw[offset..]).unwrap();
8647 assert_ne!(value, cap::ALERT);
8648 offset += used;
8649 }
8650 }
8651
8652 #[test]
8653 fn alert_is_not_saved() {
8654 let mut session = test_session();
8655 set_alert_at(&mut session, AlertState::Locate, 0);
8656 let mut buf = [0u8; SNAPSHOT_MAX];
8657 let len = session.encode_snapshot(&mut buf).unwrap();
8658
8659 let mut fresh: TestSession =
8662 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8663 fresh.attach(true);
8664 fresh.restore_at_boot(&buf[..len]).unwrap();
8665 assert_eq!(fresh.alert(), AlertState::None);
8666 assert_eq!(fresh.alert_deadline_ms(), None);
8667 }
8668
8669 #[test]
8670 fn battery_reads_still_sample_after_reset() {
8671 let mut session = test_session();
8672 let mut buf = [0u8; 16];
8673 let len = frame::reset(&mut buf, 0).unwrap();
8674 dispatch(&mut session, &buf[..len], 0);
8675
8676 let len = frame::prop_get(&mut buf, 5, prop::BATTERY).unwrap();
8679 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
8680 assert!(emitted.is_empty());
8681 assert_eq!(effect, Some(Effect::SampleBattery { tid: 5 }));
8682 }
8683
8684 fn publish(session: &mut TestSession, sample: BatteryStatus) -> (bool, Vec<Vec<u8>>) {
8686 let mut out = Vec::new();
8687 let published =
8688 session.publish_battery(sample, &mut |bytes: &[u8]| out.push(bytes.to_vec()));
8689 (published, out)
8690 }
8691
8692 fn matching_sample() -> BatteryStatus {
8693 BatteryStatus {
8694 voltage_mv: Some(3987),
8695 level_percent: None,
8696 charge_state: Some(battery::BatteryChargeState::Charging),
8697 }
8698 }
8699
8700 #[test]
8701 fn battery_publishes_unsolicited_snapshot() {
8702 let mut session = test_session();
8703 let (published, out) = publish(&mut session, matching_sample());
8704 assert!(published);
8705 let (tid, key, value) = parse_prop_is(&out[0]);
8706 assert_eq!(tid, TID_UNSOLICITED);
8708 assert_eq!(key, prop::BATTERY);
8709 assert_eq!(value, [0b101, 0x93, 0x0F, 1]);
8711 }
8712
8713 #[test]
8714 fn battery_publish_needs_an_attached_host() {
8715 let mut session = test_session();
8716 session.detach();
8717 let (published, out) = publish(&mut session, matching_sample());
8718 assert!(!published, "nobody to notify while detached");
8719 assert!(out.is_empty());
8720
8721 session.attach(true);
8723 assert!(publish(&mut session, matching_sample()).0);
8724 }
8725
8726 #[test]
8727 fn battery_publish_enforces_configured_fields() {
8728 let mut session = test_session();
8729 let (published, out) = publish(
8733 &mut session,
8734 BatteryStatus {
8735 voltage_mv: Some(4200),
8736 level_percent: Some(80),
8737 charge_state: Some(battery::BatteryChargeState::Charged),
8738 },
8739 );
8740 assert!(!published);
8741 assert!(out.is_empty());
8742
8743 let (published, out) = publish(
8748 &mut session,
8749 BatteryStatus {
8750 voltage_mv: Some(4200),
8751 level_percent: None,
8752 charge_state: None,
8753 },
8754 );
8755 assert!(published);
8756 let (_, key, value) = parse_prop_is(&out[0]);
8757 assert_eq!(key, prop::BATTERY);
8758 assert_eq!(
8759 BatteryStatus::decode(&value).unwrap().voltage_mv,
8760 Some(4200)
8761 );
8762 }
8763
8764 #[test]
8765 fn battery_publish_is_silent_without_the_capability() {
8766 let mut config = test_config();
8767 config.battery = None;
8768 let mut session: TestSession = Session::new(config, Status::RESET_POWER_ON, test_engine());
8769 session.attach(true);
8770
8771 let (published, out) = publish(&mut session, matching_sample());
8772 assert!(!published);
8773 assert!(out.is_empty());
8774 }
8775
8776 #[test]
8777 fn battery_publish_does_not_disturb_last_status_or_reads() {
8778 let mut session = test_session();
8779 publish(&mut session, matching_sample());
8782 let status = get(&mut session, prop::LAST_STATUS);
8783 assert_eq!(pui::decode(&status).unwrap().0, Status::RESET_POWER_ON.0);
8784
8785 let mut buf = [0u8; 16];
8787 let len = frame::prop_get(&mut buf, 3, prop::BATTERY).unwrap();
8788 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
8789 assert!(emitted.is_empty());
8790 assert_eq!(effect, Some(Effect::SampleBattery { tid: 3 }));
8791 }
8792
8793 #[test]
8794 fn pairing_pin_set_clear_validate_and_defer() {
8795 let mut session = test_session();
8796
8797 let (emitted, effect) = set(
8798 &mut session,
8799 prop::BLE_PAIRING_PIN,
8800 &123_456u32.to_le_bytes(),
8801 );
8802 assert!(
8803 emitted.is_empty(),
8804 "PIN must not be acknowledged before apply"
8805 );
8806 assert_eq!(
8807 effect,
8808 Some(Effect::SetPairingPin {
8809 tid: 2,
8810 pin: Some(123_456)
8811 })
8812 );
8813
8814 let (emitted, effect) = set(&mut session, prop::BLE_PAIRING_PIN, &[]);
8815 assert!(emitted.is_empty());
8816 assert_eq!(effect, Some(Effect::SetPairingPin { tid: 2, pin: None }));
8817
8818 for bad in [&1_000_000u32.to_le_bytes()[..], &[1, 2, 3][..]] {
8819 let (emitted, effect) = set(&mut session, prop::BLE_PAIRING_PIN, bad);
8820 assert!(effect.is_none());
8821 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
8822 }
8823 }
8824
8825 #[test]
8826 fn pairing_pin_completion_and_get_refusal() {
8827 let mut session = test_session();
8828 let mut emitted = Vec::new();
8829 session.respond_pin_set(7, Ok(()), &mut |frame| emitted.push(frame.to_vec()));
8830 expect_status(&emitted[0], 7, Status::OK);
8831 emitted.clear();
8832 session.respond_pin_set(6, Err(()), &mut |frame| emitted.push(frame.to_vec()));
8833 expect_status(&emitted[0], 6, Status::INTERNAL_ERROR);
8834
8835 let mut request = [0; 16];
8836 let len = frame::prop_get(&mut request, 5, prop::BLE_PAIRING_PIN).unwrap();
8837 let (emitted, effect) = dispatch(&mut session, &request[..len], 0);
8838 assert!(effect.is_none());
8839 expect_status(&emitted[0], 5, Status::UNIMPLEMENTED);
8840 }
8841
8842 #[test]
8843 fn reset_has_no_pairing_pin_effect() {
8844 let mut session = test_session();
8845 let mut request = [0; 4];
8846 let len = frame::reset(&mut request, 1).unwrap();
8847 let (_, effect) = dispatch(&mut session, &request[..len], 0);
8848 assert!(matches!(effect, Some(Effect::ApplyRadio(_))));
8849 }
8850
8851 #[test]
8852 fn transmit_lifecycle() {
8853 let mut session = test_session();
8854 enable(&mut session);
8855
8856 let packet = [0xAAu8; 32];
8857 let (emitted, effect) = send_packet(&mut session, 4, &packet, &[], 0);
8858 assert!(emitted.is_empty(), "no response until TX completes");
8859 assert_eq!(effect, Some(Effect::StartTransmit));
8860 assert_eq!(session.tx_data(), &packet);
8861 assert_eq!(session.tx_power(), TxPower::Default);
8862
8863 let (emitted, effect) = send_packet(&mut session, 5, &packet, &[], 0);
8865 assert!(effect.is_none());
8866 expect_status(&emitted[0], 5, Status::BUSY);
8867
8868 let mut emitted = Vec::new();
8870 session.on_tx_result(TxOutcome::Sent, 0, &mut |bytes: &[u8]| {
8871 emitted.push(bytes.to_vec())
8872 });
8873 expect_status(&emitted[0], 4, Status::OK);
8874 assert!(!session.has_pending_tx());
8875 let duty = get(&mut session, prop::PHY_DUTY_NOW);
8876 assert!(u16::from_le_bytes([duty[0], duty[1]]) > 0);
8877 }
8878
8879 #[test]
8880 fn target_selected_transmit_queue_pipelines_frames() {
8881 type PipelinedSession = Session<SoftwareAes, SoftwareSha256, 3>;
8882 let mut session: PipelinedSession =
8883 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
8884 session.attach(true);
8885 let mut request = [0u8; 16];
8886 let len = frame::prop_set(&mut request, 1, prop::PHY_ENABLED, &[1]).unwrap();
8887 let (_, effect) = dispatch(&mut session, &request[..len], 0);
8888 assert!(matches!(effect, Some(Effect::ApplyRadio(settings)) if settings.enabled));
8889
8890 for tid in 2..=4 {
8891 let (emitted, effect) = send_packet(&mut session, tid, &[tid; 8], &[], 0);
8892 assert!(emitted.is_empty());
8893 assert_eq!(effect, (tid == 2).then_some(Effect::StartTransmit));
8894 }
8895 let (emitted, effect) = send_packet(&mut session, 5, &[5; 8], &[], 0);
8896 assert!(effect.is_none());
8897 expect_status(&emitted[0], 5, Status::BUSY);
8898
8899 for tid in 2..=4 {
8900 assert_eq!(session.tx_data(), &[tid; 8]);
8901 let mut emitted = Vec::new();
8902 let effect = session.on_tx_result(TxOutcome::Sent, 0, &mut |bytes| {
8903 emitted.push(bytes.to_vec())
8904 });
8905 expect_status(&emitted[0], tid, Status::OK);
8906 assert_eq!(effect, (tid != 4).then_some(Effect::StartTransmit));
8907 }
8908 assert!(!session.has_pending_tx());
8909 }
8910
8911 #[test]
8912 fn transmit_requires_enabled_phy() {
8913 let mut session = test_session();
8914 let (emitted, effect) = send_packet(&mut session, 4, &[0u8; 8], &[], 0);
8915 assert!(effect.is_none());
8916 expect_status(&emitted[0], 4, Status::INVALID_STATE);
8917 }
8918
8919 #[test]
8920 fn transmit_power_override() {
8921 let mut session = test_session();
8922 enable(&mut session);
8923 let meta = [22u8, 0x00];
8924 let (_, effect) = send_packet(&mut session, 4, &[0u8; 8], &meta, 0);
8925 assert_eq!(effect, Some(Effect::StartTransmit));
8926 assert_eq!(session.tx_power(), TxPower::Dbm(22));
8927 }
8928
8929 #[test]
8930 fn duty_limit_blocks_and_noduty_bypasses() {
8931 let mut session = test_session();
8932 enable(&mut session);
8933 set(&mut session, prop::PHY_LORA_SF, &[12]);
8935 set(&mut session, prop::PHY_LORA_BW, &7_810u32.to_le_bytes());
8936 set(&mut session, prop::PHY_DUTY_LIMIT, &65u16.to_le_bytes());
8938
8939 let packet = [0u8; 255];
8940 let (emitted, effect) = send_packet(&mut session, 3, &packet, &[], 0);
8941 assert!(effect.is_none());
8942 expect_status(&emitted[0], 3, Status::DUTY_LIMIT);
8943
8944 let meta = [meta::TX_POWER_DEFAULT as u8, meta::TX_FLAG_NODUTY];
8946 let (_, effect) = send_packet(&mut session, 3, &packet, &meta, 0);
8947 assert_eq!(effect, Some(Effect::StartTransmit));
8948 }
8949
8950 #[test]
8951 fn nocca_flag_controls_channel_sensing() {
8952 let mut session = test_session();
8953 enable(&mut session);
8954
8955 let (_, effect) = send_packet(&mut session, 3, &[0u8; 8], &[], 0);
8957 assert_eq!(effect, Some(Effect::StartTransmit));
8958 assert!(!session.tx_nocca());
8959 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
8960
8961 let meta = [meta::TX_POWER_DEFAULT as u8, meta::TX_FLAG_NOCCA];
8963 let (_, effect) = send_packet(&mut session, 4, &[0u8; 8], &meta, 0);
8964 assert_eq!(effect, Some(Effect::StartTransmit));
8965 assert!(session.tx_nocca());
8966 }
8967
8968 #[test]
8969 fn channel_busy_completes_host_send_with_cca_failure() {
8970 let mut session = test_session();
8971 enable(&mut session);
8972
8973 let (_, effect) = send_packet(&mut session, 7, &[0u8; 8], &[], 0);
8974 assert_eq!(effect, Some(Effect::StartTransmit));
8975
8976 let mut emitted = Vec::new();
8979 session.on_tx_result(TxOutcome::ChannelBusy, 0, &mut |bytes: &[u8]| {
8980 emitted.push(bytes.to_vec())
8981 });
8982 expect_status(&emitted[0], 7, Status::CCA_FAILURE);
8983
8984 assert_eq!(get(&mut session, prop::PHY_DUTY_NOW), 0u16.to_le_bytes());
8986 }
8987
8988 #[test]
8989 fn delegated_ack_transmits_without_channel_sensing() {
8990 let mut session = auto_ack_session();
8994 let keys = test_pairwise();
8995 let effect = rx_effect(&mut session, &sealed_unar(5, &keys, false), 0);
8996 assert_eq!(effect, Some(Effect::StartTransmit));
8997 assert!(
8998 session.tx_nocca(),
8999 "delegated ack must skip the channel-activity check"
9000 );
9001 }
9002
9003 #[test]
9009 fn foreign_client_airtime_counts_against_the_session() {
9010 let config = test_config();
9011 let ledger = config.duty;
9012 let mut session = Session::new(config, Status::RESET_POWER_ON, test_engine());
9013 session.attach(true);
9014 enable(&mut session);
9015 set(&mut session, prop::PHY_DUTY_LIMIT, &655u16.to_le_bytes());
9016
9017 assert_eq!(get(&mut session, prop::PHY_DUTY_NOW), 0u16.to_le_bytes());
9018 for _ in 0..36 {
9020 ledger.record(0, 1_000);
9021 }
9022 let duty_now = get(&mut session, prop::PHY_DUTY_NOW);
9023 assert!(u16::from_le_bytes([duty_now[0], duty_now[1]]) >= 655);
9024
9025 let (emitted, effect) = send_packet(&mut session, 3, &[0u8; 32], &[], 0);
9026 assert!(effect.is_none());
9027 expect_status(&emitted[0], 3, Status::DUTY_LIMIT);
9028
9029 set(&mut session, prop::PHY_LORA_SF, &[12]);
9032 set(&mut session, prop::PHY_LORA_BW, &7_810u32.to_le_bytes());
9033 assert_eq!(
9034 ledger.airtime_ms(32),
9035 umsh_ulcp::airtime::lora_airtime_ms(12, 7_810, 5, 32)
9036 );
9037 }
9038
9039 #[test]
9040 fn fire_and_forget_failures_are_silent() {
9041 let mut session = test_session();
9042 let (emitted, effect) = send_packet(&mut session, 0, &[0u8; 4], &[], 0);
9044 assert!(effect.is_none());
9045 assert!(emitted.is_empty());
9046 assert_eq!(
9048 pui::decode(&get(&mut session, prop::LAST_STATUS))
9049 .unwrap()
9050 .0,
9051 Status::INVALID_STATE.0
9052 );
9053 }
9054
9055 #[test]
9056 fn radio_rx_emits_str_recv() {
9057 let mut session = test_session();
9058 enable(&mut session);
9059 let mut emitted = Vec::new();
9060 session.on_radio_rx(
9061 &[1, 2, 3],
9062 &RadioRxInfo::measured(-91, -53, None),
9063 0,
9064 &mut |bytes: &[u8]| emitted.push(bytes.to_vec()),
9065 );
9066 let parsed = Frame::parse(&emitted[0]).unwrap();
9067 assert_eq!(parsed.command(), Some(Cmd::StrRecv));
9068 assert_eq!(parsed.header.tid(), TID_UNSOLICITED);
9069 let payload = StreamPayload::parse(parsed.payload).unwrap();
9070 assert_eq!(payload.data, &[1, 2, 3]);
9071 let rx_meta = RxMeta::decode(payload.metadata).unwrap();
9072 assert_eq!(rx_meta.rssi_dbm, Some(-91));
9073 assert_eq!(rx_meta.snr_cb, Some(-53));
9074 }
9075
9076 #[test]
9077 fn radio_rx_suppressed_while_disabled() {
9078 let mut session = test_session();
9079 let mut emitted = Vec::new();
9080 session.on_radio_rx(
9081 &[1, 2, 3],
9082 &RadioRxInfo::measured(-91, -53, None),
9083 0,
9084 &mut |bytes: &[u8]| emitted.push(bytes.to_vec()),
9085 );
9086 assert!(emitted.is_empty());
9087 session.detach();
9089 session.on_radio_rx(
9090 &[1, 2, 3],
9091 &RadioRxInfo::measured(-91, -53, None),
9092 0,
9093 &mut |_: &[u8]| {},
9094 );
9095 session.attach(true);
9096 assert_eq!(
9097 get(&mut session, prop::HOST_RX_QUEUE_COUNT),
9098 0u16.to_le_bytes()
9099 );
9100 }
9101
9102 #[test]
9103 fn unknown_command_rejected() {
9104 let mut session = test_session();
9105 let (emitted, _) = dispatch(&mut session, &[0x81, 42], 0);
9106 expect_status(&emitted[0], 1, Status::INVALID_COMMAND);
9107 }
9108
9109 #[test]
9110 fn insert_remove_reject_per_property_knowledge() {
9111 let mut session = test_session();
9112 let mut buf = [0u8; 80];
9113
9114 for known in [
9116 prop::PHY_FREQ,
9117 prop::BLE_PAIRING_PIN,
9118 prop::CAPS,
9119 prop::MAC_REPEATER_ENABLED,
9120 prop::IDENT_ROLE,
9121 prop::IDENT_MOBILE,
9122 prop::IDENT_LOCATION,
9123 prop::IDENT_ALTITUDE,
9124 prop::DEV_DISCOVERABLE,
9125 prop::MAC_REPEATER_DEFAULT_REGION,
9128 prop::MAC_REPEATER_MIN_RSSI,
9129 prop::MAC_REPEATER_MIN_SNR,
9130 prop::ADVERT_INTERVAL,
9132 prop::BEACON_INTERVAL,
9133 prop::STARTUP_BEACON,
9134 ] {
9135 let len = frame::prop_insert(&mut buf, 1, known, &[0; 4]).unwrap();
9136 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
9137 assert!(effect.is_none());
9138 expect_status(&emitted[0], 1, Status::INVALID_ARGUMENT);
9139 }
9140 for unknown in [85, 1_234] {
9143 let len = frame::prop_remove(&mut buf, 2, unknown, &[0; 4]).unwrap();
9144 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
9145 assert!(effect.is_none());
9146 expect_status(&emitted[0], 2, Status::PROP_NOT_FOUND);
9147 }
9148 let (emitted, _) = dispatch(&mut session, &[0x81, Cmd::PropInsert as u8], 0);
9150 expect_status(&emitted[0], 1, Status::PARSE_ERROR);
9151 }
9152
9153 #[test]
9154 fn clear_defers_and_leaves_live_state_alone() {
9155 let mut session = test_session();
9156 let mut buf = [0u8; 8];
9157 set(&mut session, prop::DEV_NAME, b"kept name");
9161 let len = frame::clear(&mut buf, 4).unwrap();
9162 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
9163 assert!(emitted.is_empty(), "no response before the erase commits");
9164 assert_eq!(effect, Some(Effect::ClearSaved { tid: 4 }));
9165 let mut emitted = Vec::new();
9166 session.respond_clear(4, Ok(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
9167 expect_status(&emitted[0], 4, Status::OK);
9168 assert_eq!(session.device_name(), "kept name");
9169
9170 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
9172 assert_eq!(effect, Some(Effect::ClearSaved { tid: 4 }));
9173 let mut emitted = Vec::new();
9174 session.respond_clear(4, Err(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
9175 expect_status(&emitted[0], 4, Status::FAILURE);
9176 }
9177
9178 #[test]
9179 fn device_only_notifications_rejected_from_host() {
9180 let mut session = test_session();
9181 for cmd in [
9182 Cmd::PropInserted,
9183 Cmd::PropRemoved,
9184 Cmd::PropIs,
9185 Cmd::StrRecv,
9186 ] {
9187 let (emitted, effect) = dispatch(&mut session, &[0x81, cmd as u8], 0);
9188 assert!(effect.is_none());
9189 expect_status(&emitted[0], 1, Status::INVALID_COMMAND);
9190 }
9191 }
9192
9193 #[test]
9194 fn malformed_frames_ignored() {
9195 let mut session = test_session();
9196 for bad in [&[][..], &[0x81][..], &[0x00, 0x00][..], &[0xB8, 0x00][..]] {
9197 let (emitted, effect) = dispatch(&mut session, bad, 0);
9198 assert!(emitted.is_empty());
9199 assert!(effect.is_none());
9200 }
9201 }
9202
9203 use umsh_core::{ChannelId, NodeHint, PacketBuilder};
9206
9207 fn unicast_to(dst: [u8; 3]) -> Vec<u8> {
9208 let mut buf = [0u8; 64];
9209 PacketBuilder::new(&mut buf)
9210 .unicast(NodeHint(dst))
9211 .source_hint(NodeHint([9, 9, 9]))
9212 .frame_counter(1)
9213 .payload(&[1, 2, 3])
9214 .build()
9215 .unwrap()
9216 .as_bytes()
9217 .to_vec()
9218 }
9219
9220 fn multicast_on(channel: [u8; 2]) -> Vec<u8> {
9221 let mut buf = [0u8; 64];
9222 PacketBuilder::new(&mut buf)
9223 .multicast(ChannelId(channel))
9224 .source_hint(NodeHint([9, 9, 9]))
9225 .frame_counter(1)
9226 .payload(&[1, 2, 3])
9227 .build()
9228 .unwrap()
9229 .as_bytes()
9230 .to_vec()
9231 }
9232
9233 fn blind_unicast_on(channel: [u8; 2]) -> Vec<u8> {
9234 let mut buf = [0u8; 96];
9235 PacketBuilder::new(&mut buf)
9236 .blind_unicast(ChannelId(channel), NodeHint([7, 7, 7]))
9237 .source_hint(NodeHint([9, 9, 9]))
9238 .frame_counter(1)
9239 .payload(&[1, 2, 3])
9240 .build()
9241 .unwrap()
9242 .as_bytes()
9243 .to_vec()
9244 }
9245
9246 fn broadcast_frame() -> Vec<u8> {
9247 let mut buf = [0u8; 64];
9248 PacketBuilder::new(&mut buf)
9249 .broadcast()
9250 .source_hint(NodeHint([9, 9, 9]))
9251 .payload(&[1, 2, 3])
9252 .build()
9253 .unwrap()
9254 .to_vec()
9255 }
9256
9257 fn mac_ack_with_mic(ack_mic: [u8; 4]) -> Vec<u8> {
9258 let mut trailer = [0u8; 8];
9259 trailer[..4].copy_from_slice(&ack_mic);
9260 trailer[4..].copy_from_slice(&[0x5A; 4]); let mut buf = [0u8; 32];
9262 PacketBuilder::new(&mut buf)
9263 .mac_ack(trailer)
9264 .build()
9265 .unwrap()
9266 .to_vec()
9267 }
9268
9269 fn delivered(session: &mut TestSession, frame: &[u8]) -> bool {
9271 delivered_at(session, frame, 0)
9272 }
9273
9274 fn delivered_at(session: &mut TestSession, frame: &[u8], now_ms: u64) -> bool {
9275 let mut emitted = Vec::new();
9276 session.on_radio_rx(
9277 frame,
9278 &RadioRxInfo::measured(-80, 40, None),
9279 now_ms,
9280 &mut |bytes: &[u8]| emitted.push(bytes.to_vec()),
9281 );
9282 !emitted.is_empty()
9283 }
9284
9285 fn insert_item(
9286 session: &mut TestSession,
9287 key: u32,
9288 item: &[u8],
9289 ) -> (Vec<Vec<u8>>, Option<Effect>) {
9290 let mut buf = [0u8; 128];
9291 let len = frame::prop_insert(&mut buf, 5, key, item).unwrap();
9292 dispatch(session, &buf[..len], 0)
9293 }
9294
9295 fn remove_item(
9296 session: &mut TestSession,
9297 key: u32,
9298 item: &[u8],
9299 ) -> (Vec<Vec<u8>>, Option<Effect>) {
9300 let mut buf = [0u8; 128];
9301 let len = frame::prop_remove(&mut buf, 6, key, item).unwrap();
9302 dispatch(session, &buf[..len], 0)
9303 }
9304
9305 fn install_host_key(session: &mut TestSession, key: &[u8; 32]) {
9307 let (emitted, effect) = set(session, prop::HOST_KEY, key);
9308 assert!(effect.is_none(), "host replacement needs no durable step");
9309 let (_, response_key, value) = parse_prop_is(&emitted[0]);
9310 assert_eq!(response_key, prop::HOST_KEY);
9311 assert_eq!(value, key);
9312 }
9313
9314 fn parse_table_notice(bytes: &[u8], expected: Cmd, tid: u8) -> (u32, Vec<u8>) {
9316 let parsed = Frame::parse(bytes).unwrap();
9317 assert_eq!(parsed.command(), Some(expected));
9318 assert_eq!(parsed.header.tid(), tid);
9319 let payload = PropPayload::parse(parsed.payload).unwrap();
9320 (payload.key, payload.value.to_vec())
9321 }
9322
9323 #[test]
9324 fn factory_state_accepts_everything() {
9325 let mut session = test_session();
9326 enable(&mut session);
9327 assert!(delivered(&mut session, &unicast_to([1, 2, 3])));
9330 assert!(delivered(&mut session, &broadcast_frame()));
9331 assert!(delivered(&mut session, &[0x00, 0x01, 0x02]));
9332 }
9333
9334 #[test]
9335 fn host_key_round_trip_and_implicit_dest_filter() {
9336 let mut session = test_session();
9337 enable(&mut session);
9338 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
9339
9340 let key = [0xC4; 32];
9341 install_host_key(&mut session, &key);
9342 assert_eq!(get(&mut session, prop::HOST_KEY), key);
9343
9344 assert!(delivered(&mut session, &unicast_to([0xC4, 0xC4, 0xC4])));
9350 assert!(!delivered(
9351 &mut session,
9352 &mac_ack_with_mic([0xC4, 0xC4, 0xC4, 0xC4])
9353 ));
9354 assert!(!delivered(&mut session, &unicast_to([1, 2, 3])));
9355 assert!(delivered(&mut session, &broadcast_frame()));
9357 assert!(!delivered(&mut session, &[0x00, 0x01, 0x02]));
9358 }
9359
9360 #[test]
9361 fn mac_ack_accepted_only_when_expected() {
9362 let mut session = test_session();
9363 enable(&mut session);
9364 install_host_key(&mut session, &[0xC4; 32]); assert!(!delivered(
9368 &mut session,
9369 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
9370 ));
9371
9372 let frame = sealed_unar(7, &test_pairwise(), false);
9374 let header = PacketHeader::parse(&frame).unwrap();
9375 let mic = &frame[header.mic_range.clone()];
9376 let ack_mic = [mic[0], mic[1], mic[2], mic[3]];
9377 let (_emitted, effect) = send_packet(&mut session, 4, &frame, &[], 0);
9378 assert_eq!(effect, Some(Effect::StartTransmit));
9379 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
9380
9381 assert!(delivered(&mut session, &mac_ack_with_mic(ack_mic)));
9383 assert!(delivered(&mut session, &mac_ack_with_mic(ack_mic)));
9386 assert!(!delivered(
9388 &mut session,
9389 &mac_ack_with_mic([0x99, 0x88, 0x77, 0x66])
9390 ));
9391 }
9392
9393 #[test]
9400 fn repeat_of_a_transmitted_frame_passes_the_filter() {
9401 let mut session = test_session();
9402 enable(&mut session);
9403 install_host_key(&mut session, &HOST_PUB);
9404
9405 let mut buf = [0u8; 96];
9407 let mut packet = PacketBuilder::new(&mut buf)
9408 .unicast(NodeHint([PEER_PUB[0], PEER_PUB[1], PEER_PUB[2]]))
9409 .source_hint(NodeHint([HOST_PUB[0], HOST_PUB[1], HOST_PUB[2]]))
9410 .frame_counter(9)
9411 .flood_hops(5)
9412 .mic_size(MicSize::Mic8)
9413 .payload(&[4, 5, 6])
9414 .build()
9415 .unwrap();
9416 test_engine()
9417 .seal_packet(&mut packet, &test_pairwise())
9418 .unwrap();
9419 let frame = packet.as_bytes().to_vec();
9420
9421 let header = PacketHeader::parse(&frame).unwrap();
9424 let mut repeat = frame.clone();
9425 repeat[1] = header.flood_hops.unwrap().decremented().0;
9426 assert_ne!(repeat, frame);
9427
9428 assert!(!delivered(&mut session, &repeat));
9431
9432 let (_emitted, effect) = send_packet(&mut session, 4, &frame, &[], 0);
9433 assert_eq!(effect, Some(Effect::StartTransmit));
9434 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
9435
9436 assert!(delivered(&mut session, &repeat));
9440 assert!(delivered(&mut session, &repeat));
9441 }
9442
9443 #[test]
9444 fn explicit_pkt_type_filter_accepts_unexpected_mac_ack() {
9445 let mut session = test_session();
9449 enable(&mut session);
9450 install_host_key(&mut session, &[0xC4; 32]); assert!(!delivered(
9454 &mut session,
9455 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
9456 ));
9457
9458 insert_item(
9460 &mut session,
9461 prop::HOST_RX_FILTERS,
9462 &[items::FILTER_PKT_TYPE, PacketType::MacAck as u8],
9463 );
9464
9465 assert!(delivered(
9467 &mut session,
9468 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
9469 ));
9470 }
9471
9472 #[test]
9473 fn host_key_rejects_bad_lengths() {
9474 let mut session = test_session();
9475 for bad in [&[0u8; 31][..], &[0u8; 33][..], &[1u8][..]] {
9476 let (emitted, effect) = set(&mut session, prop::HOST_KEY, bad);
9477 assert!(effect.is_none());
9478 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
9479 }
9480 }
9481
9482 #[test]
9483 fn host_key_set_is_idempotent_for_current_value() {
9484 let mut session = test_session();
9485 let (emitted, effect) = set(&mut session, prop::HOST_KEY, &[]);
9487 assert!(effect.is_none());
9488 let (_, key, value) = parse_prop_is(&emitted[0]);
9489 assert_eq!(key, prop::HOST_KEY);
9490 assert!(value.is_empty());
9491
9492 let host_key = [0xC4; 32];
9493 install_host_key(&mut session, &host_key);
9494 insert_item(
9495 &mut session,
9496 prop::HOST_RX_FILTERS,
9497 &[items::FILTER_PKT_TYPE, 0],
9498 );
9499
9500 let (emitted, effect) = set(&mut session, prop::HOST_KEY, &host_key);
9502 assert!(effect.is_none());
9503 let (_, key, value) = parse_prop_is(&emitted[0]);
9504 assert_eq!(key, prop::HOST_KEY);
9505 assert_eq!(value, host_key);
9506 assert!(!get(&mut session, prop::HOST_RX_FILTERS).is_empty());
9507 }
9508
9509 #[test]
9513 fn host_replacement_clears_the_host_domain_immediately() {
9514 let mut session = test_session();
9515 install_host_key(&mut session, &[0xAA; 32]);
9516 insert_item(
9517 &mut session,
9518 prop::HOST_RX_FILTERS,
9519 &[items::FILTER_PKT_TYPE, 0],
9520 );
9521
9522 install_host_key(&mut session, &[0xBB; 32]);
9523 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
9524
9525 insert_item(
9527 &mut session,
9528 prop::HOST_RX_FILTERS,
9529 &[items::FILTER_PKT_TYPE, 0],
9530 );
9531 let (emitted, effect) = set(&mut session, prop::HOST_KEY, &[]);
9532 assert!(effect.is_none());
9533 let (_, key, value) = parse_prop_is(&emitted[0]);
9534 assert_eq!(key, prop::HOST_KEY);
9535 assert!(value.is_empty());
9536 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
9537 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
9538 }
9539
9540 #[test]
9541 fn cmd_rst_clears_host_domain() {
9542 let mut session = test_session();
9543 install_host_key(&mut session, &[0xAA; 32]);
9544 insert_item(
9545 &mut session,
9546 prop::HOST_RX_FILTERS,
9547 &[items::FILTER_PKT_TYPE, 0],
9548 );
9549 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_| {});
9550 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
9551 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
9552 }
9553
9554 #[test]
9555 fn filter_insert_remove_lifecycle() {
9556 let mut session = test_session();
9557 let item = [items::FILTER_DEST_HINT, 0x11, 0x22, 0x33];
9558
9559 let (emitted, effect) = insert_item(&mut session, prop::HOST_RX_FILTERS, &item);
9560 assert!(effect.is_none());
9561 let (key, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
9562 assert_eq!(key, prop::HOST_RX_FILTERS);
9563 assert_eq!(digest, item);
9564
9565 let (emitted, _) = insert_item(&mut session, prop::HOST_RX_FILTERS, &item);
9567 expect_status(&emitted[0], 5, Status::ALREADY);
9568
9569 let table = get(&mut session, prop::HOST_RX_FILTERS);
9571 assert_eq!(table, [&[4u8][..], &item[..]].concat());
9572
9573 let (emitted, _) = remove_item(&mut session, prop::HOST_RX_FILTERS, &item);
9574 let (key, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
9575 assert_eq!(key, prop::HOST_RX_FILTERS);
9576 assert_eq!(digest, item);
9577 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
9578
9579 let (emitted, _) = remove_item(&mut session, prop::HOST_RX_FILTERS, &item);
9581 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
9582 }
9583
9584 #[test]
9585 fn filter_insert_rejects_invalid_entries() {
9586 let mut session = test_session();
9587 for bad in [
9588 &[][..], &[3, 0][..], &[items::FILTER_DEST_HINT, 1, 2][..], &[items::FILTER_CHANNEL_ID, 1, 2, 3][..], &[items::FILTER_PKT_TYPE, 8][..], ] {
9594 let (emitted, effect) = insert_item(&mut session, prop::HOST_RX_FILTERS, bad);
9595 assert!(effect.is_none());
9596 expect_status(&emitted[0], 5, Status::INVALID_ARGUMENT);
9597 }
9598 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
9599 }
9600
9601 #[test]
9602 fn filter_table_capacity_is_bounded() {
9603 let mut session = test_session();
9604 for index in 0..MAX_RX_FILTERS as u8 {
9605 let (emitted, _) = insert_item(
9606 &mut session,
9607 prop::HOST_RX_FILTERS,
9608 &[items::FILTER_DEST_HINT, index, 0, 0],
9609 );
9610 parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
9611 }
9612 let (emitted, _) = insert_item(
9613 &mut session,
9614 prop::HOST_RX_FILTERS,
9615 &[items::FILTER_DEST_HINT, 0xFF, 0, 0],
9616 );
9617 expect_status(&emitted[0], 5, Status::NOMEM);
9618 }
9619
9620 #[test]
9621 fn whole_table_set_is_atomic() {
9622 let mut session = test_session();
9623 let good_a = [items::FILTER_DEST_HINT, 1, 2, 3];
9624 let good_b = [items::FILTER_PKT_TYPE, 0];
9625
9626 let mut table = Vec::new();
9627 for item in [&good_a[..], &good_b[..]] {
9628 table.push(item.len() as u8);
9629 table.extend_from_slice(item);
9630 }
9631 let (emitted, effect) = set(&mut session, prop::HOST_RX_FILTERS, &table);
9632 assert!(effect.is_none());
9633 let (_, key, value) = parse_prop_is(&emitted[0]);
9634 assert_eq!(key, prop::HOST_RX_FILTERS);
9635 assert_eq!(value, table);
9636
9637 let mut bad_table = table.clone();
9640 bad_table.extend_from_slice(&[2, 3, 0]); let (emitted, _) = set(&mut session, prop::HOST_RX_FILTERS, &bad_table);
9642 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
9643 assert_eq!(get(&mut session, prop::HOST_RX_FILTERS), table);
9644
9645 let (emitted, _) = set(&mut session, prop::HOST_RX_FILTERS, &[9, 1]);
9647 expect_status(&emitted[0], 2, Status::PARSE_ERROR);
9648 assert_eq!(get(&mut session, prop::HOST_RX_FILTERS), table);
9649
9650 let mut doubled = table.clone();
9652 doubled.extend_from_slice(&table);
9653 let (emitted, _) = set(&mut session, prop::HOST_RX_FILTERS, &doubled);
9654 let (_, _, value) = parse_prop_is(&emitted[0]);
9655 assert_eq!(value, table);
9656
9657 let (emitted, _) = set(&mut session, prop::HOST_RX_FILTERS, &[]);
9659 let (_, _, value) = parse_prop_is(&emitted[0]);
9660 assert!(value.is_empty());
9661 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
9662 }
9663
9664 #[test]
9665 fn explicit_filters_match_each_type() {
9666 let mut session = test_session();
9667 enable(&mut session);
9668
9669 insert_item(
9671 &mut session,
9672 prop::HOST_RX_FILTERS,
9673 &[items::FILTER_DEST_HINT, 0x11, 0x22, 0x33],
9674 );
9675 assert!(delivered(&mut session, &unicast_to([0x11, 0x22, 0x33])));
9676 assert!(!delivered(
9678 &mut session,
9679 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
9680 ));
9681 assert!(!delivered(&mut session, &unicast_to([4, 5, 6])));
9682 assert!(delivered(&mut session, &broadcast_frame()));
9685
9686 insert_item(
9689 &mut session,
9690 prop::HOST_RX_FILTERS,
9691 &[items::FILTER_CHANNEL_ID, 0xAB, 0xCD],
9692 );
9693 assert!(delivered(&mut session, &multicast_on([0xAB, 0xCD])));
9694 assert!(delivered(&mut session, &blind_unicast_on([0xAB, 0xCD])));
9695 assert!(!delivered(&mut session, &multicast_on([0x00, 0x01])));
9696
9697 insert_item(
9700 &mut session,
9701 prop::HOST_RX_FILTERS,
9702 &[items::FILTER_PKT_TYPE, PacketType::MacAck as u8],
9703 );
9704 assert!(delivered(
9705 &mut session,
9706 &mac_ack_with_mic([0x11, 0x22, 0x33, 0x44])
9707 ));
9708 assert!(!delivered(&mut session, &unicast_to([4, 5, 6])));
9710 assert!(!delivered(&mut session, &[0x00, 0x01, 0x02]));
9711 }
9712
9713 #[test]
9714 fn promiscuous_bypasses_filtering_for_live_delivery() {
9715 let mut session = test_session();
9716 enable(&mut session);
9717 insert_item(
9718 &mut session,
9719 prop::HOST_RX_FILTERS,
9720 &[items::FILTER_DEST_HINT, 0x11, 0x22, 0x33],
9721 );
9722 assert!(!delivered(&mut session, &unicast_to([4, 5, 6])));
9723
9724 set(&mut session, prop::MAC_PROMISCUOUS, &[1]);
9725 assert!(delivered(&mut session, &unicast_to([4, 5, 6])));
9726 assert!(delivered(&mut session, &[0x00, 0x01, 0x02]));
9727
9728 session.attach(true);
9730 assert!(!delivered(&mut session, &unicast_to([4, 5, 6])));
9731 }
9732
9733 #[test]
9734 fn filters_survive_attach() {
9735 let mut session = test_session();
9736 enable(&mut session);
9737 install_host_key(&mut session, &[0xC4; 32]);
9738 insert_item(
9739 &mut session,
9740 prop::HOST_RX_FILTERS,
9741 &[items::FILTER_PKT_TYPE, 0],
9742 );
9743 session.attach(true);
9744 assert_eq!(get(&mut session, prop::HOST_KEY), [0xC4; 32]);
9745 assert!(delivered(&mut session, &broadcast_frame()));
9746 assert!(delivered(&mut session, &unicast_to([0xC4, 0xC4, 0xC4])));
9747 assert!(!delivered(&mut session, &unicast_to([1, 2, 3])));
9748 }
9749
9750 #[test]
9751 fn host_key_insert_remove_is_invalid_argument() {
9752 let mut session = test_session();
9753 let (emitted, _) = insert_item(&mut session, prop::HOST_KEY, &[0; 32]);
9754 expect_status(&emitted[0], 5, Status::INVALID_ARGUMENT);
9755 let (emitted, _) = remove_item(&mut session, prop::HOST_KEY, &[0; 32]);
9756 expect_status(&emitted[0], 6, Status::INVALID_ARGUMENT);
9757 }
9758
9759 fn receive_detached(session: &mut TestSession, frame: &[u8], now_ms: u64) {
9764 assert!(!delivered_at(session, frame, now_ms));
9765 }
9766
9767 fn queue_count(session: &mut TestSession) -> u16 {
9768 let raw = get(session, prop::HOST_RX_QUEUE_COUNT);
9769 u16::from_le_bytes([raw[0], raw[1]])
9770 }
9771
9772 fn drain(session: &mut TestSession, now_ms: u64) -> Vec<(Vec<u8>, BufferedRxMeta)> {
9775 let mut buf = [0u8; 4];
9776 let len = frame::queue_drain(&mut buf, 7).unwrap();
9777 let (emitted, effect) = dispatch(session, &buf[..len], now_ms);
9778 if effect.is_none() {
9779 expect_status(&emitted[0], 7, Status::OK);
9781 return Vec::new();
9782 }
9783 assert_eq!(effect, Some(Effect::DrainQueue));
9784 assert!(emitted.is_empty());
9785 let mut steps = Vec::new();
9786 loop {
9787 let mut emitted = Vec::new();
9788 let more = session.drain_step(now_ms, &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
9789 assert_eq!(emitted.len(), 1, "each step emits exactly one frame");
9790 if !more {
9791 expect_status(&emitted[0], 7, Status::OK);
9792 return steps;
9793 }
9794 let parsed = Frame::parse(&emitted[0]).unwrap();
9795 assert_eq!(parsed.command(), Some(Cmd::StrRecv));
9796 let payload = StreamPayload::parse(parsed.payload).unwrap();
9797 steps.push((
9798 payload.data.to_vec(),
9799 BufferedRxMeta::decode(payload.metadata).unwrap(),
9800 ));
9801 }
9802 }
9803
9804 #[test]
9805 fn detached_receive_then_attach_count_drain() {
9806 let mut session = test_session();
9807 enable(&mut session);
9808 session.detach();
9809 receive_detached(&mut session, &unicast_to([1, 2, 3]), 1_000);
9810 receive_detached(&mut session, &broadcast_frame(), 3_000);
9811
9812 session.attach(true);
9815 assert_eq!(queue_count(&mut session), 2);
9816 assert!(delivered(&mut session, &unicast_to([7, 7, 7])));
9817 assert_eq!(queue_count(&mut session), 2);
9818
9819 let drained = drain(&mut session, 8_000);
9820 assert_eq!(drained.len(), 2);
9821 assert_eq!(drained[0].0, unicast_to([1, 2, 3]));
9824 assert_eq!(drained[1].0, broadcast_frame());
9825 for (_, meta) in &drained {
9826 assert_eq!(meta.flags, RX_FLAG_BUFFERED);
9827 assert_eq!(meta.rx.rssi_dbm, Some(-80));
9828 assert_eq!(meta.rx.snr_cb, Some(40));
9829 }
9830 assert_eq!((drained[0].1.age_s, drained[1].1.age_s), (7, 5));
9831
9832 assert_eq!(queue_count(&mut session), 0);
9833 assert!(drain(&mut session, 9_000).is_empty());
9835 }
9836
9837 #[test]
9838 fn queue_overflow_evicts_oldest_and_counts_dropped() {
9839 let mut session = test_session();
9840 enable(&mut session);
9841 session.detach();
9842 for index in 0..(RX_QUEUE_CAPACITY + 3) as u8 {
9844 receive_detached(&mut session, &unicast_to([index, 0, 0]), 0);
9845 }
9846 session.attach(true);
9847 assert_eq!(queue_count(&mut session), RX_QUEUE_CAPACITY as u16);
9848 assert_eq!(
9849 get(&mut session, prop::HOST_RX_QUEUE_DROPPED),
9850 3u32.to_le_bytes()
9851 );
9852 let drained = drain(&mut session, 0);
9853 assert_eq!(drained[0].0, unicast_to([3, 0, 0]));
9854 assert_eq!(
9855 drained.last().unwrap().0,
9856 unicast_to([(RX_QUEUE_CAPACITY + 2) as u8, 0, 0])
9857 );
9858 }
9859
9860 #[test]
9861 fn queue_respects_receive_filtering() {
9862 let mut session = test_session();
9863 enable(&mut session);
9864 insert_item(
9865 &mut session,
9866 prop::HOST_RX_FILTERS,
9867 &[items::FILTER_DEST_HINT, 0x11, 0x22, 0x33],
9868 );
9869 session.detach();
9870 receive_detached(&mut session, &unicast_to([0x11, 0x22, 0x33]), 0);
9871 receive_detached(&mut session, &unicast_to([4, 5, 6]), 0); receive_detached(&mut session, &[0xFF, 0xFE], 0); session.attach(true);
9874 assert_eq!(queue_count(&mut session), 1);
9875 }
9876
9877 #[test]
9878 fn broadcasts_are_implicit_live_but_follow_filters_when_queued() {
9879 let mut session = test_session();
9880 enable(&mut session);
9881 install_host_key(&mut session, &[0xC4; 32]);
9885 assert!(delivered(&mut session, &broadcast_frame()));
9886 session.detach();
9889 receive_detached(&mut session, &broadcast_frame(), 0);
9890 session.attach(true);
9891 assert_eq!(queue_count(&mut session), 0);
9892 }
9893
9894 #[test]
9895 fn unauthenticated_duplicates_occupy_separate_entries() {
9896 let mut session = test_session();
9899 enable(&mut session);
9900 session.detach();
9901 let frame = unicast_to([1, 2, 3]);
9902 receive_detached(&mut session, &frame, 0);
9903 receive_detached(&mut session, &frame, 0);
9904 session.attach(true);
9905 assert_eq!(queue_count(&mut session), 2);
9906 }
9907
9908 #[test]
9909 fn live_arrivals_interleave_with_a_drain() {
9910 let mut session = test_session();
9911 enable(&mut session);
9912 session.detach();
9913 receive_detached(&mut session, &unicast_to([1, 0, 0]), 0);
9914 receive_detached(&mut session, &unicast_to([2, 0, 0]), 0);
9915 session.attach(true);
9916
9917 let mut buf = [0u8; 4];
9918 let len = frame::queue_drain(&mut buf, 7).unwrap();
9919 let (_, effect) = dispatch(&mut session, &buf[..len], 10_000);
9920 assert_eq!(effect, Some(Effect::DrainQueue));
9921
9922 let mut emitted = Vec::new();
9924 assert!(session.drain_step(10_000, &mut |bytes: &[u8]| emitted.push(bytes.to_vec())));
9925
9926 assert!(delivered_at(&mut session, &unicast_to([3, 0, 0]), 10_000));
9929
9930 let mut frames = 0;
9932 loop {
9933 let mut emitted = Vec::new();
9934 let more = session.drain_step(10_000, &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
9935 if !more {
9936 expect_status(&emitted[0], 7, Status::OK);
9937 break;
9938 }
9939 frames += 1;
9940 }
9941 assert_eq!(frames, 1);
9942 assert_eq!(queue_count(&mut session), 0);
9943 }
9944
9945 #[test]
9946 fn second_drain_while_in_progress_is_busy() {
9947 let mut session = test_session();
9948 enable(&mut session);
9949 session.detach();
9950 receive_detached(&mut session, &unicast_to([1, 0, 0]), 0);
9951 session.attach(true);
9952
9953 let mut buf = [0u8; 4];
9954 let len = frame::queue_drain(&mut buf, 7).unwrap();
9955 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
9956 assert_eq!(effect, Some(Effect::DrainQueue));
9957
9958 let len = frame::queue_drain(&mut buf, 6).unwrap();
9959 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
9960 assert!(effect.is_none());
9961 expect_status(&emitted[0], 6, Status::BUSY);
9962 }
9963
9964 #[test]
9965 fn reset_and_host_replacement_discard_the_queue() {
9966 let mut session = test_session();
9967 enable(&mut session);
9968 session.detach();
9969 for _ in 0..(RX_QUEUE_CAPACITY + 1) {
9970 receive_detached(&mut session, &unicast_to([1, 2, 3]), 0);
9971 }
9972 session.attach(true);
9973 assert_ne!(queue_count(&mut session), 0);
9974
9975 install_host_key(&mut session, &[0xAA; 32]);
9978 assert_eq!(queue_count(&mut session), 0);
9979 assert_eq!(
9980 get(&mut session, prop::HOST_RX_QUEUE_DROPPED),
9981 0u32.to_le_bytes()
9982 );
9983
9984 enable(&mut session);
9987 session.detach();
9988 receive_detached(&mut session, &unicast_to([0xAA, 0xAA, 0xAA]), 0);
9989 session.attach(true);
9990 assert_eq!(queue_count(&mut session), 1);
9991 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_| {});
9992 assert_eq!(queue_count(&mut session), 0);
9993 }
9994
9995 fn install_channel_key(session: &mut TestSession, key: &[u8; 32]) -> [u8; 2] {
9999 let (emitted, effect) = insert_item(session, prop::HOST_CHANNEL_KEYS, key);
10000 assert!(effect.is_none());
10001 let (prop_key, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
10002 assert_eq!(prop_key, prop::HOST_CHANNEL_KEYS);
10003 digest.try_into().expect("channel digest is 2 bytes")
10004 }
10005
10006 fn peer_entry(seed: u8) -> [u8; 96] {
10007 let mut item = [0u8; 96];
10008 item[..32].fill(seed);
10009 item[32..64].fill(0xE0 | (seed & 0x0F));
10010 item[64..].fill(0x50 | (seed & 0x0F));
10011 item
10012 }
10013
10014 #[test]
10015 fn channel_key_lifecycle_and_digest_is_derived_id() {
10016 let mut session = test_session();
10017 let key = [0x42; 32];
10018 let expected_id = test_engine().derive_channel_id(&ChannelKey(key)).0;
10019
10020 let digest = install_channel_key(&mut session, &key);
10021 assert_eq!(digest, expected_id);
10022 assert_eq!(get(&mut session, prop::HOST_CHANNEL_KEYS), expected_id);
10023
10024 let (emitted, _) = insert_item(&mut session, prop::HOST_CHANNEL_KEYS, &key);
10026 expect_status(&emitted[0], 5, Status::ALREADY);
10027
10028 let (emitted, _) = remove_item(&mut session, prop::HOST_CHANNEL_KEYS, &key);
10030 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
10031 assert_eq!(digest, expected_id);
10032 assert!(get(&mut session, prop::HOST_CHANNEL_KEYS).is_empty());
10033
10034 let (emitted, _) = remove_item(&mut session, prop::HOST_CHANNEL_KEYS, &key);
10035 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
10036
10037 for bad in [&[0u8; 31][..], &[0u8; 33][..], &[][..]] {
10039 let (emitted, _) = insert_item(&mut session, prop::HOST_CHANNEL_KEYS, bad);
10040 expect_status(&emitted[0], 5, Status::INVALID_ARGUMENT);
10041 }
10042 }
10043
10044 #[test]
10045 fn channel_key_capacity_is_bounded() {
10046 let mut session = test_session();
10047 for seed in 0..MAX_CHANNEL_KEYS as u8 {
10048 install_channel_key(&mut session, &[seed; 32]);
10049 }
10050 let (emitted, _) = insert_item(&mut session, prop::HOST_CHANNEL_KEYS, &[0xFF; 32]);
10051 expect_status(&emitted[0], 5, Status::NOMEM);
10052 }
10053
10054 #[test]
10055 fn peer_key_lifecycle_replacement_and_secret_free_digests() {
10056 let mut session = test_session();
10057 let entry = peer_entry(0xA1);
10058
10059 let (emitted, _) = insert_item(&mut session, prop::HOST_PEER_KEYS, &entry);
10060 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
10061 assert_eq!(digest, entry[..32]);
10062 for frame in &emitted {
10064 assert!(!frame.windows(32).any(|window| window == &entry[32..64]));
10065 assert!(!frame.windows(32).any(|window| window == &entry[64..]));
10066 }
10067
10068 assert_eq!(get(&mut session, prop::HOST_PEER_KEYS), entry[..32]);
10070
10071 let mut replacement = entry;
10074 replacement[32..].fill(0x77);
10075 let (emitted, _) = insert_item(&mut session, prop::HOST_PEER_KEYS, &replacement);
10076 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
10077 assert_eq!(digest, entry[..32]);
10078 assert_eq!(get(&mut session, prop::HOST_PEER_KEYS), entry[..32]);
10079
10080 let (emitted, _) = remove_item(&mut session, prop::HOST_PEER_KEYS, &entry[..32]);
10082 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
10083 assert_eq!(digest, entry[..32]);
10084 assert!(get(&mut session, prop::HOST_PEER_KEYS).is_empty());
10085
10086 let (emitted, _) = remove_item(&mut session, prop::HOST_PEER_KEYS, &entry[..32]);
10087 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
10088
10089 let (emitted, _) = insert_item(&mut session, prop::HOST_PEER_KEYS, &entry[..95]);
10091 expect_status(&emitted[0], 5, Status::INVALID_ARGUMENT);
10092 }
10093
10094 #[test]
10095 fn key_table_whole_set_is_atomic_and_collapses_duplicates() {
10096 let mut session = test_session();
10097
10098 let key_a = [0xA0; 32];
10101 let key_b = [0xB0; 32];
10102 let mut table = Vec::new();
10103 table.extend_from_slice(&key_a);
10104 table.extend_from_slice(&key_b);
10105 table.extend_from_slice(&key_a);
10106 let (emitted, _) = set(&mut session, prop::HOST_CHANNEL_KEYS, &table);
10107 let (_, key, value) = parse_prop_is(&emitted[0]);
10108 assert_eq!(key, prop::HOST_CHANNEL_KEYS);
10109 assert_eq!(value.len(), 4, "two unique channels, 2-byte ids");
10110
10111 let (emitted, _) = set(&mut session, prop::HOST_CHANNEL_KEYS, &table[..40]);
10112 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
10113 assert_eq!(get(&mut session, prop::HOST_CHANNEL_KEYS).len(), 4);
10114
10115 let mut peers = Vec::new();
10117 peers.extend_from_slice(&peer_entry(0x01));
10118 let mut updated = peer_entry(0x01);
10119 updated[32..].fill(0x99);
10120 peers.extend_from_slice(&updated);
10121 let (emitted, _) = set(&mut session, prop::HOST_PEER_KEYS, &peers);
10122 let (_, _, value) = parse_prop_is(&emitted[0]);
10123 assert_eq!(value, peer_entry(0x01)[..32], "one entry, digest form");
10124
10125 let (emitted, _) = set(&mut session, prop::HOST_PEER_KEYS, &[]);
10127 let (_, _, value) = parse_prop_is(&emitted[0]);
10128 assert!(value.is_empty());
10129 let mut oversized = Vec::new();
10130 for seed in 0..(MAX_PEER_KEYS + 1) as u8 {
10131 oversized.extend_from_slice(&peer_entry(seed));
10132 }
10133 let (emitted, _) = set(&mut session, prop::HOST_PEER_KEYS, &oversized);
10134 expect_status(&emitted[0], 2, Status::NOMEM);
10135 assert!(get(&mut session, prop::HOST_PEER_KEYS).is_empty());
10136 }
10137
10138 #[test]
10139 fn insecure_transport_refuses_key_writes() {
10140 let mut session = test_session();
10141 session.attach(false); for (key, item) in [
10144 (prop::HOST_CHANNEL_KEYS, &[0x42u8; 32][..]),
10145 (prop::HOST_PEER_KEYS, &peer_entry(0x01)[..]),
10146 ] {
10147 let (emitted, effect) = set(&mut session, key, item);
10148 assert!(effect.is_none());
10149 expect_status(&emitted[0], 2, Status::INVALID_STATE);
10150 let (emitted, _) = insert_item(&mut session, key, item);
10151 expect_status(&emitted[0], 5, Status::INVALID_STATE);
10152 assert!(get(&mut session, key).is_empty(), "table must stay empty");
10153 }
10154
10155 let (emitted, _) = set(&mut session, prop::PHY_DUTY_LIMIT, &100u16.to_le_bytes());
10157 let (_, key, _) = parse_prop_is(&emitted[0]);
10158 assert_eq!(key, prop::PHY_DUTY_LIMIT);
10159
10160 session.attach(true);
10162 install_channel_key(&mut session, &[0x42; 32]);
10163 }
10164
10165 #[test]
10166 fn provisioned_channel_id_is_an_implicit_filter() {
10167 let mut session = test_session();
10168 enable(&mut session);
10169 let id = install_channel_key(&mut session, &[0x42; 32]);
10173 assert!(delivered(&mut session, &multicast_on(id)));
10174 assert!(delivered(&mut session, &blind_unicast_on(id)));
10175 let other = [id[0] ^ 0xFF, id[1]];
10176 assert!(!delivered(&mut session, &multicast_on(other)));
10177 assert!(delivered(&mut session, &broadcast_frame()));
10179 assert!(!delivered(&mut session, &[0x00, 0x01, 0x02]));
10180
10181 session.detach();
10183 receive_detached(&mut session, &multicast_on(id), 0);
10184 receive_detached(&mut session, &multicast_on(other), 0);
10185 session.attach(true);
10186 assert_eq!(queue_count(&mut session), 1);
10187 }
10188
10189 #[test]
10190 fn host_replacement_clears_key_tables() {
10191 let mut session = test_session();
10192 install_channel_key(&mut session, &[0x42; 32]);
10193 insert_item(&mut session, prop::HOST_PEER_KEYS, &peer_entry(0x01));
10194
10195 install_host_key(&mut session, &[0xAA; 32]);
10196 assert!(get(&mut session, prop::HOST_CHANNEL_KEYS).is_empty());
10197 assert!(get(&mut session, prop::HOST_PEER_KEYS).is_empty());
10198
10199 install_channel_key(&mut session, &[0x42; 32]);
10201 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_| {});
10202 assert!(get(&mut session, prop::HOST_CHANNEL_KEYS).is_empty());
10203 }
10204
10205 use umsh_core::{MicSize, PublicKey};
10208
10209 const HOST_PUB: [u8; 32] = [0xC4; 32];
10210 const PEER_PUB: [u8; 32] = [0x0A; 32];
10211
10212 fn test_pairwise() -> PairwiseKeys {
10213 PairwiseKeys {
10214 k_enc: [0x5E; 32],
10215 k_mic: [0x5F; 32],
10216 }
10217 }
10218
10219 fn peer_item(public_key: &[u8; 32], keys: &PairwiseKeys) -> [u8; 96] {
10220 let mut item = [0u8; 96];
10221 item[..32].copy_from_slice(public_key);
10222 item[32..64].copy_from_slice(&keys.k_enc);
10223 item[64..].copy_from_slice(&keys.k_mic);
10224 item
10225 }
10226
10227 fn auto_ack_session() -> TestSession {
10230 let mut session = test_session();
10231 enable(&mut session);
10232 install_host_key(&mut session, &HOST_PUB);
10233 insert_item(
10234 &mut session,
10235 prop::HOST_PEER_KEYS,
10236 &peer_item(&PEER_PUB, &test_pairwise()),
10237 );
10238 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
10239 session.detach();
10240 session
10241 }
10242
10243 fn sealed_unar(counter: u32, keys: &PairwiseKeys, full_source: bool) -> Vec<u8> {
10246 let mut buf = [0u8; 96];
10247 let builder = PacketBuilder::new(&mut buf).unicast(NodeHint([0xC4, 0xC4, 0xC4]));
10248 let builder = if full_source {
10249 builder.source_full(&PublicKey(PEER_PUB))
10250 } else {
10251 builder.source_hint(NodeHint([0x0A, 0x0A, 0x0A]))
10252 };
10253 let mut packet = builder
10254 .frame_counter(counter)
10255 .ack_requested()
10256 .mic_size(MicSize::Mic8)
10257 .payload(&[3, 1, 2])
10258 .build()
10259 .unwrap();
10260 test_engine().seal_packet(&mut packet, keys).unwrap();
10261 packet.as_bytes().to_vec()
10262 }
10263
10264 fn sealed_buar(counter: u32, channel_key: &[u8; 32]) -> Vec<u8> {
10266 let engine = test_engine();
10267 let channel_keys = engine.derive_channel_keys(&ChannelKey(*channel_key));
10268 let mut buf = [0u8; 96];
10269 let mut packet = PacketBuilder::new(&mut buf)
10270 .blind_unicast(channel_keys.channel_id, NodeHint([0xC4, 0xC4, 0xC4]))
10271 .source_hint(NodeHint([0x0A, 0x0A, 0x0A]))
10272 .frame_counter(counter)
10273 .ack_requested()
10274 .encrypted()
10275 .mic_size(MicSize::Mic8)
10276 .payload(&[3, 9, 9])
10277 .build()
10278 .unwrap();
10279 let blind = engine.derive_blind_keys(&test_pairwise(), &channel_keys);
10280 engine
10281 .seal_blind_packet(&mut packet, &blind, &channel_keys)
10282 .unwrap();
10283 packet.as_bytes().to_vec()
10284 }
10285
10286 fn rx_effect(session: &mut TestSession, frame: &[u8], now_ms: u64) -> Option<Effect> {
10288 session.on_radio_rx(
10289 frame,
10290 &RadioRxInfo::measured(-80, 40, None),
10291 now_ms,
10292 &mut |_: &[u8]| panic!("detached receive must not emit"),
10293 )
10294 }
10295
10296 fn expected_ack_trailer(frame: &[u8], keys: &PairwiseKeys) -> [u8; 8] {
10299 let engine = test_engine();
10300 let header = PacketHeader::parse(frame).unwrap();
10301 let full_mac = engine.s2v_tag(
10302 &keys.k_mic,
10303 |cmac| umsh_core::feed_aad(&header, frame, |chunk| cmac.update(chunk)),
10304 &frame[header.body_range.clone()],
10305 );
10306 engine.compute_ack_trailer(&full_mac, &keys.k_enc)
10307 }
10308
10309 fn expect_ack_transmit(
10312 session: &mut TestSession,
10313 effect: Option<Effect>,
10314 trailer: Option<[u8; 8]>,
10315 ) {
10316 assert_eq!(effect, Some(Effect::StartTransmit));
10317 let header = PacketHeader::parse(session.tx_data()).unwrap();
10318 assert_eq!(header.fcf.packet_type(), PacketType::MacAck);
10319 if let Some(trailer) = trailer {
10320 assert_eq!(session.tx_data()[header.mic_range.clone()], trailer);
10321 }
10322 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {
10323 panic!("autonomous ack must be silent")
10324 });
10325 }
10326
10327 #[test]
10328 fn unar_success_acks_queues_and_reports_acked() {
10329 let mut session = auto_ack_session();
10330 let frame = sealed_unar(100, &test_pairwise(), false);
10331 let effect = rx_effect(&mut session, &frame, 1_000);
10332 expect_ack_transmit(
10333 &mut session,
10334 effect,
10335 Some(expected_ack_trailer(&frame, &test_pairwise())),
10336 );
10337
10338 session.attach(true);
10341 assert_eq!(
10342 pui::decode(&get(&mut session, prop::LAST_STATUS))
10343 .unwrap()
10344 .0,
10345 Status::RESET_POWER_ON.0
10346 );
10347 assert_eq!(queue_count(&mut session), 1);
10348 let drained = drain(&mut session, 1_000);
10349 assert_eq!(
10350 drained[0].0, frame,
10351 "the queue holds the original wire bytes"
10352 );
10353 assert_eq!(drained[0].1.flags, RX_FLAG_BUFFERED | RX_FLAG_ACKED);
10354 }
10355
10356 #[test]
10357 fn buar_success_and_missing_channel_key() {
10358 let channel_key = [0x42; 32];
10359 let mut session = auto_ack_session();
10360 let frame = sealed_buar(7, &channel_key);
10363 assert!(rx_effect(&mut session, &frame, 0).is_none());
10364 session.attach(true);
10365 assert_eq!(queue_count(&mut session), 0);
10366
10367 install_channel_key(&mut session, &channel_key);
10370 session.detach();
10371 let effect = rx_effect(&mut session, &frame, 0);
10372 expect_ack_transmit(&mut session, effect, None);
10373 session.attach(true);
10374 assert_eq!(queue_count(&mut session), 1);
10375 let drained = drain(&mut session, 0);
10376 assert_eq!(drained[0].1.flags, RX_FLAG_BUFFERED | RX_FLAG_ACKED);
10377 }
10378
10379 #[test]
10380 fn unprovisioned_source_and_bad_mic_queue_unacked() {
10381 let mut session = auto_ack_session();
10382
10383 let wrong_keys = PairwiseKeys {
10387 k_enc: [1; 32],
10388 k_mic: [2; 32],
10389 };
10390 assert!(rx_effect(&mut session, &sealed_unar(5, &wrong_keys, false), 0).is_none());
10391
10392 let mut corrupted = sealed_unar(6, &test_pairwise(), false);
10395 let last = corrupted.len() - 1;
10396 corrupted[last] ^= 0xFF;
10397 assert!(rx_effect(&mut session, &corrupted, 0).is_none());
10398
10399 let effect = rx_effect(&mut session, &sealed_unar(1, &test_pairwise(), false), 0);
10402 expect_ack_transmit(&mut session, effect, None);
10403
10404 session.attach(true);
10405 assert_eq!(queue_count(&mut session), 3);
10406 let drained = drain(&mut session, 0);
10407 assert_eq!(drained[0].1.flags, RX_FLAG_BUFFERED);
10408 assert_eq!(drained[1].1.flags, RX_FLAG_BUFFERED);
10409 assert_eq!(drained[2].1.flags, RX_FLAG_BUFFERED | RX_FLAG_ACKED);
10410 }
10411
10412 #[test]
10413 fn ambiguous_source_hint_is_never_acked_but_full_key_resolves() {
10414 let mut session = auto_ack_session();
10415 let mut twin = PEER_PUB;
10418 twin[31] ^= 0xFF;
10419 session.attach(true);
10420 insert_item(
10421 &mut session,
10422 prop::HOST_PEER_KEYS,
10423 &peer_item(&twin, &test_pairwise()),
10424 );
10425 session.detach();
10426
10427 assert!(rx_effect(&mut session, &sealed_unar(4, &test_pairwise(), false), 0).is_none());
10429
10430 let effect = rx_effect(&mut session, &sealed_unar(4, &test_pairwise(), true), 0);
10432 expect_ack_transmit(&mut session, effect, None);
10433 }
10434
10435 #[test]
10436 fn duplicates_coalesce_and_reack_only_within_window() {
10437 let mut session = auto_ack_session();
10438 let keys = test_pairwise();
10439
10440 let first = sealed_unar(5, &keys, false);
10441 let effect = rx_effect(&mut session, &first, 0);
10442 expect_ack_transmit(
10443 &mut session,
10444 effect,
10445 Some(expected_ack_trailer(&first, &keys)),
10446 );
10447
10448 let effect = rx_effect(&mut session, &first, 10_000);
10451 expect_ack_transmit(
10452 &mut session,
10453 effect,
10454 Some(expected_ack_trailer(&first, &keys)),
10455 );
10456
10457 for counter in 6..=14 {
10459 let effect = rx_effect(&mut session, &sealed_unar(counter, &keys, false), 20_000);
10460 expect_ack_transmit(&mut session, effect, None);
10461 }
10462 assert!(rx_effect(&mut session, &first, 30_000).is_none());
10464
10465 let effect = rx_effect(&mut session, &sealed_unar(15, &keys, false), 40_000);
10468 expect_ack_transmit(&mut session, effect, None);
10469
10470 session.attach(true);
10471 assert_eq!(queue_count(&mut session), 12);
10474 }
10475
10476 #[test]
10477 fn attached_host_suppresses_delegation() {
10478 let mut session = auto_ack_session();
10479 session.attach(true);
10480 let frame = sealed_unar(5, &test_pairwise(), false);
10481 let mut emitted = Vec::new();
10482 let effect = session.on_radio_rx(
10483 &frame,
10484 &RadioRxInfo::measured(-80, 40, None),
10485 0,
10486 &mut |bytes: &[u8]| emitted.push(bytes.to_vec()),
10487 );
10488 assert!(effect.is_none());
10490 assert_eq!(emitted.len(), 1);
10491 }
10492
10493 #[test]
10494 fn auto_ack_disabled_and_duty_limit_leave_frames_unacked() {
10495 let mut session = auto_ack_session();
10496 session.attach(true);
10497 set(&mut session, prop::HOST_AUTO_ACK, &[0]);
10498 session.detach();
10499 assert!(rx_effect(&mut session, &sealed_unar(5, &test_pairwise(), false), 0).is_none());
10500
10501 session.attach(true);
10504 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
10505 set(&mut session, prop::PHY_DUTY_LIMIT, &0u16.to_le_bytes());
10506 session.detach();
10507 assert!(rx_effect(&mut session, &sealed_unar(6, &test_pairwise(), false), 0).is_none());
10508
10509 session.attach(true);
10510 assert_eq!(queue_count(&mut session), 2);
10511 for (_, meta) in drain(&mut session, 0) {
10512 assert_eq!(meta.flags, RX_FLAG_BUFFERED);
10513 }
10514 }
10515
10516 #[test]
10517 fn auto_ack_property_round_trips_and_resets() {
10518 let mut session = test_session();
10519 assert_eq!(get(&mut session, prop::HOST_AUTO_ACK), [0]);
10520 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
10521 assert_eq!(get(&mut session, prop::HOST_AUTO_ACK), [1]);
10522
10523 session.attach(true);
10525 assert_eq!(get(&mut session, prop::HOST_AUTO_ACK), [1]);
10526 install_host_key(&mut session, &[0xBB; 32]);
10527 assert_eq!(get(&mut session, prop::HOST_AUTO_ACK), [0]);
10528
10529 let (emitted, _) = set(&mut session, prop::HOST_AUTO_ACK, &[2]);
10530 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
10531 }
10532
10533 #[test]
10534 fn peer_key_replacement_preserves_replay_baseline() {
10535 let mut session = auto_ack_session();
10536 let old_keys = test_pairwise();
10537 let effect = rx_effect(&mut session, &sealed_unar(5, &old_keys, false), 0);
10538 expect_ack_transmit(&mut session, effect, None);
10539
10540 session.attach(true);
10542 let new_keys = PairwiseKeys {
10543 k_enc: [0x77; 32],
10544 k_mic: [0x78; 32],
10545 };
10546 insert_item(
10547 &mut session,
10548 prop::HOST_PEER_KEYS,
10549 &peer_item(&PEER_PUB, &new_keys),
10550 );
10551 session.detach();
10552
10553 assert!(rx_effect(&mut session, &sealed_unar(5, &new_keys, false), 10).is_none());
10557 let effect = rx_effect(&mut session, &sealed_unar(6, &new_keys, false), 20);
10558 expect_ack_transmit(&mut session, effect, None);
10559 }
10560
10561 fn strip_snapshot_options(bytes: &[u8], drop: &[u32]) -> Vec<u8> {
10566 let (format, options) = bytes.split_first().unwrap();
10567 let mut out = vec![0u8; bytes.len()];
10568 out[0] = *format;
10569 let mut encoder = OptionEncoder::new(&mut out[1..]);
10570 for item in OptionDecoder::new(options) {
10571 let (number, value) = item.unwrap();
10572 if drop.contains(&u32::from(number)) {
10573 continue;
10574 }
10575 encoder.put(number, value).unwrap();
10576 }
10577 let len = 1 + encoder.finish();
10578 out.truncate(len);
10579 out
10580 }
10581
10582 fn save(session: &mut TestSession) {
10584 let mut buf = [0u8; 4];
10585 let len = frame::save(&mut buf, 3).unwrap();
10586 let (emitted, effect) = dispatch(session, &buf[..len], 0);
10587 assert!(emitted.is_empty(), "no response before the write commits");
10588 assert_eq!(effect, Some(Effect::SaveSnapshot { tid: 3 }));
10589 let mut emitted = Vec::new();
10590 session.respond_save(3, Ok(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
10591 expect_status(&emitted[0], 3, Status::OK);
10592 }
10593
10594 fn restore(session: &mut TestSession) -> Option<Effect> {
10596 let mut buf = [0u8; 4];
10597 let len = frame::restore(&mut buf, 5).unwrap();
10598 let (emitted, effect) = dispatch(session, &buf[..len], 0);
10599 expect_status(&emitted[0], TID_UNSOLICITED, Status::RESET_RESTORED);
10600 effect
10601 }
10602
10603 fn provisioned_session() -> TestSession {
10606 let mut session = test_session();
10607 set(&mut session, prop::PHY_FREQ, &906_875u32.to_le_bytes());
10608 enable(&mut session);
10609 set(&mut session, prop::DEV_NAME, b"saved name");
10610 install_host_key(&mut session, &HOST_PUB);
10611 insert_item(
10612 &mut session,
10613 prop::HOST_RX_FILTERS,
10614 &[items::FILTER_PKT_TYPE, 0],
10615 );
10616 install_channel_key(&mut session, &[0x42; 32]);
10617 insert_item(
10618 &mut session,
10619 prop::HOST_PEER_KEYS,
10620 &peer_item(&PEER_PUB, &test_pairwise()),
10621 );
10622 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
10623 session
10624 }
10625
10626 #[test]
10627 fn save_sets_prop_saved_and_failure_rolls_back() {
10628 let mut session = test_session();
10629 assert_eq!(get(&mut session, prop::SAVED), [0]);
10630
10631 let mut buf = [0u8; 4];
10633 let len = frame::save(&mut buf, 3).unwrap();
10634 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
10635 assert_eq!(effect, Some(Effect::SaveSnapshot { tid: 3 }));
10636 let mut emitted = Vec::new();
10637 session.respond_save(3, Err(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
10638 expect_status(&emitted[0], 3, Status::FAILURE);
10639 assert_eq!(get(&mut session, prop::SAVED), [0]);
10640
10641 save(&mut session);
10642 assert_eq!(get(&mut session, prop::SAVED), [1]);
10643
10644 let (emitted, _) = set(&mut session, prop::SAVED, &[0]);
10646 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
10647 }
10648
10649 #[test]
10650 fn restore_without_snapshot_is_invalid_state() {
10651 let mut session = test_session();
10652 let mut buf = [0u8; 4];
10653 let len = frame::restore(&mut buf, 5).unwrap();
10654 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
10655 assert!(effect.is_none());
10656 expect_status(&emitted[0], 5, Status::INVALID_STATE);
10657 }
10658
10659 #[test]
10660 fn snapshot_round_trips_through_the_wire_encoding() {
10661 let session = provisioned_session();
10662 let mut bytes = [0u8; SNAPSHOT_MAX];
10663 let len = session.encode_snapshot(&mut bytes).unwrap();
10664
10665 let mut booted: TestSession =
10669 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10670 let effect = booted.restore_at_boot(&bytes[..len]).unwrap();
10671 assert!(matches!(effect, Effect::ApplyRadio(s) if s.enabled && s.freq_khz == 906_875));
10672 assert_eq!(booted.device_name(), "saved name");
10673 let id = test_engine().derive_channel_id(&ChannelKey([0x42; 32])).0;
10677 booted.on_radio_rx(
10678 &multicast_on(id),
10679 &RadioRxInfo::measured(-80, 40, None),
10680 0,
10681 &mut |_: &[u8]| panic!("detached boot must not emit"),
10682 );
10683 booted.attach(true);
10684 assert_eq!(get(&mut booted, prop::SAVED), [ids::saved::CURRENT]);
10685 assert_eq!(get(&mut booted, prop::HOST_KEY), Vec::<u8>::new());
10686 assert_eq!(get(&mut booted, prop::HOST_AUTO_ACK), [0]);
10687 assert_eq!(queue_count(&mut booted), 1);
10691
10692 let mut fresh: TestSession =
10696 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10697 assert_eq!(
10698 fresh.restore_at_boot(&bytes[..len - 1]),
10699 Err(SnapshotError::Malformed)
10700 );
10701 let mut wrong_format = bytes;
10702 wrong_format[0] = SNAPSHOT_FORMAT + 1;
10703 assert_eq!(
10704 fresh.restore_at_boot(&wrong_format[..len]),
10705 Err(SnapshotError::UnknownFormat)
10706 );
10707 let mut legacy = bytes;
10710 legacy[0] = 3;
10711 assert_eq!(
10712 fresh.restore_at_boot(&legacy[..len]),
10713 Err(SnapshotError::UnknownFormat)
10714 );
10715 fresh.attach(true);
10716 assert_eq!(get(&mut fresh, prop::SAVED), [ids::saved::NONE]);
10717 fresh.note_snapshot_rejected();
10718 assert_eq!(get(&mut fresh, prop::SAVED), [ids::saved::UNREADABLE]);
10719 }
10720
10721 #[test]
10727 fn positioning_settings_survive_a_save_and_the_clock_does_not() {
10728 let mut session = test_session();
10729 set(&mut session, prop::GNSS_ENABLED, &[1]);
10730 set(&mut session, prop::TZ_OFFSET, &(-300i16).to_le_bytes());
10731 set(&mut session, prop::GNSS_IDENT_UPDATE, &[1]);
10732 set(&mut session, prop::GNSS_IDENT_PRECISION, &[6]);
10733 set(&mut session, prop::GNSS_TIME_TRUST, &[0]);
10734 set(&mut session, prop::TIME, &1_780_000_000u32.to_le_bytes());
10735
10736 let mut bytes = [0u8; SNAPSHOT_MAX];
10737 let len = session.encode_snapshot(&mut bytes).unwrap();
10738
10739 let mut booted: TestSession =
10740 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10741 booted.restore_at_boot(&bytes[..len]).unwrap();
10742 assert!(booted.gnss_enabled());
10743 assert_eq!(booted.tz_offset_min(), -300);
10744 assert!(booted.gnss_ident_update());
10745 assert_eq!(booted.gnss_ident_precision(), 6);
10746 assert!(!booted.gnss_time_trust());
10747 booted.attach(true);
10748 assert_eq!(get(&mut booted, prop::GNSS_ENABLED), [1]);
10749 assert_eq!(get(&mut booted, prop::GNSS_TIME_TRUST), [0]);
10750 let mut buf = [0u8; 16];
10753 let get_len = frame::prop_get(&mut buf, 3, prop::TIME).unwrap();
10754 let (_, effect) = dispatch(&mut booted, &buf[..get_len], 0);
10755 assert_eq!(effect, Some(Effect::ReadTime { tid: 3 }));
10756
10757 let mut out = Vec::new();
10760 booted.reset(Status::RESET_SOFTWARE, &mut |bytes: &[u8]| {
10761 out.push(bytes.to_vec())
10762 });
10763 assert!(booted.gnss_enabled());
10764 assert_eq!(booted.tz_offset_min(), -300);
10765 }
10766
10767 #[test]
10772 fn restore_under_a_different_identity_leaves_the_phy_disabled() {
10773 let mut session = provisioned_session();
10774 session.set_boot_identity([0xAA; 32]);
10775 let mut bytes = [0u8; SNAPSHOT_MAX];
10776 let len = session.encode_snapshot(&mut bytes).unwrap();
10777
10778 let mut replacement: TestSession =
10780 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10781 replacement.set_boot_identity([0xBB; 32]);
10782 let effect = replacement.restore_at_boot(&bytes[..len]).unwrap();
10783 assert!(matches!(effect, Effect::ApplyRadio(s) if !s.enabled));
10784 assert_eq!(replacement.device_name(), "saved name");
10786 assert_eq!(replacement.settings().freq_khz, 906_875);
10787
10788 let mut same: TestSession =
10791 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10792 same.set_boot_identity([0xAA; 32]);
10793 let effect = same.restore_at_boot(&bytes[..len]).unwrap();
10794 assert!(matches!(effect, Effect::ApplyRadio(s) if s.enabled));
10795 }
10796
10797 #[test]
10802 fn saved_schema_orders_by_identifier_and_applies_by_phase() {
10803 assert!(
10804 SAVED_SCHEMA
10805 .windows(2)
10806 .all(|pair| pair[0].number < pair[1].number),
10807 "the option encoder rejects a number below the last one written"
10808 );
10809 let enable = SAVED_SCHEMA
10810 .iter()
10811 .find(|entry| u32::from(entry.number) == prop::PHY_ENABLED)
10812 .expect("PHY_ENABLED is saved");
10813 assert_eq!(enable.phase, ApplyPhase::Enable);
10814 assert_eq!(*ApplyPhase::ORDER.last().unwrap(), ApplyPhase::Enable);
10815 assert!(
10816 SAVED_SCHEMA
10817 .iter()
10818 .filter(|entry| entry.phase == ApplyPhase::Enable)
10819 .count()
10820 == 1,
10821 "only the PHY enable belongs in the last phase"
10822 );
10823 assert!(u32::from(enable.number) < prop::PHY_FREQ);
10825 }
10826
10827 #[test]
10831 fn absent_options_decode_to_post_reset_defaults() {
10832 let mut bare = [0u8; SNAPSHOT_MAX];
10833 bare[0] = SNAPSHOT_FORMAT;
10834 let mut booted: TestSession =
10835 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10836 let effect = booted.restore_at_boot(&bare[..1]).unwrap();
10837 let defaults = test_config().defaults;
10838 assert!(matches!(
10839 effect,
10840 Effect::ApplyRadio(s) if !s.enabled
10841 && s.freq_khz == defaults.freq_khz
10842 && s.sf == defaults.sf
10843 ));
10844 assert_eq!(booted.device_name(), test_config().default_device_name);
10845 booted.attach(true);
10846 assert_eq!(get(&mut booted, prop::SAVED), [ids::saved::CURRENT]);
10847 assert_eq!(get(&mut booted, prop::MAC_REPEATER_ENABLED), [0]);
10848 assert_eq!(get(&mut booted, prop::HOST_KEY), [] as [u8; 0]);
10849 }
10850
10851 #[test]
10855 fn unknown_options_are_skipped() {
10856 let session = provisioned_session();
10857 let mut bytes = [0u8; SNAPSHOT_MAX];
10858 let len = session.encode_snapshot(&mut bytes).unwrap();
10859 let mut extended = bytes;
10861 let mut encoder =
10862 OptionEncoder::with_last_number(&mut extended[len..], prop::PHY_DUTY_LIMIT as u16);
10863 encoder.put(60000, &[1, 2, 3]).unwrap();
10864 let extra = encoder.finish();
10865
10866 let mut booted: TestSession =
10867 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10868 booted.restore_at_boot(&extended[..len + extra]).unwrap();
10869 assert_eq!(booted.device_name(), "saved name");
10870 }
10871
10872 #[test]
10875 fn a_repeated_single_valued_property_is_rejected() {
10876 let mut bytes = [0u8; SNAPSHOT_MAX];
10877 bytes[0] = SNAPSHOT_FORMAT;
10878 let mut encoder = OptionEncoder::new(&mut bytes[1..]);
10879 encoder.put(prop::PHY_LORA_SF as u16, &[9]).unwrap();
10880 encoder.put(prop::PHY_LORA_SF as u16, &[10]).unwrap();
10881 let len = 1 + encoder.finish();
10882
10883 let mut booted: TestSession =
10884 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10885 assert_eq!(
10886 booted.restore_at_boot(&bytes[..len]),
10887 Err(SnapshotError::Malformed)
10888 );
10889 }
10890
10891 #[test]
10895 fn out_of_range_snapshot_values_are_rejected() {
10896 for (number, value) in [
10897 (prop::PHY_LORA_SF, &[13u8][..]),
10898 (prop::PHY_LORA_CR, &[4][..]),
10899 (prop::PHY_LORA_BW, &7_000u32.to_le_bytes()[..]),
10900 (prop::PHY_ENABLED, &[2][..]),
10901 (prop::DEV_NAME, &[][..]),
10902 ] {
10903 let mut bytes = [0u8; SNAPSHOT_MAX];
10904 bytes[0] = SNAPSHOT_FORMAT;
10905 let mut encoder = OptionEncoder::new(&mut bytes[1..]);
10906 encoder.put(number as u16, value).unwrap();
10907 let len = 1 + encoder.finish();
10908
10909 let mut booted: TestSession =
10910 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10911 assert_eq!(
10912 booted.restore_at_boot(&bytes[..len]),
10913 Err(SnapshotError::InvalidValue),
10914 "property {number} accepted an out-of-range value"
10915 );
10916 }
10917 }
10918
10919 #[test]
10923 fn snapshot_at_capacity_fits_the_buffer() {
10924 let mut session = provisioned_session();
10925 for seed in 0..MAX_CHANNEL_KEYS as u8 {
10926 let _ = session.device.channel_keys.insert(ChannelKeyEntry {
10927 key: [seed; items::CHANNEL_KEY_LEN],
10928 id: [seed, seed],
10929 });
10930 }
10931 for seed in 0..MAX_DEV_PEERS as u8 {
10932 let _ = session.device.peers.insert([seed; items::PUBLIC_KEY_LEN]);
10933 }
10934 for seed in 0..MAX_DEV_ADMINS as u8 {
10935 let _ = session.device.admins.insert([seed; items::PUBLIC_KEY_LEN]);
10936 }
10937 for seed in 0..MAX_REPEATER_REGIONS as u8 {
10938 let mut text = [b'r'; REGION_STRING_MAX_LEN];
10940 text[0] = b'a' + seed;
10941 let _ = session
10942 .device
10943 .repeater_regions
10944 .push(region_entry(&text).unwrap());
10945 }
10946 session.device.name = [b'n'; MAX_DEVICE_NAME_LEN];
10947 session.device.name_len = MAX_DEVICE_NAME_LEN;
10948 session.set_boot_identity([0xEE; 32]);
10949
10950 let mut bytes = [0u8; SNAPSHOT_MAX];
10951 let len = session
10952 .encode_snapshot(&mut bytes)
10953 .expect("a full snapshot must fit SNAPSHOT_MAX");
10954 assert!(len <= SNAPSHOT_MAX, "worst case is {len} octets");
10955
10956 let mut booted: TestSession =
10957 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
10958 booted.set_boot_identity([0xEE; 32]);
10959 booted.restore_at_boot(&bytes[..len]).unwrap();
10960 assert_eq!(booted.device.channel_keys.len, MAX_CHANNEL_KEYS);
10961 assert_eq!(booted.device.peers.len, MAX_DEV_PEERS);
10962 assert_eq!(booted.device.admins.len, MAX_DEV_ADMINS);
10963 }
10964
10965 #[test]
10966 fn reset_post_reset_values_come_from_the_snapshot() {
10967 let mut session = provisioned_session();
10968 save(&mut session);
10969
10970 set(&mut session, prop::PHY_FREQ, &915_000u32.to_le_bytes());
10972 set(&mut session, prop::DEV_NAME, b"diverged");
10973 let mut emitted = Vec::new();
10974 let effect = session.reset(Status::RESET_SOFTWARE, &mut |bytes: &[u8]| {
10975 emitted.push(bytes.to_vec())
10976 });
10977 assert!(matches!(effect, Effect::ApplyRadio(s) if s.enabled && s.freq_khz == 906_875));
10980 assert_eq!(session.device_name(), "saved name");
10981 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
10984
10985 let mut buf = [0u8; 4];
10987 let len = frame::clear(&mut buf, 4).unwrap();
10988 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
10989 assert_eq!(effect, Some(Effect::ClearSaved { tid: 4 }));
10990 session.respond_clear(4, Ok(()), &mut |_: &[u8]| {});
10991 let effect = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
10992 assert!(matches!(effect, Effect::ApplyRadio(s) if !s.enabled && s.freq_khz == 910_525));
10993 assert_eq!(session.device_name(), "Test UMSH Device");
10994 assert_eq!(get(&mut session, prop::HOST_KEY), Vec::<u8>::new());
10995 }
10996
10997 #[test]
10998 fn restore_reverts_config_but_preserves_queue_and_baselines() {
10999 let mut session = provisioned_session();
11000 save(&mut session);
11001
11002 session.detach();
11005 let first = sealed_unar(5, &test_pairwise(), false);
11006 let effect = rx_effect(&mut session, &first, 0);
11007 expect_ack_transmit(&mut session, effect, None);
11008 session.attach(true);
11009 assert_eq!(queue_count(&mut session), 1);
11010
11011 set(&mut session, prop::PHY_DUTY_LIMIT, &77u16.to_le_bytes());
11013 insert_item(
11014 &mut session,
11015 prop::HOST_RX_FILTERS,
11016 &[items::FILTER_DEST_HINT, 9, 9, 9],
11017 );
11018
11019 let effect = restore(&mut session);
11022 assert!(matches!(effect, Some(Effect::ApplyRadio(s)) if s.enabled));
11023 assert_eq!(
11024 get(&mut session, prop::PHY_DUTY_LIMIT),
11025 0xFFFFu16.to_le_bytes()
11026 );
11027 let filters = get(&mut session, prop::HOST_RX_FILTERS);
11030 assert_eq!(
11031 filters,
11032 [2, items::FILTER_PKT_TYPE, 0, 4, 0, 9, 9, 9],
11033 "host domain untouched by a restore"
11034 );
11035 assert_eq!(queue_count(&mut session), 1);
11036
11037 session.detach();
11042 let effect = rx_effect(&mut session, &first, 10_000);
11043 expect_ack_transmit(&mut session, effect, None);
11044 session.attach(true);
11045 assert_eq!(queue_count(&mut session), 1);
11046 }
11047
11048 #[test]
11052 fn restore_leaves_the_host_domain_untouched() {
11053 let mut session = provisioned_session();
11054 save(&mut session);
11055
11056 install_host_key(&mut session, &[0xBB; 32]);
11059 assert_eq!(get(&mut session, prop::SAVED), [ids::saved::CURRENT]);
11060 session.detach();
11061 assert!(!delivered_at(
11062 &mut session,
11063 &unicast_to([0xBB, 0xBB, 0xBB]),
11064 0
11065 ));
11066 session.attach(true);
11067 assert_eq!(queue_count(&mut session), 1);
11068
11069 let effect = restore(&mut session);
11070 assert!(matches!(effect, Some(Effect::ApplyRadio(_))));
11071 assert_eq!(session.device_name(), "saved name");
11073 assert_eq!(get(&mut session, prop::HOST_KEY), [0xBB; 32]);
11075 assert_eq!(queue_count(&mut session), 1);
11076 }
11077
11078 #[test]
11082 fn a_saved_snapshot_carries_no_host_domain_across_a_reboot() {
11083 let mut session = provisioned_session();
11084 save(&mut session);
11085 let mut bytes = [0u8; SNAPSHOT_MAX];
11086 let len = session.encode_snapshot(&mut bytes).unwrap();
11087
11088 let mut booted: TestSession =
11089 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
11090 booted.restore_at_boot(&bytes[..len]).unwrap();
11091 booted.attach(true);
11092 assert_eq!(booted.device_name(), "saved name");
11093 assert_eq!(get(&mut booted, prop::HOST_KEY), Vec::<u8>::new());
11094 assert!(get(&mut booted, prop::HOST_CHANNEL_KEYS).is_empty());
11095 assert!(get(&mut booted, prop::HOST_PEER_KEYS).is_empty());
11096 assert!(get(&mut booted, prop::HOST_RX_FILTERS).is_empty());
11097 assert_eq!(get(&mut booted, prop::HOST_AUTO_ACK), [0]);
11098 }
11099
11100 #[test]
11105 fn a_whole_table_peer_set_reconciles_rather_than_rebuilding() {
11106 let mut session = test_session();
11107 enable(&mut session);
11108 install_host_key(&mut session, &HOST_PUB);
11109 install_channel_key(&mut session, &[0x42; 32]);
11110 set(
11111 &mut session,
11112 prop::HOST_PEER_KEYS,
11113 &peer_item(&PEER_PUB, &test_pairwise()),
11114 );
11115 set(&mut session, prop::HOST_AUTO_ACK, &[1]);
11116
11117 session.detach();
11119 let frame = sealed_unar(9, &test_pairwise(), false);
11120 let effect = rx_effect(&mut session, &frame, 0);
11121 expect_ack_transmit(&mut session, effect, None);
11122 session.attach(true);
11123 assert_eq!(queue_count(&mut session), 1);
11124 let _ = drain(&mut session, 0);
11125
11126 let mut table = peer_item(&PEER_PUB, &test_pairwise()).to_vec();
11130 table.extend_from_slice(&peer_item(&[0x77; 32], &test_pairwise()));
11131 set(&mut session, prop::HOST_PEER_KEYS, &table);
11132 assert_eq!(get(&mut session, prop::HOST_PEER_KEYS).len(), 64);
11133
11134 session.detach();
11135 let effect = rx_effect(&mut session, &frame, 10_000);
11136 expect_ack_transmit(&mut session, effect, None);
11137 session.attach(true);
11138 assert_eq!(
11139 queue_count(&mut session),
11140 0,
11141 "a preserved baseline coalesces the replay instead of queueing it again"
11142 );
11143
11144 set(
11147 &mut session,
11148 prop::HOST_PEER_KEYS,
11149 &peer_item(&[0x77; 32], &test_pairwise()),
11150 );
11151 assert_eq!(get(&mut session, prop::HOST_PEER_KEYS), [0x77; 32]);
11152 }
11153
11154 fn drained_flags(session: &mut TestSession) -> Vec<u8> {
11159 drain(session, 0)
11160 .into_iter()
11161 .map(|(_, meta)| meta.flags)
11162 .collect()
11163 }
11164
11165 #[test]
11166 fn ack_flag_requires_confirmed_transmission() {
11167 let mut session = auto_ack_session();
11168 let keys = test_pairwise();
11169 let frame = sealed_unar(5, &keys, false);
11170
11171 let effect = rx_effect(&mut session, &frame, 0);
11174 assert_eq!(effect, Some(Effect::StartTransmit));
11175 session.on_tx_result(TxOutcome::Failed, 0, &mut |_: &[u8]| {
11176 panic!("autonomous ack is silent")
11177 });
11178
11179 let effect = rx_effect(&mut session, &frame, 10_000);
11183 assert_eq!(
11184 effect,
11185 Some(Effect::StartTransmit),
11186 "re-ack after failed TX"
11187 );
11188 session.on_tx_result(TxOutcome::Sent, 10_000, &mut |_: &[u8]| {
11189 panic!("autonomous ack is silent")
11190 });
11191
11192 session.attach(true);
11193 assert_eq!(queue_count(&mut session), 1, "duplicate coalesced");
11194 assert_eq!(
11195 drained_flags(&mut session),
11196 [RX_FLAG_BUFFERED | RX_FLAG_ACKED]
11197 );
11198 }
11199
11200 #[test]
11201 fn failed_ack_leaves_flag_clear_and_eviction_is_handle_safe() {
11202 let mut session = auto_ack_session();
11203 let keys = test_pairwise();
11204
11205 let effect = rx_effect(&mut session, &sealed_unar(1, &keys, false), 0);
11207 assert_eq!(effect, Some(Effect::StartTransmit));
11208 session.on_tx_result(TxOutcome::Failed, 0, &mut |_: &[u8]| {});
11209
11210 let effect = rx_effect(&mut session, &sealed_unar(2, &keys, false), 0);
11216 assert_eq!(effect, Some(Effect::StartTransmit));
11217 for counter in 3..(2 + RX_QUEUE_CAPACITY as u32) {
11218 assert!(rx_effect(&mut session, &sealed_unar(counter, &keys, false), 0).is_none());
11220 }
11221 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
11222
11223 session.attach(true);
11224 assert_eq!(queue_count(&mut session), RX_QUEUE_CAPACITY as u16);
11225 let flags = drained_flags(&mut session);
11226 assert_eq!(flags[0], RX_FLAG_BUFFERED | RX_FLAG_ACKED);
11229 assert!(
11230 flags[1..].iter().all(|flags| *flags == RX_FLAG_BUFFERED),
11231 "no other entry may borrow the confirmation"
11232 );
11233 }
11234
11235 fn sealed_flooded_unar(counter: u32, keys: &PairwiseKeys, accumulated: u8) -> Vec<u8> {
11240 let mut buf = [0u8; 96];
11241 let mut packet = PacketBuilder::new(&mut buf)
11242 .unicast(NodeHint([0xC4, 0xC4, 0xC4]))
11243 .source_hint(NodeHint([0x0A, 0x0A, 0x0A]))
11244 .frame_counter(counter)
11245 .ack_requested()
11246 .mic_size(MicSize::Mic8)
11247 .flood_hops(15)
11248 .payload(&[3, 1, 2])
11249 .build()
11250 .unwrap();
11251 test_engine().seal_packet(&mut packet, keys).unwrap();
11252 let mut frame = packet.as_bytes().to_vec();
11253 frame[1] = umsh_core::FloodHops::new(15 - accumulated, accumulated)
11254 .unwrap()
11255 .0;
11256 frame
11257 }
11258
11259 #[test]
11260 fn flooded_traffic_gets_flood_return_acks() {
11261 let mut session = auto_ack_session();
11262 let keys = test_pairwise();
11263
11264 let effect = rx_effect(&mut session, &sealed_unar(1, &keys, false), 0);
11266 assert_eq!(effect, Some(Effect::StartTransmit));
11267 let header = PacketHeader::parse(session.tx_data()).unwrap();
11268 assert_eq!(header.flood_hops, None);
11269 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
11270
11271 for (accumulated, expected_remaining) in [(3u8, 3u8), (0, 1), (15, 15)] {
11274 let frame = sealed_flooded_unar(u32::from(accumulated) + 10, &keys, accumulated);
11275 let effect = rx_effect(&mut session, &frame, 0);
11276 assert_eq!(
11277 effect,
11278 Some(Effect::StartTransmit),
11279 "accumulated={accumulated}"
11280 );
11281 let header = PacketHeader::parse(session.tx_data()).unwrap();
11282 assert_eq!(header.fcf.packet_type(), PacketType::MacAck);
11283 let hops = header.flood_hops.expect("flood-return ack");
11284 assert_eq!(
11285 hops.remaining(),
11286 expected_remaining,
11287 "accumulated={accumulated}"
11288 );
11289 session.on_tx_result(TxOutcome::Sent, 0, &mut |_: &[u8]| {});
11290 }
11291
11292 let retransmission = sealed_flooded_unar(25, &keys, 7);
11297 let effect = rx_effect(&mut session, &retransmission, 10_000);
11298 assert_eq!(effect, Some(Effect::StartTransmit));
11299 let header = PacketHeader::parse(session.tx_data()).unwrap();
11300 assert_eq!(
11301 header.flood_hops.expect("flood-return re-ack").remaining(),
11302 7
11303 );
11304 }
11305
11306 fn sealed_multicast(counter: u32, channel_key: &[u8; 32], fill: u8) -> Vec<u8> {
11309 let engine = test_engine();
11310 let derived = engine.derive_channel_keys(&ChannelKey(*channel_key));
11311 let mut buf = [0u8; 96];
11312 let mut packet = PacketBuilder::new(&mut buf)
11313 .multicast(derived.channel_id)
11314 .source_hint(NodeHint([0x0A, 0x0A, 0x0A]))
11315 .frame_counter(counter)
11316 .mic_size(MicSize::Mic8)
11317 .payload(&[3, fill])
11318 .build()
11319 .unwrap();
11320 let keys = PairwiseKeys {
11321 k_enc: derived.k_enc,
11322 k_mic: derived.k_mic,
11323 };
11324 engine.seal_packet(&mut packet, &keys).unwrap();
11325 packet.as_bytes().to_vec()
11326 }
11327
11328 #[test]
11329 fn authenticated_multicast_duplicates_coalesce_queue_locally() {
11330 let channel_key = [0x42u8; 32];
11331 let mut session = auto_ack_session();
11332 session.attach(true);
11333 install_channel_key(&mut session, &channel_key);
11334 session.detach();
11335
11336 let frame = sealed_multicast(9, &channel_key, 0x11);
11339 assert!(rx_effect(&mut session, &frame, 0).is_none());
11340 assert!(rx_effect(&mut session, &frame, 5).is_none());
11341 assert!(rx_effect(&mut session, &sealed_multicast(10, &channel_key, 0x11), 0).is_none());
11343 assert!(rx_effect(&mut session, &sealed_multicast(11, &channel_key, 0x22), 0).is_none());
11344
11345 session.attach(true);
11346 assert_eq!(queue_count(&mut session), 3);
11347 for flags in drained_flags(&mut session) {
11348 assert_eq!(flags, RX_FLAG_BUFFERED, "multicast is never acked");
11349 }
11350 }
11351
11352 #[test]
11353 fn unauthenticated_multicast_duplicates_occupy_separate_entries() {
11354 let mut session = test_session();
11358 enable(&mut session);
11359 insert_item(
11360 &mut session,
11361 prop::HOST_RX_FILTERS,
11362 &[items::FILTER_PKT_TYPE, PacketType::Multicast as u8],
11363 );
11364 session.detach();
11365 let frame = sealed_multicast(9, &[0x42; 32], 0x11);
11366 receive_detached(&mut session, &frame, 0);
11367 receive_detached(&mut session, &frame, 0);
11368 session.attach(true);
11369 assert_eq!(queue_count(&mut session), 2);
11370 }
11371
11372 fn public_of(secret: &[u8; 32]) -> [u8; 32] {
11376 use umsh_crypto::NodeIdentity;
11377 umsh_crypto::software::SoftwareIdentity::from_secret_bytes(secret)
11378 .public_key()
11379 .0
11380 }
11381
11382 fn provision_identity(session: &mut TestSession, tid: u8, secret: &[u8; 32]) -> [u8; 32] {
11386 let mut buf = [0u8; 64];
11387 let len = frame::prop_set(&mut buf, tid, prop::DEV_PRIVATE_KEY, secret).unwrap();
11388 let (emitted, effect) = dispatch(session, &buf[..len], 0);
11389 assert!(
11390 emitted.is_empty(),
11391 "no response before the identity is stored"
11392 );
11393 assert_eq!(effect, Some(Effect::ProvisionIdentity { tid }));
11394 let Some(IdentitySource::Install(staged)) = session.identity_request() else {
11395 panic!("staged request must carry the installed secret");
11396 };
11397 assert_eq!(staged, *secret);
11398 let public_key = public_of(&staged);
11399 let mut emitted = Vec::new();
11400 session.respond_identity(tid, Ok(public_key), &mut |bytes: &[u8]| {
11401 emitted.push(bytes.to_vec())
11402 });
11403 let (response_tid, key, value) = parse_prop_is(&emitted[0]);
11404 assert_eq!(response_tid, tid);
11405 assert_eq!(key, prop::DEV_KEY, "success is announced as the public key");
11406 assert_eq!(value, public_key);
11407 public_key
11408 }
11409
11410 #[test]
11411 fn device_identity_provisioning_lifecycle() {
11412 let mut session = test_session();
11413 assert!(get(&mut session, prop::DEV_KEY).is_empty());
11416 let mut buf = [0u8; 16];
11417 let len = frame::prop_get(&mut buf, 4, prop::DEV_PRIVATE_KEY).unwrap();
11418 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
11419 expect_status(&emitted[0], 4, Status::UNIMPLEMENTED);
11420
11421 let (emitted, _) = set(&mut session, prop::DEV_KEY, &[0x55; 32]);
11423 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
11424
11425 let (emitted, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, &[0x11; 31]);
11427 assert!(effect.is_none());
11428 expect_status(&emitted[0], 2, Status::INVALID_ARGUMENT);
11429
11430 let public_key = provision_identity(&mut session, 7, &[0x11; 32]);
11431 assert_eq!(get(&mut session, prop::DEV_KEY), public_key);
11432
11433 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
11436 assert_eq!(get(&mut session, prop::DEV_KEY), public_key);
11437
11438 insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
11441 let replaced = provision_identity(&mut session, 3, &[0x22; 32]);
11442 assert_ne!(replaced, public_key);
11443 assert_eq!(get(&mut session, prop::DEV_KEY), replaced);
11444 assert_eq!(get(&mut session, prop::DEV_PEERS), [0xD0; 32]);
11445 }
11446
11447 #[test]
11448 fn identity_generation_stages_and_concurrent_writes_are_busy() {
11449 let mut session = test_session();
11450 let (emitted, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, &[]);
11452 assert!(emitted.is_empty());
11453 assert_eq!(effect, Some(Effect::ProvisionIdentity { tid: 2 }));
11454 assert!(matches!(
11455 session.identity_request(),
11456 Some(IdentitySource::Generate)
11457 ));
11458
11459 let (emitted, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, &[0x33; 32]);
11461 assert!(effect.is_none());
11462 expect_status(&emitted[0], 2, Status::BUSY);
11463
11464 let generated = public_of(&[0x5A; 32]);
11467 let mut emitted = Vec::new();
11468 session.respond_identity(2, Ok(generated), &mut |bytes: &[u8]| {
11469 emitted.push(bytes.to_vec())
11470 });
11471 let (_, key, value) = parse_prop_is(&emitted[0]);
11472 assert_eq!(key, prop::DEV_KEY);
11473 assert_eq!(value, generated);
11474 assert!(session.identity_request().is_none());
11475 }
11476
11477 #[test]
11478 fn identity_provisioning_failure_leaves_the_identity_unchanged() {
11479 let mut session = test_session();
11480 let original = provision_identity(&mut session, 7, &[0x11; 32]);
11481
11482 let (_, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, &[0x22; 32]);
11483 assert_eq!(effect, Some(Effect::ProvisionIdentity { tid: 2 }));
11484 let mut emitted = Vec::new();
11485 session.respond_identity(2, Err(()), &mut |bytes: &[u8]| emitted.push(bytes.to_vec()));
11486 expect_status(&emitted[0], 2, Status::FAILURE);
11487 assert_eq!(get(&mut session, prop::DEV_KEY), original);
11488 assert!(session.identity_request().is_none());
11489 }
11490
11491 #[test]
11492 fn identity_and_dev_channel_writes_require_a_secure_link() {
11493 let mut session = test_session();
11494 session.attach(false);
11495
11496 for value in [&[0x11u8; 32][..], &[][..]] {
11498 let (emitted, effect) = set(&mut session, prop::DEV_PRIVATE_KEY, value);
11499 assert!(effect.is_none());
11500 expect_status(&emitted[0], 2, Status::INVALID_STATE);
11501 }
11502 let (emitted, _) = insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &[0x42; 32]);
11503 expect_status(&emitted[0], 5, Status::INVALID_STATE);
11504 let (emitted, _) = set(&mut session, prop::DEV_CHANNEL_KEYS, &[0x42; 32]);
11505 expect_status(&emitted[0], 2, Status::INVALID_STATE);
11506
11507 let (emitted, _) = insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
11510 let (key, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
11511 assert_eq!(key, prop::DEV_PEERS);
11512 assert_eq!(digest, [0xD0; 32]);
11513 }
11514
11515 #[test]
11516 fn dev_channel_keys_and_peers_lifecycle() {
11517 let mut session = test_session();
11518 let dev_channel = [0x66u8; 32];
11519 let expected_id = test_engine().derive_channel_id(&ChannelKey(dev_channel)).0;
11520
11521 let (emitted, _) = insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
11524 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
11525 assert_eq!(digest, expected_id);
11526 assert_eq!(get(&mut session, prop::DEV_CHANNEL_KEYS), expected_id);
11527 let (emitted, _) = insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
11528 expect_status(&emitted[0], 5, Status::ALREADY);
11529
11530 let (emitted, _) = insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
11533 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
11534 assert_eq!(digest, [0xD0; 32]);
11535 let (emitted, _) = insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
11536 expect_status(&emitted[0], 5, Status::ALREADY);
11537 let mut two = Vec::new();
11538 two.extend_from_slice(&[0xD1; 32]);
11539 two.extend_from_slice(&[0xD1; 32]);
11540 let (emitted, _) = set(&mut session, prop::DEV_PEERS, &two);
11541 let (_, key, value) = parse_prop_is(&emitted[0]);
11542 assert_eq!(key, prop::DEV_PEERS);
11543 assert_eq!(value, [0xD1; 32], "duplicate items collapse");
11544
11545 let (emitted, _) = remove_item(&mut session, prop::DEV_PEERS, &[0xD1; 32]);
11547 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
11548 assert_eq!(digest, [0xD1; 32]);
11549 let (emitted, _) = remove_item(&mut session, prop::DEV_PEERS, &[0xD1; 32]);
11550 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
11551 let (emitted, _) = remove_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
11552 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
11553 assert_eq!(digest, expected_id);
11554
11555 for seed in 0..MAX_DEV_PEERS as u8 {
11557 insert_item(&mut session, prop::DEV_PEERS, &[seed; 32]);
11558 }
11559 let (emitted, _) = insert_item(&mut session, prop::DEV_PEERS, &[0xFF; 32]);
11560 expect_status(&emitted[0], 5, Status::NOMEM);
11561 }
11562
11563 #[test]
11568 fn dev_admins_lifecycle() {
11569 let mut session = test_session();
11570
11571 let (emitted, _) = insert_item(&mut session, prop::DEV_ADMINS, &[0xA0; 32]);
11572 let (key, digest) = parse_table_notice(&emitted[0], Cmd::PropInserted, 5);
11573 assert_eq!(key, prop::DEV_ADMINS);
11574 assert_eq!(digest, [0xA0; 32], "the digest form is the key itself");
11575 assert_eq!(session.dev_admins().collect::<Vec<_>>(), [[0xA0; 32]]);
11576 assert_eq!(session.dev_peers().count(), 0, "peers are a separate set");
11577
11578 let (emitted, _) = insert_item(&mut session, prop::DEV_ADMINS, &[0xA0; 32]);
11579 expect_status(&emitted[0], 5, Status::ALREADY);
11580
11581 let mut two = Vec::new();
11583 two.extend_from_slice(&[0xA1; 32]);
11584 two.extend_from_slice(&[0xA1; 32]);
11585 let (emitted, _) = set(&mut session, prop::DEV_ADMINS, &two);
11586 let (_, key, value) = parse_prop_is(&emitted[0]);
11587 assert_eq!(key, prop::DEV_ADMINS);
11588 assert_eq!(value, [0xA1; 32]);
11589
11590 let (emitted, _) = remove_item(&mut session, prop::DEV_ADMINS, &[0xA1; 32]);
11591 let (_, digest) = parse_table_notice(&emitted[0], Cmd::PropRemoved, 6);
11592 assert_eq!(digest, [0xA1; 32]);
11593 let (emitted, _) = remove_item(&mut session, prop::DEV_ADMINS, &[0xA1; 32]);
11594 expect_status(&emitted[0], 6, Status::ITEM_NOT_FOUND);
11595
11596 for seed in 0..MAX_DEV_ADMINS as u8 {
11597 insert_item(&mut session, prop::DEV_ADMINS, &[seed; 32]);
11598 }
11599 let (emitted, _) = insert_item(&mut session, prop::DEV_ADMINS, &[0xFF; 32]);
11600 expect_status(&emitted[0], 5, Status::NOMEM);
11601 }
11602
11603 #[test]
11606 fn dev_admin_changes_move_the_dev_domain_version() {
11607 let mut session = test_session();
11608 let start = session.dev_domain_version();
11609
11610 insert_item(&mut session, prop::DEV_ADMINS, &[0xA0; 32]);
11611 assert_eq!(session.dev_domain_version(), start + 1);
11612 set(&mut session, prop::DEV_ADMINS, &[0xA1; 32]);
11613 assert_eq!(session.dev_domain_version(), start + 2);
11614 remove_item(&mut session, prop::DEV_ADMINS, &[0xA1; 32]);
11615 assert_eq!(session.dev_domain_version(), start + 3);
11616
11617 remove_item(&mut session, prop::DEV_ADMINS, &[0xEE; 32]);
11619 assert_eq!(session.dev_domain_version(), start + 3);
11620 }
11621
11622 #[test]
11626 fn dev_admins_survive_a_save_and_boot() {
11627 let mut session = test_session();
11628 insert_item(&mut session, prop::DEV_ADMINS, &[0xA0; 32]);
11629 insert_item(&mut session, prop::DEV_ADMINS, &[0xA1; 32]);
11630 save(&mut session);
11631
11632 let mut bytes = [0u8; SNAPSHOT_MAX];
11633 let len = session.encode_snapshot(&mut bytes).unwrap();
11634 let mut booted: TestSession =
11635 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
11636 booted.restore_at_boot(&bytes[..len]).unwrap();
11637 assert_eq!(
11638 booted.dev_admins().collect::<Vec<_>>(),
11639 [[0xA0; 32], [0xA1; 32]]
11640 );
11641 }
11642
11643 #[test]
11644 fn dev_domain_version_tracks_node_table_changes() {
11645 let mut session = test_session();
11646 assert_eq!(session.dev_domain_version(), 0);
11647 assert_eq!(session.dev_channel_keys().count(), 0);
11648 assert_eq!(session.dev_peers().count(), 0);
11649 assert!(session.dev_key().is_none());
11650
11651 let dev_channel = [0x66u8; 32];
11654 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
11655 assert_eq!(session.dev_domain_version(), 1);
11656 assert_eq!(
11657 session.dev_channel_keys().collect::<Vec<_>>(),
11658 [dev_channel]
11659 );
11660 insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
11661 assert_eq!(session.dev_domain_version(), 2);
11662 assert_eq!(session.dev_peers().collect::<Vec<_>>(), [[0xD0; 32]]);
11663
11664 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
11666 remove_item(&mut session, prop::DEV_PEERS, &[0xEE; 32]);
11667 assert_eq!(session.dev_domain_version(), 2);
11668
11669 insert_item(&mut session, prop::HOST_CHANNEL_KEYS, &[0x42; 32]);
11672 assert_eq!(session.dev_domain_version(), 2);
11673
11674 set(&mut session, prop::DEV_PEERS, &[0xD1; 32]);
11676 assert_eq!(session.dev_domain_version(), 3);
11677 remove_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
11678 assert_eq!(session.dev_domain_version(), 4);
11679
11680 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
11683 assert_eq!(session.dev_domain_version(), 5);
11684 assert_eq!(session.dev_channel_keys().count(), 0);
11685 assert_eq!(session.dev_peers().count(), 0);
11686
11687 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
11691 save(&mut session);
11692 let mut bytes = [0u8; SNAPSHOT_MAX];
11693 let len = session.encode_snapshot(&mut bytes).unwrap();
11694 let mut booted: TestSession =
11695 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
11696 assert_eq!(booted.dev_domain_version(), 0);
11697 booted.restore_at_boot(&bytes[..len]).unwrap();
11698 assert_ne!(booted.dev_domain_version(), 0);
11699 assert_eq!(booted.dev_channel_keys().collect::<Vec<_>>(), [dev_channel]);
11700 }
11701
11702 #[test]
11703 fn dev_channel_keys_do_not_create_host_receive_filters() {
11704 let mut session = test_session();
11705 enable(&mut session);
11706 install_host_key(&mut session, &HOST_PUB);
11709 let dev_channel = [0x66u8; 32];
11710 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
11711 let dev_id = test_engine().derive_channel_id(&ChannelKey(dev_channel)).0;
11712
11713 session.detach();
11716 receive_detached(&mut session, &multicast_on(dev_id), 0);
11717 session.attach(true);
11718 assert_eq!(queue_count(&mut session), 0);
11719
11720 install_channel_key(&mut session, &dev_channel);
11722 session.detach();
11723 receive_detached(&mut session, &multicast_on(dev_id), 0);
11724 session.attach(true);
11725 assert_eq!(queue_count(&mut session), 1);
11726 }
11727
11728 #[test]
11729 fn snapshot_carries_device_tables_but_never_the_identity() {
11730 let mut session = test_session();
11731 let dev_channel = [0x66u8; 32];
11732 let dev_id = test_engine().derive_channel_id(&ChannelKey(dev_channel)).0;
11733 insert_item(&mut session, prop::DEV_CHANNEL_KEYS, &dev_channel);
11734 insert_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
11735 let public_key = provision_identity(&mut session, 7, &[0x11; 32]);
11736 save(&mut session);
11737
11738 remove_item(&mut session, prop::DEV_PEERS, &[0xD0; 32]);
11741 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
11742 assert_eq!(get(&mut session, prop::DEV_PEERS), [0xD0; 32]);
11743
11744 let mut bytes = [0u8; SNAPSHOT_MAX];
11747 let len = session.encode_snapshot(&mut bytes).unwrap();
11748 let mut booted: TestSession =
11749 Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
11750 booted.restore_at_boot(&bytes[..len]).unwrap();
11751 booted.attach(true);
11752 assert_eq!(get(&mut booted, prop::DEV_CHANNEL_KEYS), dev_id);
11753 assert_eq!(get(&mut booted, prop::DEV_PEERS), [0xD0; 32]);
11754 assert!(get(&mut booted, prop::DEV_KEY).is_empty());
11755 booted.set_boot_identity(public_key);
11756 assert_eq!(get(&mut booted, prop::DEV_KEY), public_key);
11757
11758 install_host_key(&mut booted, &[0xBB; 32]);
11760 assert_eq!(get(&mut booted, prop::DEV_CHANNEL_KEYS), dev_id);
11761 assert_eq!(get(&mut booted, prop::DEV_PEERS), [0xD0; 32]);
11762 assert_eq!(get(&mut booted, prop::DEV_KEY), public_key);
11763 }
11764
11765 #[test]
11766 fn restore_never_reverts_the_identity_and_clear_plus_reset_erases_it() {
11767 let mut session = test_session();
11768 let first = provision_identity(&mut session, 7, &[0x11; 32]);
11769 save(&mut session);
11770
11771 let second = provision_identity(&mut session, 3, &[0x22; 32]);
11774 assert_ne!(second, first);
11775 let _ = restore(&mut session);
11776 assert_eq!(get(&mut session, prop::DEV_KEY), second);
11777
11778 let mut buf = [0u8; 4];
11781 let len = frame::clear(&mut buf, 4).unwrap();
11782 let (_, effect) = dispatch(&mut session, &buf[..len], 0);
11783 assert_eq!(effect, Some(Effect::ClearSaved { tid: 4 }));
11784 session.respond_clear(4, Ok(()), &mut |_: &[u8]| {});
11785 assert_eq!(get(&mut session, prop::DEV_KEY), second);
11786 let _ = session.reset(Status::RESET_SOFTWARE, &mut |_: &[u8]| {});
11787 assert!(get(&mut session, prop::DEV_KEY).is_empty());
11788 }
11789
11790 #[test]
11791 fn tid_zero_identity_provisioning_is_silent_but_applies() {
11792 let mut session = test_session();
11793 let mut buf = [0u8; 64];
11794 let len = frame::prop_set(&mut buf, 0, prop::DEV_PRIVATE_KEY, &[0x11; 32]).unwrap();
11795 let effect = dispatch_tid0_silent(&mut session, &buf[..len].to_vec());
11796 assert_eq!(effect, Some(Effect::ProvisionIdentity { tid: 0 }));
11797 let public_key = public_of(&[0x11; 32]);
11798 session.respond_identity(0, Ok(public_key), &mut |_: &[u8]| {
11799 panic!("tid-0 provisioning must be silent")
11800 });
11801 assert_eq!(get(&mut session, prop::DEV_KEY), public_key);
11802 }
11803
11804 fn dispatch_tid0_silent(session: &mut TestSession, bytes: &[u8]) -> Option<Effect> {
11806 let (emitted, effect) = dispatch(session, bytes, 0);
11807 assert!(
11808 emitted.is_empty(),
11809 "fire-and-forget commands receive no correlated response"
11810 );
11811 effect
11812 }
11813
11814 fn last_status_of(session: &mut TestSession) -> u32 {
11815 pui::decode(&get(session, prop::LAST_STATUS)).unwrap().0
11816 }
11817
11818 #[test]
11819 fn tid_zero_commands_are_fire_and_forget() {
11820 let mut session = test_session();
11821 let mut buf = [0u8; 640];
11822
11823 let len = frame::nop(&mut buf, 0).unwrap();
11826 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
11827 assert_eq!(last_status_of(&mut session), Status::OK.0);
11828
11829 let len = frame::queue_drain(&mut buf, 0).unwrap();
11830 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
11831
11832 let len = frame::save(&mut buf, 0).unwrap();
11833 let effect = dispatch_tid0_silent(&mut session, &buf[..len].to_vec());
11834 assert_eq!(effect, Some(Effect::SaveSnapshot { tid: 0 }));
11835 session.respond_save(0, Ok(()), &mut |_: &[u8]| {
11836 panic!("tid-0 save must be silent")
11837 });
11838 assert_eq!(get(&mut session, prop::SAVED), [1]);
11839
11840 let len = frame::clear(&mut buf, 0).unwrap();
11842 let effect = dispatch_tid0_silent(&mut session, &buf[..len].to_vec());
11843 assert_eq!(effect, Some(Effect::ClearSaved { tid: 0 }));
11844 session.respond_clear(0, Err(()), &mut |_: &[u8]| {
11845 panic!("tid-0 clear must be silent")
11846 });
11847 assert_eq!(last_status_of(&mut session), Status::FAILURE.0);
11848 assert_eq!(
11849 get(&mut session, prop::SAVED),
11850 [1],
11851 "failed clear rolls back nothing"
11852 );
11853
11854 let len = frame::prop_set(&mut buf, 0, prop::PHY_DUTY_LIMIT, &99u16.to_le_bytes()).unwrap();
11857 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
11858 assert_eq!(get(&mut session, prop::PHY_DUTY_LIMIT), 99u16.to_le_bytes());
11859
11860 let item = [items::FILTER_PKT_TYPE, 0];
11861 let len = frame::prop_insert(&mut buf, 0, prop::HOST_RX_FILTERS, &item).unwrap();
11862 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
11863 assert_eq!(
11864 get(&mut session, prop::HOST_RX_FILTERS),
11865 [2, items::FILTER_PKT_TYPE, 0]
11866 );
11867
11868 let len = frame::prop_remove(&mut buf, 0, prop::HOST_RX_FILTERS, &item).unwrap();
11869 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
11870 assert!(get(&mut session, prop::HOST_RX_FILTERS).is_empty());
11871
11872 let len = frame::prop_get(&mut buf, 0, prop::PHY_MTU).unwrap();
11874 assert!(dispatch_tid0_silent(&mut session, &buf[..len].to_vec()).is_none());
11875 }
11876
11877 #[test]
11878 fn tid_zero_drain_delivers_frames_but_no_completion() {
11879 let mut session = test_session();
11880 enable(&mut session);
11881 session.detach();
11882 receive_detached(&mut session, &unicast_to([1, 2, 3]), 0);
11883 session.attach(true);
11884
11885 let mut buf = [0u8; 4];
11886 let len = frame::queue_drain(&mut buf, 0).unwrap();
11887 let (emitted, effect) = dispatch(&mut session, &buf[..len], 0);
11888 assert!(emitted.is_empty());
11889 assert_eq!(effect, Some(Effect::DrainQueue));
11890
11891 let mut emitted = Vec::new();
11893 assert!(session.drain_step(0, &mut |bytes: &[u8]| emitted.push(bytes.to_vec())));
11894 assert_eq!(emitted.len(), 1);
11895 assert_eq!(
11896 Frame::parse(&emitted[0]).unwrap().command(),
11897 Some(Cmd::StrRecv)
11898 );
11899 let mut emitted = Vec::new();
11900 assert!(!session.drain_step(0, &mut |bytes: &[u8]| emitted.push(bytes.to_vec())));
11901 assert!(
11902 emitted.is_empty(),
11903 "TID-zero drain has no completion response"
11904 );
11905 assert_eq!(last_status_of(&mut session), Status::OK.0);
11906 }
11907
11908 fn admin(session: &mut TestSession, request: &[u8]) -> Vec<Vec<u8>> {
11914 let mut emitted = Vec::new();
11915 let effect = session.handle_admin_frame(request, 0, 180, &mut |bytes: &[u8]| {
11916 emitted.push(bytes.to_vec())
11917 });
11918 assert!(effect.is_none(), "this exchange defers nothing");
11919 session.end_admin_exchange();
11920 emitted
11921 }
11922
11923 #[test]
11928 fn an_admin_request_is_answered_despite_its_zero_tid() {
11929 let mut session = test_session();
11930 let mut buf = [0u8; 16];
11931 let len = frame::prop_get(&mut buf, TID_UNSOLICITED, prop::PROTOCOL_VERSION).unwrap();
11932 let emitted = admin(&mut session, &buf[..len]);
11933 assert_eq!(emitted.len(), 1);
11934 let (tid, key, _) = parse_prop_is(&emitted[0]);
11935 assert_eq!(tid, TID_UNSOLICITED);
11936 assert_eq!(key, prop::PROTOCOL_VERSION);
11937
11938 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
11940 assert!(emitted.is_empty());
11941 }
11942
11943 #[test]
11944 fn an_unparseable_admin_frame_is_answered_rather_than_ignored() {
11945 let mut session = test_session();
11946 let emitted = admin(&mut session, &[]);
11947 assert_eq!(emitted.len(), 1);
11948 expect_status(&emitted[0], TID_UNSOLICITED, Status::PARSE_ERROR);
11949 }
11950
11951 #[test]
11952 fn the_host_transports_are_not_an_admins_to_command() {
11953 let mut session = test_session();
11954 enable(&mut session);
11955 let mut buf = [0u8; 64];
11956 for len in [
11957 frame::str_send(&mut buf, TID_UNSOLICITED, stream::PHY_RAW, b"hi", &[]).unwrap(),
11958 frame::queue_drain(&mut buf, TID_UNSOLICITED).unwrap(),
11959 ]
11960 .map(|len| len)
11961 {
11962 let emitted = admin(&mut session, &buf[..len]);
11963 assert_eq!(emitted.len(), 1);
11964 expect_status(&emitted[0], TID_UNSOLICITED, Status::INVALID_COMMAND);
11965 }
11966 }
11967
11968 #[test]
11969 fn the_host_domain_does_not_exist_for_an_admin() {
11970 let mut session = test_session();
11971 let mut buf = [0u8; 64];
11972 for key in [
11973 prop::HOST_KEY,
11974 prop::HOST_CHANNEL_KEYS,
11975 prop::HOST_PEER_KEYS,
11976 prop::HOST_RX_FILTERS,
11977 prop::HOST_AUTO_ACK,
11978 prop::HOST_RX_QUEUE_COUNT,
11979 prop::HOST_RX_QUEUE_CAPACITY,
11980 prop::HOST_RX_QUEUE_DROPPED,
11981 prop::MAC_PROMISCUOUS,
11982 prop::MAC_BACKHAUL,
11983 prop::DEV_PRIVATE_KEY,
11984 ] {
11985 let len = frame::prop_get(&mut buf, TID_UNSOLICITED, key).unwrap();
11986 let emitted = admin(&mut session, &buf[..len]);
11987 expect_status(&emitted[0], TID_UNSOLICITED, Status::PROP_NOT_FOUND);
11988
11989 let len = frame::prop_set(&mut buf, TID_UNSOLICITED, key, &[0]).unwrap();
11990 let emitted = admin(&mut session, &buf[..len]);
11991 expect_status(&emitted[0], TID_UNSOLICITED, Status::PROP_NOT_FOUND);
11992
11993 let len = frame::prop_insert(&mut buf, TID_UNSOLICITED, key, &[0; 32]).unwrap();
11994 let emitted = admin(&mut session, &buf[..len]);
11995 expect_status(&emitted[0], TID_UNSOLICITED, Status::PROP_NOT_FOUND);
11996
11997 let len = frame::prop_remove(&mut buf, TID_UNSOLICITED, key, &[0; 32]).unwrap();
11998 let emitted = admin(&mut session, &buf[..len]);
11999 expect_status(&emitted[0], TID_UNSOLICITED, Status::PROP_NOT_FOUND);
12000 }
12001 }
12002
12003 #[test]
12008 fn device_domain_key_material_may_be_provisioned_remotely() {
12009 let mut session = Session::new(test_config(), Status::RESET_POWER_ON, test_engine());
12010 session.attach(false);
12011 let mut buf = [0u8; 64];
12012
12013 let len = frame::prop_insert(
12015 &mut buf,
12016 5,
12017 prop::DEV_CHANNEL_KEYS,
12018 &[7u8; items::CHANNEL_KEY_LEN],
12019 )
12020 .unwrap();
12021 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
12022 expect_status(&emitted[0], 5, Status::INVALID_STATE);
12023
12024 let len = frame::prop_insert(
12025 &mut buf,
12026 TID_UNSOLICITED,
12027 prop::DEV_CHANNEL_KEYS,
12028 &[7u8; items::CHANNEL_KEY_LEN],
12029 )
12030 .unwrap();
12031 let emitted = admin(&mut session, &buf[..len]);
12032 assert_eq!(
12033 Frame::parse(&emitted[0]).unwrap().command(),
12034 Some(Cmd::PropInserted)
12035 );
12036 }
12037
12038 #[test]
12043 fn a_reset_produces_no_response_but_a_refused_restore_does() {
12044 let mut session = test_session();
12045 let mut buf = [0u8; 16];
12046
12047 let len = frame::reset(&mut buf, TID_UNSOLICITED).unwrap();
12048 let mut emitted = Vec::new();
12049 let effect = session.handle_admin_frame(&buf[..len], 0, 180, &mut |bytes: &[u8]| {
12050 emitted.push(bytes.to_vec())
12051 });
12052 session.end_admin_exchange();
12053 assert!(matches!(effect, Some(Effect::ApplyRadio(_))));
12054 assert!(emitted.is_empty(), "a reset is answered by no response");
12055
12056 let len = frame::restore(&mut buf, TID_UNSOLICITED).unwrap();
12057 let emitted = admin(&mut session, &buf[..len]);
12058 expect_status(&emitted[0], TID_UNSOLICITED, Status::INVALID_STATE);
12059 }
12060
12061 #[test]
12064 fn ending_an_exchange_restores_the_local_binding() {
12065 let mut session = test_session();
12066 let mut buf = [0u8; 16];
12067 let len = frame::prop_get(&mut buf, TID_UNSOLICITED, prop::HOST_AUTO_ACK).unwrap();
12068 assert_eq!(admin(&mut session, &buf[..len]).len(), 1);
12069
12070 let (emitted, _) = dispatch(&mut session, &buf[..len], 0);
12071 assert!(emitted.is_empty(), "the local binding is back");
12072 let (_, _, value) = parse_prop_is(&{
12074 let len = frame::prop_get(&mut buf, 3, prop::HOST_AUTO_ACK).unwrap();
12075 dispatch(&mut session, &buf[..len], 0).0[0].clone()
12076 });
12077 assert_eq!(value.len(), 1);
12078 }
12079
12080 #[test]
12083 fn a_multi_read_reports_out_of_reach_slots_in_place() {
12084 let mut session = test_session();
12085 let mut buf = [0u8; 64];
12086 let len = frame::prop_multi_get(
12087 &mut buf,
12088 TID_UNSOLICITED,
12089 &[prop::PROTOCOL_VERSION, prop::HOST_AUTO_ACK, prop::DEV_MODEL],
12090 )
12091 .unwrap();
12092 let mut emitted = Vec::new();
12093 let effect = session.handle_admin_frame(&buf[..len], 0, 180, &mut |bytes: &[u8]| {
12094 emitted.push(bytes.to_vec())
12095 });
12096 session.end_admin_exchange();
12097 assert!(effect.is_none());
12098 let parsed = Frame::parse(&emitted[0]).unwrap();
12099 assert_eq!(parsed.command(), Some(Cmd::PropAre));
12100 let entries: Vec<_> = MultiEntries::new(parsed.payload)
12101 .map(|entry| entry.unwrap())
12102 .map(|entry| (entry.key, entry.value.to_vec()))
12103 .collect();
12104 assert_eq!(entries.len(), 3);
12105 assert_eq!(entries[0].0, prop::PROTOCOL_VERSION);
12106 assert_eq!(entries[1].0, prop::LAST_STATUS);
12107 assert_eq!(entry_status(&entries[1].1), Status::PROP_NOT_FOUND);
12108 assert_eq!(entries[2].0, prop::DEV_MODEL);
12109 }
12110
12111 #[test]
12112 fn queue_properties_are_read_only_and_capacity_fixed() {
12113 let mut session = test_session();
12114 assert_eq!(
12115 get(&mut session, prop::HOST_RX_QUEUE_CAPACITY),
12116 (RX_QUEUE_CAPACITY as u16).to_le_bytes()
12117 );
12118 for (key, status) in [
12119 (prop::HOST_RX_QUEUE_COUNT, Status::INVALID_ARGUMENT),
12120 (prop::HOST_RX_QUEUE_DROPPED, Status::INVALID_ARGUMENT),
12121 (prop::HOST_RX_QUEUE_CAPACITY, Status::UNIMPLEMENTED),
12122 ] {
12123 let (emitted, effect) = set(&mut session, key, &0u16.to_le_bytes());
12124 assert!(effect.is_none());
12125 expect_status(&emitted[0], 2, status);
12126 }
12127 }
12128}