Part 5: The IOKit API
About
We're at the last part of the series! This part will explore the IO framework.
Learnings
Enumerating all HID devices
This program will enumerate all HID devices:
import Foundation
import IOKit
import IOKit.hid
func run(and cb: IOHIDDeviceCallback) {
let manager = IOHIDManagerCreate(
kCFAllocatorDefault,
0x0 // https://developer.apple.com/documentation/iokit/1645245-anonymous/kiohidmanageroptionnone
)
let matching: [CFString: NSNumber] = [:]
// Match everything for now
IOHIDManagerSetDeviceMatching(manager, matching as NSDictionary)
// Schedule the manager
IOHIDManagerScheduleWithRunLoop(
manager,
CFRunLoopGetMain(),
CFRunLoopMode.defaultMode!.rawValue
)
// print reference when a matching device is detected
IOHIDManagerRegisterDeviceMatchingCallback(
manager,
{ (_,_,_, device) in
print(device)
},
nil)
// Start the manager
let _ = IOHIDManagerOpen(
manager,
IOOptionBits(kIOHIDOptionsTypeNone) // XXX: use `kIOHIDOptionsTypeSeizeDevice~ to perform an exclusive link when matching
)
CFRunLoopRun()
}
run()I'm interested in using my ZSA Moonlander as input device, so I identified its Vendor ID and Product ID.
<IOHIDDevice 0x1398051f0 [0x1fc998998] 'ClassName=AppleUserUSBHostHIDDevice' Transport=USB VendorID=12951 ProductID=6505 Manufacturer=ZSA Technology Labs Product=Moonlander Mark I PrimaryUsagePage=1 PrimaryUsage=2 ReportInterval=1000>Run on matches
Using the Vendor ID and Product ID from the previous steps, we can configure the IOHIDManager to only match my device.
let matching: [CFString: NSNumber] = [
kIOHIDVendorIDKey as CFString: NSNumber(value: 12951),
kIOHIDProductIDKey as CFString: NSNumber(value: 6505),
]
Finally, we register an IOHIDValueCallback to react to input events:
IOHIDManagerRegisterInputValueCallback(
manager,
{ (_, _, _, v) in
print(IOHIDValueGetIntegerValue(v))
}, nil)Many callback invocations for a single press
The callback function gets invoked many times for a single key press. When I hold the space key down, I see four integers printed:
44
0
1
44
The 44 value reflects the stand HID value for space key. I'm not sure what the
other 0 and 1 means.
And when I lift the space key up, I see another four integers printed:
0
0
0
0Privacy & Security
In the previous parts, the progams asked for permissions in Accessbility. This API requested access in Input Monitoring instead.
Program
In my other parts, I actually implemented a shortcut to have effects. In this part, I think I'll stop at a simple callback usage.
This API is very low level and I don't want to take on the challenge of implementing a shortcut using such low level primitives at this moment.
Nevertheless, here's the program at its completion with just a simple callback usage!
import Foundation
import IOKit
import IOKit.hid
func run() {
let manager = IOHIDManagerCreate(
kCFAllocatorDefault,
0x0 // https://developer.apple.com/documentation/iokit/1645245-anonymous/kiohidmanageroptionnone
)
let matching: [CFString: NSNumber] = [
kIOHIDVendorIDKey as CFString: NSNumber(value: 12951),
kIOHIDProductIDKey as CFString: NSNumber(value: 6505),
]
// Match everything for now
IOHIDManagerSetDeviceMatching(manager, matching as NSDictionary)
// Schedule the manager
IOHIDManagerScheduleWithRunLoop(
manager,
CFRunLoopGetMain(),
CFRunLoopMode.defaultMode!.rawValue
)
// Run when a matching device is detected
IOHIDManagerRegisterDeviceMatchingCallback(
manager,
{ (_, _, _, device) in
print(device)
},
nil)
IOHIDManagerRegisterInputValueCallback(
manager,
{ (_, _, _, v) in
print(IOHIDValueGetIntegerValue(v))
}, nil)
// Start the manager
let _ = IOHIDManagerOpen(
manager,
IOOptionBits(kIOHIDOptionsTypeNone)
)
CFRunLoopRun()
}
run()References
- In this post, Max Chuquimia describes how he used the IOKit API to implement a physical Tag & Build button.