Skip to main content

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}