Reading and Writing to Usb (Hid) Interrupt Endpoints on MAC

Reading and writing to USB (HID) interrupt endpoints on Mac

I have now a working Mac driver to a USB device that requires communication through interrupt endpoints. Here is how I did it:

Ultimately the method that worked well for me was option 1 (noted above). As noted, I was having issues opening the COM-style IOUSBInterfaceInterface to the device. It became clear over time that this was due to the HIDManager capturing the device. I was unable to wrest control of the device from the HIDManager once it was captured (not even the USBInterfaceOpenSeize call or the USBDeviceOpenSeize calls would work).

To take control of the device I needed to grab it before the HIDManager. The solution to this was to write a codeless kext (kernel extension). A kext is essentially a bundle that sits in System/Library/Extensions that contains (usually) a plist (property list) and (occasionally) a kernel-level driver, among other items. In my case I wanted only the plist, which would give the instructions to the kernel on what devices it matches. If the data gives a higher probe score than the HIDManager then I could essentially capture the device and use a user-space driver to communicate with it.

The kext plist written, with some project-specific details modified, is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>OSBundleLibraries</key>
<dict>
<key>com.apple.iokit.IOUSBFamily</key>
<string>1.8</string>
<key>com.apple.kernel.libkern</key>
<string>6.0</string>
</dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleGetInfoString</key>
<string>Demi USB Device</string>
<key>CFBundleIdentifier</key>
<string>com.demiart.mydevice</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Demi USB Device</string>
<key>CFBundlePackageType</key>
<string>KEXT</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>IOKitPersonalities</key>
<dict>
<key>Device Driver</key>
<dict>
<key>CFBundleIdentifier</key>
<string>com.apple.kernel.iokit</string>
<key>IOClass</key>
<string>IOService</string>
<key>IOProviderClass</key>
<string>IOUSBInterface</string>
<key>idProduct</key>
<integer>12345</integer>
<key>idVendor</key>
<integer>67890</integer>
<key>bConfigurationValue</key>
<integer>1</integer>
<key>bInterfaceNumber</key>
<integer>0</integer>
</dict>
</dict>
<key>OSBundleRequired</key>
<string>Local-Root</string>
</dict>
</plist>

The idVendor and idProduct values give the kext specificity and increase its probe score sufficiently.

In order to use the kext, the following things need to be done (which my installer will do for clients):

  1. Change the owner to root:wheel (sudo chown root:wheel DemiUSBDevice.kext)
  2. Copy the kext to Extensions (sudo cp DemiUSBDevice.kext /System/Library/Extensions)
  3. Call the kextload utility to load the kext for immediate use without restart (sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext)
  4. Touch the Extensions folder so that the next restart will force a cache rebuild (sudo touch /System/Library/Extensions)

At this point the system should use the kext to keep the HIDManager from capturing my device. Now, what to do with it? How to write to and read from it?

Following are some simplified snippets of my code, minus any error handling, that illustrate the solution. Before being able to do anything with the device, the application needs to know when the device attaches (and detaches). Note that this is merely for purposes of illustration — some of the variables are class-level, some are global, etc. Here is the initialization code that sets the attach/detach events up:

#include <IOKit/IOKitLib.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/usb/IOUSBLib.h>
#include <mach/mach.h>

#define DEMI_VENDOR_ID 12345
#define DEMI_PRODUCT_ID 67890

void DemiUSBDriver::initialize(void)
{
IOReturn result;
Int32 vendor_id = DEMI_VENDOR_ID;
Int32 product_id = DEMI_PRODUCT_ID;
mach_port_t master_port;
CFMutableDictionaryRef matching_dict;
IONotificationPortRef notify_port;
CFRunLoopSourceRef run_loop_source;

//create a master port
result = IOMasterPort(bootstrap_port, &master_port);

//set up a matching dictionary for the device
matching_dict = IOServiceMatching(kIOUSBDeviceClassName);

//add matching parameters
CFDictionarySetValue(matching_dict, CFSTR(kUSBVendorID),
CFNumberCreate(kCFAllocatorDefault, kCFNumberInt32Type, &vendor_id));
CFDictionarySetValue(matching_dict, CFSTR(kUSBProductID),
CFNumberCreate(kCFAllocatorDefault, kCFNumberInt32Type, &product_id));

//create the notification port and event source
notify_port = IONotificationPortCreate(master_port);
run_loop_source = IONotificationPortGetRunLoopSource(notify_port);
CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source,
kCFRunLoopDefaultMode);

//add an additional reference for a secondary event
// - each consumes a reference...
matching_dict = (CFMutableDictionaryRef)CFRetain(matching_dict);

//add a notification callback for detach event
//NOTE: removed_iter is a io_iterator_t, declared elsewhere
result = IOServiceAddMatchingNotification(notify_port,
kIOTerminatedNotification, matching_dict, device_detach_callback,
NULL, &removed_iter);

//call the callback to 'arm' the notification
device_detach_callback(NULL, removed_iter);

//add a notification callback for attach event
//NOTE: added_iter is a io_iterator_t, declared elsewhere
result = IOServiceAddMatchingNotification(notify_port,
kIOFirstMatchNotification, matching_dict, device_attach_callback,
NULL, &g_added_iter);
if (result)
{
throw Exception("Unable to add attach notification callback.");
}

//call the callback to 'arm' the notification
device_attach_callback(NULL, added_iter);

//'pump' the run loop to handle any previously added devices
service();
}

There are two methods that are used as callbacks in this initialization code: device_detach_callback and device_attach_callback (both declared at static methods). device_detach_callback is straightforward:

//implementation
void DemiUSBDevice::device_detach_callback(void* context, io_iterator_t iterator)
{
IOReturn result;
io_service_t obj;

while ((obj = IOIteratorNext(iterator)))
{
//close all open resources associated with this service/device...

//release the service
result = IOObjectRelease(obj);
}
}

device_attach_callback is where most of the magic happens. In my code I have this broken into multiple methods, but here I'll present it as a big monolithic method...:

void DemiUSBDevice::device_attach_callback(void * context, 
io_iterator_t iterator)
{
IOReturn result;
io_service_t usb_service;
IOCFPlugInInterface** plugin;
HRESULT hres;
SInt32 score;
UInt16 vendor;
UInt16 product;
IOUSBFindInterfaceRequest request;
io_iterator_t intf_iterator;
io_service_t usb_interface;

UInt8 interface_endpoint_count = 0;
UInt8 pipe_ref = 0xff;

UInt8 direction;
UInt8 number;
UInt8 transfer_type;
UInt16 max_packet_size;
UInt8 interval;

CFRunLoopSourceRef m_event_source;
CFRunLoopSourceRef compl_event_source;

IOUSBDeviceInterface245** dev = NULL;
IOUSBInterfaceInterface245** intf = NULL;

while ((usb_service = IOIteratorNext(iterator)))
{
//create the intermediate plugin
result = IOCreatePlugInInterfaceForService(usb_service,
kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin,
&score);

//get the device interface
hres = (*plugin)->QueryInterface(plugin,
CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID245), (void**)&dev);

//release the plugin - no further need for it
IODestroyPlugInInterface(plugin);

//double check ids for correctness
result = (*dev)->GetDeviceVendor(dev, &vendor);
result = (*dev)->GetDeviceProduct(dev, &product);
if ((vendor != DEMI_VENDOR_ID) || (product != DEMI_PRODUCT_ID))
{
continue;
}

//set up interface find request
request.bInterfaceClass = kIOUSBFindInterfaceDontCare;
request.bInterfaceSubClass = kIOUSBFindInterfaceDontCare;
request.bInterfaceProtocol = kIOUSBFindInterfaceDontCare;
request.bAlternateSetting = kIOUSBFindInterfaceDontCare;

result = (*dev)->CreateInterfaceIterator(dev, &request, &intf_iterator);

while ((usb_interface = IOIteratorNext(intf_iterator)))
{
//create intermediate plugin
result = IOCreatePlugInInterfaceForService(usb_interface,
kIOUSBInterfaceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin,
&score);

//release the usb interface - not needed
result = IOObjectRelease(usb_interface);

//get the general interface interface
hres = (*plugin)->QueryInterface(plugin, CFUUIDGetUUIDBytes(
kIOUSBInterfaceInterfaceID245), (void**)&intf);

//release the plugin interface
IODestroyPlugInInterface(plugin);

//attempt to open the interface
result = (*intf)->USBInterfaceOpen(intf);

//check that the interrupt endpoints are available on this interface
//calling 0xff invalid...
m_input_pipe = 0xff; //UInt8, pipe from device to Mac
m_output_pipe = 0xff; //UInt8, pipe from Mac to device

result = (*intf)->GetNumEndpoints(intf, &interface_endpoint_count);
if (!result)
{
//check endpoints for direction, type, etc.
//note that pipe_ref == 0 is the control endpoint (we don't want it)
for (pipe_ref = 1; pipe_ref <= interface_endpoint_count; pipe_ref++)
{
result = (*intf)->GetPipeProperties(intf, pipe_ref, &direction,
&number, &transfer_type, &max_packet_size, &interval);
if (result)
{
break;
}

if (transfer_type == kUSBInterrupt)
{
if (direction == kUSBIn)
{
m_input_pipe = pipe_ref;
}
else if (direction == kUSBOut)
{
m_output_pipe = pipe_ref;
}
}
}
}

//set up async completion notifications
result = (*m_intf)->CreateInterfaceAsyncEventSource(m_intf,
&compl_event_source);
CFRunLoopAddSource(CFRunLoopGetCurrent(), compl_event_source,
kCFRunLoopDefaultMode);

break;
}

break;
}
}

At this point we should have the numbers of the interrupt endpoints and an open IOUSBInterfaceInterface to the device. An asynchronous writing of data can be done by calling something like:

result = (intf)->WritePipeAsync(intf, m_output_pipe, 
data, OUTPUT_DATA_BUF_SZ, device_write_completion,
NULL);

where data is a char buffer of data to write, the final parameter is an optional context object to pass into the callback, and device_write_completion is a static method with the following general form:

void DemiUSBDevice::device_write_completion(void* context, 
IOReturn result, void* arg0)
{
//...
}

reading from the interrupt endpoint is similar:

result = (intf)->ReadPipeAsync(intf, m_input_pipe, 
data, INPUT_DATA_BUF_SZ, device_read_completion,
NULL);

where device_read_completion is of the following form:

void DemiUSBDevice::device_read_completion(void* context, 
IOReturn result, void* arg0)
{
//...
}

Note that to receive these callbacks the run loop must be running (see this link for more information about the CFRunLoop). One way to achieve this is to call CFRunLoopRun() after calling the async read or write methods at which point the main thread blocks while the run loop runs. After handling your callback you can call CFRunLoopStop(CFRunLoopGetCurrent()) to stop the run loop and hand execution back to the main thread.

Another alternative (which I do in my code) is to pass a context object (named 'request' in the following code sample) into the WritePipeAsync/ReadPipeAsync methods - this object contains a boolean completion flag (named 'is_done' in this example). After calling the read/write method, instead of calling CFRunLoopRun(), something like the following can be executed:

while (!(request->is_done))
{
//run for 1/10 second to handle events
Boolean returnAfterSourceHandled = false;
CFTimeInterval seconds = 0.1;
CFStringRef mode = kCFRunLoopDefaultMode;
CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled);
}

This has the benefit that if you have other threads that use the run loop you won't prematurely exit should another thread stop the run loop...

I hope that this is helpful to people. I had to pull from many incomplete sources to solve this problem and this required considerable work to get running well...

Simple HID OSX Application

OS X/macOS's HID stack is called IOHIDFamily, and much of it is open source. This includes kernel and userspace components. You can find the code for various different OS X/macOS releases at

https://opensource.apple.com/

For 10.12.2 (latest code released at time of writing) you can browse the IOHIDFamily code here:

https://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-870.31.1/

or download it as a zip file here:

https://opensource.apple.com/tarballs/IOHIDFamily/IOHIDFamily-870.31.1.tar.gz

The "tools" directory contains some userspace sample/test code which might help with what you're trying to do.

As for sample code not being updated, documentation being incomplete/outdated/missing, and provided code requiring a lot of effort to build: welcome to Mac systems & driver development. That's unfortunately how it is. I've been doing OS X kernel/driver work for 7 years, and while some things get easier with experience, downloading code from Apple and building it still feels like playing the lottery.

Determining which application is reading or writing data from within a Mac OS filter scheme driver

I was thinking of calling an API to determine the current process, but since this code is running in a KEXT (in the kernel) I don't think that will help me identify a user-land process.

This is pretty much the best you're going to get; the API doesn't pass the ultimate originator of the I/O through. In most cases though, the call will be made as a result of file system activity triggered by a file I/O syscall, and will be running in the (kernel) context of a user-space process. So the proc_* APIs (from <sys/proc.h> will most of the time give you the information you seem to need.

IOService::newUserClient() deals with user processes directly interfacing with kernel IOService objects via the user-space IOKit libraries. This isn't how IOStorage I/O calls are invoked though, they go through the IOMediaBSDClient which provides the glue between block device files in /dev/ and the IOStorage stack.

USBHIDManager HID, getReport() and setReport() On Mac Environment

In order to make use of asynchronous behavior, the event source obtained using getAsyncEventSource must be added to a run loop.

The above note is part of the comment of setReport. U might need to learn the runloop mechanism of Runloop in Mac OS first.

Since it's impossible to explain the mechanism here. The following functions and orders might help u coding when u get familiar with RunLoop.(Try to search "CFRunLoop" in google)

CFRunLoopGetCurrent();

CFRunLoopRun();

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode);

CFRunLoopStop(CFRunLoopRef rl);(i usually call this function in the callback method)



Related Topics



Leave a reply



Submit