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::{
12 SmartShiftEnhancedCapabilities, SmartShiftEnhancedFeature,
13 SmartShiftEnhancedStatusChange,
14 },
15 },
16};
17use tracing::debug;
18
19use crate::SharedChannel;
20use crate::backend::HidBackend;
21use crate::channel::route::DeviceRoute;
22use openlogi_core::hid::smartshift::{
23 SmartShiftAutoDisengage, SmartShiftMode, SmartShiftStatus, TunableTorque,
24};
25
26use super::{
27 HidppFeatureErrorKind, HidppOperation, WriteError, classify_hidpp_error, open_feature,
28 with_route,
29};
30
31const TRANSIENT_RETRY_DELAY: Duration = Duration::from_millis(50);
34
35pub(super) fn is_missing_enhanced(err: &WriteError) -> bool {
40 matches!(
41 err,
42 WriteError::FeatureUnsupported { feature_hex } if *feature_hex == 0x2111
43 )
44}
45
46pub(super) fn is_transient_smartshift_error(err: &WriteError) -> bool {
51 matches!(
52 err,
53 WriteError::HidppFeature {
54 kind: HidppFeatureErrorKind::InvalidArgument
55 | HidppFeatureErrorKind::Busy
56 | HidppFeatureErrorKind::HwError,
57 ..
58 } | WriteError::UnsupportedResponse { .. }
59 )
60}
61
62pub(super) fn status_matches_desired(current: SmartShiftStatus, desired: SmartShiftStatus) -> bool {
66 current.mode == desired.mode
67 && current.auto_disengage == desired.auto_disengage
68 && desired
69 .tunable_torque
70 .is_none_or(|torque| current.tunable_torque == Some(torque))
71}
72
73fn decode_auto_disengage(
74 value: u8,
75 feature_hex: u16,
76) -> Result<SmartShiftAutoDisengage, WriteError> {
77 SmartShiftAutoDisengage::try_from(value).map_err(|_| WriteError::UnsupportedResponse {
78 operation: HidppOperation::ReadSmartShift,
79 feature_hex,
80 })
81}
82
83fn confirm_write(confirmed: bool, feature_hex: u16) -> Result<(), WriteError> {
87 if confirmed {
88 Ok(())
89 } else {
90 Err(WriteError::UnsupportedResponse {
91 operation: HidppOperation::WriteSmartShift,
92 feature_hex,
93 })
94 }
95}
96
97fn decode_tunable_torque(value: u8) -> Result<Option<TunableTorque>, WriteError> {
101 if value == 0 {
102 return Ok(None);
103 }
104 TunableTorque::try_from(value)
105 .map(Some)
106 .map_err(|_| WriteError::UnsupportedResponse {
107 operation: HidppOperation::ReadSmartShift,
108 feature_hex: SmartShiftEnhancedFeature::ID,
109 })
110}
111
112pub(super) fn wheel_mode_to_smartshift(wheel: WheelMode) -> SmartShiftMode {
117 if matches!(wheel, WheelMode::Freespin) {
118 SmartShiftMode::Free
119 } else {
120 SmartShiftMode::Ratchet
121 }
122}
123
124pub(super) fn smartshift_to_wheel(mode: SmartShiftMode) -> WheelMode {
128 match mode {
129 SmartShiftMode::Free => WheelMode::Freespin,
130 SmartShiftMode::Ratchet => WheelMode::Ratchet,
131 }
132}
133
134enum SmartShift {
138 Enhanced(Arc<SmartShiftEnhancedFeature>),
140 Legacy(Arc<SmartShiftFeature>),
142}
143
144impl SmartShift {
145 async fn open(device: &mut Device) -> Result<Self, WriteError> {
152 match open_feature::<SmartShiftEnhancedFeature>(device).await {
153 Ok(feature) => Ok(Self::Enhanced(feature)),
154 Err(err) if is_missing_enhanced(&err) => {
155 match open_feature::<SmartShiftEnhancedFeature>(device).await {
156 Ok(feature) => Ok(Self::Enhanced(feature)),
157 Err(err) if is_missing_enhanced(&err) => {
158 let feature = open_feature::<SmartShiftFeature>(device).await?;
159 Ok(Self::Legacy(feature))
160 }
161 Err(err) => Err(err),
162 }
163 }
164 Err(err) => Err(err),
165 }
166 }
167
168 async fn status(&self) -> Result<SmartShiftStatus, WriteError> {
171 match self {
172 Self::Enhanced(feature) => {
173 let status = feature.get_ratchet_control_mode().await.map_err(|e| {
174 classify_hidpp_error(
175 e,
176 HidppOperation::ReadSmartShift,
177 SmartShiftEnhancedFeature::ID,
178 )
179 })?;
180 Ok(SmartShiftStatus {
181 mode: wheel_mode_to_smartshift(status.wheel_mode),
182 auto_disengage: decode_auto_disengage(
183 status.auto_disengage,
184 SmartShiftEnhancedFeature::ID,
185 )?,
186 tunable_torque: decode_tunable_torque(status.current_tunable_torque)?,
187 })
188 }
189 Self::Legacy(feature) => {
190 let rcm = feature.get_ratchet_control_mode().await.map_err(|e| {
191 classify_hidpp_error(e, HidppOperation::ReadSmartShift, SmartShiftFeature::ID)
192 })?;
193 Ok(SmartShiftStatus {
194 mode: wheel_mode_to_smartshift(rcm.wheel_mode),
195 auto_disengage: decode_auto_disengage(
196 rcm.auto_disengage,
197 SmartShiftFeature::ID,
198 )?,
199 tunable_torque: None,
200 })
201 }
202 }
203 }
204
205 async fn set_status(&self, status: SmartShiftStatus) -> Result<(), WriteError> {
220 let SmartShiftStatus {
221 mode,
222 auto_disengage,
223 tunable_torque,
224 } = status;
225 let wheel_mode = smartshift_to_wheel(mode);
226 let auto_disengage = NonZeroU8::from(auto_disengage);
227 match self {
228 Self::Enhanced(feature) => {
229 let tunable_torque = writable_torque(feature, tunable_torque).await?;
230 let applied = feature
231 .set_ratchet_control_mode(SmartShiftEnhancedStatusChange {
232 wheel_mode: Some(wheel_mode),
233 auto_disengage: Some(auto_disengage),
234 tunable_torque,
235 })
236 .await
237 .map_err(|e| {
238 classify_hidpp_error(
239 e,
240 HidppOperation::WriteSmartShift,
241 SmartShiftEnhancedFeature::ID,
242 )
243 })?;
244 confirm_write(
245 applied.wheel_mode == wheel_mode
246 && applied.auto_disengage == auto_disengage.get()
247 && tunable_torque
248 .is_none_or(|torque| applied.current_tunable_torque == torque.get()),
249 SmartShiftEnhancedFeature::ID,
250 )
251 }
252 Self::Legacy(feature) => {
253 let applied = feature
254 .set_ratchet_control_mode(Some(wheel_mode), Some(auto_disengage), None)
255 .await
256 .map_err(|e| {
257 classify_hidpp_error(
258 e,
259 HidppOperation::WriteSmartShift,
260 SmartShiftFeature::ID,
261 )
262 })?;
263 confirm_write(
264 applied.wheel_mode == wheel_mode
265 && applied.auto_disengage == auto_disengage.get(),
266 SmartShiftFeature::ID,
267 )
268 }
269 }
270 }
271
272 async fn set_sensitivity(&self, value: SmartShiftAutoDisengage) -> Result<(), WriteError> {
275 let current = self.status().await?;
276 self.set_status(SmartShiftStatus {
277 auto_disengage: value,
278 tunable_torque: None,
279 ..current
280 })
281 .await
282 }
283}
284
285async fn writable_torque(
292 feature: &SmartShiftEnhancedFeature,
293 torque: Option<TunableTorque>,
294) -> Result<Option<NonZeroU8>, WriteError> {
295 let Some(torque) = torque else {
296 return Ok(None);
297 };
298 let info = feature.get_capabilities().await.map_err(|e| {
299 classify_hidpp_error(
300 e,
301 HidppOperation::ReadSmartShift,
302 SmartShiftEnhancedFeature::ID,
303 )
304 })?;
305 if info
306 .capabilities
307 .contains(SmartShiftEnhancedCapabilities::TUNABLE_TORQUE)
308 {
309 Ok(Some(NonZeroU8::from(torque)))
310 } else {
311 debug!(
312 %torque,
313 "device does not advertise tunable torque; leaving it unchanged"
314 );
315 Ok(None)
316 }
317}
318
319pub async fn get_smartshift_status(
322 backend: &dyn HidBackend,
323 route: &DeviceRoute,
324) -> Result<SmartShiftStatus, WriteError> {
325 let index = route.device_index();
326 with_route(backend, route, move |channel| async move {
327 get_smartshift_status_on_channel(&channel, index).await
328 })
329 .await
330}
331
332pub(super) async fn get_smartshift_status_on_channel(
333 channel: &Arc<HidppChannel>,
334 index: u8,
335) -> Result<SmartShiftStatus, WriteError> {
336 let mut device = Device::new(Arc::clone(channel), index)
337 .await
338 .map_err(|_| WriteError::DeviceUnreachable { index })?;
339 let smartshift = SmartShift::open(&mut device).await?;
340 smartshift.status().await
341}
342
343pub async fn set_smartshift_sensitivity(
350 backend: &dyn HidBackend,
351 route: &DeviceRoute,
352 value: SmartShiftAutoDisengage,
353) -> Result<SmartShiftStatus, WriteError> {
354 let index = route.device_index();
355 with_route(backend, route, move |channel| async move {
356 let mut device = Device::new(Arc::clone(&channel), index)
357 .await
358 .map_err(|_| WriteError::DeviceUnreachable { index })?;
359 let smartshift = SmartShift::open(&mut device).await?;
360 smartshift.set_sensitivity(value).await?;
361 smartshift.status().await
362 })
363 .await
364}
365
366pub async fn toggle_smartshift(
374 backend: &dyn HidBackend,
375 route: &DeviceRoute,
376) -> Result<SmartShiftMode, WriteError> {
377 let index = route.device_index();
378 with_route(backend, route, move |channel| async move {
379 toggle_smartshift_on_channel(&channel, index).await
380 })
381 .await
382}
383
384pub(super) async fn toggle_smartshift_on_channel(
394 channel: &Arc<HidppChannel>,
395 index: u8,
396) -> Result<SmartShiftMode, WriteError> {
397 let mut device = Device::new(Arc::clone(channel), index)
398 .await
399 .map_err(|_| WriteError::DeviceUnreachable { index })?;
400 let (smartshift, target) = retry_transient_toggle(index, async || {
401 let smartshift = SmartShift::open(&mut device).await?;
402 let status = smartshift.status().await?;
403 let target = SmartShiftStatus {
404 mode: status.mode.flipped(),
405 tunable_torque: None,
406 ..status
407 };
408 Ok((smartshift, target))
409 })
410 .await?;
411 retry_transient_toggle(index, async || smartshift.set_status(target).await).await?;
412 debug!(index, next = ?target.mode, "wrote SmartShift mode");
413 Ok(target.mode)
414}
415
416async fn retry_transient_toggle<T>(
418 index: u8,
419 mut step: impl AsyncFnMut() -> Result<T, WriteError>,
420) -> Result<T, WriteError> {
421 match step().await {
422 Err(err) if is_transient_smartshift_error(&err) => {
423 debug!(
424 index,
425 error = ?err,
426 "SmartShift toggle hit a transient error; retrying once"
427 );
428 tokio::time::sleep(TRANSIENT_RETRY_DELAY).await;
429 step().await
430 }
431 result => result,
432 }
433}
434
435pub async fn set_smartshift(
446 backend: &dyn HidBackend,
447 route: &DeviceRoute,
448 status: SmartShiftStatus,
449) -> Result<(), WriteError> {
450 let index = route.device_index();
451 with_route(backend, route, move |channel| async move {
452 set_smartshift_on_channel(&channel, index, status).await
453 })
454 .await
455}
456
457pub(super) async fn set_smartshift_on_channel(
465 channel: &Arc<HidppChannel>,
466 index: u8,
467 desired: SmartShiftStatus,
468) -> Result<(), WriteError> {
469 let mut device = Device::new(Arc::clone(channel), index)
470 .await
471 .map_err(|_| WriteError::DeviceUnreachable { index })?;
472 let smartshift = SmartShift::open(&mut device).await?;
473 let current = smartshift.status().await.ok();
474 if current.is_some_and(|current| status_matches_desired(current, desired)) {
475 debug!(
476 index,
477 status = ?desired,
478 "SmartShift already matches config; skipping write"
479 );
480 return Ok(());
481 }
482 let write = SmartShiftStatus {
483 tunable_torque: desired
484 .tunable_torque
485 .filter(|torque| current.is_none_or(|current| current.tunable_torque != Some(*torque))),
486 ..desired
487 };
488 match smartshift.set_status(write).await {
489 Ok(()) => {
490 debug!(
491 index,
492 status = ?desired,
493 "wrote SmartShift config"
494 );
495 Ok(())
496 }
497 Err(err) if is_transient_smartshift_error(&err) => {
498 debug!(
499 index,
500 error = ?err,
501 "SmartShift write hit a transient error; retrying once"
502 );
503 tokio::time::sleep(TRANSIENT_RETRY_DELAY).await;
504 let smartshift = SmartShift::open(&mut device).await?;
507 smartshift.set_status(write).await?;
508 debug!(
509 index,
510 status = ?desired,
511 "wrote SmartShift config"
512 );
513 Ok(())
514 }
515 Err(err) => Err(err),
516 }
517}
518
519pub async fn toggle_smartshift_on(shared: &SharedChannel) -> Result<SmartShiftMode, WriteError> {
521 toggle_smartshift_on_channel(shared.channel(), shared.device_index()).await
522}
523
524pub async fn get_smartshift_status_on(
526 shared: &SharedChannel,
527) -> Result<SmartShiftStatus, WriteError> {
528 get_smartshift_status_on_channel(shared.channel(), shared.device_index()).await
529}
530
531pub async fn set_smartshift_on(
534 shared: &SharedChannel,
535 status: SmartShiftStatus,
536) -> Result<(), WriteError> {
537 set_smartshift_on_channel(shared.channel(), shared.device_index(), status).await
538}
539
540#[cfg(test)]
541mod tests;