2010-07-30 2 views
19

Я пытаюсь установить связь с определенным устройством USB и разрабатывать как Windows, так и Mac.Чтение и запись на конечные точки прерывания USB (HID) на 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.

ответ

28

У меня есть рабочий драйвер 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, следующие вещи необходимо сделать (что мой инсталлятор сделает для клиентов):

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

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

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

Обратите внимание, что для получения этих обратных вызовов цикл выполнения должен быть запущен (see this link for more information about the 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); 
} 

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

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

+0

+1. +100 Этот ответ блестящий, и я очень благодарен за вашу тяжелую работу. – TarkaDaal

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'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; 
} 
1

Я столкнулся с этим самым 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 установлен вы можете вызвать открытый и чтение/запись ..

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

Вы работали со спрятанным устройством? –

 Смежные вопросы

  • Нет связанных вопросов^_^