use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::time::{Duration, Instant};
use parking_lot::Mutex;
use crate::error::{Error, Result};
use crate::protocol::command::{
catch_payload, inject_payload, lock_payload, rewrite_payload, transform_payload,
};
use crate::protocol::opcode::{Q_CAPS, Q_CLIP, Q_VERSION};
use crate::protocol::{FrameDecoder, FrameType, PROTO_VER, Resp, encode, parse_resp};
use crate::transport::Transport;
use crate::types::{ClipSettings, ClipStatus, Version};
use super::correlation::HELLO_SEQ;
use super::counters::Counters;
use super::reconcile::DesiredState;
use super::restart::RestartWatch;
use super::slot::TransportSlot;
use super::{Link, write_frame};
const AUTO_RECONNECT_MIN: Duration = Duration::from_millis(100);
const AUTO_RECONNECT_MAX: Duration = Duration::from_secs(2);
const PROBE_DEADLINE: Duration = Duration::from_millis(1200);
const PROBE_QUERY_GAP: Duration = Duration::from_millis(300);
const PROBE_SEQ: u8 = 0x80;
#[derive(Clone, Debug)]
pub(crate) struct BoxIdentity {
pub(crate) serial: Option<String>,
pub(crate) mac: [u8; 6],
}
pub(crate) struct ReconnectCtx {
pub(crate) transport: Arc<TransportSlot>,
pub(crate) write_lock: Arc<Mutex<()>>,
pub(crate) seq: Arc<AtomicU8>,
pub(crate) counters: Arc<Counters>,
pub(crate) desired: Arc<Mutex<DesiredState>>,
pub(crate) reconnect_lock: Arc<Mutex<()>>,
pub(crate) identity: Arc<Mutex<Option<BoxIdentity>>>,
pub(crate) catch_lock: Arc<Mutex<()>>,
pub(crate) held_updates: Arc<Mutex<Vec<Vec<u8>>>>,
pub(crate) updates_rx: flume::Receiver<Vec<u8>>,
pub(crate) restart: Arc<RestartWatch>,
}
pub(crate) fn probe<T>(
transport: &dyn Transport,
what: u8,
read: impl Fn(&[u8]) -> Option<T>,
) -> Option<T> {
probe_seeing_hello(transport, what, read).0
}
fn probe_seeing_hello<T>(
transport: &dyn Transport,
what: u8,
read: impl Fn(&[u8]) -> Option<T>,
) -> (Option<T>, bool) {
let Ok(frame) = encode(FrameType::Query, PROBE_SEQ, &[what]) else {
return (None, false);
};
let mut decoder = FrameDecoder::new();
let start = Instant::now();
let mut last_query: Option<Instant> = None;
let mut found: Option<Option<T>> = None;
let mut hello = false;
let mut rx = [0u8; 256];
while found.is_none() && start.elapsed() < PROBE_DEADLINE {
if last_query.is_none_or(|t| t.elapsed() >= PROBE_QUERY_GAP) {
if transport.write_all(&frame).is_err() {
break;
}
last_query = Some(Instant::now());
}
match transport.read(&mut rx) {
Ok(0) => {}
Ok(n) => decoder.feed(&rx[..n], |f| {
if f.ty != FrameType::Resp {
return;
}
hello |= f.seq == HELLO_SEQ && f.payload.first() == Some(&Q_VERSION);
if found.is_none() && f.payload.first() == Some(&what) {
found = Some(read(&f.payload));
}
}),
Err(_) => break,
}
}
transport.release_read();
(found.flatten(), hello)
}
fn probe_version(transport: &dyn Transport) -> (Option<Version>, bool) {
probe_seeing_hello(transport, Q_VERSION, |p| match parse_resp(p) {
Some(Resp::Version(v)) => Some(v),
_ => None,
})
}
fn probe_caps(transport: &dyn Transport) -> Option<u8> {
probe(transport, Q_CAPS, |p| match parse_resp(p) {
Some(Resp::Caps(c)) => Some(c.mouse.n_buttons),
_ => None,
})
.filter(|&n| n > 0)
}
pub(crate) fn probe_clip(transport: &dyn Transport) -> Option<(ClipStatus, ClipSettings)> {
probe(transport, Q_CLIP, |p| {
Some((ClipStatus::from_payload(p)?, ClipSettings::from_payload(p)?))
})
}
fn reconnect(ctx: &ReconnectCtx) -> Result<()> {
let _guard = ctx.reconnect_lock.lock();
let identity = ctx.identity.lock().clone();
let ports = crate::transport::scan::find_medius();
let candidates: Vec<_> = match &identity {
Some(id) if id.serial.is_some() => {
let matched: Vec<_> = ports
.iter()
.filter(|p| p.serial == id.serial)
.cloned()
.collect();
if matched.is_empty() { ports } else { matched }
}
_ => ports,
};
if candidates.is_empty() {
return Err(Error::NotFound);
}
ctx.transport.swap(Arc::new(crate::transport::Disconnected));
std::thread::sleep(Duration::from_millis(200));
let opened = candidates.into_iter().filter_map(|port| {
let serial =
crate::transport::serial::SerialTransport::open(std::path::Path::new(&port.path))
.ok()?;
Some((port.path, Arc::new(serial) as Arc<dyn Transport>))
});
adopt_first(ctx, identity.as_ref(), opened)
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))] fn adopt_first(
ctx: &ReconnectCtx,
identity: Option<&BoxIdentity>,
opened: impl Iterator<Item = (String, Arc<dyn Transport>)>,
) -> Result<()> {
let mut refused = None;
for (path, port) in opened {
let (version, restarted) = probe_version(&*port);
if let Some(id) = identity
&& version.as_ref().is_none_or(|v| v.mac != id.mac)
{
continue;
}
if let Some(got) = version
.as_ref()
.map(|v| v.proto_ver)
.filter(|&p| p != PROTO_VER)
{
trace_event!(
target: "medius::device",
tracing::Level::WARN,
port = %path,
got,
expected = PROTO_VER,
"reconnect: unsupported protocol version",
);
refused = Some(got);
continue;
}
if let Some(n) = probe_caps(&*port) {
ctx.desired.lock().note_declared_buttons(n);
}
let _reassert = ctx.catch_lock.lock();
let clip = if restarted { None } else { probe_clip(&*port) };
ctx.transport.swap(port);
ctx.held_updates.lock().clear();
while ctx.updates_rx.try_recv().is_ok() {}
ctx.counters.inc_reconnects();
trace_event!(
target: "medius::device",
tracing::Level::INFO,
port = %path,
reason = "rescan",
"reconnected",
);
if restarted {
ctx.restart.begin();
return Ok(());
}
if let Some((status, settings)) = clip {
ctx.desired.lock().clip_adopt(&status, &settings);
}
return reapply_held_locked(ctx);
}
Err(match refused {
Some(got) => Error::BadProtoVer { got },
None => Error::NotFound,
})
}
fn reapply_held(ctx: &ReconnectCtx) -> Result<()> {
let _serial = ctx.catch_lock.lock();
reapply_held_locked(ctx)
}
fn reapply_held_locked(ctx: &ReconnectCtx) -> Result<()> {
reapply_locked(
&ctx.transport,
&ctx.write_lock,
&ctx.seq,
&ctx.counters,
&ctx.desired,
false,
)
}
pub(crate) fn reapply_locked(
transport: &TransportSlot,
write_lock: &Mutex<()>,
seq: &AtomicU8,
counters: &Counters,
desired: &Mutex<DesiredState>,
with_clip: bool,
) -> Result<()> {
let (held, held_locks, catch, rewrites, transforms, clip) = {
let d = desired.lock();
(
d.held().collect::<Vec<_>>(),
d.held_locks(),
d.catch(),
d.held_rewrites(),
d.held_transforms(),
if with_clip {
d.clip_config_frames()
} else {
Vec::new()
},
)
};
let send = |ty: FrameType, payload: &[u8]| {
let seq = seq.fetch_add(1, Ordering::Relaxed);
write_frame(transport, write_lock, counters, seq, ty, payload)
};
for (usage, action) in held {
let (class, id) = usage.class_id();
send(
FrameType::Inject,
&inject_payload(class, id, action.as_u8()),
)?;
}
for ((class, usage, direction), scale) in held_locks {
send(
FrameType::Lock,
&lock_payload(class, usage, direction, scale),
)?;
}
for f in catch.values() {
let (class, id) = f.wire();
send(
FrameType::Catch,
&catch_payload(class, id, f.direction().as_u8(), 1, f.capture().as_u8()),
)?;
}
for r in rewrites {
send(
FrameType::Rewrite,
&rewrite_payload(
r.class,
r.id,
r.direction,
1,
r.action,
r.offset,
&r.match_bytes,
&r.mask,
&r.payload,
),
)?;
}
for t in transforms {
send(
FrameType::Transform,
&transform_payload(t.op, t.sclass, t.sid, t.dclass, t.did, 1),
)?;
}
for (ty, payload) in clip {
send(ty, &payload)?;
}
Ok(())
}
pub(crate) fn auto_reconnect(ctx: &ReconnectCtx, stop: &AtomicBool) {
let mut backoff = AUTO_RECONNECT_MIN;
while !stop.load(Ordering::SeqCst) {
if reconnect(ctx).is_ok() {
return;
}
std::thread::sleep(backoff);
backoff = (backoff * 2).min(AUTO_RECONNECT_MAX);
}
}
impl Link {
fn reconnect_ctx(&self) -> ReconnectCtx {
ReconnectCtx {
transport: Arc::clone(&self.inner.transport),
write_lock: Arc::clone(&self.inner.write_lock),
seq: Arc::clone(&self.inner.seq),
counters: Arc::clone(&self.inner.counters),
desired: Arc::clone(&self.inner.desired),
reconnect_lock: Arc::clone(&self.inner.reconnect_lock),
identity: Arc::clone(&self.inner.identity),
catch_lock: Arc::clone(&self.inner.catch_lock),
held_updates: Arc::clone(&self.inner.held_updates),
updates_rx: self.inner.updates_rx.clone(),
restart: Arc::clone(&self.inner.restart),
}
}
pub(crate) fn set_identity(&self, id: BoxIdentity) {
*self.inner.identity.lock() = Some(id);
}
pub(crate) fn reconnect(&self) -> Result<()> {
reconnect(&self.reconnect_ctx())
}
#[cfg(test)]
pub(crate) fn adopt_reopened(&self, opened: Vec<(String, Arc<dyn Transport>)>) -> Result<()> {
let ctx = self.reconnect_ctx();
let _guard = ctx.reconnect_lock.lock();
let identity = ctx.identity.lock().clone();
adopt_first(&ctx, identity.as_ref(), opened.into_iter())
}
pub(crate) fn reapply(&self) -> Result<()> {
reapply_held(&self.reconnect_ctx())
}
}