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)]
112mod tests {
113 use std::assert_matches;
114
115 use super::parse_dpi_list_payload;
116 use crate::protocol::v20::Hidpp20Error;
117
118 #[test]
119 fn parses_explicit_dpi_list() {
120 let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40, 0x00, 0x00];
121
122 assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
123 }
124
125 #[test]
126 fn expands_range_encoded_dpi_list() {
127 let payload = [0x01, 0x90, 0xe1, 0x90, 0x06, 0x40, 0x00, 0x00];
128
129 assert_eq!(
130 parse_dpi_list_payload(&payload).unwrap(),
131 [400, 800, 1200, 1600]
132 );
133 }
134
135 #[test]
136 fn sorts_and_deduplicates_values() {
137 let payload = [0x06, 0x40, 0x03, 0x20, 0x03, 0x20, 0x00, 0x00];
138
139 assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [800, 1600]);
140 }
141
142 #[test]
143 fn rejects_range_marker_without_previous_value() {
144 let payload = [0xe0, 0x32, 0x1f, 0x40, 0x00, 0x00];
145
146 assert_matches!(
147 parse_dpi_list_payload(&payload),
148 Err(Hidpp20Error::UnsupportedResponse)
149 );
150 }
151
152 #[test]
153 fn rejects_range_marker_without_end_value() {
154 let payload = [0x01, 0x90, 0xe0, 0x32];
155
156 assert_matches!(
157 parse_dpi_list_payload(&payload),
158 Err(Hidpp20Error::UnsupportedResponse)
159 );
160 }
161
162 #[test]
163 fn rejects_zero_step_range_marker() {
164 let payload = [0x01, 0x90, 0xe0, 0x00, 0x06, 0x40, 0x00, 0x00];
165
166 assert_matches!(
167 parse_dpi_list_payload(&payload),
168 Err(Hidpp20Error::UnsupportedResponse)
169 );
170 }
171
172 #[test]
173 fn rejects_descending_range_marker() {
174 let payload = [0x06, 0x40, 0xe0, 0x32, 0x01, 0x90, 0x00, 0x00];
175
176 assert_matches!(
177 parse_dpi_list_payload(&payload),
178 Err(Hidpp20Error::UnsupportedResponse)
179 );
180 }
181
182 #[test]
183 fn range_keeps_off_grid_high_endpoint() {
184 let payload = [0x01, 0x90, 0xe1, 0x90, 0x05, 0xdc, 0x00, 0x00];
187
188 assert_eq!(
189 parse_dpi_list_payload(&payload).unwrap(),
190 [400, 800, 1200, 1500]
191 );
192 }
193
194 #[test]
195 fn parses_full_list_without_terminator() {
196 let payload = [0x01, 0x90, 0x03, 0x20, 0x06, 0x40];
199
200 assert_eq!(parse_dpi_list_payload(&payload).unwrap(), [400, 800, 1600]);
201 }
202
203 #[test]
204 fn rejects_payload_with_no_values() {
205 assert_matches!(
206 parse_dpi_list_payload(&[0x00, 0x00]),
207 Err(Hidpp20Error::UnsupportedResponse)
208 );
209 }
210}