openlogi_hid/write/
dpi.rs1use std::sync::Arc;
2
3use hidpp::{
4 device::Device,
5 feature::{CreatableFeature, adjustable_dpi::AdjustableDpiFeature},
6 protocol::v20::{ErrorType, Hidpp20Error},
7};
8use serde::{Deserialize, Serialize};
9use tracing::debug;
10
11use crate::route::DeviceRoute;
12
13use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct DpiCapabilities {
18 values: Vec<u16>,
19}
20
21impl DpiCapabilities {
22 pub fn new(mut values: Vec<u16>) -> Result<Self, WriteError> {
25 values.sort_unstable();
26 values.dedup();
27 if values.is_empty() {
28 return Err(WriteError::EmptyDpiList);
29 }
30 Ok(Self { values })
31 }
32
33 #[must_use]
35 pub fn values(&self) -> &[u16] {
36 &self.values
37 }
38
39 #[must_use]
41 pub fn min(&self) -> u16 {
42 self.values[0]
43 }
44
45 #[must_use]
47 pub fn max(&self) -> u16 {
48 self.values[self.values.len() - 1]
49 }
50
51 #[must_use]
53 pub fn contains(&self, dpi: u16) -> bool {
54 self.values.binary_search(&dpi).is_ok()
55 }
56
57 #[must_use]
59 pub fn nearest(&self, dpi: u32) -> u16 {
60 let mut nearest = self.values[0];
61 let mut best_delta = u32::from(nearest).abs_diff(dpi);
62 for &candidate in &self.values[1..] {
63 let delta = u32::from(candidate).abs_diff(dpi);
64 if delta < best_delta {
65 nearest = candidate;
66 best_delta = delta;
67 }
68 }
69 nearest
70 }
71
72 #[must_use]
76 pub fn snap(&self, dpi: u32) -> u32 {
77 u32::from(self.nearest(dpi))
78 }
79
80 #[must_use]
83 pub fn step_hint(&self) -> u16 {
84 self.values
85 .array_windows::<2>()
86 .filter_map(|&[low, high]| high.checked_sub(low))
87 .filter(|step| *step > 0)
88 .min()
89 .unwrap_or(1)
90 }
91
92 #[must_use]
94 pub fn adjacent_test_target(&self, current: u16) -> Option<u16> {
95 if self.values.len() < 2 {
96 return None;
97 }
98 match self.values.binary_search(¤t) {
99 Ok(index) if index + 1 < self.values.len() => Some(self.values[index + 1]),
100 Ok(index) if index > 0 => Some(self.values[index - 1]),
101 Ok(_) => None,
102 Err(index) if index < self.values.len() => Some(self.values[index]),
103 Err(_) => self.values.last().copied(),
104 }
105 .filter(|target| *target != current)
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub struct DpiInfo {
116 pub current: u16,
118 pub capabilities: DpiCapabilities,
120}
121
122pub async fn get_dpi(route: &DeviceRoute) -> Result<u16, WriteError> {
126 let index = route.device_index();
127 with_route(route, move |channel| async move {
128 let mut device = Device::new(Arc::clone(&channel), index)
129 .await
130 .map_err(|_| WriteError::DeviceUnreachable { index })?;
131 let feature = open_feature::<AdjustableDpiFeature>(&mut device).await?;
132 feature
133 .get_sensor_dpi(0)
134 .await
135 .map_err(|e| classify_hidpp_error(e, HidppOperation::ReadDpi, AdjustableDpiFeature::ID))
136 })
137 .await
138}
139
140fn classify_dpi_error(error: Hidpp20Error) -> WriteError {
147 match error {
148 Hidpp20Error::Feature(ErrorType::Unsupported | ErrorType::InvalidFunctionId)
149 | Hidpp20Error::UnsupportedResponse => WriteError::FeatureUnsupported {
150 feature_hex: AdjustableDpiFeature::ID,
151 },
152 other => classify_hidpp_error(
153 other,
154 HidppOperation::ReadDpiCapabilities,
155 AdjustableDpiFeature::ID,
156 ),
157 }
158}
159
160pub async fn get_dpi_info(route: &DeviceRoute) -> Result<DpiInfo, WriteError> {
163 let index = route.device_index();
164 with_route(route, move |channel| async move {
165 let mut device = Device::new(Arc::clone(&channel), index)
166 .await
167 .map_err(|_| WriteError::DeviceUnreachable { index })?;
168 let feature = open_feature::<AdjustableDpiFeature>(&mut device).await?;
169 let sensor_count = feature
170 .get_sensor_count()
171 .await
172 .map_err(classify_dpi_error)?;
173 if sensor_count == 0 {
174 return Err(WriteError::FeatureUnsupported {
177 feature_hex: AdjustableDpiFeature::ID,
178 });
179 }
180 let current = feature
181 .get_sensor_dpi(0)
182 .await
183 .map_err(classify_dpi_error)?;
184 let values = feature
185 .get_sensor_dpi_list(0)
186 .await
187 .map_err(classify_dpi_error)?;
188 Ok(DpiInfo {
189 current,
190 capabilities: DpiCapabilities::new(values)?,
191 })
192 })
193 .await
194}
195
196pub async fn set_dpi(route: &DeviceRoute, dpi: u16) -> Result<(), WriteError> {
198 let index = route.device_index();
199 with_route(route, move |channel| async move {
200 set_dpi_on_channel(&channel, index, dpi).await
201 })
202 .await
203}
204
205pub(super) async fn set_dpi_on_channel(
209 channel: &Arc<hidpp::channel::HidppChannel>,
210 index: u8,
211 dpi: u16,
212) -> Result<(), WriteError> {
213 let mut device = Device::new(Arc::clone(channel), index)
214 .await
215 .map_err(|_| WriteError::DeviceUnreachable { index })?;
216 let feature = open_feature::<AdjustableDpiFeature>(&mut device).await?;
217 feature
218 .set_sensor_dpi(0, dpi)
219 .await
220 .map_err(|e| classify_hidpp_error(e, HidppOperation::WriteDpi, AdjustableDpiFeature::ID))?;
221 if let Ok(actual) = feature.get_sensor_dpi(0).await {
227 if actual == dpi {
228 debug!(index, dpi, "wrote DPI (verified)");
229 } else {
230 tracing::warn!(
231 index,
232 requested = dpi,
233 actual,
234 "DPI write accepted but device reports a different value — \
235 likely out of the device's supported range"
236 );
237 }
238 } else {
239 debug!(index, dpi, "wrote DPI (read-back skipped)");
240 }
241 Ok(())
242}