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::Thumbwheel;
17use crate::{ChannelRegistry, IoSuspended, SharedChannel};
18
19/// Shared slot holding the active capture session's open channel, so bounded
20/// hardware writes can reuse it instead of opening a second connection.
21pub type CaptureChannelSlot = Arc<RwLock<Option<SharedChannel>>>;
22
23/// Why a capture session could not start (or had to stop).
24#[derive(Debug, Error)]
25pub enum CaptureError {
26    /// HID transport-level failure while enumerating or opening the device.
27    #[error("HID transport error")]
28    Hid(#[from] BackendError),
29    /// No connected device matched the capture route.
30    #[error("no connected device matched the capture route")]
31    DeviceNotFound,
32    /// The device at the target index did not answer HID++.
33    #[error("device at index {0:#04x} did not respond to HID++")]
34    DeviceUnreachable(u8),
35    /// A HID++ feature call returned an error; inner string carries context.
36    #[error("HID++ protocol error: {0}")]
37    Hidpp(String),
38}
39
40impl From<Hidpp20Error> for CaptureError {
41    fn from(error: Hidpp20Error) -> Self {
42        Self::Hidpp(format!("{error:?}"))
43    }
44}
45
46impl From<IoSuspended> for CaptureError {
47    fn from(error: IoSuspended) -> Self {
48        Self::Hid(error.into())
49    }
50}
51
52/// One `0x1b04` control whose original reporting state can restore a failed
53/// or completed capture transaction.
54#[derive(Clone, Copy)]
55pub(crate) struct ArmedReporting {
56    pub(crate) cid: u16,
57    pub(crate) original: reprog_controls::CidReporting,
58}
59
60/// A non-empty set of `0x1b04` controls owned through one feature index.
61pub(crate) struct ReprogRestore {
62    feature_index: u8,
63    controls: Vec<ArmedReporting>,
64}
65
66impl ReprogRestore {
67    pub(crate) fn new(feature_index: u8, controls: Vec<ArmedReporting>) -> Option<Self> {
68        (!controls.is_empty()).then_some(Self {
69            feature_index,
70            controls,
71        })
72    }
73}
74
75#[derive(Clone, Copy)]
76pub(crate) enum CaptureStop {
77    /// The owner deliberately requested teardown.
78    Shutdown,
79    /// Inventory removed or replaced the channel that armed capture.
80    ChannelChanged,
81}
82
83/// What a capture session writes to hand its controls back: every diverted
84/// `0x1b04` control's reporting, and the thumb wheel's.
85///
86/// One untimed attempt per control; a failed one leaves the whole plan owed.
87pub struct CaptureRestorePlan {
88    reprog: Option<ReprogRestore>,
89    thumb_index: Option<u8>,
90}
91
92impl fmt::Debug for CaptureRestorePlan {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        f.debug_struct("CaptureRestorePlan")
95            .field(
96                "reporting_count",
97                &self
98                    .reprog
99                    .as_ref()
100                    .map_or(0, |reprog| reprog.controls.len()),
101            )
102            .field("has_thumbwheel", &self.thumb_index.is_some())
103            .finish()
104    }
105}
106
107impl RestorePlan for CaptureRestorePlan {
108    async fn restore_on(&self, current: &SharedChannel) -> bool {
109        let channel = Arc::clone(current.channel());
110        let device_index = current.device_index();
111        let mut restored = true;
112        if let Some(reprog) = &self.reprog {
113            let controls =
114                ReprogControlsV4::new(channel.clone(), device_index, reprog.feature_index);
115            for &reporting in &reprog.controls {
116                restored &= restore_reporting(&controls, reporting, "captured control").await;
117            }
118        }
119        if let Some(feature_index) = self.thumb_index {
120            let thumbwheel = Thumbwheel::new(channel, device_index, feature_index);
121            restored &= restore_result(thumbwheel.undivert().await, "thumb wheel");
122        }
123        restored
124    }
125}
126
127/// Firmware ownership a capture session could not release, to retry on the
128/// channel inventory publishes next.
129pub type PendingCaptureRestore = PendingRestore<CaptureRestorePlan>;
130
131/// How a capture session completed its firmware teardown.
132pub type CaptureSessionOutcome = RestoreOutcome<CaptureRestorePlan>;
133
134/// A capture setup failure plus any firmware ownership its rollback could not
135/// release.
136pub type CaptureSessionFailure = SessionFailure<CaptureError, CaptureRestorePlan>;
137
138impl PendingCaptureRestore {
139    /// The restore owed for `reprog` and the thumb wheel, or `None` when the
140    /// session diverted nothing.
141    pub(crate) fn new(
142        retired: &SharedChannel,
143        reprog: Option<ReprogRestore>,
144        thumb_index: Option<u8>,
145    ) -> Option<Self> {
146        if reprog.is_none() && thumb_index.is_none() {
147            return None;
148        }
149        Some(Self::owing(
150            retired,
151            CaptureRestorePlan {
152                reprog,
153                thumb_index,
154            },
155        ))
156    }
157}
158
159/// Release firmware ownership after an active session stops.
160pub(crate) async fn restore_after_stop(
161    stop: CaptureStop,
162    pending: Option<PendingCaptureRestore>,
163    registry: &ChannelRegistry,
164) -> CaptureSessionOutcome {
165    let Some(pending) = pending else {
166        return CaptureSessionOutcome::Restored;
167    };
168    match stop {
169        CaptureStop::Shutdown => pending.allow_current_channel().retry(registry).await,
170        CaptureStop::ChannelChanged => pending.retry(registry).await,
171    }
172}
173
174/// Re-check inventory at shutdown so a simultaneously ready stop request does
175/// not win over publication replacement and write through a retired channel.
176pub(crate) fn stop_for_current_publication(
177    registry: &ChannelRegistry,
178    retired: &SharedChannel,
179) -> CaptureStop {
180    if registry.is_current(retired) {
181        CaptureStop::Shutdown
182    } else {
183        CaptureStop::ChannelChanged
184    }
185}
186
187/// Wait until inventory removes or replaces the channel on which capture was
188/// armed. The returned reason never carries a cached replacement across an
189/// await; restoration performs a fresh registry lookup instead.
190pub(crate) async fn wait_for_channel_change(
191    registry: &ChannelRegistry,
192    retired: &SharedChannel,
193) -> CaptureStop {
194    let mut changes = registry.subscribe();
195    loop {
196        if !registry.is_current(retired) {
197            return CaptureStop::ChannelChanged;
198        }
199        if changes.changed().await.is_err() {
200            // The borrowed registry owns the sender, so this is unreachable;
201            // stay pending if that invariant ever changes rather than
202            // inventing a channel transition.
203            return std::future::pending().await;
204        }
205    }
206}
207
208/// Keep accepting diverted reports until firmware teardown has completed.
209pub(crate) async fn drop_listener_after<T, R>(listener: T, teardown: impl Future<Output = R>) -> R {
210    let result = teardown.await;
211    drop(listener);
212    result
213}
214
215/// Divert a control in the requested mode while preserving its remap target.
216pub(crate) fn divert_change(
217    reporting: reprog_controls::CidReporting,
218    raw_xy: bool,
219) -> reprog_controls::CidReportingChange {
220    reprog_controls::CidReportingChange {
221        diverted: Some(true),
222        raw_xy: Some(raw_xy),
223        remap: reporting.remap,
224        ..Default::default()
225    }
226}
227
228/// Restore one captured reporting record, preserving its remap target and
229/// touching only the diversion bits capture owns.
230pub(crate) async fn restore_reporting(
231    controls: &ReprogControlsV4,
232    reporting: ArmedReporting,
233    what: &str,
234) -> bool {
235    let result = controls
236        .set_cid_reporting_full(reporting.cid, undivert_change(reporting.original))
237        .await
238        .map(|_| ());
239    restore_result(result, what)
240}
241
242pub(crate) fn restore_result<E: fmt::Display>(result: Result<(), E>, what: &str) -> bool {
243    if let Err(error) = result {
244        tracing::warn!(%error, control = what, "failed to restore control mapping");
245        false
246    } else {
247        true
248    }
249}
250
251/// Clear diversion while preserving the control's original remap target.
252pub(crate) fn undivert_change(
253    reporting: reprog_controls::CidReporting,
254) -> reprog_controls::CidReportingChange {
255    reprog_controls::CidReportingChange {
256        diverted: Some(false),
257        raw_xy: Some(false),
258        remap: reporting.remap,
259        ..Default::default()
260    }
261}