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)];
const SHORT_REPORT_ID: u8 = 0x10;
const LONG_REPORT_ID: u8 = 0x11;
const LONG_REPORT_LEN: usize = 20;
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))
}
fn short_as_long(src: &[u8]) -> Option<[u8; LONG_REPORT_LEN]> {
if src.first() != Some(&SHORT_REPORT_ID) || src.len() > LONG_REPORT_LEN {
return None;
}
let mut long = [0u8; LONG_REPORT_LEN];
long[0] = LONG_REPORT_ID;
long[1..src.len()].copy_from_slice(&src[1..]);
Some(long)
}
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>> {
let mut w = self.writer.lock().await;
match self.long_only.then(|| short_as_long(src)).flatten() {
Some(long) => w.write_output_report(&long).await?,
None => w.write_output_report(src).await?,
}
Ok(src.len())
}
async fn read_report(&self, buf: &mut [u8]) -> Result<usize, Box<dyn Error>> {
let mut r = self.reader.lock().await;
Ok(r.read_input_report(buf).await?)
}
fn supports_short_long_hidpp(&self) -> Option<(bool, bool)> {
Some((true, true))
}
async fn get_report_descriptor(&self, _buf: &mut [u8]) -> Result<usize, Box<dyn Error>> {
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)); }
#[test]
fn upconverts_short_report_preserving_header_and_padding() {
let short = [SHORT_REPORT_ID, 0xff, 0x00, 0x1e, 0xaa, 0xbb, 0xcc];
let Some(long) = short_as_long(&short) else {
panic!("short report should up-convert");
};
assert_eq!(long[0], LONG_REPORT_ID);
assert_eq!(&long[1..7], &short[1..]); assert!(long[7..].iter().all(|&b| b == 0)); assert_eq!(long.len(), LONG_REPORT_LEN);
}
#[test]
fn passes_through_non_short_reports() {
let long_in = [LONG_REPORT_ID, 0xff, 0x00, 0x1e];
assert!(short_as_long(&long_in).is_none());
assert!(short_as_long(&[]).is_none());
}
}