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