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}