Ответ 1
У меня есть рабочий драйвер 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, необходимо выполнить следующие действия (которые мой установщик сделает для клиентов):
- Измените владельца на root: wheel (
sudo chown root:wheel DemiUSBDevice.kext
) - Скопируйте 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 не мог захватить мое устройство. Теперь, что с этим делать? Как писать и читать из него?
Ниже приведены некоторые упрощенные фрагменты кода, минус любая обработка ошибок, которые иллюстрируют решение. Перед тем, как приступить к работе с устройством, приложение должно знать, когда устройство подключается (и отделяется). Обратите внимание, что это просто для иллюстрации - некоторые из переменных являются уровнями класса, некоторые являются глобальными и т.д. Вот код инициализации, который устанавливает события attach/detach вверх:
#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);
где данные представляют собой буфер данных char для записи, последний параметр является необязательным объектом контекста для передачи в обратный вызов, а 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())
, чтобы остановить цикл выполнения и выполнить ручное выполнение в основной поток.
Другой вариант (который я делаю в моем коде) - передать объект контекста (названный "запрос" в следующем примере кода) в методы 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);
}
Это имеет то преимущество, что если у вас есть другие потоки, которые используют цикл цикла, вы не сможете преждевременно выйти, если другой поток остановит цикл выполнения...
Я надеюсь, что это полезно людям. Мне пришлось тянуть из многих неполных источников, чтобы решить эту проблему, и для этого потребовалась значительная работа, чтобы хорошо работать...