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::{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/// A software id a request may carry: `1..=15`.
65///
66/// Id `0` is the wire's device-notification marker (event decoding treats
67/// `software_id == 0` as "not a response"), so a request sent with it would
68/// have its response indistinguishable from an event — made unrepresentable
69/// here by construction.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct RequestSwId(U4);
72
73impl RequestSwId {
74    /// The id as a request software id, or `None` for the reserved id `0`.
75    #[must_use]
76    pub fn new(id: U4) -> Option<Self> {
77        (id.to_lo() != 0).then_some(Self(id))
78    }
79
80    /// The nibble the wire carries.
81    #[must_use]
82    pub fn get(self) -> U4 {
83        self.0
84    }
85}
86
87/// How the channel assigns the software id each outgoing request carries.
88///
89/// One value instead of three cooperating fields (a rotate flag, the current
90/// id, an optional lease): the triple admitted states the wire cannot mean —
91/// rotation walking over ids other channels hold leases on, or a leased id
92/// different from the id actually sent. Constructed whole, those states are
93/// unrepresentable.
94pub enum SwIdPolicy {
95    /// Every request carries the same id.
96    Fixed(RequestSwId),
97    /// Walk `1..=15`, one id per request — eases mapping responses to
98    /// requests for a single exclusive user of a node. The counter is this
99    /// policy's own; the id `0` slot is skipped in the wrap.
100    Rotating(AtomicU8),
101    /// A fixed id owned process-wide: `free(id)` runs exactly once when this
102    /// policy is dropped, handing the lease back to the allocator — so
103    /// concurrent opens of one HID node never share a correlation id.
104    /// (OpenLogi local addition.)
105    Leased {
106        /// The leased id every request carries.
107        id: RequestSwId,
108        /// Returns the id to the allocator on drop.
109        free: fn(u8),
110    },
111}
112
113impl SwIdPolicy {
114    /// A fresh rotation, starting at id `1`.
115    #[must_use]
116    pub fn rotating() -> Self {
117        Self::Rotating(AtomicU8::new(0x01))
118    }
119}
120
121impl Default for SwIdPolicy {
122    /// Fixed id `1`, matching the protocol's conventional default.
123    fn default() -> Self {
124        Self::Fixed(RequestSwId(U4::from_lo(0x01)))
125    }
126}
127
128impl Drop for SwIdPolicy {
129    fn drop(&mut self) {
130        if let Self::Leased { id, free } = self {
131            free(id.get().to_lo());
132        }
133    }
134}
135
136/// Represents a HID communication channel supporting HID++.
137pub struct HidppChannel {
138    /// Whether the channel supports short (7 bytes) HID++ messages.
139    pub supports_short: bool,
140
141    /// Whether the channel supports long (20 bytes) HID++ messages.
142    pub supports_long: bool,
143
144    /// The vendor ID of the connected HID device.
145    pub vendor_id: u16,
146
147    /// The product ID of the connected HID device.
148    pub product_id: u16,
149
150    /// The underlying raw HID channel.
151    raw_channel: Arc<dyn RawHidChannel>,
152
153    /// The software-id policy for outgoing requests (see [`SwIdPolicy`]).
154    ///
155    /// This must remain after `raw_channel`: fields drop in declaration order,
156    /// so the final lease is returned only after channel shutdown has joined
157    /// the read thread and released the raw transport.
158    sw_id_policy: SwIdPolicy,
159
160    /// All sent messages that are waiting for a response.
161    pending_messages: Arc<Mutex<VecDeque<PendingMessage>>>,
162
163    /// The request ID assigned to the next pending message.
164    pending_message_id: AtomicU64,
165
166    /// Registered listeners that will receive notifications about incoming
167    /// messages.
168    message_listeners: Arc<Mutex<HashMap<u32, MessageListener>>>,
169
170    /// The handle assigned to the next registered message listener.
171    ///
172    /// A counter, not a random draw: a handle is only ever a key into
173    /// [`Self::message_listeners`], so counting up is collision-free by
174    /// construction where drawing needed a retry loop to be merely unlikely.
175    next_listener_hdl: AtomicU32,
176
177    /// The sender signaling the read thread to stop.
178    read_thread_close: Option<oneshot::Sender<()>>,
179
180    /// The handle to the read thread. Should be joined after signaling
181    /// [`Self::read_thread_close`].
182    read_thread_hdl: Option<JoinHandle<()>>,
183}
184
185impl Drop for HidppChannel {
186    fn drop(&mut self) {
187        if let Some(read_thread_close) = self.read_thread_close.take() {
188            // This only fails if the receiving end, which is owned by the read thread in
189            // this case, is dropped.
190            // This just means that the read thread is already stopped, so we can ignore the
191            // error here.
192            let _ = read_thread_close.send(());
193        }
194
195        if let Some(read_thread_hdl) = self.read_thread_hdl.take() {
196            // Joining is not politeness: together with the subsequent
197            // `raw_channel` field drop, it makes the OS handle close before the
198            // software-id lease is returned. A caller can therefore drop a
199            // channel and reopen the same node without overlapping lifetimes.
200            #[expect(
201                clippy::unwrap_used,
202                reason = "propagate a read-thread panic instead of ignoring a crashed background worker"
203            )]
204            read_thread_hdl.join().unwrap();
205        }
206    }
207}
208
209/// Represents a message that was sent and is waiting for a response.
210struct PendingMessage {
211    /// Unique ID used to remove this request when its waiter goes away.
212    id: u64,
213
214    /// The predicate that has to match for an incoming message to be classified
215    /// as the response.
216    response_predicate: Box<dyn Fn(&HidppMessage) -> bool + Send>,
217
218    /// The oneshot sender used to provide the response message to the receiving
219    /// end.
220    sender: oneshot::Sender<HidppMessage>,
221}
222
223/// One registered request and the receiver waiting for its response.
224///
225/// Dropping this value unregisters the request, including when an outer async
226/// deadline cancels [`HidppChannel::send_with_timeout`] during its write.
227struct PendingRequest {
228    id: u64,
229    pending_messages: Arc<Mutex<VecDeque<PendingMessage>>>,
230    receiver: oneshot::Receiver<HidppMessage>,
231}
232
233impl PendingRequest {
234    fn register(
235        id: u64,
236        pending_messages: Arc<Mutex<VecDeque<PendingMessage>>>,
237        response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
238    ) -> Self {
239        let (sender, receiver) = oneshot::channel();
240        let request = Self {
241            id,
242            pending_messages,
243            receiver,
244        };
245        lock(&request.pending_messages).push_back(PendingMessage {
246            id,
247            response_predicate: Box::new(response_predicate),
248            sender,
249        });
250        request
251    }
252
253    async fn receive(mut self) -> Result<HidppMessage, ChannelError> {
254        (&mut self.receiver)
255            .await
256            .map_err(|_| ChannelError::NoResponse)
257    }
258}
259
260impl Drop for PendingRequest {
261    fn drop(&mut self) {
262        let mut pending = lock(&self.pending_messages);
263        if let Some(pos) = pending.iter().position(|message| message.id == self.id) {
264            pending.remove(pos);
265        }
266    }
267}
268
269impl HidppChannel {
270    /// Tries to construct a HID++ channel from a raw HID channel.
271    ///
272    /// Reads run on a thread this owns, joined on drop — so the underlying OS
273    /// handle is closed by the time a dropped channel's `drop` returns. Callers
274    /// that reopen the same node right after closing one depend on that.
275    ///
276    /// If the given HID channel does not support HID++,
277    /// [`ChannelError::HidppNotSupported`] will be returned.
278    pub async fn from_raw_channel(raw: impl RawHidChannel) -> Result<Self, ChannelError> {
279        let (supports_short, supports_long) = supports_short_long_hidpp(&raw).await?;
280
281        if !supports_short && !supports_long {
282            return Err(ChannelError::HidppNotSupported);
283        }
284
285        let raw_channel_rc = Arc::new(raw);
286        let pending_messages_rc = Arc::new(Mutex::new(VecDeque::<PendingMessage>::new()));
287        let message_listeners_rc = Arc::new(Mutex::new(HashMap::<u32, MessageListener>::new()));
288
289        let (close_sender, close_receiver) = oneshot::channel::<()>();
290
291        let read_thread_hdl = thread::spawn({
292            let raw_channel = Arc::clone(&raw_channel_rc);
293            let pending_messages = Arc::clone(&pending_messages_rc);
294            let message_listeners = Arc::clone(&message_listeners_rc);
295
296            move || {
297                futures::executor::block_on(read_loop(
298                    &*raw_channel,
299                    &pending_messages,
300                    &message_listeners,
301                    close_receiver,
302                ));
303            }
304        });
305
306        Ok(Self {
307            supports_short,
308            supports_long,
309            vendor_id: raw_channel_rc.vendor_id(),
310            product_id: raw_channel_rc.product_id(),
311            raw_channel: raw_channel_rc,
312            sw_id_policy: SwIdPolicy::default(),
313            pending_messages: pending_messages_rc,
314            pending_message_id: AtomicU64::new(1),
315            next_listener_hdl: AtomicU32::new(1),
316            message_listeners: message_listeners_rc,
317            read_thread_close: Some(close_sender),
318            read_thread_hdl: Some(read_thread_hdl),
319        })
320    }
321
322    /// Whether the underlying HID transport still reports a live connection.
323    pub fn is_connected(&self) -> bool {
324        self.raw_channel.is_connected()
325    }
326
327    /// Replace the software-id policy for outgoing requests.
328    ///
329    /// `&mut self` on purpose: the policy is decided while the channel is
330    /// still exclusively owned (right after opening, before it is shared), so
331    /// no request can race a policy change. Replacing a [`SwIdPolicy::Leased`]
332    /// policy returns its lease before this method returns. The channel's final
333    /// lease is returned only after its read thread and raw transport stop.
334    pub fn set_sw_id_policy(&mut self, policy: SwIdPolicy) {
335        self.sw_id_policy = policy;
336    }
337
338    /// Provides a software ID that can be used to send a HID++ message across
339    /// the channel.
340    ///
341    /// This method should be called separately for every message to send, as a
342    /// [`SwIdPolicy::Rotating`] policy advances per call.
343    pub fn get_sw_id(&self) -> U4 {
344        match &self.sw_id_policy {
345            SwIdPolicy::Fixed(id) | SwIdPolicy::Leased { id, .. } => id.get(),
346            SwIdPolicy::Rotating(counter) => {
347                // The closure always returns `Some`, so `fetch_update` never
348                // reports `Err`; both arms carry the same pre-update value.
349                let previous =
350                    match counter.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |old| {
351                        Some(if old & 0x0f == 0x0f {
352                            0x01
353                        } else {
354                            old.wrapping_add(1)
355                        })
356                    }) {
357                        Ok(previous) | Err(previous) => previous,
358                    };
359                U4::from_lo(previous)
360            }
361        }
362    }
363
364    /// Checks whether the channel supports the given HID++ message.
365    pub fn supports_msg(&self, msg: &HidppMessage) -> bool {
366        match msg {
367            HidppMessage::Short(_) => self.supports_short,
368            HidppMessage::Long(_) => self.supports_long,
369        }
370    }
371
372    /// Re-frames a short message as long on a long-only channel — a device that
373    /// exposes only the long HID++ report (e.g. a Bluetooth-LE-direct mouse on
374    /// macOS, where `IOHIDDeviceSetReport` rejects the short report). The HID++
375    /// header bytes sit at the same offsets in both widths, so the only change
376    /// is the report id plus zero-padding the extra payload; the device answers
377    /// with a long report, which still matches the request by header. A no-op on
378    /// channels that advertise short support.
379    ///
380    /// (OpenLogi local addition — candidate for upstreaming.)
381    fn normalize_outgoing(&self, msg: HidppMessage) -> HidppMessage {
382        match msg {
383            HidppMessage::Short(_) if !self.supports_short && self.supports_long => msg.widened(),
384            other => other,
385        }
386    }
387
388    /// Sends a HID++ message across the channel and waits for a response.
389    ///
390    /// If no response is expected/required, use [`Self::send_and_forget`].
391    ///
392    /// The whole request — the report write plus the wait for a matching
393    /// response — is bounded by [`SEND_RESPONSE_TIMEOUT`]; the future resolves
394    /// to [`ChannelError::Timeout`] on elapse. Use [`Self::send_with_timeout`]
395    /// to choose a different budget.
396    pub async fn send(
397        &self,
398        msg: HidppMessage,
399        response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
400    ) -> Result<HidppMessage, ChannelError> {
401        self.send_with_timeout(msg, response_predicate, SEND_RESPONSE_TIMEOUT)
402            .await
403    }
404
405    /// Sends a HID++ message across the channel and waits for a response,
406    /// bounding the whole request — the report write plus the wait for a
407    /// matching response — by `timeout`.
408    ///
409    /// On elapse the request's pending entry is removed (concurrent in-flight
410    /// requests are unaffected) and [`ChannelError::Timeout`] is returned; a
411    /// response that still arrives later reaches message listeners as an
412    /// unmatched message.
413    ///
414    /// [`Self::send`] uses this with [`SEND_RESPONSE_TIMEOUT`], which suits
415    /// requests to a device that may be asleep. Requests that should fail
416    /// faster — e.g. probing a receiver that answers immediately or not at
417    /// all — can pass a tighter budget.
418    pub async fn send_with_timeout(
419        &self,
420        msg: HidppMessage,
421        response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
422        timeout: Duration,
423    ) -> Result<HidppMessage, ChannelError> {
424        let msg = self.normalize_outgoing(msg);
425        if !self.supports_msg(&msg) {
426            return Err(ChannelError::MessageTypeNotSupported);
427        }
428
429        // Wire trace (off by default; `OPENLOGI_LOG=hidpp=trace`). Capture the
430        // header before `msg` is moved into the send future so the outcome line
431        // below can name the same request.
432        let (dev, feat, func) = msg.header();
433        trace!(dev, feat, func, "hidpp request");
434
435        let pending_id = self.pending_message_id.fetch_add(1, Ordering::SeqCst);
436        let pending_request = PendingRequest::register(
437            pending_id,
438            Arc::clone(&self.pending_messages),
439            response_predicate,
440        );
441
442        // The deadline covers the write as well: `write_report` has no
443        // bounded-time contract of its own, so a wedged device could otherwise
444        // park `send` forever before the response wait even starts.
445        let result = {
446            let mut request = std::pin::pin!(
447                async move {
448                    self.send_and_forget(msg).await?;
449                    pending_request.receive().await
450                }
451                .fuse()
452            );
453
454            select! {
455                result = request => result,
456                () = futures_timer::Delay::new(timeout).fuse() => Err(ChannelError::Timeout),
457            }
458        };
459
460        match &result {
461            Ok(_) => trace!(dev, feat, "hidpp response"),
462            Err(e) => trace!(dev, feat, error = ?e, "hidpp no response"),
463        }
464
465        result
466    }
467
468    /// Sends a HID++ message across the channel and does not wait for a
469    /// response.
470    ///
471    /// If a response is expected, use [`Self::send`],
472    pub async fn send_and_forget(&self, msg: HidppMessage) -> Result<(), ChannelError> {
473        let msg = self.normalize_outgoing(msg);
474        if !self.supports_msg(&msg) {
475            return Err(ChannelError::MessageTypeNotSupported);
476        }
477
478        let mut buf = [0u8; LONG_REPORT_LENGTH];
479        let len = msg.write_raw(&mut buf);
480        self.raw_channel
481            .write_report(&buf[..len])
482            .await
483            .map(|_| ())
484            .map_err(ChannelError::Implementation)
485    }
486
487    /// Write one raw HID report through this channel's already-owned transport.
488    ///
489    /// Reports must contain `1..=64` bytes, including their report ID. The
490    /// operation is bounded by [`SEND_RESPONSE_TIMEOUT`] and returns the exact
491    /// byte count reported by the transport. This is intended for HID++ report
492    /// widths such as the 64-byte `0x12` lighting frame that [`HidppMessage`]
493    /// cannot represent.
494    pub async fn write_raw_report(&self, report: &[u8]) -> Result<usize, ChannelError> {
495        self.write_raw_report_with_timeout(report, SEND_RESPONSE_TIMEOUT)
496            .await
497    }
498
499    async fn write_raw_report_with_timeout(
500        &self,
501        report: &[u8],
502        timeout: Duration,
503    ) -> Result<usize, ChannelError> {
504        if !(1..=MAX_RAW_REPORT_LENGTH).contains(&report.len()) {
505            return Err(ChannelError::InvalidRawReportLength(report.len()));
506        }
507
508        let mut write = std::pin::pin!(self.raw_channel.write_report(report).fuse());
509        select! {
510            result = write => result.map_err(ChannelError::Implementation),
511            () = futures_timer::Delay::new(timeout).fuse() => Err(ChannelError::Timeout),
512        }
513    }
514
515    /// Registers a listener that will be called for every incoming message.
516    ///
517    /// Returns a handle that can be used to remove the listener using a call to
518    /// [`Self::remove_msg_listener`].
519    pub fn add_msg_listener(
520        &self,
521        listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
522    ) -> u32 {
523        let hdl = self.next_listener_hdl.fetch_add(1, Ordering::Relaxed);
524        lock(&self.message_listeners).insert(hdl, Arc::new(listener));
525        hdl
526    }
527
528    /// Registers a listener that is automatically removed when the returned
529    /// guard is dropped.
530    pub fn add_msg_listener_guarded(
531        &self,
532        listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
533    ) -> MessageListenerGuard {
534        let hdl = self.add_msg_listener(listener);
535        MessageListenerGuard {
536            message_listeners: Arc::downgrade(&self.message_listeners),
537            hdl,
538        }
539    }
540
541    /// Removes a previously registered message listener.
542    ///
543    /// Returns whether a listener was found using the given handle.
544    pub fn remove_msg_listener(&self, hdl: u32) -> bool {
545        lock(&self.message_listeners).remove(&hdl).is_some()
546    }
547}
548
549/// Reads reports from `raw_channel` until `close` fires, resolving each one
550/// against the pending requests and then handing it to every listener.
551///
552/// Runs on the channel's dedicated read thread. `read_report` is always raced
553/// against `close` so a transport that parks forever on a dead device still
554/// lets the channel shut down — see [`RawHidChannel::read_report`].
555async fn read_loop(
556    raw_channel: &dyn RawHidChannel,
557    pending_messages: &Mutex<VecDeque<PendingMessage>>,
558    message_listeners: &Mutex<HashMap<u32, MessageListener>>,
559    mut close: oneshot::Receiver<()>,
560) {
561    let mut buf = [0u8; MAX_REPORT_LENGTH];
562
563    loop {
564        let res = select! {
565            _ = close => break,
566            res = raw_channel.read_report(&mut buf).fuse() => res,
567        };
568
569        let len = match res {
570            Ok(len) => len,
571            Err(error) => {
572                // A silently erroring handle is indistinguishable from a deaf
573                // one without this line.
574                trace!(?error, "read_report error");
575                continue;
576            }
577        };
578
579        let Some(msg) = HidppMessage::read_raw(&buf[..len]) else {
580            trace!(len, "report not HID++ — dropped");
581            continue;
582        };
583
584        let mut matched = false;
585        let pending_count;
586        {
587            let mut msgs = lock(pending_messages);
588            pending_count = msgs.len();
589            if let Some(pos) = msgs.iter().position(|elem| (elem.response_predicate)(&msg))
590                && let Some(waiting) = msgs.remove(pos)
591            {
592                let _ = waiting.sender.send(msg);
593                matched = true;
594            }
595        }
596
597        trace!(
598            len,
599            matched,
600            pending_count,
601            payload = format_args!("{:02x?}", &buf[..len.min(16)]),
602            "raw report received"
603        );
604
605        // Collected before dispatch so a listener may add or remove listeners
606        // without deadlocking on the lock it is being called under.
607        let listeners: Vec<_> = lock(message_listeners).values().cloned().collect();
608        for listener in listeners {
609            listener(msg, matched);
610        }
611    }
612}