Теперь у меня есть работающий драйвер 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, необходимо сделать следующее (что мой установщик сделает для клиентов):
- Смена владельца на root: wheel (
sudo chown root:wheel DemiUSBDevice.kext
)
- Скопируйте кекст в Расширения (
sudo cp DemiUSBDevice.kext /System/Library/Extensions
)
- Вызовите утилиту kextload , чтобы загрузить kext для немедленного использования без перезапуска (
sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext
)
- Коснитесь папки «Расширения», чтобы при следующем перезапуске была произведена перестройка кэша (
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);
}
Это имеет то преимущество, что если у вас есть другие потоки, которые используют цикл выполнения, который вы выиграли 'преждевременно завершить работу, если другой поток остановит цикл выполнения ...
Я надеюсь, что это полезно для людей.Мне пришлось извлечь из многих неполных источников, чтобы решить эту проблему, и это потребовало значительной работы, чтобы работать хорошо ...