Access USB device on Mac OS X using I/O Kit


This post shows how you can access a USB device from user space on Mac OS X. I used Xcode 4.3.2 on Mac OS X Lion (10.7.3) for testing the code.

Apple provides the USB Device Interface Guide that shows how USB devices can be accessed from user space. The book OS X and iOS Kernel Programming is also very helpful, see Chapter 15 – User-Space USB Drivers.

Create an Xcode console application. Add the following libraries to the default target.

  • IOKit.framework
  • CoreFoundation.framework

Xcode will have created main.c, add the following headers.

#include <stdio.h>
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/IOCFPlugIn.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/usb/USBSpec.h>

Let’s now implement the main function. Add the following variable declarations.

    CFMutableDictionaryRef matchingDictionary = NULL;
    SInt32 idVendor = 0x0000; // set vendor id
    SInt32 idProduct = 0x0000; // set product id
    io_iterator_t iterator = 0;
    io_service_t usbRef;
    SInt32 score;
    IOCFPlugInInterface** plugin;
    IOUSBDeviceInterface300** usbDevice = NULL;
    IOReturn ret;
    IOUSBConfigurationDescriptorPtr config;
    IOUSBFindInterfaceRequest interfaceRequest;
    IOUSBInterfaceInterface300** usbInterface;
    char out[] = { 0x00, 0x00 }; // set data to send
    char* in;
    UInt32 numBytes;

We now try to find the USB device using the vendor and product id.

    matchingDictionary = IOServiceMatching(kIOUSBDeviceClassName);
    CFDictionaryAddValue(matchingDictionary,
                         CFSTR(kUSBVendorID),
                         CFNumberCreate(kCFAllocatorDefault,
                                        kCFNumberSInt32Type, &idVendor));
    CFDictionaryAddValue(matchingDictionary,
                         CFSTR(kUSBProductID),
                         CFNumberCreate(kCFAllocatorDefault,
                                        kCFNumberSInt32Type, &idProduct));
    IOServiceGetMatchingServices(kIOMasterPortDefault,
                                 matchingDictionary, &iterator);
    usbRef = IOIteratorNext(iterator);
    if (usbRef == 0)
    {
        printf("Device not found\n");
        return -1;
    }
    IOObjectRelease(iterator);
    IOCreatePlugInInterfaceForService(usbRef, kIOUSBDeviceUserClientTypeID,
                                      kIOCFPlugInInterfaceID, &plugin, &score);
    IOObjectRelease(usbRef);
    (*plugin)->QueryInterface(plugin,
                              CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID300),
                              (LPVOID)&usbDevice);
    (*plugin)->Release(plugin);

Now that we have found the device, we open the device and set the first configuration as active.

    ret = (*usbDevice)->USBDeviceOpen(usbDevice);
    if (ret == kIOReturnSuccess)
    {
        // set first configuration as active
        ret = (*usbDevice)->GetConfigurationDescriptorPtr(usbDevice, 0, &config);
        if (ret != kIOReturnSuccess)
        {
            printf("Could not set active configuration (error: %x)\n", ret);
            return -1;
        }
        (*usbDevice)->SetConfiguration(usbDevice, config->bConfigurationValue);
    }
    else if (ret == kIOReturnExclusiveAccess)
    {
        // this is not a problem as we can still do some things
    }
    else
    {
        printf("Could not open device (error: %x)\n", ret);
        return -1;
    }

Having done that, we need to find the interface we want to use for sending and receiving data. Our device is a USB RNDIS device that has two bulk data endpoints on the second interface (#1).

    interfaceRequest.bInterfaceClass = kIOUSBFindInterfaceDontCare;
    interfaceRequest.bInterfaceSubClass = kIOUSBFindInterfaceDontCare;
    interfaceRequest.bInterfaceProtocol = kIOUSBFindInterfaceDontCare;
    interfaceRequest.bAlternateSetting = kIOUSBFindInterfaceDontCare;
    (*usbDevice)->CreateInterfaceIterator(usbDevice,
                                          &interfaceRequest, &iterator);
    IOIteratorNext(iterator); // skip interface #0
    usbRef = IOIteratorNext(iterator);
    IOObjectRelease(iterator);
    IOCreatePlugInInterfaceForService(usbRef,
                                      kIOUSBInterfaceUserClientTypeID,
                                      kIOCFPlugInInterfaceID, &plugin, &score);
    IOObjectRelease(usbRef);
    (*plugin)->QueryInterface(plugin,
                              CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID300),
                              (LPVOID)&usbInterface);
    (*plugin)->Release(plugin);

Now that we have found the interface, let us open it.

    ret = (*usbInterface)->USBInterfaceOpen(usbInterface);
    if (ret != kIOReturnSuccess)
    {
        printf("Could not open interface (error: %x)\n", ret);
        return -1;
    }

Finally, we can send and receive some data. The pipe references are in the same order as the end points that appear in the Bus Probe tab of USB Prober. In our case, pipe 0 is the default control endpoint (does not have an endpoint descriptor and will not appear in USB Prober), pipe 1 is the bulk data output endpoint, and pipe 2 is the bulk data input endpoint.

For some reason, if we read less than 64 bytes I/O Kit returns a kIOReturnOverrun error. Coincidentally, the max packet size of the bulk input pipe of the device is also 64 bytes.

    // Send data through pipe 1
    (*usbInterface)->WritePipe(usbInterface, 1, out, sizeof(out));

    // Read data through pipe 2
    numBytes = 64;
    in = malloc(numBytes);
    ret = (*usbInterface)->ReadPipe(usbInterface, 2, in, &numBytes);
    if (ret == kIOReturnSuccess)
    {
        printf("Read %d bytes\n", numBytes);
    }
    else
    {
        printf("Read failed (error: %x)\n", ret);
    }

To wrap it all, we close the interface and device, and return from main.

    (*usbInterface)->USBInterfaceClose(usbInterface);
    (*usbDevice)->USBDeviceClose(usbDevice);

    return 0;

Found an issue? Leave a comment below.

Advertisements

Linux USB ethernet gadget driver for RNDIS connection establishment


In this post I’ll document the RNDIS control message sequence between the USB RNDIS gadget driver and the device.

For those who are not familiar with RNDIS I refer you to Microsoft’s RNDIS page or the downloadable spec. In particular, you’ll need to become familiar with how RNDIS is mapped to USB. You’ll also need to have good familiarity with the USB.

This list summarizes the mapping:

  • USB Device
    • Interface #0
      • Endpoint #0 – Control endpoint, id 0x80 (IN) or 0x00 (OUT)
      • Endpoint #1 – Interrupt transfer, id 0x81 (IN)
    • Interface #1
      • Endpoint #3 – Bulk data transfer, id 0x03 (OUT)
      • Endpoint #2 – Bulk data transfer, id 0x82 (IN)

The above can be reproduced quite easily using the lsusb -v command.

Armed with that information I used Wireshark to sniff the USB bus. The USB capture is usually very verbose, I used the filter usb.data_flag == "present (0)" to get to the relevant packets.

Here’s the sequence of control messages that establish RNDIS communication. The first four bytes in the hex stream is the message id, in little endian order.

The REMOTE_NDIS_INITIALIZE_MSG sent by the driver over EP 0x00:

0000   02 00 00 00 18 00 00 00 01 00 00 00 01 00 00 00
0010   00 00 00 00 40 06 00 00

The REMOTE_NDIS_INITIALIZE_CMPLT message sent by the device over EP 0x80:

0000   02 00 00 80 34 00 00 00 01 00 00 00 00 00 00 00
0010   01 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00
0020   01 00 00 00 16 06 00 00 02 00 00 00 00 00 00 00
0030   00 00 00 00

A REMOTE_NDIS_QUERY_MSG message sent by the driver over EP 0x00. The OID is 0x00010202 (no official documentation):

0000   04 00 00 00 1c 00 00 00 02 00 00 00 02 02 01 00
0010   00 00 00 00 14 00 00 00 00 00 00 00

The REMOTE_NDIS_QUERY_CMPLT message sent by device as a reply, over EP 0x80. Status is RNDIS_STATUS_NOT_SUPPORTED:

0000   04 00 00 80 18 00 00 00 02 00 00 00 bb 00 00 c0
0010   00 00 00 00 00 00 00 00

The REMOTE_NDIS_QUERY_MSG message sent by the driver to discover the MAC address, over EP 0x00. The OID queried is OID_802_3_PERMANENT_ADDRESS:

0000   04 00 00 00 4c 00 00 00 03 00 00 00 01 01 01 01
0010   30 00 00 00 14 00 00 00 00 00 00 00 00 00 00 00
0020   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0030   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0040   00 00 00 00 00 00 00 00 00 00 00 00

The REMOTE_NDIS_QUERY_CMPLT message sent by device with the MAC address, over EP 0x80. Query status is RNDIS_STATUS_SUCCESS:

0000   04 00 00 80 1e 00 00 00 03 00 00 00 00 00 00 00
0010   06 00 00 00 10 00 00 00 0a 00 3e 97 c5 df

The REMOTE_NDIS_SET_MSG message send by the driver over EP 0x00. The OID being set is OID_GEN_CURRENT_PACKET_FILTER, with the value NDIS_PACKET_TYPE_PROMISCUOUS | NDIS_PACKET_TYPE_BROADCAST | NDIS_PACKET_TYPE_ALL_MULTICAST | NDIS_PACKET_TYPE_DIRECTED

0000   05 00 00 00 20 00 00 00 04 00 00 00 0e 01 01 00
0010   04 00 00 00 14 00 00 00 00 00 00 00 2d 00 00 00

The REMOTE_NDIS_SET_CMPLT message sent by the device in reply over EP 0x80:

0000   05 00 00 80 10 00 00 00 04 00 00 00 00 00 00 00

After the control message sequence is completed with success, rest of the communication is done using the REMOTE_NDIS_PACKET_MSG message (0x00000001) over the bulk transfer endpoints.

Compare two PowerPoint presentations


You can’t do it with Microsoft PowerPoint 2007. PowerPoint 2010 does have a compare feature, as did 2003. You can however publish the presentations as Word documents (including annotations) and compare those. It is a bit of a hassle but better than nothing. Note to self, if you have to review a presentation use Comments instead of making direct changes.

Poor batteries


Poor rechargeable batteries killed this otherwise fine screwdriver I purchased in 2010. I don’t think I’ll be buying a battery powered Skil gadget made in “Brasil”.

20120418-202144.jpg

Install Mac OS X Leopard in VirtualBox for Mac


Install VirtualBox version 4.1 for Mac. You’ll need the original install media of Mac OS X Leopard. Create a new VM with guest OS as Mac OS X Server. Mount the DVD drive of the host as the DVD drive for your VM. Start the VM. You’ll need to partition the virtual drive using the disk utility as a GUID Partition Table.

Kindle vs iBooks


20120418-082156.jpg

Is it just me? The mobi format book open in Kindle (on the left) doesn’t look as good as the epub format book open in iBooks. Both apps are set for night view for some late night reading.

Here’s what I find better with iBooks (epub):

– The text caption below the figure is better aligned to the figure

– There’s more text to read on the screen (having chosen the smallest font size in both apps). The text font can be changed too.

– Night view shows figures with a lower contrast, better on the eyes

– Search books

– Copy text from the book to other apps

– Smaller file size (4.7MB) than mobi (5.6MB)

I don’t know how the Kindle native format (prc) compares to mobi. The Kindle app may also be less feature-rich on the iPhone.

Communicate with USB device in C# using LibUsbDotNet


LibUsbDotNet is a cross-platform library. The source code below has been tested on Mac OS X (version 10.6.8 to be precise) using the Mono .NET run-time. On Linux and Mac LibUsbDotNet requires libusb.

On Mac OS X, you’ll need to obtain and build libusb from source. Ensure you have downloaded and installed the developer SDK for Mac. To build and install it, execute the following commands from a terminal, in the folder that contains the source of libusb.

./configure CC="gcc -m32" --prefix=/usr
make
sudo make install

The following source code snippet is an example of how to send data to a device. The code itself is a heavily modified version of the Read.Write example that ships with LibUsbDotNet.

Here’s a sample output produced by the code.

00-02-00-00-00-00-00-06-00-00-00-07-00-00-00-02-01-01-01
IoTimedOut:No more bytes!

The same code should run on Windows and Linux. For more details see the documentation page of LibUSBDotNet. They have a really nice wizard that can create an INF and installer package for Windows.

If Mono raises a System.DllNotFoundException, you might want to take a look at this page.