Skip to content

Commit cd4de32

Browse files
committed
fix(hid): expose HID transport liveness
1 parent bdaa2e3 commit cd4de32

2 files changed

Lines changed: 41 additions & 3 deletions

File tree

crates/openlogi-hid/src/transport.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
1111
#[cfg(not(target_os = "windows"))]
1212
use std::error::Error;
13+
#[cfg(not(target_os = "windows"))]
14+
use std::sync::atomic::{AtomicBool, Ordering};
1315
use std::sync::{Arc, LazyLock};
1416

1517
#[cfg(not(target_os = "windows"))]
@@ -266,6 +268,7 @@ pub(crate) struct AsyncHidChannel {
266268
reader: Mutex<DeviceReader>,
267269
writer: Mutex<DeviceWriter>,
268270
info: DeviceInfo,
271+
connected: AtomicBool,
269272
/// Whether the device exposes only the long HID++ report (a BLE-direct
270273
/// peripheral on macOS). Reported via `supports_short_long_hidpp` so the
271274
/// `hidpp` channel up-converts outgoing short messages to long.
@@ -284,9 +287,16 @@ impl AsyncHidChannel {
284287
reader: Mutex::new(reader),
285288
writer: Mutex::new(writer),
286289
info,
290+
connected: AtomicBool::new(true),
287291
long_only,
288292
}
289293
}
294+
295+
fn mark_disconnected(&self) {
296+
if self.connected.swap(false, Ordering::AcqRel) {
297+
debug!(name = %self.info.name, "HID channel disconnected");
298+
}
299+
}
290300
}
291301

292302
#[cfg(not(target_os = "windows"))]
@@ -302,8 +312,15 @@ impl RawHidChannel for AsyncHidChannel {
302312

303313
async fn write_report(&self, src: &[u8]) -> Result<usize, Box<dyn Error + Send + Sync>> {
304314
let mut w = self.writer.lock().await;
305-
w.write_output_report(src).await?;
306-
Ok(src.len())
315+
match w.write_output_report(src).await {
316+
Ok(()) => Ok(src.len()),
317+
Err(e) => {
318+
if matches!(e, async_hid::HidError::Disconnected) {
319+
self.mark_disconnected();
320+
}
321+
Err(e.into())
322+
}
323+
}
307324
}
308325

309326
async fn read_report(&self, buf: &mut [u8]) -> Result<usize, Box<dyn Error + Send + Sync>> {
@@ -320,11 +337,18 @@ impl RawHidChannel for AsyncHidChannel {
320337
// until the inventory watcher evicts the channel), so park instead.
321338
// The contract guarantees every caller races this future against
322339
// the channel's close signal, which tears the read down on drop.
323-
Err(async_hid::HidError::Disconnected) => std::future::pending().await,
340+
Err(async_hid::HidError::Disconnected) => {
341+
self.mark_disconnected();
342+
std::future::pending().await
343+
}
324344
Err(e) => Err(e.into()),
325345
}
326346
}
327347

348+
fn is_connected(&self) -> bool {
349+
self.connected.load(Ordering::Acquire)
350+
}
351+
328352
fn supports_short_long_hidpp(&self) -> Option<(bool, bool)> {
329353
// USB / receiver collections carry both reports; BLE-direct collections
330354
// are long-only (no short report on macOS), where the `hidpp` channel

crates/openlogi-hidpp/src/channel.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ pub trait RawHidChannel: Sync + Send + 'static {
9696
/// must do the same and must not await `read_report` bare.
9797
async fn read_report(&self, buf: &mut [u8]) -> Result<usize, Box<dyn Error + Sync + Send>>;
9898

99+
/// Whether the underlying device connection is still usable.
100+
///
101+
/// Implementations that can detect a permanent disconnect should override
102+
/// this. The default preserves the behavior of transports that cannot
103+
/// report connection state.
104+
fn is_connected(&self) -> bool {
105+
true
106+
}
107+
99108
/// If the implementation already knows whether the underlying HID channel
100109
/// supports HID++ messages, it should return `Some((supports_short,
101110
/// supports_long))` from this method.
@@ -402,6 +411,11 @@ impl HidppChannel {
402411
})
403412
}
404413

414+
/// Whether the underlying HID transport still reports a live connection.
415+
pub fn is_connected(&self) -> bool {
416+
self.raw_channel.is_connected()
417+
}
418+
405419
/// Sets the software ID that should be returned by the next call to
406420
/// [`Self::get_sw_id`].
407421
///

0 commit comments

Comments
 (0)