use std::{any::TypeId, collections::HashMap, sync::Arc, time::Duration};
use futures::{FutureExt, select};
use thiserror::Error;
use tracing::trace;
use crate::{
channel::{ChannelError, HidppChannel},
feature::{
self, CreatableFeature, Feature,
feature_set::{FeatureInformation, FeatureSetFeature},
root::RootFeature,
},
protocol::{self, ProtocolVersion, v20::Hidpp20Error},
};
#[derive(Clone)]
pub struct Device {
chan: Arc<HidppChannel>,
features: HashMap<TypeId, Arc<dyn Feature>>,
pub device_index: u8,
pub protocol_version: ProtocolVersion,
}
impl Device {
pub async fn new(chan: Arc<HidppChannel>, device_index: u8) -> Result<Self, DeviceError> {
let protocol_version = protocol::determine_version(&chan, device_index).await?;
if protocol_version.is_none() {
return Err(DeviceError::DeviceNotFound);
}
let version = protocol_version.unwrap();
if version == ProtocolVersion::V10 {
return Err(DeviceError::UnsupportedProtocolVersion);
}
let mut device = Self {
chan,
features: HashMap::new(),
device_index,
protocol_version: version,
};
device.add_feature::<RootFeature>(0);
Ok(device)
}
pub fn root(&self) -> Arc<RootFeature> {
self.get_feature::<RootFeature>().unwrap()
}
pub fn add_feature_instance<F: Feature>(&mut self, feature: F) -> Arc<F> {
let feat_rc: Arc<dyn Feature> = Arc::new(feature);
self.features
.insert(TypeId::of::<F>(), Arc::clone(&feat_rc));
Arc::downcast::<F>(feat_rc).unwrap()
}
pub fn add_feature<F: CreatableFeature>(&mut self, feature_index: u8) -> Arc<F> {
self.add_feature_instance(F::new(
Arc::clone(&self.chan),
self.device_index,
feature_index,
))
}
pub fn provides_feature<F: Feature>(&self) -> bool {
self.features.contains_key(&TypeId::of::<F>())
}
pub fn get_feature<F: Feature>(&self) -> Option<Arc<F>> {
self.features
.get(&TypeId::of::<F>())
.cloned()
.and_then(|feat| Arc::downcast::<F>(feat).ok())
}
pub async fn enumerate_features(
&mut self,
) -> Result<Option<Vec<FeatureInformation>>, Hidpp20Error> {
let Some(feature_set_info) = self.root().get_feature(FeatureSetFeature::ID).await? else {
return Ok(None);
};
let feature_set_feature = self.add_feature::<FeatureSetFeature>(feature_set_info.index);
let count = feature_set_feature.count().await?;
trace!(
index = self.device_index,
count, "enumerating feature table"
);
let mut features = Vec::with_capacity(count as usize);
for i in 1..=count {
let info = read_feature_entry(&feature_set_feature, self.device_index, i).await?;
trace!(
index = self.device_index,
slot = i,
id = format_args!("{:#06x}", info.id),
version = info.version,
"feature",
);
features.push(info);
if i == feature_set_info.index {
continue;
}
let Some(impls) = feature::registry::lookup_version(info.id, info.version) else {
continue;
};
for feat_impl in impls {
let (type_id, instance) =
(feat_impl.producer)(Arc::clone(&self.chan), self.device_index, i);
self.features.insert(type_id, instance);
}
}
Ok(Some(features))
}
}
const FEATURE_READ_ATTEMPT: Duration = Duration::from_millis(700);
const FEATURE_READ_ATTEMPTS: u8 = 4;
const FEATURE_READ_BACKOFF: Duration = Duration::from_millis(120);
async fn read_feature_entry(
feature_set: &FeatureSetFeature,
device_index: u8,
index: u8,
) -> Result<FeatureInformation, Hidpp20Error> {
let mut last_error = None;
for attempt in 1..=FEATURE_READ_ATTEMPTS {
let mut read = std::pin::pin!(feature_set.get_feature(index).fuse());
let outcome = select! {
result = read => Some(result),
_ = futures_timer::Delay::new(FEATURE_READ_ATTEMPT).fuse() => None,
};
match outcome {
Some(Ok(info)) => return Ok(info),
Some(Err(e @ (Hidpp20Error::Feature(_) | Hidpp20Error::UnsupportedResponse))) => {
return Err(e);
}
Some(Err(e)) => last_error = Some(e),
None => trace!(
index = device_index,
slot = index,
attempt,
"feature-table read timed out — re-asking"
),
}
if attempt < FEATURE_READ_ATTEMPTS {
futures_timer::Delay::new(FEATURE_READ_BACKOFF).await;
}
}
Err(last_error.unwrap_or(Hidpp20Error::Channel(ChannelError::Timeout)))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::{
channel::{HidppChannel, tests::MockRawHidChannel},
feature::{CreatableFeature as _, feature_set::FeatureSetFeature},
protocol::v20::Hidpp20Error,
};
use super::{FEATURE_READ_ATTEMPTS, read_feature_entry};
#[test]
fn lost_feature_entry_is_retried_before_giving_up() {
futures::executor::block_on(async {
let (raw, handle) = MockRawHidChannel::new();
let channel = Arc::new(HidppChannel::from_raw_channel(raw).await.unwrap());
let feature_set = FeatureSetFeature::new(Arc::clone(&channel), 0xff, 0x01);
let err = read_feature_entry(&feature_set, 0xff, 1).await.unwrap_err();
assert!(
matches!(err, Hidpp20Error::Channel(_)),
"an unanswered entry surfaces as a transport failure, got {err:?}"
);
assert_eq!(
handle.written_reports().len(),
usize::from(FEATURE_READ_ATTEMPTS),
"every attempt should reach the wire"
);
});
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DeviceError {
#[error("the HID++ channel returned an error")]
Channel(#[from] ChannelError),
#[error("there is no device with the specified device index")]
DeviceNotFound,
#[error("the device does not support HID++2.0 or newer")]
UnsupportedProtocolVersion,
}