hidpp/channel.rs
1//! Implements basic messaging across HID and HID++ channels.
2//!
3//! This includes mapping incoming messages to previously sent requests. A
4//! reply is matched to the oldest pending request whose predicate accepts it,
5//! and every predicate keys on the report's first three bytes — device,
6//! feature (or HID++1.0 sub id), function and software id (or register
7//! address). Two requests sharing those bytes therefore get replies nothing on
8//! the wire can tell apart, and a wireless receiver may answer them out of
9//! order (a retransmitted radio packet completes after a later one). The
10//! channel never lets that happen: a request whose key is already in flight
11//! waits until that request is answered — or, when it timed out or was
12//! cancelled unanswered, until its reply lands and is discarded or
13//! [`STALE_REPLY_GRACE`] passes without one. That holds even for a request
14//! that re-asks the abandoned one byte for byte — the same question does not
15//! promise the same answer once a write has gone between them — unless the
16//! caller says otherwise with [`AbandonedReply::AdoptIdentical`], which only
17//! a query of immutable state may.
18
19use std::{
20 any::Any,
21 collections::HashMap,
22 sync::{
23 Arc, Mutex, Weak,
24 atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering},
25 },
26 thread::{self, JoinHandle},
27 time::{Duration, Instant},
28};
29
30use futures::{FutureExt, channel::oneshot, select};
31use tracing::trace;
32
33use crate::{nibble::U4, sync::lock};
34
35mod error;
36mod message;
37mod observation;
38mod pending;
39mod raw;
40
41#[cfg(test)]
42pub(crate) mod mock;
43#[cfg(test)]
44mod tests;
45
46pub use error::ChannelError;
47pub use message::{
48 HidppMessage, LONG_REPORT_ID, LONG_REPORT_LENGTH, SHORT_REPORT_ID, SHORT_REPORT_LENGTH,
49 is_hidpp_report_id,
50};
51pub use observation::{ChannelObservation, ChannelObserver, ObservedReport, RequestOutcome};
52pub use raw::RawHidChannel;
53
54use observation::{RequestObservation, emit_report};
55use pending::{
56 PendingMessage, PendingQueue, PendingRequest, PendingRequestCompletion, StaleKey,
57 finish_request,
58};
59use raw::supports_short_long_hidpp;
60
61/// This is the size of the buffer incoming reports are read into.
62/// As we only care about HID++ reports, this equals to [`LONG_REPORT_LENGTH`].
63const MAX_REPORT_LENGTH: usize = LONG_REPORT_LENGTH;
64
65/// Largest output report accepted by [`HidppChannel::write_raw_report`].
66/// Logitech's very-long HID++ lighting report (`0x12`) is 64 bytes.
67const MAX_RAW_REPORT_LENGTH: usize = 64;
68
69/// The default time budget for a [`HidppChannel::send`] request: the report
70/// write plus the wait for a matching response. Callers that need a different
71/// budget can use [`HidppChannel::send_with_timeout`].
72pub const SEND_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
73
74/// How long a request abandoned unanswered — timed out, or cancelled by an
75/// outer deadline — keeps its reply header reserved. A reply that lands inside
76/// this window is discarded; one that never comes frees the header when the
77/// window closes. Without the reservation a late reply would answer the next
78/// request with the same header, which nothing on the wire can distinguish.
79///
80/// One second covers the late replies seen in practice: a receiver answering a
81/// register read a few hundred milliseconds after a tight probe budget gave up
82/// on it. It is deliberately much shorter than [`SEND_RESPONSE_TIMEOUT`]: a
83/// device that never answers costs the next same-header request one grace
84/// window, not a whole timeout, and a reply later than the grace is as
85/// unattributable as it always was.
86///
87/// The quarantine makes no exception of its own for a request that re-asks
88/// the abandoned one byte for byte. The bytes say what was asked, not what
89/// the answer is: a DPI read abandoned before its reply, a DPI write
90/// acknowledged, and the read re-asked all fit inside one grace window, and
91/// the first read's late reply would hand the re-ask the value from before
92/// the write. A caller whose query cannot be changed by any write may opt in
93/// with [`AbandonedReply::AdoptIdentical`].
94pub const STALE_REPLY_GRACE: Duration = Duration::from_secs(1);
95
96/// What a request does about a reply still owed to an abandoned request with
97/// the same header — see [`STALE_REPLY_GRACE`].
98#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
99pub enum AbandonedReply {
100 /// Wait for it to land and be discarded, or for the grace to pass. The
101 /// default, and the only safe choice for a query whose answer a write
102 /// could have changed since the abandoned ask.
103 #[default]
104 Quarantine,
105 /// Take it as this request's own answer, provided the abandoned request
106 /// was byte-identical; the request goes out at once and is answered by
107 /// whichever reply comes first, the other being discarded on arrival.
108 /// The caller vouches that nothing can have changed the answer between
109 /// the two asks — a lookup in a device's feature table, which is fixed
110 /// for the life of the connection. A re-ask then costs no grace wait,
111 /// which is what keeps a feature walk over a lossy link moving.
112 AdoptIdentical,
113}
114
115type MessageListener = Arc<dyn Fn(HidppMessage, bool) + Send + Sync + 'static>;
116
117/// Removes a HID++ message listener when dropped.
118pub struct MessageListenerGuard {
119 message_listeners: Weak<Mutex<HashMap<u32, MessageListener>>>,
120 hdl: u32,
121}
122
123impl Drop for MessageListenerGuard {
124 fn drop(&mut self) {
125 if let Some(message_listeners) = self.message_listeners.upgrade() {
126 lock(&message_listeners).remove(&self.hdl);
127 }
128 }
129}
130
131/// A software id a request may carry: `1..=15`.
132///
133/// Id `0` is the wire's device-notification marker (event decoding treats
134/// `software_id == 0` as "not a response"), so a request sent with it would
135/// have its response indistinguishable from an event — made unrepresentable
136/// here by construction.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct RequestSwId(U4);
139
140impl RequestSwId {
141 /// The id as a request software id, or `None` for the reserved id `0`.
142 #[must_use]
143 pub fn new(id: U4) -> Option<Self> {
144 (id.to_lo() != 0).then_some(Self(id))
145 }
146
147 /// The nibble the wire carries.
148 #[must_use]
149 pub fn get(self) -> U4 {
150 self.0
151 }
152}
153
154/// How the channel assigns the software id each outgoing request carries.
155///
156/// One value instead of three cooperating fields (a rotate flag, the current
157/// id, an optional lease): the triple admitted states the wire cannot mean —
158/// rotation walking over ids other channels hold leases on, or a leased id
159/// different from the id actually sent. Constructed whole, those states are
160/// unrepresentable.
161pub enum SwIdPolicy {
162 /// Every request carries the same id.
163 Fixed(RequestSwId),
164 /// Walk `1..=15`, one id per request — eases mapping responses to
165 /// requests for a single exclusive user of a node. The counter is this
166 /// policy's own; the id `0` slot is skipped in the wrap.
167 Rotating(AtomicU8),
168 /// A fixed id held for the channel's lifetime, so concurrent opens of one
169 /// HID node never share a correlation id. The `lease` is whatever the
170 /// allocator hands out to back it — an entry in a table, an OS file lock —
171 /// and is dropped with the policy, which is when the id goes back.
172 /// (OpenLogi local addition.)
173 Leased {
174 /// The leased id every request carries.
175 id: RequestSwId,
176 /// Owns the lease; dropping it returns the id to the allocator.
177 lease: Box<dyn Any + Send + Sync>,
178 },
179}
180
181impl SwIdPolicy {
182 /// A fresh rotation, starting at id `1`.
183 #[must_use]
184 pub fn rotating() -> Self {
185 Self::Rotating(AtomicU8::new(0x01))
186 }
187}
188
189impl Default for SwIdPolicy {
190 /// Fixed id `1`, matching the protocol's conventional default.
191 fn default() -> Self {
192 Self::Fixed(RequestSwId(U4::from_lo(0x01)))
193 }
194}
195
196/// Represents a HID communication channel supporting HID++.
197pub struct HidppChannel {
198 /// Whether the channel supports short (7 bytes) HID++ messages.
199 pub supports_short: bool,
200
201 /// Whether the channel supports long (20 bytes) HID++ messages.
202 pub supports_long: bool,
203
204 /// The vendor ID of the connected HID device.
205 pub vendor_id: u16,
206
207 /// The product ID of the connected HID device.
208 pub product_id: u16,
209
210 /// The underlying raw HID channel.
211 raw_channel: Arc<dyn RawHidChannel>,
212
213 /// Optional sink for reports and request lifecycle events.
214 observer: Option<Arc<dyn ChannelObserver>>,
215
216 /// The software-id policy for outgoing requests (see [`SwIdPolicy`]).
217 ///
218 /// This must remain after `raw_channel`: fields drop in declaration order,
219 /// so the final lease is returned only after channel shutdown has joined
220 /// the read thread and released the raw transport.
221 sw_id_policy: SwIdPolicy,
222
223 /// All sent messages that are waiting for a response, and the requests
224 /// parked behind one that shares their header.
225 pending_messages: Arc<Mutex<PendingQueue>>,
226
227 /// The request ID assigned to the next pending message.
228 pending_message_id: AtomicU64,
229
230 /// Registered listeners that will receive notifications about incoming
231 /// messages.
232 message_listeners: Arc<Mutex<HashMap<u32, MessageListener>>>,
233
234 /// The handle assigned to the next registered message listener.
235 ///
236 /// A counter, not a random draw: a handle is only ever a key into
237 /// [`Self::message_listeners`], so counting up is collision-free by
238 /// construction where drawing needed a retry loop to be merely unlikely.
239 next_listener_hdl: AtomicU32,
240
241 /// The sender signaling the read thread to stop.
242 read_thread_close: Option<oneshot::Sender<()>>,
243
244 /// The handle to the read thread. Should be joined after signaling
245 /// [`Self::read_thread_close`].
246 read_thread_hdl: Option<JoinHandle<()>>,
247}
248
249impl Drop for HidppChannel {
250 fn drop(&mut self) {
251 if let Some(read_thread_close) = self.read_thread_close.take() {
252 // This only fails if the receiving end, which is owned by the read thread in
253 // this case, is dropped.
254 // This just means that the read thread is already stopped, so we can ignore the
255 // error here.
256 let _ = read_thread_close.send(());
257 }
258
259 if let Some(read_thread_hdl) = self.read_thread_hdl.take() {
260 // Joining is not politeness: together with the subsequent
261 // `raw_channel` field drop, it makes the OS handle close before the
262 // software-id lease is returned. A caller can therefore drop a
263 // channel and reopen the same node without overlapping lifetimes.
264 #[expect(
265 clippy::unwrap_used,
266 reason = "propagate a read-thread panic instead of ignoring a crashed background worker"
267 )]
268 read_thread_hdl.join().unwrap();
269 }
270 }
271}
272
273impl HidppChannel {
274 /// Tries to construct a HID++ channel from a raw HID channel.
275 ///
276 /// Reads run on a thread this owns, joined on drop — so the underlying OS
277 /// handle is closed by the time a dropped channel's `drop` returns. Callers
278 /// that reopen the same node right after closing one depend on that.
279 ///
280 /// If the given HID channel does not support HID++,
281 /// [`ChannelError::HidppNotSupported`] will be returned.
282 pub async fn from_raw_channel(raw: impl RawHidChannel) -> Result<Self, ChannelError> {
283 Self::from_raw_channel_inner(raw, None).await
284 }
285
286 /// Tries to construct a HID++ channel that reports wire and request facts
287 /// to `observer`.
288 ///
289 /// This has the same channel behavior and failure contract as
290 /// [`Self::from_raw_channel`]. Observation is best enabled at construction
291 /// so incoming reports cannot race observer installation.
292 pub async fn from_raw_channel_with_observer(
293 raw: impl RawHidChannel,
294 observer: Arc<dyn ChannelObserver>,
295 ) -> Result<Self, ChannelError> {
296 Self::from_raw_channel_inner(raw, Some(observer)).await
297 }
298
299 async fn from_raw_channel_inner(
300 raw: impl RawHidChannel,
301 observer: Option<Arc<dyn ChannelObserver>>,
302 ) -> Result<Self, ChannelError> {
303 let (supports_short, supports_long) = supports_short_long_hidpp(&raw).await?;
304
305 if !supports_short && !supports_long {
306 return Err(ChannelError::HidppNotSupported);
307 }
308
309 let raw_channel_rc = Arc::new(raw);
310 let pending_messages_rc = Arc::new(Mutex::new(PendingQueue::default()));
311 let message_listeners_rc = Arc::new(Mutex::new(HashMap::<u32, MessageListener>::new()));
312
313 let (close_sender, close_receiver) = oneshot::channel::<()>();
314
315 let read_thread_hdl = thread::spawn({
316 let raw_channel = Arc::clone(&raw_channel_rc);
317 let pending_messages = Arc::clone(&pending_messages_rc);
318 let message_listeners = Arc::clone(&message_listeners_rc);
319 let observer = observer.clone();
320
321 move || {
322 futures::executor::block_on(read_loop(
323 &*raw_channel,
324 &pending_messages,
325 &message_listeners,
326 observer.as_deref(),
327 close_receiver,
328 ));
329 }
330 });
331
332 Ok(Self {
333 supports_short,
334 supports_long,
335 vendor_id: raw_channel_rc.vendor_id(),
336 product_id: raw_channel_rc.product_id(),
337 raw_channel: raw_channel_rc,
338 observer,
339 sw_id_policy: SwIdPolicy::default(),
340 pending_messages: pending_messages_rc,
341 pending_message_id: AtomicU64::new(1),
342 next_listener_hdl: AtomicU32::new(1),
343 message_listeners: message_listeners_rc,
344 read_thread_close: Some(close_sender),
345 read_thread_hdl: Some(read_thread_hdl),
346 })
347 }
348
349 /// Whether the underlying HID transport still reports a live connection.
350 pub fn is_connected(&self) -> bool {
351 self.raw_channel.is_connected()
352 }
353
354 /// Replace the software-id policy for outgoing requests.
355 ///
356 /// `&mut self` on purpose: the policy is decided while the channel is
357 /// still exclusively owned (right after opening, before it is shared), so
358 /// no request can race a policy change. Replacing a [`SwIdPolicy::Leased`]
359 /// policy returns its lease before this method returns. The channel's final
360 /// lease is returned only after its read thread and raw transport stop.
361 pub fn set_sw_id_policy(&mut self, policy: SwIdPolicy) {
362 self.sw_id_policy = policy;
363 }
364
365 /// Provides a software ID that can be used to send a HID++ message across
366 /// the channel.
367 ///
368 /// This method should be called separately for every message to send, as a
369 /// [`SwIdPolicy::Rotating`] policy advances per call.
370 pub fn get_sw_id(&self) -> U4 {
371 match &self.sw_id_policy {
372 SwIdPolicy::Fixed(id) | SwIdPolicy::Leased { id, .. } => id.get(),
373 SwIdPolicy::Rotating(counter) => {
374 let previous = counter.update(Ordering::SeqCst, Ordering::SeqCst, |old| {
375 if old & 0x0f == 0x0f {
376 0x01
377 } else {
378 old.wrapping_add(1)
379 }
380 });
381 U4::from_lo(previous)
382 }
383 }
384 }
385
386 /// Checks whether the channel supports the given HID++ message.
387 pub fn supports_msg(&self, msg: &HidppMessage) -> bool {
388 match msg {
389 HidppMessage::Short(_) => self.supports_short,
390 HidppMessage::Long(_) => self.supports_long,
391 }
392 }
393
394 /// Re-frames a short message as long on a long-only channel — a device that
395 /// exposes only the long HID++ report (e.g. a Bluetooth-LE-direct mouse on
396 /// macOS, where `IOHIDDeviceSetReport` rejects the short report). The HID++
397 /// header bytes sit at the same offsets in both widths, so the only change
398 /// is the report id plus zero-padding the extra payload; the device answers
399 /// with a long report, which still matches the request by header. A no-op on
400 /// channels that advertise short support.
401 ///
402 /// (OpenLogi local addition — candidate for upstreaming.)
403 fn normalize_outgoing(&self, msg: HidppMessage) -> HidppMessage {
404 match msg {
405 HidppMessage::Short(_) if !self.supports_short && self.supports_long => msg.widened(),
406 other => other,
407 }
408 }
409
410 /// Sends a HID++ message across the channel and waits for a response.
411 ///
412 /// If no response is expected/required, use [`Self::send_and_forget`].
413 ///
414 /// The whole request — the report write plus the wait for a matching
415 /// response — is bounded by [`SEND_RESPONSE_TIMEOUT`]; the future resolves
416 /// to [`ChannelError::Timeout`] on elapse. Use [`Self::send_with_timeout`]
417 /// to choose a different budget.
418 pub async fn send(
419 &self,
420 msg: HidppMessage,
421 response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
422 ) -> Result<HidppMessage, ChannelError> {
423 self.send_with_timeout(msg, response_predicate, SEND_RESPONSE_TIMEOUT)
424 .await
425 }
426
427 /// Sends a HID++ message across the channel and waits for a response,
428 /// bounding the whole request — the report write plus the wait for a
429 /// matching response — by `timeout`.
430 ///
431 /// On elapse the request's pending entry is removed (concurrent in-flight
432 /// requests are unaffected) and [`ChannelError::Timeout`] is returned; a
433 /// response that still arrives later reaches message listeners as an
434 /// unmatched message.
435 ///
436 /// A request whose header — device, feature, function and software id —
437 /// is already in flight goes out only once that request is answered:
438 /// their replies would be indistinguishable, and a receiver may deliver
439 /// them out of order. If that request timed out or was cancelled
440 /// unanswered, its reply is still expected: the header stays reserved
441 /// until the reply lands and is discarded, or for [`STALE_REPLY_GRACE`]
442 /// at most — a byte-identical re-ask included, since a write may have gone
443 /// between the two asks. The wait counts against `timeout`. A query of
444 /// immutable state can adopt that reply instead through [`Self::send_with`].
445 ///
446 /// [`Self::send`] uses this with [`SEND_RESPONSE_TIMEOUT`], which suits
447 /// requests to a device that may be asleep. Requests that should fail
448 /// faster — e.g. probing a receiver that answers immediately or not at
449 /// all — can pass a tighter budget.
450 pub async fn send_with_timeout(
451 &self,
452 msg: HidppMessage,
453 response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
454 timeout: Duration,
455 ) -> Result<HidppMessage, ChannelError> {
456 self.send_with(msg, response_predicate, timeout, AbandonedReply::Quarantine)
457 .await
458 }
459
460 /// [`Self::send_with_timeout`], choosing what to do about a reply still
461 /// owed to an abandoned request with the same header. Only a query whose
462 /// answer no write can change should pass
463 /// [`AbandonedReply::AdoptIdentical`].
464 pub async fn send_with(
465 &self,
466 msg: HidppMessage,
467 response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
468 timeout: Duration,
469 abandoned: AbandonedReply,
470 ) -> Result<HidppMessage, ChannelError> {
471 let msg = self.normalize_outgoing(msg);
472 if !self.supports_msg(&msg) {
473 return Err(ChannelError::MessageTypeNotSupported);
474 }
475
476 // Wire trace (off by default; `OPENLOGI_LOG=hidpp=trace`). Capture the
477 // header before `msg` is moved into the send future so the outcome line
478 // below can name the same request.
479 let (dev, feat, func) = msg.header();
480 trace!(dev, feat, func, "hidpp request");
481
482 let pending_id = self.pending_message_id.fetch_add(1, Ordering::SeqCst);
483 let observation = RequestObservation::new(self.observer.as_deref(), pending_id);
484
485 // The deadline covers the wait for an identical in-flight header and
486 // the write as well: `write_report` has no bounded-time contract of its
487 // own, so a wedged device could otherwise park `send` forever before
488 // the response wait even starts.
489 let completion = {
490 let mut request = std::pin::pin!(
491 async move {
492 let pending_request = PendingRequest::register_when_key_free(
493 pending_id,
494 Arc::clone(&self.pending_messages),
495 msg,
496 abandoned,
497 response_predicate,
498 )
499 .await;
500 if let Err(error) = self.write_hidpp_report(msg, Some(pending_id)).await {
501 return PendingRequestCompletion::WriteFailed(error);
502 }
503 pending_request.receive().await.map_or(
504 PendingRequestCompletion::NoResponse,
505 PendingRequestCompletion::Response,
506 )
507 }
508 .fuse()
509 );
510
511 select! {
512 completion = request => completion,
513 () = futures_timer::Delay::new(timeout).fuse() => PendingRequestCompletion::TimedOut,
514 }
515 };
516
517 finish_request(completion, observation, dev, feat)
518 }
519
520 /// Sends a HID++ message without timing out its transport write, then
521 /// waits at most `response_timeout` for a matching response.
522 ///
523 /// This ordering is for an owner that must know the native write has
524 /// completed before issuing a later request such as rollback. A native HID
525 /// implementation may keep a write alive after its Rust future is dropped,
526 /// so timing out and discarding that future cannot guarantee wire order.
527 ///
528 /// The caller **must drive this future to completion**, even after its own
529 /// requester has cancelled or exceeded a deadline. Requester deadlines
530 /// belong outside the task that owns this future and should only signal
531 /// that owner. A wedged native write cannot honestly be bounded without a
532 /// transport-level cancellation or completion guarantee.
533 ///
534 /// Pending-request cleanup, observations, and response matching otherwise
535 /// follow [`Self::send_with_timeout`].
536 pub async fn send_write_through(
537 &self,
538 msg: HidppMessage,
539 response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
540 response_timeout: Duration,
541 ) -> Result<HidppMessage, ChannelError> {
542 let msg = self.normalize_outgoing(msg);
543 if !self.supports_msg(&msg) {
544 return Err(ChannelError::MessageTypeNotSupported);
545 }
546
547 let (dev, feat, func) = msg.header();
548 trace!(dev, feat, func, "hidpp request");
549
550 let pending_id = self.pending_message_id.fetch_add(1, Ordering::SeqCst);
551 let observation = RequestObservation::new(self.observer.as_deref(), pending_id);
552 let pending_request = PendingRequest::register_when_key_free(
553 pending_id,
554 Arc::clone(&self.pending_messages),
555 msg,
556 AbandonedReply::Quarantine,
557 response_predicate,
558 )
559 .await;
560
561 let completion = if let Err(error) = self.write_hidpp_report(msg, Some(pending_id)).await {
562 drop(pending_request);
563 PendingRequestCompletion::WriteFailed(error)
564 } else {
565 let mut response = std::pin::pin!(pending_request.receive().fuse());
566 select! {
567 response = response => response.map_or(
568 PendingRequestCompletion::NoResponse,
569 PendingRequestCompletion::Response,
570 ),
571 () = futures_timer::Delay::new(response_timeout).fuse() => {
572 PendingRequestCompletion::TimedOut
573 },
574 }
575 };
576
577 finish_request(completion, observation, dev, feat)
578 }
579
580 /// Sends a HID++ message across the channel and does not wait for a
581 /// response.
582 ///
583 /// If a response is expected, use [`Self::send`],
584 pub async fn send_and_forget(&self, msg: HidppMessage) -> Result<(), ChannelError> {
585 let msg = self.normalize_outgoing(msg);
586 if !self.supports_msg(&msg) {
587 return Err(ChannelError::MessageTypeNotSupported);
588 }
589
590 self.write_hidpp_report(msg, None).await
591 }
592
593 async fn write_hidpp_report(
594 &self,
595 msg: HidppMessage,
596 request_id: Option<u64>,
597 ) -> Result<(), ChannelError> {
598 let mut buf = [0u8; LONG_REPORT_LENGTH];
599 let len = msg.write_raw(&mut buf);
600 emit_report(self.observer.as_deref(), &buf[..len], |report| {
601 ChannelObservation::OutgoingReport { request_id, report }
602 });
603 self.raw_channel
604 .write_report(&buf[..len])
605 .await
606 .map(|_| ())
607 .map_err(ChannelError::Implementation)
608 }
609
610 /// Write one raw HID report through this channel's already-owned transport.
611 ///
612 /// Reports must contain `1..=64` bytes, including their report ID. The
613 /// operation is bounded by [`SEND_RESPONSE_TIMEOUT`] and returns the exact
614 /// byte count reported by the transport. This is intended for HID++ report
615 /// widths such as the 64-byte `0x12` lighting frame that [`HidppMessage`]
616 /// cannot represent.
617 pub async fn write_raw_report(&self, report: &[u8]) -> Result<usize, ChannelError> {
618 self.write_raw_report_with_timeout(report, SEND_RESPONSE_TIMEOUT)
619 .await
620 }
621
622 async fn write_raw_report_with_timeout(
623 &self,
624 report: &[u8],
625 timeout: Duration,
626 ) -> Result<usize, ChannelError> {
627 if !(1..=MAX_RAW_REPORT_LENGTH).contains(&report.len()) {
628 return Err(ChannelError::InvalidRawReportLength(report.len()));
629 }
630
631 emit_report(self.observer.as_deref(), report, |report| {
632 ChannelObservation::OutgoingReport {
633 request_id: None,
634 report,
635 }
636 });
637 let mut write = std::pin::pin!(self.raw_channel.write_report(report).fuse());
638 select! {
639 result = write => result.map_err(ChannelError::Implementation),
640 () = futures_timer::Delay::new(timeout).fuse() => Err(ChannelError::Timeout),
641 }
642 }
643
644 /// Registers a listener that will be called for every incoming message.
645 ///
646 /// Returns a handle that can be used to remove the listener using a call to
647 /// [`Self::remove_msg_listener`].
648 pub fn add_msg_listener(
649 &self,
650 listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
651 ) -> u32 {
652 let hdl = self.next_listener_hdl.fetch_add(1, Ordering::Relaxed);
653 lock(&self.message_listeners).insert(hdl, Arc::new(listener));
654 hdl
655 }
656
657 /// Registers a listener that is automatically removed when the returned
658 /// guard is dropped.
659 pub fn add_msg_listener_guarded(
660 &self,
661 listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
662 ) -> MessageListenerGuard {
663 let hdl = self.add_msg_listener(listener);
664 MessageListenerGuard {
665 message_listeners: Arc::downgrade(&self.message_listeners),
666 hdl,
667 }
668 }
669
670 /// Removes a previously registered message listener.
671 ///
672 /// Returns whether a listener was found using the given handle.
673 pub fn remove_msg_listener(&self, hdl: u32) -> bool {
674 lock(&self.message_listeners).remove(&hdl).is_some()
675 }
676}
677
678/// Reads reports from `raw_channel` until `close` fires, resolving each one
679/// against the pending requests and then handing it to every listener.
680///
681/// Runs on the channel's dedicated read thread. `read_report` is always raced
682/// against `close` so a transport that parks forever on a dead device still
683/// lets the channel shut down — see [`RawHidChannel::read_report`].
684async fn read_loop(
685 raw_channel: &dyn RawHidChannel,
686 pending_messages: &Mutex<PendingQueue>,
687 message_listeners: &Mutex<HashMap<u32, MessageListener>>,
688 observer: Option<&dyn ChannelObserver>,
689 mut close: oneshot::Receiver<()>,
690) {
691 let mut buf = [0u8; MAX_REPORT_LENGTH];
692
693 loop {
694 let res = select! {
695 _ = close => break,
696 res = raw_channel.read_report(&mut buf).fuse() => res,
697 };
698
699 let len = match res {
700 Ok(len) => len,
701 Err(error) => {
702 // A silently erroring handle is indistinguishable from a deaf
703 // one without this line.
704 trace!(?error, "read_report error");
705 continue;
706 }
707 };
708
709 let Some(msg) = HidppMessage::read_raw(&buf[..len]) else {
710 // Only a HID++ report ID can be malformed here: any other ID
711 // belongs to a protocol sharing this node — Logitech DJ on a
712 // Unifying receiver — and is foreign traffic, not broken HID++.
713 let foreign = buf[..len]
714 .first()
715 .is_some_and(|id| !is_hidpp_report_id(*id));
716 emit_report(observer, &buf[..len], |report| {
717 if foreign {
718 ChannelObservation::ForeignIncomingReport { report }
719 } else {
720 ChannelObservation::MalformedIncomingReport { report }
721 }
722 });
723 trace!(len, foreign, "report not HID++ — dropped");
724 continue;
725 };
726
727 let mut matched_id = None;
728 let mut stale = false;
729 let pending_count;
730 {
731 let mut queue = lock(pending_messages);
732 pending_count = queue.messages.len();
733 if let Some(answered) = queue
734 .messages
735 .iter()
736 .position(|elem| (elem.response_predicate)(&msg))
737 .and_then(|pos| queue.messages.remove(pos))
738 {
739 matched_id = Some(answered.id);
740 let PendingMessage {
741 key,
742 request,
743 response_predicate,
744 sender,
745 extra_replies,
746 ..
747 } = answered;
748 let _ = sender.send(msg);
749 if extra_replies > 0 {
750 // Answered by one of several replies it was owed: the
751 // others are still coming and must not answer the next
752 // request with this header.
753 queue.stale.push(StaleKey {
754 key,
755 request,
756 response_predicate,
757 outstanding: extra_replies,
758 expires: Instant::now() + STALE_REPLY_GRACE,
759 });
760 }
761 queue.wake_key_waiters();
762 } else {
763 // The reply of a request nobody waits for any more. Discarding
764 // it here is what frees its header for the next request.
765 stale = queue.discard_stale_reply(&msg);
766 }
767 }
768
769 emit_report(observer, &buf[..len], |report| {
770 ChannelObservation::IncomingReport {
771 request_id: matched_id,
772 report,
773 }
774 });
775
776 trace!(
777 len,
778 matched = matched_id.is_some(),
779 stale,
780 pending_count,
781 payload = format_args!("{:02x?}", &buf[..len.min(16)]),
782 "raw report received"
783 );
784
785 // Collected before dispatch so a listener may add or remove listeners
786 // without deadlocking on the lock it is being called under.
787 let listeners: Vec<_> = lock(message_listeners).values().cloned().collect();
788 for listener in listeners {
789 listener(msg, matched_id.is_some());
790 }
791 }
792}