hidpp/feature/gestures2/
mod.rs1use std::sync::Arc;
7
8use crate::{
9 channel::HidppChannel,
10 feature::{CreatableFeature, Feature, FeatureEndpoint},
11 protocol::v20::Hidpp20Error,
12};
13
14pub const THUMBWHEEL_GESTURE_ID: u8 = 46;
16
17const MAX_DESCRIPTOR_FIELDS: u16 = 1024;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct ThumbwheelGesture {
25 pub diversion_index: Option<u16>,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31enum DescriptorScan {
32 Thumbwheel(ThumbwheelGesture),
33 End,
34 Continue { next_diversion_index: u16 },
35}
36
37fn scan_descriptor_page(payload: &[u8], mut diversion_index: u16) -> DescriptorScan {
38 for field in payload.chunks_exact(2).take(8) {
39 let high = field[0];
40 let low = field[1];
41 if high == 0x01 {
42 return DescriptorScan::End;
43 }
44 if high & 0x80 == 0 {
45 continue;
46 }
47
48 let divertable = high & 0x02 != 0;
49 if low == THUMBWHEEL_GESTURE_ID {
50 return DescriptorScan::Thumbwheel(ThumbwheelGesture {
51 diversion_index: divertable.then_some(diversion_index),
52 });
53 }
54 if divertable {
55 diversion_index = diversion_index.saturating_add(1);
56 }
57 }
58 DescriptorScan::Continue {
59 next_diversion_index: diversion_index,
60 }
61}
62
63fn diversion_address(index: u16) -> Result<(u8, u8), Hidpp20Error> {
64 let offset = u8::try_from(index >> 3).map_err(|_| Hidpp20Error::UnsupportedResponse)?;
65 let mask = 1u8 << u32::from(index & 7);
66 Ok((offset, mask))
67}
68
69fn diversion_write_payload(index: u16, diverted: bool) -> Result<[u8; 16], Hidpp20Error> {
70 let (offset, mask) = diversion_address(index)?;
71 let mut payload = [0u8; 16];
72 payload[..4].copy_from_slice(&[offset, 0x01, mask, if diverted { mask } else { 0 }]);
73 Ok(payload)
74}
75
76#[derive(Clone)]
78pub struct Gestures2Feature {
79 endpoint: FeatureEndpoint,
80}
81
82impl CreatableFeature for Gestures2Feature {
83 const ID: u16 = 0x6501;
84 const STARTING_VERSION: u8 = 0;
85
86 fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
87 Self {
88 endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
89 }
90 }
91}
92
93impl Feature for Gestures2Feature {}
94
95impl Gestures2Feature {
96 pub async fn thumbwheel(&self) -> Result<Option<ThumbwheelGesture>, Hidpp20Error> {
100 let mut index = 0u16;
101 let mut diversion_index = 0u16;
102 while index < MAX_DESCRIPTOR_FIELDS {
103 let [hi, lo] = index.to_be_bytes();
104 let payload = self.endpoint.call(0, [hi, lo, 0]).await?.extend_payload();
105 match scan_descriptor_page(&payload, diversion_index) {
106 DescriptorScan::Thumbwheel(thumbwheel) => return Ok(Some(thumbwheel)),
107 DescriptorScan::End => return Ok(None),
108 DescriptorScan::Continue {
109 next_diversion_index,
110 } => {
111 diversion_index = next_diversion_index;
112 index = index.saturating_add(8);
113 }
114 }
115 }
116 Err(Hidpp20Error::UnsupportedResponse)
117 }
118
119 pub async fn has_thumbwheel(&self) -> Result<bool, Hidpp20Error> {
121 Ok(self.thumbwheel().await?.is_some())
122 }
123
124 pub async fn thumbwheel_diverted(&self) -> Result<Option<bool>, Hidpp20Error> {
127 let Some(index) = self.thumbwheel().await?.and_then(|g| g.diversion_index) else {
128 return Ok(None);
129 };
130 let (offset, mask) = diversion_address(index)?;
131 let payload = self
132 .endpoint
133 .call(3, [offset, 0x01, mask])
134 .await?
135 .extend_payload();
136 Ok(Some(payload[0] & mask != 0))
137 }
138
139 pub async fn set_thumbwheel_diverted(&self, diverted: bool) -> Result<bool, Hidpp20Error> {
143 let Some(index) = self.thumbwheel().await?.and_then(|g| g.diversion_index) else {
144 return Ok(false);
145 };
146 self.endpoint
147 .call_long(4, diversion_write_payload(index, diverted)?)
148 .await?;
149 Ok(true)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn descriptor_page_detects_thumbwheel_and_end_marker() {
159 let mut payload = [0u8; 16];
160 payload[0] = 0x83; payload[1] = THUMBWHEEL_GESTURE_ID;
162 assert_eq!(
163 scan_descriptor_page(&payload, 0),
164 DescriptorScan::Thumbwheel(ThumbwheelGesture {
165 diversion_index: Some(0)
166 })
167 );
168
169 let mut end = [0u8; 16];
170 end[0] = 0x01;
171 assert_eq!(scan_descriptor_page(&end, 0), DescriptorScan::End);
172 }
173
174 #[test]
175 fn descriptor_page_ignores_other_gestures() {
176 let mut payload = [0u8; 16];
177 payload[0] = 0x83;
178 payload[1] = 45; assert_eq!(
180 scan_descriptor_page(&payload, 0),
181 DescriptorScan::Continue {
182 next_diversion_index: 1
183 }
184 );
185 }
186
187 #[test]
188 fn descriptor_page_counts_divertable_gestures_before_thumbwheel() {
189 let mut payload = [0u8; 16];
190 payload[0] = 0x82; payload[1] = 40;
192 payload[2] = 0x80; payload[3] = 41;
194 payload[4] = 0x82; payload[5] = THUMBWHEEL_GESTURE_ID;
196
197 assert_eq!(
198 scan_descriptor_page(&payload, 3),
199 DescriptorScan::Thumbwheel(ThumbwheelGesture {
200 diversion_index: Some(4)
201 })
202 );
203 }
204
205 #[test]
206 fn diversion_write_payload_uses_offset_mask_and_value() {
207 let enabled = diversion_write_payload(9, true).unwrap();
208 assert_eq!(&enabled[..4], &[1, 1, 2, 2]);
209
210 let disabled = diversion_write_payload(9, false).unwrap();
211 assert_eq!(&disabled[..4], &[1, 1, 2, 0]);
212 }
213}