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