#![allow(non_upper_case_globals)]
use objc2_core_foundation::{
kCFAllocatorDefault, CFArray, CFDictionary, CFNumber, CFRetained, CFString,
CFStringBuiltInEncodings, CFType,
};
use objc2_io_kit::{
io_service_t, kHIDPage_Button, kHIDPage_Consumer, kHIDPage_GenericDesktop, kHIDPage_Simulation,
kHIDUsage_Button_1, kHIDUsage_GD_DPadDown, kHIDUsage_GD_DPadLeft, kHIDUsage_GD_DPadRight,
kHIDUsage_GD_DPadUp, kHIDUsage_GD_Dial, kHIDUsage_GD_GamePad, kHIDUsage_GD_Hatswitch,
kHIDUsage_GD_Joystick, kHIDUsage_GD_MultiAxisController, kHIDUsage_GD_Rx, kHIDUsage_GD_Ry,
kHIDUsage_GD_Rz, kHIDUsage_GD_Select, kHIDUsage_GD_Slider, kHIDUsage_GD_Start,
kHIDUsage_GD_SystemMainMenu, kHIDUsage_GD_Wheel, kHIDUsage_GD_X, kHIDUsage_GD_Y,
kHIDUsage_GD_Z, kHIDUsage_Sim_Accelerator, kHIDUsage_Sim_Brake, kHIDUsage_Sim_Rudder,
kHIDUsage_Sim_Throttle, kIOHIDDeviceUsageKey, kIOHIDDeviceUsagePageKey, kIOHIDLocationIDKey,
kIOHIDOptionsTypeNone, kIOHIDPrimaryUsageKey, kIOHIDPrimaryUsagePageKey, kIOHIDProductIDKey,
kIOHIDProductKey, kIOHIDVendorIDKey, kIOHIDVersionNumberKey, kIOReturnSuccess, IOHIDDevice,
IOHIDElement, IOHIDElementType, IOHIDManager, IOObjectRelease, IOObjectRetain,
IORegistryEntryGetRegistryEntryID, IO_OBJECT_NULL,
};
use std::ffi::CStr;
pub fn new_manager() -> Option<CFRetained<IOHIDManager>> {
let manager = IOHIDManager::new(None, kIOHIDOptionsTypeNone);
let matchers = CFArray::from_retained_objects(&[
create_hid_device_matcher(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick),
create_hid_device_matcher(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad),
create_hid_device_matcher(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController),
]);
unsafe { manager.set_device_matching_multiple(Some(matchers.as_opaque())) };
let ret = manager.open(kIOHIDOptionsTypeNone);
if ret != kIOReturnSuccess {
None
} else {
Some(manager)
}
}
#[derive(Debug, Clone)]
pub struct Device(pub CFRetained<IOHIDDevice>);
unsafe impl Sync for Device {}
unsafe impl Send for Device {}
pub trait DeviceExt: Properties {
fn device(&self) -> &IOHIDDevice;
fn get_name(&self) -> Option<String> {
self.get_string_property(kIOHIDProductKey)
.map(|name| name.to_string())
}
fn get_location_id(&self) -> Option<u32> {
self.get_number_property(kIOHIDLocationIDKey)
.and_then(|location_id| location_id.as_i32().map(|location_id| location_id as u32))
}
fn get_vendor_id(&self) -> Option<u16> {
self.get_number_property(kIOHIDVendorIDKey)
.and_then(|vendor_id| vendor_id.as_i32().map(|vendor_id| vendor_id as u16))
}
fn get_product_id(&self) -> Option<u16> {
self.get_number_property(kIOHIDProductIDKey)
.and_then(|product_id| product_id.as_i32().map(|product_id| product_id as u16))
}
fn get_version(&self) -> Option<u16> {
self.get_number_property(kIOHIDVersionNumberKey)
.and_then(|version| version.as_i32().map(|version| version as u16))
}
fn get_page(&self) -> Option<u32> {
self.get_number_property(kIOHIDPrimaryUsagePageKey)
.and_then(|page| page.as_i32().map(|page| page as u32))
}
fn get_usage(&self) -> Option<u32> {
self.get_number_property(kIOHIDPrimaryUsageKey)
.and_then(|usage| usage.as_i32().map(|usage| usage as u32))
}
fn get_service(&self) -> Option<IOService> {
IOService::new(self.device().service())
}
}
pub fn device_elements(device: &IOHIDDevice) -> Vec<CFRetained<IOHIDElement>> {
let elements = unsafe { device.matching_elements(None, kIOHIDOptionsTypeNone) };
let Some(elements) = elements else {
return vec![];
};
let elements = unsafe { elements.cast_unchecked::<IOHIDElement>() };
elements.into_iter().collect()
}
impl DeviceExt for IOHIDDevice {
fn device(&self) -> &IOHIDDevice {
self
}
}
impl Properties for IOHIDDevice {
fn get_property(&self, key: &CStr) -> Option<CFRetained<CFType>> {
debug_assert!(key.to_str().is_ok());
let key = unsafe {
CFString::with_c_string(
kCFAllocatorDefault,
key.as_ptr(),
CFStringBuiltInEncodings::EncodingUTF8.0,
)?
};
self.property(&key)
}
}
pub fn element_is_collection(type_: IOHIDElementType) -> bool {
type_ == IOHIDElementType::Collection
}
pub fn element_is_axis(type_: IOHIDElementType, page: u32, usage: u32) -> bool {
match type_ {
IOHIDElementType::Input_Misc
| IOHIDElementType::Input_Button
| IOHIDElementType::Input_Axis => match page {
kHIDPage_GenericDesktop => {
matches!(
usage,
kHIDUsage_GD_X
| kHIDUsage_GD_Y
| kHIDUsage_GD_Z
| kHIDUsage_GD_Rx
| kHIDUsage_GD_Ry
| kHIDUsage_GD_Rz
| kHIDUsage_GD_Slider
| kHIDUsage_GD_Dial
| kHIDUsage_GD_Wheel
)
}
kHIDPage_Simulation => matches!(
usage,
kHIDUsage_Sim_Rudder
| kHIDUsage_Sim_Throttle
| kHIDUsage_Sim_Accelerator
| kHIDUsage_Sim_Brake
),
_ => false,
},
_ => false,
}
}
pub fn element_is_button(type_: IOHIDElementType, page: u32, usage: u32) -> bool {
match type_ {
IOHIDElementType::Input_Misc
| IOHIDElementType::Input_Button
| IOHIDElementType::Input_Axis => match page {
kHIDPage_GenericDesktop => matches!(
usage,
kHIDUsage_GD_DPadUp
| kHIDUsage_GD_DPadDown
| kHIDUsage_GD_DPadRight
| kHIDUsage_GD_DPadLeft
| kHIDUsage_GD_Start
| kHIDUsage_GD_Select
| kHIDUsage_GD_SystemMainMenu
),
kHIDPage_Button | kHIDPage_Consumer => true,
_ => false,
},
_ => false,
}
}
pub fn element_is_hat(type_: IOHIDElementType, page: u32, usage: u32) -> bool {
match type_ {
IOHIDElementType::Input_Misc
| IOHIDElementType::Input_Button
| IOHIDElementType::Input_Axis => match page {
kHIDPage_GenericDesktop => matches!(usage, USAGE_AXIS_DPADX | USAGE_AXIS_DPADY),
_ => false,
},
_ => false,
}
}
pub fn element_children(element: &IOHIDElement) -> Vec<CFRetained<IOHIDElement>> {
let elements = element.children();
let Some(elements) = elements else {
return vec![];
};
let elements = unsafe { elements.cast_unchecked::<IOHIDElement>() };
elements.into_iter().collect()
}
impl Properties for IOHIDElement {
fn get_property(&self, key: &CStr) -> Option<CFRetained<CFType>> {
debug_assert!(key.to_str().is_ok());
let key = unsafe {
CFString::with_c_string(
kCFAllocatorDefault,
key.as_ptr(),
CFStringBuiltInEncodings::EncodingUTF8.0,
)?
};
self.property(&key)
}
}
#[repr(C)]
#[derive(Debug)]
pub(crate) struct IOService(io_service_t);
impl IOService {
pub fn new(io_service: io_service_t) -> Option<IOService> {
if io_service == IO_OBJECT_NULL {
return None;
}
let result = IOObjectRetain(io_service);
if result == kIOReturnSuccess {
Some(IOService(io_service))
} else {
None
}
}
pub fn get_registry_entry_id(&self) -> Option<u64> {
IOObjectRetain(self.0);
let mut entry_id = 0;
let result = unsafe { IORegistryEntryGetRegistryEntryID(self.0, &mut entry_id) };
IOObjectRelease(self.0);
if result == kIOReturnSuccess {
Some(entry_id)
} else {
None
}
}
}
impl Drop for IOService {
fn drop(&mut self) {
IOObjectRelease(self.0 as _);
}
}
pub trait Properties {
fn get_property(&self, key: &CStr) -> Option<CFRetained<CFType>>;
fn get_number_property(&self, key: &CStr) -> Option<CFRetained<CFNumber>> {
self.get_property(key)
.and_then(|value| value.downcast::<CFNumber>().ok())
}
fn get_string_property(&self, key: &CStr) -> Option<CFRetained<CFString>> {
self.get_property(key)
.and_then(|value| value.downcast::<CFString>().ok())
}
}
fn create_hid_device_matcher(
page: u32,
usage: u32,
) -> CFRetained<CFDictionary<CFString, CFNumber>> {
let page_key = CFString::from_static_str(kIOHIDDeviceUsagePageKey.to_str().unwrap());
let page_value = CFNumber::new_i32(page as i32);
let usage_key = CFString::from_static_str(kIOHIDDeviceUsageKey.to_str().unwrap());
let usage_value = CFNumber::new_i32(usage as i32);
CFDictionary::from_slices(&[&*page_key, &*usage_key], &[&*page_value, &*usage_value])
}
pub const PAGE_GENERIC_DESKTOP: u32 = kHIDPage_GenericDesktop;
pub const PAGE_SIMULATION: u32 = kHIDPage_Simulation;
pub const PAGE_BUTTON: u32 = kHIDPage_Button;
pub const USAGE_AXIS_LSTICKX: u32 = kHIDUsage_GD_X;
pub const USAGE_AXIS_LSTICKY: u32 = kHIDUsage_GD_Y;
pub const USAGE_AXIS_LEFTZ: u32 = 0; pub const USAGE_AXIS_RSTICKX: u32 = kHIDUsage_GD_Z;
pub const USAGE_AXIS_RSTICKY: u32 = kHIDUsage_GD_Rz;
pub const USAGE_AXIS_RIGHTZ: u32 = 0; pub const USAGE_AXIS_DPADX: u32 = kHIDUsage_GD_Hatswitch;
pub const USAGE_AXIS_DPADY: u32 = kHIDUsage_GD_Hatswitch + 1;
pub const USAGE_AXIS_RT: u32 = 0; pub const USAGE_AXIS_LT: u32 = 0; pub const USAGE_AXIS_RT2: u32 = kHIDUsage_Sim_Accelerator;
pub const USAGE_AXIS_LT2: u32 = kHIDUsage_Sim_Brake;
pub const USAGE_BTN_SOUTH: u32 = kHIDUsage_Button_1;
pub const USAGE_BTN_EAST: u32 = kHIDUsage_Button_1 + 1;
pub const USAGE_BTN_WEST: u32 = kHIDUsage_Button_1 + 3;
pub const USAGE_BTN_NORTH: u32 = kHIDUsage_Button_1 + 4;
pub const USAGE_BTN_LT: u32 = kHIDUsage_Button_1 + 6;
pub const USAGE_BTN_RT: u32 = kHIDUsage_Button_1 + 7;
pub const USAGE_BTN_LT2: u32 = kHIDUsage_Button_1 + 8; pub const USAGE_BTN_RT2: u32 = kHIDUsage_Button_1 + 9; pub const USAGE_BTN_SELECT: u32 = kHIDUsage_Button_1 + 10;
pub const USAGE_BTN_START: u32 = kHIDUsage_Button_1 + 11;
pub const USAGE_BTN_MODE: u32 = kHIDUsage_Button_1 + 12;
pub const USAGE_BTN_LTHUMB: u32 = kHIDUsage_Button_1 + 13;
pub const USAGE_BTN_RTHUMB: u32 = kHIDUsage_Button_1 + 14;
pub const USAGE_BTN_DPAD_UP: u32 = kHIDUsage_Button_1 + 15; pub const USAGE_BTN_DPAD_DOWN: u32 = kHIDUsage_Button_1 + 16; pub const USAGE_BTN_DPAD_LEFT: u32 = kHIDUsage_Button_1 + 17; pub const USAGE_BTN_DPAD_RIGHT: u32 = kHIDUsage_Button_1 + 18; pub const USAGE_BTN_C: u32 = kHIDUsage_Button_1 + 19; pub const USAGE_BTN_Z: u32 = kHIDUsage_Button_1 + 20;