openlogi_device/session/
capture_restore.rs1use 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
19pub type CaptureChannelSlot = Arc<RwLock<Option<SharedChannel>>>;
22
23#[derive(Debug, Error)]
25pub enum CaptureError {
26 #[error("HID transport error")]
28 Hid(#[from] BackendError),
29 #[error("no connected device matched the capture route")]
31 DeviceNotFound,
32 #[error("device at index {0:#04x} did not respond to HID++")]
34 DeviceUnreachable(u8),
35 #[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#[derive(Clone, Copy)]
55pub(crate) struct ArmedReporting {
56 pub(crate) cid: u16,
57 pub(crate) original: reprog_controls::CidReporting,
58}
59
60pub(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 Shutdown,
79 ChannelChanged,
81}
82
83pub 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
127pub type PendingCaptureRestore = PendingRestore<CaptureRestorePlan>;
130
131pub type CaptureSessionOutcome = RestoreOutcome<CaptureRestorePlan>;
133
134pub type CaptureSessionFailure = SessionFailure<CaptureError, CaptureRestorePlan>;
137
138impl PendingCaptureRestore {
139 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
159pub(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
174pub(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
187pub(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 return std::future::pending().await;
204 }
205 }
206}
207
208pub(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
215pub(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
228pub(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
251pub(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}