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