1use std::num::NonZeroU8;
2use std::sync::Arc;
3use std::time::Duration;
4
5use hidpp::{
6 channel::HidppChannel,
7 device::Device,
8 feature::{
9 CreatableFeature,
10 smartshift::{SmartShiftFeature, WheelMode},
11 smartshift_enhanced::{SmartShiftEnhancedFeature, SmartShiftEnhancedStatusChange},
12 },
13};
14use tracing::debug;
15
16use crate::SharedChannel;
17use crate::backend::HidBackend;
18use crate::channel::route::DeviceRoute;
19use openlogi_core::hid::smartshift::{
20 SmartShiftAutoDisengage, SmartShiftMode, SmartShiftStatus, TunableTorque,
21};
22
23use super::{
24 HidppFeatureErrorKind, HidppOperation, WriteError, classify_hidpp_error, open_feature,
25 with_route,
26};
27
28const TRANSIENT_RETRY_DELAY: Duration = Duration::from_millis(50);
31
32pub(super) fn is_missing_enhanced(err: &WriteError) -> bool {
37 matches!(
38 err,
39 WriteError::FeatureUnsupported { feature_hex } if *feature_hex == 0x2111
40 )
41}
42
43pub(super) fn is_transient_smartshift_error(err: &WriteError) -> bool {
48 matches!(
49 err,
50 WriteError::HidppFeature {
51 kind: HidppFeatureErrorKind::InvalidArgument
52 | HidppFeatureErrorKind::Busy
53 | HidppFeatureErrorKind::HwError,
54 ..
55 } | WriteError::UnsupportedResponse { .. }
56 )
57}
58
59pub(super) fn status_matches_desired(current: SmartShiftStatus, desired: SmartShiftStatus) -> bool {
63 current.mode == desired.mode
64 && current.auto_disengage == desired.auto_disengage
65 && desired
66 .tunable_torque
67 .is_none_or(|torque| current.tunable_torque == Some(torque))
68}
69
70fn decode_auto_disengage(
71 value: u8,
72 feature_hex: u16,
73) -> Result<SmartShiftAutoDisengage, WriteError> {
74 SmartShiftAutoDisengage::try_from(value).map_err(|_| WriteError::UnsupportedResponse {
75 operation: HidppOperation::ReadSmartShift,
76 feature_hex,
77 })
78}
79
80pub(super) fn wheel_mode_to_smartshift(wheel: WheelMode) -> SmartShiftMode {
85 if matches!(wheel, WheelMode::Freespin) {
86 SmartShiftMode::Free
87 } else {
88 SmartShiftMode::Ratchet
89 }
90}
91
92pub(super) fn smartshift_to_wheel(mode: SmartShiftMode) -> WheelMode {
96 match mode {
97 SmartShiftMode::Free => WheelMode::Freespin,
98 SmartShiftMode::Ratchet => WheelMode::Ratchet,
99 }
100}
101
102enum SmartShift {
106 Enhanced(Arc<SmartShiftEnhancedFeature>),
108 Legacy(Arc<SmartShiftFeature>),
110}
111
112impl SmartShift {
113 async fn open(device: &mut Device) -> Result<Self, WriteError> {
120 match open_feature::<SmartShiftEnhancedFeature>(device).await {
121 Ok(feature) => Ok(Self::Enhanced(feature)),
122 Err(err) if is_missing_enhanced(&err) => {
123 match open_feature::<SmartShiftEnhancedFeature>(device).await {
124 Ok(feature) => Ok(Self::Enhanced(feature)),
125 Err(err) if is_missing_enhanced(&err) => {
126 let feature = open_feature::<SmartShiftFeature>(device).await?;
127 Ok(Self::Legacy(feature))
128 }
129 Err(err) => Err(err),
130 }
131 }
132 Err(err) => Err(err),
133 }
134 }
135
136 async fn status(&self) -> Result<SmartShiftStatus, WriteError> {
139 match self {
140 Self::Enhanced(feature) => {
141 let status = feature.get_ratchet_control_mode().await.map_err(|e| {
142 classify_hidpp_error(
143 e,
144 HidppOperation::ReadSmartShift,
145 SmartShiftEnhancedFeature::ID,
146 )
147 })?;
148 Ok(SmartShiftStatus {
149 mode: wheel_mode_to_smartshift(status.wheel_mode),
150 auto_disengage: decode_auto_disengage(
151 status.auto_disengage,
152 SmartShiftEnhancedFeature::ID,
153 )?,
154 tunable_torque: TunableTorque::try_from(status.current_tunable_torque).ok(),
155 })
156 }
157 Self::Legacy(feature) => {
158 let rcm = feature.get_ratchet_control_mode().await.map_err(|e| {
159 classify_hidpp_error(e, HidppOperation::ReadSmartShift, SmartShiftFeature::ID)
160 })?;
161 Ok(SmartShiftStatus {
162 mode: wheel_mode_to_smartshift(rcm.wheel_mode),
163 auto_disengage: decode_auto_disengage(
164 rcm.auto_disengage,
165 SmartShiftFeature::ID,
166 )?,
167 tunable_torque: None,
168 })
169 }
170 }
171 }
172
173 async fn set_status(&self, status: SmartShiftStatus) -> Result<(), WriteError> {
179 let SmartShiftStatus {
180 mode,
181 auto_disengage,
182 tunable_torque,
183 } = status;
184 let auto_disengage = NonZeroU8::from(auto_disengage);
185 match self {
186 Self::Enhanced(feature) => feature
187 .set_ratchet_control_mode(SmartShiftEnhancedStatusChange {
188 wheel_mode: Some(smartshift_to_wheel(mode)),
189 auto_disengage: Some(auto_disengage),
190 tunable_torque: tunable_torque.map(NonZeroU8::from),
191 })
192 .await
193 .map(|_| ())
194 .map_err(|e| {
195 classify_hidpp_error(
196 e,
197 HidppOperation::WriteSmartShift,
198 SmartShiftEnhancedFeature::ID,
199 )
200 }),
201 Self::Legacy(feature) => feature
202 .set_ratchet_control_mode(
203 Some(smartshift_to_wheel(mode)),
204 Some(auto_disengage.get()),
205 None,
206 )
207 .await
208 .map_err(|e| {
209 classify_hidpp_error(e, HidppOperation::WriteSmartShift, SmartShiftFeature::ID)
210 }),
211 }
212 }
213
214 async fn set_sensitivity(&self, value: SmartShiftAutoDisengage) -> Result<(), WriteError> {
218 let current = self.status().await?;
219 let wire_value = NonZeroU8::from(value);
220 match self {
221 Self::Enhanced(feature) => feature
222 .set_ratchet_control_mode(SmartShiftEnhancedStatusChange {
223 wheel_mode: Some(smartshift_to_wheel(current.mode)),
224 auto_disengage: Some(wire_value),
225 tunable_torque: current.tunable_torque.map(NonZeroU8::from),
226 })
227 .await
228 .map(|_| ())
229 .map_err(|e| {
230 classify_hidpp_error(
231 e,
232 HidppOperation::WriteSmartShift,
233 SmartShiftEnhancedFeature::ID,
234 )
235 }),
236 Self::Legacy(_) => {
237 self.set_status(SmartShiftStatus {
238 auto_disengage: value,
239 ..current
240 })
241 .await
242 }
243 }
244 }
245}
246
247pub async fn get_smartshift_status(
250 backend: &dyn HidBackend,
251 route: &DeviceRoute,
252) -> Result<SmartShiftStatus, WriteError> {
253 let index = route.device_index();
254 with_route(backend, route, move |channel| async move {
255 get_smartshift_status_on_channel(&channel, index).await
256 })
257 .await
258}
259
260pub(super) async fn get_smartshift_status_on_channel(
261 channel: &Arc<HidppChannel>,
262 index: u8,
263) -> Result<SmartShiftStatus, WriteError> {
264 let mut device = Device::new(Arc::clone(channel), index)
265 .await
266 .map_err(|_| WriteError::DeviceUnreachable { index })?;
267 let smartshift = SmartShift::open(&mut device).await?;
268 smartshift.status().await
269}
270
271pub async fn set_smartshift_sensitivity(
278 backend: &dyn HidBackend,
279 route: &DeviceRoute,
280 value: SmartShiftAutoDisengage,
281) -> Result<SmartShiftStatus, WriteError> {
282 let index = route.device_index();
283 with_route(backend, route, move |channel| async move {
284 let mut device = Device::new(Arc::clone(&channel), index)
285 .await
286 .map_err(|_| WriteError::DeviceUnreachable { index })?;
287 let smartshift = SmartShift::open(&mut device).await?;
288 smartshift.set_sensitivity(value).await?;
289 smartshift.status().await
290 })
291 .await
292}
293
294pub async fn toggle_smartshift(
302 backend: &dyn HidBackend,
303 route: &DeviceRoute,
304) -> Result<SmartShiftMode, WriteError> {
305 let index = route.device_index();
306 with_route(backend, route, move |channel| async move {
307 toggle_smartshift_on_channel(&channel, index).await
308 })
309 .await
310}
311
312pub(super) async fn toggle_smartshift_on_channel(
318 channel: &Arc<HidppChannel>,
319 index: u8,
320) -> Result<SmartShiftMode, WriteError> {
321 let mut device = Device::new(Arc::clone(channel), index)
322 .await
323 .map_err(|_| WriteError::DeviceUnreachable { index })?;
324 match toggle_once(&mut device, index).await {
325 Ok(mode) => Ok(mode),
326 Err(err) if is_transient_smartshift_error(&err) => {
327 debug!(
328 index,
329 error = ?err,
330 "SmartShift toggle hit a transient error; retrying once"
331 );
332 tokio::time::sleep(TRANSIENT_RETRY_DELAY).await;
333 toggle_once(&mut device, index).await
334 }
335 Err(err) => Err(err),
336 }
337}
338
339async fn toggle_once(device: &mut Device, index: u8) -> Result<SmartShiftMode, WriteError> {
340 let smartshift = SmartShift::open(device).await?;
341 let status = smartshift.status().await?;
342 let next = status.mode.flipped();
343 smartshift
344 .set_status(SmartShiftStatus {
345 mode: next,
346 ..status
347 })
348 .await?;
349 debug!(index, ?next, "wrote SmartShift mode");
350 Ok(next)
351}
352
353pub async fn set_smartshift(
360 backend: &dyn HidBackend,
361 route: &DeviceRoute,
362 status: SmartShiftStatus,
363) -> Result<(), WriteError> {
364 let index = route.device_index();
365 with_route(backend, route, move |channel| async move {
366 set_smartshift_on_channel(&channel, index, status).await
367 })
368 .await
369}
370
371pub(super) async fn set_smartshift_on_channel(
379 channel: &Arc<HidppChannel>,
380 index: u8,
381 desired: SmartShiftStatus,
382) -> Result<(), WriteError> {
383 let mut device = Device::new(Arc::clone(channel), index)
384 .await
385 .map_err(|_| WriteError::DeviceUnreachable { index })?;
386 let smartshift = SmartShift::open(&mut device).await?;
387 if let Ok(current) = smartshift.status().await
388 && status_matches_desired(current, desired)
389 {
390 debug!(
391 index,
392 status = ?desired,
393 "SmartShift already matches config; skipping write"
394 );
395 return Ok(());
396 }
397 match smartshift.set_status(desired).await {
398 Ok(()) => {
399 debug!(
400 index,
401 status = ?desired,
402 "wrote SmartShift config"
403 );
404 Ok(())
405 }
406 Err(err) if is_transient_smartshift_error(&err) => {
407 debug!(
408 index,
409 error = ?err,
410 "SmartShift write hit a transient error; retrying once"
411 );
412 tokio::time::sleep(TRANSIENT_RETRY_DELAY).await;
413 let smartshift = SmartShift::open(&mut device).await?;
416 smartshift.set_status(desired).await?;
417 debug!(
418 index,
419 status = ?desired,
420 "wrote SmartShift config"
421 );
422 Ok(())
423 }
424 Err(err) => Err(err),
425 }
426}
427
428pub async fn toggle_smartshift_on(shared: &SharedChannel) -> Result<SmartShiftMode, WriteError> {
430 toggle_smartshift_on_channel(shared.channel(), shared.device_index()).await
431}
432
433pub async fn get_smartshift_status_on(
435 shared: &SharedChannel,
436) -> Result<SmartShiftStatus, WriteError> {
437 get_smartshift_status_on_channel(shared.channel(), shared.device_index()).await
438}
439
440pub async fn set_smartshift_on(
443 shared: &SharedChannel,
444 status: SmartShiftStatus,
445) -> Result<(), WriteError> {
446 set_smartshift_on_channel(shared.channel(), shared.device_index(), status).await
447}