use std::sync::Arc;
use hidpp::{
device::Device,
feature::{
CreatableFeature,
adjustable_dpi::AdjustableDpiFeature,
extended_dpi::{DpiDirection, DpiRange, ExtendedDpiFeature, SetDpiParameters},
},
protocol::v20::{ErrorType, Hidpp20Error},
};
use tracing::debug;
use crate::route::DeviceRoute;
use super::{HidppOperation, WriteError, classify_hidpp_error, with_route};
pub use openlogi_core::hid::dpi::{DpiCapabilities, DpiInfo};
const SENSOR: u8 = 0;
enum DpiFeature {
Adjustable(Arc<AdjustableDpiFeature>),
Extended(Arc<ExtendedDpiFeature>),
}
impl DpiFeature {
async fn open(device: &mut Device) -> Result<Self, WriteError> {
if let Some(index) = feature_index(device, AdjustableDpiFeature::ID).await? {
return Ok(Self::Adjustable(device.add_feature(index)));
}
if let Some(index) = feature_index(device, ExtendedDpiFeature::ID).await? {
return Ok(Self::Extended(device.add_feature(index)));
}
Err(WriteError::FeatureUnsupported {
feature_hex: AdjustableDpiFeature::ID,
})
}
const fn id(&self) -> u16 {
match self {
Self::Adjustable(_) => AdjustableDpiFeature::ID,
Self::Extended(_) => ExtendedDpiFeature::ID,
}
}
async fn sensor_count(&self) -> Result<u8, Hidpp20Error> {
match self {
Self::Adjustable(feature) => feature.get_sensor_count().await,
Self::Extended(feature) => feature.get_sensor_count().await,
}
}
async fn current_dpi(&self) -> Result<u16, Hidpp20Error> {
match self {
Self::Adjustable(feature) => feature.get_sensor_dpi(SENSOR).await,
Self::Extended(feature) => Ok(feature.get_sensor_dpi_parameters(SENSOR).await?.dpi_x),
}
}
async fn supported_dpi(&self) -> Result<Vec<u16>, Hidpp20Error> {
match self {
Self::Adjustable(feature) => feature.get_sensor_dpi_list(SENSOR).await,
Self::Extended(feature) => {
let ranges = feature
.get_sensor_dpi_ranges(SENSOR, DpiDirection::X)
.await?;
Ok(expand_dpi_ranges(&ranges))
}
}
}
async fn set_dpi(&self, dpi: u16) -> Result<(), Hidpp20Error> {
match self {
Self::Adjustable(feature) => feature.set_sensor_dpi(SENSOR, dpi).await,
Self::Extended(feature) => {
let current = feature.get_sensor_dpi_parameters(SENSOR).await?;
feature
.set_sensor_dpi_parameters(
SENSOR,
SetDpiParameters {
dpi_x: dpi,
dpi_y: if current.dpi_y == 0 { 0 } else { dpi },
lod: current.lod,
},
)
.await
}
}
}
}
async fn feature_index(device: &mut Device, feature_hex: u16) -> Result<Option<u8>, WriteError> {
Ok(device
.root()
.get_feature(feature_hex)
.await
.map_err(|e| classify_hidpp_error(e, HidppOperation::ResolveFeature, feature_hex))?
.map(|info| info.index))
}
pub(super) fn expand_dpi_ranges(ranges: &[DpiRange]) -> Vec<u16> {
let mut values = Vec::new();
for range in ranges {
match *range {
DpiRange::Fixed(value) => values.push(value),
DpiRange::Stepped { from, to, step } => {
let mut value = u32::from(from);
while value < u32::from(to) {
if let Ok(value) = u16::try_from(value) {
values.push(value);
}
value += u32::from(step);
}
values.push(to);
}
}
}
values
}
pub async fn get_dpi(route: &DeviceRoute) -> Result<u16, WriteError> {
let index = route.device_index();
with_route(route, move |channel| async move {
get_dpi_on_channel(&channel, index).await
})
.await
}
async fn get_dpi_on_channel(
channel: &Arc<hidpp::channel::HidppChannel>,
index: u8,
) -> Result<u16, WriteError> {
let mut device = Device::new(Arc::clone(channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let feature = DpiFeature::open(&mut device).await?;
feature
.current_dpi()
.await
.map_err(|e| classify_hidpp_error(e, HidppOperation::ReadDpi, feature.id()))
}
fn classify_dpi_error(feature_hex: u16, error: Hidpp20Error) -> WriteError {
match error {
Hidpp20Error::Feature(ErrorType::Unsupported | ErrorType::InvalidFunctionId)
| Hidpp20Error::UnsupportedResponse => WriteError::FeatureUnsupported { feature_hex },
other => classify_hidpp_error(other, HidppOperation::ReadDpiCapabilities, feature_hex),
}
}
pub async fn get_dpi_info(route: &DeviceRoute) -> Result<DpiInfo, WriteError> {
let index = route.device_index();
with_route(route, move |channel| async move {
get_dpi_info_on_channel(&channel, index).await
})
.await
}
pub(super) async fn get_dpi_info_on_channel(
channel: &Arc<hidpp::channel::HidppChannel>,
index: u8,
) -> Result<DpiInfo, WriteError> {
let mut device = Device::new(Arc::clone(channel), index)
.await
.map_err(|_| WriteError::DeviceUnreachable { index })?;
let feature = DpiFeature::open(&mut device).await?;
let feature_hex = feature.id();
let sensor_count = feature
.sensor_count()
.await
.map_err(|e| classify_dpi_error(feature_hex, e))?;
if sensor_count == 0 {
return Err(WriteError::FeatureUnsupported { feature_hex });
}
let current = feature
.current_dpi()
.await
.map_err(|e| classify_dpi_error(feature_hex, e))?;
let values = feature
.supported_dpi()
.await
.map_err(|e| classify_dpi_error(feature_hex, e))?;
Ok(DpiInfo {
current,
capabilities: DpiCapabilities::new(values)?,
})
}
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 = DpiFeature::open(&mut device).await?;
feature
.set_dpi(dpi)
.await
.map_err(|e| classify_hidpp_error(e, HidppOperation::WriteDpi, feature.id()))?;
if let Ok(actual) = feature.current_dpi().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(())
}