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