Skip to main content

openlogi_core/hid/
smartshift.rs

1//! HID++ `SmartShift Enhanced` (feature `0x2111`) — wheel ratchet ↔
2//! free-spin control with sensitivity threshold.
3//!
4//! The protocol-level `0x2111` wrapper lives in `openlogi-hidpp`; this module
5//! keeps OpenLogi's IPC/config-facing mode and status types.
6//!
7//! Mode encoding (consistent across 0x2110 / 0x2111):
8//! - `wheelMode` `1` = free-spin (no ratchet, infinite scroll), `2` =
9//!   ratchet (clicky).
10//! - `autoDisengage` `0x01`–`0xFE` = the wheel speed (in 0.25 turn/s steps)
11//!   past which a ratchet-mode wheel releases into free-spin — i.e. the
12//!   "SmartShift" threshold. `0xFF` keeps the ratchet engaged permanently
13//!   (never auto-switches). See [`AUTO_DISENGAGE_PERMANENT`].
14
15use num_enum::{IntoPrimitive, TryFromPrimitive};
16use serde::{Deserialize, Serialize};
17
18/// SmartShift mode values understood by the firmware. `Free` = free-spin,
19/// `Ratchet` = clicky / smartshift-off. The discriminant is the wire byte;
20/// reserved values (`0` / `3` / future) fail [`TryFrom`] and callers fall back
21/// to whatever they consider sane.
22///
23/// Also crosses the agent↔GUI IPC — where serde encodes the variant *index*
24/// (Free=0, Ratchet=1), not the `#[repr(u8)]` firmware discriminant — so
25/// variant order is wire format and changes require a `PROTOCOL_VERSION` bump
26/// (guarded by `openlogi-ipc/tests/wire_format.rs`).
27#[derive(
28    Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, Serialize, Deserialize,
29)]
30#[repr(u8)]
31pub enum SmartShiftMode {
32    /// Wheel is in free-spin mode.
33    Free = 1,
34    /// Wheel is in ratchet mode.
35    Ratchet = 2,
36}
37
38impl SmartShiftMode {
39    /// The opposite mode — used when toggling SmartShift between free-spin
40    /// and ratchet in the write path.
41    #[must_use]
42    pub fn flipped(self) -> Self {
43        match self {
44            Self::Free => Self::Ratchet,
45            Self::Ratchet => Self::Free,
46        }
47    }
48}
49
50// The config file persists the wheel mode in its own representation
51// (`crate::config::WheelMode`, kept IPC-free); these conversions are the
52// single mapping between the persisted and the wire/firmware form, used by
53// the GUI when committing and by the agent when re-applying after a reconnect.
54impl From<crate::config::WheelMode> for SmartShiftMode {
55    fn from(mode: crate::config::WheelMode) -> Self {
56        match mode {
57            crate::config::WheelMode::Free => Self::Free,
58            crate::config::WheelMode::Ratchet => Self::Ratchet,
59        }
60    }
61}
62
63impl From<SmartShiftMode> for crate::config::WheelMode {
64    fn from(mode: SmartShiftMode) -> Self {
65        match mode {
66            SmartShiftMode::Free => Self::Free,
67            SmartShiftMode::Ratchet => Self::Ratchet,
68        }
69    }
70}
71
72/// `autoDisengage` value that keeps the ratchet engaged permanently — the
73/// wheel never auto-releases into free-spin, regardless of speed. Any other
74/// value (`0x01`–`0xFE`) is a SmartShift speed threshold.
75pub const AUTO_DISENGAGE_PERMANENT: u8 = 0xff;
76
77/// Snapshot returned from OpenLogi's SmartShift read helpers.
78///
79/// Crosses the agent↔GUI IPC (`read_smartshift`), so field order is wire
80/// format — changes require a `PROTOCOL_VERSION` bump (guarded by
81/// `openlogi-ipc/tests/wire_format.rs`).
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83pub struct SmartShiftStatus {
84    /// Current wheel mode.
85    pub mode: SmartShiftMode,
86    /// SmartShift speed threshold: `0x01`–`0xFE` in 0.25 turn/s steps (higher
87    /// = harder to flip into free-spin while scrolling; Logitech defaults to
88    /// ~16 on the MX line), or [`AUTO_DISENGAGE_PERMANENT`] for a permanently
89    /// engaged ratchet.
90    pub auto_disengage: u8,
91    /// Tunable-torque force as a percentage (`1`–`100`) of the device's max
92    /// force, or `0` when the device doesn't support tunable torque. Read back
93    /// and re-sent unchanged so adjusting the mode or threshold doesn't
94    /// disturb the wheel's resistance.
95    pub tunable_torque: u8,
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn flipped_is_an_involution() {
104        assert_eq!(SmartShiftMode::Free.flipped(), SmartShiftMode::Ratchet);
105        assert_eq!(SmartShiftMode::Ratchet.flipped(), SmartShiftMode::Free);
106        assert_eq!(
107            SmartShiftMode::Free.flipped().flipped(),
108            SmartShiftMode::Free
109        );
110    }
111}