Skip to main content

hidpp/feature/
hires_wheel.rs

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