1use bitflags::bitflags;
8use hidpp_transport::HidapiChannel;
9use tracing::{debug, trace};
10
11use crate::error::{HidppErrorCode, ProtocolError, Result};
12use crate::protocol::{build_long_request, get_error_code, is_error_response};
13
14mod function_id {
16 pub const GET_INFO: u8 = 0x00;
18 pub const GET_STATUS: u8 = 0x01;
20 pub const SET_REPORTING: u8 = 0x02;
22}
23
24bitflags! {
25 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
27 pub struct ThumbWheelCapabilities: u8 {
28 const TIMESTAMP = 0x01;
30 const TOUCH = 0x02;
32 const PROXY = 0x04;
34 const SINGLE_TAP = 0x08;
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[repr(u8)]
42pub enum RotationStatus {
43 Inactive = 0,
45 Start = 1,
47 Active = 2,
49 Stop = 3,
51}
52
53impl TryFrom<u8> for RotationStatus {
54 type Error = ();
55
56 fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
57 match value {
58 0 => Ok(Self::Inactive),
59 1 => Ok(Self::Start),
60 2 => Ok(Self::Active),
61 3 => Ok(Self::Stop),
62 _ => Err(()),
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct ThumbWheelInfo {
70 pub native_resolution: u16,
72 pub diverted_resolution: u16,
74 pub default_direction: bool,
76 pub capabilities: ThumbWheelCapabilities,
78 pub time_elapsed: u16,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84#[allow(clippy::struct_excessive_bools)] pub struct ThumbWheelStatus {
86 pub divert: bool,
88 pub invert: bool,
90 pub touch: bool,
92 pub proxy: bool,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Default)]
98pub struct ThumbWheelConfig {
99 pub divert: bool,
101 pub invert: bool,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ThumbWheelEvent {
108 pub rotation: i16,
110 pub timestamp: u16,
112 pub status: RotationStatus,
114 pub touch: bool,
116 pub proxy: bool,
118 pub single_tap: bool,
120}
121
122pub struct ThumbWheelFeature {
124 device_index: u8,
125 feature_index: u8,
126}
127
128impl ThumbWheelFeature {
129 #[must_use]
135 pub fn new(device_index: u8, feature_index: u8) -> Self {
136 Self {
137 device_index,
138 feature_index,
139 }
140 }
141
142 pub async fn get_info(&self, channel: &HidapiChannel) -> Result<ThumbWheelInfo> {
149 let request = build_long_request(
150 self.device_index,
151 self.feature_index,
152 function_id::GET_INFO,
153 &[],
154 );
155
156 trace!("getting thumb wheel info");
157 let response = channel.request(&request, 5).await?;
158
159 if is_error_response(&response) {
160 let code = get_error_code(&response).unwrap_or(0);
161 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
162 }
163
164 if response.len() < 12 {
165 return Err(ProtocolError::InvalidResponse(
166 "thumb wheel info response too short".to_string(),
167 ));
168 }
169
170 let native_resolution = u16::from_be_bytes([response[4], response[5]]);
177 let diverted_resolution = u16::from_be_bytes([response[6], response[7]]);
178 let default_direction = response[8] != 0;
179 let capabilities = ThumbWheelCapabilities::from_bits_truncate(response[9]);
180 let time_elapsed = u16::from_be_bytes([response[10], response[11]]);
181
182 let info = ThumbWheelInfo {
183 native_resolution,
184 diverted_resolution,
185 default_direction,
186 capabilities,
187 time_elapsed,
188 };
189
190 debug!(
191 native = native_resolution,
192 diverted = diverted_resolution,
193 caps = ?capabilities,
194 "got thumb wheel info"
195 );
196
197 Ok(info)
198 }
199
200 pub async fn get_status(&self, channel: &HidapiChannel) -> Result<ThumbWheelStatus> {
205 let request = build_long_request(
206 self.device_index,
207 self.feature_index,
208 function_id::GET_STATUS,
209 &[],
210 );
211
212 trace!("getting thumb wheel status");
213 let response = channel.request(&request, 5).await?;
214
215 if is_error_response(&response) {
216 let code = get_error_code(&response).unwrap_or(0);
217 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
218 }
219
220 if response.len() < 5 {
221 return Err(ProtocolError::InvalidResponse(
222 "thumb wheel status response too short".to_string(),
223 ));
224 }
225
226 let flags = response[4];
232 let status = ThumbWheelStatus {
233 divert: (flags & 0x01) != 0,
234 invert: (flags & 0x02) != 0,
235 touch: (flags & 0x04) != 0,
236 proxy: (flags & 0x08) != 0,
237 };
238
239 debug!(
240 divert = status.divert,
241 invert = status.invert,
242 touch = status.touch,
243 proxy = status.proxy,
244 "got thumb wheel status"
245 );
246
247 Ok(status)
248 }
249
250 pub async fn set_reporting(
259 &self,
260 channel: &HidapiChannel,
261 config: &ThumbWheelConfig,
262 ) -> Result<()> {
263 let mut flags: u8 = 0;
264 if config.divert {
265 flags |= 0x01;
266 }
267 if config.invert {
268 flags |= 0x02;
269 }
270
271 let request = build_long_request(
272 self.device_index,
273 self.feature_index,
274 function_id::SET_REPORTING,
275 &[flags],
276 );
277
278 trace!(
279 divert = config.divert,
280 invert = config.invert,
281 "setting thumb wheel reporting"
282 );
283 let response = channel.request(&request, 5).await?;
284
285 if is_error_response(&response) {
286 let code = get_error_code(&response).unwrap_or(0);
287 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
288 }
289
290 debug!(
291 divert = config.divert,
292 invert = config.invert,
293 "set thumb wheel reporting"
294 );
295
296 Ok(())
297 }
298
299 #[must_use]
310 pub fn parse_event(report: &[u8]) -> Option<ThumbWheelEvent> {
311 if report.len() < 10 {
317 return None;
318 }
319
320 let rotation = i16::from_be_bytes([report[4], report[5]]);
321 let timestamp = u16::from_be_bytes([report[6], report[7]]);
322 let status = RotationStatus::try_from(report[8] & 0x03).unwrap_or(RotationStatus::Inactive);
323 let flags = report[9];
324
325 Some(ThumbWheelEvent {
326 rotation,
327 timestamp,
328 status,
329 touch: (flags & 0x01) != 0,
330 proxy: (flags & 0x02) != 0,
331 single_tap: (flags & 0x04) != 0,
332 })
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_capability_flags() {
342 let caps = ThumbWheelCapabilities::TOUCH | ThumbWheelCapabilities::SINGLE_TAP;
343 assert!(caps.contains(ThumbWheelCapabilities::TOUCH));
344 assert!(caps.contains(ThumbWheelCapabilities::SINGLE_TAP));
345 assert!(!caps.contains(ThumbWheelCapabilities::TIMESTAMP));
346 }
347
348 #[test]
349 fn test_rotation_status() {
350 assert_eq!(RotationStatus::try_from(0), Ok(RotationStatus::Inactive));
351 assert_eq!(RotationStatus::try_from(1), Ok(RotationStatus::Start));
352 assert_eq!(RotationStatus::try_from(2), Ok(RotationStatus::Active));
353 assert_eq!(RotationStatus::try_from(3), Ok(RotationStatus::Stop));
354 assert!(RotationStatus::try_from(4).is_err());
355 }
356
357 #[test]
358 fn test_parse_event() {
359 let mut report = vec![0u8; 10];
361 report[4] = 0x00;
362 report[5] = 0x64; report[6] = 0x01;
364 report[7] = 0xF4; report[8] = 0x02; report[9] = 0x01; let event = ThumbWheelFeature::parse_event(&report).unwrap();
369 assert_eq!(event.rotation, 100);
370 assert_eq!(event.timestamp, 500);
371 assert_eq!(event.status, RotationStatus::Active);
372 assert!(event.touch);
373 assert!(!event.proxy);
374 }
375
376 #[test]
377 fn test_config_default() {
378 let config = ThumbWheelConfig::default();
379 assert!(!config.divert);
380 assert!(!config.invert);
381 }
382}