async-hid 0.3.0

A async library for interacting with HID devices
Documentation
mod descriptor;
mod ioctl;

use std::fs::{read_dir, read_to_string, OpenOptions};
use std::os::fd::{AsRawFd, OwnedFd};
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use futures_lite::stream::iter;
use futures_lite::StreamExt;
use nix::fcntl::OFlag;
use nix::unistd::{read, write};

use crate::backend::hidraw::descriptor::HidrawReportDescriptor;
use crate::utils::TryIterExt;
use crate::{ensure, AsyncHidRead, AsyncHidWrite, DeviceId, DeviceInfo, HidError, HidResult};
use crate::backend::{Backend, DeviceInfoStream};
use crate::backend::hidraw::async_api::{read_with, write_with, AsyncFd};
use crate::backend::hidraw::ioctl::hidraw_ioc_grdescsize;

#[derive(Default)]
pub struct HidRawBackend;

impl Backend for HidRawBackend {
    type Reader = HidDevice;
    type Writer = HidDevice;

    async fn enumerate(&self) -> HidResult<DeviceInfoStream> {
        let devices = read_dir("/sys/class/hidraw/")?
            .map(|r| r.map(|e| e.path()))
            .try_collect_vec()?;
        let devices = devices
            .into_iter()
            .map(get_device_info_raw)
            .try_flatten();
        Ok(iter(devices).boxed())
    }

    async fn open(&self, id: &DeviceId, read: bool, write: bool) -> HidResult<(Option<Self::Reader>, Option<Self::Writer>)> {
        let DeviceId::DevPath(id) = id;
        
        let fd: OwnedFd = OpenOptions::new()
            .read(read)
            .write(write)
            .custom_flags((OFlag::O_CLOEXEC | OFlag::O_NONBLOCK).bits())
            .open(id)?
            .into();

        let mut size = 0i32;
        unsafe { hidraw_ioc_grdescsize(fd.as_raw_fd(), &mut size) }
            .map_err(|e| HidError::message(format!("ioctl(GRDESCSIZE) error for {:?}, not a HIDRAW device?: {}", id, e)))?;
        
        let device = HidDevice(Arc::new(AsyncFd::new(fd)?));
        
        Ok((read.then(|| device.clone()), write.then(|| device.clone())))
    }
}

fn get_device_info_raw(path: PathBuf) -> HidResult<Vec<DeviceInfo>> {
    let properties = read_to_string(path.join("uevent"))?;
    let id = read_property(&properties, "DEVNAME")
        .ok_or(HidError::message("Can't find dev name"))
        .and_then(mange_dev_name)?;

    let properties = read_to_string(path.join("device/uevent"))?;

    let (_bus, vendor_id, product_id) = read_property(&properties, "HID_ID")
        .and_then(parse_hid_vid_pid)
        .ok_or(HidError::message("Can't find hid ids"))?;

    let name = read_property(&properties, "HID_NAME")
        .ok_or(HidError::message("Can't find hid name"))?
        .to_string();

    let serial_number = read_property(&properties, "HID_UNIQ")
        .filter(|s| !s.is_empty())
        .map(str::to_string);

    let info = DeviceInfo {
        id: DeviceId::DevPath(id),
        name,
        product_id,
        vendor_id,
        usage_id: 0,
        usage_page: 0,
        serial_number,
    };

    let results = HidrawReportDescriptor::from_syspath(&path)
        .map(|descriptor| {
            descriptor
                .usages()
                .map(|(usage_page, usage_id)| DeviceInfo {
                    usage_page,
                    usage_id,
                    ..info.clone()
                })
                .collect()
        })
        .unwrap_or_else(|_| vec![info]);
    Ok(results)
}

fn read_property<'a>(properties: &'a str, key: &str) -> Option<&'a str> {
    properties
        .lines()
        .filter_map(|l| l.split_once('='))
        .find_map(|(k, v)| (k == key).then_some(v))
}

fn mange_dev_name(dev_name: &str) -> HidResult<PathBuf> {
    let path = Path::new(dev_name);
    if path.is_absolute() {
        ensure!(
            dev_name
                .strip_prefix("/dev/")
                .is_some_and(|z| !z.is_empty()),
            HidError::message("Absolute device paths must start with /dev/")
        );
        Ok(path.to_path_buf())
    } else {
        Ok(Path::new("/dev/").join(path))
    }
}

fn parse_hid_vid_pid(s: &str) -> Option<(u16, u16, u16)> {
    let mut elems = s.split(':').filter_map(|s| u16::from_str_radix(s, 16).ok());
    let devtype = elems.next()?;
    let vendor = elems.next()?;
    let product = elems.next()?;

    Some((devtype, vendor, product))
}

#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct HidDevice(Arc<AsyncFd>);

impl AsyncHidRead for HidDevice {
    async fn read_input_report<'a>(&'a mut self, buf: &'a mut [u8]) -> HidResult<usize> {
        read_with(&self.0, |fd| read(fd.as_raw_fd(), buf).map_err(std::io::Error::from))
            .await
            .map_err(HidError::from)
    }
}

impl AsyncHidWrite for HidDevice {
    async fn write_output_report<'a>(&'a mut self, buf: &'a [u8]) -> HidResult<()> {
        write_with(&self.0, |fd| write(fd, buf).map_err(std::io::Error::from))
            .await
            .map_err(HidError::from)
            .map(|i| debug_assert_eq!(i, buf.len()))
    }
}

#[cfg(all(feature = "async-io", feature = "tokio"))]
compile_error!("Only tokio or async-io can be active at the same time");

#[cfg(feature = "async-io")]
mod async_api {
    use std::os::fd::OwnedFd;
    use async_io::Async;

    pub type AsyncFd = Async<OwnedFd>;

    pub async fn read_with<R>(inner: &AsyncFd, op: impl FnMut(&OwnedFd) -> std::io::Result<R>) -> std::io::Result<R> {
        inner.read_with(op).await
    }

    pub async fn write_with<R>(inner: &AsyncFd, op: impl FnMut(&OwnedFd) -> std::io::Result<R>) -> std::io::Result<R> {
        inner.write_with(op).await
    }
}

#[cfg(feature = "tokio")]
mod async_api {
    use std::os::fd::OwnedFd;
    use tokio::io::Interest;

    pub type AsyncFd = tokio::io::unix::AsyncFd<OwnedFd>;

    pub async fn read_with<R>(inner: &AsyncFd, op: impl FnMut(&OwnedFd) -> std::io::Result<R>) -> std::io::Result<R> {
        inner.async_io(Interest::READABLE, op).await
    }

    pub async fn write_with<R>(inner: &AsyncFd, op: impl FnMut(&OwnedFd) -> std::io::Result<R>) -> std::io::Result<R> {
        inner.async_io(Interest::WRITABLE, op).await
    }
}