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):
- cambiar el propietario a root: rueda (
sudo chown root:wheel DemiUSBDevice.kext
)
- Copiar el kext a las extensiones (
sudo cp DemiUSBDevice.kext /System/Library/Extensions
)
- llamada la utilidad kextload para cargar el kext para su uso inmediato sin necesidad de reiniciar (
sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext
)
- 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 ...
+1. +100 Esta respuesta es brillante, y estoy muy agradecido por su arduo trabajo. – TarkaDaal