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    error::Error,
8    sync::{
9        Arc, Mutex, Weak,
10        atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
11    },
12    thread::{self, JoinHandle},
13    time::Duration,
14};
15
16use async_trait::async_trait;
17use futures::{FutureExt, channel::oneshot, select};
18use hidreport::{Field, Report, ReportDescriptor, Usage, UsageId, UsagePage};
19use rand::Rng;
20use thiserror::Error;
21use tracing::trace;
22
23use crate::nibble::U4;
24
25/// hidapi defines this as the maximum EXPECTED size of report descriptors.
26/// We will trust this for now, but a workaround may be required if devices do
27/// in fact return longer descriptors.
28const MAX_REPORT_DESCRIPTOR_LENGTH: usize = 4096;
29
30/// This is the size of the buffer incoming reports are read into.
31/// As we only care about HID++ reports, this equals to [`LONG_REPORT_LENGTH`].
32const MAX_REPORT_LENGTH: usize = LONG_REPORT_LENGTH;
33
34/// The default time budget for a [`HidppChannel::send`] request: the report
35/// write plus the wait for a matching response. Callers that need a different
36/// budget can use [`HidppChannel::send_with_timeout`].
37pub const SEND_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
38
39/// The ID of the HID report that is used to transmit short HID++ messages.
40pub const SHORT_REPORT_ID: u8 = 0x10;
41
42/// The HID usage page ID of short HID++ message reports.
43pub const SHORT_REPORT_USAGE_PAGE: u16 = 0xff00;
44
45/// The HID usage ID of short HID++ message reports.
46pub const SHORT_REPORT_USAGE: u16 = 0x0001;
47
48/// The length of short HID++ message reports (including report ID).
49pub const SHORT_REPORT_LENGTH: usize = 7;
50
51/// The ID of the HID report that is used to transmit long HID++ messages.
52pub const LONG_REPORT_ID: u8 = 0x11;
53
54/// The HID usage page ID of long HID++ message reports.
55pub const LONG_REPORT_USAGE_PAGE: u16 = 0xff00;
56
57/// The HID usage ID of long HID++ message reports.
58pub const LONG_REPORT_USAGE: u16 = 0x0002;
59
60/// The length of long HID++ message reports (including report ID).
61pub const LONG_REPORT_LENGTH: usize = 20;
62
63/// Represents an arbitrary HID communication channel that is both readable and
64/// writable. It has to support async I/O.
65///
66/// Any type this trait is implemented for can be used for HID(++)
67/// communication. If a specific channel supports HID++ is determined at a later
68/// stage and is not directly related to potential implementations of this
69/// trait.
70#[async_trait]
71pub trait RawHidChannel: Sync + Send + 'static {
72    /// Provides the vendor ID of the connected HID device.
73    fn vendor_id(&self) -> u16;
74
75    /// Provides the product ID of the connected HID device.
76    fn product_id(&self) -> u16;
77
78    /// Writes a raw report to the channel.
79    ///
80    /// Returns the exact amount of written bytes on success.
81    async fn write_report(&self, src: &[u8]) -> Result<usize, Box<dyn Error + Sync + Send>>;
82
83    /// Reads a raw report from the channel.
84    ///
85    /// If the buffer is not large enough to fit the whole report, its remainder
86    /// should be discarded and must not be returned by any succeeding call to
87    /// [`Self::read_report`].
88    ///
89    /// Returns the exact amount or read bytes on success. An `Err` is treated
90    /// as transient: the [`HidppChannel`] read loop logs it and retries, so an
91    /// implementation must not surface a condition that will never clear (it
92    /// would busy-spin the loop). For a *permanent* failure — the device is
93    /// gone and no report will ever arrive — the future may instead park
94    /// forever. That is sound because the read loop always races this future
95    /// against the channel's close signal in a `select!`; any other caller
96    /// must do the same and must not await `read_report` bare.
97    async fn read_report(&self, buf: &mut [u8]) -> Result<usize, Box<dyn Error + Sync + Send>>;
98
99    /// If the implementation already knows whether the underlying HID channel
100    /// supports HID++ messages, it should return `Some((supports_short,
101    /// supports_long))` from this method.
102    ///
103    /// In this case, the report descriptor will not be read and parsed.
104    fn supports_short_long_hidpp(&self) -> Option<(bool, bool)>;
105
106    /// Retrieves the raw HID report descriptor from the channel.
107    ///
108    /// This is used to determine whether the channel supports HID++.
109    ///
110    /// Returns the exact size of the report descriptor on success.
111    async fn get_report_descriptor(
112        &self,
113        buf: &mut [u8],
114    ) -> Result<usize, Box<dyn Error + Sync + Send>>;
115}
116
117/// Checks whether a raw channel supports short or long HID++ messages.
118async fn supports_short_long_hidpp(
119    chan: &impl RawHidChannel,
120) -> Result<(bool, bool), ChannelError> {
121    if let Some((supports_short, supports_long)) = chan.supports_short_long_hidpp() {
122        return Ok((supports_short, supports_long));
123    }
124
125    let mut raw_descriptor = vec![0u8; MAX_REPORT_DESCRIPTOR_LENGTH];
126    let descriptor_size = chan.get_report_descriptor(&mut raw_descriptor).await?;
127
128    let descriptor = match ReportDescriptor::try_from(&raw_descriptor[..descriptor_size]) {
129        Ok(val) => val,
130        Err(err) => return Err(ChannelError::ReportDescriptor(err)),
131    };
132
133    let supports_short = descriptor
134        .find_input_report(&[SHORT_REPORT_ID])
135        .and_then(|report| report.fields().first())
136        .and_then(|field| match field {
137            Field::Array(arr) => Some(arr.usage_range()),
138            _ => None,
139        })
140        .is_some_and(|range| {
141            range
142                .lookup_usage(&Usage::from_page_and_id(
143                    UsagePage::from(SHORT_REPORT_USAGE_PAGE),
144                    UsageId::from(SHORT_REPORT_USAGE),
145                ))
146                .is_some()
147        });
148
149    let supports_long = descriptor
150        .find_input_report(&[LONG_REPORT_ID])
151        .and_then(|report| report.fields().first())
152        .and_then(|field| match field {
153            Field::Array(arr) => Some(arr.usage_range()),
154            _ => None,
155        })
156        .is_some_and(|range| {
157            range
158                .lookup_usage(&Usage::from_page_and_id(
159                    UsagePage::from(LONG_REPORT_USAGE_PAGE),
160                    UsageId::from(LONG_REPORT_USAGE),
161                ))
162                .is_some()
163        });
164
165    Ok((supports_short, supports_long))
166}
167
168/// Represents an unversioned HID++ message.
169#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
170pub enum HidppMessage {
171    /// Represents a short HID++ message.
172    ///
173    /// Please check [`HidppChannel::supports_short`] before sending this kind
174    /// of message.
175    Short([u8; SHORT_REPORT_LENGTH - 1]),
176
177    /// Represents a long HID++ message.
178    ///
179    /// Please check [`HidppChannel::supports_long`] before sending this kind of
180    /// message.
181    Long([u8; LONG_REPORT_LENGTH - 1]),
182}
183
184impl HidppMessage {
185    /// Tries to read a HID++ message from raw data.
186    pub fn read_raw(data: &[u8]) -> Option<Self> {
187        let (&report_id, rest) = data.split_first()?;
188
189        // The empty-remainder patterns enforce the exact report lengths.
190        if report_id == SHORT_REPORT_ID
191            && let Some((&payload, [])) = rest.split_first_chunk()
192        {
193            Some(HidppMessage::Short(payload))
194        } else if report_id == LONG_REPORT_ID
195            && let Some((&payload, [])) = rest.split_first_chunk()
196        {
197            Some(HidppMessage::Long(payload))
198        } else {
199            None
200        }
201    }
202
203    /// Writes a HID++ message in its raw byte form into a buffer.
204    ///
205    /// Returns the amount of written bytes.
206    pub fn write_raw(&self, buf: &mut [u8]) -> usize {
207        match self {
208            Self::Short(payload) => {
209                buf[0] = SHORT_REPORT_ID;
210                buf[1..SHORT_REPORT_LENGTH].copy_from_slice(payload);
211                SHORT_REPORT_LENGTH
212            }
213            Self::Long(payload) => {
214                buf[0] = LONG_REPORT_ID;
215                buf[1..LONG_REPORT_LENGTH].copy_from_slice(payload);
216                LONG_REPORT_LENGTH
217            }
218        }
219    }
220
221    /// The HID++ addressing header `(device_index, feature_index, function)` —
222    /// the first three payload bytes, present on both report kinds. Used only
223    /// for wire tracing (OpenLogi-specific; not in upstream hidpp).
224    fn header(&self) -> (u8, u8, u8) {
225        let payload: &[u8] = match self {
226            Self::Short(payload) => payload,
227            Self::Long(payload) => payload,
228        };
229        (payload[0], payload[1], payload[2])
230    }
231}
232
233type MessageListener = Arc<dyn Fn(HidppMessage, bool) + Send + Sync + 'static>;
234
235/// Removes a HID++ message listener when dropped.
236pub struct MessageListenerGuard {
237    message_listeners: Weak<Mutex<HashMap<u32, MessageListener>>>,
238    hdl: u32,
239}
240
241impl Drop for MessageListenerGuard {
242    fn drop(&mut self) {
243        if let Some(message_listeners) = self.message_listeners.upgrade() {
244            message_listeners.lock().unwrap().remove(&self.hdl);
245        }
246    }
247}
248
249/// Represents a HID communication channel supporting HID++.
250pub struct HidppChannel {
251    /// Whether the channel supports short (7 bytes) HID++ messages.
252    pub supports_short: bool,
253
254    /// Whether the channel supports long (20 bytes) HID++ messages.
255    pub supports_long: bool,
256
257    /// The vendor ID of the connected HID device.
258    pub vendor_id: u16,
259
260    /// The product ID of the connected HID device.
261    pub product_id: u16,
262
263    /// The underlying raw HID channel.
264    raw_channel: Arc<dyn RawHidChannel>,
265
266    /// Whether to rotate the [`Self::software_id`].
267    rotate_software_id: AtomicBool,
268
269    /// The software ID to provide at the next call to [`Self::get_sw_id`].
270    software_id: AtomicU8,
271
272    /// All sent messages that are waiting for a response.
273    pending_messages: Arc<Mutex<VecDeque<PendingMessage>>>,
274
275    /// The request ID assigned to the next pending message.
276    pending_message_id: AtomicU64,
277
278    /// Registered listeners that will receive notifications about incoming
279    /// messages.
280    message_listeners: Arc<Mutex<HashMap<u32, MessageListener>>>,
281
282    /// The sender signaling the read thread to stop.
283    read_thread_close: Option<oneshot::Sender<()>>,
284
285    /// The handle to the read thread. Should be joined after signaling
286    /// [`Self::read_thread_close`].
287    read_thread_hdl: Option<JoinHandle<()>>,
288}
289
290impl Drop for HidppChannel {
291    fn drop(&mut self) {
292        if let Some(read_thread_close) = self.read_thread_close.take() {
293            // This only fails if the receiving end, which is owned by the read thread in
294            // this case, is dropped.
295            // This just means that the read thread is already stopped, so we can ignore the
296            // error here.
297            let _ = read_thread_close.send(());
298        }
299
300        if let Some(read_thread_hdl) = self.read_thread_hdl.take() {
301            read_thread_hdl.join().unwrap();
302        }
303    }
304}
305
306/// Represents a message that was sent and is waiting for a response.
307struct PendingMessage {
308    /// Unique ID used to remove this request if it times out.
309    id: u64,
310
311    /// The predicate that has to match for an incoming message to be classified
312    /// as the response.
313    response_predicate: Box<dyn Fn(&HidppMessage) -> bool + Send>,
314
315    /// The oneshot sender used to provide the response message to the receiving
316    /// end.
317    sender: oneshot::Sender<HidppMessage>,
318}
319
320impl HidppChannel {
321    /// Tries to construct a HID++ channel from a raw HID channel.
322    ///
323    /// If the given HID channel does not support HID++,
324    /// [`ChannelError::HidppNotSupported`] will be returned.
325    pub async fn from_raw_channel(raw: impl RawHidChannel) -> Result<Self, ChannelError> {
326        let (supports_short, supports_long) = supports_short_long_hidpp(&raw).await?;
327
328        if !supports_short && !supports_long {
329            return Err(ChannelError::HidppNotSupported);
330        }
331
332        let raw_channel_rc = Arc::new(raw);
333        let pending_messages_rc = Arc::new(Mutex::new(VecDeque::<PendingMessage>::new()));
334        let message_listeners_rc = Arc::new(Mutex::new(HashMap::<u32, MessageListener>::new()));
335
336        let (close_sender, mut close_receiver) = oneshot::channel::<()>();
337
338        let read_thread_hdl = thread::spawn({
339            let raw_channel = Arc::clone(&raw_channel_rc);
340            let pending_messages = Arc::clone(&pending_messages_rc);
341            let message_listeners = Arc::clone(&message_listeners_rc);
342
343            move || {
344                futures::executor::block_on(async {
345                    let mut buf = [0u8; MAX_REPORT_LENGTH];
346
347                    loop {
348                        let res = select! {
349                            _ = close_receiver => {
350                                break;
351                            },
352                            res = raw_channel.read_report(&mut buf).fuse() => res
353                        };
354
355                        let Ok(len) = res else {
356                            continue;
357                        };
358
359                        let Some(msg) = HidppMessage::read_raw(&buf[..len]) else {
360                            continue;
361                        };
362
363                        let mut matched = false;
364                        {
365                            let mut msgs = pending_messages.lock().unwrap();
366                            if let Some(pos) =
367                                msgs.iter().position(|elem| (elem.response_predicate)(&msg))
368                            {
369                                let waiting = msgs.remove(pos).unwrap();
370                                let _ = waiting.sender.send(msg);
371                                matched = true;
372                            }
373                        }
374
375                        let listeners: Vec<_> = message_listeners
376                            .lock()
377                            .unwrap()
378                            .values()
379                            .cloned()
380                            .collect();
381                        for listener in listeners {
382                            listener(msg, matched);
383                        }
384                    }
385                });
386            }
387        });
388
389        Ok(Self {
390            supports_short,
391            supports_long,
392            vendor_id: raw_channel_rc.vendor_id(),
393            product_id: raw_channel_rc.product_id(),
394            raw_channel: raw_channel_rc,
395            rotate_software_id: AtomicBool::new(false),
396            software_id: AtomicU8::new(0x01),
397            pending_messages: pending_messages_rc,
398            pending_message_id: AtomicU64::new(1),
399            message_listeners: message_listeners_rc,
400            read_thread_close: Some(close_sender),
401            read_thread_hdl: Some(read_thread_hdl),
402        })
403    }
404
405    /// Sets the software ID that should be returned by the next call to
406    /// [`Self::get_sw_id`].
407    ///
408    /// Using software ID `0` is highly discouraged as it is used for device
409    /// notifications.
410    pub fn set_sw_id(&self, sw_id: U4) {
411        self.software_id.store(sw_id.to_lo(), Ordering::SeqCst);
412    }
413
414    /// Sets whether the software ID returned by a call to [`Self::get_sw_id`]
415    /// should increment (and potentially wrap around) after each call.
416    ///
417    /// This comes in handy when trying to map responses to requests
418    /// consistently.
419    ///
420    /// Software ID `0` will be skipped in the rotation process as it is
421    /// reserved for device notifications.
422    pub fn set_rotating_sw_id(&self, enable: bool) {
423        self.rotate_software_id.store(enable, Ordering::SeqCst);
424    }
425
426    /// Provides a software ID that can be used to send a HID++ message across
427    /// the channel.
428    ///
429    /// This method should be called separately for every message to send as it
430    /// may rotate (as indicated by [`Self::set_rotating_sw_id`]).
431    pub fn get_sw_id(&self) -> U4 {
432        if self.rotate_software_id.load(Ordering::SeqCst) {
433            U4::from_lo(
434                self.software_id
435                    .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |old| {
436                        Some(if old & 0x0f == 0x0f {
437                            0x01
438                        } else {
439                            old.wrapping_add(1)
440                        })
441                    })
442                    .unwrap(),
443            )
444        } else {
445            U4::from_lo(self.software_id.load(Ordering::SeqCst))
446        }
447    }
448
449    /// Checks whether the channel supports the given HID++ message.
450    pub fn supports_msg(&self, msg: &HidppMessage) -> bool {
451        match msg {
452            HidppMessage::Short(_) => self.supports_short,
453            HidppMessage::Long(_) => self.supports_long,
454        }
455    }
456
457    /// Re-frames a short message as long on a long-only channel — a device that
458    /// exposes only the long HID++ report (e.g. a Bluetooth-LE-direct mouse on
459    /// macOS, where `IOHIDDeviceSetReport` rejects the short report). The HID++
460    /// header bytes sit at the same offsets in both widths, so the only change
461    /// is the report id plus zero-padding the extra payload; the device answers
462    /// with a long report, which still matches the request by header. A no-op on
463    /// channels that advertise short support.
464    ///
465    /// (OpenLogi local addition — candidate for upstreaming.)
466    fn normalize_outgoing(&self, msg: HidppMessage) -> HidppMessage {
467        match msg {
468            HidppMessage::Short(payload) if !self.supports_short && self.supports_long => {
469                HidppMessage::Long(short_payload_as_long(&payload))
470            }
471            other => other,
472        }
473    }
474
475    /// Sends a HID++ message across the channel and waits for a response.
476    ///
477    /// If no response is expected/required, use [`Self::send_and_forget`].
478    ///
479    /// The whole request — the report write plus the wait for a matching
480    /// response — is bounded by [`SEND_RESPONSE_TIMEOUT`]; the future resolves
481    /// to [`ChannelError::Timeout`] on elapse. Use [`Self::send_with_timeout`]
482    /// to choose a different budget.
483    pub async fn send(
484        &self,
485        msg: HidppMessage,
486        response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
487    ) -> Result<HidppMessage, ChannelError> {
488        self.send_with_timeout(msg, response_predicate, SEND_RESPONSE_TIMEOUT)
489            .await
490    }
491
492    /// Sends a HID++ message across the channel and waits for a response,
493    /// bounding the whole request — the report write plus the wait for a
494    /// matching response — by `timeout`.
495    ///
496    /// On elapse the request's pending entry is removed (concurrent in-flight
497    /// requests are unaffected) and [`ChannelError::Timeout`] is returned; a
498    /// response that still arrives later reaches message listeners as an
499    /// unmatched message.
500    ///
501    /// [`Self::send`] uses this with [`SEND_RESPONSE_TIMEOUT`], which suits
502    /// requests to a device that may be asleep. Requests that should fail
503    /// faster — e.g. probing a receiver that answers immediately or not at
504    /// all — can pass a tighter budget.
505    pub async fn send_with_timeout(
506        &self,
507        msg: HidppMessage,
508        response_predicate: impl Fn(&HidppMessage) -> bool + Send + 'static,
509        timeout: Duration,
510    ) -> Result<HidppMessage, ChannelError> {
511        let msg = self.normalize_outgoing(msg);
512        if !self.supports_msg(&msg) {
513            return Err(ChannelError::MessageTypeNotSupported);
514        }
515
516        // Wire trace (off by default; `OPENLOGI_LOG=hidpp=trace`). Capture the
517        // header before `msg` is moved into the send future so the outcome line
518        // below can name the same request.
519        let (dev, feat, func) = msg.header();
520        trace!(dev, feat, func, "hidpp request");
521
522        let (sender, receiver) = oneshot::channel::<HidppMessage>();
523        let pending_id = self.pending_message_id.fetch_add(1, Ordering::SeqCst);
524
525        {
526            let mut pending = self.pending_messages.lock().unwrap();
527            // Drop abandoned requests before queuing this one. Timeouts and
528            // write failures remove their entry eagerly below, but a caller
529            // cancelled mid-flight (an outer `timeout(..)` dropping the whole
530            // future) still leaves its `PendingMessage` behind. On a channel
531            // reused across inventory ticks those would accumulate unboundedly
532            // — and a late response could be mis-delivered to a recycled
533            // software id. `is_canceled()` is true once the receiver is gone,
534            // so this prunes exactly the give-ups.
535            pending.retain(|m| !m.sender.is_canceled());
536            pending.push_back(PendingMessage {
537                id: pending_id,
538                response_predicate: Box::new(response_predicate),
539                sender,
540            });
541        }
542
543        // The deadline covers the write as well: `write_report` has no
544        // bounded-time contract of its own, so a wedged device could otherwise
545        // park `send` forever before the response wait even starts.
546        let mut request = std::pin::pin!(
547            async {
548                self.send_and_forget(msg).await?;
549                receiver.await.map_err(|_| ChannelError::NoResponse)
550            }
551            .fuse()
552        );
553
554        let result = select! {
555            result = request => result,
556            _ = futures_timer::Delay::new(timeout).fuse() => Err(ChannelError::Timeout),
557        };
558
559        match &result {
560            Ok(_) => trace!(dev, feat, "hidpp response"),
561            Err(e) => trace!(dev, feat, error = ?e, "hidpp no response"),
562        }
563
564        if result.is_err() {
565            // A timeout or write failure leaves the entry queued — remove it
566            // eagerly. After a matched response the read thread has already
567            // taken it, so this is a no-op then.
568            self.remove_pending_message(pending_id);
569        }
570
571        result
572    }
573
574    fn remove_pending_message(&self, id: u64) {
575        let mut pending = self.pending_messages.lock().unwrap();
576        if let Some(pos) = pending.iter().position(|msg| msg.id == id) {
577            pending.remove(pos);
578        }
579    }
580
581    /// Sends a HID++ message across the channel and does not wait for a
582    /// response.
583    ///
584    /// If a response is expected, use [`Self::send`],
585    pub async fn send_and_forget(&self, msg: HidppMessage) -> Result<(), ChannelError> {
586        let msg = self.normalize_outgoing(msg);
587        if !self.supports_msg(&msg) {
588            return Err(ChannelError::MessageTypeNotSupported);
589        }
590
591        let mut buf = [0u8; LONG_REPORT_LENGTH];
592        let len = msg.write_raw(&mut buf);
593        self.raw_channel
594            .write_report(&buf[..len])
595            .await
596            .map(|_| ())
597            .map_err(ChannelError::Implementation)
598    }
599
600    /// Registers a listener that will be called for every incoming message.
601    ///
602    /// Returns a handle that can be used to remove the listener using a call to
603    /// [`Self::remove_msg_listener`].
604    pub fn add_msg_listener(
605        &self,
606        listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
607    ) -> u32 {
608        let mut listeners = self.message_listeners.lock().unwrap();
609
610        let mut rng = rand::rng();
611        let mut hdl = rng.random::<u32>();
612        while listeners.contains_key(&hdl) {
613            hdl = rng.random::<u32>();
614        }
615
616        listeners.insert(hdl, Arc::new(listener));
617        hdl
618    }
619
620    /// Registers a listener that is automatically removed when the returned
621    /// guard is dropped.
622    pub fn add_msg_listener_guarded(
623        &self,
624        listener: impl Fn(HidppMessage, bool) + Send + Sync + 'static,
625    ) -> MessageListenerGuard {
626        let hdl = self.add_msg_listener(listener);
627        MessageListenerGuard {
628            message_listeners: Arc::downgrade(&self.message_listeners),
629            hdl,
630        }
631    }
632
633    /// Removes a previously registered message listener.
634    ///
635    /// Returns whether a listener was found using the given handle.
636    pub fn remove_msg_listener(&self, hdl: u32) -> bool {
637        self.message_listeners
638            .lock()
639            .unwrap()
640            .remove(&hdl)
641            .is_some()
642    }
643}
644
645/// Represents an error that occurred when creating or interacting with a HID or
646/// HID++ communication channel.
647#[derive(Debug, Error)]
648#[non_exhaustive]
649pub enum ChannelError {
650    /// Indicates that the concrete implementation of [`RawHidChannel`] returned
651    /// an error.
652    #[error("the HID channel implementation returned an error")]
653    Implementation(#[from] Box<dyn Error + Sync + Send>),
654
655    /// Indicates that the HID report descriptor could not be parsed.
656    #[error("the report descriptor could not be parsed")]
657    ReportDescriptor(hidreport::ParserError),
658
659    /// Indicates that the channel in question does not support HID++.
660    #[error("the HID channel does not support HID++")]
661    HidppNotSupported,
662
663    /// Indicates that the HID++ channel does not support messages of the given
664    /// type (short/long).
665    #[error("the channel does not support the given HID++ message type")]
666    MessageTypeNotSupported,
667
668    /// Indicates that no response was received following a request.
669    #[error("the device did not respond to the request")]
670    NoResponse,
671
672    /// Indicates that a request did not complete within its time budget —
673    /// typically the device is asleep, out of range or connected to another
674    /// host. See [`HidppChannel::send_with_timeout`].
675    #[error("the request timed out before the device responded")]
676    Timeout,
677}
678
679/// Widen a short HID++ payload (6 bytes) to a long one (19 bytes): the HID++
680/// header bytes (device / feature / function|sw) sit at the same offsets in
681/// both widths, so the only change is zero-padding the trailing payload. Used
682/// to re-frame short messages as long on a long-only channel — see
683/// [`HidppChannel::normalize_outgoing`]. (OpenLogi local addition.)
684fn short_payload_as_long(payload: &[u8; SHORT_REPORT_LENGTH - 1]) -> [u8; LONG_REPORT_LENGTH - 1] {
685    let mut long = [0u8; LONG_REPORT_LENGTH - 1];
686    long[..payload.len()].copy_from_slice(payload);
687    long
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693    use std::{
694        io,
695        sync::{
696            Arc, Mutex,
697            atomic::{AtomicUsize, Ordering},
698        },
699        time::{Duration, Instant},
700    };
701
702    use crate::{
703        nibble,
704        protocol::v20::{self, ErrorType, Hidpp20Error},
705    };
706
707    #[test]
708    fn short_payload_widens_preserving_header_and_padding() {
709        // [device, feature, function|sw, p0, p1, p2]
710        let short = [0xff, 0x05, 0x1e, 0xaa, 0xbb, 0xcc];
711        let long = short_payload_as_long(&short);
712        assert_eq!(&long[..short.len()], &short[..]); // header + payload copied verbatim
713        assert!(long[short.len()..].iter().all(|&b| b == 0)); // remainder zero-padded
714        assert_eq!(long.len(), LONG_REPORT_LENGTH - 1);
715    }
716
717    #[test]
718    fn send_returns_response_before_timeout() {
719        futures::executor::block_on(async {
720            let (raw, handle) = MockRawHidChannel::new();
721            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
722
723            let request = short_msg(0x10);
724            let response = short_msg(0x20);
725            handle.queue_response(response);
726
727            let actual = channel
728                .send_with_timeout(
729                    request,
730                    move |candidate| *candidate == response,
731                    Duration::from_secs(1),
732                )
733                .await
734                .unwrap();
735
736            assert_eq!(actual, response);
737            assert_eq!(handle.written_reports().len(), 1);
738            assert_pending_empty(&channel);
739        });
740    }
741
742    #[test]
743    fn send_times_out_and_removes_pending_message() {
744        futures::executor::block_on(async {
745            let (raw, handle) = MockRawHidChannel::new();
746            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
747            let request = short_msg(0x10);
748            let response = short_msg(0x20);
749
750            let started = Instant::now();
751            let err = channel
752                .send_with_timeout(
753                    request,
754                    move |candidate| *candidate == response,
755                    Duration::from_millis(25),
756                )
757                .await
758                .unwrap_err();
759
760            assert!(matches!(err, ChannelError::Timeout));
761            assert!(started.elapsed() < Duration::from_secs(1));
762            assert_eq!(handle.written_reports().len(), 1);
763            assert_pending_empty(&channel);
764        });
765    }
766
767    #[test]
768    fn timeout_removes_only_its_own_pending_message() {
769        futures::executor::block_on(async {
770            let (raw, handle) = MockRawHidChannel::new();
771            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
772
773            let never_answered = short_msg(0x20);
774            let slow_response = short_msg(0x21);
775
776            let timed_out = channel.send_with_timeout(
777                short_msg(0x10),
778                move |candidate| *candidate == never_answered,
779                Duration::from_millis(25),
780            );
781            let answered = channel.send_with_timeout(
782                short_msg(0x11),
783                move |candidate| *candidate == slow_response,
784                Duration::from_secs(1),
785            );
786            // Answer the second request only after the first has timed out, so
787            // a removal that took the wrong entry would fail this test.
788            let respond_late = async {
789                futures_timer::Delay::new(Duration::from_millis(100)).await;
790                handle.send_incoming(slow_response).await;
791            };
792
793            let (timed_out, answered, ()) = futures::join!(timed_out, answered, respond_late);
794
795            assert!(matches!(timed_out.unwrap_err(), ChannelError::Timeout));
796            assert_eq!(answered.unwrap(), slow_response);
797            assert_pending_empty(&channel);
798        });
799    }
800
801    #[test]
802    fn late_response_after_timeout_is_ignored() {
803        futures::executor::block_on(async {
804            let (raw, handle) = MockRawHidChannel::new();
805            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
806            let events = Arc::new(Mutex::new(Vec::new()));
807            let listener_events = Arc::clone(&events);
808            channel.add_msg_listener(move |msg, matched| {
809                listener_events.lock().unwrap().push((msg, matched));
810            });
811
812            let request = short_msg(0x10);
813            let late_response = short_msg(0x20);
814            let err = channel
815                .send_with_timeout(
816                    request,
817                    move |candidate| *candidate == late_response,
818                    Duration::from_millis(25),
819                )
820                .await
821                .unwrap_err();
822
823            assert!(matches!(err, ChannelError::Timeout));
824            assert_pending_empty(&channel);
825
826            handle.send_incoming(late_response).await;
827            wait_for_event_count(&events, 1).await;
828            assert_eq!(events.lock().unwrap()[0], (late_response, false));
829            assert_pending_empty(&channel);
830
831            let later_request = short_msg(0x30);
832            let later_response = short_msg(0x40);
833            handle.queue_response(later_response);
834            let actual = channel
835                .send_with_timeout(
836                    later_request,
837                    move |candidate| *candidate == later_response,
838                    Duration::from_secs(1),
839                )
840                .await
841                .unwrap();
842
843            assert_eq!(actual, later_response);
844            wait_for_event_count(&events, 2).await;
845            assert_eq!(events.lock().unwrap()[1], (later_response, true));
846            assert_pending_empty(&channel);
847        });
848    }
849
850    #[test]
851    fn send_and_forget_writes_without_pending_message() {
852        futures::executor::block_on(async {
853            let (raw, handle) = MockRawHidChannel::new();
854            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
855
856            channel.send_and_forget(short_msg(0x10)).await.unwrap();
857
858            assert_eq!(handle.written_reports().len(), 1);
859            assert_pending_empty(&channel);
860        });
861    }
862
863    #[test]
864    fn listener_can_remove_another_listener_during_dispatch() {
865        futures::executor::block_on(async {
866            let (raw, handle) = MockRawHidChannel::new();
867            let channel = Arc::new(HidppChannel::from_raw_channel(raw).await.unwrap());
868            let removed_listener_calls = Arc::new(AtomicUsize::new(0));
869            let removing_listener_calls = Arc::new(AtomicUsize::new(0));
870
871            let removed_listener_calls_for_listener = Arc::clone(&removed_listener_calls);
872            let removed_hdl = channel.add_msg_listener(move |_, _| {
873                removed_listener_calls_for_listener.fetch_add(1, Ordering::SeqCst);
874            });
875
876            let channel_for_listener = Arc::clone(&channel);
877            let removing_listener_calls_for_listener = Arc::clone(&removing_listener_calls);
878            channel.add_msg_listener(move |_, _| {
879                removing_listener_calls_for_listener.fetch_add(1, Ordering::SeqCst);
880                channel_for_listener.remove_msg_listener(removed_hdl);
881            });
882
883            handle.send_incoming(short_msg(0x20)).await;
884            wait_for_atomic_count(&removing_listener_calls, 1).await;
885            wait_for_atomic_count(&removed_listener_calls, 1).await;
886
887            handle.send_incoming(short_msg(0x21)).await;
888            wait_for_atomic_count(&removing_listener_calls, 2).await;
889
890            assert_eq!(removed_listener_calls.load(Ordering::SeqCst), 1);
891        });
892    }
893
894    // --- HID++2.0 (v20) send/matcher characterization tests -----------------
895    //
896    // `HidppChannel::send`/`send_with_timeout` above are protocol-agnostic:
897    // they match on an arbitrary predicate over raw `HidppMessage`s. The
898    // v20-specific correlation logic (matching by header, splitting out error
899    // frames) lives in `protocol::v20::HidppChannel::send_v20`, which is built
900    // directly on top of `send`. These tests pin that logic's current
901    // behaviour using the same mock transport as the tests above.
902
903    #[test]
904    fn send_v20_matches_response_by_header_ignoring_unrelated_messages() {
905        futures::executor::block_on(async {
906            let (raw, handle) = MockRawHidChannel::new();
907            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
908
909            let header = v20::MessageHeader {
910                device_index: 0x01,
911                feature_index: 0x05,
912                function_id: U4::from_lo(0x2),
913                software_id: U4::from_lo(0x3),
914            };
915            let request = v20::Message::Short(header, [0x00, 0x00, 0x00]);
916            let response = v20::Message::Short(header, [0xaa, 0xbb, 0xcc]);
917
918            // Each decoy differs from the request in exactly one header field, so
919            // none of them may be mistaken for its response.
920            let wrong_device = v20::Message::Short(
921                v20::MessageHeader {
922                    device_index: 0x02,
923                    ..header
924                },
925                [0, 0, 0],
926            );
927            let wrong_feature = v20::Message::Short(
928                v20::MessageHeader {
929                    feature_index: 0x06,
930                    ..header
931                },
932                [0, 0, 0],
933            );
934            let wrong_sw_id = v20::Message::Short(
935                v20::MessageHeader {
936                    software_id: U4::from_lo(0x4),
937                    ..header
938                },
939                [0, 0, 0],
940            );
941
942            let send_fut = channel.send_v20(request);
943            let feed_fut = async {
944                handle.send_incoming(wrong_device.into()).await;
945                handle.send_incoming(wrong_feature.into()).await;
946                handle.send_incoming(wrong_sw_id.into()).await;
947                handle.send_incoming(response.into()).await;
948            };
949
950            let (result, ()) = futures::join!(send_fut, feed_fut);
951
952            assert_eq!(result.unwrap(), response);
953            assert_pending_empty(&channel);
954        });
955    }
956
957    #[test]
958    fn send_v20_broadcast_event_does_not_resolve_pending_request() {
959        futures::executor::block_on(async {
960            let (raw, handle) = MockRawHidChannel::new();
961            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
962            let events = Arc::new(Mutex::new(Vec::new()));
963            let listener_events = Arc::clone(&events);
964            channel.add_msg_listener(move |msg, matched| {
965                listener_events.lock().unwrap().push((msg, matched));
966            });
967
968            let header = v20::MessageHeader {
969                device_index: 0x01,
970                feature_index: 0x05,
971                function_id: U4::from_lo(0x2),
972                software_id: U4::from_lo(0x3),
973            };
974            let request = v20::Message::Short(header, [0, 0, 0]);
975            let response = v20::Message::Short(header, [0xaa, 0xbb, 0xcc]);
976
977            // Software ID 0 is reserved for unsolicited device notifications
978            // (see `feature::event_payload`). The request above uses a non-zero
979            // ID, so an incoming broadcast sharing device/feature but using ID 0
980            // must be routed to listeners, not consumed as this request's
981            // response.
982            let event = v20::Message::Short(
983                v20::MessageHeader {
984                    software_id: U4::from_lo(0x0),
985                    ..header
986                },
987                [0x01, 0x02, 0x03],
988            );
989
990            let send_fut = channel.send_v20(request);
991            let feed_fut = async {
992                handle.send_incoming(event.into()).await;
993                wait_for_event_count(&events, 1).await;
994                handle.send_incoming(response.into()).await;
995            };
996
997            let (result, ()) = futures::join!(send_fut, feed_fut);
998
999            assert_eq!(result.unwrap(), response);
1000            // The oneshot resolves before the listener loop runs on the read
1001            // thread; wait for both deliveries before asserting on them.
1002            wait_for_event_count(&events, 2).await;
1003            let recorded = events.lock().unwrap().clone();
1004            assert_eq!(
1005                recorded,
1006                vec![
1007                    (HidppMessage::from(event), false),
1008                    (HidppMessage::from(response), true),
1009                ]
1010            );
1011            assert_pending_empty(&channel);
1012        });
1013    }
1014
1015    #[test]
1016    fn send_v20_response_may_arrive_as_a_different_report_width() {
1017        futures::executor::block_on(async {
1018            let (raw, handle) = MockRawHidChannel::new();
1019            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
1020
1021            let header = v20::MessageHeader {
1022                device_index: 0x01,
1023                feature_index: 0x05,
1024                function_id: U4::from_lo(0x2),
1025                software_id: U4::from_lo(0x3),
1026            };
1027            let request = v20::Message::Short(header, [0, 0, 0]);
1028            // Quirk: `send_v20`'s response predicate compares only the parsed
1029            // v20 header, not the underlying report width. A device replying
1030            // with a long report to a short request — same header, wider
1031            // payload — is still accepted as the response.
1032            let response = v20::Message::Long(header, [0xaa; 16]);
1033            handle.queue_response(response.into());
1034
1035            let result = channel.send_v20(request).await.unwrap();
1036
1037            assert_eq!(result, response);
1038            assert_pending_empty(&channel);
1039        });
1040    }
1041
1042    #[test]
1043    fn send_v20_error_frame_resolves_to_feature_error() {
1044        futures::executor::block_on(async {
1045            let (raw, handle) = MockRawHidChannel::new();
1046            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
1047
1048            let header = v20::MessageHeader {
1049                device_index: 0x01,
1050                feature_index: 0x05,
1051                function_id: U4::from_lo(0x2),
1052                software_id: U4::from_lo(0x3),
1053            };
1054            let request = v20::Message::Short(header, [0, 0, 0]);
1055            let error_response = v20_error_frame(header, ErrorType::InvalidArgument.into());
1056            handle.queue_response(error_response.into());
1057
1058            let err = channel.send_v20(request).await.unwrap_err();
1059
1060            assert!(matches!(
1061                err,
1062                Hidpp20Error::Feature(ErrorType::InvalidArgument)
1063            ));
1064            assert_pending_empty(&channel);
1065        });
1066    }
1067
1068    #[test]
1069    fn send_v20_error_frame_with_unmapped_code_is_unsupported_response() {
1070        futures::executor::block_on(async {
1071            let (raw, handle) = MockRawHidChannel::new();
1072            let channel = HidppChannel::from_raw_channel(raw).await.unwrap();
1073
1074            let header = v20::MessageHeader {
1075                device_index: 0x01,
1076                feature_index: 0x05,
1077                function_id: U4::from_lo(0x2),
1078                software_id: U4::from_lo(0x3),
1079            };
1080            let request = v20::Message::Short(header, [0, 0, 0]);
1081            // 0xfe is not a defined `ErrorType` variant.
1082            let error_response = v20_error_frame(header, 0xfe);
1083            handle.queue_response(error_response.into());
1084
1085            let err = channel.send_v20(request).await.unwrap_err();
1086
1087            assert!(matches!(err, Hidpp20Error::UnsupportedResponse));
1088            assert_pending_empty(&channel);
1089        });
1090    }
1091
1092    /// Builds the HID++2.0 error-frame encoding for `request_header`: feature
1093    /// index 0xFF, with the original feature index and function|software byte
1094    /// shifted one byte to the right (see `v20::HidppChannel::send_v20`'s
1095    /// `is_error` predicate for the reverse mapping).
1096    fn v20_error_frame(request_header: v20::MessageHeader, error_code: u8) -> v20::Message {
1097        let error_header = v20::MessageHeader {
1098            device_index: request_header.device_index,
1099            feature_index: 0xff,
1100            function_id: U4::from_hi(request_header.feature_index),
1101            software_id: U4::from_lo(request_header.feature_index),
1102        };
1103        let mut payload = [0u8; 3];
1104        payload[0] = nibble::combine(request_header.function_id, request_header.software_id);
1105        payload[1] = error_code;
1106        v20::Message::Short(error_header, payload)
1107    }
1108
1109    #[derive(Clone)]
1110    struct MockRawHidHandle {
1111        incoming_tx: async_channel::Sender<Vec<u8>>,
1112        written_reports: Arc<Mutex<Vec<Vec<u8>>>>,
1113        responses_on_write: Arc<Mutex<VecDeque<Vec<u8>>>>,
1114    }
1115
1116    impl MockRawHidHandle {
1117        fn queue_response(&self, msg: HidppMessage) {
1118            self.responses_on_write
1119                .lock()
1120                .unwrap()
1121                .push_back(raw_report(msg));
1122        }
1123
1124        async fn send_incoming(&self, msg: HidppMessage) {
1125            self.incoming_tx.send(raw_report(msg)).await.unwrap();
1126        }
1127
1128        fn written_reports(&self) -> Vec<Vec<u8>> {
1129            self.written_reports.lock().unwrap().clone()
1130        }
1131    }
1132
1133    struct MockRawHidChannel {
1134        incoming_tx: async_channel::Sender<Vec<u8>>,
1135        incoming_rx: async_channel::Receiver<Vec<u8>>,
1136        written_reports: Arc<Mutex<Vec<Vec<u8>>>>,
1137        responses_on_write: Arc<Mutex<VecDeque<Vec<u8>>>>,
1138    }
1139
1140    impl MockRawHidChannel {
1141        fn new() -> (Self, MockRawHidHandle) {
1142            let (incoming_tx, incoming_rx) = async_channel::unbounded();
1143            let written_reports = Arc::new(Mutex::new(Vec::new()));
1144            let responses_on_write = Arc::new(Mutex::new(VecDeque::new()));
1145
1146            let handle = MockRawHidHandle {
1147                incoming_tx: incoming_tx.clone(),
1148                written_reports: Arc::clone(&written_reports),
1149                responses_on_write: Arc::clone(&responses_on_write),
1150            };
1151
1152            (
1153                Self {
1154                    incoming_tx,
1155                    incoming_rx,
1156                    written_reports,
1157                    responses_on_write,
1158                },
1159                handle,
1160            )
1161        }
1162    }
1163
1164    #[async_trait]
1165    impl RawHidChannel for MockRawHidChannel {
1166        fn vendor_id(&self) -> u16 {
1167            0x046d
1168        }
1169
1170        fn product_id(&self) -> u16 {
1171            0xc539
1172        }
1173
1174        async fn write_report(&self, src: &[u8]) -> Result<usize, Box<dyn Error + Sync + Send>> {
1175            self.written_reports.lock().unwrap().push(src.to_vec());
1176            let response = self.responses_on_write.lock().unwrap().pop_front();
1177            if let Some(response) = response {
1178                self.incoming_tx.send(response).await.unwrap();
1179            }
1180
1181            Ok(src.len())
1182        }
1183
1184        async fn read_report(&self, buf: &mut [u8]) -> Result<usize, Box<dyn Error + Sync + Send>> {
1185            let report = self.incoming_rx.recv().await.map_err(|_| mock_error())?;
1186            let len = report.len().min(buf.len());
1187            buf[..len].copy_from_slice(&report[..len]);
1188            Ok(len)
1189        }
1190
1191        fn supports_short_long_hidpp(&self) -> Option<(bool, bool)> {
1192            Some((true, true))
1193        }
1194
1195        async fn get_report_descriptor(
1196            &self,
1197            _buf: &mut [u8],
1198        ) -> Result<usize, Box<dyn Error + Sync + Send>> {
1199            unreachable!("mock declares HID++ support")
1200        }
1201    }
1202
1203    fn short_msg(marker: u8) -> HidppMessage {
1204        HidppMessage::Short([0xff, marker, 0x10, marker, marker, marker])
1205    }
1206
1207    fn raw_report(msg: HidppMessage) -> Vec<u8> {
1208        let mut buf = [0u8; LONG_REPORT_LENGTH];
1209        let len = msg.write_raw(&mut buf);
1210        buf[..len].to_vec()
1211    }
1212
1213    fn assert_pending_empty(channel: &HidppChannel) {
1214        assert!(channel.pending_messages.lock().unwrap().is_empty());
1215    }
1216
1217    async fn wait_for_event_count(events: &Arc<Mutex<Vec<(HidppMessage, bool)>>>, count: usize) {
1218        let started = Instant::now();
1219        while started.elapsed() < Duration::from_secs(1) {
1220            if events.lock().unwrap().len() >= count {
1221                return;
1222            }
1223            futures_timer::Delay::new(Duration::from_millis(10)).await;
1224        }
1225
1226        panic!("timed out waiting for {count} listener events");
1227    }
1228
1229    async fn wait_for_atomic_count(count: &AtomicUsize, expected: usize) {
1230        let started = Instant::now();
1231        while started.elapsed() < Duration::from_secs(1) {
1232            if count.load(Ordering::SeqCst) >= expected {
1233                return;
1234            }
1235            futures_timer::Delay::new(Duration::from_millis(10)).await;
1236        }
1237
1238        panic!("timed out waiting for atomic count {expected}");
1239    }
1240
1241    fn mock_error() -> Box<dyn Error + Sync + Send> {
1242        Box::new(io::Error::new(
1243            io::ErrorKind::BrokenPipe,
1244            "mock channel closed",
1245        ))
1246    }
1247}