Skip to main content

hidpp/feature/
smartshift_enhanced.rs

1//! Implements `SmartShiftWheelEnhanced` (feature `0x2111`).
2
3use std::num::NonZeroU8;
4
5use openlogi_hidpp_derive::Feature;
6
7use crate::{
8    feature::{FeatureEndpoint, smartshift::WheelMode},
9    protocol::v20::Hidpp20Error,
10};
11
12bitflags::bitflags! {
13    /// Capabilities reported by `SmartShiftWheelEnhanced`.
14    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
16    pub struct SmartShiftEnhancedCapabilities: u8 {
17        /// The device supports tunable ratchet torque.
18        const TUNABLE_TORQUE = 1 << 0;
19    }
20}
21
22/// Capability and default values for enhanced SmartShift.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25#[non_exhaustive]
26pub struct SmartShiftEnhancedInfo {
27    /// Supported capabilities.
28    pub capabilities: SmartShiftEnhancedCapabilities,
29    /// Default automatic disengage threshold.
30    pub auto_disengage_default: u8,
31    /// Default tunable torque, as a percentage of maximum force.
32    pub default_tunable_torque: u8,
33    /// Maximum force in gram-force units.
34    pub max_force: u8,
35}
36
37/// Current enhanced SmartShift status.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40#[non_exhaustive]
41pub struct SmartShiftEnhancedStatus {
42    /// Current requested wheel mode.
43    pub wheel_mode: WheelMode,
44    /// Automatic disengage threshold.
45    pub auto_disengage: u8,
46    /// Current tunable torque, as a percentage of maximum force.
47    pub current_tunable_torque: u8,
48}
49
50/// Enhanced SmartShift status update.
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize))]
53pub struct SmartShiftEnhancedStatusChange {
54    /// Wheel mode to apply, or `None` to leave unchanged.
55    pub wheel_mode: Option<WheelMode>,
56    /// Automatic disengage threshold, or `None` to leave unchanged.
57    ///
58    /// HID++ encodes `0` as “do not change”, so writable values must be non-zero.
59    pub auto_disengage: Option<NonZeroU8>,
60    /// Tunable torque, or `None` to leave unchanged.
61    ///
62    /// HID++ encodes `0` as “do not change”, so writable values must be non-zero.
63    pub tunable_torque: Option<NonZeroU8>,
64}
65
66/// Implements the `SmartShiftWheelEnhanced` / `0x2111` feature.
67#[derive(Clone, Feature)]
68#[creatable(id = 0x2111, version = 0)]
69pub struct SmartShiftEnhancedFeature {
70    /// The endpoint this feature talks to.
71    endpoint: FeatureEndpoint,
72}
73
74impl SmartShiftEnhancedFeature {
75    /// Retrieves enhanced SmartShift capabilities and defaults.
76    pub async fn get_capabilities(&self) -> Result<SmartShiftEnhancedInfo, Hidpp20Error> {
77        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
78        Ok(SmartShiftEnhancedInfo {
79            capabilities: SmartShiftEnhancedCapabilities::from_bits_retain(payload[0]),
80            auto_disengage_default: payload[1],
81            default_tunable_torque: payload[2],
82            max_force: payload[3],
83        })
84    }
85
86    /// Retrieves the current enhanced SmartShift ratchet control mode.
87    pub async fn get_ratchet_control_mode(&self) -> Result<SmartShiftEnhancedStatus, Hidpp20Error> {
88        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
89        SmartShiftEnhancedStatus::from_payload(payload)
90    }
91
92    /// Sets selected enhanced SmartShift fields and returns the resulting status.
93    ///
94    /// A `None` field is encoded as `0`, the documented “do not change” value.
95    pub async fn set_ratchet_control_mode(
96        &self,
97        change: SmartShiftEnhancedStatusChange,
98    ) -> Result<SmartShiftEnhancedStatus, Hidpp20Error> {
99        let payload = self
100            .endpoint
101            .call(
102                2,
103                [
104                    change.wheel_mode.map_or(0, u8::from),
105                    change.auto_disengage.map_or(0, NonZeroU8::get),
106                    change.tunable_torque.map_or(0, NonZeroU8::get),
107                ],
108            )
109            .await?
110            .extend_payload();
111        SmartShiftEnhancedStatus::from_payload(payload)
112    }
113}
114
115impl SmartShiftEnhancedStatus {
116    fn from_payload(payload: [u8; 16]) -> Result<Self, Hidpp20Error> {
117        Ok(Self {
118            wheel_mode: WheelMode::try_from(payload[0])
119                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
120            auto_disengage: payload[1],
121            current_tunable_torque: payload[2],
122        })
123    }
124}
125
126#[cfg(test)]
127#[allow(clippy::unwrap_used, reason = "expect/unwrap are idiomatic in tests")]
128mod tests {
129    use super::{Hidpp20Error, SmartShiftEnhancedStatus, WheelMode};
130
131    #[test]
132    fn parses_status() {
133        let mut payload = [0; 16];
134        payload[0] = 2;
135        payload[1] = 0xff;
136        payload[2] = 33;
137
138        let status = SmartShiftEnhancedStatus::from_payload(payload).unwrap();
139
140        assert_eq!(status.wheel_mode, WheelMode::Ratchet);
141        assert_eq!(status.auto_disengage, 0xff);
142        assert_eq!(status.current_tunable_torque, 33);
143    }
144
145    #[test]
146    fn unknown_wheel_mode_is_an_unsupported_response() {
147        let mut payload = [0; 16];
148        payload[0] = 9;
149
150        let err = SmartShiftEnhancedStatus::from_payload(payload).unwrap_err();
151
152        assert!(matches!(err, Hidpp20Error::UnsupportedResponse));
153    }
154}