logiops_core/features/
smartshift.rs1use hidpp_transport::HidapiChannel;
8use tracing::{debug, trace};
9
10use crate::error::{HidppErrorCode, ProtocolError, Result};
11use crate::protocol::{build_long_request, get_error_code, is_error_response};
12
13pub struct SmartShiftFeature {
15 device_index: u8,
16 feature_index: u8,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct SmartShiftConfig {
22 pub active: bool,
24 pub wheel_mode: WheelMode,
26 pub auto_disengage: u8,
29 pub torque: u8,
32}
33
34impl Default for SmartShiftConfig {
35 fn default() -> Self {
36 Self {
37 active: true,
38 wheel_mode: WheelMode::Ratchet,
39 auto_disengage: 30,
40 torque: 50,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum WheelMode {
48 #[default]
50 Ratchet,
51 FreeSpin,
53}
54
55impl WheelMode {
56 #[must_use]
58 pub fn from_byte(value: u8) -> Self {
59 if value == 0 {
60 Self::Ratchet
61 } else {
62 Self::FreeSpin
63 }
64 }
65
66 #[must_use]
68 pub fn to_byte(self) -> u8 {
69 match self {
70 Self::Ratchet => 0,
71 Self::FreeSpin => 1,
72 }
73 }
74}
75
76impl std::fmt::Display for WheelMode {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Self::Ratchet => write!(f, "Ratchet"),
80 Self::FreeSpin => write!(f, "Free-spin"),
81 }
82 }
83}
84
85impl SmartShiftFeature {
86 #[must_use]
92 pub fn new(device_index: u8, feature_index: u8) -> Self {
93 Self {
94 device_index,
95 feature_index,
96 }
97 }
98
99 pub async fn get_status(&self, channel: &HidapiChannel) -> Result<SmartShiftConfig> {
104 let request = build_long_request(self.device_index, self.feature_index, 0x00, &[]);
106
107 trace!("getting SmartShift status");
108 let response = channel.request(&request, 5).await?;
109
110 if is_error_response(&response) {
111 let code = get_error_code(&response).unwrap_or(0);
112 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
113 }
114
115 if response.len() < 8 {
116 return Err(ProtocolError::InvalidResponse(
117 "SmartShift status response too short".to_string(),
118 ));
119 }
120
121 let wheel_mode = WheelMode::from_byte(response[4]);
124 let active = response[5] != 0;
125 let auto_disengage = response[6];
126 let torque = response.get(7).copied().unwrap_or(0);
127
128 let config = SmartShiftConfig {
129 active,
130 wheel_mode,
131 auto_disengage,
132 torque,
133 };
134
135 debug!(
136 active = config.active,
137 mode = %config.wheel_mode,
138 threshold = config.auto_disengage,
139 torque = config.torque,
140 "got SmartShift status"
141 );
142
143 Ok(config)
144 }
145
146 pub async fn set_status(
151 &self,
152 channel: &HidapiChannel,
153 config: &SmartShiftConfig,
154 ) -> Result<()> {
155 let params = [
158 config.wheel_mode.to_byte(),
159 u8::from(config.active),
160 config.auto_disengage,
161 ];
162 let request = build_long_request(self.device_index, self.feature_index, 0x01, ¶ms);
163
164 trace!(
165 active = config.active,
166 mode = ?config.wheel_mode,
167 threshold = config.auto_disengage,
168 "setting SmartShift status"
169 );
170
171 let response = channel.request(&request, 5).await?;
172
173 if is_error_response(&response) {
174 let code = get_error_code(&response).unwrap_or(0);
175 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
176 }
177
178 debug!(
179 active = config.active,
180 mode = %config.wheel_mode,
181 "set SmartShift status"
182 );
183
184 Ok(())
185 }
186
187 pub async fn set_enabled(&self, channel: &HidapiChannel, enabled: bool) -> Result<()> {
192 let mut config = self.get_status(channel).await?;
193 config.active = enabled;
194 self.set_status(channel, &config).await
195 }
196
197 pub async fn set_threshold(&self, channel: &HidapiChannel, threshold: u8) -> Result<()> {
207 let mut config = self.get_status(channel).await?;
208 config.auto_disengage = threshold;
209 self.set_status(channel, &config).await
210 }
211
212 pub async fn set_wheel_mode(&self, channel: &HidapiChannel, mode: WheelMode) -> Result<()> {
217 let mut config = self.get_status(channel).await?;
218 config.wheel_mode = mode;
219 self.set_status(channel, &config).await
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_wheel_mode() {
229 assert_eq!(WheelMode::from_byte(0), WheelMode::Ratchet);
230 assert_eq!(WheelMode::from_byte(1), WheelMode::FreeSpin);
231 assert_eq!(WheelMode::from_byte(2), WheelMode::FreeSpin);
232
233 assert_eq!(WheelMode::Ratchet.to_byte(), 0);
234 assert_eq!(WheelMode::FreeSpin.to_byte(), 1);
235 }
236
237 #[test]
238 fn test_default_config() {
239 let config = SmartShiftConfig::default();
240 assert!(config.active);
241 assert_eq!(config.wheel_mode, WheelMode::Ratchet);
242 assert_eq!(config.auto_disengage, 30);
243 assert_eq!(config.torque, 50);
244 }
245}