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}