logiops_core/features/
adjustable_dpi.rs

1//! `Adjustable DPI` feature (0x2201) - DPI configuration.
2//!
3//! This feature allows reading and setting the DPI (dots per inch)
4//! sensitivity of pointing devices.
5
6use hidpp_transport::HidapiChannel;
7use tracing::{debug, trace};
8
9use crate::error::{HidppErrorCode, ProtocolError, Result};
10use crate::protocol::{build_long_request, get_error_code, is_error_response};
11
12/// `Adjustable DPI` feature implementation.
13pub struct AdjustableDpiFeature {
14    device_index: u8,
15    feature_index: u8,
16}
17
18/// DPI sensor information.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct SensorInfo {
21    /// Sensor index (usually 0).
22    pub index: u8,
23    /// Minimum DPI value.
24    pub dpi_min: u16,
25    /// Maximum DPI value.
26    pub dpi_max: u16,
27    /// DPI step size (0 means predefined list).
28    pub dpi_step: u16,
29    /// Default DPI value.
30    pub default_dpi: u16,
31    /// List of predefined DPI values (if `dpi_step` is 0).
32    pub dpi_list: Vec<u16>,
33}
34
35impl AdjustableDpiFeature {
36    /// Creates a new adjustable DPI feature accessor.
37    ///
38    /// # Arguments
39    /// * `device_index` - Device index (0xFF for direct)
40    /// * `feature_index` - Feature index from root feature discovery
41    #[must_use]
42    pub fn new(device_index: u8, feature_index: u8) -> Self {
43        Self {
44            device_index,
45            feature_index,
46        }
47    }
48
49    /// Gets the number of sensors on the device.
50    ///
51    /// Most mice have 1 sensor, but some gaming mice have multiple.
52    ///
53    /// # Errors
54    /// Returns an error if HID++ communication fails.
55    pub async fn get_sensor_count(&self, channel: &HidapiChannel) -> Result<u8> {
56        // getSensorCount: function_id=0
57        let request = build_long_request(self.device_index, self.feature_index, 0x00, &[]);
58
59        trace!("getting sensor count");
60        let response = channel.request(&request, 5).await?;
61
62        if is_error_response(&response) {
63            let code = get_error_code(&response).unwrap_or(0);
64            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
65        }
66
67        if response.len() < 5 {
68            return Err(ProtocolError::InvalidResponse(
69                "sensor count response too short".to_string(),
70            ));
71        }
72
73        let count = response[4];
74        debug!(count, "got sensor count");
75        Ok(count)
76    }
77
78    /// Gets DPI information for a sensor.
79    ///
80    /// # Arguments
81    /// * `channel` - HID channel
82    /// * `sensor_index` - Sensor index (usually 0)
83    ///
84    /// # Errors
85    /// Returns an error if HID++ communication fails.
86    pub async fn get_sensor_dpi_info(
87        &self,
88        channel: &HidapiChannel,
89        sensor_index: u8,
90    ) -> Result<SensorInfo> {
91        // getSensorDpiList: function_id=1, param=sensor_index
92        let request =
93            build_long_request(self.device_index, self.feature_index, 0x01, &[sensor_index]);
94
95        trace!(sensor_index, "getting sensor DPI info");
96        let response = channel.request(&request, 5).await?;
97
98        if is_error_response(&response) {
99            let code = get_error_code(&response).unwrap_or(0);
100            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
101        }
102
103        if response.len() < 9 {
104            return Err(ProtocolError::InvalidResponse(
105                "sensor DPI info response too short".to_string(),
106            ));
107        }
108
109        // Response varies by device. Some return list, some return range.
110        // Check if this is a DPI list (step=0) or range response
111        let byte4 = response[4];
112        let byte5 = response[5];
113        let byte6 = response[6];
114        let byte7 = response.get(7).copied().unwrap_or(0);
115        let byte8 = response.get(8).copied().unwrap_or(0);
116
117        // Try to interpret as range first (newer format)
118        // Format: dpi_step (2), dpi_min (2), dpi_max (2)
119        let dpi_step = u16::from_be_bytes([byte4, byte5]);
120
121        let (dpi_min, dpi_max, default_dpi, dpi_list) = if dpi_step > 0 {
122            // Range format
123            let dpi_min = u16::from_be_bytes([byte6, byte7]);
124            let dpi_max = u16::from_be_bytes([byte8, response.get(9).copied().unwrap_or(0)]);
125            (dpi_min, dpi_max, dpi_min, Vec::new())
126        } else {
127            // DPI list format - parse list of DPIs
128            let mut dpi_list = Vec::new();
129            let mut i = 4;
130            while i + 1 < response.len() {
131                let dpi = u16::from_be_bytes([response[i], response[i + 1]]);
132                if dpi == 0 {
133                    break;
134                }
135                // DPIs in the list are often encoded with the high bit indicating "current"
136                let actual_dpi = dpi & 0x7FFF;
137                if actual_dpi > 0 {
138                    dpi_list.push(actual_dpi);
139                }
140                i += 2;
141            }
142
143            let min = dpi_list.iter().copied().min().unwrap_or(400);
144            let max = dpi_list.iter().copied().max().unwrap_or(3200);
145            let default = dpi_list.first().copied().unwrap_or(1000);
146            (min, max, default, dpi_list)
147        };
148
149        let info = SensorInfo {
150            index: sensor_index,
151            dpi_min,
152            dpi_max,
153            dpi_step,
154            default_dpi,
155            dpi_list,
156        };
157
158        debug!(
159            sensor = sensor_index,
160            min = dpi_min,
161            max = dpi_max,
162            step = dpi_step,
163            "got sensor DPI info"
164        );
165
166        Ok(info)
167    }
168
169    /// Gets the current DPI value for a sensor.
170    ///
171    /// # Arguments
172    /// * `channel` - HID channel
173    /// * `sensor_index` - Sensor index (usually 0)
174    ///
175    /// # Errors
176    /// Returns an error if HID++ communication fails.
177    pub async fn get_sensor_dpi(&self, channel: &HidapiChannel, sensor_index: u8) -> Result<u16> {
178        // getSensorDpi: function_id=2, param=sensor_index
179        let request =
180            build_long_request(self.device_index, self.feature_index, 0x02, &[sensor_index]);
181
182        trace!(sensor_index, "getting sensor DPI");
183        let response = channel.request(&request, 5).await?;
184
185        if is_error_response(&response) {
186            let code = get_error_code(&response).unwrap_or(0);
187            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
188        }
189
190        if response.len() < 7 {
191            return Err(ProtocolError::InvalidResponse(
192                "sensor DPI response too short".to_string(),
193            ));
194        }
195
196        // Response: [report_id, device_idx, feature_idx, func_sw_id, sensor_idx, dpi_hi, dpi_lo]
197        let dpi = u16::from_be_bytes([response[5], response[6]]);
198        debug!(sensor = sensor_index, dpi, "got sensor DPI");
199
200        Ok(dpi)
201    }
202
203    /// Sets the DPI value for a sensor.
204    ///
205    /// # Arguments
206    /// * `channel` - HID channel
207    /// * `sensor_index` - Sensor index (usually 0)
208    /// * `dpi` - DPI value to set
209    ///
210    /// # Errors
211    /// Returns an error if HID++ communication fails or the DPI value is out of range.
212    pub async fn set_sensor_dpi(
213        &self,
214        channel: &HidapiChannel,
215        sensor_index: u8,
216        dpi: u16,
217    ) -> Result<()> {
218        // setSensorDpi: function_id=3, params=[sensor_index, dpi_hi, dpi_lo]
219        let dpi_bytes = dpi.to_be_bytes();
220        let request = build_long_request(
221            self.device_index,
222            self.feature_index,
223            0x03,
224            &[sensor_index, dpi_bytes[0], dpi_bytes[1]],
225        );
226
227        trace!(sensor_index, dpi, "setting sensor DPI");
228        let response = channel.request(&request, 5).await?;
229
230        if is_error_response(&response) {
231            let code = get_error_code(&response).unwrap_or(0);
232            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
233        }
234
235        debug!(sensor = sensor_index, dpi, "set sensor DPI");
236        Ok(())
237    }
238
239    /// Gets the default DPI for a sensor.
240    ///
241    /// # Arguments
242    /// * `channel` - HID channel
243    /// * `sensor_index` - Sensor index (usually 0)
244    ///
245    /// # Errors
246    /// Returns an error if HID++ communication fails.
247    pub async fn get_default_dpi(&self, channel: &HidapiChannel, sensor_index: u8) -> Result<u16> {
248        // getDefaultDpi: function_id=4, param=sensor_index (may not be supported on all devices)
249        let request =
250            build_long_request(self.device_index, self.feature_index, 0x04, &[sensor_index]);
251
252        trace!(sensor_index, "getting default DPI");
253        let response = channel.request(&request, 5).await?;
254
255        if is_error_response(&response) {
256            let code = get_error_code(&response).unwrap_or(0);
257            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
258        }
259
260        if response.len() < 7 {
261            return Err(ProtocolError::InvalidResponse(
262                "default DPI response too short".to_string(),
263            ));
264        }
265
266        let dpi = u16::from_be_bytes([response[5], response[6]]);
267        debug!(sensor = sensor_index, dpi, "got default DPI");
268
269        Ok(dpi)
270    }
271}
272
273/// Convenience methods for single-sensor devices.
274impl AdjustableDpiFeature {
275    /// Gets the current DPI for the primary sensor (index 0).
276    ///
277    /// # Errors
278    /// Returns an error if HID++ communication fails.
279    pub async fn get_dpi(&self, channel: &HidapiChannel) -> Result<u16> {
280        self.get_sensor_dpi(channel, 0).await
281    }
282
283    /// Sets the DPI for the primary sensor (index 0).
284    ///
285    /// # Errors
286    /// Returns an error if HID++ communication fails or the DPI is out of range.
287    pub async fn set_dpi(&self, channel: &HidapiChannel, dpi: u16) -> Result<()> {
288        self.set_sensor_dpi(channel, 0, dpi).await
289    }
290
291    /// Gets DPI info for the primary sensor (index 0).
292    ///
293    /// # Errors
294    /// Returns an error if HID++ communication fails.
295    pub async fn get_dpi_info(&self, channel: &HidapiChannel) -> Result<SensorInfo> {
296        self.get_sensor_dpi_info(channel, 0).await
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_sensor_info() {
306        let info = SensorInfo {
307            index: 0,
308            dpi_min: 400,
309            dpi_max: 8000,
310            dpi_step: 50,
311            default_dpi: 1000,
312            dpi_list: vec![],
313        };
314
315        assert_eq!(info.dpi_min, 400);
316        assert_eq!(info.dpi_max, 8000);
317    }
318}