hidpp/feature/adjustable_dpi/
mod.rs1use std::sync::Arc;
5
6use crate::{
7 channel::HidppChannel,
8 feature::{CreatableFeature, Feature, FeatureEndpoint},
9 protocol::v20::Hidpp20Error,
10};
11
12#[derive(Clone)]
14pub struct AdjustableDpiFeature {
15 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 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 pub async fn get_sensor_dpi_list(&self, sensor_index: u8) -> Result<Vec<u16>, Hidpp20Error> {
48 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 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 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 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 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 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 super::parse_dpi_list_payload;
130 use crate::protocol::v20::Hidpp20Error;
131
132 #[test]
133 fn parses_explicit_dpi_list() {
134 let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40, 0x00, 0x00];
135
136 assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
137 }
138
139 #[test]
140 fn expands_range_encoded_dpi_list() {
141 let payload = [0x01, 0x90, 0xe1, 0x90, 0x06, 0x40, 0x00, 0x00];
142
143 assert_eq!(
144 parse_dpi_list_payload(&payload).unwrap(),
145 [400, 800, 1200, 1600]
146 );
147 }
148
149 #[test]
150 fn sorts_and_deduplicates_values() {
151 let payload = [0x06, 0x40, 0x03, 0x20, 0x03, 0x20, 0x00, 0x00];
152
153 assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [800, 1600]);
154 }
155
156 #[test]
157 fn rejects_range_marker_without_previous_value() {
158 let payload = [0xe0, 0x32, 0x1f, 0x40, 0x00, 0x00];
159
160 assert!(matches!(
161 parse_dpi_list_payload(&payload),
162 Err(Hidpp20Error::UnsupportedResponse)
163 ));
164 }
165
166 #[test]
167 fn rejects_range_marker_without_end_value() {
168 let payload = [0x01, 0x90, 0xe0, 0x32];
169
170 assert!(matches!(
171 parse_dpi_list_payload(&payload),
172 Err(Hidpp20Error::UnsupportedResponse)
173 ));
174 }
175
176 #[test]
177 fn rejects_zero_step_range_marker() {
178 let payload = [0x01, 0x90, 0xe0, 0x00, 0x06, 0x40, 0x00, 0x00];
179
180 assert!(matches!(
181 parse_dpi_list_payload(&payload),
182 Err(Hidpp20Error::UnsupportedResponse)
183 ));
184 }
185
186 #[test]
187 fn rejects_descending_range_marker() {
188 let payload = [0x06, 0x40, 0xe0, 0x32, 0x01, 0x90, 0x00, 0x00];
189
190 assert!(matches!(
191 parse_dpi_list_payload(&payload),
192 Err(Hidpp20Error::UnsupportedResponse)
193 ));
194 }
195
196 #[test]
197 fn range_keeps_off_grid_high_endpoint() {
198 let payload = [0x01, 0x90, 0xe1, 0x90, 0x05, 0xdc, 0x00, 0x00];
201
202 assert_eq!(
203 parse_dpi_list_payload(&payload).unwrap(),
204 [400, 800, 1200, 1500]
205 );
206 }
207
208 #[test]
209 fn parses_full_list_without_terminator() {
210 let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40];
213
214 assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
215 }
216
217 #[test]
218 fn rejects_payload_with_no_values() {
219 assert!(matches!(
220 parse_dpi_list_payload(&[0x00, 0x00]),
221 Err(Hidpp20Error::UnsupportedResponse)
222 ));
223 }
224}