Skip to main content

hidpp/feature/adjustable_dpi/
mod.rs

1//! Implements the `AdjustableDpi` feature (ID `0x2201`) that allows reading
2//! and changing a mouse sensor's DPI.
3
4use std::sync::Arc;
5
6use crate::{
7    channel::HidppChannel,
8    feature::{CreatableFeature, Feature},
9    nibble::U4,
10    protocol::v20::{self, Hidpp20Error},
11};
12
13/// Implements the `AdjustableDpi` / `0x2201` feature.
14#[derive(Clone)]
15pub struct AdjustableDpiFeature {
16    /// The underlying HID++ channel.
17    chan: Arc<HidppChannel>,
18
19    /// The index of the device to implement the feature for.
20    device_index: u8,
21
22    /// The index of the feature in the feature table.
23    feature_index: u8,
24}
25
26impl CreatableFeature for AdjustableDpiFeature {
27    const ID: u16 = 0x2201;
28    const STARTING_VERSION: u8 = 0;
29
30    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
31        Self {
32            chan,
33            device_index,
34            feature_index,
35        }
36    }
37}
38
39impl Feature for AdjustableDpiFeature {}
40
41impl AdjustableDpiFeature {
42    /// Retrieves the number of sensors the device exposes.
43    pub async fn get_sensor_count(&self) -> Result<u8, Hidpp20Error> {
44        let response = self
45            .chan
46            .send_v20(v20::Message::Short(
47                v20::MessageHeader {
48                    device_index: self.device_index,
49                    feature_index: self.feature_index,
50                    function_id: U4::from_lo(0),
51                    software_id: self.chan.get_sw_id(),
52                },
53                [0x00, 0x00, 0x00],
54            ))
55            .await?;
56
57        Ok(response.extend_payload()[0])
58    }
59
60    /// Retrieves the supported DPI values for `sensor_index`.
61    ///
62    /// `getSensorDpiList` takes the sensor index in the first parameter byte and
63    /// returns the whole list in a single long response: the echoed sensor index
64    /// followed by up to seven big-endian values, terminated by `0x0000` (the
65    /// terminator is absent when the values fill the response). Each value is
66    /// either an explicit DPI or a compact range marker (`0xe000 | step`) whose
67    /// start is the previous value and whose end is the next value. The returned
68    /// list is sorted and deduplicated.
69    pub async fn get_sensor_dpi_list(&self, sensor_index: u8) -> Result<Vec<u16>, Hidpp20Error> {
70        let response = self
71            .chan
72            .send_v20(v20::Message::Short(
73                v20::MessageHeader {
74                    device_index: self.device_index,
75                    feature_index: self.feature_index,
76                    function_id: U4::from_lo(1),
77                    software_id: self.chan.get_sw_id(),
78                },
79                [sensor_index, 0x00, 0x00],
80            ))
81            .await?;
82
83        // Skip the echoed sensor index in byte 0; the DPI values follow.
84        let payload = response.extend_payload();
85        parse_dpi_list_payload(&payload[1..])
86    }
87
88    /// Retrieves the currently configured DPI for `sensor_index`.
89    pub async fn get_sensor_dpi(&self, sensor_index: u8) -> Result<u16, Hidpp20Error> {
90        let response = self
91            .chan
92            .send_v20(v20::Message::Short(
93                v20::MessageHeader {
94                    device_index: self.device_index,
95                    feature_index: self.feature_index,
96                    function_id: U4::from_lo(2),
97                    software_id: self.chan.get_sw_id(),
98                },
99                [sensor_index, 0x00, 0x00],
100            ))
101            .await?;
102        let payload = response.extend_payload();
103
104        Ok(u16::from_be_bytes([payload[1], payload[2]]))
105    }
106
107    /// Sets the DPI for `sensor_index`.
108    pub async fn set_sensor_dpi(&self, sensor_index: u8, dpi: u16) -> Result<(), Hidpp20Error> {
109        let [dpi_hi, dpi_lo] = dpi.to_be_bytes();
110        let _ = self
111            .chan
112            .send_v20(v20::Message::Short(
113                v20::MessageHeader {
114                    device_index: self.device_index,
115                    feature_index: self.feature_index,
116                    function_id: U4::from_lo(3),
117                    software_id: self.chan.get_sw_id(),
118                },
119                [sensor_index, dpi_hi, dpi_lo],
120            ))
121            .await?;
122
123        Ok(())
124    }
125}
126
127fn parse_dpi_list_payload(bytes: &[u8]) -> Result<Vec<u16>, Hidpp20Error> {
128    let mut values = Vec::new();
129    let mut offset = 0;
130
131    while offset + 1 < bytes.len() {
132        let value = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]);
133        // `0x0000` terminates the list. A list that fills the whole response
134        // has no room for it, so absence of a terminator is not an error — we
135        // simply stop when the buffer runs out below.
136        if value == 0 {
137            break;
138        }
139
140        if value >> 13 == 0b111 {
141            let step = value & 0x1fff;
142            if step == 0 || offset + 3 >= bytes.len() {
143                return Err(Hidpp20Error::UnsupportedResponse);
144            }
145            // A range marker's start is the preceding explicit value; a leading
146            // marker with no predecessor is malformed.
147            let start = u32::from(*values.last().ok_or(Hidpp20Error::UnsupportedResponse)?);
148            let last = u16::from_be_bytes([bytes[offset + 2], bytes[offset + 3]]);
149            if u32::from(last) < start {
150                return Err(Hidpp20Error::UnsupportedResponse);
151            }
152            let mut next = start + u32::from(step);
153            while next < u32::from(last) {
154                values.push(u16::try_from(next).map_err(|_| Hidpp20Error::UnsupportedResponse)?);
155                next += u32::from(step);
156            }
157            // The high endpoint is always supported, even when it is not an
158            // exact multiple of `step` from the low endpoint.
159            values.push(last);
160            offset += 4;
161        } else {
162            values.push(value);
163            offset += 2;
164        }
165    }
166
167    if values.is_empty() {
168        return Err(Hidpp20Error::UnsupportedResponse);
169    }
170    values.sort_unstable();
171    values.dedup();
172    Ok(values)
173}
174
175#[cfg(test)]
176mod tests {
177    use super::parse_dpi_list_payload;
178    use crate::protocol::v20::Hidpp20Error;
179
180    #[test]
181    fn parses_explicit_dpi_list() {
182        let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40, 0x00, 0x00];
183
184        assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
185    }
186
187    #[test]
188    fn expands_range_encoded_dpi_list() {
189        let payload = [0x01, 0x90, 0xe1, 0x90, 0x06, 0x40, 0x00, 0x00];
190
191        assert_eq!(
192            parse_dpi_list_payload(&payload).unwrap(),
193            [400, 800, 1200, 1600]
194        );
195    }
196
197    #[test]
198    fn sorts_and_deduplicates_values() {
199        let payload = [0x06, 0x40, 0x03, 0x20, 0x03, 0x20, 0x00, 0x00];
200
201        assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [800, 1600]);
202    }
203
204    #[test]
205    fn rejects_range_marker_without_previous_value() {
206        let payload = [0xe0, 0x32, 0x1f, 0x40, 0x00, 0x00];
207
208        assert!(matches!(
209            parse_dpi_list_payload(&payload),
210            Err(Hidpp20Error::UnsupportedResponse)
211        ));
212    }
213
214    #[test]
215    fn rejects_range_marker_without_end_value() {
216        let payload = [0x01, 0x90, 0xe0, 0x32];
217
218        assert!(matches!(
219            parse_dpi_list_payload(&payload),
220            Err(Hidpp20Error::UnsupportedResponse)
221        ));
222    }
223
224    #[test]
225    fn rejects_zero_step_range_marker() {
226        let payload = [0x01, 0x90, 0xe0, 0x00, 0x06, 0x40, 0x00, 0x00];
227
228        assert!(matches!(
229            parse_dpi_list_payload(&payload),
230            Err(Hidpp20Error::UnsupportedResponse)
231        ));
232    }
233
234    #[test]
235    fn rejects_descending_range_marker() {
236        let payload = [0x06, 0x40, 0xe0, 0x32, 0x01, 0x90, 0x00, 0x00];
237
238        assert!(matches!(
239            parse_dpi_list_payload(&payload),
240            Err(Hidpp20Error::UnsupportedResponse)
241        ));
242    }
243
244    #[test]
245    fn range_keeps_off_grid_high_endpoint() {
246        // min 400, step 400, max 1500 — 1500 is not on the 400 grid but is a
247        // supported value and must be kept.
248        let payload = [0x01, 0x90, 0xe1, 0x90, 0x05, 0xdc, 0x00, 0x00];
249
250        assert_eq!(
251            parse_dpi_list_payload(&payload).unwrap(),
252            [400, 800, 1200, 1500]
253        );
254    }
255
256    #[test]
257    fn parses_full_list_without_terminator() {
258        // A list that fills the response leaves no room for a 0x0000
259        // terminator; the values are still valid.
260        let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40];
261
262        assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
263    }
264
265    #[test]
266    fn rejects_payload_with_no_values() {
267        assert!(matches!(
268            parse_dpi_list_payload(&[0x00, 0x00]),
269            Err(Hidpp20Error::UnsupportedResponse)
270        ));
271    }
272}