1use std::sync::Arc;
2
3use hidpp::{
4 device::Device,
5 feature::{
6 CreatableFeature,
7 adjustable_dpi::AdjustableDpiFeature,
8 extended_dpi::{DpiDirection, DpiRange, ExtendedDpiFeature, SetDpiParameters},
9 },
10 protocol::v20::{ErrorType, Hidpp20Error},
11};
12use tracing::debug;
13
14use crate::SharedChannel;
15use crate::backend::HidBackend;
16use crate::channel::route::DeviceRoute;
17
18use super::{HidppOperation, WriteError, classify_hidpp_error, with_route};
19
20pub use openlogi_core::hid::dpi::{Dpi, DpiCapabilities, DpiInfo};
24
25const SENSOR: u8 = 0;
29
30enum DpiFeature {
38 Adjustable(Arc<AdjustableDpiFeature>),
40
41 Extended(Arc<ExtendedDpiFeature>),
44}
45
46impl DpiFeature {
47 async fn open(device: &mut Device) -> Result<Self, WriteError> {
54 if let Some(index) = feature_index(device, AdjustableDpiFeature::ID).await? {
55 return Ok(Self::Adjustable(device.add_feature(index)));
56 }
57 if let Some(index) = feature_index(device, ExtendedDpiFeature::ID).await? {
58 return Ok(Self::Extended(device.add_feature(index)));
59 }
60 Err(WriteError::FeatureUnsupported {
64 feature_hex: AdjustableDpiFeature::ID,
65 })
66 }
67
68 const fn id(&self) -> u16 {
70 match self {
71 Self::Adjustable(_) => AdjustableDpiFeature::ID,
72 Self::Extended(_) => ExtendedDpiFeature::ID,
73 }
74 }
75
76 async fn sensor_count(&self) -> Result<u8, Hidpp20Error> {
78 match self {
79 Self::Adjustable(feature) => feature.get_sensor_count().await,
80 Self::Extended(feature) => feature.get_sensor_count().await,
81 }
82 }
83
84 async fn current_dpi(&self) -> Result<Dpi, Hidpp20Error> {
86 match self {
87 Self::Adjustable(feature) => feature.get_sensor_dpi(SENSOR).await.map(Dpi::from),
88 Self::Extended(feature) => Ok(feature
89 .get_sensor_dpi_parameters(SENSOR)
90 .await?
91 .dpi_x
92 .into()),
93 }
94 }
95
96 async fn supported_dpi(&self) -> Result<Vec<u16>, Hidpp20Error> {
98 match self {
99 Self::Adjustable(feature) => feature.get_sensor_dpi_list(SENSOR).await,
100 Self::Extended(feature) => {
101 let ranges = feature
105 .get_sensor_dpi_ranges(SENSOR, DpiDirection::X)
106 .await?;
107 Ok(expand_dpi_ranges(&ranges))
108 }
109 }
110 }
111
112 async fn set_dpi(&self, dpi: Dpi) -> Result<(), Hidpp20Error> {
114 let dpi = dpi.into();
115 match self {
116 Self::Adjustable(feature) => feature.set_sensor_dpi(SENSOR, dpi).await,
117 Self::Extended(feature) => {
118 let current = feature.get_sensor_dpi_parameters(SENSOR).await?;
124 feature
125 .set_sensor_dpi_parameters(
126 SENSOR,
127 SetDpiParameters {
128 dpi_x: dpi,
129 dpi_y: if current.dpi_y == 0 { 0 } else { dpi },
135 lod: current.lod,
136 },
137 )
138 .await
139 }
140 }
141 }
142}
143
144async fn feature_index(device: &mut Device, feature_hex: u16) -> Result<Option<u8>, WriteError> {
151 Ok(device
152 .root()
153 .get_feature(feature_hex)
154 .await
155 .map_err(|e| classify_hidpp_error(e, HidppOperation::ResolveFeature, feature_hex))?
156 .map(|info| info.index))
157}
158
159pub(super) fn expand_dpi_ranges(ranges: &[DpiRange]) -> Vec<u16> {
166 let mut values = Vec::new();
167 for range in ranges {
168 match *range {
169 DpiRange::Fixed(value) => values.push(value),
170 DpiRange::Stepped { from, to, step } => {
171 let mut value = u32::from(from);
174 while value < u32::from(to) {
175 if let Ok(value) = u16::try_from(value) {
176 values.push(value);
177 }
178 value += u32::from(step);
179 }
180 values.push(to);
181 }
182 }
183 }
184 values
185}
186
187pub async fn get_dpi(backend: &dyn HidBackend, route: &DeviceRoute) -> Result<Dpi, WriteError> {
191 let index = route.device_index();
192 with_route(backend, route, move |channel| async move {
193 get_dpi_on_channel(&channel, index).await
194 })
195 .await
196}
197
198async fn get_dpi_on_channel(
199 channel: &Arc<hidpp::channel::HidppChannel>,
200 index: u8,
201) -> Result<Dpi, WriteError> {
202 let mut device = Device::new(Arc::clone(channel), index)
203 .await
204 .map_err(|_| WriteError::DeviceUnreachable { index })?;
205 let feature = DpiFeature::open(&mut device).await?;
206 feature
207 .current_dpi()
208 .await
209 .map_err(|e| classify_hidpp_error(e, HidppOperation::ReadDpi, feature.id()))
210}
211
212fn classify_dpi_error(feature_hex: u16, error: Hidpp20Error) -> WriteError {
219 match error {
220 Hidpp20Error::Feature(ErrorType::Unsupported | ErrorType::InvalidFunctionId)
221 | Hidpp20Error::UnsupportedResponse => WriteError::FeatureUnsupported { feature_hex },
222 other => classify_hidpp_error(other, HidppOperation::ReadDpiCapabilities, feature_hex),
223 }
224}
225
226pub async fn get_dpi_info(
229 backend: &dyn HidBackend,
230 route: &DeviceRoute,
231) -> Result<DpiInfo, WriteError> {
232 let index = route.device_index();
233 with_route(backend, route, move |channel| async move {
234 get_dpi_info_on_channel(&channel, index).await
235 })
236 .await
237}
238
239pub(super) async fn get_dpi_info_on_channel(
240 channel: &Arc<hidpp::channel::HidppChannel>,
241 index: u8,
242) -> Result<DpiInfo, WriteError> {
243 let mut device = Device::new(Arc::clone(channel), index)
244 .await
245 .map_err(|_| WriteError::DeviceUnreachable { index })?;
246 let feature = DpiFeature::open(&mut device).await?;
247 let feature_hex = feature.id();
248 let sensor_count = feature
249 .sensor_count()
250 .await
251 .map_err(|e| classify_dpi_error(feature_hex, e))?;
252 if sensor_count == 0 {
253 return Err(WriteError::FeatureUnsupported { feature_hex });
256 }
257 let current = feature
258 .current_dpi()
259 .await
260 .map_err(|e| classify_dpi_error(feature_hex, e))?;
261 let values = feature
262 .supported_dpi()
263 .await
264 .map_err(|e| classify_dpi_error(feature_hex, e))?;
265 Ok(DpiInfo {
266 current,
267 capabilities: DpiCapabilities::new(values)?,
268 })
269}
270
271pub async fn set_dpi(
273 backend: &dyn HidBackend,
274 route: &DeviceRoute,
275 dpi: Dpi,
276) -> Result<(), WriteError> {
277 let index = route.device_index();
278 with_route(backend, route, move |channel| async move {
279 set_dpi_on_channel(&channel, index, dpi).await
280 })
281 .await
282}
283
284pub(super) async fn set_dpi_on_channel(
288 channel: &Arc<hidpp::channel::HidppChannel>,
289 index: u8,
290 dpi: Dpi,
291) -> Result<(), WriteError> {
292 let mut device = Device::new(Arc::clone(channel), index)
293 .await
294 .map_err(|_| WriteError::DeviceUnreachable { index })?;
295 let feature = DpiFeature::open(&mut device).await?;
296 feature
297 .set_dpi(dpi)
298 .await
299 .map_err(|e| classify_hidpp_error(e, HidppOperation::WriteDpi, feature.id()))?;
300 if let Ok(actual) = feature.current_dpi().await {
306 if actual == dpi {
307 debug!(index, %dpi, "wrote DPI (verified)");
308 } else {
309 tracing::warn!(
310 index,
311 requested = %dpi,
312 %actual,
313 "DPI write accepted but device reports a different value — \
314 likely out of the device's supported range"
315 );
316 }
317 } else {
318 debug!(index, %dpi, "wrote DPI (read-back skipped)");
319 }
320 Ok(())
321}
322
323pub async fn set_dpi_on(shared: &SharedChannel, dpi: Dpi) -> Result<(), WriteError> {
326 set_dpi_on_channel(shared.channel(), shared.device_index(), dpi).await
327}
328
329pub async fn get_dpi_info_on(shared: &SharedChannel) -> Result<DpiInfo, WriteError> {
331 get_dpi_info_on_channel(shared.channel(), shared.device_index()).await
332}