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