Чтение и запись на конечные точки прерывания USB (HID) на Mac
Я пытаюсь общаться с довольно конкретным USB-устройством и разрабатывать для этого как Windows, так и Mac-код.
устройство представляет собой USB-устройство с интерфейсом HID (класс 3) с двумя конечными точками, входом прерывания и выходом прерывания. Природа устройства такова, что данные отправляются с устройства на входную конечную точку только тогда, когда данные запрашиваются от хоста: хост отправляет ему данные, на которые устройство отвечает на своей входной конечной точке прерывания. Получение данных на устройство (запись) намного проще...
код для Windows довольно прямолинейный: я получаю дескриптор устройства, а затем вызываю ReadFile или WriteFile. По-видимому, большая часть лежащего в основе асинхронного поведения абстрагирована. Кажется, он работает нормально.
на Mac, однако, это немного более липким. Я пробовал много вещей, ни одна из которых не была полностью успешной, но вот две вещи, которые казались наиболее многообещающий...
1.) Попытайтесь получить доступ к устройству (как USB) через IOUSBInterfaceInterface, выполните итерацию через конечные точки для определения входных и выходных конечных точек и (надеюсь) используйте ReadPipe и WritePipe для связи. К сожалению, я не могу открыть интерфейс, как только он у меня есть, с возвращаемым значением (kIOReturnExclusiveAccess), отмечая, что что-то уже имеет устройство, открытое исключительно. Я попытался использовать IOUSBinterfaceInterface183, чтобы я мог позвонить USBInterfaceOpenSeize, но это приводит к тому же возвращаемому значению ошибки.
--- обновление 7/30/2010 ---
Видимо, Яблоко IOUSBHIDDriver матчи рано устройства, и это скорее мешает открытием IOUSBInterfaceInterface. Из некоторых раскопок кажется, что общий способ предотвратить сопоставление IOUSBHIDDriver-написать код без kext (расширение ядра) с более высоким баллом зонда. Это будет соответствовать рано, предотвращая IOUSBHIDDriver от открытие устройства и теоретически должно позволить мне открыть интерфейс, а также писать и читать непосредственно конечным точкам. Это нормально, но я бы предпочел не устанавливать что-то дополнительное на пользовательской машине. Если кто-нибудь знает твердую альтернативу, я был бы благодарен за информацию.
2.) Откройте устройство как IOHIDDeviceInterface122 (или более поздней версии). Для чтения я настроил асинхронный порт, источник событий и метод обратного вызова, который будет вызываться, когда данные готовы - когда данные отправлено с устройства на конечной точке входного прерывания. Однако для записи данных, необходимых устройству, чтобы инициализировать ответ, я не могу найти способ. Я в тупике. setReport обычно записывает в конечную точку элемента управления, плюс мне нужна запись, которая не ожидает прямого ответа, никакой блокировки.
Я посмотрел в интернете и пробовал много вещей, но ни одна из них не дает мне успеха. Есть советы? Я не могу использовать большую часть кода HIDManager Apple так много, что на 10,5+ и мое приложение должно работать и на 10.4.
3 ответов
теперь у меня есть рабочий драйвер Mac для USB-устройства, которое требует связи через конечные точки прерывания. Вот как я это сделал:
в конечном счете метод, который хорошо работал для меня был вариант 1 (отмечено выше). Как уже отмечалось, у меня возникли проблемы с открытием COM-стиля IOUSBInterfaceInterface для устройства. Со временем стало ясно, что это связано с захватом устройства HIDManager. Я не смог вырвать контроль над устройством из HIDManager после его захвата (даже вызов USBInterfaceOpenSeize или вызовы USBDeviceOpenSeize не будут работать).
контроль устройств мне нужно, чтобы схватить его до HIDManager. Решением этого было написать бескодовый kext (расширение ядра). Kext-это, по сути, пакет, который находится в системе / библиотеке / расширениях, который содержит (обычно) 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
) - коснитесь папки Extensions, чтобы при следующем перезапуске восстановить кэш (
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);
где data является буфером 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" в этом примере). После вызова метода read / write вместо вызова 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);
}
это имеет то преимущество, что если у вас есть другие потоки, которые используют цикл выполнения, вы не будете преждевременно выходить, если другой поток остановит цикл выполнения...
я надеюсь, что это полезно для людей. Я должен был вытащить из многих неполных источников, чтобы решить эту проблему, и это потребовало значительной работы, чтобы хорошо работать...
прочитав этот вопрос несколько раз и подумав об этом немного, я подумал о другом решении для эмуляции поведения блокировки чтения, но используя HID manager вместо его замены.
блокирующая функция чтения может зарегистрировать входной обратный вызов для устройства, зарегистрировать устройство в текущем цикле выполнения, а затем заблокировать, вызвав CFRunLoopRun(). Входной обратный вызов может затем скопировать отчет в общий буфер и вызвать CFRunLoopStop (), что вызывает CFRunLoopRun() чтобы вернуться, тем самым разблокировав read(). Затем read () может вернуть отчет вызывающему объекту.
первая проблема, о которой я могу думать, - это случай, когда устройство уже запланировано в цикле выполнения. Планирования, а затем unscheduling устройством в функция чтения может оказать отрицательное воздействие. Но это будет проблемой, только если приложение пытается использовать синхронные и асинхронные вызовы на одном устройстве.
второе, что приходит на ум-это случай, когда вызывающий код уже имеет цикл выполнения (например, приложения Cocoa и Qt). Но документация для CFRunLoopStop (), похоже, указывает, что вложенные вызовы CFRunLoopRun () обрабатываются правильно. Так что все должно быть в порядке.
вот немного упрощенный код, чтобы пойти с этим. Я просто реализовала нечто подобное HID библиотека и это, кажется, работает, хотя я еще не протестировали.
/* 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;
}
я столкнулся с этим же kIOReturnExclusiveAccess. Вместо того, чтобы бороться с ним (создание kext и т. д.). Я нашел устройство и использовал api 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)];
}
}
}
Как только devPath установлен, вы можете вызвать open и read / write..
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);
}