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