use std::{
any::Any,
collections::HashMap,
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 pending;
mod raw;
#[cfg(test)]
pub(crate) mod mock;
#[cfg(test)]
mod tests;
pub use error::ChannelError;
pub use message::{
HidppMessage, LONG_REPORT_ID, LONG_REPORT_LENGTH, SHORT_REPORT_ID, SHORT_REPORT_LENGTH,
is_hidpp_report_id,
};
pub use observation::{ChannelObservation, ChannelObserver, ObservedReport, RequestOutcome};
pub use raw::RawHidChannel;
use observation::{RequestObservation, emit_report};
use pending::{
PendingMessage, PendingQueue, PendingRequest, PendingRequestCompletion, StaleKey,
finish_request,
};
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();
}
}
}
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 = counter.update(Ordering::SeqCst, Ordering::SeqCst, |old| {
if old & 0x0f == 0x0f {
0x01
} else {
old.wrapping_add(1)
}
});
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 {
let foreign = buf[..len]
.first()
.is_some_and(|id| !is_hidpp_report_id(*id));
emit_report(observer, &buf[..len], |report| {
if foreign {
ChannelObservation::ForeignIncomingReport { report }
} else {
ChannelObservation::MalformedIncomingReport { report }
}
});
trace!(len, foreign, "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());
}
}
}