openlogi_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-agent-core/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 by [`crate::write::toggle_smartshift`].
40 #[must_use]
41 pub fn flipped(self) -> Self {
42 match self {
43 Self::Free => Self::Ratchet,
44 Self::Ratchet => Self::Free,
45 }
46 }
47}
48
49// The config file persists the wheel mode in its own representation
50// (`openlogi_core::config::WheelMode`, kept IPC-free); these conversions are
51// the single mapping between the persisted and the wire/firmware form, used by
52// the GUI when committing and by the agent when re-applying after a reconnect.
53impl From<openlogi_core::config::WheelMode> for SmartShiftMode {
54 fn from(mode: openlogi_core::config::WheelMode) -> Self {
55 match mode {
56 openlogi_core::config::WheelMode::Free => Self::Free,
57 openlogi_core::config::WheelMode::Ratchet => Self::Ratchet,
58 }
59 }
60}
61
62impl From<SmartShiftMode> for openlogi_core::config::WheelMode {
63 fn from(mode: SmartShiftMode) -> Self {
64 match mode {
65 SmartShiftMode::Free => Self::Free,
66 SmartShiftMode::Ratchet => Self::Ratchet,
67 }
68 }
69}
70
71/// `autoDisengage` value that keeps the ratchet engaged permanently — the
72/// wheel never auto-releases into free-spin, regardless of speed. Any other
73/// value (`0x01`–`0xFE`) is a SmartShift speed threshold.
74pub const AUTO_DISENGAGE_PERMANENT: u8 = 0xff;
75
76/// Snapshot returned from OpenLogi's SmartShift read helpers.
77///
78/// Crosses the agent↔GUI IPC (`read_smartshift`), so field order is wire
79/// format — changes require a `PROTOCOL_VERSION` bump (guarded by
80/// `openlogi-agent-core/tests/wire_format.rs`).
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub struct SmartShiftStatus {
83 /// Current wheel mode.
84 pub mode: SmartShiftMode,
85 /// SmartShift speed threshold: `0x01`–`0xFE` in 0.25 turn/s steps (higher
86 /// = harder to flip into free-spin while scrolling; Logitech defaults to
87 /// ~16 on the MX line), or [`AUTO_DISENGAGE_PERMANENT`] for a permanently
88 /// engaged ratchet.
89 pub auto_disengage: u8,
90 /// Tunable-torque force as a percentage (`1`–`100`) of the device's max
91 /// force, or `0` when the device doesn't support tunable torque. Read back
92 /// and re-sent unchanged so adjusting the mode or threshold doesn't
93 /// disturb the wheel's resistance.
94 pub tunable_torque: u8,
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn flipped_is_an_involution() {
103 assert_eq!(SmartShiftMode::Free.flipped(), SmartShiftMode::Ratchet);
104 assert_eq!(SmartShiftMode::Ratchet.flipped(), SmartShiftMode::Free);
105 assert_eq!(
106 SmartShiftMode::Free.flipped().flipped(),
107 SmartShiftMode::Free
108 );
109 }
110}