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(
96 route: DeviceRoute,
97 capture_thumbwheel: bool,
98 divert_gesture_button: bool,
99 sink: mpsc::UnboundedSender<CapturedInput>,
100 shutdown: oneshot::Receiver<()>,
101 channel_slot: CaptureChannel,
102) -> Result<(), GestureError> {
103 let chan = open_route_channel(&route)
104 .await?
105 .ok_or(GestureError::DeviceNotFound)?;
106 let device_index = route.device_index();
107 let armed = arm_controls(
108 &chan,
109 device_index,
110 capture_thumbwheel,
111 divert_gesture_button,
112 )
113 .await?;
114
115 if let Ok(mut slot) = channel_slot.write() {
118 *slot = Some(SharedChannel::new(Arc::clone(&chan), route.clone()));
119 }
120
121 let accum = Arc::new(Mutex::new(CaptureAccum::default()));
122 let reprog_index = armed.reprog.as_ref().map(|(_, idx)| *idx);
123 let thumb_index = armed.thumb.as_ref().map(|(_, idx)| *idx);
124 let dpi_set = armed.dpi_cids.clone();
125 let listener = chan.add_msg_listener_guarded({
126 let accum = Arc::clone(&accum);
127 let sink = sink.clone();
128 move |raw, matched| {
129 if matched {
130 return;
131 }
132 let msg = v20::Message::from(raw);
133 if let Some(idx) = reprog_index
134 && let Some(event) = reprog_controls::decode_event(&msg, device_index, idx)
135 {
136 let mut acc = accum.lock().unwrap_or_else(PoisonError::into_inner);
139 handle_reprog(&mut acc, event, &dpi_set, &sink);
140 return;
141 }
142 if let Some(idx) = thumb_index
143 && let Some(event) = thumbwheel::decode_event(&msg, device_index, idx)
144 {
145 if event.single_tap {
146 let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::Thumbwheel));
147 }
148 if event.rotation != 0 {
149 let _ = sink.send(CapturedInput::Scroll(event.rotation));
150 }
151 }
152 }
153 });
154
155 info!(
156 index = device_index,
157 gesture = armed.gesture_diverted,
158 dpi_buttons = armed.dpi_cids.len(),
159 thumbwheel = armed.thumb.is_some(),
160 "control capture active"
161 );
162 let _ = shutdown.await;
163
164 drop(listener);
165 if let Ok(mut slot) = channel_slot.write() {
166 *slot = None;
167 }
168 armed.disarm().await;
169 debug!(index = device_index, "control capture stopped");
170 Ok(())
171}
172
173struct ArmedControls {
176 reprog: Option<(ReprogControlsV4, u8)>,
178 gesture_diverted: bool,
180 dpi_cids: Vec<u16>,
182 thumb: Option<(Thumbwheel, u8)>,
185}
186
187impl ArmedControls {
188 async fn disarm(&self) {
190 if let Some((rc, _)) = self.reprog.as_ref() {
191 if self.gesture_diverted {
192 let r = rc
193 .set_cid_reporting(reprog_controls::GESTURE_BUTTON_CID, false, false)
194 .await;
195 restore(r, "gesture button");
196 }
197 for &cid in &self.dpi_cids {
198 restore(rc.set_cid_reporting(cid, false, false).await, "DPI button");
199 }
200 }
201 if let Some((tw, _)) = self.thumb.as_ref() {
202 restore(tw.set_reporting(false, false).await, "thumb wheel");
203 }
204 }
205}
206
207async fn arm_controls(
213 chan: &Arc<HidppChannel>,
214 slot: u8,
215 capture_thumbwheel: bool,
216 divert_gesture_button: bool,
217) -> Result<ArmedControls, GestureError> {
218 let device = Device::new(Arc::clone(chan), slot)
219 .await
220 .map_err(|_| GestureError::DeviceUnreachable(slot))?;
221
222 let mut reprog: Option<(ReprogControlsV4, u8)> = None;
223 let mut gesture_diverted = false;
224 let mut dpi_cids: Vec<u16> = Vec::new();
225 if let Some(info) = device
226 .root()
227 .get_feature(reprog_controls::FEATURE_ID)
228 .await
229 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?
230 {
231 let rc = ReprogControlsV4::new(Arc::clone(chan), slot, info.index);
232 let controls = enumerate_controls(&rc).await?;
233
234 if divert_gesture_button
237 && controls
238 .iter()
239 .any(|c| c.cid == reprog_controls::GESTURE_BUTTON_CID && c.supports_raw_xy())
240 {
241 rc.set_cid_reporting(reprog_controls::GESTURE_BUTTON_CID, true, true)
242 .await
243 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
244 gesture_diverted = true;
245 }
246 for &cid in &reprog_controls::DPI_MODE_SHIFT_CIDS {
247 if controls.iter().any(|c| c.cid == cid && c.is_divertable()) {
248 rc.set_cid_reporting(cid, true, false)
249 .await
250 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
251 dpi_cids.push(cid);
252 }
253 }
254 reprog = Some((rc, info.index));
255 }
256
257 let mut thumb: Option<(Thumbwheel, u8)> = None;
258 if capture_thumbwheel
259 && let Some(info) = device
260 .root()
261 .get_feature(thumbwheel::FEATURE_ID)
262 .await
263 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?
264 {
265 let tw = Thumbwheel::new(Arc::clone(chan), slot, info.index);
266 let supports_single_tap = match tw.get_info().await {
270 Ok(twinfo) => twinfo.supports_single_tap,
271 Err(e) => {
272 warn!(error = ?e, "thumb wheel getInfo failed");
273 false
274 }
275 };
276 if supports_single_tap {
277 tw.set_reporting(true, false)
278 .await
279 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
280 thumb = Some((tw, info.index));
281 } else {
282 debug!("thumb wheel reports no single tap — click not capturable");
283 }
284 }
285
286 if !gesture_diverted && dpi_cids.is_empty() && thumb.is_none() {
287 debug!(slot, "no capturable controls — idle session");
288 }
289 Ok(ArmedControls {
290 reprog,
291 gesture_diverted,
292 dpi_cids,
293 thumb,
294 })
295}
296
297fn restore<E: std::fmt::Display>(result: Result<(), E>, what: &str) {
299 if let Err(e) = result {
300 warn!(error = %e, control = what, "failed to restore control mapping on shutdown");
301 }
302}
303
304async fn enumerate_controls(
307 rc: &ReprogControlsV4,
308) -> Result<Vec<reprog_controls::CtrlIdInfo>, GestureError> {
309 let count = rc
310 .get_count()
311 .await
312 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
313 let mut controls = Vec::with_capacity(usize::from(count));
314 for index in 0..count {
315 controls.push(
316 rc.get_ctrl_id_info(index)
317 .await
318 .map_err(|e| GestureError::Hidpp(format!("{e:?}")))?,
319 );
320 }
321 Ok(controls)
322}
323
324fn handle_reprog(
329 acc: &mut CaptureAccum,
330 event: RawControlEvent,
331 dpi_cids: &[u16],
332 sink: &mpsc::UnboundedSender<CapturedInput>,
333) {
334 match event {
335 RawControlEvent::DivertedButtons(cids) => {
336 let gesture_held = cids.contains(&reprog_controls::GESTURE_BUTTON_CID);
337 if gesture_held && !acc.swipe.is_holding() {
338 acc.swipe.begin();
339 } else if !gesture_held && acc.swipe.is_holding() {
340 if acc.swipe.end() {
342 debug!("gesture click");
343 let _ = sink.send(CapturedInput::Gesture(GestureDirection::Click));
344 }
345 }
346
347 let dpi_down = dpi_cids.iter().any(|cid| cids.contains(cid));
348 if dpi_down && !acc.dpi_down {
349 let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::DpiToggle));
350 }
351 acc.dpi_down = dpi_down;
352 }
353 RawControlEvent::RawXy { dx, dy } => {
354 if let Some(direction) = acc.swipe.accumulate(i32::from(dx), i32::from(dy)) {
358 debug!(?direction, "gesture committed");
359 let _ = sink.send(CapturedInput::Gesture(direction));
360 }
361 }
362 }
363}
364#[cfg(test)]
365mod tests;