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}