簡體   English   中英

在Mac上讀寫USB(HID)中斷端點

[英]Reading and writing to USB (HID) interrupt endpoints on Mac

我試圖與相當特定的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上運行。

我現在有一個USB設備的工作Mac驅動程序,該設備需要通過中斷端點進行通信。 這是我的做法:

最終,最適合我的方法是選項1(上面已指出)。 如前所述,我在打開COM樣式的IOUSBInterfaceInterface到設備時遇到問題。 隨着時間的流逝,很明顯這是由於HIDManager捕獲了設備。 捕獲設備后,我無法從HIDManager奪取該設備的控制權(甚至USBInterfaceOpenSeize調用或USBDeviceOpenSeize調用都不起作用)。

要控制該設備,我需要在HIDManager之前先進行抓取。 解決方案是編寫一個無代碼的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>

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

在此初始化代碼中,有兩種方法用作回調: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);

其中data是要寫入的數據的char緩沖區,final參數是要傳遞到回調中的可選上下文對象,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_completion具有以下形式:

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

請注意,要接收這些回調,必須運行run循環( 有關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);
}

這樣做的好處是,如果您有其他使用運行循環的線程,則在另一個線程停止運行循環的情況下,您不會過早退出...

我希望這對人們有幫助。 為了解決這個問題,我不得不從許多不完整的資源中獲取資源,這需要大量的工作才能正常運行...

我遇到了同樣的kIOReturnExclusiveAccess。 而不是與之抗爭(建造結局等)。 我找到了設備並使用了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);
  }

在幾次閱讀此問題並進行了思考之后,我想到了另一種模擬阻塞讀取行為的解決方案,但使用HID管理器而不是替換它。

阻塞讀取功能可以為設備注冊輸入回調,在當前運行循環上注冊設備,然后通過調用CFRunLoopRun()進行阻塞。 然后,輸入回調可以將報告復制到共享緩沖區中,並調用CFRunLoopStop(),這將導致CFRunLoopRun()返回,從而取消阻塞read()。 然后,read()可以將報告返回給調用方。

我能想到的第一個問題是設備已經在運行循環中進行調度的情況。 在讀取功能中安排設備的時間,然后再安排其時間,可能會產生不利影響。 但這僅是一個問題,如果應用程序嘗試在同一設備上同時使用同步和異步調用。

想到的第二件事是調用代碼已經有一個運行循環正在運行的情況(例如Cocoa和Qt應用程序)。 但是,CFRunLoopStop()的文檔似乎表明對CFRunLoopRun()的嵌套調用已得到正確處理。 因此,應該沒問題。

這里有一些簡化的代碼。 我剛剛在我的HID庫中實現了類似的功能,盡管我沒有對其進行廣泛的測試,但它似乎仍然可以工作。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM