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

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

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

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

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

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

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

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

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

20
задан Demi 20 August 2010 в 00:13
поделиться

1 ответ

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

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

Чтобы получить контроль над устройством, мне нужно было захватить его до HIDManager. Решением было написать kext (расширение ядра) без кода. Kext - это, по сути, пакет, который находится в System / Library / Extensions, который, помимо прочего, содержит (обычно) plist (список свойств) и (иногда) драйвер уровня ядра. В моем случае мне нужен был только plist, который давал бы ядру инструкции, на каких устройствах оно соответствует. Если данные дают более высокий тестовый балл , чем HIDManager, то я мог бы захватить устройство и использовать драйвер пользовательского пространства для связи с ним.

Список kext с некоторыми измененными деталями, относящимися к конкретному проекту, выглядит следующим образом:

<?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 не мог захватить мое устройство. Что теперь с этим делать? Как писать и читать из него?

Ниже приведены некоторые упрощенные фрагменты моего кода без обработки ошибок, которые иллюстрируют решение. Прежде чем делать что-либо с устройством, приложение должно знать, когда устройство подключается (и отключается). Обратите внимание, что это просто для целей иллюстрации - некоторые из переменных являются уровнями класса, некоторые - глобальными и т. Д. Вот код инициализации, который устанавливает события присоединения / отсоединения:

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

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

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

33
ответ дан 30 November 2019 в 00:04
поделиться
Другие вопросы по тегам:

Похожие вопросы: