2010-07-30 156 views
19

我正在尝试与相当特定的USB设备进行通信并开发Windows和Mac代码。在Mac上读取和写入USB(HID)中断端点

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

Windows的代码非常简单:我得到设备句柄,然后调用ReadFile或WriteFile。显然,很多基础异步行为都被抽象出来了。它似乎工作正常。

但是,在Mac上,它有点粘。我已经尝试了很多东西,但没有一个已经完全成功,但这里有两件事似乎最有前途......

1.)试图通过IOUSBInterfaceInterface访问设备(如USB),迭代通过端点确定输入和输出端点,并(希望)使用ReadPipe和WritePipe进行通信。不幸的是,我一旦拥有它就无法打开接口,返回值(kIOReturnExclusiveAccess)注意到某些东西已经使设备独占打开。我尝试过使用IOUSBinterfaceInterface183,以便我可以调用USBInterfaceOpenSeize,但这会导致相同的返回错误值。

---更新2010年7月30日---
显然,苹果IOUSBHIDDriver年初到设备相匹配,这是什么可能阻止打开IOUSBInterfaceInterface。从一些挖掘看来,防止IOUSBHIDDriver匹配的常见方式是编写一个代码更小的kext(内核扩展),并使用更高的探测分数。这可以尽早匹配,防止IOUSBHIDDriver打开设备,并且理论上应该允许我打开接口并直接写入和读取端点。这是好的,但我更愿意不必在用户机器上安装额外的东西。如果有人知道一个可靠的选择,我会感谢信息。

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

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

回答

28

我现在有一个可用的Mac驱动程序到需要通过中断端点进行通信的USB设备。以下是我的做法:

最终,对我而言效果最好的方法是选项1(如上所述)。如上所述,我在向设备打开COM样式的IOUSBInterfaceInterface时遇到了问题。随着时间的推移,很明显这是由于HIDManager捕获设备造成的。一旦捕获到设备(即使USBInterfaceOpenSeize调用或USBDeviceOpenSeize调用不起作用),我也无法从HIDManager控制设备。

要控制设备,我需要在HIDManager之前抓住它。解决方案是编写无代码kext(内核扩展)。kext基本上是一个包含在System/Library/Extensions中的包,通常包含plist(属性列表)和(有时)内核级驱动程序等。在我的情况下,我只想要plist,它会向内核提供与它匹配的设备的说明。如果数据比HIDManager提供更高的探测分数,那么我基本上可以捕获设备并使用用户空间驱动程序与其通信。

的内核扩展的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> 

的idVendor和idProduct值给出的kext特异性和充分提高其探测积分。

为了使用KEXT,下面的事情需要做(这我的安装程序会为客户做):

  1. 更改所有者根:车轮(sudo chown root:wheel DemiUSBDevice.kext
  2. 复制KEXT以扩展(sudo cp DemiUSBDevice.kext /System/Library/Extensions
  3. 调用kextload实用程序加载立即使用的kext不重新启动(sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext
  4. 触摸扩展文件夹,以便下一次重启将强制缓存重建(sudo touch /System/Library/Extensions

此时系统应该使用kext保持HIDManager无法捕获我的设备。现在,该怎么办?如何写入和读取它?

以下是我的代码的简化片段,减去任何错误处理,说明解决方案。在能够对设备进行任何操作之前,应用程序需要知道设备何时附着(和分离)。请注意,这仅仅是为了说明目的 - 一些变量是类级别的,有些是全球性的,等这里是初始化代码,设置安装/拆卸活动起来:

#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(); 
} 

有两种方法在初始化代码中用作回调: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); 
    } 
} 

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; 
    } 
} 

在这一点上,我们应该有中断端点的数量和开放IOUSBInterfaceInterface到设备。数据的异步写入可以通过调用像这样做:

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

,其中数据是写的字符缓冲区,最后一个参数是一个可选的上下文对象传递给回调函数,并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); 

其中device_read_completi在如下形式:

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

注意,要接收这些回调的运行循环必须运行(see this link for more information about the 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); 
} 

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

我希望这是有帮助的人。我不得不从许多来源不全拉来解决这个问题,这需要大量的工作来获得运行良好......

+0

+1。 +100这个答案很棒,我非常感谢你的辛勤工作。 – TarkaDaal 2014-05-08 09:50:27

2

阅读这个问题了几次,对这个问题考虑了一下之后,我认为另一个解决方案,用于仿真阻止读取行为,但使用HID管理器而不是替换它。

一种阻塞读取功能可以为设备寄存器的输入的回调,寄存器上当前运行回路的装置中,然后通过调用CFRunLoopRun块()。输入回调可以将报表复制到共享缓冲区并调用CFRunLoopStop(),这会导致CFRunLoopRun()返回,从而解除对read()的阻止。然后,read()可以将该报告返回给调用者。

我能想到的第一个问题就是该设备已经被调度上运行循环的情况。在读取功能中调度然后取消调度设备可能会产生不利影响。但是,如果应用程序试图在同一设备上使用同步和异步调用,那只会是一个问题。

,想到的第二件事是在调用的代码已经运行(可可和Qt的应用程序例如)运行循环的情况。但是,CFRunLoopStop()的文档似乎表明,对CFRunLoopRun()的嵌套调用可以正确处理。所以,应该没问题。

这里有点简化代码走这一点。我只是在我的HID Library中实现了类似的东西,它似乎能够工作,尽管我还没有对其进行广泛的测试。

/* 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; 
} 
1

我碰到了这个相同的kIOReturnExclusiveAccess。而不是打击它(建立kext等)。我找到了该设备并使用了POSIX API。

//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)]; 
    } 
    } 
} 

一旦DEVPATH设置,你可以调用open和读/写..

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); 
    } 
+0

你是否在使用隐藏设备? – 2015-02-04 15:02:32