Skip to main content

hidpp/feature/smartshift_enhanced/
mod.rs

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