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