use std::{error::Error, sync::Arc};
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); 2] =
[(0xff00, 0x0002, false), (0xff43, 0x0202, true)];
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))
}
pub(crate) async fn enumerate_hidpp_devices() -> Result<Vec<async_hid::Device>, async_hid::HidError>
{
let backend = HidBackend::default();
let all: Vec<async_hid::Device> = 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_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_both_usb_and_ble_hidpp_collections() {
assert!(is_hidpp_long_collection(0xff00, 0x0002)); assert!(is_hidpp_long_collection(0xff43, 0x0202)); 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(0x0001, 0x0002)); }
}