Skip to main content

openlogi_device/session/
capture_restore.rs

1//! What capture owes the firmware and when it stops: the restore plan, the
2//! stop reasons and the reporting writes shared by mouse and keyboard capture
3//! without coupling their manager loops or input semantics. The token that
4//! carries an unfinished restore is `session::restore`'s.
5
6use std::fmt;
7use std::future::Future;
8use std::sync::{Arc, RwLock};
9
10use hidpp::protocol::v20::Hidpp20Error;
11use thiserror::Error;
12
13use super::restore::{PendingRestore, RestoreOutcome, RestorePlan, SessionFailure};
14use crate::backend::BackendError;
15use crate::reprog_controls::{self, ReprogControlsV4};
16use crate::thumbwheel::{self, Thumbwheel, ThumbwheelStatus, WheelDirection};
17use crate::{ChannelRegistry, DeviceRoute, IoSuspended, SharedChannel};
18
19/// Why a capture manager is stopping an active session.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum CaptureSessionStop {
22    /// Capture is no longer wanted, or its controls changed on the same route.
23    Shutdown,
24    /// The same physical device moved to another route. Restore through that
25    /// route before a successor arms.
26    Handoff(DeviceRoute),
27}
28
29/// Shared slot holding the active capture session's open channel, so bounded
30/// hardware writes can reuse it instead of opening a second connection.
31pub type CaptureChannelSlot = Arc<RwLock<Option<SharedChannel>>>;
32
33/// Why a capture session could not start (or had to stop).
34#[derive(Debug, Error)]
35pub enum CaptureError {
36    /// HID transport-level failure while enumerating or opening the device.
37    #[error("HID transport error")]
38    Hid(#[from] BackendError),
39    /// No connected device matched the capture route.
40    #[error("no connected device matched the capture route")]
41    DeviceNotFound,
42    /// The device at the target index did not answer HID++.
43    #[error("device at index {0:#04x} did not respond to HID++")]
44    DeviceUnreachable(u8),
45    /// A HID++ feature call returned an error; inner string carries context.
46    #[error("HID++ protocol error: {0}")]
47    Hidpp(String),
48}
49
50impl From<Hidpp20Error> for CaptureError {
51    fn from(error: Hidpp20Error) -> Self {
52        Self::Hidpp(format!("{error:?}"))
53    }
54}
55
56impl From<IoSuspended> for CaptureError {
57    fn from(error: IoSuspended) -> Self {
58        Self::Hid(error.into())
59    }
60}
61
62/// One `0x1b04` control whose original reporting state can restore a failed
63/// or completed capture transaction.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub(crate) struct ArmedReporting {
66    pub(crate) cid: u16,
67    pub(crate) original: reprog_controls::CidReporting,
68    policy: ReprogRestorePolicy,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72enum ReprogRestorePolicy {
73    /// Pointer control: clear diversion and raw XY.
74    Pointer,
75    /// Keyboard key: clear diversion without touching raw XY.
76    Keyboard,
77}
78
79impl ArmedReporting {
80    /// Capture ownership of a pointer control whose teardown clears both
81    /// diversion and raw XY.
82    pub(crate) fn pointer(cid: u16, original: reprog_controls::CidReporting) -> Self {
83        Self {
84            cid,
85            original,
86            policy: ReprogRestorePolicy::Pointer,
87        }
88    }
89
90    /// Capture ownership of a keyboard key whose teardown clears diversion
91    /// without touching raw XY.
92    pub(crate) fn keyboard(cid: u16, original: reprog_controls::CidReporting) -> Self {
93        Self {
94            cid,
95            original,
96            policy: ReprogRestorePolicy::Keyboard,
97        }
98    }
99
100    /// Compute the `setCidReporting` change that restores this control.
101    pub(crate) fn undivert_change(self) -> reprog_controls::CidReportingChange {
102        match self.policy {
103            ReprogRestorePolicy::Pointer => undivert_change(self.original),
104            ReprogRestorePolicy::Keyboard => undivert_keyboard_key(self.original),
105        }
106    }
107}
108
109/// A non-empty set of `0x1b04` controls owned through one feature index.
110pub(crate) struct ReprogRestore {
111    feature_index: u8,
112    controls: Vec<ArmedReporting>,
113}
114
115impl ReprogRestore {
116    pub(crate) fn new(feature_index: u8, controls: Vec<ArmedReporting>) -> Option<Self> {
117        (!controls.is_empty()).then_some(Self {
118            feature_index,
119            controls,
120        })
121    }
122}
123
124/// The thumb wheel a capture session diverted, and the native rotation sign
125/// to hand back with native reporting.
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub(crate) struct ThumbwheelRestore {
128    feature_index: u8,
129    native_direction: WheelDirection,
130}
131
132impl ThumbwheelRestore {
133    /// Teardown always selects native reporting, with the sign
134    /// [`thumbwheel::native_direction_to_restore`] derives from the status read
135    /// before arming.
136    pub(crate) fn new(feature_index: u8, original: Option<ThumbwheelStatus>) -> Self {
137        Self {
138            feature_index,
139            native_direction: thumbwheel::native_direction_to_restore(original),
140        }
141    }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub(crate) enum CaptureStop {
146    /// The owner deliberately requested teardown.
147    Shutdown,
148    /// Inventory removed or replaced the channel that armed capture.
149    ChannelChanged,
150    /// The same physical device moved. Restore through the successor route
151    /// instead of the transport it left.
152    Handoff(DeviceRoute),
153}
154
155/// What a capture session writes to hand its controls back: every diverted
156/// `0x1b04` control's reporting, and the thumb wheel's.
157///
158/// One untimed attempt per control; a failed one leaves the whole plan owed.
159pub struct CaptureRestorePlan {
160    reprog: Option<ReprogRestore>,
161    thumb: Option<ThumbwheelRestore>,
162}
163
164impl fmt::Debug for CaptureRestorePlan {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        f.debug_struct("CaptureRestorePlan")
167            .field(
168                "reporting_count",
169                &self
170                    .reprog
171                    .as_ref()
172                    .map_or(0, |reprog| reprog.controls.len()),
173            )
174            .field("has_thumbwheel", &self.thumb.is_some())
175            .finish()
176    }
177}
178
179impl RestorePlan for CaptureRestorePlan {
180    async fn restore_on(&self, current: &SharedChannel) -> bool {
181        let channel = Arc::clone(current.channel());
182        let device_index = current.device_index();
183        let mut restored = true;
184        if let Some(reprog) = &self.reprog {
185            let controls =
186                ReprogControlsV4::new(channel.clone(), device_index, reprog.feature_index);
187            for &reporting in &reprog.controls {
188                restored &= restore_reporting(&controls, reporting, "captured control").await;
189            }
190        }
191        if let Some(thumb) = self.thumb {
192            let thumbwheel = Thumbwheel::new(channel, device_index, thumb.feature_index);
193            restored &= restore_result(
194                thumbwheel.undivert(thumb.native_direction).await,
195                "thumb wheel",
196            );
197        }
198        restored
199    }
200}
201
202/// Firmware ownership a capture session could not release, to retry on the
203/// channel inventory publishes next.
204pub type PendingCaptureRestore = PendingRestore<CaptureRestorePlan>;
205
206/// How a capture session completed its firmware teardown.
207pub type CaptureSessionOutcome = RestoreOutcome<CaptureRestorePlan>;
208
209/// A capture setup failure plus any firmware ownership its rollback could not
210/// release.
211pub type CaptureSessionFailure = SessionFailure<CaptureError, CaptureRestorePlan>;
212
213impl PendingCaptureRestore {
214    /// Token aimed at `route` whose retry writes nothing and completes once that route is current.
215    #[must_use]
216    pub fn detached(route: DeviceRoute) -> Self {
217        Self::without_channel(
218            route,
219            CaptureRestorePlan {
220                reprog: None,
221                thumb: None,
222            },
223        )
224    }
225
226    /// The restore owed for `reprog` and the thumb wheel, or `None` when the
227    /// session diverted nothing.
228    pub(crate) fn new(
229        retired: &SharedChannel,
230        reprog: Option<ReprogRestore>,
231        thumb: Option<ThumbwheelRestore>,
232    ) -> Option<Self> {
233        if reprog.is_none() && thumb.is_none() {
234            return None;
235        }
236        Some(Self::owing(retired, CaptureRestorePlan { reprog, thumb }))
237    }
238}
239
240/// Release firmware ownership after an active session stops.
241pub(crate) async fn restore_after_stop(
242    stop: CaptureStop,
243    pending: Option<PendingCaptureRestore>,
244    registry: &ChannelRegistry,
245) -> CaptureSessionOutcome {
246    let Some(pending) = pending else {
247        return CaptureSessionOutcome::Restored;
248    };
249    match stop {
250        CaptureStop::Shutdown => pending.allow_current_channel().retry(registry).await,
251        CaptureStop::ChannelChanged => pending.retry(registry).await,
252        CaptureStop::Handoff(route) => pending.retry_via(route, registry).await,
253    }
254}
255
256/// Re-check inventory at shutdown so a simultaneously ready stop request does
257/// not win over publication replacement and write through a retired channel.
258pub(crate) fn stop_for_current_publication(
259    registry: &ChannelRegistry,
260    retired: &SharedChannel,
261) -> CaptureStop {
262    if registry.is_current(retired) {
263        CaptureStop::Shutdown
264    } else {
265        CaptureStop::ChannelChanged
266    }
267}
268
269/// Wait until inventory removes or replaces the channel on which capture was
270/// armed. The returned reason never carries a cached replacement across an
271/// await; restoration performs a fresh registry lookup instead.
272pub(crate) async fn wait_for_channel_change(
273    registry: &ChannelRegistry,
274    retired: &SharedChannel,
275) -> CaptureStop {
276    let mut changes = registry.subscribe();
277    loop {
278        if !registry.is_current(retired) {
279            return CaptureStop::ChannelChanged;
280        }
281        if changes.changed().await.is_err() {
282            // The borrowed registry owns the sender, so this is unreachable;
283            // stay pending if that invariant ever changes rather than
284            // inventing a channel transition.
285            return std::future::pending().await;
286        }
287    }
288}
289
290/// Keep accepting diverted reports until firmware teardown has completed.
291pub(crate) async fn drop_listener_after<T, R>(listener: T, teardown: impl Future<Output = R>) -> R {
292    let result = teardown.await;
293    drop(listener);
294    result
295}
296
297/// Divert a control in the requested mode while preserving its remap target.
298pub(crate) fn divert_change(
299    reporting: reprog_controls::CidReporting,
300    raw_xy: bool,
301) -> reprog_controls::CidReportingChange {
302    reprog_controls::CidReportingChange {
303        diverted: Some(true),
304        raw_xy: Some(raw_xy),
305        remap: reporting.remap,
306        ..Default::default()
307    }
308}
309
310/// Divert a keyboard control without touching raw XY (keyboards do not support raw XY).
311pub(crate) fn divert_keyboard_key(
312    reporting: reprog_controls::CidReporting,
313) -> reprog_controls::CidReportingChange {
314    reprog_controls::CidReportingChange {
315        diverted: Some(true),
316        raw_xy: None,
317        remap: reporting.remap,
318        ..Default::default()
319    }
320}
321
322/// Restore one captured reporting record, preserving its remap target and
323/// touching only the diversion bits capture owns.
324pub(crate) async fn restore_reporting(
325    controls: &ReprogControlsV4,
326    reporting: ArmedReporting,
327    what: &str,
328) -> bool {
329    let result = controls
330        .set_cid_reporting_full(reporting.cid, reporting.undivert_change())
331        .await
332        .map(|_| ());
333    restore_result(result, what)
334}
335
336pub(crate) fn restore_result<E: fmt::Display>(result: Result<(), E>, what: &str) -> bool {
337    if let Err(error) = result {
338        tracing::warn!(%error, control = what, "failed to restore control mapping");
339        false
340    } else {
341        true
342    }
343}
344
345/// Clear diversion and raw XY while preserving the control's original remap target.
346pub(crate) fn undivert_change(
347    reporting: reprog_controls::CidReporting,
348) -> reprog_controls::CidReportingChange {
349    reprog_controls::CidReportingChange {
350        diverted: Some(false),
351        raw_xy: Some(false),
352        remap: reporting.remap,
353        ..Default::default()
354    }
355}
356
357/// Clear keyboard diversion without touching raw XY while preserving remap.
358pub(crate) fn undivert_keyboard_key(
359    reporting: reprog_controls::CidReporting,
360) -> reprog_controls::CidReportingChange {
361    reprog_controls::CidReportingChange {
362        diverted: Some(false),
363        raw_xy: None,
364        remap: reporting.remap,
365        ..Default::default()
366    }
367}