umsh_ulcp/
describe.rs

1//! Allocation-free human-readable descriptions of ULCP values.
2//!
3//! Keeping this layer next to the wire grammar gives native tools, firmware
4//! diagnostics, and browser clients one shared vocabulary without requiring
5//! any of them to allocate.
6
7use core::fmt;
8
9use crate::{
10    Status,
11    frame::{Cmd, Frame, PropPayload, StreamPayload},
12    ids::{cap, prop},
13    pui,
14};
15
16/// The spec mnemonic for a known property identifier.
17pub 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
81/// The spec mnemonic for a known capability code.
82pub 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
106/// A display adapter for one ULCP frame.
107///
108/// Values are summarized by length and never dumped, so callers can safely use
109/// the result in logs even for secret-bearing property writes.
110pub 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}