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