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, MultiEntries, MultiGetKeys, 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::DEV_MODEL => "PROP_DEV_MODEL",
23        prop::INTERFACE_TYPE => "PROP_INTERFACE_TYPE",
24        prop::CAPS => "PROP_CAPS",
25        prop::UPTIME => "PROP_UPTIME",
26        prop::PHY_ENABLED => "PROP_PHY_ENABLED",
27        prop::PHY_FREQ => "PROP_PHY_FREQ",
28        prop::PHY_TX_POWER => "PROP_PHY_TX_POWER",
29        prop::PHY_RSSI => "PROP_PHY_RSSI",
30        prop::PHY_LORA_BW => "PROP_PHY_LORA_BW",
31        prop::PHY_LORA_SF => "PROP_PHY_LORA_SF",
32        prop::PHY_LORA_CR => "PROP_PHY_LORA_CR",
33        prop::PHY_MTU => "PROP_PHY_MTU",
34        prop::PHY_LORA_SW => "PROP_PHY_LORA_SW",
35        prop::MAC_PROMISCUOUS => "PROP_MAC_PROMISCUOUS",
36        prop::MAC_BACKHAUL => "PROP_MAC_BACKHAUL",
37        prop::SAVED => "PROP_SAVED",
38        prop::DEV_KEY => "PROP_DEV_KEY",
39        prop::DEV_PRIVATE_KEY => "PROP_DEV_PRIVATE_KEY",
40        prop::DEV_CHANNEL_KEYS => "PROP_DEV_CHANNEL_KEYS",
41        prop::DEV_PEERS => "PROP_DEV_PEERS",
42        prop::DEV_NAME => "PROP_DEV_NAME",
43        prop::BATTERY => "PROP_BATTERY",
44        prop::MAC_REPEATER_ENABLED => "PROP_MAC_REPEATER_ENABLED",
45        prop::IDENT => "PROP_IDENT",
46        prop::IDENT_ROLE => "PROP_IDENT_ROLE",
47        prop::IDENT_MOBILE => "PROP_IDENT_MOBILE",
48        prop::MAC_REPEATER_REGIONS => "PROP_MAC_REPEATER_REGIONS",
49        prop::MAC_REPEATER_DEFAULT_REGION => "PROP_MAC_REPEATER_DEFAULT_REGION",
50        prop::MAC_REPEATER_MIN_RSSI => "PROP_MAC_REPEATER_MIN_RSSI",
51        prop::MAC_REPEATER_MIN_SNR => "PROP_MAC_REPEATER_MIN_SNR",
52        prop::DEV_DISCOVERABLE => "PROP_DEV_DISCOVERABLE",
53        prop::ALERT => "PROP_ALERT",
54        prop::ADVERT_INTERVAL => "PROP_ADVERT_INTERVAL",
55        prop::BEACON_INTERVAL => "PROP_BEACON_INTERVAL",
56        prop::STARTUP_BEACON => "PROP_STARTUP_BEACON",
57        prop::IDENT_LOCATION => "PROP_IDENT_LOCATION",
58        prop::IDENT_ALTITUDE => "PROP_IDENT_ALTITUDE",
59        prop::GNSS_ENABLED => "PROP_GNSS_ENABLED",
60        prop::GNSS_LOCATION => "PROP_GNSS_LOCATION",
61        prop::GNSS_ALTITUDE => "PROP_GNSS_ALTITUDE",
62        prop::GNSS_FIX => "PROP_GNSS_FIX",
63        prop::GNSS_PRECISION => "PROP_GNSS_PRECISION",
64        prop::GNSS_SATELLITES => "PROP_GNSS_SATELLITES",
65        prop::ILLUMINANCE => "PROP_ILLUMINANCE",
66        prop::HOST_KEY => "PROP_HOST_KEY",
67        prop::HOST_CHANNEL_KEYS => "PROP_HOST_CHANNEL_KEYS",
68        prop::HOST_PEER_KEYS => "PROP_HOST_PEER_KEYS",
69        prop::HOST_RX_FILTERS => "PROP_HOST_RX_FILTERS",
70        prop::HOST_AUTO_ACK => "PROP_HOST_AUTO_ACK",
71        prop::HOST_RX_QUEUE_COUNT => "PROP_HOST_RX_QUEUE_COUNT",
72        prop::HOST_RX_QUEUE_CAPACITY => "PROP_HOST_RX_QUEUE_CAPACITY",
73        prop::HOST_RX_QUEUE_DROPPED => "PROP_HOST_RX_QUEUE_DROPPED",
74        prop::PHY_DUTY_NOW => "PROP_PHY_DUTY_NOW",
75        prop::PHY_DUTY_LIMIT => "PROP_PHY_DUTY_LIMIT",
76        prop::STAT_TX_PACKETS => "PROP_STAT_TX_PACKETS",
77        prop::STAT_TX_CHANNEL_BUSY => "PROP_STAT_TX_CHANNEL_BUSY",
78        prop::STAT_RX_PACKETS => "PROP_STAT_RX_PACKETS",
79        prop::STAT_RX_BAD_CRC => "PROP_STAT_RX_BAD_CRC",
80        prop::STAT_RX_NON_UMSH => "PROP_STAT_RX_NON_UMSH",
81        prop::STAT_RX_ACCEPTED => "PROP_STAT_RX_ACCEPTED",
82        prop::STAT_FORWARDED => "PROP_STAT_FORWARDED",
83        prop::STAT_FORWARD_DROPPED => "PROP_STAT_FORWARD_DROPPED",
84        prop::STAT_FORWARD_CANCELLED => "PROP_STAT_FORWARD_CANCELLED",
85        prop::BLE_PAIRING_PIN => "PROP_BLE_PAIRING_PIN",
86        prop::DEV_ADMINS => "PROP_DEV_ADMINS",
87        prop::TIME => "PROP_TIME",
88        prop::TZ_OFFSET => "PROP_TZ_OFFSET",
89        prop::GNSS_IDENT_UPDATE => "PROP_GNSS_IDENT_UPDATE",
90        prop::GNSS_IDENT_PRECISION => "PROP_GNSS_IDENT_PRECISION",
91        prop::GNSS_TIME_TRUST => "PROP_GNSS_TIME_TRUST",
92        prop::BLE_ENABLED => "PROP_BLE_ENABLED",
93        prop::BLE_BOND_COUNT => "PROP_BLE_BOND_COUNT",
94        prop::BLE_LINK => "PROP_BLE_LINK",
95        prop::BLE_PAIRING => "PROP_BLE_PAIRING",
96        _ => return None,
97    })
98}
99
100/// Every property this module can name.
101///
102/// A tool that resolves a mnemonic back to an identifier derives its
103/// lookup from this list rather than keeping a second copy of the table:
104/// one place to add a property, one place that can fall out of date.
105pub const PROPERTIES: &[u32] = &[
106    prop::LAST_STATUS,
107    prop::PROTOCOL_VERSION,
108    prop::DEV_VERSION,
109    prop::DEV_MODEL,
110    prop::INTERFACE_TYPE,
111    prop::CAPS,
112    prop::UPTIME,
113    prop::PHY_ENABLED,
114    prop::PHY_FREQ,
115    prop::PHY_TX_POWER,
116    prop::PHY_RSSI,
117    prop::PHY_LORA_BW,
118    prop::PHY_LORA_SF,
119    prop::PHY_LORA_CR,
120    prop::PHY_MTU,
121    prop::PHY_LORA_SW,
122    prop::MAC_PROMISCUOUS,
123    prop::MAC_BACKHAUL,
124    prop::SAVED,
125    prop::DEV_KEY,
126    prop::DEV_PRIVATE_KEY,
127    prop::DEV_CHANNEL_KEYS,
128    prop::DEV_PEERS,
129    prop::DEV_NAME,
130    prop::BATTERY,
131    prop::MAC_REPEATER_ENABLED,
132    prop::IDENT,
133    prop::IDENT_ROLE,
134    prop::IDENT_MOBILE,
135    prop::MAC_REPEATER_REGIONS,
136    prop::MAC_REPEATER_DEFAULT_REGION,
137    prop::MAC_REPEATER_MIN_RSSI,
138    prop::MAC_REPEATER_MIN_SNR,
139    prop::DEV_DISCOVERABLE,
140    prop::ALERT,
141    prop::ADVERT_INTERVAL,
142    prop::BEACON_INTERVAL,
143    prop::STARTUP_BEACON,
144    prop::IDENT_LOCATION,
145    prop::IDENT_ALTITUDE,
146    prop::GNSS_ENABLED,
147    prop::GNSS_LOCATION,
148    prop::GNSS_ALTITUDE,
149    prop::GNSS_FIX,
150    prop::GNSS_PRECISION,
151    prop::GNSS_SATELLITES,
152    prop::ILLUMINANCE,
153    prop::HOST_KEY,
154    prop::HOST_CHANNEL_KEYS,
155    prop::HOST_PEER_KEYS,
156    prop::HOST_RX_FILTERS,
157    prop::HOST_AUTO_ACK,
158    prop::HOST_RX_QUEUE_COUNT,
159    prop::HOST_RX_QUEUE_CAPACITY,
160    prop::HOST_RX_QUEUE_DROPPED,
161    prop::PHY_DUTY_NOW,
162    prop::PHY_DUTY_LIMIT,
163    prop::STAT_TX_PACKETS,
164    prop::STAT_TX_CHANNEL_BUSY,
165    prop::STAT_RX_PACKETS,
166    prop::STAT_RX_BAD_CRC,
167    prop::STAT_RX_NON_UMSH,
168    prop::STAT_RX_ACCEPTED,
169    prop::STAT_FORWARDED,
170    prop::STAT_FORWARD_DROPPED,
171    prop::STAT_FORWARD_CANCELLED,
172    prop::BLE_PAIRING_PIN,
173    prop::DEV_ADMINS,
174    prop::TIME,
175    prop::TZ_OFFSET,
176    prop::GNSS_IDENT_UPDATE,
177    prop::GNSS_IDENT_PRECISION,
178    prop::GNSS_TIME_TRUST,
179    prop::BLE_ENABLED,
180    prop::BLE_BOND_COUNT,
181    prop::BLE_LINK,
182    prop::BLE_PAIRING,
183];
184
185/// How a property's octets are meant to be read.
186///
187/// Enough to render a value a person can check and to accept one typed
188/// by hand. Anything whose shape is a structure rather than a scalar —
189/// a battery snapshot, an interleaved location, a table of keys — is
190/// [`PropertyType::Bytes`], which is an honest answer: the octets are
191/// the value, and whatever decodes them knows more than this table does.
192///
193/// Numerics are little-endian, as ULCP has them throughout.
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub enum PropertyType {
196    /// One octet, 0 or 1.
197    Bool,
198    U8,
199    U16,
200    U32,
201    I8,
202    I16,
203    I32,
204    /// UTF-8, NUL-terminated on the wire.
205    Text,
206    /// A 32-octet Ed25519 public key or channel key.
207    Key32,
208    /// A status code in PUI form.
209    Status,
210    /// Octets whose meaning is structural, not scalar.
211    Bytes,
212}
213
214/// How to read a known property's octets.
215///
216/// A property absent from the table has no scalar reading, which is the
217/// same answer as [`PropertyType::Bytes`] but says so by omission: the
218/// caller knows it is looking at something this module has never heard
219/// of rather than something deliberately opaque.
220pub const fn property_type(key: u32) -> Option<PropertyType> {
221    use PropertyType::{Bool, Bytes, I8, I16, I32, Key32, Status, Text, U8, U16, U32};
222    Some(match key {
223        prop::LAST_STATUS => Status,
224        // Major and minor, one octet each.
225        prop::PROTOCOL_VERSION => Bytes,
226        prop::DEV_VERSION | prop::DEV_MODEL | prop::DEV_NAME => Text,
227        prop::INTERFACE_TYPE => U32,
228        // A list of capability codes in PUI form.
229        prop::CAPS => Bytes,
230        prop::UPTIME => U32,
231        prop::PHY_ENABLED => Bool,
232        prop::PHY_FREQ => U32,
233        prop::PHY_TX_POWER | prop::PHY_RSSI => I8,
234        prop::PHY_LORA_BW => U32,
235        prop::PHY_LORA_SF | prop::PHY_LORA_CR => U8,
236        prop::PHY_MTU | prop::PHY_LORA_SW => U16,
237        prop::MAC_PROMISCUOUS | prop::MAC_BACKHAUL => Bool,
238        prop::SAVED => U8,
239        prop::DEV_KEY | prop::DEV_PRIVATE_KEY | prop::HOST_KEY => Key32,
240        // Multiple-value tables, reported as digests.
241        prop::DEV_CHANNEL_KEYS
242        | prop::DEV_PEERS
243        | prop::DEV_ADMINS
244        | prop::HOST_CHANNEL_KEYS
245        | prop::HOST_PEER_KEYS
246        | prop::HOST_RX_FILTERS => Bytes,
247        // A flags octet followed by whichever components it claims.
248        prop::BATTERY => Bytes,
249        prop::MAC_REPEATER_ENABLED => Bool,
250        // The signed node-identity blob.
251        prop::IDENT => Bytes,
252        prop::IDENT_ROLE => U8,
253        prop::IDENT_MOBILE => Bool,
254        // UTF-8 region names, one per item.
255        prop::MAC_REPEATER_REGIONS => Text,
256        // A 2-octet region code.
257        prop::MAC_REPEATER_DEFAULT_REGION => Bytes,
258        prop::MAC_REPEATER_MIN_RSSI => I16,
259        prop::MAC_REPEATER_MIN_SNR => I8,
260        prop::DEV_DISCOVERABLE => Bool,
261        prop::ALERT => U8,
262        prop::ADVERT_INTERVAL | prop::BEACON_INTERVAL => U32,
263        prop::STARTUP_BEACON => Bool,
264        // Variable-precision interleaved coordinates.
265        prop::IDENT_LOCATION | prop::GNSS_LOCATION => Bytes,
266        // A minimal-length signed integer, which is its own encoding.
267        prop::IDENT_ALTITUDE => Bytes,
268        prop::GNSS_ENABLED => Bool,
269        prop::GNSS_ALTITUDE => I32,
270        prop::GNSS_FIX => U8,
271        prop::GNSS_PRECISION => U16,
272        // Used in the solution, optionally followed by in view.
273        prop::GNSS_SATELLITES => Bytes,
274        prop::ILLUMINANCE => U32,
275        prop::HOST_AUTO_ACK => Bool,
276        prop::HOST_RX_QUEUE_COUNT | prop::HOST_RX_QUEUE_CAPACITY => U16,
277        prop::HOST_RX_QUEUE_DROPPED => U32,
278        prop::PHY_DUTY_NOW | prop::PHY_DUTY_LIMIT => U16,
279        prop::STAT_TX_PACKETS
280        | prop::STAT_TX_CHANNEL_BUSY
281        | prop::STAT_RX_PACKETS
282        | prop::STAT_RX_BAD_CRC
283        | prop::STAT_RX_NON_UMSH
284        | prop::STAT_RX_ACCEPTED
285        | prop::STAT_FORWARDED
286        | prop::STAT_FORWARD_DROPPED
287        | prop::STAT_FORWARD_CANCELLED => U32,
288        prop::BLE_PAIRING_PIN => U32,
289        prop::TIME => U32,
290        prop::TZ_OFFSET => I16,
291        prop::GNSS_IDENT_UPDATE => Bool,
292        prop::GNSS_IDENT_PRECISION => U8,
293        prop::GNSS_TIME_TRUST | prop::BLE_ENABLED | prop::BLE_PAIRING => Bool,
294        prop::BLE_BOND_COUNT | prop::BLE_LINK => U8,
295        _ => return None,
296    })
297}
298
299/// The spec mnemonic for a known capability code.
300pub const fn capability_name(code: u32) -> Option<&'static str> {
301    Some(match code {
302        cap::WRITABLE_RAW_STREAM => "WRITABLE_RAW_STREAM",
303        cap::PHY_DUTY_LIMIT => "PHY_DUTY_LIMIT",
304        cap::PHY_LORA => "PHY_LORA",
305        cap::HOST_FILTER => "HOST_FILTER",
306        cap::HOST_RX_QUEUE => "HOST_RX_QUEUE",
307        cap::HOST_KEYS => "HOST_KEYS",
308        cap::HOST_AUTO_ACK => "HOST_AUTO_ACK",
309        cap::SAVE => "SAVE",
310        cap::DEV_IDENTITY => "DEV_IDENTITY",
311        cap::DEV_NAME => "DEV_NAME",
312        cap::BATTERY => "BATTERY",
313        cap::REPEATER => "REPEATER",
314        cap::IDENT => "IDENT",
315        cap::ALERT => "ALERT",
316        cap::TIME => "TIME",
317        cap::GNSS => "GNSS",
318        cap::ADVERT => "ADVERT",
319        cap::ILLUMINANCE => "ILLUMINANCE",
320        cap::MAC_BACKHAUL => "MAC_BACKHAUL",
321        cap::ADMIN => "ADMIN",
322        cap::CMD_MULTI => "CMD_MULTI",
323        cap::BLE => "BLE",
324        cap::REBOOT => "REBOOT",
325        cap::STATS => "STATS",
326        _ => return None,
327    })
328}
329
330/// A display adapter for one ULCP frame.
331///
332/// Values are summarized by length and never dumped, so callers can safely use
333/// the result in logs even for secret-bearing property writes.
334pub struct FrameDescription<'a>(pub &'a [u8]);
335
336impl fmt::Display for FrameDescription<'_> {
337    fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
338        let bytes = self.0;
339        let Ok(frame) = Frame::parse(bytes) else {
340            return write!(out, "malformed frame ({} bytes)", bytes.len());
341        };
342        let tid = frame.header.tid();
343        let Some(command) = frame.command() else {
344            return write!(out, "unknown command tid={tid} ({} bytes)", bytes.len());
345        };
346        match command {
347            Cmd::Nop
348            | Cmd::Reset
349            | Cmd::QueueDrain
350            | Cmd::Save
351            | Cmd::Clear
352            | Cmd::Restore
353            | Cmd::FactoryReset
354            | Cmd::Reboot
355            | Cmd::BleClearBonds => {
356                write!(out, "{command:?} tid={tid}")
357            }
358            Cmd::PropGet
359            | Cmd::PropSet
360            | Cmd::PropIs
361            | Cmd::PropInsert
362            | Cmd::PropRemove
363            | Cmd::PropInserted
364            | Cmd::PropRemoved => {
365                let Ok(payload) = PropPayload::parse(frame.payload) else {
366                    return write!(out, "{command:?} tid={tid} (malformed payload)");
367                };
368                let name = property_name(payload.key);
369                if payload.key == prop::LAST_STATUS && command == Cmd::PropIs {
370                    let status = pui::decode(payload.value)
371                        .map(|(code, _)| Status(code))
372                        .unwrap_or(Status::FAILURE);
373                    if let Some(name) = name {
374                        write!(out, "{command:?} tid={tid} {name} = {status:?}")
375                    } else {
376                        write!(
377                            out,
378                            "{command:?} tid={tid} prop {} = {status:?}",
379                            payload.key
380                        )
381                    }
382                } else if let Some(name) = name {
383                    write!(
384                        out,
385                        "{command:?} tid={tid} {name} ({} value bytes)",
386                        payload.value.len()
387                    )
388                } else {
389                    write!(
390                        out,
391                        "{command:?} tid={tid} prop {} ({} value bytes)",
392                        payload.key,
393                        payload.value.len()
394                    )
395                }
396            }
397            Cmd::PropMultiGet => {
398                let mut count = 0usize;
399                for key in MultiGetKeys::new(frame.payload) {
400                    if key.is_err() {
401                        return write!(out, "{command:?} tid={tid} (malformed payload)");
402                    }
403                    count += 1;
404                }
405                write!(out, "{command:?} tid={tid} ({count} properties)")
406            }
407            Cmd::PropMultiSet | Cmd::PropAre => {
408                let mut count = 0usize;
409                let mut value_bytes = 0usize;
410                for entry in MultiEntries::new(frame.payload) {
411                    let Ok(entry) = entry else {
412                        return write!(out, "{command:?} tid={tid} (malformed payload)");
413                    };
414                    count += 1;
415                    value_bytes += entry.value.len();
416                }
417                write!(
418                    out,
419                    "{command:?} tid={tid} ({count} entries, {value_bytes} value bytes)"
420                )
421            }
422            Cmd::StrSend | Cmd::StrRecv => match StreamPayload::parse(frame.payload) {
423                Ok(payload) => write!(
424                    out,
425                    "{command:?} tid={tid} stream={} ({} data bytes, {} meta bytes)",
426                    payload.stream,
427                    payload.data.len(),
428                    payload.metadata.len()
429                ),
430                Err(_) => write!(out, "{command:?} tid={tid} (malformed payload)"),
431            },
432        }
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use crate::frame;
440
441    #[test]
442    fn describes_known_and_unknown_properties_without_values() {
443        let mut buf = [0u8; 16];
444        let len = frame::prop_set(&mut buf, 2, prop::PHY_FREQ, &[0x7e, 0x42]).unwrap();
445        assert_eq!(
446            FrameDescription(&buf[..len]).to_string(),
447            "PropSet tid=2 PROP_PHY_FREQ (2 value bytes)"
448        );
449
450        let len = frame::prop_get(&mut buf, 3, 60_000).unwrap();
451        assert_eq!(
452            FrameDescription(&buf[..len]).to_string(),
453            "PropGet tid=3 prop 60000 (0 value bytes)"
454        );
455    }
456
457    #[test]
458    fn describes_status_and_malformed_frames() {
459        let mut buf = [0u8; 8];
460        let len = frame::last_status(&mut buf, 4, Status::OK).unwrap();
461        assert_eq!(
462            FrameDescription(&buf[..len]).to_string(),
463            "PropIs tid=4 PROP_LAST_STATUS = Status::OK"
464        );
465        assert_eq!(
466            FrameDescription(&[0x80]).to_string(),
467            "malformed frame (1 bytes)"
468        );
469    }
470
471    #[test]
472    fn describes_multi_property_frames() {
473        let mut buf = [0u8; 64];
474        let len = frame::prop_multi_get(&mut buf, 1, &[prop::CAPS, prop::DEV_ADMINS]).unwrap();
475        assert_eq!(
476            FrameDescription(&buf[..len]).to_string(),
477            "PropMultiGet tid=1 (2 properties)"
478        );
479
480        let mut writer = frame::prop_are(&mut buf, 2).unwrap();
481        writer.write_entry(prop::PHY_TX_POWER, &[14]).unwrap();
482        writer.write_status_entry(Status::PROP_NOT_FOUND).unwrap();
483        let len = writer.finish();
484        assert_eq!(
485            FrameDescription(&buf[..len]).to_string(),
486            "PropAre tid=2 (2 entries, 2 value bytes)"
487        );
488
489        // A body length that runs past the end of the payload.
490        let malformed = [0x82, 0x17, 0x09, 0x71, 0xAA];
491        assert_eq!(
492            FrameDescription(&malformed).to_string(),
493            "PropAre tid=2 (malformed payload)"
494        );
495    }
496
497    #[test]
498    fn names_capabilities() {
499        assert_eq!(capability_name(cap::HOST_RX_QUEUE), Some("HOST_RX_QUEUE"));
500        assert_eq!(capability_name(cap::BATTERY), Some("BATTERY"));
501        assert_eq!(capability_name(cap::REPEATER), Some("REPEATER"));
502        assert_eq!(capability_name(cap::IDENT), Some("IDENT"));
503        assert_eq!(capability_name(cap::TIME), Some("TIME"));
504        assert_eq!(capability_name(cap::GNSS), Some("GNSS"));
505        assert_eq!(capability_name(60_000), None);
506    }
507
508    #[test]
509    fn names_time_and_positioning_properties() {
510        assert_eq!(property_name(prop::TIME), Some("PROP_TIME"));
511        assert_eq!(property_name(prop::TZ_OFFSET), Some("PROP_TZ_OFFSET"));
512        assert_eq!(property_name(prop::GNSS_ENABLED), Some("PROP_GNSS_ENABLED"));
513        assert_eq!(
514            property_name(prop::GNSS_LOCATION),
515            Some("PROP_GNSS_LOCATION")
516        );
517        assert_eq!(
518            property_name(prop::GNSS_ALTITUDE),
519            Some("PROP_GNSS_ALTITUDE")
520        );
521        assert_eq!(property_name(prop::GNSS_FIX), Some("PROP_GNSS_FIX"));
522        assert_eq!(
523            property_name(prop::GNSS_PRECISION),
524            Some("PROP_GNSS_PRECISION")
525        );
526        assert_eq!(
527            property_name(prop::GNSS_SATELLITES),
528            Some("PROP_GNSS_SATELLITES")
529        );
530        assert_eq!(
531            property_name(prop::GNSS_IDENT_UPDATE),
532            Some("PROP_GNSS_IDENT_UPDATE")
533        );
534        assert_eq!(
535            property_name(prop::GNSS_IDENT_PRECISION),
536            Some("PROP_GNSS_IDENT_PRECISION")
537        );
538        assert_eq!(
539            property_name(prop::GNSS_TIME_TRUST),
540            Some("PROP_GNSS_TIME_TRUST")
541        );
542    }
543
544    #[test]
545    fn names_repeater_policy_properties() {
546        assert_eq!(
547            property_name(prop::MAC_REPEATER_REGIONS),
548            Some("PROP_MAC_REPEATER_REGIONS")
549        );
550        assert_eq!(
551            property_name(prop::MAC_REPEATER_DEFAULT_REGION),
552            Some("PROP_MAC_REPEATER_DEFAULT_REGION")
553        );
554        assert_eq!(
555            property_name(prop::MAC_REPEATER_MIN_RSSI),
556            Some("PROP_MAC_REPEATER_MIN_RSSI")
557        );
558        assert_eq!(
559            property_name(prop::MAC_REPEATER_MIN_SNR),
560            Some("PROP_MAC_REPEATER_MIN_SNR")
561        );
562        assert_eq!(property_name(prop::IDENT_ROLE), Some("PROP_IDENT_ROLE"));
563    }
564
565    #[test]
566    fn names_the_bluetooth_toggle() {
567        assert_eq!(property_name(prop::BLE_ENABLED), Some("PROP_BLE_ENABLED"));
568        assert_eq!(property_type(prop::BLE_ENABLED), Some(PropertyType::Bool));
569    }
570
571    /// Bluetooth has one capability and three properties. Everything past
572    /// reachability is discovered by asking, so nothing here may grow a
573    /// capability of its own without the tables saying so out loud.
574    #[test]
575    fn names_the_bond_count_and_the_link() {
576        assert_eq!(
577            property_name(prop::BLE_BOND_COUNT),
578            Some("PROP_BLE_BOND_COUNT")
579        );
580        assert_eq!(property_type(prop::BLE_BOND_COUNT), Some(PropertyType::U8));
581        assert_eq!(property_name(prop::BLE_LINK), Some("PROP_BLE_LINK"));
582        assert_eq!(property_type(prop::BLE_LINK), Some(PropertyType::U8));
583        assert_eq!(property_name(prop::BLE_PAIRING), Some("PROP_BLE_PAIRING"));
584        assert_eq!(
585            property_type(prop::BLE_PAIRING),
586            Some(PropertyType::Bool),
587            "the window is a state you set, not an act you request"
588        );
589        assert_eq!(capability_name(cap::BLE), Some("BLE"));
590        // One past the last allocated code: an unassigned capability has
591        // no name to give, whatever a device claims by advertising it.
592        assert_eq!(capability_name(cap::STATS + 1), None);
593    }
594
595    /// The three tables answer for the same set of properties. A name
596    /// with no entry in `PROPERTIES` cannot be looked up backwards, and
597    /// one with no type reads back as hex forever — both are the kind of
598    /// omission that happens when a property is added in a hurry.
599    #[test]
600    fn every_named_property_is_listed_and_typed() {
601        for &key in PROPERTIES {
602            assert!(
603                property_name(key).is_some(),
604                "prop {key} is listed but unnamed"
605            );
606            assert!(
607                property_type(key).is_some(),
608                "prop {key} is listed but untyped"
609            );
610        }
611        // The listing is a set, not a sequence with repeats.
612        for (index, &key) in PROPERTIES.iter().enumerate() {
613            assert!(
614                !PROPERTIES[..index].contains(&key),
615                "prop {key} is listed twice"
616            );
617        }
618    }
619
620    #[test]
621    fn property_types_follow_the_wire() {
622        assert_eq!(property_type(prop::PHY_FREQ), Some(PropertyType::U32));
623        assert_eq!(property_type(prop::PHY_TX_POWER), Some(PropertyType::I8));
624        assert_eq!(property_type(prop::DEV_NAME), Some(PropertyType::Text));
625        assert_eq!(property_type(prop::DEV_KEY), Some(PropertyType::Key32));
626        assert_eq!(property_type(prop::TZ_OFFSET), Some(PropertyType::I16));
627        assert_eq!(
628            property_type(prop::HOST_RX_QUEUE_DROPPED),
629            Some(PropertyType::U32)
630        );
631        // Structured values are honestly opaque rather than mis-typed.
632        assert_eq!(property_type(prop::BATTERY), Some(PropertyType::Bytes));
633        assert_eq!(property_type(60_000), None);
634    }
635
636    #[test]
637    fn describes_battery_snapshots_and_the_empty_form() {
638        let mut buf = [0u8; 16];
639        let len = frame::prop_is(&mut buf, 5, prop::BATTERY, &[0b101, 0x74, 0x0E, 0]).unwrap();
640        assert_eq!(
641            FrameDescription(&buf[..len]).to_string(),
642            "PropIs tid=5 PROP_BATTERY (4 value bytes)"
643        );
644
645        let len = frame::prop_is(&mut buf, 6, prop::BATTERY, &[]).unwrap();
646        assert_eq!(
647            FrameDescription(&buf[..len]).to_string(),
648            "PropIs tid=6 PROP_BATTERY (0 value bytes)"
649        );
650    }
651}