Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>I have now a working Mac driver to a USB device that requires communication through interrupt endpoints. Here is how I did it:</p> <p>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).</p> <p>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 <em>probe score</em> than the HIDManager then I could essentially capture the device and use a user-space driver to communicate with it.</p> <p>The kext plist written, with some project-specific details modified, is as follows:</p> <pre><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt; &lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt; &lt;plist version="1.0"&gt; &lt;dict&gt; &lt;key&gt;OSBundleLibraries&lt;/key&gt; &lt;dict&gt; &lt;key&gt;com.apple.iokit.IOUSBFamily&lt;/key&gt; &lt;string&gt;1.8&lt;/string&gt; &lt;key&gt;com.apple.kernel.libkern&lt;/key&gt; &lt;string&gt;6.0&lt;/string&gt; &lt;/dict&gt; &lt;key&gt;CFBundleDevelopmentRegion&lt;/key&gt; &lt;string&gt;English&lt;/string&gt; &lt;key&gt;CFBundleGetInfoString&lt;/key&gt; &lt;string&gt;Demi USB Device&lt;/string&gt; &lt;key&gt;CFBundleIdentifier&lt;/key&gt; &lt;string&gt;com.demiart.mydevice&lt;/string&gt; &lt;key&gt;CFBundleInfoDictionaryVersion&lt;/key&gt; &lt;string&gt;6.0&lt;/string&gt; &lt;key&gt;CFBundleName&lt;/key&gt; &lt;string&gt;Demi USB Device&lt;/string&gt; &lt;key&gt;CFBundlePackageType&lt;/key&gt; &lt;string&gt;KEXT&lt;/string&gt; &lt;key&gt;CFBundleSignature&lt;/key&gt; &lt;string&gt;????&lt;/string&gt; &lt;key&gt;CFBundleVersion&lt;/key&gt; &lt;string&gt;1.0.0&lt;/string&gt; &lt;key&gt;IOKitPersonalities&lt;/key&gt; &lt;dict&gt; &lt;key&gt;Device Driver&lt;/key&gt; &lt;dict&gt; &lt;key&gt;CFBundleIdentifier&lt;/key&gt; &lt;string&gt;com.apple.kernel.iokit&lt;/string&gt; &lt;key&gt;IOClass&lt;/key&gt; &lt;string&gt;IOService&lt;/string&gt; &lt;key&gt;IOProviderClass&lt;/key&gt; &lt;string&gt;IOUSBInterface&lt;/string&gt; &lt;key&gt;idProduct&lt;/key&gt; &lt;integer&gt;12345&lt;/integer&gt; &lt;key&gt;idVendor&lt;/key&gt; &lt;integer&gt;67890&lt;/integer&gt; &lt;key&gt;bConfigurationValue&lt;/key&gt; &lt;integer&gt;1&lt;/integer&gt; &lt;key&gt;bInterfaceNumber&lt;/key&gt; &lt;integer&gt;0&lt;/integer&gt; &lt;/dict&gt; &lt;/dict&gt; &lt;key&gt;OSBundleRequired&lt;/key&gt; &lt;string&gt;Local-Root&lt;/string&gt; &lt;/dict&gt; &lt;/plist&gt; </code></pre> <p>The idVendor and idProduct values give the kext specificity and increase its probe score sufficiently.</p> <p>In order to use the kext, the following things need to be done (which my installer will do for clients):</p> <ol> <li>Change the owner to root:wheel (<code>sudo chown root:wheel DemiUSBDevice.kext</code>)</li> <li>Copy the kext to Extensions (<code>sudo cp DemiUSBDevice.kext /System/Library/Extensions</code>)</li> <li>Call the <em>kextload</em> utility to load the kext for immediate use without restart (<code>sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext</code>)</li> <li>Touch the Extensions folder so that the next restart will force a cache rebuild (<code>sudo touch /System/Library/Extensions</code>)</li> </ol> <p>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?</p> <p>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:</p> <pre><code>#include &lt;IOKit/IOKitLib.h&gt; #include &lt;IOKit/IOCFPlugIn.h&gt; #include &lt;IOKit/usb/IOUSBLib.h&gt; #include &lt;mach/mach.h&gt; #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, &amp;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, &amp;vendor_id)); CFDictionarySetValue(matching_dict, CFSTR(kUSBProductID), CFNumberCreate(kCFAllocatorDefault, kCFNumberInt32Type, &amp;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, &amp;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, &amp;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(); } </code></pre> <p>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:</p> <pre><code>//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); } } </code></pre> <p>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...:</p> <pre><code>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, &amp;plugin, &amp;score); //get the device interface hres = (*plugin)-&gt;QueryInterface(plugin, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID245), (void**)&amp;dev); //release the plugin - no further need for it IODestroyPlugInInterface(plugin); //double check ids for correctness result = (*dev)-&gt;GetDeviceVendor(dev, &amp;vendor); result = (*dev)-&gt;GetDeviceProduct(dev, &amp;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)-&gt;CreateInterfaceIterator(dev, &amp;request, &amp;intf_iterator); while ((usb_interface = IOIteratorNext(intf_iterator))) { //create intermediate plugin result = IOCreatePlugInInterfaceForService(usb_interface, kIOUSBInterfaceUserClientTypeID, kIOCFPlugInInterfaceID, &amp;plugin, &amp;score); //release the usb interface - not needed result = IOObjectRelease(usb_interface); //get the general interface interface hres = (*plugin)-&gt;QueryInterface(plugin, CFUUIDGetUUIDBytes( kIOUSBInterfaceInterfaceID245), (void**)&amp;intf); //release the plugin interface IODestroyPlugInInterface(plugin); //attempt to open the interface result = (*intf)-&gt;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)-&gt;GetNumEndpoints(intf, &amp;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 &lt;= interface_endpoint_count; pipe_ref++) { result = (*intf)-&gt;GetPipeProperties(intf, pipe_ref, &amp;direction, &amp;number, &amp;transfer_type, &amp;max_packet_size, &amp;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)-&gt;CreateInterfaceAsyncEventSource(m_intf, &amp;compl_event_source); CFRunLoopAddSource(CFRunLoopGetCurrent(), compl_event_source, kCFRunLoopDefaultMode); break; } break; } } </code></pre> <p>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: </p> <pre><code>result = (intf)-&gt;WritePipeAsync(intf, m_output_pipe, data, OUTPUT_DATA_BUF_SZ, device_write_completion, NULL); </code></pre> <p>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:</p> <pre><code>void DemiUSBDevice::device_write_completion(void* context, IOReturn result, void* arg0) { //... } </code></pre> <p>reading from the interrupt endpoint is similar: </p> <pre><code>result = (intf)-&gt;ReadPipeAsync(intf, m_input_pipe, data, INPUT_DATA_BUF_SZ, device_read_completion, NULL); </code></pre> <p>where device_read_completion is of the following form:</p> <pre><code>void DemiUSBDevice::device_read_completion(void* context, IOReturn result, void* arg0) { //... } </code></pre> <p>Note that to receive these callbacks the run loop must be running (<a href="http://developer.apple.com/mac/library/documentation/CoreFoundation/Reference/CFRunLoopRef/Reference/reference.html" rel="noreferrer">see this link for more information about the CFRunLoop</a>). One way to achieve this is to call <code>CFRunLoopRun()</code> 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 <code>CFRunLoopStop(CFRunLoopGetCurrent())</code> to stop the run loop and hand execution back to the main thread. </p> <p>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 <code>CFRunLoopRun()</code>, something like the following can be executed:</p> <pre><code>while (!(request-&gt;is_done)) { //run for 1/10 second to handle events Boolean returnAfterSourceHandled = false; CFTimeInterval seconds = 0.1; CFStringRef mode = kCFRunLoopDefaultMode; CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled); } </code></pre> <p>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...</p> <p>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...</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload