C++ 读取和写入Mac上的USB(HID)中断端点

C++ 读取和写入Mac上的USB(HID)中断端点,c++,macos,usb,hid,kernel-extension,C++,Macos,Usb,Hid,Kernel Extension,我正在尝试与一个相当特殊的USB设备通信,并开发Windows和Mac代码来实现这一点 该设备是具有HID接口(3类)的USB设备,具有两个端点、一个中断输入和一个中断输出。设备的性质是,只有当从主机请求数据时,才在输入端点上从设备发送数据:主机向其发送设备在其输入中断端点上响应的数据。将数据写入设备(写入)要简单得多 Windows的代码相当简单:我获得设备的句柄,然后调用ReadFile或WriteFile。显然,许多底层异步行为都被抽象出来了。它似乎工作得很好 然而,在Mac上,它有点粘。

我正在尝试与一个相当特殊的USB设备通信,并开发Windows和Mac代码来实现这一点

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

Windows的代码相当简单:我获得设备的句柄,然后调用ReadFile或WriteFile。显然,许多底层异步行为都被抽象出来了。它似乎工作得很好

然而,在Mac上,它有点粘。我已经尝试了很多事情,没有一件是完全成功的,但是这里有两件事情看起来是最有希望的

1.)尝试通过iOnBinterfaceInterface访问设备(如USB),遍历端点以确定输入和输出端点,并(希望)使用ReadPipe和WritePie进行通信。不幸的是,一旦我有了接口,我就无法打开它,返回值(kioreturneExclusiveAccess)指出某些东西已经以独占方式打开了设备。我曾尝试使用iOnBinterfaceInterface183,以便调用usBinterfaceOpenCapture,但这会导致相同的返回错误值

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

2.)将设备作为IOHidDeviceInterface 122(或更高版本)打开。为了进行读取,我设置了一个异步端口、事件源和回调方法,以便在数据准备就绪时调用——当数据从输入中断端点上的设备发送时调用。但是,要写入设备需要的数据来初始化响应,我找不到方法。我被难住了。setReport通常写入控制端点,另外我需要一个不需要任何直接响应、无阻塞的写入


我在网上四处看看,尝试了很多东西,但没有一件能让我成功。有什么建议吗?我不能使用太多Apple HIDManager代码,因为大部分代码都是10.5+,我的应用程序也必须在10.4上运行。

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

最终,对我有效的方法是选项1(如上所述)。如前所述,我在打开设备的COM风格iOnBinterface接口时遇到问题。随着时间的推移,很明显这是由于HIDManager捕获了设备。一旦设备被捕获,我就无法从HIDManager获得对它的控制权(即使是usbinterfaceOpenCapture调用或usbdeviceSensize调用也无法工作)

为了控制这个设备,我需要在经理面前抓住它。解决方法是编写一个无代码的kext(内核扩展)。kext本质上是一个位于系统/库/扩展中的包,其中包含(通常)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>
在此初始化代码中,有两种方法用作回调:device_detach_callback和device_attach_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);
    }
}
设备连接回调是最神奇的地方。在我的代码中,我将其分解为多个方法,但在这里,我将把它作为一个大的整体方法来表示…:

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;
    }
}
其中data是要写入的数据的char缓冲区,最后一个参数是要传递到回调的可选上下文对象,device_write_completion是一个静态方法,具有以下一般形式:

void DemiUSBDevice::device_write_completion(void* context, 
    IOReturn result, void* arg0)
{
  //...
}
从中断端点读取的数据类似:

result = (intf)->ReadPipeAsync(intf, m_input_pipe, 
          data, INPUT_DATA_BUF_SZ, device_read_completion, 
          NULL);
其中,设备读取完成为以下形式:

void DemiUSBDevice::device_read_completion(void* context, 
    IOReturn result, void* arg0)
{
  //...
}
请注意,要接收这些回调,运行循环必须正在运行()。实现这一点的一种方法是在调用异步读或写方法之后调用
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);
}
这样做的好处是,如果有其他线程使用运行循环,那么在其他线程停止运行循环时,您不会过早退出

我希望这对人们有帮助。为了解决这个问题,我不得不从许多不完整的资料中寻找线索
void DemiUSBDevice::device_read_completion(void* context, 
    IOReturn result, void* arg0)
{
  //...
}
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);
}
/* An IN report callback that stops its run loop when called. 
   This is purely for emulating blocking behavior in the read() method */
static void input_oneshot(void*           context,
                          IOReturn        result,
                          void*           deviceRef,
                          IOHIDReportType type,
                          uint32_t        reportID,
                          uint8_t*        report,
                          CFIndex         length)
{
    buffer_type *const buffer = static_cast<HID::buffer_type*>(context);

    /* If the report is valid, copy it into the caller's buffer
         The Report ID is prepended to the buffer so the caller can identify
         the report */
    if( buffer )
    {
        buffer->clear();    // Return an empty buffer on error
        if( !result && report && deviceRef )
        {
            buffer->reserve(length+1);
            buffer->push_back(reportID);
            buffer->insert(buffer->end(), report, report+length);
        }
    }

    CFRunLoopStop(CFRunLoopGetCurrent());
}

// Block while waiting for an IN interrupt report
bool read(buffer_type& buffer)
{
    uint8_t _bufferInput[_lengthInputBuffer];

    // Register a callback
    IOHIDDeviceRegisterInputReportCallback(deviceRef, _bufferInput, _lengthInputBuffer, input_oneshot, &buffer);

    // Schedule the device on the current run loop
    IOHIDDeviceScheduleWithRunLoop(deviceRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

    // Trap in the run loop until a report is received
    CFRunLoopRun();

    // The run loop has returned, so unschedule the device
    IOHIDDeviceUnscheduleFromRunLoop(deviceRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

    if( buffer.size() )
        return true;
    return false;
}
//My funcation was named differently, but I'm using this for continuity..
void DemiUSBDevice::device_attach_callback(void * context, 
    io_iterator_t iterator)
{
DeviceManager *deviceManager = (__bridge DADeviceManager *)context;
  io_registry_entry_t device;
  while ((device = IOIteratorNext(iterator))) {

    CFTypeRef prop;
    prop = IORegistryEntrySearchCFProperty(device,
                                           kIOServicePlane,
                                           CFSTR(kIODialinDeviceKey),
                                           kCFAllocatorDefault,
                                           kIORegistryIterateRecursively);
    if(prop){
      deviceManager->devPath = (__bridge NSString *)prop;
      [deviceManager performSelector:@selector(openDevice)];
    }
  }
}
int dfd;
dfd = open([devPath UTF8String], O_RDWR | O_NOCTTY | O_NDELAY);
  if (dfd == -1) {
    //Could not open the port.
    NSLog(@"open_port: Unable to open %@", devPath);
    return;
  } else {
    fcntl(fd, F_SETFL, 0);
  }