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 let (fields, _partial) = payload.as_chunks::<2>();
35 for &[high, low] in fields.iter().take(8) {
36 if high == 0x01 {
37 return DescriptorScan::End;
38 }
39 if high & 0x80 == 0 {
40 continue;
41 }
42
43 let divertable = high & 0x02 != 0;
44 if low == THUMBWHEEL_GESTURE_ID {
45 return DescriptorScan::Thumbwheel(ThumbwheelGesture {
46 diversion_index: divertable.then_some(diversion_index),
47 });
48 }
49 if divertable {
50 diversion_index = diversion_index.saturating_add(1);
51 }
52 }
53 DescriptorScan::Continue {
54 next_diversion_index: diversion_index,
55 }
56}
57
58fn diversion_address(index: u16) -> Result<(u8, u8), Hidpp20Error> {
59 let offset = u8::try_from(index >> 3).map_err(|_| Hidpp20Error::UnsupportedResponse)?;
60 let mask = 1u8 << u32::from(index & 7);
61 Ok((offset, mask))
62}
63
64fn diversion_write_payload(index: u16, diverted: bool) -> Result<[u8; 16], Hidpp20Error> {
65 let (offset, mask) = diversion_address(index)?;
66 let mut payload = [0u8; 16];
67 payload[..4].copy_from_slice(&[offset, 0x01, mask, if diverted { mask } else { 0 }]);
68 Ok(payload)
69}
70
71#[derive(Clone, Feature)]
73#[creatable(id = 0x6501, version = 0)]
74pub struct Gestures2Feature {
75 endpoint: FeatureEndpoint,
76}
77
78impl Gestures2Feature {
79 pub async fn thumbwheel(&self) -> Result<Option<ThumbwheelGesture>, Hidpp20Error> {
83 let mut index = 0u16;
84 let mut diversion_index = 0u16;
85 while index < MAX_DESCRIPTOR_FIELDS {
86 let [hi, lo] = index.to_be_bytes();
87 let payload = self.endpoint.call(0, [hi, lo, 0]).await?.extend_payload();
88 match scan_descriptor_page(&payload, diversion_index) {
89 DescriptorScan::Thumbwheel(thumbwheel) => return Ok(Some(thumbwheel)),
90 DescriptorScan::End => return Ok(None),
91 DescriptorScan::Continue {
92 next_diversion_index,
93 } => {
94 diversion_index = next_diversion_index;
95 index = index.saturating_add(8);
96 }
97 }
98 }
99 Err(Hidpp20Error::UnsupportedResponse)
100 }
101
102 pub async fn has_thumbwheel(&self) -> Result<bool, Hidpp20Error> {
104 Ok(self.thumbwheel().await?.is_some())
105 }
106
107 pub async fn thumbwheel_diverted(&self) -> Result<Option<bool>, Hidpp20Error> {
110 let Some(index) = self.thumbwheel().await?.and_then(|g| g.diversion_index) else {
111 return Ok(None);
112 };
113 let (offset, mask) = diversion_address(index)?;
114 let payload = self
115 .endpoint
116 .call(3, [offset, 0x01, mask])
117 .await?
118 .extend_payload();
119 Ok(Some(payload[0] & mask != 0))
120 }
121
122 pub async fn set_thumbwheel_diverted(&self, diverted: bool) -> Result<bool, Hidpp20Error> {
126 let Some(index) = self.thumbwheel().await?.and_then(|g| g.diversion_index) else {
127 return Ok(false);
128 };
129 self.endpoint
130 .call_long(4, diversion_write_payload(index, diverted)?)
131 .await?;
132 Ok(true)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn descriptor_page_detects_thumbwheel_and_end_marker() {
142 let mut payload = [0u8; 16];
143 payload[0] = 0x83; payload[1] = THUMBWHEEL_GESTURE_ID;
145 assert_eq!(
146 scan_descriptor_page(&payload, 0),
147 DescriptorScan::Thumbwheel(ThumbwheelGesture {
148 diversion_index: Some(0)
149 })
150 );
151
152 let mut end = [0u8; 16];
153 end[0] = 0x01;
154 assert_eq!(scan_descriptor_page(&end, 0), DescriptorScan::End);
155 }
156
157 #[test]
158 fn descriptor_page_ignores_other_gestures() {
159 let mut payload = [0u8; 16];
160 payload[0] = 0x83;
161 payload[1] = 45; assert_eq!(
163 scan_descriptor_page(&payload, 0),
164 DescriptorScan::Continue {
165 next_diversion_index: 1
166 }
167 );
168 }
169
170 #[test]
171 fn descriptor_page_counts_divertable_gestures_before_thumbwheel() {
172 let mut payload = [0u8; 16];
173 payload[0] = 0x82; payload[1] = 40;
175 payload[2] = 0x80; payload[3] = 41;
177 payload[4] = 0x82; payload[5] = THUMBWHEEL_GESTURE_ID;
179
180 assert_eq!(
181 scan_descriptor_page(&payload, 3),
182 DescriptorScan::Thumbwheel(ThumbwheelGesture {
183 diversion_index: Some(4)
184 })
185 );
186 }
187
188 #[test]
189 fn diversion_write_payload_uses_offset_mask_and_value() {
190 let enabled = diversion_write_payload(9, true).unwrap();
191 assert_eq!(&enabled[..4], &[1, 1, 2, 2]);
192
193 let disabled = diversion_write_payload(9, false).unwrap();
194 assert_eq!(&disabled[..4], &[1, 1, 2, 0]);
195 }
196}