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