hidpp/feature/adjustable_dpi/
mod.rs1use std::sync::Arc;
5
6use crate::{
7 channel::HidppChannel,
8 feature::{CreatableFeature, Feature},
9 nibble::U4,
10 protocol::v20::{self, Hidpp20Error},
11};
12
13#[derive(Clone)]
15pub struct AdjustableDpiFeature {
16 chan: Arc<HidppChannel>,
18
19 device_index: u8,
21
22 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 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 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 let payload = response.extend_payload();
85 parse_dpi_list_payload(&payload[1..])
86 }
87
88 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 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 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 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 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 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 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}