Skip to main content

hidpp/feature/
thumbwheel.rs

1//! Implements the `Thumbwheel` feature (ID `0x2150`) that allows configuration
2//! and diversion of thumbwheel events.
3
4use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};
5use openlogi_hidpp_derive::Feature;
6
7use crate::{
8    feature::{DecodeEvent, EventSource, FeatureEndpoint},
9    protocol::v20::Hidpp20Error,
10};
11
12/// Implements the `Thumbwheel` / `0x2150` feature.
13#[derive(Feature)]
14#[creatable(id = 0x2150, version = 0)]
15pub struct ThumbwheelFeature {
16    /// The endpoint this feature talks to.
17    endpoint: FeatureEndpoint,
18
19    /// Publishes decoded events to listeners.
20    events: EventSource<ThumbwheelEvent>,
21}
22
23impl DecodeEvent for ThumbwheelEvent {
24    fn decode(sub_id: u8, payload: &[u8; 16]) -> Option<Self> {
25        // The status update is the only event and carries sub-id 0.
26        if sub_id != 0 {
27            return None;
28        }
29
30        Some(ThumbwheelEvent::StatusUpdate(ThumbwheelStatusUpdate {
31            rotation: i16::from_be_bytes([payload[0], payload[1]]),
32            time_elapsed: u16::from_be_bytes([payload[2], payload[3]]),
33            rotation_status: ThumbwheelRotationStatus::from(payload[4]),
34            touch: payload[5] & (1 << 1) != 0,
35            proxy: payload[5] & (1 << 2) != 0,
36            single_tap: payload[5] & (1 << 3) != 0,
37        }))
38    }
39}
40
41impl ThumbwheelFeature {
42    /// Retrieves some information about the thumbwheel.
43    pub async fn get_thumbwheel_info(&self) -> Result<ThumbwheelInfo, Hidpp20Error> {
44        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
45
46        Ok(ThumbwheelInfo {
47            native_resolution: u16::from_be_bytes([payload[0], payload[1]]),
48            diverted_resolution: u16::from_be_bytes([payload[2], payload[3]]),
49            time_unit: u16::from_be_bytes([payload[6], payload[7]]),
50            default_direction: ThumbwheelDirection::try_from(payload[4] & 1)
51                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
52            capabilities: ThumbwheelCapabilities::from_bits_retain(payload[5]),
53        })
54    }
55
56    /// Retrieves the custom status of the thumbwheel.
57    pub async fn get_thumbwheel_status(&self) -> Result<ThumbwheelStatus, Hidpp20Error> {
58        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
59
60        Ok(ThumbwheelStatus {
61            reporting_mode: ThumbwheelReportingMode::try_from(payload[0])
62                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
63            direction_inverted: payload[1] & 1 != 0,
64            touch: payload[1] & (1 << 1) != 0,
65            proxy: payload[1] & (1 << 2) != 0,
66        })
67    }
68
69    /// Sets the reporting mode of the thumbwheel.
70    ///
71    /// This can be used to divert the thumbwheel notifications to HID++.
72    ///
73    /// If `invert_direction` is set, the [`ThumbwheelStatusUpdate::rotation`]
74    /// field will be the inverse of that would be expected if following
75    /// [`ThumbwheelInfo::default_direction`].
76    pub async fn set_thumbwheel_reporting(
77        &self,
78        mode: ThumbwheelReportingMode,
79        invert_direction: bool,
80    ) -> Result<(), Hidpp20Error> {
81        self.endpoint
82            .call(2, [mode.into(), u8::from(invert_direction), 0x00])
83            .await?;
84
85        Ok(())
86    }
87}
88
89/// Represents information about the thumbwheel as reported by
90/// [`ThumbwheelFeature::get_thumbwheel_info`].
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize))]
93#[non_exhaustive]
94pub struct ThumbwheelInfo {
95    /// The number of ratchets generated by revolution when in native (HID)
96    /// mode.
97    pub native_resolution: u16,
98
99    /// The number of rotation increments generated by revolution when in
100    /// diverted (HID++) mode
101    pub diverted_resolution: u16,
102
103    /// If [`ThumbwheelCapabilities::TIME_STAMP`] is set, this is set to the
104    /// timestamp unit used for [`ThumbwheelStatusUpdate::time_elapsed`] in
105    /// microseconds. If the capability is not supported, this will always be
106    /// `0`.
107    pub time_unit: u16,
108
109    /// The default rotation direction. This determines which rotation direction
110    /// corresponds to which number range (positive or negative) for the
111    /// [`ThumbwheelStatusUpdate::rotation`] value.
112    pub default_direction: ThumbwheelDirection,
113
114    /// The capabilities of the thumbwheel.
115    pub capabilities: ThumbwheelCapabilities,
116}
117
118/// Determines which thumbwheel rotation corresponds to which number range
119/// (positive or negative) for the [`ThumbwheelStatusUpdate::rotation`] value.
120///
121/// The direction descriptors (`LeftOrBack`, `RightOrFront`) are
122/// specific to the device orientation.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize))]
125#[non_exhaustive]
126#[repr(u8)]
127pub enum ThumbwheelDirection {
128    /// Positive rotation means left/back for this device orientation.
129    PositiveWhenLeftOrBack = 0,
130    /// Positive rotation means right/front for this device orientation.
131    PositiveWhenRightOrFront = 1,
132}
133
134bitflags::bitflags! {
135    /// Represents the capabilities the thumbwheel may support.
136    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
137    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
138    pub struct ThumbwheelCapabilities: u8 {
139        /// The thumbwheel emits the elapsed time between two events via
140        /// [`ThumbwheelStatusUpdate::time_elapsed`].
141        const TIME_STAMP = 1 << 0;
142
143        /// The thumbwheel is equipped with a touch sensor, so
144        /// [`ThumbwheelStatusUpdate::touch`] reports whether the user is
145        /// touching it.
146        const TOUCH = 1 << 1;
147
148        /// The thumbwheel is equipped with a proximity sensor, so
149        /// [`ThumbwheelStatusUpdate::proxy`] reports whether the user is close
150        /// to it.
151        const PROXY = 1 << 2;
152
153        /// The thumbwheel detects single taps, so
154        /// [`ThumbwheelStatusUpdate::single_tap`] reports whether the user
155        /// tapped it.
156        const SINGLE_TAP = 1 << 3;
157    }
158}
159
160/// Represents information about the thumbwheel status as reported by
161/// [`ThumbwheelFeature::get_thumbwheel_status`].
162#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize))]
164#[non_exhaustive]
165pub struct ThumbwheelStatus {
166    /// The mode how thumbwheel events are reported (native/HID or
167    /// diverted/HID++).
168    pub reporting_mode: ThumbwheelReportingMode,
169
170    /// Whether the default direction as reported by
171    /// [`ThumbwheelInfo::default_direction`] is inverted.
172    pub direction_inverted: bool,
173
174    /// Whether the user touches the thumbwheel.
175    ///
176    /// This is only set if the device supports touch detection as reported by
177    /// [`ThumbwheelCapabilities::TOUCH`].
178    pub touch: bool,
179
180    /// Whether the user is close to the thumbwheel.
181    ///
182    /// This is only set if the device supports proximity detection as reported
183    /// by [`ThumbwheelCapabilities::PROXY`].
184    pub proxy: bool,
185}
186
187/// Represents the mode how the thumbwheel reports its events.
188#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize))]
190#[non_exhaustive]
191#[repr(u8)]
192pub enum ThumbwheelReportingMode {
193    /// Thumbwheel events are reported only to the native HID channel.
194    Native = 0,
195
196    /// Thumbwheel events are reported only to the diverted HID++ channel.
197    ///
198    /// This mode is required for
199    /// [`ThumbwheelFeature::listen`](crate::feature::EmittingFeature::listen) to
200    /// report any events.
201    Diverted = 1,
202}
203
204/// Represents an event emitted by the [`ThumbwheelFeature`] feature.
205#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
206#[cfg_attr(feature = "serde", derive(serde::Serialize))]
207#[non_exhaustive]
208pub enum ThumbwheelEvent {
209    /// Is emitted whenever the thumbwheel status updates.
210    ///
211    /// Requires the thumbwheel to be in diverted reporting mode.
212    StatusUpdate(ThumbwheelStatusUpdate),
213}
214
215/// Represents the data of the [`ThumbwheelEvent::StatusUpdate`] event.
216#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize))]
218#[non_exhaustive]
219pub struct ThumbwheelStatusUpdate {
220    /// The rotation in relation to [`ThumbwheelInfo::native_resolution`] or
221    /// [`ThumbwheelInfo::diverted_resolution`].
222    pub rotation: i16,
223
224    /// The time elapsed since the last event.
225    ///
226    /// The unit of this value is reported in [`ThumbwheelInfo::time_unit`].
227    ///
228    /// If [`ThumbwheelCapabilities::TIME_STAMP`] is not supported, this value
229    /// will be `0`.
230    pub time_elapsed: u16,
231
232    /// The status of the current rotation.
233    pub rotation_status: ThumbwheelRotationStatus,
234
235    /// Whether the user touches the thumbwheel.
236    ///
237    /// This is only set if the device supports touch detection as reported by
238    /// [`ThumbwheelCapabilities::TOUCH`].
239    pub touch: bool,
240
241    /// Whether the user is close to the thumbwheel.
242    ///
243    /// This is only set if the device supports proximity detection as reported
244    /// by [`ThumbwheelCapabilities::PROXY`].
245    pub proxy: bool,
246
247    /// Whether the user single-tapped the thumbwheel.
248    ///
249    /// This is only set if the device supports single-tap detection as reported
250    /// by [`ThumbwheelCapabilities::SINGLE_TAP`].
251    pub single_tap: bool,
252}
253
254/// Represents a thumbwheel rotation status as reported in
255/// [`ThumbwheelStatusUpdate::rotation_status`].
256///
257/// Infallible decode on purpose: this rides a *diverted* input event, so
258/// failing on an unknown byte would silently drop the whole update — rotation
259/// delta included — input the user already produced and the OS no longer
260/// sees. The unknown byte is preserved instead.
261#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)]
262#[cfg_attr(feature = "serde", derive(serde::Serialize))]
263#[non_exhaustive]
264#[repr(u8)]
265pub enum ThumbwheelRotationStatus {
266    /// The thumbwheel was not rotated.
267    Inactive = 0,
268
269    /// The thumbwheel rotation was started.
270    Start = 1,
271
272    /// The thumbwheel rotation is ongoing.
273    Active = 2,
274
275    /// The thumbwheel was released.
276    Stop = 3,
277
278    /// A status this crate does not model; carries the raw byte.
279    #[num_enum(catch_all)]
280    Other(u8),
281}