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 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 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 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}