use std::collections::VecDeque;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
use std::task::Waker;
use std::thread;
use std::time::{Duration, Instant};
use core_foundation_sys::array::{
CFArrayGetCount, CFArrayGetTypeID, CFArrayGetValueAtIndex, CFArrayRef,
};
use core_foundation_sys::base::{
kCFAllocatorDefault, CFAllocatorRef, CFGetTypeID, CFIndex, CFRelease, CFTypeRef,
};
use core_foundation_sys::data::{CFDataGetBytePtr, CFDataGetLength, CFDataGetTypeID, CFDataRef};
use core_foundation_sys::dictionary::{
CFDictionaryGetTypeID, CFDictionaryGetValue, CFDictionaryRef, CFMutableDictionaryRef,
};
use core_foundation_sys::number::{
kCFNumberSInt32Type, CFNumberGetTypeID, CFNumberGetValue, CFNumberRef,
};
use core_foundation_sys::runloop::{
kCFRunLoopDefaultMode, kCFRunLoopRunFinished, kCFRunLoopRunStopped, CFRunLoopGetCurrent,
CFRunLoopGetMain, CFRunLoopRef, CFRunLoopRunInMode, CFRunLoopStop, CFRunLoopWakeUp,
};
use core_foundation_sys::set::{CFSetGetCount, CFSetGetValues, CFSetRef};
use core_foundation_sys::string::{
kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringGetCString, CFStringGetCStringPtr,
CFStringGetLength, CFStringGetMaximumSizeForEncoding, CFStringGetTypeID, CFStringRef,
};
use crate::error::{HidError, HidResult};
use crate::{BusType, DeviceInfo};
#[allow(non_camel_case_types)]
type io_object_t = u32; #[allow(non_camel_case_types)]
type io_service_t = io_object_t;
type IOOptionBits = u32;
type IOReturn = i32; type IOHIDManagerRef = *mut c_void;
type IOHIDDeviceRef = *mut c_void;
type IOHIDReportType = u32;
type IOHIDReportCallback = unsafe extern "C" fn(
context: *mut c_void,
result: IOReturn,
sender: *mut c_void,
report_type: IOHIDReportType,
report_id: u32,
report: *mut u8,
report_length: CFIndex,
);
type IOHIDCallback =
unsafe extern "C" fn(context: *mut c_void, result: IOReturn, sender: *mut c_void);
#[link(name = "IOKit", kind = "framework")]
extern "C" {
fn IOHIDManagerCreate(allocator: CFAllocatorRef, options: IOOptionBits) -> IOHIDManagerRef;
fn IOHIDManagerSetDeviceMatching(manager: IOHIDManagerRef, matching: CFDictionaryRef);
fn IOHIDManagerCopyDevices(manager: IOHIDManagerRef) -> CFSetRef;
fn IOHIDDeviceCreate(allocator: CFAllocatorRef, service: io_service_t) -> IOHIDDeviceRef;
fn IOHIDDeviceOpen(device: IOHIDDeviceRef, options: IOOptionBits) -> IOReturn;
fn IOHIDDeviceClose(device: IOHIDDeviceRef, options: IOOptionBits) -> IOReturn;
fn IOHIDDeviceGetProperty(device: IOHIDDeviceRef, key: CFStringRef) -> CFTypeRef;
fn IOHIDDeviceSetReport(
device: IOHIDDeviceRef,
report_type: IOHIDReportType,
report_id: CFIndex,
report: *const u8,
report_length: CFIndex,
) -> IOReturn;
fn IOHIDDeviceGetReport(
device: IOHIDDeviceRef,
report_type: IOHIDReportType,
report_id: CFIndex,
report: *mut u8,
report_length: *mut CFIndex,
) -> IOReturn;
fn IOHIDDeviceRegisterInputReportCallback(
device: IOHIDDeviceRef,
report: *mut u8,
report_length: CFIndex,
callback: Option<IOHIDReportCallback>,
context: *mut c_void,
);
fn IOHIDDeviceRegisterRemovalCallback(
device: IOHIDDeviceRef,
callback: Option<IOHIDCallback>,
context: *mut c_void,
);
fn IOHIDDeviceScheduleWithRunLoop(
device: IOHIDDeviceRef,
run_loop: CFRunLoopRef,
run_loop_mode: CFStringRef,
);
fn IOHIDDeviceUnscheduleFromRunLoop(
device: IOHIDDeviceRef,
run_loop: CFRunLoopRef,
run_loop_mode: CFStringRef,
);
fn IOHIDDeviceGetService(device: IOHIDDeviceRef) -> io_service_t;
fn IORegistryEntryGetRegistryEntryID(entry: io_object_t, entry_id: *mut u64) -> IOReturn;
fn IORegistryEntryIDMatching(entry_id: u64) -> CFMutableDictionaryRef;
fn IORegistryEntrySearchCFProperty(
entry: io_service_t,
plane: *const c_char,
key: CFStringRef,
allocator: CFAllocatorRef,
options: u32,
) -> CFTypeRef;
fn IOServiceGetMatchingService(
main_port: io_object_t,
matching: CFDictionaryRef,
) -> io_service_t;
fn IOObjectRelease(object: io_object_t) -> IOReturn;
}
const IO_RETURN_SUCCESS: IOReturn = 0;
const IO_REGISTRY_ITERATE_PARENTS_RECURSIVELY: u32 = 0x1 | 0x2;
const IO_MASTER_PORT_DEFAULT: io_object_t = 0;
const IOHID_OPTIONS_TYPE_NONE: IOOptionBits = 0;
const IOHID_OPTIONS_TYPE_SEIZE_DEVICE: IOOptionBits = 1;
const IOHID_MANAGER_OPTION_NONE: IOOptionBits = 0;
const IOHID_REPORT_TYPE_INPUT: IOHIDReportType = 0;
const IOHID_REPORT_TYPE_OUTPUT: IOHIDReportType = 1;
const IOHID_REPORT_TYPE_FEATURE: IOHIDReportType = 2;
const KEY_VENDOR_ID: &str = "VendorID"; const KEY_PRODUCT_ID: &str = "ProductID"; const KEY_SERIAL_NUMBER: &str = "SerialNumber"; const KEY_MANUFACTURER: &str = "Manufacturer"; const KEY_PRODUCT: &str = "Product"; const KEY_VERSION_NUMBER: &str = "VersionNumber"; const KEY_PRIMARY_USAGE_PAGE: &str = "PrimaryUsagePage"; const KEY_PRIMARY_USAGE: &str = "PrimaryUsage"; const KEY_DEVICE_USAGE_PAIRS: &str = "DeviceUsagePairs"; const KEY_DEVICE_USAGE_PAGE: &str = "DeviceUsagePage"; const KEY_DEVICE_USAGE: &str = "DeviceUsage"; const KEY_TRANSPORT: &str = "Transport"; const KEY_MAX_INPUT_REPORT_SIZE: &str = "MaxInputReportSize"; const KEY_REPORT_DESCRIPTOR: &str = "ReportDescriptor";
const PATH_PREFIX: &str = "DevSrvsID:";
const MAX_QUEUED_REPORTS: usize = 30;
fn parse_dev_srvs_id(path: &str) -> Option<u64> {
path.strip_prefix(PATH_PREFIX)?.parse().ok()
}
fn format_dev_srvs_id(entry_id: u64) -> String {
format!("{PATH_PREFIX}{entry_id}")
}
fn bus_type_from_transport(transport: &str) -> BusType {
match transport {
"USB" => BusType::Usb,
"Bluetooth" | "BluetoothLowEnergy" | "Bluetooth Low Energy" => BusType::Bluetooth,
"I2C" => BusType::I2c,
"SPI" => BusType::Spi,
_ => BusType::Unknown,
}
}
unsafe fn cfstr(s: &str) -> CFStringRef {
let c = CString::new(s).expect("CF key contains NUL");
CFStringCreateWithCString(kCFAllocatorDefault, c.as_ptr(), kCFStringEncodingUTF8)
}
unsafe fn cfstring_to_string(value: CFTypeRef) -> Option<String> {
if value.is_null() || CFGetTypeID(value) != CFStringGetTypeID() {
return None;
}
let s = value as CFStringRef;
let ptr = CFStringGetCStringPtr(s, kCFStringEncodingUTF8);
if !ptr.is_null() {
return Some(CStr::from_ptr(ptr).to_string_lossy().into_owned());
}
let max = CFStringGetMaximumSizeForEncoding(CFStringGetLength(s), kCFStringEncodingUTF8) + 1;
let mut buf = vec![0u8; max as usize];
if CFStringGetCString(
s,
buf.as_mut_ptr() as *mut c_char,
max,
kCFStringEncodingUTF8,
) == 0
{
return None;
}
let nul = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
buf.truncate(nul);
String::from_utf8(buf).ok()
}
unsafe fn cfnumber_to_i32(value: CFTypeRef) -> Option<i32> {
if value.is_null() || CFGetTypeID(value) != CFNumberGetTypeID() {
return None;
}
let mut out: i32 = 0;
CFNumberGetValue(
value as CFNumberRef,
kCFNumberSInt32Type,
&mut out as *mut i32 as *mut c_void,
)
.then_some(out)
}
unsafe fn device_property(device: IOHIDDeviceRef, key: &str) -> CFTypeRef {
let key = cfstr(key);
let value = IOHIDDeviceGetProperty(device, key);
CFRelease(key as CFTypeRef);
value
}
unsafe fn string_property(device: IOHIDDeviceRef, key: &str) -> Option<String> {
cfstring_to_string(device_property(device, key))
}
unsafe fn i32_property(device: IOHIDDeviceRef, key: &str) -> Option<i32> {
cfnumber_to_i32(device_property(device, key))
}
unsafe fn registry_entry_id(device: IOHIDDeviceRef) -> Option<u64> {
let service = IOHIDDeviceGetService(device);
if service == 0 {
return None;
}
let mut entry_id = 0u64;
(IORegistryEntryGetRegistryEntryID(service, &mut entry_id) == IO_RETURN_SUCCESS)
.then_some(entry_id)
}
unsafe fn usb_interface_number(device: IOHIDDeviceRef) -> i32 {
let service = IOHIDDeviceGetService(device);
if service == 0 {
return -1;
}
let plane = b"IOService\0";
let key = cfstr("bInterfaceNumber");
let value = IORegistryEntrySearchCFProperty(
service,
plane.as_ptr() as *const c_char,
key,
kCFAllocatorDefault,
IO_REGISTRY_ITERATE_PARENTS_RECURSIVELY,
);
CFRelease(key as CFTypeRef);
if value.is_null() {
return -1;
}
let n = cfnumber_to_i32(value).unwrap_or(-1);
CFRelease(value);
n
}
unsafe fn device_infos(device: IOHIDDeviceRef) -> Vec<DeviceInfo> {
let info = DeviceInfo {
path: registry_entry_id(device)
.map(format_dev_srvs_id)
.unwrap_or_default(),
vendor_id: i32_property(device, KEY_VENDOR_ID).unwrap_or(0) as u16,
product_id: i32_property(device, KEY_PRODUCT_ID).unwrap_or(0) as u16,
serial_number: string_property(device, KEY_SERIAL_NUMBER),
release_number: i32_property(device, KEY_VERSION_NUMBER).unwrap_or(0) as u16,
manufacturer_string: string_property(device, KEY_MANUFACTURER),
product_string: string_property(device, KEY_PRODUCT),
usage_page: i32_property(device, KEY_PRIMARY_USAGE_PAGE).unwrap_or(0) as u16,
usage: i32_property(device, KEY_PRIMARY_USAGE).unwrap_or(0) as u16,
interface_number: usb_interface_number(device),
bus_type: string_property(device, KEY_TRANSPORT)
.as_deref()
.map(bus_type_from_transport)
.unwrap_or(BusType::Unknown),
};
let mut entries = vec![info.clone()];
let pairs = device_property(device, KEY_DEVICE_USAGE_PAIRS);
if !pairs.is_null() && CFGetTypeID(pairs) == CFArrayGetTypeID() {
let pairs = pairs as CFArrayRef;
let page_key = cfstr(KEY_DEVICE_USAGE_PAGE);
let usage_key = cfstr(KEY_DEVICE_USAGE);
for i in 0..CFArrayGetCount(pairs) {
let dict = CFArrayGetValueAtIndex(pairs, i) as CFTypeRef;
if dict.is_null() || CFGetTypeID(dict) != CFDictionaryGetTypeID() {
continue;
}
let dict = dict as CFDictionaryRef;
let page = cfnumber_to_i32(CFDictionaryGetValue(dict, page_key as *const c_void));
let usage = cfnumber_to_i32(CFDictionaryGetValue(dict, usage_key as *const c_void));
let (Some(page), Some(usage)) = (page, usage) else {
continue;
};
let (page, usage) = (page as u16, usage as u16);
if page == info.usage_page && usage == info.usage {
continue; }
let mut entry = info.clone();
entry.usage_page = page;
entry.usage = usage;
entries.push(entry);
}
CFRelease(page_key as CFTypeRef);
CFRelease(usage_key as CFTypeRef);
}
entries
}
pub(crate) struct MacApi {
open_exclusive: AtomicBool,
}
impl MacApi {
pub fn new() -> HidResult<Self> {
Ok(MacApi {
open_exclusive: AtomicBool::new(false),
})
}
pub fn set_open_exclusive(&self, exclusive: bool) {
self.open_exclusive.store(exclusive, Ordering::Relaxed);
}
pub fn open_exclusive(&self) -> bool {
self.open_exclusive.load(Ordering::Relaxed)
}
pub fn enumerate(&self, vendor_id: u16, product_id: u16) -> HidResult<Vec<DeviceInfo>> {
let mut result = Vec::new();
unsafe {
let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOHID_MANAGER_OPTION_NONE);
if manager.is_null() {
return Err(HidError::Initialization {
message: "IOHIDManagerCreate returned NULL".into(),
});
}
IOHIDManagerSetDeviceMatching(manager, std::ptr::null());
let set = IOHIDManagerCopyDevices(manager);
if set.is_null() {
CFRelease(manager as CFTypeRef);
return Ok(result);
}
let count = CFSetGetCount(set) as usize;
let mut devices: Vec<*const c_void> = vec![std::ptr::null(); count];
CFSetGetValues(set, devices.as_mut_ptr());
for device in devices {
if device.is_null() {
continue;
}
for info in device_infos(device as IOHIDDeviceRef) {
let vid_ok = vendor_id == 0 || info.vendor_id == vendor_id;
let pid_ok = product_id == 0 || info.product_id == product_id;
if vid_ok && pid_ok {
result.push(info);
}
}
}
CFRelease(set as CFTypeRef);
CFRelease(manager as CFTypeRef);
}
Ok(result)
}
pub fn open(
&self,
vendor_id: u16,
product_id: u16,
serial: Option<&str>,
) -> HidResult<MacDevice> {
let candidates = self.enumerate(vendor_id, product_id)?;
let info = candidates
.into_iter()
.find(|info| match serial {
Some(s) => info.serial_number.as_deref() == Some(s),
None => true,
})
.ok_or(HidError::DeviceNotFound)?;
self.open_path(&info.path)
}
pub fn open_path(&self, path: &str) -> HidResult<MacDevice> {
let entry_id = parse_dev_srvs_id(path).ok_or_else(|| HidError::InvalidData {
message: format!("not a macOS device path (expected \"DevSrvsID:<id>\"): {path}"),
})?;
let options = if self.open_exclusive() {
IOHID_OPTIONS_TYPE_SEIZE_DEVICE
} else {
IOHID_OPTIONS_TYPE_NONE
};
MacDevice::open(entry_id, options)
}
}
#[derive(Default)]
struct Shared {
state: Mutex<State>,
cond: Condvar,
}
#[derive(Default)]
struct State {
reports: VecDeque<Vec<u8>>,
disconnected: bool,
shutdown: bool,
run_loop: usize,
ready: bool,
wakers: Vec<Waker>,
}
impl Shared {
fn lock_state(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
}
unsafe extern "C" fn input_report_callback(
context: *mut c_void,
_result: IOReturn,
_sender: *mut c_void,
_report_type: IOHIDReportType,
_report_id: u32,
report: *mut u8,
report_length: CFIndex,
) {
if report.is_null() || report_length <= 0 {
return;
}
let shared = &*(context as *const Shared);
let data = std::slice::from_raw_parts(report, report_length as usize).to_vec();
let wakers = {
let mut state = shared.lock_state();
state.reports.push_back(data);
if state.reports.len() > MAX_QUEUED_REPORTS {
state.reports.pop_front();
}
shared.cond.notify_all();
std::mem::take(&mut state.wakers)
};
for waker in wakers {
waker.wake();
}
}
unsafe extern "C" fn removal_callback(
context: *mut c_void,
_result: IOReturn,
_sender: *mut c_void,
) {
let shared = &*(context as *const Shared);
let wakers = {
let mut state = shared.lock_state();
state.disconnected = true;
shared.cond.notify_all();
if state.run_loop != 0 {
CFRunLoopStop(state.run_loop as CFRunLoopRef);
}
std::mem::take(&mut state.wakers)
};
for waker in wakers {
waker.wake();
}
}
fn read_thread(device: usize, mode: usize, shared: Arc<Shared>) {
let device = device as IOHIDDeviceRef;
let mode = mode as CFStringRef;
unsafe {
let run_loop = CFRunLoopGetCurrent();
IOHIDDeviceScheduleWithRunLoop(device, run_loop, mode);
{
let mut state = shared.lock_state();
state.run_loop = run_loop as usize;
state.ready = true;
shared.cond.notify_all();
}
loop {
{
let state = shared.lock_state();
if state.shutdown || state.disconnected {
break;
}
}
let code = CFRunLoopRunInMode(mode, 1000.0, 0);
if code == kCFRunLoopRunFinished || code == kCFRunLoopRunStopped {
let mut state = shared.lock_state();
if !state.shutdown {
state.disconnected = true;
}
break;
}
}
let wakers = {
let mut state = shared.lock_state();
shared.cond.notify_all();
std::mem::take(&mut state.wakers)
};
for waker in wakers {
waker.wake();
}
}
}
pub(crate) struct MacDevice {
device: IOHIDDeviceRef,
open_options: IOOptionBits,
run_loop_mode: CFStringRef,
input_buf: *mut u8,
input_buf_len: usize,
shared: Arc<Shared>,
read_thread: Option<thread::JoinHandle<()>>,
#[allow(dead_code)]
blocking: AtomicBool,
}
unsafe impl Send for MacDevice {}
unsafe impl Sync for MacDevice {}
impl MacDevice {
fn open(entry_id: u64, options: IOOptionBits) -> HidResult<Self> {
unsafe {
let matching = IORegistryEntryIDMatching(entry_id);
if matching.is_null() {
return Err(HidError::backend("IORegistryEntryIDMatching failed"));
}
let service = IOServiceGetMatchingService(IO_MASTER_PORT_DEFAULT, matching);
if service == 0 {
return Err(HidError::DeviceNotFound);
}
let device = IOHIDDeviceCreate(kCFAllocatorDefault, service);
IOObjectRelease(service);
if device.is_null() {
return Err(HidError::backend("IOHIDDeviceCreate failed"));
}
let ret = IOHIDDeviceOpen(device, options);
if ret != IO_RETURN_SUCCESS {
CFRelease(device as CFTypeRef);
return Err(HidError::OpenFailed {
message: format!("IOHIDDeviceOpen failed: IOReturn 0x{:08x}", ret as u32),
});
}
let input_buf_len = i32_property(device, KEY_MAX_INPUT_REPORT_SIZE)
.filter(|&len| len > 0)
.unwrap_or(64) as usize;
let input_buf = Box::into_raw(vec![0u8; input_buf_len].into_boxed_slice()) as *mut u8;
let mode_name =
CString::new(format!("HIDAPI_{device:p}")).expect("no NUL in pointer format");
let run_loop_mode = CFStringCreateWithCString(
kCFAllocatorDefault,
mode_name.as_ptr(),
kCFStringEncodingUTF8,
);
let shared = Arc::new(Shared::default());
let context = Arc::as_ptr(&shared) as *mut c_void;
IOHIDDeviceRegisterInputReportCallback(
device,
input_buf,
input_buf_len as CFIndex,
Some(input_report_callback),
context,
);
IOHIDDeviceRegisterRemovalCallback(device, Some(removal_callback), context);
let thread = thread::Builder::new().name("hidra-hid-read".into()).spawn({
let shared = Arc::clone(&shared);
let device = device as usize;
let mode = run_loop_mode as usize;
move || read_thread(device, mode, shared)
});
let thread = match thread {
Ok(thread) => thread,
Err(err) => {
IOHIDDeviceRegisterInputReportCallback(
device,
input_buf,
input_buf_len as CFIndex,
None,
context,
);
IOHIDDeviceRegisterRemovalCallback(device, None, context);
IOHIDDeviceClose(device, options);
CFRelease(device as CFTypeRef);
CFRelease(run_loop_mode as CFTypeRef);
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
input_buf,
input_buf_len,
)));
return Err(HidError::io("spawning HID read thread", err));
}
};
{
let mut state = shared.lock_state();
while !state.ready {
state = shared
.cond
.wait(state)
.unwrap_or_else(PoisonError::into_inner);
}
}
Ok(MacDevice {
device,
open_options: options,
run_loop_mode,
input_buf,
input_buf_len,
shared,
read_thread: Some(thread),
blocking: AtomicBool::new(true),
})
}
}
fn disconnected(&self) -> bool {
self.shared.lock_state().disconnected
}
fn set_report(&self, report_type: IOHIDReportType, data: &[u8]) -> HidResult<usize> {
if data.is_empty() {
return Err(HidError::InvalidData {
message: "report data must contain a report ID byte".into(),
});
}
if self.disconnected() {
return Err(HidError::Disconnected);
}
let report_id = data[0];
let payload = if report_id == 0 { &data[1..] } else { data };
let ret = unsafe {
IOHIDDeviceSetReport(
self.device,
report_type,
report_id as CFIndex,
payload.as_ptr(),
payload.len() as CFIndex,
)
};
if ret != IO_RETURN_SUCCESS {
return Err(HidError::backend(format!(
"IOHIDDeviceSetReport failed: IOReturn 0x{:08x}",
ret as u32
)));
}
Ok(data.len())
}
fn get_report(&self, report_type: IOHIDReportType, buf: &mut [u8]) -> HidResult<usize> {
if buf.is_empty() {
return Err(HidError::InvalidData {
message: "buffer must contain a report ID byte".into(),
});
}
if self.disconnected() {
return Err(HidError::Disconnected);
}
let report_id = buf[0];
let body = if report_id == 0 {
&mut buf[1..]
} else {
&mut *buf
};
let mut len = body.len() as CFIndex;
let ret = unsafe {
IOHIDDeviceGetReport(
self.device,
report_type,
report_id as CFIndex,
body.as_mut_ptr(),
&mut len,
)
};
if ret != IO_RETURN_SUCCESS {
return Err(HidError::backend(format!(
"IOHIDDeviceGetReport failed: IOReturn 0x{:08x}",
ret as u32
)));
}
let mut returned = len as usize;
if report_id == 0 {
returned += 1; }
Ok(returned)
}
pub fn write(&self, data: &[u8]) -> HidResult<usize> {
self.set_report(IOHID_REPORT_TYPE_OUTPUT, data)
}
#[allow(dead_code)] pub fn read(&self, buf: &mut [u8]) -> HidResult<usize> {
let timeout = if self.blocking.load(Ordering::Relaxed) {
-1
} else {
0
};
self.read_timeout(buf, timeout)
}
#[allow(dead_code)] pub fn read_timeout(&self, buf: &mut [u8], timeout_ms: i32) -> HidResult<usize> {
if buf.is_empty() {
return Err(HidError::InvalidData {
message: "read buffer must not be empty".into(),
});
}
let deadline =
(timeout_ms > 0).then(|| Instant::now() + Duration::from_millis(timeout_ms as u64));
let mut state = self.shared.lock_state();
loop {
if let Some(report) = state.reports.pop_front() {
let len = report.len().min(buf.len());
buf[..len].copy_from_slice(&report[..len]);
return Ok(len);
}
if state.disconnected {
return Err(HidError::Disconnected);
}
if state.shutdown {
return Err(HidError::backend("device read thread is shut down"));
}
state = match deadline {
None if timeout_ms == 0 => return Ok(0),
None => self
.shared
.cond
.wait(state)
.unwrap_or_else(PoisonError::into_inner),
Some(deadline) => {
let now = Instant::now();
if now >= deadline {
return Ok(0);
}
self.shared
.cond
.wait_timeout(state, deadline - now)
.unwrap_or_else(PoisonError::into_inner)
.0
}
};
}
}
pub fn read_async<'a>(&'a self, buf: &'a mut [u8]) -> ReadAsync<'a> {
ReadAsync { dev: self, buf }
}
#[allow(dead_code)] pub fn set_blocking_mode(&self, blocking: bool) -> HidResult<()> {
self.blocking.store(blocking, Ordering::Relaxed);
Ok(())
}
pub fn send_feature_report(&self, data: &[u8]) -> HidResult<()> {
self.set_report(IOHID_REPORT_TYPE_FEATURE, data).map(|_| ())
}
pub fn get_feature_report(&self, buf: &mut [u8]) -> HidResult<usize> {
self.get_report(IOHID_REPORT_TYPE_FEATURE, buf)
}
pub fn get_input_report(&self, buf: &mut [u8]) -> HidResult<usize> {
self.get_report(IOHID_REPORT_TYPE_INPUT, buf)
}
pub fn get_manufacturer_string(&self) -> HidResult<Option<String>> {
Ok(unsafe { string_property(self.device, KEY_MANUFACTURER) })
}
pub fn get_product_string(&self) -> HidResult<Option<String>> {
Ok(unsafe { string_property(self.device, KEY_PRODUCT) })
}
pub fn get_serial_number_string(&self) -> HidResult<Option<String>> {
Ok(unsafe { string_property(self.device, KEY_SERIAL_NUMBER) })
}
pub fn get_indexed_string(&self, _index: u32) -> HidResult<Option<String>> {
Err(HidError::Unsupported {
message: "indexed strings are not available via IOHIDDevice; use the usb backend"
.into(),
})
}
pub fn get_report_descriptor(&self, buf: &mut [u8]) -> HidResult<usize> {
unsafe {
let value = device_property(self.device, KEY_REPORT_DESCRIPTOR);
if value.is_null() || CFGetTypeID(value) != CFDataGetTypeID() {
return Err(HidError::backend(
"failed to read the ReportDescriptor property",
));
}
let data = value as CFDataRef;
let len = (CFDataGetLength(data) as usize).min(buf.len());
std::ptr::copy_nonoverlapping(CFDataGetBytePtr(data), buf.as_mut_ptr(), len);
Ok(len)
}
}
pub fn get_device_info(&self) -> HidResult<DeviceInfo> {
let mut infos = unsafe { device_infos(self.device) };
Ok(infos.remove(0))
}
}
impl Drop for MacDevice {
fn drop(&mut self) {
unsafe {
let (disconnected, run_loop) = {
let state = self.shared.lock_state();
(state.disconnected, state.run_loop)
};
let context = Arc::as_ptr(&self.shared) as *mut c_void;
if !disconnected {
IOHIDDeviceRegisterInputReportCallback(
self.device,
self.input_buf,
self.input_buf_len as CFIndex,
None,
context,
);
IOHIDDeviceRegisterRemovalCallback(self.device, None, context);
if run_loop != 0 {
IOHIDDeviceUnscheduleFromRunLoop(
self.device,
run_loop as CFRunLoopRef,
self.run_loop_mode,
);
IOHIDDeviceScheduleWithRunLoop(
self.device,
CFRunLoopGetMain(),
kCFRunLoopDefaultMode,
);
}
}
{
let mut state = self.shared.lock_state();
state.shutdown = true;
self.shared.cond.notify_all();
}
if run_loop != 0 {
CFRunLoopStop(run_loop as CFRunLoopRef);
CFRunLoopWakeUp(run_loop as CFRunLoopRef);
}
if let Some(thread) = self.read_thread.take() {
let _ = thread.join();
}
if !disconnected {
IOHIDDeviceClose(self.device, self.open_options);
}
CFRelease(self.device as CFTypeRef);
CFRelease(self.run_loop_mode as CFTypeRef);
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
self.input_buf,
self.input_buf_len,
)));
}
}
}
pub(crate) struct ReadAsync<'a> {
dev: &'a MacDevice,
buf: &'a mut [u8],
}
impl std::future::Future for ReadAsync<'_> {
type Output = HidResult<usize>;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = self.get_mut();
if this.buf.is_empty() {
return std::task::Poll::Ready(Err(HidError::InvalidData {
message: "read buffer must not be empty".into(),
}));
}
let mut state = this.dev.shared.lock_state();
if let Some(report) = state.reports.pop_front() {
let len = report.len().min(this.buf.len());
this.buf[..len].copy_from_slice(&report[..len]);
return std::task::Poll::Ready(Ok(len));
}
if state.disconnected {
return std::task::Poll::Ready(Err(HidError::Disconnected));
}
if state.shutdown {
return std::task::Poll::Ready(Err(HidError::backend(
"device read thread is shut down",
)));
}
if !state.wakers.iter().any(|w| w.will_wake(cx.waker())) {
state.wakers.push(cx.waker().clone());
}
std::task::Poll::Pending
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_dev_srvs_id_paths() {
assert_eq!(parse_dev_srvs_id("DevSrvsID:4295123456"), Some(4295123456));
assert_eq!(parse_dev_srvs_id("DevSrvsID:0"), Some(0));
assert_eq!(parse_dev_srvs_id("DevSrvsID:"), None);
assert_eq!(parse_dev_srvs_id("DevSrvsID:abc"), None);
assert_eq!(parse_dev_srvs_id("DevSrvsID:-3"), None);
assert_eq!(parse_dev_srvs_id("/dev/hidraw0"), None);
assert_eq!(parse_dev_srvs_id(""), None);
}
#[test]
fn formats_and_reparses_paths() {
assert_eq!(format_dev_srvs_id(42), "DevSrvsID:42");
let path = format_dev_srvs_id(u64::MAX);
assert_eq!(path, "DevSrvsID:18446744073709551615");
assert_eq!(parse_dev_srvs_id(&path), Some(u64::MAX));
}
#[test]
fn maps_transport_strings() {
assert_eq!(bus_type_from_transport("USB"), BusType::Usb);
assert_eq!(bus_type_from_transport("Bluetooth"), BusType::Bluetooth);
assert_eq!(
bus_type_from_transport("BluetoothLowEnergy"),
BusType::Bluetooth
);
assert_eq!(
bus_type_from_transport("Bluetooth Low Energy"),
BusType::Bluetooth
);
assert_eq!(bus_type_from_transport("I2C"), BusType::I2c);
assert_eq!(bus_type_from_transport("SPI"), BusType::Spi);
assert_eq!(bus_type_from_transport("AirPlay"), BusType::Unknown);
assert_eq!(bus_type_from_transport(""), BusType::Unknown);
}
}