Подтвердить что ты не робот

Чтение и запись на конечные точки прерывания USB (HID) на Mac

Я пытаюсь общаться с довольно специфическим USB-устройством и разрабатывать как Windows, так и Mac-код для этого.

Устройство представляет собой USB-устройство с интерфейсом HID (класс 3) с двумя конечными точками, вход прерывания и выход прерывания. Характер устройства таков, что данные отправляются с устройства на конечную точку ввода только тогда, когда запрашиваются данные от хоста: хост отправляет ему данные, на которые устройство отвечает на его конечную точку прерывания ввода. Получение данных на устройство (запись) намного проще...

Код для Windows довольно прямолинейный: я получаю дескриптор устройства, а затем вызываю либо ReadFile, либо WriteFile. По-видимому, большая часть основного асинхронного поведения абстрагируется. Он работает нормально.

Однако на Mac это немного более стильно. Я пробовал несколько вещей, ни один из них не был полностью успешным, но вот две вещи, которые казались наиболее перспективными...

1.) Попытайтесь получить доступ к устройству (как USB) через IOUSBInterfaceInterface, итерации по конечным точкам для определения конечных точек ввода и вывода и (надеюсь) использовать ReadPipe и WritePipe для связи. К сожалению, я не могу открыть интерфейс, как только он есть, с возвращаемым значением (kIOReturnExclusiveAccess), отметившим, что что-то уже открыло устройство. Я попытался использовать IOUSBinterfaceInterface183, чтобы я мог вызвать USBInterfaceOpenSeize, но это приводит к тому же возврату ошибки.

--- обновление 7/30/2010 ---
По-видимому, Apple IOUSBHIDDriver соответствует раннему устройству, и это, вероятно, предотвращает открытие IOUSBInterfaceInterface. Из-за некоторого копания кажется, что общий способ предотвратить совпадение IOUSBHIDDriver заключается в написании кода без ключа (расширение ядра) с более высокой оценкой зонда. Это будет соответствовать ранним, не позволяя IOUSBHIDDriver открыть устройство, и теоретически должно позволить мне открыть интерфейс и напрямую написать и прочитать конечные точки. Это нормально, но я бы предпочел не устанавливать что-то дополнительное на машине пользователя. Если кто-нибудь знает о твердой альтернативе, я был бы благодарен за информацию.

2.) Откройте устройство как IOHIDDeviceInterface122 (или позже). Чтобы читать, я настроил асинхронный порт, источник события и метод обратного вызова, которые будут вызываться, когда данные готовы - когда данные отправляются с устройства на конечную точку прерывания ввода. Однако, чтобы написать данные - что нужно устройству - чтобы инициализировать ответ, я не могу найти способ. Я в тупике. setReport обычно записывает в конечную точку управления, плюс мне нужна запись, которая не ожидает прямого ответа, без блокировки.

Я посмотрел в Интернете и много пробовал, но ни один из них не дает мне успеха. Любой совет? Я не могу использовать большую часть кода Apple HIDManager, так как большая часть из них - 10.5+, и мое приложение должно работать и на 10.4.

4b9b3361

Ответ 1

У меня есть рабочий драйвер 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>

Значения idVendor и idProduct дают специфичность kext и значительно увеличивают его оценку зонда.

Чтобы использовать kext, необходимо выполнить следующие действия (которые мой установщик сделает для клиентов):

  • Измените владельца на root: wheel (sudo chown root:wheel DemiUSBDevice.kext)
  • Скопируйте kext в расширения (sudo cp DemiUSBDevice.kext /System/Library/Extensions)
  • Вызвать утилиту kextload для загрузки kext для немедленного использования без перезагрузки (sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext)
  • Прикоснитесь к папке "Расширения", чтобы следующий перезапуск заставил восстановить кеш (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);

где данные представляют собой буфер данных 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);

где device_read_completion имеет следующую форму:

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

Обратите внимание, что для получения этих обратных вызовов должен выполняться цикл выполнения (см. эту ссылку для получения дополнительной информации о CFRunLoop). Один из способов добиться этого - вызвать CFRunLoopRun() после вызова методов асинхронного чтения или записи, в этот момент основной поток блокируется при запуске цикла выполнения. После обработки обратного вызова вы можете вызвать CFRunLoopStop(CFRunLoopGetCurrent()), чтобы остановить цикл выполнения и выполнить ручное выполнение в основной поток.

Другой вариант (который я делаю в моем коде) - передать объект контекста (названный "запрос" в следующем примере кода) в методы 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);
}

Это имеет то преимущество, что если у вас есть другие потоки, которые используют цикл цикла, вы не сможете преждевременно выйти, если другой поток остановит цикл выполнения...

Я надеюсь, что это полезно людям. Мне пришлось тянуть из многих неполных источников, чтобы решить эту проблему, и для этого потребовалась значительная работа, чтобы хорошо работать...

Ответ 2

Прочитав этот вопрос несколько раз и немного подумав об этом, я подумал о другом решении для эмуляции поведения чтения блокировки, но вместо 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 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;
}

Ответ 3

Я столкнулся с тем же самым 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);
  }