hidpp/feature/thumbwheel/mod.rs
1//! Implements the `Thumbwheel` feature (ID `0x2150`) that allows configuration
2//! and diversion of thumbwheel events.
3
4use std::sync::Arc;
5
6use num_enum::{IntoPrimitive, TryFromPrimitive};
7
8use crate::{
9 channel::HidppChannel,
10 event::EventEmitter,
11 feature::{CreatableFeature, EmittingFeature, Feature},
12 nibble::{self, U4},
13 protocol::v20::{self, Hidpp20Error},
14};
15
16/// Implements the `Thumbwheel` / `0x2150` feature.
17pub struct ThumbwheelFeature {
18 /// The underlying HID++ channel.
19 chan: Arc<HidppChannel>,
20
21 /// The index of the device to implement the feature for.
22 device_index: u8,
23
24 /// The index of the feature in the feature table.
25 feature_index: u8,
26
27 /// The emitter used to emit events.
28 emitter: Arc<EventEmitter<ThumbwheelEvent>>,
29
30 /// The handle assigned to the message listener registered via
31 /// [`HidppChannel::add_msg_listener`].
32 /// This is used to remove the listener when the feature is dropped.
33 msg_listener_hdl: u32,
34}
35
36impl CreatableFeature for ThumbwheelFeature {
37 const ID: u16 = 0x2150;
38 const STARTING_VERSION: u8 = 0;
39
40 fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
41 let emitter = Arc::new(EventEmitter::new());
42
43 let hdl = chan.add_msg_listener({
44 let emitter = Arc::clone(&emitter);
45
46 move |raw, matched| {
47 if matched {
48 return;
49 }
50
51 let msg = v20::Message::from(raw);
52
53 let header = msg.header();
54 if header.device_index != device_index
55 || header.feature_index != feature_index
56 || nibble::combine(header.software_id, header.function_id) != 0
57 {
58 return;
59 }
60
61 let payload = msg.extend_payload();
62 let Ok(rotation_status) = ThumbwheelRotationStatus::try_from(payload[4]) else {
63 return;
64 };
65
66 emitter.emit(ThumbwheelEvent::StatusUpdate(ThumbwheelStatusUpdate {
67 rotation: i16::from_be_bytes(payload[0..=1].try_into().unwrap()),
68 time_elapsed: u16::from_be_bytes(payload[2..=3].try_into().unwrap()),
69 rotation_status,
70 touch: payload[5] & (1 << 1) != 0,
71 proxy: payload[5] & (1 << 2) != 0,
72 single_tap: payload[5] & (1 << 3) != 0,
73 }));
74 }
75 });
76
77 Self {
78 chan,
79 device_index,
80 feature_index,
81 emitter,
82 msg_listener_hdl: hdl,
83 }
84 }
85}
86
87impl Feature for ThumbwheelFeature {}
88
89impl EmittingFeature<ThumbwheelEvent> for ThumbwheelFeature {
90 fn listen(&self) -> async_channel::Receiver<ThumbwheelEvent> {
91 self.emitter.create_receiver()
92 }
93}
94
95impl Drop for ThumbwheelFeature {
96 fn drop(&mut self) {
97 self.chan.remove_msg_listener(self.msg_listener_hdl);
98 }
99}
100
101impl ThumbwheelFeature {
102 /// Retrieves some information about the thumbwheel.
103 pub async fn get_thumbwheel_info(&self) -> Result<ThumbwheelInfo, Hidpp20Error> {
104 let response = self
105 .chan
106 .send_v20(v20::Message::Short(
107 v20::MessageHeader {
108 device_index: self.device_index,
109 feature_index: self.feature_index,
110 function_id: U4::from_lo(0),
111 software_id: self.chan.get_sw_id(),
112 },
113 [0x00, 0x00, 0x00],
114 ))
115 .await?;
116
117 let payload = response.extend_payload();
118
119 Ok(ThumbwheelInfo {
120 native_resolution: u16::from_be_bytes(payload[0..=1].try_into().unwrap()),
121 diverted_resolution: u16::from_be_bytes(payload[2..=3].try_into().unwrap()),
122 time_unit: u16::from_be_bytes(payload[6..=7].try_into().unwrap()),
123 default_direction: ThumbwheelDirection::try_from(payload[4] & 1)
124 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
125 capabilities: ThumbwheelCapabilities::from(payload[5]),
126 })
127 }
128
129 /// Retrieves the custom status of the thumbwheel.
130 pub async fn get_thumbwheel_status(&self) -> Result<ThumbwheelStatus, Hidpp20Error> {
131 let response = self
132 .chan
133 .send_v20(v20::Message::Short(
134 v20::MessageHeader {
135 device_index: self.device_index,
136 feature_index: self.feature_index,
137 function_id: U4::from_lo(1),
138 software_id: self.chan.get_sw_id(),
139 },
140 [0x00, 0x00, 0x00],
141 ))
142 .await?;
143
144 let payload = response.extend_payload();
145
146 Ok(ThumbwheelStatus {
147 reporting_mode: ThumbwheelReportingMode::try_from(payload[0])
148 .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
149 direction_inverted: payload[1] & 1 != 0,
150 touch: payload[1] & (1 << 1) != 0,
151 proxy: payload[1] & (1 << 2) != 0,
152 })
153 }
154
155 /// Sets the reporting mode of the thumbwheel.
156 ///
157 /// This can be used to divert the thumbwheel notifications to HID++.
158 ///
159 /// If `invert_direction` is set, the [`ThumbwheelStatusUpdate::rotation`]
160 /// field will be the inverse of that would be expected if following
161 /// [`ThumbwheelInfo::default_direction`].
162 pub async fn set_thumbwheel_reporting(
163 &self,
164 mode: ThumbwheelReportingMode,
165 invert_direction: bool,
166 ) -> Result<(), Hidpp20Error> {
167 self.chan
168 .send_v20(v20::Message::Short(
169 v20::MessageHeader {
170 device_index: self.device_index,
171 feature_index: self.feature_index,
172 function_id: U4::from_lo(2),
173 software_id: self.chan.get_sw_id(),
174 },
175 [mode.into(), if invert_direction { 1 } else { 0 }, 0x00],
176 ))
177 .await?;
178
179 Ok(())
180 }
181}
182
183/// Represents information about the thumbwheel as reported by
184/// [`ThumbwheelFeature::get_thumbwheel_info`].
185#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize))]
187#[non_exhaustive]
188pub struct ThumbwheelInfo {
189 /// The number of ratchets generated by revolution when in native (HID)
190 /// mode.
191 pub native_resolution: u16,
192
193 /// The number of rotation increments generated by revolution when in
194 /// diverted (HID++) mode
195 pub diverted_resolution: u16,
196
197 /// If [`ThumbwheelCapabilities::time_stamp`] is set, this is set to the
198 /// timestamp unit used for [`ThumbwheelStatusUpdate::time_elapsed`] in
199 /// microseconds. If the capability is not supported, this will always be
200 /// `0`.
201 pub time_unit: u16,
202
203 /// The default rotation direction. This determines which rotation direction
204 /// corresponds to which number range (positive or negative) for the
205 /// [`ThumbwheelStatusUpdate::rotation`] value.
206 pub default_direction: ThumbwheelDirection,
207
208 /// The capabilites of the thumbwheel.
209 pub capabilities: ThumbwheelCapabilities,
210}
211
212/// Determines which thumbwheel rotation corresponds to which number range
213/// (positive or negative) for the [`ThumbwheelStatusUpdate::rotation`] value.
214///
215/// The direction descriptors (`LeftOrBack`, `RightOrFront`) are
216/// specific to the device orientation.
217#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
218#[cfg_attr(feature = "serde", derive(serde::Serialize))]
219#[non_exhaustive]
220#[repr(u8)]
221pub enum ThumbwheelDirection {
222 PositiveWhenLeftOrBack = 0,
223 PositiveWhenRightOrFront = 1,
224}
225
226/// Represents the capabilities the thumbwheel may support.
227#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
228#[cfg_attr(feature = "serde", derive(serde::Serialize))]
229#[non_exhaustive]
230pub struct ThumbwheelCapabilities {
231 /// Whether the thumbwheel supports emitting the elapsed time between two
232 /// events via [`ThumbwheelStatusUpdate::time_elapsed`].
233 pub time_stamp: bool,
234
235 /// Whether the thumbwheel is equipped with a touch sensor.
236 ///
237 /// If this capability is supported, [`ThumbwheelStatusUpdate::touch`] will
238 /// be set to whether the user touches the thumbwheel.
239 pub touch: bool,
240
241 /// Whether the thumbwheel is equipped with a proximity sensor.
242 ///
243 /// If this capability is supported, [`ThumbwheelStatusUpdate::proxy`] will
244 /// be set to whether the user is close to the thumbwheel.
245 pub proxy: bool,
246
247 /// Whether the thumbwheel supports detecting single taps.
248 ///
249 /// If this capability is supported, [`ThumbwheelStatusUpdate::single_tap`]
250 /// will be set to whether the user tapped the thumbwheel.
251 pub single_tap: bool,
252}
253
254impl From<u8> for ThumbwheelCapabilities {
255 fn from(value: u8) -> Self {
256 Self {
257 time_stamp: value & 1 != 0,
258 touch: value & (1 << 1) != 0,
259 proxy: value & (1 << 2) != 0,
260 single_tap: value & (1 << 3) != 0,
261 }
262 }
263}
264
265/// Represents information about the thumbwheel status as reported by
266/// [`ThumbwheelFeature::get_thumbwheel_status`].
267#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
268#[cfg_attr(feature = "serde", derive(serde::Serialize))]
269#[non_exhaustive]
270pub struct ThumbwheelStatus {
271 /// The mode how thumbwheel events are reported (native/HID or
272 /// diverted/HID++).
273 pub reporting_mode: ThumbwheelReportingMode,
274
275 /// Whether the default direction as reported by
276 /// [`ThumbwheelInfo::default_direction`] is inverted.
277 pub direction_inverted: bool,
278
279 /// Whether the user touches the thumbwheel.
280 ///
281 /// This is only set if the device supports touch detection as reported by
282 /// [`ThumbwheelCapabilities::touch`].
283 pub touch: bool,
284
285 /// Whether the user is close to the thumbwheel.
286 ///
287 /// This is only set if the device supports proximity detection as reported
288 /// by [`ThumbwheelCapabilities::proxy`].
289 pub proxy: bool,
290}
291
292/// Represents the mode how the thumbwheel reports its events.
293#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
294#[cfg_attr(feature = "serde", derive(serde::Serialize))]
295#[non_exhaustive]
296#[repr(u8)]
297pub enum ThumbwheelReportingMode {
298 /// Thumbwheel events are reported only to the native HID channel.
299 Native = 0,
300
301 /// Thumbwheel events are reported only to the diverted HID++ channel.
302 ///
303 /// This mode is required for [`ThumbwheelFeature::listen`] to report any
304 /// events.
305 Diverted = 1,
306}
307
308/// Represents an event emitted by the [`ThumbwheelFeature`] feature.
309#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
310#[cfg_attr(feature = "serde", derive(serde::Serialize))]
311#[non_exhaustive]
312pub enum ThumbwheelEvent {
313 /// Is emitted whenever the thumbwheel status updates.
314 ///
315 /// Requires the thumbwheel to be in diverted reporting mode.
316 StatusUpdate(ThumbwheelStatusUpdate),
317}
318
319/// Represents the data of the [`ThumbwheelEvent::StatusUpdate`] event.
320#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
321#[cfg_attr(feature = "serde", derive(serde::Serialize))]
322#[non_exhaustive]
323pub struct ThumbwheelStatusUpdate {
324 /// The rotation in relation to [`ThumbwheelInfo::native_resolution`] or
325 /// [`ThumbwheelInfo::diverted_resolution`].
326 pub rotation: i16,
327
328 /// The time elapsed since the last event.
329 ///
330 /// The unit of this value is reported in [`ThumbwheelInfo::time_unit`].
331 ///
332 /// If [`ThumbwheelCapabilities::time_stamp`] is not supported, this value
333 /// will be `0`.
334 pub time_elapsed: u16,
335
336 /// The status of the current rotation.
337 pub rotation_status: ThumbwheelRotationStatus,
338
339 /// Whether the user touches the thumbwheel.
340 ///
341 /// This is only set if the device supports touch detection as reported by
342 /// [`ThumbwheelCapabilities::touch`].
343 pub touch: bool,
344
345 /// Whether the user is close to the thumbwheel.
346 ///
347 /// This is only set if the device supports proximity detection as reported
348 /// by [`ThumbwheelCapabilities::proxy`].
349 pub proxy: bool,
350
351 /// Whether the user single-tapped the thumbwheel.
352 ///
353 /// This is only set if the device supports single-tap detection as reported
354 /// by [`ThumbwheelCapabilities::single_tap`].
355 pub single_tap: bool,
356}
357
358/// Represents a thumbwheel rotation status as reported in
359/// [`ThumbwheelStatusUpdate::rotation_status`].
360#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
361#[cfg_attr(feature = "serde", derive(serde::Serialize))]
362#[non_exhaustive]
363#[repr(u8)]
364pub enum ThumbwheelRotationStatus {
365 /// The thumbwheel was not rotated.
366 Inactive = 0,
367
368 /// The thumbwheel rotation was started.
369 Start = 1,
370
371 /// The thumbwheel rotation is ongoing.
372 Active = 2,
373
374 /// The thumbwheel was released.
375 Stop = 3,
376}