2010-07-30 12 views
19

Estoy tratando de comunicarme con un dispositivo USB bastante específico y desarrollando tanto el código de Windows como el de Mac para hacerlo.Lectura y escritura en puntos finales de interrupción USB (HID) en Mac

El dispositivo es un dispositivo USB con una interfaz HID (clase 3) con dos puntos finales, una entrada de interrupción y una salida de interrupción. La naturaleza del dispositivo es tal que los datos se envían desde el dispositivo en el punto final de entrada solo cuando se solicitan datos desde el host: el host envía los datos a los que responde el dispositivo en su punto final de interrupción de entrada. Obtener datos en el dispositivo (una escritura) es mucho más simple ...

El código para Windows es bastante sencillo: obtengo un identificador para el dispositivo y luego llamo a ReadFile o WriteFile. Aparentemente, gran parte del comportamiento asincrónico subyacente se abstrae. Parece que funciona bien.

En Mac, sin embargo, es un poco más pegajoso. He intentado varias cosas, ninguna que ha sido totalmente exitosa, pero aquí están las dos cosas que parecían más prometedoras ...

1.) Intento tener acceso al dispositivo (como USB) a través de IOUSBInterfaceInterface, iterate a través de los puntos finales para determinar los puntos finales de entrada y salida, y (con suerte) utilizar ReadPipe y WritePipe para comunicarse. Lamentablemente, no puedo abrir la interfaz una vez que la tengo, con el valor de retorno (kIOReturnExclusiveAccess) que indica que algo ya tiene el dispositivo abierto exclusivamente. He intentado usar IOUSBinterfaceInterface183, para poder llamar a USBInterfaceOpenSeize, pero eso da como resultado el mismo valor de error de retorno.

--- --- actualizar 7/30/2010
Al parecer, Apple IOUSBHIDDriver partidos temprano para el dispositivo y esto es lo que probablemente impide la apertura de la IOUSBInterfaceInterface. De alguna investigación al respecto, parece que la forma más común de evitar que IOUSBHIDDriver coincida es escribir un kext sin código (extensión kernel) con un puntaje de prueba más alto. Esto coincidiría temprano, evitando que IOUSBHIDDriver abra el dispositivo, y debería, en teoría, permitirme abrir la interfaz y escribir y leer directamente a los puntos finales. Esto está bien, pero preferiría no tener que instalar algo adicional en la máquina del usuario. Si alguien conoce una alternativa sólida, agradecería la información.

2.) Abra el dispositivo como un IOHIDDeviceInterface122 (o posterior). Para leer, configuré un puerto asíncrono, un origen de evento y un método de devolución de llamada para llamar cuando los datos están listos, cuando los datos se envían desde el dispositivo en el punto final de interrupción de entrada. Sin embargo, para escribir los datos (que el dispositivo necesita) para inicializar una respuesta, no puedo encontrar la manera. Estoy perplejo. setReport generalmente escribe en el punto final de control, además de que necesito una escritura que no espera ninguna respuesta directa, ningún bloqueo.

He mirado en línea y he intentado muchas cosas, pero ninguna de ellas me está dando éxito. ¿Algún consejo? No puedo usar gran parte del código de Apple HIDManager ya que gran parte de eso es 10.5+ y mi aplicación también debe funcionar en 10.4.

Respuesta

28

Ahora tengo un controlador de Mac que funciona en un dispositivo USB que requiere comunicación a través de puntos finales de interrupción. Así es como lo hice:

En última instancia, el método que funcionó bien para mí fue la opción 1 (señalada anteriormente). Como noté, estaba teniendo problemas para abrir el IOUSBInterfaceInterface estilo COM al dispositivo. Con el tiempo quedó claro que esto se debía a que HIDManager capturaba el dispositivo. No pude arrebatar el control del dispositivo desde el HIDManager una vez que fue capturado (ni siquiera la llamada USBInterfaceOpenSeize o las llamadas USBDeviceOpenSeize funcionarían).

Para tomar el control del dispositivo, necesitaba agarrarlo antes del HIDManager. La solución a esto fue escribir un kext sin código (extensión kernel).Un kext es esencialmente un paquete que se encuentra en System/Library/Extensions que contiene (normalmente) un plist (lista de propiedades) y (ocasionalmente) un controlador de kernel, entre otros elementos. En mi caso, solo quería el plist, que daría las instrucciones al kernel sobre con qué dispositivos coincidiría. Si los datos dan una puntuación de la sonda más alta que HIDManager, entonces esencialmente podría capturar el dispositivo y usar un controlador de espacio de usuario para comunicarme con él.

El plist kext escrita, con algunos detalles específicos del proyecto modificados, es como sigue:

<?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> 

Los valores idVendor y idProduct dan la especificidad kext y aumentar su puntuación sonda suficientemente.

Para utilizar el kext, las siguientes cosas hay que hacer (que mi instalador hará para los clientes):

  1. cambiar el propietario a root: rueda (sudo chown root:wheel DemiUSBDevice.kext)
  2. Copiar el kext a las extensiones (sudo cp DemiUSBDevice.kext /System/Library/Extensions)
  3. llamada la utilidad kextload para cargar el kext para su uso inmediato sin necesidad de reiniciar (sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext)
  4. táctil de la carpeta Extensiones de modo que el siguiente reinicio obligará a reconstruir una memoria caché (sudo touch /System/Library/Extensions)

En este punto, el sistema debe utilizar el kext para mantener el HIDManager desde la captura de mi dispositivo. Ahora, ¿qué hacer con eso? ¿Cómo escribir y leer de él?

Los siguientes son algunos fragmentos simplificados de mi código, menos el manejo de errores, que ilustran la solución. Antes de poder hacer nada con el dispositivo, la aplicación necesita saber cuándo el dispositivo se conecta (y se separa). Tenga en cuenta que esto es sólo para fines de ilustración - algunas de las variables son clase de nivel, algunos son globales, etc. Este es el código de inicialización que establece la fijación/separan los eventos hasta:

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

Hay dos métodos que se utilizan como devoluciones de llamada en este código de inicialización: device_detach_callback y device_attach_callback (ambos declarados en métodos estáticos). device_detach_callback es sencillo:

//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 es donde la mayor parte de la magia sucede. En mi código tengo esta dividida en varios métodos, pero aquí voy a presentarlo como un método monolítico grande ...:

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

En este punto debemos tener los números de los puntos finales de interrupción y una abierta IOUSBInterfaceInterface al dispositivo.Una escritura asíncrona de datos se puede hacer llamando a algo como:

result = (intf)->WritePipeAsync(intf, m_output_pipe, 
      data, OUTPUT_DATA_BUF_SZ, device_write_completion, 
      NULL); 

donde los datos es un char buffer de datos para escribir, el último parámetro es un objeto de contexto opcional para pasar a la devolución de llamada, y device_write_completion es una estática método con la siguiente forma general:

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

leyendo desde el extremo de interrupción es similar:

result = (intf)->ReadPipeAsync(intf, m_input_pipe, 
      data, INPUT_DATA_BUF_SZ, device_read_completion, 
      NULL); 

donde device_read_completi el es de la siguiente forma:

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

Tenga en cuenta que estas devoluciones de llamada para recibir el bucle de ejecución debe estar en ejecución (see this link for more information about the CFRunLoop). Una forma de lograr esto es llamar al CFRunLoopRun() después de llamar a los métodos de lectura o escritura asíncronos, en cuyo punto se bloquea el hilo principal mientras se ejecuta el ciclo de ejecución. Después de manejar su devolución de llamada, puede llamar al CFRunLoopStop(CFRunLoopGetCurrent()) para detener el ciclo de ejecución y la ejecución manual de nuevo al hilo principal.

Otra alternativa (que hago en mi código) es pasar un objeto de contexto (llamado 'solicitud' en el siguiente ejemplo de código) a los métodos WritePipeAsync/ReadPipeAsync - este objeto contiene un indicador de finalización booleano (llamado 'is_done' en este ejemplo). Después de llamar al método de lectura/escritura, en lugar de llamar CFRunLoopRun(), algo como lo siguiente puede ser ejecutada:

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

Esto tiene la ventaja de que si usted tiene otros hilos que utilizan el bucle de ejecución que no lo hará de forma prematura salida debe otro hilo detener el ciclo de ejecución ...

Espero que esto sea útil para las personas. Tuve que extraer de muchas fuentes incompletas para resolver este problema y esto requirió un trabajo considerable para funcionar bien ...

+0

+1. +100 Esta respuesta es brillante, y estoy muy agradecido por su arduo trabajo. – TarkaDaal

2

Después de leer esta pregunta unas cuantas veces y pensar en ello un poco, pensé en otra solución para emular bloqueando el comportamiento de lectura, pero usando el administrador HID en lugar de reemplazarlo.

Una función de lectura de bloqueo puede registrar una devolución de llamada de entrada para el dispositivo, registrar el dispositivo en el ciclo de ejecución actual y luego bloquear llamando a CFRunLoopRun(). La devolución de llamada de entrada puede copiar el informe en un búfer compartido y llamar a CFRunLoopStop(), lo que hace que CFRunLoopRun() regrese, lo que desbloquea la lectura(). Luego, read() puede devolver el informe a la persona que llama.

El primer problema que se me ocurre es el caso en el que el dispositivo ya está programado en un ciclo de ejecución. Programar y luego no programar el dispositivo en la función de lectura puede tener efectos adversos. Pero eso solo sería un problema si la aplicación está intentando usar llamadas sincrónicas y asíncronas en el mismo dispositivo.

Lo segundo que viene a la mente es el caso en el que el código de llamada ya tiene un bucle de ejecución en ejecución (aplicaciones Cocoa y Qt, por ejemplo). Pero, la documentación para CFRunLoopStop() parece indicar que las llamadas anidadas a CFRunLoopRun() se manejan correctamente. Por lo tanto, debería estar bien.

Aquí hay un poco de código simplificado para ir con eso. Acabo de implementar algo similar en mi HID Library y parece funcionar, aunque no lo he probado exhaustivamente.

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

Me encontré con este mismo kIOReturnExclusiveAccess. En lugar de luchar contra él (construir un kext, etc.). Encontré el dispositivo y usé las API de 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)]; 
    } 
    } 
} 

una vez que se establece DEVPATH puede llamar a abrir y leer/escribir ..

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

¿Estabas trabajando con un dispositivo escondido? –

Cuestiones relacionadas