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