use std::sync::{Arc, Mutex, PoisonError};
use hidpp::{
channel::HidppChannel,
device::Device,
protocol::v20,
receiver::{self, Receiver},
};
use openlogi_core::binding::{GestureDirection, classify_gesture};
use thiserror::Error;
use tokio::sync::{mpsc, oneshot};
use tracing::{debug, info, warn};
use crate::reprog_controls::{self, RawControlEvent, ReprogControlsV4};
use crate::transport::{enumerate_hidpp_devices, open_hidpp_channel};
const MIN_TRAVEL: u32 = 50;
#[derive(Debug, Clone)]
pub struct GestureTarget {
pub receiver_uid: Option<String>,
pub slot: u8,
}
#[derive(Debug, Error)]
pub enum GestureError {
#[error("HID transport error")]
Hid(#[from] async_hid::HidError),
#[error("no matching receiver for the gesture target")]
ReceiverNotFound,
#[error("device on slot {0} did not respond to HID++")]
DeviceUnreachable(u8),
#[error("device does not expose ReprogControlsV4 (0x1b04)")]
Unsupported,
#[error("device has no raw-XY gesture button")]
NoGestureButton,
#[error("HID++ protocol error: {0}")]
Hidpp(String),
}
#[derive(Default)]
struct GestureAccum {
held: bool,
dx: i32,
dy: i32,
}
pub async fn run_gesture_session(
target: GestureTarget,
sink: mpsc::UnboundedSender<GestureDirection>,
shutdown: oneshot::Receiver<()>,
) -> Result<(), GestureError> {
let chan = open_target_channel(&target).await?;
let device = Device::new(Arc::clone(&chan), target.slot)
.await
.map_err(|_| GestureError::DeviceUnreachable(target.slot))?;
let info = device
.root()
.get_feature(reprog_controls::FEATURE_ID)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?
.ok_or(GestureError::Unsupported)?;
let reprog = ReprogControlsV4::new(Arc::clone(&chan), target.slot, info.index);
let control = reprog
.find_control(reprog_controls::GESTURE_BUTTON_CID)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?
.ok_or(GestureError::NoGestureButton)?;
if !control.supports_raw_xy() {
return Err(GestureError::NoGestureButton);
}
reprog
.set_cid_reporting(reprog_controls::GESTURE_BUTTON_CID, true, true)
.await
.map_err(|e| GestureError::Hidpp(format!("{e:?}")))?;
let accum = Arc::new(Mutex::new(GestureAccum::default()));
let device_index = target.slot;
let feature_index = info.index;
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);
let Some(event) = reprog_controls::decode_event(&msg, device_index, feature_index)
else {
return;
};
let mut acc = accum.lock().unwrap_or_else(PoisonError::into_inner);
match event {
RawControlEvent::DivertedButtons(_) => {
let held = event.is_pressed(reprog_controls::GESTURE_BUTTON_CID);
if held && !acc.held {
acc.held = true;
acc.dx = 0;
acc.dy = 0;
} else if !held && acc.held {
acc.held = false;
let direction = classify_gesture(acc.dx, acc.dy, MIN_TRAVEL);
debug!(?direction, dx = acc.dx, dy = acc.dy, "gesture released");
let _ = sink.send(direction);
}
}
RawControlEvent::RawXy { dx, dy } => {
if acc.held {
acc.dx = acc.dx.saturating_add(i32::from(dx));
acc.dy = acc.dy.saturating_add(i32::from(dy));
}
}
}
}
});
info!(slot = target.slot, "gesture capture active");
let _ = shutdown.await;
chan.remove_msg_listener(hdl);
if let Err(e) = reprog
.set_cid_reporting(reprog_controls::GESTURE_BUTTON_CID, false, false)
.await
{
warn!(error = %e, "failed to restore gesture button mapping on shutdown");
}
debug!(slot = target.slot, "gesture capture stopped");
Ok(())
}
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)
}