首页 文章

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

提问于
浏览
20

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

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

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

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

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

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

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

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

3 回答

  • 2

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

    最终,对我来说效果很好的方法是选项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>
    

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

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

    • 将所有者更改为root:wheel( sudo chown root:wheel DemiUSBDevice.kext

    • 将kext复制到Extensions( sudo cp DemiUSBDevice.kext /System/Library/Extensions

    • 调用kextload实用程序加载kext以便立即使用而无需重启( sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext

    • 触摸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();
    }
    

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

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

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

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

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

    从中断 endpoints 读取类似:

    result = (intf)->ReadPipeAsync(intf, m_input_pipe, 
              data, INPUT_DATA_BUF_SZ, device_read_completion, 
              NULL);
    

    其中device_read_completion具有以下形式:

    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') . 在调用read / write方法之后,不是调用 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);
    }
    

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

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

  • 30

    在阅读了几次这个问题并稍微思考之后,我想到了另一种模拟阻塞读取行为的解决方案,但是使用HID管理器而不是替换它 .

    阻塞读取功能可以为设备注册输入回调,在当前运行循环中注册设备,然后通过调用CFRunLoopRun()来阻止 . 然后,输入回调可以将报告复制到共享缓冲区并调用CFRunLoopStop(),这会导致CFRunLoopRun()返回,从而解除对read()的阻塞 . 然后,read()可以将报告返回给调用者 .

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

    我想到的第二件事是调用代码已经运行了一个运行循环(例如Cocoa和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;
    }
    
  • 2

    我遇到了同样的kIOReturnExclusiveAccess . 而不是打架( Build 一个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和read / write ..

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

相关问题