1use std::sync::{Arc, Mutex, PoisonError, RwLock};
19
20use hidpp::{channel::HidppChannel, device::Device, protocol::v20};
21use openlogi_core::binding::{ButtonId, GestureDirection, SwipeAccumulator};
22use serde::{Deserialize, Serialize};
23use thiserror::Error;
24use tokio::sync::{mpsc, oneshot};
25use tracing::{debug, info, warn};
26
27use crate::reprog_controls::{self, RawControlEvent, ReprogControlsV4};
28use crate::route::{DeviceRoute, open_route_channel};
29use crate::thumbwheel::{self, Thumbwheel};
30use crate::write::SharedChannel;
31
32pub type CaptureChannel = Arc<RwLock<Option<SharedChannel>>>;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum CapturedInput {
40 Gesture(GestureDirection),
42 ButtonPressed(ButtonId),
46 Scroll(i16),
50}
51
52#[derive(Debug, Error)]
54pub enum GestureError {
55 #[error("HID transport error")]
57 Hid(#[from] async_hid::HidError),
58 #[error("no connected device matched the capture route")]
60 DeviceNotFound,
61 #[error("device at index {0:#04x} did not respond to HID++")]
63 DeviceUnreachable(u8),
64 #[error("HID++ protocol error: {0}")]
66 Hidpp(String),
67}
68
69#[derive(Default)]
72struct CaptureAccum {
73 swipe: SwipeAccumulator,
75 dpi_down: bool,
78}
79
80pub async fn run_capture_session(
95 route: DeviceRoute,
96 capture_thumbwheel: bool,
97 divert_gesture_button: bool,
98 sink: mpsc::UnboundedSender<CapturedInput>,
99 shutdown: oneshot::Receiver<()>,
100 channel_slot: CaptureChannel,
101) -> Result<(), GestureError> {
102 let chan = open_route_channel(&route)
103 .await?
104 .ok_or(GestureError::DeviceNotFound)?;
105 let device_index = route.device_index();
106 let armed = arm_controls(
107 &chan,
108 device_index,
109 capture_thumbwheel,
110 divert_gesture_button,
111 )
112 .await?;
113
114 if let Ok(mut slot) = channel_slot.write() {
117 *slot = Some(SharedChannel::new(Arc::clone(&chan), route.clone()));
118 }
119
120 let accum = Arc::new(Mutex::new(CaptureAccum::default()));
121 let reprog_index = armed.reprog.as_ref().map(|(_, idx)| *idx);
122 let thumb_index = armed.thumb.as_ref().map(|(_, idx)| *idx);
123 let dpi_set = armed.dpi_cids.clone();
124 let hdl = chan.add_msg_listener({
125 let accum = Arc::clone(&accum);
126 let sink = sink.clone();
127 move |raw, matched| {
128 if matched {
129 return;
130 }
131 let msg = v20::Message::from(raw);
132 if let Some(idx) = reprog_index
133 && let Some(event) = reprog_controls::decode_event(&msg, device_index, idx)
134 {
135 let mut acc = accum.lock().unwrap_or_else(PoisonError::into_inner);
138 handle_reprog(&mut acc, event, &dpi_set, &sink);
139 return;
140 }
141 if let Some(idx) = thumb_index
142 && let Some(event) = thumbwheel::decode_event(&msg, device_index, idx)
143 {
144 if event.single_tap {
145 let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::Thumbwheel));
146 }
147 if event.rotation != 0 {
148 let _ = sink.send(CapturedInput::Scroll(event.rotation));
149 }
150 }
151 }
152 });
153
154 info!(
155 index = device_index,
156 gesture = armed.gesture_diverted,
157 dpi_buttons = armed.dpi_cids.len(),
158 thumbwheel = armed.thumb.is_some(),
159 "control capture active"
160 );
161 let _ = shutdown.await;
162
163 chan.remove_msg_listener(hdl);
164 if let Ok(mut slot) = channel_slot.write() {
165 *slot = None;
166 }
167 armed.disarm().await;
168 debug!(index = device_index, "control capture stopped");
169 Ok(())
170}
171
172struct ArmedControls {
175 reprog: Option<(ReprogControlsV4, u8)>,
177 gesture_diverted: bool,
179 dpi_cids: Vec<u16>,
181 thumb: Option<(Thumbwheel, u8)>,
184}
185
186impl ArmedControls {
187 async fn disarm(&self) {
189 if let Some((rc, _)) = self.reprog.as_ref() {
190 if self.gesture_diverted {
191 let r = rc
192 .set_cid_reporting(reprog_controls::GESTURE_BUTTON_CID, false, false)
193 .await;
194 restore(r, "gesture button");
195 }
196 for &cid in &self.dpi_cids {
197 restore(rc.set_cid_reporting(cid, false, false).await, "DPI button");
198 }
199 }
200 if let Some((tw, _)) = self.thumb.as_ref() {
201 restore(tw.set_reporting(false, false).await, "thumb wheel");
202 }
203 }
204}
205
206async fn arm_controls(
212 chan: &Arc<HidppChannel>,
213 slot: u8,
214 capture_thumbwheel: bool,
215 divert_gesture_button: bool,
216) -> Result<ArmedControls, GestureError> {
217 let device = Device::new(Arc::clone(chan), slot)
218 .await
219 .map_err(|_| GestureError::DeviceUnreachable(slot))?;
220
221 let mut reprog: Option<(ReprogControlsV4, u8)> = None;
222 let mut gesture_diverted = false;
223 let mut dpi_cids: Vec<u16> = Vec::new();
224 if let Some(info) = device
225 .root()
226 .get_feature(reprog_controls::FEATURE_ID)
227 .await
228 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?
229 {
230 let rc = ReprogControlsV4::new(Arc::clone(chan), slot, info.index);
231 let controls = enumerate_controls(&rc).await?;
232
233 if divert_gesture_button
236 && controls
237 .iter()
238 .any(|c| c.cid == reprog_controls::GESTURE_BUTTON_CID && c.supports_raw_xy())
239 {
240 rc.set_cid_reporting(reprog_controls::GESTURE_BUTTON_CID, true, true)
241 .await
242 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
243 gesture_diverted = true;
244 }
245 for &cid in &reprog_controls::DPI_MODE_SHIFT_CIDS {
246 if controls.iter().any(|c| c.cid == cid && c.is_divertable()) {
247 rc.set_cid_reporting(cid, true, false)
248 .await
249 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
250 dpi_cids.push(cid);
251 }
252 }
253 reprog = Some((rc, info.index));
254 }
255
256 let mut thumb: Option<(Thumbwheel, u8)> = None;
257 if capture_thumbwheel
258 && let Some(info) = device
259 .root()
260 .get_feature(thumbwheel::FEATURE_ID)
261 .await
262 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?
263 {
264 let tw = Thumbwheel::new(Arc::clone(chan), slot, info.index);
265 let supports_single_tap = match tw.get_info().await {
269 Ok(twinfo) => twinfo.supports_single_tap,
270 Err(e) => {
271 warn!(error = ?e, "thumb wheel getInfo failed");
272 false
273 }
274 };
275 if supports_single_tap {
276 tw.set_reporting(true, false)
277 .await
278 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
279 thumb = Some((tw, info.index));
280 } else {
281 debug!("thumb wheel reports no single tap — click not capturable");
282 }
283 }
284
285 if !gesture_diverted && dpi_cids.is_empty() && thumb.is_none() {
286 debug!(slot, "no capturable controls — idle session");
287 }
288 Ok(ArmedControls {
289 reprog,
290 gesture_diverted,
291 dpi_cids,
292 thumb,
293 })
294}
295
296fn restore<E: std::fmt::Display>(result: Result<(), E>, what: &str) {
298 if let Err(e) = result {
299 warn!(error = %e, control = what, "failed to restore control mapping on shutdown");
300 }
301}
302
303async fn enumerate_controls(
306 rc: &ReprogControlsV4,
307) -> Result<Vec<reprog_controls::CtrlIdInfo>, GestureError> {
308 let count = rc
309 .get_count()
310 .await
311 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
312 let mut controls = Vec::with_capacity(usize::from(count));
313 for index in 0..count {
314 controls.push(
315 rc.get_ctrl_id_info(index)
316 .await
317 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?,
318 );
319 }
320 Ok(controls)
321}
322
323fn handle_reprog(
328 acc: &mut CaptureAccum,
329 event: RawControlEvent,
330 dpi_cids: &[u16],
331 sink: &mpsc::UnboundedSender<CapturedInput>,
332) {
333 match event {
334 RawControlEvent::DivertedButtons(cids) => {
335 let gesture_held = cids.contains(&reprog_controls::GESTURE_BUTTON_CID);
336 if gesture_held && !acc.swipe.is_holding() {
337 acc.swipe.begin();
338 } else if !gesture_held && acc.swipe.is_holding() {
339 if acc.swipe.end() {
341 debug!("gesture click");
342 let _ = sink.send(CapturedInput::Gesture(GestureDirection::Click));
343 }
344 }
345
346 let dpi_down = dpi_cids.iter().any(|cid| cids.contains(cid));
347 if dpi_down && !acc.dpi_down {
348 let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::DpiToggle));
349 }
350 acc.dpi_down = dpi_down;
351 }
352 RawControlEvent::RawXy { dx, dy } => {
353 if let Some(direction) = acc.swipe.accumulate(i32::from(dx), i32::from(dy)) {
357 debug!(?direction, "gesture committed");
358 let _ = sink.send(CapturedInput::Gesture(direction));
359 }
360 }
361 }
362}
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn press() -> RawControlEvent {
368 RawControlEvent::DivertedButtons([reprog_controls::GESTURE_BUTTON_CID, 0, 0, 0])
369 }
370
371 fn release() -> RawControlEvent {
372 RawControlEvent::DivertedButtons([0, 0, 0, 0])
373 }
374
375 #[test]
376 fn quick_tap_is_a_click_even_while_the_cursor_moves() {
377 let (tx, mut rx) = mpsc::unbounded_channel();
378 let mut acc = CaptureAccum::default();
379
380 handle_reprog(&mut acc, press(), &[], &tx);
381 handle_reprog(
382 &mut acc,
383 RawControlEvent::RawXy { dx: 120, dy: 5 },
384 &[],
385 &tx,
386 );
387 handle_reprog(&mut acc, release(), &[], &tx);
388
389 assert_eq!(
390 rx.try_recv(),
391 Ok(CapturedInput::Gesture(GestureDirection::Click))
392 );
393 assert!(
394 rx.try_recv().is_err(),
395 "a quick tap emits exactly one click"
396 );
397 }
398
399 #[test]
400 fn a_held_gesture_commits_a_swipe_and_does_not_also_click() {
401 let (tx, mut rx) = mpsc::unbounded_channel();
402 let mut acc = CaptureAccum::default();
403
404 handle_reprog(&mut acc, press(), &[], &tx);
405 acc.swipe.backdate_hold_for_test();
407 handle_reprog(
408 &mut acc,
409 RawControlEvent::RawXy { dx: 120, dy: 5 },
410 &[],
411 &tx,
412 );
413
414 assert_eq!(
415 rx.try_recv(),
416 Ok(CapturedInput::Gesture(GestureDirection::Right))
417 );
418
419 handle_reprog(&mut acc, release(), &[], &tx);
420 assert!(
421 rx.try_recv().is_err(),
422 "a committed swipe must not also click on release"
423 );
424 }
425
426 #[test]
427 fn a_held_dpi_button_presses_once_on_the_rising_edge() {
428 let (tx, mut rx) = mpsc::unbounded_channel();
429 let mut acc = CaptureAccum::default();
430 let dpi = reprog_controls::DPI_MODE_SHIFT_CIDS[0];
431 let down = RawControlEvent::DivertedButtons([dpi, 0, 0, 0]);
432
433 handle_reprog(&mut acc, down, &[dpi], &tx);
434 handle_reprog(&mut acc, down, &[dpi], &tx);
435
436 assert_eq!(
437 rx.try_recv(),
438 Ok(CapturedInput::ButtonPressed(ButtonId::DpiToggle))
439 );
440 assert!(rx.try_recv().is_err(), "a held DPI button presses once");
441 }
442
443 #[test]
444 fn a_dpi_button_re_presses_after_a_release() {
445 let (tx, mut rx) = mpsc::unbounded_channel();
449 let mut acc = CaptureAccum::default();
450 let dpi = reprog_controls::DPI_MODE_SHIFT_CIDS[0];
451 let down = RawControlEvent::DivertedButtons([dpi, 0, 0, 0]);
452 let up = RawControlEvent::DivertedButtons([0, 0, 0, 0]);
453
454 handle_reprog(&mut acc, down, &[dpi], &tx);
455 handle_reprog(&mut acc, up, &[dpi], &tx);
456 handle_reprog(&mut acc, down, &[dpi], &tx);
457
458 assert_eq!(
459 rx.try_recv(),
460 Ok(CapturedInput::ButtonPressed(ButtonId::DpiToggle))
461 );
462 assert_eq!(
463 rx.try_recv(),
464 Ok(CapturedInput::ButtonPressed(ButtonId::DpiToggle)),
465 "a release re-arms the rising edge"
466 );
467 assert!(rx.try_recv().is_err());
468 }
469}