use std::{
error::Error,
sync::{Arc, LazyLock},
};
use async_hid::{AsyncHidRead, AsyncHidWrite, DeviceInfo, DeviceReader, DeviceWriter, HidBackend};
use futures_lite::StreamExt as _;
use hidpp::{
async_trait,
channel::{HidppChannel, RawHidChannel},
};
use tokio::sync::Mutex;
use tracing::debug;
const LOGITECH_VID: u16 = 0x046d;
const HIDPP_LONG_COLLECTIONS: [(u16, u16, bool); 3] = [
(0xff00, 0x0002, false),
(0xff43, 0x0202, true),
(0xff43, 0x0602, false),
];
fn is_hidpp_long_collection(usage_page: u16, usage_id: u16) -> bool {
HIDPP_LONG_COLLECTIONS
.iter()
.any(|&(page, usage, _)| (page, usage) == (usage_page, usage_id))
}
fn is_long_only_collection(usage_page: u16, usage_id: u16) -> bool {
HIDPP_LONG_COLLECTIONS
.iter()
.any(|&(page, usage, long_only)| long_only && (page, usage) == (usage_page, usage_id))
}
static HID_BACKEND: LazyLock<HidBackend> = LazyLock::new(HidBackend::default);
pub(crate) async fn enumerate_hidpp_devices() -> Result<Vec<async_hid::Device>, async_hid::HidError>
{
let all: Vec<async_hid::Device> = HID_BACKEND.enumerate().await?.collect().await;
for d in all.iter().filter(|d| d.vendor_id == LOGITECH_VID) {
debug!(
name = %d.name,
pid = format_args!("{:04x}", d.product_id),
usage_page = format_args!("{:#06x}", d.usage_page),
usage_id = format_args!("{:#06x}", d.usage_id),
matched = is_hidpp_long_collection(d.usage_page, d.usage_id),
"logitech HID node"
);
}
Ok(all
.into_iter()
.filter(|d| {
d.vendor_id == LOGITECH_VID && is_hidpp_long_collection(d.usage_page, d.usage_id)
})
.collect())
}
pub(crate) async fn open_route_writer(
route: &crate::route::DeviceRoute,
) -> Result<Option<DeviceWriter>, async_hid::HidError> {
let crate::route::DeviceRoute::Direct {
vendor_id,
product_id,
} = route
else {
return Ok(None);
};
let candidates = enumerate_hidpp_devices().await?;
for dev in candidates {
if dev.vendor_id == *vendor_id && dev.product_id == *product_id {
let (_reader, writer) = dev.open().await?;
return Ok(Some(writer));
}
}
Ok(None)
}
pub(crate) async fn open_hidpp_channel(
dev: async_hid::Device,
) -> Result<Option<(DeviceInfo, Arc<HidppChannel>)>, async_hid::HidError> {
let info: DeviceInfo = (*dev).clone();
let (reader, writer) = dev.open().await?;
let long_only = is_long_only_collection(info.usage_page, info.usage_id);
let raw = AsyncHidChannel::new(reader, writer, info.clone(), long_only);
let channel = match HidppChannel::from_raw_channel(raw).await {
Ok(c) => Arc::new(c),
Err(e) => {
debug!(name = %info.name, error = ?e, "not a HID++ channel");
return Ok(None);
}
};
Ok(Some((info, channel)))
}
pub(crate) struct AsyncHidChannel {
reader: Mutex<DeviceReader>,
writer: Mutex<DeviceWriter>,
info: DeviceInfo,
long_only: bool,
}
impl AsyncHidChannel {
pub(crate) fn new(
reader: DeviceReader,
writer: DeviceWriter,
info: DeviceInfo,
long_only: bool,
) -> Self {
Self {
reader: Mutex::new(reader),
writer: Mutex::new(writer),
info,
long_only,
}
}
}
#[async_trait]
impl RawHidChannel for AsyncHidChannel {
fn vendor_id(&self) -> u16 {
self.info.vendor_id
}
fn product_id(&self) -> u16 {
self.info.product_id
}
async fn write_report(&self, src: &[u8]) -> Result<usize, Box<dyn Error + Send + Sync>> {
let mut w = self.writer.lock().await;
w.write_output_report(src).await?;
Ok(src.len())
}
async fn read_report(&self, buf: &mut [u8]) -> Result<usize, Box<dyn Error + Send + Sync>> {
let mut r = self.reader.lock().await;
Ok(r.read_input_report(buf).await?)
}
fn supports_short_long_hidpp(&self) -> Option<(bool, bool)> {
Some((!self.long_only, true))
}
async fn get_report_descriptor(
&self,
_buf: &mut [u8],
) -> Result<usize, Box<dyn Error + Send + Sync>> {
Err("get_report_descriptor is not implemented; pre-filter to HID++ usage pages".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_usb_ble_and_keyboard_hidpp_collections() {
assert!(is_hidpp_long_collection(0xff00, 0x0002)); assert!(is_hidpp_long_collection(0xff43, 0x0202)); assert!(is_hidpp_long_collection(0xff43, 0x0602)); assert!(!is_hidpp_long_collection(0x0001, 0x0002)); assert!(!is_hidpp_long_collection(0xff43, 0x0002)); }
#[test]
fn only_ble_collection_is_long_only() {
assert!(is_long_only_collection(0xff43, 0x0202)); assert!(!is_long_only_collection(0xff00, 0x0002)); assert!(!is_long_only_collection(0xff43, 0x0602)); assert!(!is_long_only_collection(0x0001, 0x0002)); }
}