Skip to main content

hidpp/feature/hires_wheel/
mod.rs

1//! Implements the `HiResWheel` feature (ID `0x2121`) that allows configuring
2//! and using high-resolution scrolling.
3
4use std::{hash::Hash, sync::Arc};
5
6use num_enum::{IntoPrimitive, TryFromPrimitive};
7
8use crate::{
9    channel::{HidppChannel, MessageListenerGuard},
10    event::EventEmitter,
11    feature::{CreatableFeature, EmittingFeature, Feature, FeatureEndpoint, event_payload},
12    nibble::U4,
13    protocol::v20::Hidpp20Error,
14};
15
16/// Implements the `HiResWheel` / `0x2121` feature.
17///
18/// The analytics part of the feature is not implemented here as its data
19/// structure lacks any documentation.
20pub struct HiResWheelFeature {
21    /// The endpoint this feature talks to.
22    endpoint: FeatureEndpoint,
23
24    /// The emitter used to emit events.
25    emitter: Arc<EventEmitter<HiResWheelEvent>>,
26
27    /// Removes the message listener when the feature is dropped.
28    _msg_listener: MessageListenerGuard,
29}
30
31impl CreatableFeature for HiResWheelFeature {
32    const ID: u16 = 0x2121;
33    const STARTING_VERSION: u8 = 0;
34
35    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
36        let emitter = Arc::new(EventEmitter::new());
37
38        let listener = chan.add_msg_listener_guarded({
39            let emitter = Arc::clone(&emitter);
40
41            move |raw, matched| {
42                let Some((func, payload)) =
43                    event_payload(raw, matched, device_index, feature_index)
44                else {
45                    return;
46                };
47
48                // HiResWheel dispatches on the sub-id: 0 = movement, 1 = ratchet switch.
49                let event = match func.to_lo() {
50                    0 => {
51                        let Ok(resolution) =
52                            WheelResolution::try_from((payload[0] & (1 << 4)) >> 4)
53                        else {
54                            return;
55                        };
56
57                        HiResWheelEvent::WheelMovement(WheelMovementData {
58                            resolution,
59                            periods: U4::from_lo(payload[0]),
60                            delta_vertical: i16::from_be_bytes(payload[1..=2].try_into().unwrap()),
61                        })
62                    }
63                    1 => {
64                        let Ok(state) = WheelRatchetState::try_from(payload[0] & 1) else {
65                            return;
66                        };
67
68                        HiResWheelEvent::RatchetSwitch(state)
69                    }
70                    _ => return,
71                };
72
73                emitter.emit(event);
74            }
75        });
76
77        Self {
78            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
79            emitter,
80            _msg_listener: listener,
81        }
82    }
83}
84
85impl Feature for HiResWheelFeature {}
86
87impl EmittingFeature<HiResWheelEvent> for HiResWheelFeature {
88    fn listen(&self) -> async_channel::Receiver<HiResWheelEvent> {
89        self.emitter.create_receiver()
90    }
91}
92
93impl HiResWheelFeature {
94    /// Retrieves the capabilities of the hi-res wheel and this feature.
95    pub async fn get_wheel_capabilities(&self) -> Result<WheelCapabilities, Hidpp20Error> {
96        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
97
98        Ok(WheelCapabilities {
99            multiplier: payload[0],
100            has_invert: payload[1] & (1 << 3) != 0,
101            has_switch: payload[1] & (1 << 2) != 0,
102            ratches_per_rotation: payload[2],
103            wheel_diameter: payload[3],
104        })
105    }
106
107    /// Retrieves the current mode of the hi-res wheel.
108    pub async fn get_wheel_mode(&self) -> Result<WheelMode, Hidpp20Error> {
109        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
110
111        Ok(WheelMode {
112            inverted: payload[0] & (1 << 2) != 0,
113            resolution: WheelResolution::try_from((payload[0] & (1 << 1)) >> 1)
114                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
115            target: WheelEventTarget::try_from(payload[0] & 1)
116                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
117        })
118    }
119
120    /// Sets the mode of the hi-res wheel.
121    ///
122    /// Setting the bit to control analytics collection is not supported in this
123    /// feature implementation as the analytics data structure is completely
124    /// undocumented.\
125    /// If this is implemented in the future, a new implementation will do so to
126    /// not break this one.
127    pub async fn set_wheel_mode(
128        &self,
129        target: WheelEventTarget,
130        resolution: WheelResolution,
131        inverted: bool,
132    ) -> Result<WheelMode, Hidpp20Error> {
133        let mut mode_byte = 0u8;
134        if inverted {
135            mode_byte |= 1 << 2;
136        }
137        mode_byte |= u8::from(resolution) << 1;
138        mode_byte |= u8::from(target);
139
140        let payload = self
141            .endpoint
142            .call(2, [mode_byte, 0x00, 0x00])
143            .await?
144            .extend_payload();
145
146        Ok(WheelMode {
147            inverted: payload[0] & (1 << 2) != 0,
148            resolution: WheelResolution::try_from((payload[0] & (1 << 1)) >> 1)
149                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
150            target: WheelEventTarget::try_from(payload[0] & 1)
151                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
152        })
153    }
154
155    /// Retrieves the current state of the ratchet switch.
156    pub async fn get_ratchet_switch_state(&self) -> Result<WheelRatchetState, Hidpp20Error> {
157        let payload = self.endpoint.call(3, [0; 3]).await?.extend_payload();
158
159        WheelRatchetState::try_from(payload[0] & 1).map_err(|_| Hidpp20Error::UnsupportedResponse)
160    }
161}
162
163/// Represents the capabilities of the hi-res wheel and this feature as reported
164/// by [`HiResWheelFeature::get_wheel_capabilities`].
165#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
166#[cfg_attr(feature = "serde", derive(serde::Serialize))]
167#[non_exhaustive]
168pub struct WheelCapabilities {
169    /// The report multiplier for the high-resolution mode. A single ratchet
170    /// distance will produce this amount of wheel movement reports in hi-res
171    /// mode.
172    pub multiplier: u8,
173
174    /// Whether the device supports inverting the scrolling direction when in
175    /// native HID reporting mode.
176    ///
177    /// Inverting is never supported in diverted HID++ mode.
178    pub has_invert: bool,
179
180    /// Whether the device has a switch to control the ratchet mode.
181    pub has_switch: bool,
182
183    /// The amount of ratches that would be generated by a whole rotation of the
184    /// scroll wheel.
185    pub ratches_per_rotation: u8,
186
187    /// The nominal wheel diameter in millimeters.
188    pub wheel_diameter: u8,
189}
190
191/// Represents the wheel mode as reported by
192/// [`HiResWheelFeature::get_wheel_mode`].
193#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
194#[cfg_attr(feature = "serde", derive(serde::Serialize))]
195#[non_exhaustive]
196pub struct WheelMode {
197    /// Whether the scrolling direction is inverted.
198    /// Only applies when in native HID mode.
199    pub inverted: bool,
200
201    /// The current scrolling resolution.
202    pub resolution: WheelResolution,
203
204    /// The target of wheel movement reports (native or diverted).
205    pub target: WheelEventTarget,
206}
207
208/// Represents the resolution of the hi-res wheel.
209#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize))]
211#[non_exhaustive]
212#[repr(u8)]
213pub enum WheelResolution {
214    /// Low-resolution wheel reporting.
215    Low = 0,
216    /// High-resolution wheel reporting.
217    High = 1,
218}
219
220/// Represents the target of wheel movement reports.
221#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
222#[cfg_attr(feature = "serde", derive(serde::Serialize))]
223#[non_exhaustive]
224#[repr(u8)]
225pub enum WheelEventTarget {
226    /// Wheel reports go to the native HID path.
227    Native = 0,
228    /// Wheel reports are diverted to HID++ events.
229    Diverted = 1,
230}
231
232/// Represents the state of the wheel ratchet.
233#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
234#[cfg_attr(feature = "serde", derive(serde::Serialize))]
235#[non_exhaustive]
236#[repr(u8)]
237pub enum WheelRatchetState {
238    /// Wheel is in free-spin mode.
239    Freespin = 0,
240    /// Wheel is in ratchet mode.
241    Ratchet = 1,
242}
243
244/// Represents an event emitted by the [`HiResWheelFeature`] feature.
245#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
246#[cfg_attr(feature = "serde", derive(serde::Serialize))]
247#[non_exhaustive]
248pub enum HiResWheelEvent {
249    /// Is emitted whenever the scroll wheel is moved in diverted HID++ mode.
250    WheelMovement(WheelMovementData),
251
252    /// Is emitted whenever the wheel ratchet mode is changed.
253    ///
254    /// This event is always enabled.
255    RatchetSwitch(WheelRatchetState),
256}
257
258/// Represents the data of the [`HiResWheelEvent::WheelMovement`] event.
259#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
260#[cfg_attr(feature = "serde", derive(serde::Serialize))]
261#[non_exhaustive]
262pub struct WheelMovementData {
263    /// The current resolution of the wheel.
264    pub resolution: WheelResolution,
265
266    /// The amount of sampling periods for this event. Maxes at 15.
267    pub periods: U4,
268
269    /// The vertical movement delta. Moving away from the user produces positive
270    /// values.
271    pub delta_vertical: i16,
272}