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.
4
5use std::{
6    collections::{HashMap, VecDeque},
7    sync::{
8        Arc, Mutex, Weak,
9        atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering},
10    },
11    thread::{self, JoinHandle},
12    time::Duration,
13};
14
15use futures::{FutureExt, channel::oneshot, select};
16use tracing::trace;
17
18use crate::{nibble::U4, sync::lock};
19
20mod error;
21mod message;
22mod raw;
23
24#[cfg(test)]
25pub(crate) mod tests;
26
27pub use error::ChannelError;
28pub use message::{
29    HidppMessage, LONG_REPORT_ID, LONG_REPORT_LENGTH, SHORT_REPORT_ID, SHORT_REPORT_LENGTH,
30};
31pub use raw::RawHidChannel;
32
33use raw::supports_short_long_hidpp;
34
35/// This is the size of the buffer incoming reports are read into.
36/// As we only care about HID++ reports, this equals to [`LONG_REPORT_LENGTH`].
37const MAX_REPORT_LENGTH: usize = LONG_REPORT_LENGTH;
38
39/// Largest output report accepted by [`HidppChannel::write_raw_report`].
40/// Logitech's very-long HID++ lighting report (`0x12`) is 64 bytes.
41const MAX_RAW_REPORT_LENGTH: usize = 64;
42
43/// The default time budget for a [`HidppChannel::send`] request: the report
44/// write plus the wait for a matching response. Callers that need a different
45/// budget can use [`HidppChannel::send_with_timeout`].
46pub const SEND_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
47
48type MessageListener = Arc<dyn Fn(HidppMessage, bool) + Send + Sync + 'static>;
49
50/// Removes a HID++ message listener when dropped.
51pub struct MessageListenerGuard {
52    message_listeners: Weak<Mutex<HashMap<u32, MessageListener>>>,
53    hdl: u32,
54}
55
56impl Drop for MessageListenerGuard {
57    fn drop(&mut self) {
58        if let Some(message_listeners) = self.message_listeners.upgrade() {
59            lock(&message_listeners).remove(&self.hdl);
60        }
61    }
62}
63
64/// Represents a HID communication channel supporting HID++.
65pub struct HidppChannel {
66    /// Whether the channel supports short (7 bytes) HID++ messages.
67    pub supports_short: bool,
68
69    /// Whether the channel supports long (20 bytes) HID++ messages.
70    pub supports_long: bool,
71
72    /// The vendor ID of the connected HID device.
73    pub vendor_id: u16,
74
75    /// The product ID of the connected HID device.
76    pub product_id: u16,
77
78    /// The underlying raw HID channel.
79    raw_channel: Arc<dyn RawHidChannel>,
80
81    /// Whether to rotate the [`Self::software_id`].
82    rotate_software_id: AtomicBool,
83
84    /// The software ID to provide at the next call to [`Self::get_sw_id`].
85    software_id: AtomicU8,
86
87    /// All sent messages that are waiting for a response.
88    pending_messages: Arc<Mutex<VecDeque<PendingMessage>>>,
89
90    /// The request ID assigned to the next pending message.
91    pending_message_id: AtomicU64,
92
93    /// Registered listeners that will receive notifications about incoming
94    /// messages.
95    message_listeners: Arc<Mutex<HashMap<u32, MessageListener>>>,
96
97    /// The handle assigned to the next registered message listener.
98    ///
99    /// A counter, not a random draw: a handle is only ever a key into
100    /// [`Self::message_listeners`], so counting up is collision-free by
101    /// construction where drawing needed a retry loop to be merely unlikely.
102    next_listener_hdl: AtomicU32,
103
104    /// The sender signaling the read thread to stop.
105    read_thread_close: Option<oneshot::Sender<()>>,
106
107    /// The handle to the read thread. Should be joined after signaling
108    /// [`Self::read_thread_close`].
109    read_thread_hdl: Option<JoinHandle<()>>,
110
111    /// Optional process-wide software-id lease: `(id, free)` run on drop.
112    ///
113    /// OpenLogi leases a unique HID++ software id per open so concurrent
114    /// channels on the same physical HID node never share a correlation id
115    /// (software id `0` is reserved for device notifications). Local addition.
116    sw_id_lease: Option<(u8, fn(u8))>,
117}
118
119impl Drop for HidppChannel {
120    fn drop(&mut self) {
121        if let Some((id, free)) = self.sw_id_lease.take() {
122            free(id);
123        }
124
125        if let Some(read_thread_close) = self.read_thread_close.take() {
126            // This only fails if the receiving end, which is owned by the read thread in
127            // this case, is dropped.
128            // This just means that the read thread is already stopped, so we can ignore the
129            // error here.
130            let _ = read_thread_close.send(());
131        }
132
133        if let Some(read_thread_hdl) = self.read_thread_hdl.take() {
134            // Joining is not politeness: it is what makes the OS handle closed
135            // by the time this returns, so a caller that drops a channel and
136            // reopens the same node never has two opens of it alive at once.
137            #[expect(
138                clippy::unwrap_used,
139                reason = "propagate a read-thread panic instead of ignoring a crashed background worker"
140            )]
141            read_thread_hdl.join().unwrap();
142        }
143    }
144}
145
146/// Represents a message that was sent and is waiting for a response.
147struct PendingMessage {
148    /// Unique ID used to remove this request if it times out.
149    id: u64,
150
151    /// The predicate that has to match for an incoming message to be classified
152    /// as the response.
153    response_predicate: Box<dyn Fn(&HidppMessage) -> bool + Send>,
154
155    /// The oneshot sender used to provide the response message to the receiving
156    /// end.
157    sender: oneshot::Sender<HidppMessage>,
158}
159
160impl HidppChannel {
161    /// Tries to construct a HID++ channel from a raw HID channel.
162    ///
163    /// Reads run on a thread this owns, joined on drop — so the underlying OS
164    /// handle is closed by the time a dropped channel's `drop` returns. Callers
165    /// that reopen the same node right after closing one depend on that.
166    ///
167    /// If the given HID channel does not support HID++,
168    /// [`ChannelError::HidppNotSupported`] will be returned.
169    pub async fn from_raw_channel(raw: impl RawHidChannel) -> Result<Self, ChannelError> {
170        let (supports_short, supports_long) = supports_short_long_hidpp(&raw).await?;
171
172        if !supports_short && !supports_long {
173            return Err(ChannelError::HidppNotSupported);
174        }
175
176        let raw_channel_rc = Arc::new(raw);
177        let pending_messages_rc = Arc::new(Mutex::new(VecDeque::<PendingMessage>::new()));
178        let message_listeners_rc = Arc::new(Mutex::new(HashMap::<u32, MessageListener>::new()));
179
180        let (close_sender, close_receiver) = oneshot::channel::<()>();
181
182        let read_thread_hdl = thread::spawn({
183            let raw_channel = Arc::clone(&raw_channel_rc);
184            let pending_messages = Arc::clone(&pending_messages_rc);
185            let message_listeners = Arc::clone(&message_listeners_rc);
186
187            move || {
188                futures::executor::block_on(read_loop(
189                    &*raw_channel,
190                    &pending_messages,
191                    &message_listeners,
192                    close_receiver,
193                ));
194            }
195        });
196
197        Ok(Self {
198            supports_short,
199            supports_long,
200            vendor_id: raw_channel_rc.vendor_id(),
201            product_id: raw_channel_rc.product_id(),
202            raw_channel: raw_channel_rc,
203            rotate_software_id: AtomicBool::new(false),
204            software_id: AtomicU8::new(0x01),
205            pending_messages: pending_messages_rc,
206            pending_message_id: AtomicU64::new(1),
207            next_listener_hdl: AtomicU32::new(1),
208            message_listeners: message_listeners_rc,
209            read_thread_close: Some(close_sender),
210            read_thread_hdl: Some(read_thread_hdl),
211            sw_id_lease: None,
212        })
213    }
214
215    /// Whether the underlying HID transport still reports a live connection.
216    pub fn is_connected(&self) -> bool {
217        self.raw_channel.is_connected()
218    }
219
220    /// Sets the software ID that should be returned by the next call to
221    /// [`Self::get_sw_id`].
222    ///
223    /// Using software ID `0` is highly discouraged as it is used for device
224    /// notifications.
225    pub fn set_sw_id(&self, sw_id: U4) {
226        self.software_id.store(sw_id.to_lo(), Ordering::SeqCst);
227    }
228
229    /// Sets whether the software ID returned by a call to [`Self::get_sw_id`]
230    /// should increment (and potentially wrap around) after each call.
231    ///
232    /// This comes in handy when trying to map responses to requests
233    /// consistently.
234    ///
235    /// Software ID `0` will be skipped in the rotation process as it is
236    /// reserved for device notifications.
237    pub fn set_rotating_sw_id(&self, enable: bool) {
238        self.rotate_software_id.store(enable, Ordering::SeqCst);
239    }
240
241    /// Lease software id `id` until this channel is dropped, then call `free(id)`.
242    ///
243    /// Replaces any previous lease. Used by OpenLogi so concurrent opens of the
244    /// same HID node hold distinct correlation ids for their full lifetime.
245    ///
246    /// OpenLogi local addition.
247    pub fn set_sw_id_lease(&mut self, id: u8, free: fn(u8)) {
248        self.sw_id_lease = Some((id, free));
249    }
250
251    /// Provides a software ID that can be used to send a HID++ message across
252    /// the channel.
253    ///
254    /// This method should be called separately for every message to send as it
255    /// may rotate (as indicated by [`Self::set_rotating_sw_id`]).
256    pub fn get_sw_id(&self) -> U4 {
257        if self.rotate_software_id.load(Ordering::SeqCst) {
258            // The closure always returns `Some`, so `fetch_update` never
259            // reports `Err`; both arms carry the same pre-update value.
260            let previous =
261                match self
262                    .software_id
263                    .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |old| {
264                        Some(if old & 0x0f == 0x0f {
265                            0x01
266                        } else {
267                            old.wrapping_add(1)
268                        })
269                    }) {
270                    Ok(previous) | Err(previous) => previous,
271                };
272            U4::from_lo(previous)
273        } else {
274            U4::from_lo(self.software_id.load(Ordering::SeqCst))
275        }
276    }
277
278    /// Checks whether the channel supports the given HID++ message.
279    pub fn supports_msg(&self, msg: &HidppMessage) -> bool {
280        match msg {
281            HidppMessage::Short(_) => self.supports_short,
282            HidppMessage::Long(_) => self.supports_long,
283        }
284    }
285
286    /// Re-frames a short message as long on a long-only channel — a device that
287    /// exposes only the long HID++ report (e.g. a Bluetooth-LE-direct mouse on
288    /// macOS, where `IOHIDDeviceSetReport` rejects the short report). The HID++
289    /// header bytes sit at the same offsets in both widths, so the only change
290    /// is the report id plus zero-padding the extra payload; the device answers
291    /// with a long report, which still matches the request by header. A no-op on
292    /// channels that advertise short support.
293    ///
294    /// (OpenLogi local addition — candidate for upstreaming.)
295    fn normalize_outgoing(&self, msg: HidppMessage) -> HidppMessage {
296        match msg {
297            HidppMessage::Short(_) if !self.supports_short && self.supports_long => msg.widened(),
298            other => other,
299        }
300    }
301
302    /// Sends a HID++ message across the channel and waits for a response.
303    ///
304    /// If no response is expected/required, use [`Self::send_and_forget`].
305    ///
306    /// The whole request — the report write plus the wait for a matching
307    /// response — is bounded by [`SEND_RESPONSE_TIMEOUT`]; the future resolves
308    /// to [`ChannelError::Timeout`] on elapse. Use [`Self::send_with_timeout`]
309    /// to choose a different budget.
310    pub async fn send(
311        &self,
312        msg: HidppMessage,
313        response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
314    ) -> Result<HidppMessage, ChannelError> {
315        self.send_with_timeout(msg, response_predicate, SEND_RESPONSE_TIMEOUT)
316            .await
317    }
318
319    /// Sends a HID++ message across the channel and waits for a response,
320    /// bounding the whole request — the report write plus the wait for a
321    /// matching response — by `timeout`.
322    ///
323    /// On elapse the request's pending entry is removed (concurrent in-flight
324    /// requests are unaffected) and [`ChannelError::Timeout`] is returned; a
325    /// response that still arrives later reaches message listeners as an
326    /// unmatched message.
327    ///
328    /// [`Self::send`] uses this with [`SEND_RESPONSE_TIMEOUT`], which suits
329    /// requests to a device that may be asleep. Requests that should fail
330    /// faster — e.g. probing a receiver that answers immediately or not at
331    /// all — can pass a tighter budget.
332    pub async fn send_with_timeout(
333        &self,
334        msg: HidppMessage,
335        response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
336        timeout: Duration,
337    ) -> Result<HidppMessage, ChannelError> {
338        let msg = self.normalize_outgoing(msg);
339        if !self.supports_msg(&msg) {
340            return Err(ChannelError::MessageTypeNotSupported);
341        }
342
343        // Wire trace (off by default; `OPENLOGI_LOG=hidpp=trace`). Capture the
344        // header before `msg` is moved into the send future so the outcome line
345        // below can name the same request.
346        let (dev, feat, func) = msg.header();
347        trace!(dev, feat, func, "hidpp request");
348
349        let (sender, receiver) = oneshot::channel::<HidppMessage>();
350        let pending_id = self.pending_message_id.fetch_add(1, Ordering::SeqCst);
351
352        {
353            let mut pending = lock(&self.pending_messages);
354            // Drop abandoned requests before queuing this one. Timeouts and
355            // write failures remove their entry eagerly below, but a caller
356            // cancelled mid-flight (an outer `timeout(..)` dropping the whole
357            // future) still leaves its `PendingMessage` behind. On a channel
358            // reused across inventory ticks those would accumulate unboundedly
359            // — and a late response could be mis-delivered to a recycled
360            // software id. `is_canceled()` is true once the receiver is gone,
361            // so this prunes exactly the give-ups.
362            pending.retain(|m| !m.sender.is_canceled());
363            pending.push_back(PendingMessage {
364                id: pending_id,
365                response_predicate: Box::new(response_predicate),
366                sender,
367            });
368        }
369
370        // The deadline covers the write as well: `write_report` has no
371        // bounded-time contract of its own, so a wedged device could otherwise
372        // park `send` forever before the response wait even starts.
373        let mut request = std::pin::pin!(
374            async {
375                self.send_and_forget(msg).await?;
376                receiver.await.map_err(|_| ChannelError::NoResponse)
377            }
378            .fuse()
379        );
380
381        let result = select! {
382            result = request => result,
383            () = futures_timer::Delay::new(timeout).fuse() => Err(ChannelError::Timeout),
384        };
385
386        match &result {
387            Ok(_) => trace!(dev, feat, "hidpp response"),
388            Err(e) => trace!(dev, feat, error = ?e, "hidpp no response"),
389        }
390
391        if result.is_err() {
392            // A timeout or write failure leaves the entry queued — remove it
393            // eagerly. After a matched response the read thread has already
394            // taken it, so this is a no-op then.
395            self.remove_pending_message(pending_id);
396        }
397
398        result
399    }
400
401    fn remove_pending_message(&self, id: u64) {
402        let mut pending = lock(&self.pending_messages);
403        if let Some(pos) = pending.iter().position(|msg| msg.id == id) {
404            pending.remove(pos);
405        }
406    }
407
408    /// Sends a HID++ message across the channel and does not wait for a
409    /// response.
410    ///
411    /// If a response is expected, use [`Self::send`],
412    pub async fn send_and_forget(&self, msg: HidppMessage) -> Result<(), ChannelError> {
413        let msg = self.normalize_outgoing(msg);
414        if !self.supports_msg(&msg) {
415            return Err(ChannelError::MessageTypeNotSupported);
416        }
417
418        let mut buf = [0u8; LONG_REPORT_LENGTH];
419        let len = msg.write_raw(&mut buf);
420        self.raw_channel
421            .write_report(&buf[..len])
422            .await
423            .map(|_| ())
424            .map_err(ChannelError::Implementation)
425    }
426
427    /// Write one raw HID report through this channel's already-owned transport.
428    ///
429    /// Reports must contain `1..=64` bytes, including their report ID. The
430    /// operation is bounded by [`SEND_RESPONSE_TIMEOUT`] and returns the exact
431    /// byte count reported by the transport. This is intended for HID++ report
432    /// widths such as the 64-byte `0x12` lighting frame that [`HidppMessage`]
433    /// cannot represent.
434    pub async fn write_raw_report(&self, report: &[u8]) -> Result<usize, ChannelError> {
435        self.write_raw_report_with_timeout(report, SEND_RESPONSE_TIMEOUT)
436            .await
437    }
438
439    async fn write_raw_report_with_timeout(
440        &self,
441        report: &[u8],
442        timeout: Duration,
443    ) -> Result<usize, ChannelError> {
444        if !(1..=MAX_RAW_REPORT_LENGTH).contains(&report.len()) {
445            return Err(ChannelError::InvalidRawReportLength(report.len()));
446        }
447
448        let mut write = std::pin::pin!(self.raw_channel.write_report(report).fuse());
449        select! {
450            result = write => result.map_err(ChannelError::Implementation),
451            () = futures_timer::Delay::new(timeout).fuse() => Err(ChannelError::Timeout),
452        }
453    }
454
455    /// Registers a listener that will be called for every incoming message.
456    ///
457    /// Returns a handle that can be used to remove the listener using a call to
458    /// [`Self::remove_msg_listener`].
459    pub fn add_msg_listener(
460        &self,
461        listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
462    ) -> u32 {
463        let hdl = self.next_listener_hdl.fetch_add(1, Ordering::Relaxed);
464        lock(&self.message_listeners).insert(hdl, Arc::new(listener));
465        hdl
466    }
467
468    /// Registers a listener that is automatically removed when the returned
469    /// guard is dropped.
470    pub fn add_msg_listener_guarded(
471        &self,
472        listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
473    ) -> MessageListenerGuard {
474        let hdl = self.add_msg_listener(listener);
475        MessageListenerGuard {
476            message_listeners: Arc::downgrade(&self.message_listeners),
477            hdl,
478        }
479    }
480
481    /// Removes a previously registered message listener.
482    ///
483    /// Returns whether a listener was found using the given handle.
484    pub fn remove_msg_listener(&self, hdl: u32) -> bool {
485        lock(&self.message_listeners).remove(&hdl).is_some()
486    }
487}
488
489/// Reads reports from `raw_channel` until `close` fires, resolving each one
490/// against the pending requests and then handing it to every listener.
491///
492/// Runs on the channel's dedicated read thread. `read_report` is always raced
493/// against `close` so a transport that parks forever on a dead device still
494/// lets the channel shut down — see [`RawHidChannel::read_report`].
495async fn read_loop(
496    raw_channel: &dyn RawHidChannel,
497    pending_messages: &Mutex<VecDeque<PendingMessage>>,
498    message_listeners: &Mutex<HashMap<u32, MessageListener>>,
499    mut close: oneshot::Receiver<()>,
500) {
501    let mut buf = [0u8; MAX_REPORT_LENGTH];
502
503    loop {
504        let res = select! {
505            _ = close => break,
506            res = raw_channel.read_report(&mut buf).fuse() => res,
507        };
508
509        let len = match res {
510            Ok(len) => len,
511            Err(error) => {
512                // A silently erroring handle is indistinguishable from a deaf
513                // one without this line.
514                trace!(?error, "read_report error");
515                continue;
516            }
517        };
518
519        let Some(msg) = HidppMessage::read_raw(&buf[..len]) else {
520            trace!(len, "report not HID++ — dropped");
521            continue;
522        };
523
524        let mut matched = false;
525        let pending_count;
526        {
527            let mut msgs = lock(pending_messages);
528            pending_count = msgs.len();
529            if let Some(pos) = msgs.iter().position(|elem| (elem.response_predicate)(&msg))
530                && let Some(waiting) = msgs.remove(pos)
531            {
532                let _ = waiting.sender.send(msg);
533                matched = true;
534            }
535        }
536
537        trace!(
538            len,
539            matched,
540            pending_count,
541            payload = format_args!("{:02x?}", &buf[..len.min(16)]),
542            "raw report received"
543        );
544
545        // Collected before dispatch so a listener may add or remove listeners
546        // without deadlocking on the lock it is being called under.
547        let listeners: Vec<_> = lock(message_listeners).values().cloned().collect();
548        for listener in listeners {
549            listener(msg, matched);
550        }
551    }
552}