1use openlogi_hidpp_derive::Feature;
5
6use crate::{feature::FeatureEndpoint, protocol::v20::Hidpp20Error};
7
8#[derive(Clone, Feature)]
10#[creatable(id = 0x2201, version = 0)]
11pub struct AdjustableDpiFeature {
12 endpoint: FeatureEndpoint,
14}
15
16impl AdjustableDpiFeature {
17 pub async fn get_sensor_count(&self) -> Result<u8, Hidpp20Error> {
19 Ok(self.endpoint.call(0, [0; 3]).await?.extend_payload()[0])
20 }
21
22 pub async fn get_sensor_dpi_list(&self, sensor_index: u8) -> Result<Vec<u16>, Hidpp20Error> {
32 let payload = self
34 .endpoint
35 .call(1, [sensor_index, 0x00, 0x00])
36 .await?
37 .extend_payload();
38 parse_dpi_list_payload(&payload[1..])
39 }
40
41 pub async fn get_sensor_dpi(&self, sensor_index: u8) -> Result<u16, Hidpp20Error> {
43 let payload = self
44 .endpoint
45 .call(2, [sensor_index, 0x00, 0x00])
46 .await?
47 .extend_payload();
48
49 Ok(u16::from_be_bytes([payload[1], payload[2]]))
50 }
51
52 pub async fn set_sensor_dpi(&self, sensor_index: u8, dpi: u16) -> Result<(), Hidpp20Error> {
54 let [dpi_hi, dpi_lo] = dpi.to_be_bytes();
55 self.endpoint
56 .call(3, [sensor_index, dpi_hi, dpi_lo])
57 .await?;
58
59 Ok(())
60 }
61}
62
63fn parse_dpi_list_payload(bytes: &[u8]) -> Result<Vec<u16>, Hidpp20Error> {
64 let mut values = Vec::new();
65 let mut offset = 0;
66
67 while offset + 1 < bytes.len() {
68 let value = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]);
69 if value == 0 {
73 break;
74 }
75
76 if value >> 13 == 0b111 {
77 let step = value & 0x1fff;
78 if step == 0 || offset + 3 >= bytes.len() {
79 return Err(Hidpp20Error::UnsupportedResponse);
80 }
81 let start = u32::from(*values.last().ok_or(Hidpp20Error::UnsupportedResponse)?);
84 let last = u16::from_be_bytes([bytes[offset + 2], bytes[offset + 3]]);
85 if u32::from(last) < start {
86 return Err(Hidpp20Error::UnsupportedResponse);
87 }
88 let mut next = start + u32::from(step);
89 while next < u32::from(last) {
90 values.push(u16::try_from(next).map_err(|_| Hidpp20Error::UnsupportedResponse)?);
91 next += u32::from(step);
92 }
93 values.push(last);
96 offset += 4;
97 } else {
98 values.push(value);
99 offset += 2;
100 }
101 }
102
103 if values.is_empty() {
104 return Err(Hidpp20Error::UnsupportedResponse);
105 }
106 values.sort_unstable();
107 values.dedup();
108 Ok(values)
109}
110
111#[cfg(test)]
112#[allow(clippy::unwrap_used, reason = "expect/unwrap are idiomatic in tests")]
113mod tests {
114 use std::assert_matches;
115
116 use super::parse_dpi_list_payload;
117 use crate::protocol::v20::Hidpp20Error;
118
119 #[test]
120 fn parses_explicit_dpi_list() {
121 let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40, 0x00, 0x00];
122
123 assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
124 }
125
126 #[test]
127 fn expands_range_encoded_dpi_list() {
128 let payload = [0x01, 0x90, 0xe1, 0x90, 0x06, 0x40, 0x00, 0x00];
129
130 assert_eq!(
131 parse_dpi_list_payload(&payload).unwrap(),
132 [400, 800, 1200, 1600]
133 );
134 }
135
136 #[test]
137 fn sorts_and_deduplicates_values() {
138 let payload = [0x06, 0x40, 0x03, 0x20, 0x03, 0x20, 0x00, 0x00];
139
140 assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [800, 1600]);
141 }
142
143 #[test]
144 fn rejects_range_marker_without_previous_value() {
145 let payload = [0xe0, 0x32, 0x1f, 0x40, 0x00, 0x00];
146
147 assert_matches!(
148 parse_dpi_list_payload(&payload),
149 Err(Hidpp20Error::UnsupportedResponse)
150 );
151 }
152
153 #[test]
154 fn rejects_range_marker_without_end_value() {
155 let payload = [0x01, 0x90, 0xe0, 0x32];
156
157 assert_matches!(
158 parse_dpi_list_payload(&payload),
159 Err(Hidpp20Error::UnsupportedResponse)
160 );
161 }
162
163 #[test]
164 fn rejects_zero_step_range_marker() {
165 let payload = [0x01, 0x90, 0xe0, 0x00, 0x06, 0x40, 0x00, 0x00];
166
167 assert_matches!(
168 parse_dpi_list_payload(&payload),
169 Err(Hidpp20Error::UnsupportedResponse)
170 );
171 }
172
173 #[test]
174 fn rejects_descending_range_marker() {
175 let payload = [0x06, 0x40, 0xe0, 0x32, 0x01, 0x90, 0x00, 0x00];
176
177 assert_matches!(
178 parse_dpi_list_payload(&payload),
179 Err(Hidpp20Error::UnsupportedResponse)
180 );
181 }
182
183 #[test]
184 fn range_keeps_off_grid_high_endpoint() {
185 let payload = [0x01, 0x90, 0xe1, 0x90, 0x05, 0xdc, 0x00, 0x00];
188
189 assert_eq!(
190 parse_dpi_list_payload(&payload).unwrap(),
191 [400, 800, 1200, 1500]
192 );
193 }
194
195 #[test]
196 fn parses_full_list_without_terminator() {
197 let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40];
200
201 assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
202 }
203
204 #[test]
205 fn rejects_payload_with_no_values() {
206 assert_matches!(
207 parse_dpi_list_payload(&[0x00, 0x00]),
208 Err(Hidpp20Error::UnsupportedResponse)
209 );
210 }
211}