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