umshctl/command/info/
props.rs

1//! A bag of property values, and the reading of one.
2//!
3//! Everything `info` prints comes from a set of properties fetched
4//! together. Once fetched, a renderer works against this rather than
5//! against a device handle, which is what makes the renderers testable
6//! without a radio and what keeps the round trips in one place.
7
8use std::collections::HashMap;
9
10use umsh::ulcp::{FrameLink, UlcpDevice};
11use umsh::ulcp_wire::Status;
12use umsh::ulcp_wire::ids::cap;
13
14/// Properties fetched together, each either answered or refused.
15///
16/// A refusal is an answer: an OPTIONAL property a device does not
17/// implement refuses the read, and over the mesh the unreachable half of
18/// the property space refuses every position. Both mean "absent", which
19/// is why the accessors collapse them.
20///
21/// An *empty* value is not absent. Several properties spell "no fix",
22/// "no threshold", or "never tag" as zero octets, so [`Self::bytes`]
23/// hands back an empty slice rather than `None` when the device answered
24/// with one.
25#[derive(Debug, Default)]
26pub struct PropSet {
27    values: HashMap<u32, Result<Vec<u8>, Status>>,
28}
29
30impl PropSet {
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    pub fn insert(&mut self, key: u32, answer: Result<Vec<u8>, Status>) {
36        self.values.insert(key, answer);
37    }
38
39    /// Whether the device answered this property at all.
40    pub fn answered(&self, key: u32) -> bool {
41        matches!(self.values.get(&key), Some(Ok(_)))
42    }
43
44    /// The octets the device answered with, if it answered.
45    pub fn bytes(&self, key: u32) -> Option<&[u8]> {
46        match self.values.get(&key) {
47            Some(Ok(value)) => Some(value),
48            _ => None,
49        }
50    }
51
52    /// The octets, treating an empty answer as no answer.
53    ///
54    /// For the properties where empty is the device saying it has
55    /// nothing rather than saying zero.
56    pub fn non_empty(&self, key: u32) -> Option<&[u8]> {
57        self.bytes(key).filter(|value| !value.is_empty())
58    }
59
60    /// The status a refused property came back with.
61    pub fn refusal(&self, key: u32) -> Option<Status> {
62        match self.values.get(&key) {
63            Some(Err(status)) => Some(*status),
64            _ => None,
65        }
66    }
67
68    pub fn bool(&self, key: u32) -> Option<bool> {
69        self.bytes(key)?.first().map(|&byte| byte != 0)
70    }
71
72    pub fn u8(&self, key: u32) -> Option<u8> {
73        self.bytes(key)?.first().copied()
74    }
75
76    pub fn i8(&self, key: u32) -> Option<i8> {
77        self.u8(key).map(|byte| byte as i8)
78    }
79
80    pub fn u16(&self, key: u32) -> Option<u16> {
81        <[u8; 2]>::try_from(self.bytes(key)?)
82            .ok()
83            .map(u16::from_le_bytes)
84    }
85
86    pub fn i16(&self, key: u32) -> Option<i16> {
87        <[u8; 2]>::try_from(self.bytes(key)?)
88            .ok()
89            .map(i16::from_le_bytes)
90    }
91
92    pub fn u32(&self, key: u32) -> Option<u32> {
93        <[u8; 4]>::try_from(self.bytes(key)?)
94            .ok()
95            .map(u32::from_le_bytes)
96    }
97
98    pub fn i32(&self, key: u32) -> Option<i32> {
99        <[u8; 4]>::try_from(self.bytes(key)?)
100            .ok()
101            .map(i32::from_le_bytes)
102    }
103
104    /// A STRING property, without the NUL the wire carries.
105    pub fn text(&self, key: u32) -> Option<String> {
106        let value = self.bytes(key)?;
107        Some(
108            String::from_utf8_lossy(value)
109                .trim_end_matches('\0')
110                .to_owned(),
111        )
112    }
113
114    pub fn key32(&self, key: u32) -> Option<[u8; 32]> {
115        <[u8; 32]>::try_from(self.bytes(key)?).ok()
116    }
117}
118
119/// Read `keys` in as few exchanges as the device allows.
120///
121/// One `CMD_PROP_MULTI_GET`, continued where a reply ran out of room.
122/// A device without `CAP_CMD_MULTI` never learned the command, so it
123/// gets the same questions one at a time — the report is identical
124/// either way, and only the cost differs.
125///
126/// A key the device refuses lands in the set as a refusal rather than
127/// ending the read: assembling a report is exactly the case where one
128/// unimplemented property must not cost the other eleven.
129pub async fn fetch<L: FrameLink>(
130    device: &mut UlcpDevice<L>,
131    keys: &[u32],
132    batched: bool,
133) -> anyhow::Result<PropSet> {
134    let mut set = PropSet::new();
135    if keys.is_empty() {
136        return Ok(set);
137    }
138    if batched {
139        let answers = device.read_each(keys).await?;
140        for (&key, answer) in keys.iter().zip(answers) {
141            set.insert(key, answer);
142        }
143        return Ok(set);
144    }
145    for &key in keys {
146        let answer = match device.get_prop(key).await {
147            Ok(value) => Ok(value),
148            // A device that refused says which way; a link that failed
149            // has ended the conversation, and pressing on with eleven
150            // more questions it cannot hear helps nobody.
151            Err(umsh::ulcp::UlcpError::Status(status)) => Err(status),
152            Err(error) => return Err(error.into()),
153        };
154        set.insert(key, answer);
155    }
156    Ok(set)
157}
158
159/// Whether this device serves the multi-property commands.
160pub fn batched(caps: &[u32]) -> bool {
161    caps.contains(&cap::CMD_MULTI)
162}