Skip to main content

hidpp/feature/
crown.rs

1//! Implements the `Crown` feature (ID `0x4600`) for the MX Master's rotary
2//! crown: reading its capabilities, configuring its mode (HID vs diverted,
3//! free vs ratchet, timeouts), and receiving diverted rotation/touch/button
4//! events.
5
6pub mod event;
7
8#[cfg(test)]
9mod tests;
10
11use std::num::NonZeroU8;
12
13use num_enum::{IntoPrimitive, TryFromPrimitive};
14use openlogi_hidpp_derive::Feature;
15
16pub use event::{ActivityState, ButtonState, CrownEvent, CrownGesture, CrownUpdate, RotationState};
17
18use crate::{
19    feature::{EventSource, FeatureEndpoint},
20    protocol::v20::Hidpp20Error,
21};
22
23bitflags::bitflags! {
24    /// Crown control capabilities, from [`get_info`](CrownFeature::get_info).
25    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
27    pub struct CrownControlCapabilities: u8 {
28        /// The crown has a button.
29        const BUTTON = 1 << 0;
30        /// The button reports long presses.
31        const BUTTON_LONG_PRESS = 1 << 1;
32        /// The ratchet is mechanized (no manual control).
33        const MECHANIZED_RATCHET = 1 << 2;
34        /// The rotation timeout is configurable.
35        const ROTATION_TIMEOUT_CONFIGURABLE = 1 << 3;
36        /// The short-long timeout is configurable.
37        const SHORT_LONG_TIMEOUT_CONFIGURABLE = 1 << 4;
38        /// The double-tap speed is configurable.
39        const DOUBLE_TAP_SPEED_CONFIGURABLE = 1 << 5;
40    }
41}
42
43bitflags::bitflags! {
44    /// Crown sensor capabilities, from [`get_info`](CrownFeature::get_info).
45    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
47    pub struct CrownSensorCapabilities: u8 {
48        /// The crown has a proximity sensor.
49        const PROXIMITY = 1 << 0;
50        /// The crown has a touch sensor.
51        const TOUCH = 1 << 1;
52        /// The crown detects tap gestures.
53        const TAP_GESTURE = 1 << 2;
54        /// The crown detects double-tap gestures.
55        const DOUBLE_TAP_GESTURE = 1 << 3;
56    }
57}
58
59/// How crown events are reported.
60///
61/// The wire's `0` is the write-side "leave unchanged" sentinel, kept out of
62/// this enum: [`SetCrownMode`] says "unchanged" with `None`, and a response
63/// answering `0` for the current mode is [`Hidpp20Error::UnsupportedResponse`]
64/// like any other value no mode can be.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize))]
67#[non_exhaustive]
68#[repr(u8)]
69pub enum ReportingMode {
70    /// Events go to the native HID channel.
71    Hid = 1,
72    /// Events are diverted to HID++ (required for [`CrownEvent`]).
73    Diverted = 2,
74}
75
76/// The crown's ratchet mode. The wire's `0` sentinel is kept out of the enum
77/// for the same reason as [`ReportingMode`]'s.
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize))]
80#[non_exhaustive]
81#[repr(u8)]
82pub enum RatchetMode {
83    /// Free-spinning mode.
84    Free = 1,
85    /// Ratchet (detented) mode.
86    Ratchet = 2,
87}
88
89/// Crown info constants from [`get_info`](CrownFeature::get_info).
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize))]
92#[non_exhaustive]
93pub struct CrownInfo {
94    /// Control capabilities.
95    pub controls: CrownControlCapabilities,
96    /// Sensor capabilities.
97    pub sensors: CrownSensorCapabilities,
98    /// Number of slots per revolution.
99    pub slots: u16,
100    /// Number of ratchets per revolution.
101    pub ratchets: u16,
102}
103
104/// The crown's current mode, from [`get_mode`](CrownFeature::get_mode).
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize))]
107#[non_exhaustive]
108pub struct CrownMode {
109    /// How events are reported.
110    pub diverting: ReportingMode,
111    /// Ratchet mode.
112    pub ratchet_mode: RatchetMode,
113    /// Rotation timeout, in 10 ms steps.
114    pub rotation_timeout: u8,
115    /// Short-long press timeout, in 10 ms steps.
116    pub short_long_timeout: u8,
117    /// Double-tap speed, in 10 ms steps.
118    pub double_tap_speed: u8,
119}
120
121impl CrownMode {
122    fn from_payload(payload: &[u8; 16]) -> Result<Self, Hidpp20Error> {
123        Ok(Self {
124            diverting: ReportingMode::try_from(payload[0])
125                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
126            ratchet_mode: RatchetMode::try_from(payload[1])
127                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
128            rotation_timeout: payload[2],
129            short_long_timeout: payload[3],
130            double_tap_speed: payload[4],
131        })
132    }
133}
134
135/// Mode settings to write with [`set_mode`](CrownFeature::set_mode).
136///
137/// `None` leaves a setting unchanged — encoded as the wire's `0` sentinel, so
138/// "set it to 0" is unrepresentable rather than a silent no-op. The rotation
139/// timeout is clipped to `0x40` by the device.
140#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
141#[cfg_attr(feature = "serde", derive(serde::Serialize))]
142pub struct SetCrownMode {
143    /// How events are reported, or `None` to leave unchanged.
144    pub diverting: Option<ReportingMode>,
145    /// Ratchet mode, or `None` to leave unchanged.
146    pub ratchet_mode: Option<RatchetMode>,
147    /// Rotation timeout in 10 ms steps, or `None` to leave unchanged.
148    pub rotation_timeout: Option<NonZeroU8>,
149    /// Short-long timeout in 10 ms steps, or `None` to leave unchanged.
150    pub short_long_timeout: Option<NonZeroU8>,
151    /// Double-tap speed in 10 ms steps, or `None` to leave unchanged.
152    pub double_tap_speed: Option<NonZeroU8>,
153}
154
155/// Implements the `Crown` / `0x4600` feature.
156#[derive(Feature)]
157#[creatable(id = 0x4600, version = 0)]
158pub struct CrownFeature {
159    /// The endpoint this feature talks to.
160    endpoint: FeatureEndpoint,
161
162    /// Publishes decoded events to listeners.
163    events: EventSource<CrownEvent>,
164}
165
166impl CrownFeature {
167    /// Retrieves the crown's capabilities and slot/ratchet counts.
168    pub async fn get_info(&self) -> Result<CrownInfo, Hidpp20Error> {
169        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
170        Ok(CrownInfo {
171            controls: CrownControlCapabilities::from_bits_retain(payload[0]),
172            sensors: CrownSensorCapabilities::from_bits_retain(payload[1]),
173            slots: u16::from_be_bytes([payload[2], payload[3]]),
174            ratchets: u16::from_be_bytes([payload[4], payload[5]]),
175        })
176    }
177
178    /// Retrieves the crown's current mode.
179    pub async fn get_mode(&self) -> Result<CrownMode, Hidpp20Error> {
180        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
181        CrownMode::from_payload(&payload)
182    }
183
184    /// Sets the crown's mode.
185    ///
186    /// Divert the crown ([`ReportingMode::Diverted`]) for [`CrownEvent`]s to be
187    /// emitted.
188    ///
189    /// The spec's `SetMode` response merely echoes the request bytes — `None`
190    /// sentinels included — so it carries no statement about the device's
191    /// resulting mode and is deliberately not decoded (a `None` field's echo
192    /// would even trip the strict [`CrownMode`] decoder). Read the mode back
193    /// with [`get_mode`](Self::get_mode) when it is needed.
194    pub async fn set_mode(&self, mode: SetCrownMode) -> Result<(), Hidpp20Error> {
195        let mut args = [0; 16];
196        args[..5].copy_from_slice(&[
197            mode.diverting.map_or(0, u8::from),
198            mode.ratchet_mode.map_or(0, u8::from),
199            mode.rotation_timeout.map_or(0, NonZeroU8::get),
200            mode.short_long_timeout.map_or(0, NonZeroU8::get),
201            mode.double_tap_speed.map_or(0, NonZeroU8::get),
202        ]);
203        self.endpoint.call_long(2, args).await?;
204        Ok(())
205    }
206}