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,
10    event::EventEmitter,
11    feature::{CreatableFeature, EmittingFeature, Feature},
12    nibble::U4,
13    protocol::v20::{self, 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 underlying HID++ channel.
22    chan: Arc<HidppChannel>,
23
24    /// The index of the device to implement the feature for.
25    device_index: u8,
26
27    /// The index of the feature in the feature table.
28    feature_index: u8,
29
30    /// The emitter used to emit events.
31    emitter: Arc<EventEmitter<HiResWheelEvent>>,
32
33    /// The handle assigned to the message listener registered via
34    /// [`HidppChannel::add_msg_listener`].
35    /// This is used to remove the listener when the feature is dropped.
36    msg_listener_hdl: u32,
37}
38
39impl CreatableFeature for HiResWheelFeature {
40    const ID: u16 = 0x2121;
41    const STARTING_VERSION: u8 = 0;
42
43    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
44        let emitter = Arc::new(EventEmitter::new());
45
46        let hdl = chan.add_msg_listener({
47            let emitter = Arc::clone(&emitter);
48
49            move |raw, matched| {
50                if matched {
51                    return;
52                }
53
54                let msg = v20::Message::from(raw);
55
56                let header = msg.header();
57                if header.device_index != device_index
58                    || header.feature_index != feature_index
59                    || header.software_id.to_lo() != 0
60                {
61                    return;
62                }
63
64                let payload = msg.extend_payload();
65
66                let event = match header.function_id.to_lo() {
67                    0 => {
68                        let Ok(resolution) =
69                            WheelResolution::try_from((payload[0] & (1 << 4)) >> 4)
70                        else {
71                            return;
72                        };
73
74                        HiResWheelEvent::WheelMovement(WheelMovementData {
75                            resolution,
76                            periods: U4::from_lo(payload[0]),
77                            delta_vertical: i16::from_be_bytes(payload[1..=2].try_into().unwrap()),
78                        })
79                    }
80                    1 => {
81                        let Ok(state) = WheelRatchetState::try_from(payload[0] & 1) else {
82                            return;
83                        };
84
85                        HiResWheelEvent::RatchetSwitch(state)
86                    }
87                    _ => return,
88                };
89
90                emitter.emit(event);
91            }
92        });
93
94        Self {
95            chan,
96            device_index,
97            feature_index,
98            emitter,
99            msg_listener_hdl: hdl,
100        }
101    }
102}
103
104impl Feature for HiResWheelFeature {}
105
106impl EmittingFeature<HiResWheelEvent> for HiResWheelFeature {
107    fn listen(&self) -> async_channel::Receiver<HiResWheelEvent> {
108        self.emitter.create_receiver()
109    }
110}
111
112impl Drop for HiResWheelFeature {
113    fn drop(&mut self) {
114        self.chan.remove_msg_listener(self.msg_listener_hdl);
115    }
116}
117
118impl HiResWheelFeature {
119    /// Retrieves the capabilities of the hi-res wheel and this feature.
120    pub async fn get_wheel_capabilities(&self) -> Result<WheelCapabilities, Hidpp20Error> {
121        let response = self
122            .chan
123            .send_v20(v20::Message::Short(
124                v20::MessageHeader {
125                    device_index: self.device_index,
126                    feature_index: self.feature_index,
127                    function_id: U4::from_lo(0),
128                    software_id: self.chan.get_sw_id(),
129                },
130                [0x00, 0x00, 0x00],
131            ))
132            .await?;
133
134        let payload = response.extend_payload();
135
136        Ok(WheelCapabilities {
137            multiplier: payload[0],
138            has_invert: payload[1] & (1 << 3) != 0,
139            has_switch: payload[1] & (1 << 2) != 0,
140            ratches_per_rotation: payload[2],
141            wheel_diameter: payload[3],
142        })
143    }
144
145    /// Retrieves the current mode of the hi-res wheel.
146    pub async fn get_wheel_mode(&self) -> Result<WheelMode, Hidpp20Error> {
147        let response = self
148            .chan
149            .send_v20(v20::Message::Short(
150                v20::MessageHeader {
151                    device_index: self.device_index,
152                    feature_index: self.feature_index,
153                    function_id: U4::from_lo(1),
154                    software_id: self.chan.get_sw_id(),
155                },
156                [0x00, 0x00, 0x00],
157            ))
158            .await?;
159
160        let payload = response.extend_payload();
161
162        Ok(WheelMode {
163            inverted: payload[0] & (1 << 2) != 0,
164            resolution: WheelResolution::try_from((payload[0] & (1 << 1)) >> 1)
165                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
166            target: WheelEventTarget::try_from(payload[0] & 1)
167                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
168        })
169    }
170
171    /// Sets the mode of the hi-res wheel.
172    ///
173    /// Setting the bit to control analytics collection is not supported in this
174    /// feature implementation as the analytics data structure is completely
175    /// undocumented.\
176    /// If this is implemented in the future, a new implementation will do so to
177    /// not break this one.
178    pub async fn set_wheel_mode(
179        &self,
180        target: WheelEventTarget,
181        resolution: WheelResolution,
182        inverted: bool,
183    ) -> Result<WheelMode, Hidpp20Error> {
184        let mut mode_byte = 0u8;
185        if inverted {
186            mode_byte |= 1 << 2;
187        }
188        mode_byte |= u8::from(resolution) << 1;
189        mode_byte |= u8::from(target);
190
191        let response = self
192            .chan
193            .send_v20(v20::Message::Short(
194                v20::MessageHeader {
195                    device_index: self.device_index,
196                    feature_index: self.feature_index,
197                    function_id: U4::from_lo(2),
198                    software_id: self.chan.get_sw_id(),
199                },
200                [mode_byte, 0x00, 0x00],
201            ))
202            .await?;
203
204        let payload = response.extend_payload();
205
206        Ok(WheelMode {
207            inverted: payload[0] & (1 << 2) != 0,
208            resolution: WheelResolution::try_from((payload[0] & (1 << 1)) >> 1)
209                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
210            target: WheelEventTarget::try_from(payload[0] & 1)
211                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
212        })
213    }
214
215    /// Retrieves the current state of the ratchet switch.
216    pub async fn get_ratchet_switch_state(&self) -> Result<WheelRatchetState, Hidpp20Error> {
217        let response = self
218            .chan
219            .send_v20(v20::Message::Short(
220                v20::MessageHeader {
221                    device_index: self.device_index,
222                    feature_index: self.feature_index,
223                    function_id: U4::from_lo(3),
224                    software_id: self.chan.get_sw_id(),
225                },
226                [0x00, 0x00, 0x00],
227            ))
228            .await?;
229
230        let payload = response.extend_payload();
231
232        WheelRatchetState::try_from(payload[0] & 1).map_err(|_| Hidpp20Error::UnsupportedResponse)
233    }
234}
235
236/// Represents the capabilities of the hi-res wheel and this feature as reported
237/// by [`HiResWheelFeature::get_wheel_capabilities`].
238#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
239#[cfg_attr(feature = "serde", derive(serde::Serialize))]
240#[non_exhaustive]
241pub struct WheelCapabilities {
242    /// The report multiplier for the high-resolution mode. A single ratchet
243    /// distance will produce this amount of wheel movement reports in hi-res
244    /// mode.
245    pub multiplier: u8,
246
247    /// Whether the device supports inverting the scrolling direction when in
248    /// native HID reporting mode.
249    ///
250    /// Inverting is never supported in diverted HID++ mode.
251    pub has_invert: bool,
252
253    /// Whether the device has a switch to control the ratchet mode.
254    pub has_switch: bool,
255
256    /// The amount of ratches that would be generated by a whole rotation of the
257    /// scroll wheel.
258    pub ratches_per_rotation: u8,
259
260    /// The nominal wheel diameter in millimeters.
261    pub wheel_diameter: u8,
262}
263
264/// Represents the wheel mode as reported by
265/// [`HiResWheelFeature::get_wheel_mode`].
266#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
267#[cfg_attr(feature = "serde", derive(serde::Serialize))]
268#[non_exhaustive]
269pub struct WheelMode {
270    /// Whether the scrolling direction is inverted.
271    /// Only applies when in native HID mode.
272    pub inverted: bool,
273
274    /// The current scrolling resolution.
275    pub resolution: WheelResolution,
276
277    /// The target of wheel movement reports (native or diverted).
278    pub target: WheelEventTarget,
279}
280
281/// Represents the resolution of the hi-res wheel.
282#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
283#[cfg_attr(feature = "serde", derive(serde::Serialize))]
284#[non_exhaustive]
285#[repr(u8)]
286pub enum WheelResolution {
287    Low = 0,
288    High = 1,
289}
290
291/// Represents the target of wheel movement reports.
292#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize))]
294#[non_exhaustive]
295#[repr(u8)]
296pub enum WheelEventTarget {
297    Native = 0,
298    Diverted = 1,
299}
300
301/// Represents the state of the wheel ratchet.
302#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
303#[cfg_attr(feature = "serde", derive(serde::Serialize))]
304#[non_exhaustive]
305#[repr(u8)]
306pub enum WheelRatchetState {
307    Freespin = 0,
308    Ratchet = 1,
309}
310
311/// Represents an event emitted by the [`HiResWheelFeature`] feature.
312#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
313#[cfg_attr(feature = "serde", derive(serde::Serialize))]
314#[non_exhaustive]
315pub enum HiResWheelEvent {
316    /// Is emitted whenever the scroll wheel is moved in diverted HID++ mode.
317    WheelMovement(WheelMovementData),
318
319    /// Is emitted whenever the wheel ratchet mode is changed.
320    ///
321    /// This event is always enabled.
322    RatchetSwitch(WheelRatchetState),
323}
324
325/// Represents the data of the [`HiResWheelEvent::WheelMovement`] event.
326#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
327#[cfg_attr(feature = "serde", derive(serde::Serialize))]
328#[non_exhaustive]
329pub struct WheelMovementData {
330    /// The current resolution of the wheel.
331    pub resolution: WheelResolution,
332
333    /// The amount of sampling periods for this event. Maxes at 15.
334    pub periods: U4,
335
336    /// The vertical movement delta. Moving away from the user produces positive
337    /// values.
338    pub delta_vertical: i16,
339}