mod liveness;
use std::sync::Arc;
use std::time::Duration;
use hidpp::{
device::Device,
feature::{
CreatableFeature, EmittingFeature,
root::RootFeature,
wireless_device_status::{WirelessDeviceStatusEvent, WirelessDeviceStatusFeature},
},
protocol::v20,
};
use tokio::sync::{mpsc, oneshot};
use tokio::time::Instant;
use tracing::{debug, info, warn};
use liveness::{CaptureLiveness, ChannelActivity, LivenessDecision, PingOutcome};
use super::capture_restore::{
CaptureChannelSlot, CaptureError, CaptureSessionOutcome, CaptureStop, PendingCaptureRestore,
drop_listener_after, restore_after_stop, stop_for_current_publication, wait_for_channel_change,
};
use super::gesture::CapturedInput;
use crate::{ChannelRegistry, DeviceIoGate, DeviceRoute, SharedChannel};
const REARM_SETTLE_DELAY: Duration = Duration::from_millis(200);
pub struct CaptureHost<'a> {
pub sink: mpsc::UnboundedSender<CapturedInput>,
pub shutdown: oneshot::Receiver<()>,
pub channel_slot: CaptureChannelSlot,
pub registry: &'a ChannelRegistry,
pub device_io: DeviceIoGate,
}
impl CaptureHost<'_> {
pub(super) fn channel_for(&self, route: &DeviceRoute) -> Result<SharedChannel, CaptureError> {
let shared = self
.registry
.lookup(route)
.ok_or(CaptureError::DeviceNotFound)?;
self.device_io.ensure_allowed()?;
Ok(shared)
}
}
pub(super) async fn open_device(shared: &SharedChannel) -> Result<Device, CaptureError> {
let device_index = shared.device_index();
Device::new(Arc::clone(shared.channel()), device_index)
.await
.map_err(|_| CaptureError::DeviceUnreachable(device_index))
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum Liveness {
Watched,
Unwatched,
}
pub(super) trait ArmedCapture {
const NAME: &'static str;
const LIVENESS: Liveness;
fn log_active(&self, device_index: u8, wake_rearm: bool);
fn report_handler(
&self,
device_index: u8,
sink: mpsc::UnboundedSender<CapturedInput>,
) -> impl Fn(&v20::Message) + Send + Sync + 'static;
fn reset_input_state(&self) {}
async fn rearm(&self);
fn into_pending(self, retired: &SharedChannel) -> Option<PendingCaptureRestore>;
}
pub(super) async fn run_capture<A: ArmedCapture>(
shared: SharedChannel,
armed: A,
host: CaptureHost<'_>,
) -> CaptureSessionOutcome {
let CaptureHost {
sink,
shutdown,
channel_slot,
registry,
device_io,
} = host;
let chan = Arc::clone(shared.channel());
let device_index = shared.device_index();
if let Ok(mut slot) = channel_slot.write() {
*slot = Some(shared.clone());
}
let activity = match A::LIVENESS {
Liveness::Watched => Some(Arc::new(ChannelActivity::default())),
Liveness::Unwatched => None,
};
let listener = chan.add_msg_listener_guarded({
let activity = activity.clone();
let on_report = armed.report_handler(device_index, sink.clone());
move |raw, matched| {
if let Some(activity) = &activity {
activity.record();
}
if matched {
return;
}
on_report(&v20::Message::from(raw));
}
});
let root = RootFeature::new(Arc::clone(&chan), device_index, 0);
let wireless = root
.get_feature(WirelessDeviceStatusFeature::ID)
.await
.ok()
.flatten()
.map(|info| WirelessDeviceStatusFeature::new(Arc::clone(&chan), device_index, info.index));
armed.log_active(device_index, wireless.is_some());
let stop = monitor(
CaptureMonitor {
armed: &armed,
root: &root,
device_index,
registry,
shared: &shared,
activity: activity.as_deref(),
},
wireless,
shutdown,
device_io,
)
.await;
if let Ok(mut slot) = channel_slot.write()
&& slot
.as_ref()
.is_some_and(|published| Arc::ptr_eq(published.channel(), &chan))
{
*slot = None;
}
let pending = armed.into_pending(&shared);
let outcome = drop_listener_after(listener, restore_after_stop(stop, pending, registry)).await;
debug!(index = device_index, capture = A::NAME, "capture stopped");
outcome
}
struct CaptureMonitor<'a, A> {
armed: &'a A,
root: &'a RootFeature,
device_index: u8,
registry: &'a ChannelRegistry,
shared: &'a SharedChannel,
activity: Option<&'a ChannelActivity>,
}
struct Watchdog<'a> {
activity: &'a ChannelActivity,
liveness: CaptureLiveness,
}
impl<'a> Watchdog<'a> {
fn new(activity: &'a ChannelActivity) -> Self {
Self {
activity,
liveness: CaptureLiveness::new(Instant::now(), activity.seq()),
}
}
fn restart(&mut self) {
self.liveness
.record_activity(Instant::now(), self.activity.seq());
}
}
async fn next_activity(watchdog: Option<&Watchdog<'_>>) -> u64 {
match watchdog {
Some(watchdog) => {
watchdog
.activity
.changed_after(watchdog.liveness.activity_seq())
.await
}
None => std::future::pending().await,
}
}
async fn idle_deadline(watchdog: Option<&Watchdog<'_>>) {
match watchdog {
Some(watchdog) => tokio::time::sleep_until(watchdog.liveness.idle_deadline()).await,
None => std::future::pending().await,
}
}
async fn monitor<A: ArmedCapture>(
context: CaptureMonitor<'_, A>,
wireless: Option<WirelessDeviceStatusFeature>,
shutdown: oneshot::Receiver<()>,
mut device_io: DeviceIoGate,
) -> CaptureStop {
let mut wake_events = wireless.as_ref().map(EmittingFeature::listen);
let mut shutdown = std::pin::pin!(shutdown);
let mut watchdog = context.activity.map(Watchdog::new);
loop {
if !device_io.allows_io() {
if !device_io.wait_until_allowed().await {
return stop_for_current_publication(context.registry, context.shared);
}
if let Some(watchdog) = &mut watchdog {
watchdog.restart();
}
}
tokio::select! {
biased;
allowed = device_io.changed() => {
match allowed {
Some(true) => {
if let Some(watchdog) = &mut watchdog {
watchdog.restart();
}
}
Some(false) => {}
None => return stop_for_current_publication(context.registry, context.shared),
}
}
transition = wait_for_channel_change(
context.registry,
context.shared,
) => {
info!(index = context.device_index, capture = A::NAME, "inventory replaced or removed capture channel — restarting session");
return transition;
}
_ = &mut shutdown => {
return stop_for_current_publication(context.registry, context.shared);
}
event = async {
match wake_events.as_ref() {
Some(events) => events.recv().await.ok(),
None => std::future::pending().await,
}
} => {
let Some(WirelessDeviceStatusEvent::StatusBroadcast(broadcast)) = event else {
wake_events = None;
continue;
};
info!(?broadcast, capture = A::NAME, "device reconnected — re-arming capture");
context.armed.reset_input_state();
tokio::time::sleep(REARM_SETTLE_DELAY).await;
if device_io.allows_io() {
context.armed.rearm().await;
}
}
seq = next_activity(watchdog.as_ref()) => {
if let Some(watchdog) = &mut watchdog {
watchdog.liveness.record_activity(Instant::now(), seq);
}
}
() = idle_deadline(watchdog.as_ref()) => {
let Some(watchdog) = &mut watchdog else {
continue;
};
if !watchdog
.liveness
.ping_due(Instant::now(), watchdog.activity.seq())
{
continue;
}
let outcome = match context.root.ping(0x5a).await {
Err(v20::Hidpp20Error::Channel(
hidpp::channel::ChannelError::Timeout
| hidpp::channel::ChannelError::NoResponse,
)) => PingOutcome::AllSilent,
Ok(_)
| Err(
v20::Hidpp20Error::Feature(_)
| v20::Hidpp20Error::UnsupportedResponse,
) => PingOutcome::Delivered,
Err(_) => PingOutcome::ChannelFailed,
};
if watchdog.liveness.finish_ping(
Instant::now(),
watchdog.activity.seq(),
outcome,
) == LivenessDecision::Restart {
warn!(index = context.device_index, capture = A::NAME, "capture channel stopped delivering — restarting session on a fresh channel");
return stop_for_current_publication(context.registry, context.shared);
}
}
}
}
}