use std::{
any::Any,
collections::{HashMap, VecDeque},
sync::{
Arc, Mutex, Weak,
atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering},
},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use futures::{FutureExt, channel::oneshot, select};
use tracing::trace;
use crate::{nibble::U4, sync::lock};
mod error;
mod message;
mod observation;
mod raw;
#[cfg(test)]
pub(crate) mod tests;
pub use error::ChannelError;
pub use message::{
HidppMessage, LONG_REPORT_ID, LONG_REPORT_LENGTH, SHORT_REPORT_ID, SHORT_REPORT_LENGTH,
};
pub use observation::{ChannelObservation, ChannelObserver, ObservedReport, RequestOutcome};
pub use raw::RawHidChannel;
use observation::{RequestObservation, emit_report};
use raw::supports_short_long_hidpp;
const MAX_REPORT_LENGTH: usize = LONG_REPORT_LENGTH;
const MAX_RAW_REPORT_LENGTH: usize = 64;
pub const SEND_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
pub const STALE_REPLY_GRACE: Duration = Duration::from_secs(1);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AbandonedReply {
#[default]
Quarantine,
AdoptIdentical,
}
type MessageListener = Arc<dyn Fn(HidppMessage, bool) + Send + Sync + 'static>;
pub struct MessageListenerGuard {
message_listeners: Weak<Mutex<HashMap<u32, MessageListener>>>,
hdl: u32,
}
impl Drop for MessageListenerGuard {
fn drop(&mut self) {
if let Some(message_listeners) = self.message_listeners.upgrade() {
lock(&message_listeners).remove(&self.hdl);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RequestSwId(U4);
impl RequestSwId {
#[must_use]
pub fn new(id: U4) -> Option<Self> {
(id.to_lo() != 0).then_some(Self(id))
}
#[must_use]
pub fn get(self) -> U4 {
self.0
}
}
pub enum SwIdPolicy {
Fixed(RequestSwId),
Rotating(AtomicU8),
Leased {
id: RequestSwId,
lease: Box<dyn Any + Send + Sync>,
},
}
impl SwIdPolicy {
#[must_use]
pub fn rotating() -> Self {
Self::Rotating(AtomicU8::new(0x01))
}
}
impl Default for SwIdPolicy {
fn default() -> Self {
Self::Fixed(RequestSwId(U4::from_lo(0x01)))
}
}
pub struct HidppChannel {
pub supports_short: bool,
pub supports_long: bool,
pub vendor_id: u16,
pub product_id: u16,
raw_channel: Arc<dyn RawHidChannel>,
observer: Option<Arc<dyn ChannelObserver>>,
sw_id_policy: SwIdPolicy,
pending_messages: Arc<Mutex<PendingQueue>>,
pending_message_id: AtomicU64,
message_listeners: Arc<Mutex<HashMap<u32, MessageListener>>>,
next_listener_hdl: AtomicU32,
read_thread_close: Option<oneshot::Sender<()>>,
read_thread_hdl: Option<JoinHandle<()>>,
}
impl Drop for HidppChannel {
fn drop(&mut self) {
if let Some(read_thread_close) = self.read_thread_close.take() {
let _ = read_thread_close.send(());
}
if let Some(read_thread_hdl) = self.read_thread_hdl.take() {
#[expect(
clippy::unwrap_used,
reason = "propagate a read-thread panic instead of ignoring a crashed background worker"
)]
read_thread_hdl.join().unwrap();
}
}
}
type CorrelationKey = (u8, u8, u8);
#[derive(Default)]
struct PendingQueue {
messages: VecDeque<PendingMessage>,
stale: Vec<StaleKey>,
key_waiters: Vec<oneshot::Sender<()>>,
}
struct StaleKey {
key: CorrelationKey,
request: HidppMessage,
response_predicate: Box<dyn Fn(&HidppMessage) -> bool + Send>,
outstanding: usize,
expires: Instant,
}
enum Wait {
InFlight,
Stale(Instant),
}
impl PendingQueue {
fn try_register(
&mut self,
mut message: PendingMessage,
now: Instant,
) -> Result<(), (PendingMessage, Wait)> {
self.stale.retain(|stale| stale.expires > now);
if self
.messages
.iter()
.any(|pending| pending.key == message.key)
{
return Err((message, Wait::InFlight));
}
let adopts = |stale: &StaleKey| {
message.abandoned == AbandonedReply::AdoptIdentical && stale.request == message.request
};
if let Some(until) = self
.stale
.iter()
.filter(|stale| stale.key == message.key && !adopts(stale))
.map(|stale| stale.expires)
.max()
{
return Err((message, Wait::Stale(until)));
}
message.extra_replies = self
.stale
.iter()
.filter(|stale| stale.key == message.key)
.map(|stale| stale.outstanding)
.sum();
self.stale.retain(|stale| stale.key != message.key);
self.messages.push_back(message);
Ok(())
}
fn abandon(&mut self, id: u64, now: Instant) {
let Some(pos) = self.messages.iter().position(|message| message.id == id) else {
return;
};
let Some(abandoned) = self.messages.remove(pos) else {
return;
};
let outstanding = abandoned.extra_replies + 1;
self.owe_replies(abandoned, outstanding, now);
self.wake_key_waiters();
}
fn owe_replies(&mut self, message: PendingMessage, outstanding: usize, now: Instant) {
let PendingMessage {
key,
request,
response_predicate,
..
} = message;
self.stale.push(StaleKey {
key,
request,
response_predicate,
outstanding,
expires: now + STALE_REPLY_GRACE,
});
}
fn discard_stale_reply(&mut self, msg: &HidppMessage) -> bool {
let Some(pos) = self
.stale
.iter()
.position(|stale| (stale.response_predicate)(msg))
else {
return false;
};
self.stale[pos].outstanding -= 1;
if self.stale[pos].outstanding == 0 {
self.stale.remove(pos);
self.wake_key_waiters();
}
true
}
fn wake_key_waiters(&mut self) {
for waiter in self.key_waiters.drain(..) {
let _ = waiter.send(());
}
}
}
struct PendingMessage {
id: u64,
key: CorrelationKey,
request: HidppMessage,
abandoned: AbandonedReply,
response_predicate: Box<dyn Fn(&HidppMessage) -> bool + Send>,
sender: oneshot::Sender<HidppMessage>,
extra_replies: usize,
}
struct PendingRequest {
id: u64,
pending_messages: Arc<Mutex<PendingQueue>>,
receiver: oneshot::Receiver<HidppMessage>,
}
impl PendingRequest {
async fn register_when_key_free(
id: u64,
pending_messages: Arc<Mutex<PendingQueue>>,
request: HidppMessage,
abandoned: AbandonedReply,
response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
) -> Self {
let (sender, receiver) = oneshot::channel();
let key = request.header();
let mut message = PendingMessage {
id,
key,
request,
abandoned,
response_predicate: Box::new(response_predicate),
sender,
extra_replies: 0,
};
loop {
let now = Instant::now();
let (parked, stale_until) = {
let mut queue = lock(&pending_messages);
let stale_until = match queue.try_register(message, now) {
Ok(()) => break,
Err((returned, Wait::InFlight)) => {
message = returned;
None
}
Err((returned, Wait::Stale(expires))) => {
message = returned;
Some(expires)
}
};
let (wake, parked) = oneshot::channel();
queue.key_waiters.push(wake);
(parked, stale_until)
};
let (dev, feat, func) = key;
match stale_until {
None => {
trace!(
dev,
feat, func, "hidpp request parked — same header in flight"
);
let _ = parked.await;
}
Some(expires) => {
trace!(
dev,
feat,
func,
"hidpp request parked — replies with its header are still outstanding"
);
let mut parked = parked.fuse();
let mut grace =
futures_timer::Delay::new(expires.saturating_duration_since(now)).fuse();
select! {
_ = parked => {}
() = grace => {}
}
}
}
}
Self {
id,
pending_messages,
receiver,
}
}
async fn receive(mut self) -> Option<HidppMessage> {
(&mut self.receiver).await.ok()
}
}
impl Drop for PendingRequest {
fn drop(&mut self) {
lock(&self.pending_messages).abandon(self.id, Instant::now());
}
}
enum PendingRequestCompletion {
Response(HidppMessage),
WriteFailed(ChannelError),
NoResponse,
TimedOut,
}
fn finish_request(
completion: PendingRequestCompletion,
mut observation: RequestObservation<'_>,
dev: u8,
feat: u8,
) -> Result<HidppMessage, ChannelError> {
match completion {
PendingRequestCompletion::Response(response) => {
observation.complete(RequestOutcome::Succeeded);
trace!(dev, feat, "hidpp response");
Ok(response)
}
PendingRequestCompletion::WriteFailed(error) => {
observation.complete(RequestOutcome::WriteFailed);
trace!(dev, feat, error = ?error, "hidpp no response");
Err(error)
}
PendingRequestCompletion::NoResponse => {
observation.complete(RequestOutcome::NoResponse);
trace!(dev, feat, error = ?ChannelError::NoResponse, "hidpp no response");
Err(ChannelError::NoResponse)
}
PendingRequestCompletion::TimedOut => {
observation.complete(RequestOutcome::TimedOut);
trace!(dev, feat, error = ?ChannelError::Timeout, "hidpp no response");
Err(ChannelError::Timeout)
}
}
}
impl HidppChannel {
pub async fn from_raw_channel(raw: impl RawHidChannel) -> Result<Self, ChannelError> {
Self::from_raw_channel_inner(raw, None).await
}
pub async fn from_raw_channel_with_observer(
raw: impl RawHidChannel,
observer: Arc<dyn ChannelObserver>,
) -> Result<Self, ChannelError> {
Self::from_raw_channel_inner(raw, Some(observer)).await
}
async fn from_raw_channel_inner(
raw: impl RawHidChannel,
observer: Option<Arc<dyn ChannelObserver>>,
) -> Result<Self, ChannelError> {
let (supports_short, supports_long) = supports_short_long_hidpp(&raw).await?;
if !supports_short && !supports_long {
return Err(ChannelError::HidppNotSupported);
}
let raw_channel_rc = Arc::new(raw);
let pending_messages_rc = Arc::new(Mutex::new(PendingQueue::default()));
let message_listeners_rc = Arc::new(Mutex::new(HashMap::<u32, MessageListener>::new()));
let (close_sender, close_receiver) = oneshot::channel::<()>();
let read_thread_hdl = thread::spawn({
let raw_channel = Arc::clone(&raw_channel_rc);
let pending_messages = Arc::clone(&pending_messages_rc);
let message_listeners = Arc::clone(&message_listeners_rc);
let observer = observer.clone();
move || {
futures::executor::block_on(read_loop(
&*raw_channel,
&pending_messages,
&message_listeners,
observer.as_deref(),
close_receiver,
));
}
});
Ok(Self {
supports_short,
supports_long,
vendor_id: raw_channel_rc.vendor_id(),
product_id: raw_channel_rc.product_id(),
raw_channel: raw_channel_rc,
observer,
sw_id_policy: SwIdPolicy::default(),
pending_messages: pending_messages_rc,
pending_message_id: AtomicU64::new(1),
next_listener_hdl: AtomicU32::new(1),
message_listeners: message_listeners_rc,
read_thread_close: Some(close_sender),
read_thread_hdl: Some(read_thread_hdl),
})
}
pub fn is_connected(&self) -> bool {
self.raw_channel.is_connected()
}
pub fn set_sw_id_policy(&mut self, policy: SwIdPolicy) {
self.sw_id_policy = policy;
}
pub fn get_sw_id(&self) -> U4 {
match &self.sw_id_policy {
SwIdPolicy::Fixed(id) | SwIdPolicy::Leased { id, .. } => id.get(),
SwIdPolicy::Rotating(counter) => {
let previous =
match counter.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |old| {
Some(if old & 0x0f == 0x0f {
0x01
} else {
old.wrapping_add(1)
})
}) {
Ok(previous) | Err(previous) => previous,
};
U4::from_lo(previous)
}
}
}
pub fn supports_msg(&self, msg: &HidppMessage) -> bool {
match msg {
HidppMessage::Short(_) => self.supports_short,
HidppMessage::Long(_) => self.supports_long,
}
}
fn normalize_outgoing(&self, msg: HidppMessage) -> HidppMessage {
match msg {
HidppMessage::Short(_) if !self.supports_short && self.supports_long => msg.widened(),
other => other,
}
}
pub async fn send(
&self,
msg: HidppMessage,
response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
) -> Result<HidppMessage, ChannelError> {
self.send_with_timeout(msg, response_predicate, SEND_RESPONSE_TIMEOUT)
.await
}
pub async fn send_with_timeout(
&self,
msg: HidppMessage,
response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
timeout: Duration,
) -> Result<HidppMessage, ChannelError> {
self.send_with(msg, response_predicate, timeout, AbandonedReply::Quarantine)
.await
}
pub async fn send_with(
&self,
msg: HidppMessage,
response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
timeout: Duration,
abandoned: AbandonedReply,
) -> Result<HidppMessage, ChannelError> {
let msg = self.normalize_outgoing(msg);
if !self.supports_msg(&msg) {
return Err(ChannelError::MessageTypeNotSupported);
}
let (dev, feat, func) = msg.header();
trace!(dev, feat, func, "hidpp request");
let pending_id = self.pending_message_id.fetch_add(1, Ordering::SeqCst);
let observation = RequestObservation::new(self.observer.as_deref(), pending_id);
let completion = {
let mut request = std::pin::pin!(
async move {
let pending_request = PendingRequest::register_when_key_free(
pending_id,
Arc::clone(&self.pending_messages),
msg,
abandoned,
response_predicate,
)
.await;
if let Err(error) = self.write_hidpp_report(msg, Some(pending_id)).await {
return PendingRequestCompletion::WriteFailed(error);
}
pending_request.receive().await.map_or(
PendingRequestCompletion::NoResponse,
PendingRequestCompletion::Response,
)
}
.fuse()
);
select! {
completion = request => completion,
() = futures_timer::Delay::new(timeout).fuse() => PendingRequestCompletion::TimedOut,
}
};
finish_request(completion, observation, dev, feat)
}
pub async fn send_write_through(
&self,
msg: HidppMessage,
response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
response_timeout: Duration,
) -> Result<HidppMessage, ChannelError> {
let msg = self.normalize_outgoing(msg);
if !self.supports_msg(&msg) {
return Err(ChannelError::MessageTypeNotSupported);
}
let (dev, feat, func) = msg.header();
trace!(dev, feat, func, "hidpp request");
let pending_id = self.pending_message_id.fetch_add(1, Ordering::SeqCst);
let observation = RequestObservation::new(self.observer.as_deref(), pending_id);
let pending_request = PendingRequest::register_when_key_free(
pending_id,
Arc::clone(&self.pending_messages),
msg,
AbandonedReply::Quarantine,
response_predicate,
)
.await;
let completion = if let Err(error) = self.write_hidpp_report(msg, Some(pending_id)).await {
drop(pending_request);
PendingRequestCompletion::WriteFailed(error)
} else {
let mut response = std::pin::pin!(pending_request.receive().fuse());
select! {
response = response => response.map_or(
PendingRequestCompletion::NoResponse,
PendingRequestCompletion::Response,
),
() = futures_timer::Delay::new(response_timeout).fuse() => {
PendingRequestCompletion::TimedOut
},
}
};
finish_request(completion, observation, dev, feat)
}
pub async fn send_and_forget(&self, msg: HidppMessage) -> Result<(), ChannelError> {
let msg = self.normalize_outgoing(msg);
if !self.supports_msg(&msg) {
return Err(ChannelError::MessageTypeNotSupported);
}
self.write_hidpp_report(msg, None).await
}
async fn write_hidpp_report(
&self,
msg: HidppMessage,
request_id: Option<u64>,
) -> Result<(), ChannelError> {
let mut buf = [0u8; LONG_REPORT_LENGTH];
let len = msg.write_raw(&mut buf);
emit_report(self.observer.as_deref(), &buf[..len], |report| {
ChannelObservation::OutgoingReport { request_id, report }
});
self.raw_channel
.write_report(&buf[..len])
.await
.map(|_| ())
.map_err(ChannelError::Implementation)
}
pub async fn write_raw_report(&self, report: &[u8]) -> Result<usize, ChannelError> {
self.write_raw_report_with_timeout(report, SEND_RESPONSE_TIMEOUT)
.await
}
async fn write_raw_report_with_timeout(
&self,
report: &[u8],
timeout: Duration,
) -> Result<usize, ChannelError> {
if !(1..=MAX_RAW_REPORT_LENGTH).contains(&report.len()) {
return Err(ChannelError::InvalidRawReportLength(report.len()));
}
emit_report(self.observer.as_deref(), report, |report| {
ChannelObservation::OutgoingReport {
request_id: None,
report,
}
});
let mut write = std::pin::pin!(self.raw_channel.write_report(report).fuse());
select! {
result = write => result.map_err(ChannelError::Implementation),
() = futures_timer::Delay::new(timeout).fuse() => Err(ChannelError::Timeout),
}
}
pub fn add_msg_listener(
&self,
listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
) -> u32 {
let hdl = self.next_listener_hdl.fetch_add(1, Ordering::Relaxed);
lock(&self.message_listeners).insert(hdl, Arc::new(listener));
hdl
}
pub fn add_msg_listener_guarded(
&self,
listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
) -> MessageListenerGuard {
let hdl = self.add_msg_listener(listener);
MessageListenerGuard {
message_listeners: Arc::downgrade(&self.message_listeners),
hdl,
}
}
pub fn remove_msg_listener(&self, hdl: u32) -> bool {
lock(&self.message_listeners).remove(&hdl).is_some()
}
}
async fn read_loop(
raw_channel: &dyn RawHidChannel,
pending_messages: &Mutex<PendingQueue>,
message_listeners: &Mutex<HashMap<u32, MessageListener>>,
observer: Option<&dyn ChannelObserver>,
mut close: oneshot::Receiver<()>,
) {
let mut buf = [0u8; MAX_REPORT_LENGTH];
loop {
let res = select! {
_ = close => break,
res = raw_channel.read_report(&mut buf).fuse() => res,
};
let len = match res {
Ok(len) => len,
Err(error) => {
trace!(?error, "read_report error");
continue;
}
};
let Some(msg) = HidppMessage::read_raw(&buf[..len]) else {
emit_report(observer, &buf[..len], |report| {
ChannelObservation::MalformedIncomingReport { report }
});
trace!(len, "report not HID++ — dropped");
continue;
};
let mut matched_id = None;
let mut stale = false;
let pending_count;
{
let mut queue = lock(pending_messages);
pending_count = queue.messages.len();
if let Some(answered) = queue
.messages
.iter()
.position(|elem| (elem.response_predicate)(&msg))
.and_then(|pos| queue.messages.remove(pos))
{
matched_id = Some(answered.id);
let PendingMessage {
key,
request,
response_predicate,
sender,
extra_replies,
..
} = answered;
let _ = sender.send(msg);
if extra_replies > 0 {
queue.stale.push(StaleKey {
key,
request,
response_predicate,
outstanding: extra_replies,
expires: Instant::now() + STALE_REPLY_GRACE,
});
}
queue.wake_key_waiters();
} else {
stale = queue.discard_stale_reply(&msg);
}
}
emit_report(observer, &buf[..len], |report| {
ChannelObservation::IncomingReport {
request_id: matched_id,
report,
}
});
trace!(
len,
matched = matched_id.is_some(),
stale,
pending_count,
payload = format_args!("{:02x?}", &buf[..len.min(16)]),
"raw report received"
);
let listeners: Vec<_> = lock(message_listeners).values().cloned().collect();
for listener in listeners {
listener(msg, matched_id.is_some());
}
}
}