Skip to main content

hidpp/
device.rs

1//! Implements peripheral devices connected to HID++ channels.
2
3use std::{any::TypeId, collections::HashMap, sync::Arc, time::Duration};
4
5use futures::{FutureExt, select};
6use thiserror::Error;
7use tracing::trace;
8
9use crate::{
10    channel::{ChannelError, HidppChannel},
11    feature::{
12        self, CreatableFeature, Feature,
13        feature_set::{FeatureInformation, FeatureSetFeature},
14        root::RootFeature,
15    },
16    protocol::{self, ProtocolVersion, v20::Hidpp20Error},
17};
18
19/// Represents a single HID++ device connected to a [`HidppChannel`].
20///
21/// This is used only for peripheral devices and not receivers.
22#[derive(Clone)]
23pub struct Device {
24    /// The underlying HID++ channel.
25    chan: Arc<HidppChannel>,
26
27    /// Cached handle to the root feature. [`Self::new`] always installs one
28    /// before returning, so [`Self::root`] can hand it back directly instead
29    /// of going through the generic (and fallible) `features` lookup.
30    root: Arc<RootFeature>,
31
32    /// The initialized implementation of features the device supports.
33    features: HashMap<TypeId, Arc<dyn Feature>>,
34
35    /// The index of the device on the HID++ channel.
36    pub device_index: u8,
37
38    /// The supported protocol version reported by the device.
39    pub protocol_version: ProtocolVersion,
40}
41
42impl Device {
43    /// Tries to initialize a device on a HID++ channel.
44    ///
45    /// This will automatically ping the device to determine the protocol
46    /// version it supports via [`protocol::determine_version`].
47    ///
48    /// Returns [`DeviceError::DeviceNotFound`] if there is no device with the
49    /// specified index connected to the channel.
50    ///
51    /// Returns [`DeviceError::UnsupportedProtocolVersion`] if the device only
52    /// supports [`ProtocolVersion::V10`].
53    pub async fn new(chan: Arc<HidppChannel>, device_index: u8) -> Result<Self, DeviceError> {
54        let Some(version) = protocol::determine_version(&chan, device_index).await? else {
55            return Err(DeviceError::DeviceNotFound);
56        };
57
58        if version == ProtocolVersion::V10 {
59            return Err(DeviceError::UnsupportedProtocolVersion);
60        }
61
62        // Every HID++2.0 device supports the root feature.
63        // We implicitly verified that using [`protocol::determine_version`].
64        let mut features: HashMap<TypeId, Arc<dyn Feature>> = HashMap::new();
65        let root = insert_feature(
66            &mut features,
67            RootFeature::new(Arc::clone(&chan), device_index, 0),
68        );
69
70        Ok(Self {
71            chan,
72            root,
73            features,
74            device_index,
75            protocol_version: version,
76        })
77    }
78
79    /// A convenience wrapper around [`Self::get_feature`] to obtain the root
80    /// feature.
81    #[must_use]
82    pub fn root(&self) -> Arc<RootFeature> {
83        Arc::clone(&self.root)
84    }
85
86    /// Adds a new feature implementation to the list of available features.
87    /// This will override an existing implementation of the same type.
88    /// The caller is responsible for making sure the device actually supports
89    /// the feature.
90    pub fn add_feature_instance<F: Feature>(&mut self, feature: F) -> Arc<F> {
91        insert_feature(&mut self.features, feature)
92    }
93
94    /// Adds a new feature implementation to the list of available features.
95    /// This will override an existing implementation of the same type.
96    /// The caller is responsible for making sure the device actually supports
97    /// the feature.
98    ///
99    /// This method uses [`CreatableFeature`] to automatically create an
100    /// instance of the feature implementation and adds it using
101    /// [`Self::add_feature_instance`].
102    pub fn add_feature<F: CreatableFeature>(&mut self, feature_index: u8) -> Arc<F> {
103        self.add_feature_instance(F::new(
104            Arc::clone(&self.chan),
105            self.device_index,
106            feature_index,
107        ))
108    }
109
110    /// Checks whether a specific feature implementation is provided by the
111    /// device.
112    #[must_use]
113    pub fn provides_feature<F: Feature>(&self) -> bool {
114        self.features.contains_key(&TypeId::of::<F>())
115    }
116
117    /// Tries to retrieve a feature implementation from the device.
118    ///
119    /// Returns [`None`] if the requested feature implementation is not
120    /// provided.
121    #[must_use]
122    pub fn get_feature<F: Feature>(&self) -> Option<Arc<F>> {
123        self.features
124            .get(&TypeId::of::<F>())
125            .cloned()
126            .and_then(|feat| Arc::downcast::<F>(feat).ok())
127    }
128
129    /// Tries to detect all features supported by the device and add
130    /// implementations for them using [`feature::registry::lookup_version`].
131    ///
132    /// Returns a vector containing all feature IDs supported by the device.
133    ///
134    /// Returns `Ok(None)` if the [`FeatureSetFeature`] feature, which is
135    /// required for feature enumeration, is not supported by the device.
136    pub async fn enumerate_features(
137        &mut self,
138    ) -> Result<Option<Vec<FeatureInformation>>, Hidpp20Error> {
139        let Some(feature_set_info) = self.root().get_feature(FeatureSetFeature::ID).await? else {
140            return Ok(None);
141        };
142
143        let feature_set_feature = self.add_feature::<FeatureSetFeature>(feature_set_info.index);
144
145        let count = feature_set_feature.count().await?;
146        trace!(
147            index = self.device_index,
148            count, "enumerating feature table"
149        );
150        let mut features = Vec::with_capacity(count as usize);
151        for i in 1..=count {
152            let info = read_feature_entry(&feature_set_feature, self.device_index, i).await?;
153            trace!(
154                index = self.device_index,
155                slot = i,
156                id = format_args!("{:#06x}", info.id),
157                version = info.version,
158                "feature",
159            );
160            features.push(info);
161
162            if i == feature_set_info.index {
163                continue;
164            }
165
166            let Some(impls) = feature::registry::lookup_version(info.id, info.version) else {
167                continue;
168            };
169
170            for feat_impl in impls {
171                let (type_id, instance) =
172                    (feat_impl.producer)(Arc::clone(&self.chan), self.device_index, i);
173
174                self.features.insert(type_id, instance);
175            }
176        }
177
178        Ok(Some(features))
179    }
180}
181
182/// Inserts a feature implementation into a device's feature map, returning a
183/// concretely-typed handle to it.
184///
185/// Building the `Arc<F>` once and coercing a clone to `Arc<dyn Feature>` for
186/// storage avoids an erase-then-downcast round trip through the map, so the
187/// returned handle can never fail to be `F`.
188fn insert_feature<F: Feature>(
189    features: &mut HashMap<TypeId, Arc<dyn Feature>>,
190    feature: F,
191) -> Arc<F> {
192    let feat_rc = Arc::new(feature);
193    features.insert(TypeId::of::<F>(), Arc::clone(&feat_rc) as Arc<dyn Feature>);
194    feat_rc
195}
196
197/// Per-attempt deadline for one feature-table read during enumeration.
198///
199/// The channel's default [`crate::channel::SEND_RESPONSE_TIMEOUT`] (5s) is
200/// longer than the budget most callers give the whole walk, so one dropped
201/// report used to consume the caller's entire probe budget and abort
202/// enumeration. A HID++ round trip that is going to answer answers in tens of
203/// milliseconds; past this the report is lost, and re-asking beats waiting.
204const FEATURE_READ_ATTEMPT: Duration = Duration::from_millis(700);
205
206/// Attempts per feature-table entry before enumeration gives up on it.
207///
208/// Bluetooth-direct links drop individual reports while the table itself stays
209/// stable, so a lost entry is worth re-asking for rather than discarding a walk
210/// that may already be thirty entries deep.
211const FEATURE_READ_ATTEMPTS: u8 = 4;
212
213/// Pause between attempts, letting the link drain before re-asking.
214const FEATURE_READ_BACKOFF: Duration = Duration::from_millis(120);
215
216/// Reads one feature-table entry, re-asking under a short per-attempt deadline
217/// when the link drops the report.
218///
219/// A feature-level refusal ([`Hidpp20Error::Feature`]) or an unsupported
220/// response returns immediately: the device answered, so re-asking cannot
221/// change the answer. Only transport failures are retried.
222async fn read_feature_entry(
223    feature_set: &FeatureSetFeature,
224    device_index: u8,
225    index: u8,
226) -> Result<FeatureInformation, Hidpp20Error> {
227    let mut last_error = None;
228    for attempt in 1..=FEATURE_READ_ATTEMPTS {
229        let mut read = std::pin::pin!(feature_set.get_feature(index).fuse());
230        let outcome = select! {
231            result = read => Some(result),
232            () = futures_timer::Delay::new(FEATURE_READ_ATTEMPT).fuse() => None,
233        };
234        match outcome {
235            Some(Ok(info)) => return Ok(info),
236            Some(Err(e @ (Hidpp20Error::Feature(_) | Hidpp20Error::UnsupportedResponse))) => {
237                return Err(e);
238            }
239            Some(Err(e)) => last_error = Some(e),
240            None => trace!(
241                index = device_index,
242                slot = index,
243                attempt,
244                "feature-table read timed out — re-asking"
245            ),
246        }
247        if attempt < FEATURE_READ_ATTEMPTS {
248            futures_timer::Delay::new(FEATURE_READ_BACKOFF).await;
249        }
250    }
251    Err(last_error.unwrap_or(Hidpp20Error::Channel(ChannelError::Timeout)))
252}
253
254#[cfg(test)]
255#[allow(
256    clippy::unwrap_used,
257    clippy::expect_used,
258    reason = "expect/unwrap are idiomatic in tests"
259)]
260mod tests {
261    use std::sync::Arc;
262
263    use crate::{
264        channel::{HidppChannel, tests::MockRawHidChannel},
265        feature::{CreatableFeature as _, feature_set::FeatureSetFeature},
266        protocol::v20::Hidpp20Error,
267    };
268
269    use super::{FEATURE_READ_ATTEMPTS, read_feature_entry};
270
271    /// An entry whose report is lost is re-asked rather than abandoned. Aborting
272    /// on the first lost report is what made Bluetooth-direct enumeration give
273    /// up mid-table, which callers then misread as "not a peripheral".
274    #[test]
275    fn lost_feature_entry_is_retried_before_giving_up() {
276        futures::executor::block_on(async {
277            let (raw, handle) = MockRawHidChannel::new();
278            let channel = Arc::new(HidppChannel::from_raw_channel(raw).await.unwrap());
279            // The mock answers nothing, so every attempt runs to its deadline.
280            let feature_set = FeatureSetFeature::new(Arc::clone(&channel), 0xff, 0x01);
281
282            let err = read_feature_entry(&feature_set, 0xff, 1).await.unwrap_err();
283
284            assert!(
285                matches!(err, Hidpp20Error::Channel(_)),
286                "an unanswered entry surfaces as a transport failure, got {err:?}"
287            );
288            assert_eq!(
289                handle.written_reports().len(),
290                usize::from(FEATURE_READ_ATTEMPTS),
291                "every attempt should reach the wire"
292            );
293        });
294    }
295}
296
297/// Represents a device-specific error.
298#[derive(Debug, Error)]
299#[non_exhaustive]
300pub enum DeviceError {
301    /// Indicates that the underlying [`HidppChannel`] returned an error.
302    #[error("the HID++ channel returned an error")]
303    Channel(#[from] ChannelError),
304
305    /// Indicates that the specified device index points to no device.
306    #[error("there is no device with the specified device index")]
307    DeviceNotFound,
308
309    /// Indicates that the addressed device does only support HID++1.0.
310    #[error("the device does not support HID++2.0 or newer")]
311    UnsupportedProtocolVersion,
312}