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,
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}