C++ 读取和写入Mac上的USB(HID)中断端点
我正在尝试与一个相当特殊的USB设备通信,并开发Windows和Mac代码来实现这一点 该设备是具有HID接口(3类)的USB设备,具有两个端点、一个中断输入和一个中断输出。设备的性质是,只有当从主机请求数据时,才在输入端点上从设备发送数据:主机向其发送设备在其输入中断端点上响应的数据。将数据写入设备(写入)要简单得多 Windows的代码相当简单:我获得设备的句柄,然后调用ReadFile或WriteFile。显然,许多底层异步行为都被抽象出来了。它似乎工作得很好 然而,在Mac上,它有点粘。我已经尝试了很多事情,没有一件是完全成功的,但是这里有两件事情看起来是最有希望的 1.)尝试通过iOnBinterfaceInterface访问设备(如USB),遍历端点以确定输入和输出端点,并(希望)使用ReadPipe和WritePie进行通信。不幸的是,一旦我有了接口,我就无法打开它,返回值(kioreturneExclusiveAccess)指出某些东西已经以独占方式打开了设备。我曾尝试使用iOnBinterfaceInterface183,以便调用usBinterfaceOpenCapture,但这会导致相同的返回错误值 ---2010年7月30日更新--C++ 读取和写入Mac上的USB(HID)中断端点,c++,macos,usb,hid,kernel-extension,C++,Macos,Usb,Hid,Kernel Extension,我正在尝试与一个相当特殊的USB设备通信,并开发Windows和Mac代码来实现这一点 该设备是具有HID接口(3类)的USB设备,具有两个端点、一个中断输入和一个中断输出。设备的性质是,只有当从主机请求数据时,才在输入端点上从设备发送数据:主机向其发送设备在其输入中断端点上响应的数据。将数据写入设备(写入)要简单得多 Windows的代码相当简单:我获得设备的句柄,然后调用ReadFile或WriteFile。显然,许多底层异步行为都被抽象出来了。它似乎工作得很好 然而,在Mac上,它有点粘。
显然,Apple IOUSBHIDDriver很早就与设备匹配,这可能是阻止IOUSBHIDDriver打开接口的原因。从一些挖掘中可以看出,防止IOUSBHIDDriver匹配的常用方法是编写一个具有更高探测分数的无代码kext(内核扩展)。这将提前匹配,防止IOUSBHIDDriver打开设备,理论上应该允许我打开接口并直接写入和读取端点。这是可以的,但我更希望不必在用户机器上安装额外的东西。如果有人知道一个可靠的替代方案,我将非常感谢这些信息 2.)将设备作为IOHidDeviceInterface 122(或更高版本)打开。为了进行读取,我设置了一个异步端口、事件源和回调方法,以便在数据准备就绪时调用——当数据从输入中断端点上的设备发送时调用。但是,要写入设备需要的数据来初始化响应,我找不到方法。我被难住了。setReport通常写入控制端点,另外我需要一个不需要任何直接响应、无阻塞的写入
我在网上四处看看,尝试了很多东西,但没有一件能让我成功。有什么建议吗?我不能使用太多Apple HIDManager代码,因为大部分代码都是10.5+,我的应用程序也必须在10.4上运行。我现在有一个USB设备的Mac驱动程序,需要通过中断端点进行通信。我是这样做的: 最终,对我有效的方法是选项1(如上所述)。如前所述,我在打开设备的COM风格iOnBinterface接口时遇到问题。随着时间的推移,很明显这是由于HIDManager捕获了设备。一旦设备被捕获,我就无法从HIDManager获得对它的控制权(即使是usbinterfaceOpenCapture调用或usbdeviceSensize调用也无法工作) 为了控制这个设备,我需要在经理面前抓住它。解决方法是编写一个无代码的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>
在此初始化代码中,有两种方法用作回调:device_detach_callback和device_attach_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);
}
}
设备连接回调是最神奇的地方。在我的代码中,我将其分解为多个方法,但在这里,我将把它作为一个大的整体方法来表示…:
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;
}
}
其中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);
其中,设备读取完成为以下形式:
void DemiUSBDevice::device_read_completion(void* context,
IOReturn result, void* arg0)
{
//...
}
请注意,要接收这些回调,运行循环必须正在运行()。实现这一点的一种方法是在调用异步读或写方法之后调用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);
}
这样做的好处是,如果有其他线程使用运行循环,那么在其他线程停止运行循环时,您不会过早退出
我希望这对人们有帮助。为了解决这个问题,我不得不从许多不完整的资料中寻找线索
void DemiUSBDevice::device_read_completion(void* context,
IOReturn result, void* arg0)
{
//...
}
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);
}
/* 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;
}
//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)];
}
}
}
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);
}