async-hid 0.3.0

A async library for interacting with HID devices
Documentation
mod device;
mod manager;
mod runloop;
mod service;
mod utils;

use std::sync::Arc;

use async_channel::{bounded, Receiver, TrySendError};
use bytes::{BufMut, Bytes, BytesMut};
use core_foundation::array::CFArray;
use core_foundation::base::TCFType;
use core_foundation::dictionary::CFDictionary;
use core_foundation::runloop::{kCFRunLoopDefaultMode, CFRunLoop};
use core_foundation::string::CFString;
use futures_lite::stream::iter;
use futures_lite::StreamExt;
use io_kit_sys::hid::keys::*;
use io_kit_sys::types::IOOptionBits;

use crate::backend::iohidmanager::device::{CallbackGuard, IOHIDDevice};
use crate::backend::iohidmanager::manager::IOHIDManager;
use crate::backend::iohidmanager::runloop::RunLoop;
use crate::backend::iohidmanager::service::{IOService, RegistryEntryId};
use crate::backend::iohidmanager::utils::{CFDictionaryExt};
use crate::{AsyncHidRead, AsyncHidWrite, DeviceId, DeviceInfo, HidError, HidResult};
use crate::backend::{Backend, DeviceInfoStream};
use crate::utils::TryIterExt;

#[derive(Default)]
pub struct IoHidManagerBackend;

impl Backend for IoHidManagerBackend {
    type Reader = InputReceiver;
    type Writer = Arc<BackendDevice>;

    async fn enumerate(&self) -> HidResult<DeviceInfoStream> {
        let mut manager = IOHIDManager::new()?;
        let devices = manager
            .get_devices()?
            .into_iter()
            .map(get_device_infos)
            .try_flatten();

        Ok(iter(devices).boxed())
    }

    async fn open(&self, id: &DeviceId, read: bool, write: bool) -> HidResult<(Option<Self::Reader>, Option<Self::Writer>)> {
        let id = match id {
            DeviceId::RegistryEntryId(id) => RegistryEntryId(*id)
        };
        let open_options = 0;
        let device = IOHIDDevice::try_from(id)?;
        device.open(open_options)?;
        let device = Arc::new(BackendDevice {
            device,
            open_options,
        });
        
        let input_receiver = if read {
            Some(InputReceiver::new(device.clone()).await?)
        } else {
            None
        };

        Ok((input_receiver, write.then_some(device)))
    }
}


fn get_device_infos(device: IOHIDDevice) -> HidResult<Vec<DeviceInfo>> {
    let primary_usage_page = device.get_i32_property(kIOHIDPrimaryUsagePageKey)? as u16;
    let primary_usage = device.get_i32_property(kIOHIDPrimaryUsageKey)? as u16;
    let vendor_id = device.get_i32_property(kIOHIDVendorIDKey)? as u16;
    let product_id = device.get_i32_property(kIOHIDProductIDKey)? as u16;
    let serial_number = device.get_string_property(kIOHIDProductKey).ok();
    let name = device.get_string_property(kIOHIDProductKey)?;
    let id = IOService::try_from(&device).and_then(|i| i.get_registry_entry_id())?;

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

    let mut results = Vec::new();
    results.extend(
        device
            .property::<CFArray>(kIOHIDDeviceUsagePairsKey)?
            .iter()
            .map(|i| unsafe { CFDictionary::wrap_under_get_rule(*i as _) })
            .filter_map(|dict| {
                let usage = dict.lookup_i32(kIOHIDDeviceUsageKey).ok()? as u16;
                let usage_page = dict.lookup_i32(kIOHIDDeviceUsagePageKey).ok()? as u16;
                Some((usage, usage_page))
            })
            .filter(|(usage, usage_page)| (*usage_page != primary_usage_page) || (*usage != primary_usage))
            .map(|(usage_id, usage_page)| DeviceInfo {
                usage_id,
                usage_page,
                ..info.clone()
            })
    );
    results.push(info.clone());

    Ok(results)
}

pub struct InputReceiver {
    device: Arc<BackendDevice>,
    run_loop: Arc<RunLoop>,
    _callback: CallbackGuard,
    read_channel: Receiver<Bytes>
}

impl InputReceiver {
    async fn new(device: Arc<BackendDevice>) -> HidResult<Self> {
        let mut byte_buffer = BytesMut::with_capacity(1024);
        let (sender, receiver) = bounded(64);

        let drain = receiver.clone();
        let callback = device.device.register_input_report_callback(move |report| {
            byte_buffer.put(report);
            let mut bytes = byte_buffer.split().freeze();
            while let Err(TrySendError::Full(ret)) = sender.try_send(bytes) {
                log::trace!("Dropping previous input report because the queue is full");
                let _ = drain.try_recv();
                bytes = ret;
            }
        })?;
        let run_loop = RunLoop::get_run_loop().await?;
        run_loop.schedule_device(&device.device)?;

        Ok(Self {
            device,
            run_loop,
            _callback: callback,
            read_channel: receiver
        })
    }

    fn stop(&self, device: &IOHIDDevice) {
        self.run_loop
            .unschedule_device(device)
            .unwrap_or_else(|_| log::warn!("Failed to unschedule IOHIDDevice from run loop"));
        let default_mode = unsafe { CFString::wrap_under_create_rule(kCFRunLoopDefaultMode) };
        device.schedule_with_runloop(&CFRunLoop::get_main(), &default_mode);
    }

    async fn recv(&self) -> HidResult<Bytes> {
        self.read_channel
            .recv()
            .await
            .map_err(|_| HidError::message("Input report callback got dropped unexpectedly"))
    }
}

impl AsyncHidRead for InputReceiver {
    async fn read_input_report<'a>(&'a mut self, buf: &'a mut [u8]) -> HidResult<usize> {
        let bytes = self
            .recv()
            .await?;
        let length = bytes.len().min(buf.len());
        buf[..length].copy_from_slice(&bytes[..length]);
        Ok(length)
    }
}

impl Drop for InputReceiver {
    fn drop(&mut self) {
        self.stop(&self.device.device)
    }
}

pub struct BackendDevice {
    device: IOHIDDevice,
    open_options: IOOptionBits
}

impl Drop for BackendDevice {
    fn drop(&mut self) {
        self.device
            .close(self.open_options)
            .unwrap_or_else(|err| log::warn!("Failed to close IOHIDDevice\n\t{err:?}"));
    }
}

impl AsyncHidWrite for Arc<BackendDevice> {
    async fn write_output_report<'a>(&'a mut self, buf: &'a [u8]) -> HidResult<()> {
        let report_id = buf[0];
        let data_to_send = if report_id == 0x0 { &buf[1..] } else { buf };

        self.device.set_report(1, report_id as _, data_to_send)
    }
}