use std::sync::Arc;
use hidpp::{
device::Device,
feature::{CreatableFeature, adjustable_dpi::AdjustableDpiFeature},
protocol::v20::{ErrorType, Hidpp20Error},
};
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::route::DeviceRoute;
use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DpiCapabilities {
values: Vec<u16>,
}
impl DpiCapabilities {
pub fn new(mut values: Vec<u16>) -> Result<Self, WriteError> {
values.sort_unstable();
values.dedup();
if values.is_empty() {
return Err(WriteError::EmptyDpiList);
}
Ok(Self { values })
}
#[must_use]
pub fn values(&self) -> &[u16] {
&self.values
}
#[must_use]
pub fn min(&self) -> u16 {
self.values[0]
}
#[must_use]
pub fn max(&self) -> u16 {
self.values[self.values.len() - 1]
}
#[must_use]
pub fn contains(&self, dpi: u16) -> bool {
self.values.binary_search(&dpi).is_ok()
}
#[must_use]
pub fn nearest(&self, dpi: u32) -> u16 {
let mut nearest = self.values[0];
let mut best_delta = u32::from(nearest).abs_diff(dpi);
for &candidate in &self.values[1..] {
let delta = u32::from(candidate).abs_diff(dpi);
if delta < best_delta {
nearest = candidate;
best_delta = delta;
}
}
nearest
}
#[must_use]
pub fn snap(&self, dpi: u32) -> u32 {
u32::from(self.nearest(dpi))
}
#[must_use]
pub fn step_hint(&self) -> u16 {
self.values
.windows(2)
.filter_map(|pair| pair[1].checked_sub(pair[0]))
.filter(|step| *step > 0)
.min()
.unwrap_or(1)
}
#[must_use]
pub fn adjacent_test_target(&self, current: u16) -> Option<u16> {
if self.values.len() < 2 {
return None;
}
match self.values.binary_search(¤t) {
Ok(index) if index + 1 < self.values.len() => Some(self.values[index + 1]),
Ok(index) if index > 0 => Some(self.values[index - 1]),
Ok(_) => None,
Err(index) if index < self.values.len() => Some(self.values[index]),
Err(_) => self.values.last().copied(),
}
.filter(|target| *target != current)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DpiInfo {
pub current: u16,
pub capabilities: DpiCapabilities,
}
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::<AdjustableDpiFeature>(&mut device).await?;
feature
.get_sensor_dpi(0)
.await
.map_err(|e| classify_hidpp_error(e, HidppOperation::ReadDpi, AdjustableDpiFeature::ID))
})
.await
}
fn classify_dpi_error(error: Hidpp20Error) -> WriteError {
match error {
Hidpp20Error::Feature(ErrorType::Unsupported | ErrorType::InvalidFunctionId)
| Hidpp20Error::UnsupportedResponse => WriteError::FeatureUnsupported {
feature_hex: AdjustableDpiFeature::ID,
},
other => classify_hidpp_error(
other,
HidppOperation::ReadDpiCapabilities,
AdjustableDpiFeature::ID,
),
}
}
pub async fn get_dpi_info(route: &DeviceRoute) -> Result<DpiInfo, 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::<AdjustableDpiFeature>(&mut device).await?;
let sensor_count = feature
.get_sensor_count()
.await
.map_err(classify_dpi_error)?;
if sensor_count == 0 {
return Err(WriteError::FeatureUnsupported {
feature_hex: AdjustableDpiFeature::ID,
});
}
let current = feature
.get_sensor_dpi(0)
.await
.map_err(classify_dpi_error)?;
let values = feature
.get_sensor_dpi_list(0)
.await
.map_err(classify_dpi_error)?;
Ok(DpiInfo {
current,
capabilities: DpiCapabilities::new(values)?,
})
})
.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
}
pub(super) async fn set_dpi_on_channel(
channel: &Arc<hidpp::channel::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::<AdjustableDpiFeature>(&mut device).await?;
feature
.set_sensor_dpi(0, dpi)
.await
.map_err(|e| classify_hidpp_error(e, HidppOperation::WriteDpi, AdjustableDpiFeature::ID))?;
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(())
}