use std::sync::Arc;
use hidpp::{channel::HidppChannel, device::Device, feature::CreatableFeature};
use thiserror::Error;
use tracing::debug;
use crate::adjustable_dpi::AdjustableDpiFeatureV0;
use crate::route::{DeviceRoute, open_route_channel};
use crate::smartshift::{SmartShiftFeatureV0, SmartShiftMode, SmartShiftStatus};
#[derive(Debug, Error)]
pub enum WriteError {
#[error("HID transport error")]
Hid(#[from] async_hid::HidError),
#[error("no connected device matched the route")]
DeviceNotFound,
#[error("device at index {index:#04x} did not respond to HID++")]
DeviceUnreachable { index: u8 },
#[error("device does not expose HID++ feature {feature_hex:#06x}")]
FeatureUnsupported { feature_hex: u16 },
#[error("HID++ protocol error: {0}")]
Hidpp(String),
}
#[derive(Debug, Clone, Copy)]
pub struct FeatureEntry {
pub id: u16,
pub version: u8,
}
pub async fn dump_features(route: &DeviceRoute) -> Result<Vec<FeatureEntry>, WriteError> {
use hidpp::feature::feature_set::FeatureSetFeature;
let index = route.device_index();
with_route(route, move |channel| async move {
let mut device = Device::new(Arc::clone(&channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let feature_set_info = device
.root()
.get_feature(FeatureSetFeature::ID)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?
.ok_or(WriteError::FeatureUnsupported {
feature_hex: FeatureSetFeature::ID,
})?;
let feature_set = device.add_feature::<FeatureSetFeature>(feature_set_info.index);
let count = feature_set
.count()
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
let mut entries = Vec::with_capacity(usize::from(count));
for i in 0..=count {
let info = feature_set
.get_feature(i)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
entries.push(FeatureEntry {
id: info.id,
version: info.version,
});
}
Ok(entries)
})
.await
}
async fn open_feature<F: CreatableFeature + 'static>(
device: &mut Device,
) -> Result<Arc<F>, WriteError> {
let info = device
.root()
.get_feature(F::ID)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?
.ok_or(WriteError::FeatureUnsupported { feature_hex: F::ID })?;
Ok(device.add_feature::<F>(info.index))
}
pub async fn get_dpi(route: &DeviceRoute) -> Result<u16, WriteError> {
let index = route.device_index();
with_route(route, move |channel| async move {
let mut device = Device::new(Arc::clone(&channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let feature = open_feature::<AdjustableDpiFeatureV0>(&mut device).await?;
feature
.get_sensor_dpi(0)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))
})
.await
}
pub async fn get_smartshift_status(route: &DeviceRoute) -> Result<SmartShiftStatus, WriteError> {
let index = route.device_index();
with_route(route, move |channel| async move {
let mut device = Device::new(Arc::clone(&channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let feature = open_feature::<SmartShiftFeatureV0>(&mut device).await?;
feature
.get_status()
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))
})
.await
}
pub async fn set_dpi(route: &DeviceRoute, dpi: u16) -> Result<(), WriteError> {
let index = route.device_index();
with_route(route, move |channel| async move {
set_dpi_on_channel(&channel, index, dpi).await
})
.await
}
async fn set_dpi_on_channel(
channel: &Arc<HidppChannel>,
index: u8,
dpi: u16,
) -> Result<(), WriteError> {
let mut device = Device::new(Arc::clone(channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let feature = open_feature::<AdjustableDpiFeatureV0>(&mut device).await?;
feature
.set_sensor_dpi(0, dpi)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
if let Ok(actual) = feature.get_sensor_dpi(0).await {
if actual == dpi {
debug!(index, dpi, "wrote DPI (verified)");
} else {
tracing::warn!(
index,
requested = dpi,
actual,
"DPI write accepted but device reports a different value — \
likely out of the device's supported range"
);
}
} else {
debug!(index, dpi, "wrote DPI (read-back skipped)");
}
Ok(())
}
pub async fn toggle_smartshift(route: &DeviceRoute) -> Result<SmartShiftMode, WriteError> {
let index = route.device_index();
with_route(route, move |channel| async move {
toggle_smartshift_on_channel(&channel, index).await
})
.await
}
async fn toggle_smartshift_on_channel(
channel: &Arc<HidppChannel>,
index: u8,
) -> Result<SmartShiftMode, WriteError> {
let mut device = Device::new(Arc::clone(channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let feature = open_feature::<SmartShiftFeatureV0>(&mut device).await?;
let SmartShiftStatus { mode, sensitivity } = feature
.get_status()
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
let next = mode.flipped();
feature
.set_status(next, sensitivity)
.await
.map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
debug!(index, ?next, "wrote SmartShift mode");
Ok(next)
}
#[derive(Clone)]
pub struct SharedChannel {
channel: Arc<HidppChannel>,
route: DeviceRoute,
}
impl SharedChannel {
#[must_use]
pub(crate) fn new(channel: Arc<HidppChannel>, route: DeviceRoute) -> Self {
Self { channel, route }
}
#[must_use]
pub fn matches(&self, route: &DeviceRoute) -> bool {
self.route == *route
}
}
pub async fn set_dpi_on(shared: &SharedChannel, dpi: u16) -> Result<(), WriteError> {
set_dpi_on_channel(&shared.channel, shared.route.device_index(), dpi).await
}
pub async fn toggle_smartshift_on(shared: &SharedChannel) -> Result<SmartShiftMode, WriteError> {
toggle_smartshift_on_channel(&shared.channel, shared.route.device_index()).await
}
async fn with_route<F, Fut, T>(route: &DeviceRoute, f: F) -> Result<T, WriteError>
where
F: FnOnce(Arc<HidppChannel>) -> Fut,
Fut: std::future::Future<Output = Result<T, WriteError>>,
{
match open_route_channel(route).await? {
Some(channel) => f(channel).await,
None => Err(WriteError::DeviceNotFound),
}
}