use std::sync::{Arc, Mutex, PoisonError, RwLock};
use hidpp::{
channel::HidppChannel,
device::Device,
protocol::v20,
receiver::{self, Receiver},
};
use openlogi_core::binding::{ButtonId, GestureDirection, detect_swipe};
use thiserror::Error;
use tokio::sync::{mpsc, oneshot};
use tracing::{debug, info, warn};
use crate::reprog_controls::{self, RawControlEvent, ReprogControlsV4};
use crate::thumbwheel::{self, Thumbwheel};
use crate::transport::{enumerate_hidpp_devices, open_hidpp_channel};
use crate::write::SharedChannel;
pub type CaptureChannel = Arc<RwLock<Option<SharedChannel>>>;
#[derive(Debug, Clone)]
pub struct GestureTarget {
pub receiver_uid: Option<String>,
pub slot: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapturedInput {
Gesture(GestureDirection),
ButtonPressed(ButtonId),
Scroll(i16),
}
#[derive(Debug, Error)]
pub enum GestureError {
#[error("HID transport error")]
Hid(#[from] async_hid::HidError),
#[error("no matching receiver for the capture target")]
ReceiverNotFound,
#[error("device on slot {0} did not respond to HID++")]
DeviceUnreachable(u8),
#[error("HID++ protocol error: {0}")]
Hidpp(String),
}
#[derive(Default)]
struct CaptureAccum {
gesture_held: bool,
dx: i32,
dy: i32,
fired: bool,
dpi_down: bool,
}
pub async fn run_capture_session(
target: GestureTarget,
capture_thumbwheel: bool,
sink: mpsc::UnboundedSender<CapturedInput>,
shutdown: oneshot::Receiver<()>,
channel_slot: CaptureChannel,
) -> Result<(), GestureError> {
let chan = open_target_channel(&target).await?;
let armed = arm_controls(&chan, target.slot, capture_thumbwheel).await?;
if let Ok(mut slot) = channel_slot.write() {
*slot = Some(SharedChannel::new(
Arc::clone(&chan),
target.receiver_uid.clone(),
target.slot,
));
}
let accum = Arc::new(Mutex::new(CaptureAccum::default()));
let reprog_index = armed.reprog.as_ref().map(|(_, idx)| *idx);
let thumb_index = armed.thumb.as_ref().map(|(_, idx)| *idx);
let dpi_set = armed.dpi_cids.clone();
let device_index = target.slot;
let hdl = chan.add_msg_listener({
let accum = Arc::clone(&accum);
let sink = sink.clone();
move |raw, matched| {
if matched {
return;
}
let msg = v20::Message::from(raw);
if let Some(idx) = reprog_index {
if let Some(event) = reprog_controls::decode_event(&msg, device_index, idx) {
let mut acc = accum.lock().unwrap_or_else(PoisonError::into_inner);
handle_reprog(&mut acc, event, &dpi_set, &sink);
return;
}
}
if let Some(idx) = thumb_index {
if let Some(event) = thumbwheel::decode_event(&msg, device_index, idx) {
if event.single_tap {
let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::Thumbwheel));
}
if event.rotation != 0 {
let _ = sink.send(CapturedInput::Scroll(event.rotation));
}
}
}
}
});
info!(
slot = target.slot,
gesture = armed.gesture_diverted,
dpi_buttons = armed.dpi_cids.len(),
thumbwheel = armed.thumb.is_some(),
"control capture active"
);
let _ = shutdown.await;
chan.remove_msg_listener(hdl);
if let Ok(mut slot) = channel_slot.write() {
*slot = None;
}
armed.disarm().await;
debug!(slot = target.slot, "control capture stopped");
Ok(())
}
struct ArmedControls {
reprog: Option<(ReprogControlsV4, u8)>,
gesture_diverted: bool,
dpi_cids: Vec<u16>,
thumb: Option<(Thumbwheel, u8)>,
}
impl ArmedControls {
async fn disarm(&self) {
if let Some((rc, _)) = self.reprog.as_ref() {
if self.gesture_diverted {
let r = rc
.set_cid_reporting(reprog_controls::GESTURE_BUTTON_CID, false, false)
.await;
restore(r, "gesture button");
}
for &cid in &self.dpi_cids {
restore(rc.set_cid_reporting(cid, false, false).await, "DPI button");
}
}
if let Some((tw, _)) = self.thumb.as_ref() {
restore(tw.set_reporting(false, false).await, "thumb wheel");
}
}
}
async fn arm_controls(
chan: &Arc<HidppChannel>,
slot: u8,
capture_thumbwheel: bool,
) -> Result<ArmedControls, GestureError> {
let device = Device::new(Arc::clone(chan), slot)
.await
.map_err(|_| GestureError::DeviceUnreachable(slot))?;
let mut reprog: Option<(ReprogControlsV4, u8)> = None;
let mut gesture_diverted = false;
let mut dpi_cids: Vec<u16> = Vec::new();
if let Some(info) = device
.root()
.get_feature(reprog_controls::FEATURE_ID)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?
{
let rc = ReprogControlsV4::new(Arc::clone(chan), slot, info.index);
let controls = enumerate_controls(&rc).await?;
if controls
.iter()
.any(|c| c.cid == reprog_controls::GESTURE_BUTTON_CID && c.supports_raw_xy())
{
rc.set_cid_reporting(reprog_controls::GESTURE_BUTTON_CID, true, true)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
gesture_diverted = true;
}
for &cid in &reprog_controls::DPI_MODE_SHIFT_CIDS {
if controls.iter().any(|c| c.cid == cid && c.is_divertable()) {
rc.set_cid_reporting(cid, true, false)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
dpi_cids.push(cid);
}
}
reprog = Some((rc, info.index));
}
let mut thumb: Option<(Thumbwheel, u8)> = None;
if capture_thumbwheel {
if let Some(info) = device
.root()
.get_feature(thumbwheel::FEATURE_ID)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?
{
let tw = Thumbwheel::new(Arc::clone(chan), slot, info.index);
let supports_single_tap = match tw.get_info().await {
Ok(twinfo) => twinfo.supports_single_tap,
Err(e) => {
warn!(error = ?e, "thumb wheel getInfo failed");
false
}
};
if supports_single_tap {
tw.set_reporting(true, false)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
thumb = Some((tw, info.index));
} else {
debug!("thumb wheel reports no single tap — click not capturable");
}
}
}
if !gesture_diverted && dpi_cids.is_empty() && thumb.is_none() {
debug!(slot, "no capturable controls — idle session");
}
Ok(ArmedControls {
reprog,
gesture_diverted,
dpi_cids,
thumb,
})
}
fn restore<E: std::fmt::Display>(result: Result<(), E>, what: &str) {
if let Err(e) = result {
warn!(error = %e, control = what, "failed to restore control mapping on shutdown");
}
}
async fn enumerate_controls(
rc: &ReprogControlsV4,
) -> Result<Vec<reprog_controls::CtrlIdInfo>, GestureError> {
let count = rc
.get_count()
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
let mut controls = Vec::with_capacity(usize::from(count));
for index in 0..count {
controls.push(
rc.get_ctrl_id_info(index)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?,
);
}
Ok(controls)
}
fn handle_reprog(
acc: &mut CaptureAccum,
event: RawControlEvent,
dpi_cids: &[u16],
sink: &mpsc::UnboundedSender<CapturedInput>,
) {
match event {
RawControlEvent::DivertedButtons(cids) => {
let gesture_held = cids.contains(&reprog_controls::GESTURE_BUTTON_CID);
if gesture_held && !acc.gesture_held {
acc.gesture_held = true;
acc.dx = 0;
acc.dy = 0;
acc.fired = false;
} else if !gesture_held && acc.gesture_held {
acc.gesture_held = false;
if !acc.fired {
debug!("gesture click");
let _ = sink.send(CapturedInput::Gesture(GestureDirection::Click));
}
}
let dpi_down = dpi_cids.iter().any(|cid| cids.contains(cid));
if dpi_down && !acc.dpi_down {
let _ = sink.send(CapturedInput::ButtonPressed(ButtonId::DpiToggle));
}
acc.dpi_down = dpi_down;
}
RawControlEvent::RawXy { dx, dy } => {
if acc.gesture_held && !acc.fired {
acc.dx = acc.dx.saturating_add(i32::from(dx));
acc.dy = acc.dy.saturating_add(i32::from(dy));
if let Some(direction) = detect_swipe(acc.dx, acc.dy) {
debug!(?direction, dx = acc.dx, dy = acc.dy, "gesture committed");
acc.fired = true;
let _ = sink.send(CapturedInput::Gesture(direction));
}
}
}
}
}
async fn open_target_channel(target: &GestureTarget) -> Result<Arc<HidppChannel>, GestureError> {
let candidates = enumerate_hidpp_devices().await?;
for dev in candidates {
let Some((_, channel)) = open_hidpp_channel(dev).await? else {
continue;
};
let Some(Receiver::Bolt(bolt)) = receiver::detect(Arc::clone(&channel)) else {
continue;
};
if let Some(want) = target.receiver_uid.as_deref() {
match bolt.get_unique_id().await {
Ok(uid) if uid.eq_ignore_ascii_case(want) => {}
_ => continue,
}
}
return Ok(channel);
}
Err(GestureError::ReceiverNotFound)
}