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::channel::route::DeviceRoute;
16
17use super::{HidppOperation, WriteError, classify_hidpp_error, with_route};
18
19pub use openlogi_core::hid::dpi::{Dpi, DpiCapabilities, DpiInfo};
23
24const SENSOR: u8 = 0;
28
29enum DpiFeature {
37 Adjustable(Arc<AdjustableDpiFeature>),
39
40 Extended(Arc<ExtendedDpiFeature>),
43}
44
45impl DpiFeature {
46 async fn open(device: &mut Device) -> Result<Self, WriteError> {
53 if let Some(index) = feature_index(device, AdjustableDpiFeature::ID).await? {
54 return Ok(Self::Adjustable(device.add_feature(index)));
55 }
56 if let Some(index) = feature_index(device, ExtendedDpiFeature::ID).await? {
57 return Ok(Self::Extended(device.add_feature(index)));
58 }
59 Err(WriteError::FeatureUnsupported {
63 feature_hex: AdjustableDpiFeature::ID,
64 })
65 }
66
67 const fn id(&self) -> u16 {
69 match self {
70 Self::Adjustable(_) => AdjustableDpiFeature::ID,
71 Self::Extended(_) => ExtendedDpiFeature::ID,
72 }
73 }
74
75 async fn sensor_count(&self) -> Result<u8, Hidpp20Error> {
77 match self {
78 Self::Adjustable(feature) => feature.get_sensor_count().await,
79 Self::Extended(feature) => feature.get_sensor_count().await,
80 }
81 }
82
83 async fn current_dpi(&self) -> Result<Dpi, Hidpp20Error> {
85 match self {
86 Self::Adjustable(feature) => feature.get_sensor_dpi(SENSOR).await.map(Dpi::from),
87 Self::Extended(feature) => Ok(feature
88 .get_sensor_dpi_parameters(SENSOR)
89 .await?
90 .dpi_x
91 .into()),
92 }
93 }
94
95 async fn supported_dpi(&self) -> Result<Vec<u16>, Hidpp20Error> {
97 match self {
98 Self::Adjustable(feature) => feature.get_sensor_dpi_list(SENSOR).await,
99 Self::Extended(feature) => {
100 let ranges = feature
104 .get_sensor_dpi_ranges(SENSOR, DpiDirection::X)
105 .await?;
106 Ok(expand_dpi_ranges(&ranges))
107 }
108 }
109 }
110
111 async fn set_dpi(&self, dpi: Dpi) -> Result<(), Hidpp20Error> {
113 let dpi = dpi.into();
114 match self {
115 Self::Adjustable(feature) => feature.set_sensor_dpi(SENSOR, dpi).await,
116 Self::Extended(feature) => {
117 let current = feature.get_sensor_dpi_parameters(SENSOR).await?;
123 feature
124 .set_sensor_dpi_parameters(
125 SENSOR,
126 SetDpiParameters {
127 dpi_x: dpi,
128 dpi_y: if current.dpi_y == 0 { 0 } else { dpi },
134 lod: current.lod,
135 },
136 )
137 .await
138 }
139 }
140 }
141}
142
143async fn feature_index(device: &mut Device, feature_hex: u16) -> Result<Option<u8>, WriteError> {
150 Ok(device
151 .root()
152 .get_feature(feature_hex)
153 .await
154 .map_err(|e| classify_hidpp_error(e, HidppOperation::ResolveFeature, feature_hex))?
155 .map(|info| info.index))
156}
157
158pub(super) fn expand_dpi_ranges(ranges: &[DpiRange]) -> Vec<u16> {
165 let mut values = Vec::new();
166 for range in ranges {
167 match *range {
168 DpiRange::Fixed(value) => values.push(value),
169 DpiRange::Stepped { from, to, step } => {
170 let mut value = u32::from(from);
173 while value < u32::from(to) {
174 if let Ok(value) = u16::try_from(value) {
175 values.push(value);
176 }
177 value += u32::from(step);
178 }
179 values.push(to);
180 }
181 }
182 }
183 values
184}
185
186pub async fn get_dpi(route: &DeviceRoute) -> Result<Dpi, WriteError> {
190 let index = route.device_index();
191 with_route(route, move |channel| async move {
192 get_dpi_on_channel(&channel, index).await
193 })
194 .await
195}
196
197async fn get_dpi_on_channel(
198 channel: &Arc<hidpp::channel::HidppChannel>,
199 index: u8,
200) -> Result<Dpi, WriteError> {
201 let mut device = Device::new(Arc::clone(channel), index)
202 .await
203 .map_err(|_| WriteError::DeviceUnreachable { index })?;
204 let feature = DpiFeature::open(&mut device).await?;
205 feature
206 .current_dpi()
207 .await
208 .map_err(|e| classify_hidpp_error(e, HidppOperation::ReadDpi, feature.id()))
209}
210
211fn classify_dpi_error(feature_hex: u16, error: Hidpp20Error) -> WriteError {
218 match error {
219 Hidpp20Error::Feature(ErrorType::Unsupported | ErrorType::InvalidFunctionId)
220 | Hidpp20Error::UnsupportedResponse => WriteError::FeatureUnsupported { feature_hex },
221 other => classify_hidpp_error(other, HidppOperation::ReadDpiCapabilities, feature_hex),
222 }
223}
224
225pub async fn get_dpi_info(route: &DeviceRoute) -> Result<DpiInfo, WriteError> {
228 let index = route.device_index();
229 with_route(route, move |channel| async move {
230 get_dpi_info_on_channel(&channel, index).await
231 })
232 .await
233}
234
235pub(super) async fn get_dpi_info_on_channel(
236 channel: &Arc<hidpp::channel::HidppChannel>,
237 index: u8,
238) -> Result<DpiInfo, WriteError> {
239 let mut device = Device::new(Arc::clone(channel), index)
240 .await
241 .map_err(|_| WriteError::DeviceUnreachable { index })?;
242 let feature = DpiFeature::open(&mut device).await?;
243 let feature_hex = feature.id();
244 let sensor_count = feature
245 .sensor_count()
246 .await
247 .map_err(|e| classify_dpi_error(feature_hex, e))?;
248 if sensor_count == 0 {
249 return Err(WriteError::FeatureUnsupported { feature_hex });
252 }
253 let current = feature
254 .current_dpi()
255 .await
256 .map_err(|e| classify_dpi_error(feature_hex, e))?;
257 let values = feature
258 .supported_dpi()
259 .await
260 .map_err(|e| classify_dpi_error(feature_hex, e))?;
261 Ok(DpiInfo {
262 current,
263 capabilities: DpiCapabilities::new(values)?,
264 })
265}
266
267pub async fn set_dpi(route: &DeviceRoute, dpi: Dpi) -> Result<(), WriteError> {
269 let index = route.device_index();
270 with_route(route, move |channel| async move {
271 set_dpi_on_channel(&channel, index, dpi).await
272 })
273 .await
274}
275
276pub(super) async fn set_dpi_on_channel(
280 channel: &Arc<hidpp::channel::HidppChannel>,
281 index: u8,
282 dpi: Dpi,
283) -> Result<(), WriteError> {
284 let mut device = Device::new(Arc::clone(channel), index)
285 .await
286 .map_err(|_| WriteError::DeviceUnreachable { index })?;
287 let feature = DpiFeature::open(&mut device).await?;
288 feature
289 .set_dpi(dpi)
290 .await
291 .map_err(|e| classify_hidpp_error(e, HidppOperation::WriteDpi, feature.id()))?;
292 if let Ok(actual) = feature.current_dpi().await {
298 if actual == dpi {
299 debug!(index, %dpi, "wrote DPI (verified)");
300 } else {
301 tracing::warn!(
302 index,
303 requested = %dpi,
304 %actual,
305 "DPI write accepted but device reports a different value — \
306 likely out of the device's supported range"
307 );
308 }
309 } else {
310 debug!(index, %dpi, "wrote DPI (read-back skipped)");
311 }
312 Ok(())
313}
314
315pub async fn set_dpi_on(shared: &SharedChannel, dpi: Dpi) -> Result<(), WriteError> {
318 set_dpi_on_channel(shared.channel(), shared.device_index(), dpi).await
319}
320
321pub async fn get_dpi_info_on(shared: &SharedChannel) -> Result<DpiInfo, WriteError> {
323 get_dpi_info_on_channel(shared.channel(), shared.device_index()).await
324}