1use core::fmt;
8
9use crate::{
10 Status,
11 frame::{Cmd, Frame, PropPayload, StreamPayload},
12 ids::{cap, prop},
13 pui,
14};
15
16pub const fn property_name(key: u32) -> Option<&'static str> {
18 Some(match key {
19 prop::LAST_STATUS => "PROP_LAST_STATUS",
20 prop::PROTOCOL_VERSION => "PROP_PROTOCOL_VERSION",
21 prop::DEV_VERSION => "PROP_DEV_VERSION",
22 prop::INTERFACE_TYPE => "PROP_INTERFACE_TYPE",
23 prop::CAPS => "PROP_CAPS",
24 prop::PHY_ENABLED => "PROP_PHY_ENABLED",
25 prop::PHY_FREQ => "PROP_PHY_FREQ",
26 prop::PHY_TX_POWER => "PROP_PHY_TX_POWER",
27 prop::PHY_RSSI => "PROP_PHY_RSSI",
28 prop::PHY_LORA_BW => "PROP_PHY_LORA_BW",
29 prop::PHY_LORA_SF => "PROP_PHY_LORA_SF",
30 prop::PHY_LORA_CR => "PROP_PHY_LORA_CR",
31 prop::PHY_MTU => "PROP_PHY_MTU",
32 prop::PHY_LORA_SW => "PROP_PHY_LORA_SW",
33 prop::MAC_PROMISCUOUS => "PROP_MAC_PROMISCUOUS",
34 prop::SAVED => "PROP_SAVED",
35 prop::DEV_KEY => "PROP_DEV_KEY",
36 prop::DEV_PRIVATE_KEY => "PROP_DEV_PRIVATE_KEY",
37 prop::DEV_CHANNEL_KEYS => "PROP_DEV_CHANNEL_KEYS",
38 prop::DEV_PEERS => "PROP_DEV_PEERS",
39 prop::DEV_NAME => "PROP_DEV_NAME",
40 prop::BATTERY => "PROP_BATTERY",
41 prop::MAC_REPEATER_ENABLED => "PROP_MAC_REPEATER_ENABLED",
42 prop::IDENT => "PROP_IDENT",
43 prop::IDENT_ROLE => "PROP_IDENT_ROLE",
44 prop::IDENT_MOBILE => "PROP_IDENT_MOBILE",
45 prop::MAC_REPEATER_REGIONS => "PROP_MAC_REPEATER_REGIONS",
46 prop::MAC_REPEATER_DEFAULT_REGION => "PROP_MAC_REPEATER_DEFAULT_REGION",
47 prop::MAC_REPEATER_MIN_RSSI => "PROP_MAC_REPEATER_MIN_RSSI",
48 prop::MAC_REPEATER_MIN_SNR => "PROP_MAC_REPEATER_MIN_SNR",
49 prop::DEV_DISCOVERABLE => "PROP_DEV_DISCOVERABLE",
50 prop::ALERT => "PROP_ALERT",
51 prop::ADVERT_INTERVAL => "PROP_ADVERT_INTERVAL",
52 prop::BEACON_INTERVAL => "PROP_BEACON_INTERVAL",
53 prop::STARTUP_BEACON => "PROP_STARTUP_BEACON",
54 prop::GNSS_ENABLED => "PROP_GNSS_ENABLED",
55 prop::GNSS_LOCATION => "PROP_GNSS_LOCATION",
56 prop::GNSS_ALTITUDE => "PROP_GNSS_ALTITUDE",
57 prop::GNSS_FIX => "PROP_GNSS_FIX",
58 prop::GNSS_PRECISION => "PROP_GNSS_PRECISION",
59 prop::GNSS_SATELLITES => "PROP_GNSS_SATELLITES",
60 prop::ILLUMINANCE => "PROP_ILLUMINANCE",
61 prop::HOST_KEY => "PROP_HOST_KEY",
62 prop::HOST_CHANNEL_KEYS => "PROP_HOST_CHANNEL_KEYS",
63 prop::HOST_PEER_KEYS => "PROP_HOST_PEER_KEYS",
64 prop::HOST_RX_FILTERS => "PROP_HOST_RX_FILTERS",
65 prop::HOST_AUTO_ACK => "PROP_HOST_AUTO_ACK",
66 prop::HOST_RX_QUEUE_COUNT => "PROP_HOST_RX_QUEUE_COUNT",
67 prop::HOST_RX_QUEUE_CAPACITY => "PROP_HOST_RX_QUEUE_CAPACITY",
68 prop::HOST_RX_QUEUE_DROPPED => "PROP_HOST_RX_QUEUE_DROPPED",
69 prop::PHY_DUTY_NOW => "PROP_PHY_DUTY_NOW",
70 prop::PHY_DUTY_LIMIT => "PROP_PHY_DUTY_LIMIT",
71 prop::BLE_PAIRING_PIN => "PROP_BLE_PAIRING_PIN",
72 prop::TIME => "PROP_TIME",
73 prop::TZ_OFFSET => "PROP_TZ_OFFSET",
74 prop::GNSS_IDENT_UPDATE => "PROP_GNSS_IDENT_UPDATE",
75 prop::GNSS_IDENT_PRECISION => "PROP_GNSS_IDENT_PRECISION",
76 prop::GNSS_TIME_TRUST => "PROP_GNSS_TIME_TRUST",
77 _ => return None,
78 })
79}
80
81pub const fn capability_name(code: u32) -> Option<&'static str> {
83 Some(match code {
84 cap::WRITABLE_RAW_STREAM => "WRITABLE_RAW_STREAM",
85 cap::PHY_DUTY_LIMIT => "PHY_DUTY_LIMIT",
86 cap::PHY_LORA => "PHY_LORA",
87 cap::HOST_FILTER => "HOST_FILTER",
88 cap::HOST_RX_QUEUE => "HOST_RX_QUEUE",
89 cap::HOST_KEYS => "HOST_KEYS",
90 cap::HOST_AUTO_ACK => "HOST_AUTO_ACK",
91 cap::SAVE => "SAVE",
92 cap::DEV_IDENTITY => "DEV_IDENTITY",
93 cap::DEV_NAME => "DEV_NAME",
94 cap::BATTERY => "BATTERY",
95 cap::REPEATER => "REPEATER",
96 cap::IDENT => "IDENT",
97 cap::ALERT => "ALERT",
98 cap::TIME => "TIME",
99 cap::GNSS => "GNSS",
100 cap::ADVERT => "ADVERT",
101 cap::ILLUMINANCE => "ILLUMINANCE",
102 _ => return None,
103 })
104}
105
106pub struct FrameDescription<'a>(pub &'a [u8]);
111
112impl fmt::Display for FrameDescription<'_> {
113 fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
114 let bytes = self.0;
115 let Ok(frame) = Frame::parse(bytes) else {
116 return write!(out, "malformed frame ({} bytes)", bytes.len());
117 };
118 let tid = frame.header.tid();
119 let Some(command) = frame.command() else {
120 return write!(out, "unknown command tid={tid} ({} bytes)", bytes.len());
121 };
122 match command {
123 Cmd::Nop
124 | Cmd::Reset
125 | Cmd::QueueDrain
126 | Cmd::Save
127 | Cmd::Clear
128 | Cmd::Restore
129 | Cmd::FactoryReset => {
130 write!(out, "{command:?} tid={tid}")
131 }
132 Cmd::PropGet
133 | Cmd::PropSet
134 | Cmd::PropIs
135 | Cmd::PropInsert
136 | Cmd::PropRemove
137 | Cmd::PropInserted
138 | Cmd::PropRemoved => {
139 let Ok(payload) = PropPayload::parse(frame.payload) else {
140 return write!(out, "{command:?} tid={tid} (malformed payload)");
141 };
142 let name = property_name(payload.key);
143 if payload.key == prop::LAST_STATUS && command == Cmd::PropIs {
144 let status = pui::decode(payload.value)
145 .map(|(code, _)| Status(code))
146 .unwrap_or(Status::FAILURE);
147 if let Some(name) = name {
148 write!(out, "{command:?} tid={tid} {name} = {status:?}")
149 } else {
150 write!(
151 out,
152 "{command:?} tid={tid} prop {} = {status:?}",
153 payload.key
154 )
155 }
156 } else if let Some(name) = name {
157 write!(
158 out,
159 "{command:?} tid={tid} {name} ({} value bytes)",
160 payload.value.len()
161 )
162 } else {
163 write!(
164 out,
165 "{command:?} tid={tid} prop {} ({} value bytes)",
166 payload.key,
167 payload.value.len()
168 )
169 }
170 }
171 Cmd::StrSend | Cmd::StrRecv => match StreamPayload::parse(frame.payload) {
172 Ok(payload) => write!(
173 out,
174 "{command:?} tid={tid} stream={} ({} data bytes, {} meta bytes)",
175 payload.stream,
176 payload.data.len(),
177 payload.metadata.len()
178 ),
179 Err(_) => write!(out, "{command:?} tid={tid} (malformed payload)"),
180 },
181 }
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::frame;
189
190 #[test]
191 fn describes_known_and_unknown_properties_without_values() {
192 let mut buf = [0u8; 16];
193 let len = frame::prop_set(&mut buf, 2, prop::PHY_FREQ, &[0x7e, 0x42]).unwrap();
194 assert_eq!(
195 FrameDescription(&buf[..len]).to_string(),
196 "PropSet tid=2 PROP_PHY_FREQ (2 value bytes)"
197 );
198
199 let len = frame::prop_get(&mut buf, 3, 60_000).unwrap();
200 assert_eq!(
201 FrameDescription(&buf[..len]).to_string(),
202 "PropGet tid=3 prop 60000 (0 value bytes)"
203 );
204 }
205
206 #[test]
207 fn describes_status_and_malformed_frames() {
208 let mut buf = [0u8; 8];
209 let len = frame::last_status(&mut buf, 4, Status::OK).unwrap();
210 assert_eq!(
211 FrameDescription(&buf[..len]).to_string(),
212 "PropIs tid=4 PROP_LAST_STATUS = Status::OK"
213 );
214 assert_eq!(
215 FrameDescription(&[0x80]).to_string(),
216 "malformed frame (1 bytes)"
217 );
218 }
219
220 #[test]
221 fn names_capabilities() {
222 assert_eq!(capability_name(cap::HOST_RX_QUEUE), Some("HOST_RX_QUEUE"));
223 assert_eq!(capability_name(cap::BATTERY), Some("BATTERY"));
224 assert_eq!(capability_name(cap::REPEATER), Some("REPEATER"));
225 assert_eq!(capability_name(cap::IDENT), Some("IDENT"));
226 assert_eq!(capability_name(cap::TIME), Some("TIME"));
227 assert_eq!(capability_name(cap::GNSS), Some("GNSS"));
228 assert_eq!(capability_name(60_000), None);
229 }
230
231 #[test]
232 fn names_time_and_positioning_properties() {
233 assert_eq!(property_name(prop::TIME), Some("PROP_TIME"));
234 assert_eq!(property_name(prop::TZ_OFFSET), Some("PROP_TZ_OFFSET"));
235 assert_eq!(property_name(prop::GNSS_ENABLED), Some("PROP_GNSS_ENABLED"));
236 assert_eq!(
237 property_name(prop::GNSS_LOCATION),
238 Some("PROP_GNSS_LOCATION")
239 );
240 assert_eq!(
241 property_name(prop::GNSS_ALTITUDE),
242 Some("PROP_GNSS_ALTITUDE")
243 );
244 assert_eq!(property_name(prop::GNSS_FIX), Some("PROP_GNSS_FIX"));
245 assert_eq!(
246 property_name(prop::GNSS_PRECISION),
247 Some("PROP_GNSS_PRECISION")
248 );
249 assert_eq!(
250 property_name(prop::GNSS_SATELLITES),
251 Some("PROP_GNSS_SATELLITES")
252 );
253 assert_eq!(
254 property_name(prop::GNSS_IDENT_UPDATE),
255 Some("PROP_GNSS_IDENT_UPDATE")
256 );
257 assert_eq!(
258 property_name(prop::GNSS_IDENT_PRECISION),
259 Some("PROP_GNSS_IDENT_PRECISION")
260 );
261 assert_eq!(
262 property_name(prop::GNSS_TIME_TRUST),
263 Some("PROP_GNSS_TIME_TRUST")
264 );
265 }
266
267 #[test]
268 fn names_repeater_policy_properties() {
269 assert_eq!(
270 property_name(prop::MAC_REPEATER_REGIONS),
271 Some("PROP_MAC_REPEATER_REGIONS")
272 );
273 assert_eq!(
274 property_name(prop::MAC_REPEATER_DEFAULT_REGION),
275 Some("PROP_MAC_REPEATER_DEFAULT_REGION")
276 );
277 assert_eq!(
278 property_name(prop::MAC_REPEATER_MIN_RSSI),
279 Some("PROP_MAC_REPEATER_MIN_RSSI")
280 );
281 assert_eq!(
282 property_name(prop::MAC_REPEATER_MIN_SNR),
283 Some("PROP_MAC_REPEATER_MIN_SNR")
284 );
285 assert_eq!(property_name(prop::IDENT_ROLE), Some("PROP_IDENT_ROLE"));
286 }
287
288 #[test]
289 fn describes_battery_snapshots_and_the_empty_form() {
290 let mut buf = [0u8; 16];
291 let len = frame::prop_is(&mut buf, 5, prop::BATTERY, &[0b101, 0x74, 0x0E, 0]).unwrap();
292 assert_eq!(
293 FrameDescription(&buf[..len]).to_string(),
294 "PropIs tid=5 PROP_BATTERY (4 value bytes)"
295 );
296
297 let len = frame::prop_is(&mut buf, 6, prop::BATTERY, &[]).unwrap();
298 assert_eq!(
299 FrameDescription(&buf[..len]).to_string(),
300 "PropIs tid=6 PROP_BATTERY (0 value bytes)"
301 );
302 }
303}