在Mac上读取和写入USB(HID)中断端点

Dem*_*emi 20 c++ macos usb hid kernel-extension

我正在尝试与一个相当具体的USB设备进行通信,并开发Windows和Mac代码.

该设备是带有HID接口(3级)的USB设备,带有两个端点,一个中断输入和一个中断输出.设备的性质使得只有在从主机请求数据时才从输入端点上的设备发送数据:主机向其发送设备在其输入中断端点上响应的数据.将数据传输到设备(写入)要简单得多......

Windows的代码非常简单:我获得了设备的句柄,然后调用ReadFile或WriteFile.显然,大部分底层异步行为都被抽象出来了.它似乎工作正常.

然而,在Mac上,它有点粘.我尝试了很多东西,没有一个已经完全成功,但这里有两件似乎最有希望的东西......

1.)尝试通过IOUSBInterfaceInterface访问设备(作为USB),遍历端点以确定输入和输出端点,并(希望)使用ReadPipe和WritePipe进行通信.不幸的是,我无法打开界面,返回值(kIOReturnExclusiveAccess)注意到某些东西已经让设备独占打开.我已经尝试使用IOUSBinterfaceInterface183,因此我可以调用USBInterfaceOpenSeize,但这会导致相同的返回错误值.

---更新
7/30/2010 --- 显然,Apple IOUSBHIDDriver会早期与设备匹配,这可能会阻止打开IOUSBInterfaceInterface.从一些挖掘中可以看出,防止IOUSBHIDDriver匹配的常见方法是编写具有更高探测分数的无代码kext(内核扩展).这将提前匹配,阻止IOUSBHIDDriver打开设备,理论上应该允许我打开接口并直接写入和读取端点.这没关系,但我更希望不必在用户机器上安装额外的东西.如果有人知道一个可靠的选择,我会感谢这些信息.

2.)将设备作为IOHIDDeviceInterface122(或更高版本)打开.为了读取,我设置了一个异步端口,事件源和回调方法,在数据就绪时调用 - 当数据从输入中断端点上的设备发送时.但是,要写入数据 - 设备需要 - 来初始化响应我找不到办法.我很难过.setReport通常写入控制端点,而且我需要一个不期望任何直接响应,没有阻塞的写入.

我在网上看了看并尝试过很多东西,但没有一个能给我带来成功.有什么建议?我不能使用很多Apple HIDManager代码,因为其中大部分是10.5+,我的应用程序也必须在10.4上运行.

Dem*_*emi 33

我现在有一个工作的Mac驱动程序到USB设备,需要通过中断端点进行通信.我是这样做的:

最终,对我来说效果很好的方法是选项1(如上所述).如上所述,我在向设备打开COM样式的IOUSBInterfaceInterface时遇到了问题.随着时间的推移,这很明显是由于HIDManager捕获设备.一旦被捕获,我就无法从HIDManager中夺取设备的控制权(甚至USBInterfaceOpenSeize调用或USBDeviceOpenSeize调用都不起作用).

要控制我需要在HIDManager之前抓取它的设备.解决方法是编写无代码kext(内核扩展).kext本质上是一个包含在System/Library/Extensions中的包,它包含(通常)plist(属性列表)和(偶尔)内核级驱动程序以及其他项目.在我的情况下,我只想要plist,它会向内核提供与它匹配的设备的指令.如果数据提供的探测分数高于HIDManager,那么我基本上可以捕获设备并使用用户空间驱动程序与之通信.

编写的kext plist,修改了一些项目特定的细节,如下所示:

<?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>
Run Code Online (Sandbox Code Playgroud)

idVendor和idProduct值赋予kext特异性并充分提高其探针得分.

为了使用kext,需要完成以下事项(我的安装程序将为客户端执行此操作):

  1. 将所有者更改为root:wheel(sudo chown root:wheel DemiUSBDevice.kext)
  2. 将kext复制到Extensions(sudo cp DemiUSBDevice.kext /System/Library/Extensions)
  3. 调用kextload实用程序加载kext以便立即使用而无需重启(sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext)
  4. 触摸Extensions文件夹,以便下次重新启动将强制缓存重建(sudo touch /System/Library/Extensions)

此时系统应使用kext来防止HIDManager捕获我的设备.现在,该怎么办?如何写入和读取?

以下是我的代码的一些简化片段,减去任何错误处理,说明了解决方案.在能够对设备执行任何操作之前,应用程序需要知道设备何时附加(和分离).请注意,这仅仅是为了说明 - 一些变量是类级别的,一些是全局的,等等.以下是设置attach/detach事件的初始化代码:

#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();
}
Run Code Online (Sandbox Code Playgroud)

在此初始化代码中有两种方法用作回调:device_detach_callback和device_attach_callback(均在静态方法中声明).device_detach_callback非常简单:

//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);
    }
}
Run Code Online (Sandbox Code Playgroud)

device_attach_callback是大多数魔法发生的地方.在我的代码中,我将其分解为多种方法,但在这里我将它呈现为一个巨大的单片方法......:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

此时,我们应该拥有中断端点的编号和设备的开放IOUSBInterfaceInterface.可以通过调用以下内容来完成异步数据写入:

result = (intf)->WritePipeAsync(intf, m_output_pipe, 
          data, OUTPUT_DATA_BUF_SZ, device_write_completion, 
          NULL);
Run Code Online (Sandbox Code Playgroud)

其中data是要写入的数据的char缓冲区,final参数是传递给回调的可选上下文对象,device_write_completion是一个静态方法,具有以下一般形式:

void DemiUSBDevice::device_write_completion(void* context, 
    IOReturn result, void* arg0)
{
  //...
}
Run Code Online (Sandbox Code Playgroud)

从中断端点读取类似:

result = (intf)->ReadPipeAsync(intf, m_input_pipe, 
          data, INPUT_DATA_BUF_SZ, device_read_completion, 
          NULL);
Run Code Online (Sandbox Code Playgroud)

其中device_read_completion具有以下形式:

void DemiUSBDevice::device_read_completion(void* context, 
    IOReturn result, void* arg0)
{
  //...
}
Run Code Online (Sandbox Code Playgroud)

请注意,要接收这些回调,必须运行运行循环(有关CFRunLoop的更多信息,请参阅此链接).实现此目的的一种方法是CFRunLoopRun()在调用异步读取或写入方法之后调用,此时主线程在运行循环运行时阻塞.处理CFRunLoopStop(CFRunLoopGetCurrent())完回调后,您可以调用停止运行循环并将执行交回主线程.

另一个替代方法(我在我的代码中执行)是将上下文对象(在以下代码示例中名为'request')传递给WritePipeAsync/ReadPipeAsync方法 - 此对象包含一个布尔完成标志(在此示例中名为'is_done') .在调用读/写方法之后,CFRunLoopRun()可以执行以下类似的操作,而不是调用:

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);
}
Run Code Online (Sandbox Code Playgroud)

这样做的好处是,如果你有其他线程使用运行循环,如果另一个线程停止运行循环,你将不会过早退出...

我希望这对人们有所帮助.我不得不从许多不完整的消息来源解决这个问题,这需要相当多的工作来运行良好...

  • +1。+100 这个答案很棒,我非常感谢你的辛勤工作。 (2认同)