Skip to main content

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}