Чтение и запись на конечные точки прерываний USB (HID) на Mac - PullRequest
20 голосов
/ 30 июля 2010

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

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

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

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

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

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

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

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

Ответы [ 3 ]

31 голосов
/ 20 августа 2010

Теперь у меня есть работающий драйвер Mac для устройства USB, который требует связи через конечные точки прерывания. Вот как я это сделал:

В конечном счете, метод, который хорошо работал для меня, был вариант 1 (отмеченный выше). Как уже отмечалось, у меня возникли проблемы с открытием IOUSBInterfaceInterface в стиле COM на устройстве. Со временем стало ясно, что это связано с захватом устройства HIDManager. Мне не удалось вырвать управление устройством из HIDManager после его захвата (даже вызов USBInterfaceOpenSeize или вызовы USBDeviceOpenSeize работать не будут).

Чтобы получить контроль над устройством, мне нужно было захватить его перед HIDManager. Решением этой проблемы было написание без кода кода (расширение ядра). По сути, 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, необходимо сделать следующее (что мой установщик сделает для клиентов):

  1. Смена владельца на root: wheel (sudo chown root:wheel DemiUSBDevice.kext)
  2. Скопируйте кекст в Расширения (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);

где data - это буфер данных для записи, последний параметр - необязательный объект контекста для передачи в обратный вызов, а 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()), чтобы остановить цикл выполнения и вручную вернуться к основному потоку.

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

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

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

2 голосов
/ 16 января 2013

Я столкнулся с тем же kIOReturnExclusiveAccess. Вместо того, чтобы бороться с этим (создание kext и т. Д.). Я нашел устройство и использовал API POSIX.

//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);
  }
2 голосов
/ 04 мая 2011

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