我正在尝试与相当特定的USB设备通信,并同时开发Windows和Mac代码。
该设备是具有HID接口(3类)的USB设备,该设备具有两个端点,一个中断输入和一个中断输出。设备的性质是,仅当从主机请求数据时,才在输入端点上从设备发送数据:主机向其发送数据,设备在其输入中断端点上做出响应。将数据获取(写入)到设备要简单得多...
Windows的代码非常简单:我得到了设备的句柄,然后调用ReadFile或WriteFile。显然,许多底层的异步行为已被抽象出来。它似乎工作正常。
但是,在Mac上,它有点粘性。我尝试了很多事情,没有一个完全成功,但是下面两件事似乎最有希望...
1.)尝试通过IOUSBInterfaceInterface访问设备(作为USB),遍历端点以确定输入和输出端点,并(希望)使用ReadPipe和WritePipe进行通信。不幸的是,一旦获得接口,我将无法打开该接口,返回值(kIOReturnExclusiveAccess)指出某些东西已经专门打开了该设备。我尝试使用IOUSBinterfaceInterface183,以便可以调用USBInterfaceOpenSeize,但这会导致相同的返回错误值。
-更新7/30/2010-
显然,Apple IOUSBHIDDriver较早地与设备匹配,这可能是阻止打开IOUSBInterfaceInterface的原因。从某种程度上讲,防止IOUSBHIDDriver匹配的常见方法似乎是编写具有较高探针得分的无代码kext(内核扩展)。这将尽早匹配,从而防止IOUSBHIDDriver打开设备,并且从理论上讲,应该允许我打开接口并直接对端点进行读写。可以,但是我更希望不必在用户计算机上安装其他东西。如果有人知道一个可靠的选择,我将感谢您提供的信息。
2.)作为IOHIDDeviceInterface122(或更高版本)打开设备。为了进行读取,我设置了一个异步端口,事件源和回调方法,以便在数据准备就绪时(从输入中断端点上的设备发送数据时)调用该方法。但是,写数据(设备需要的数据)来初始化响应,我找不到办法。我很困惑setReport通常会写到控制端点,再加上我需要的写法是不期望任何直接响应,不会阻塞。
我在网上四处张望,尝试了很多事情,但没有一个给我成功。有什么建议吗?我不能使用太多的Apple HIDManager代码,因为其中很多是10.5+,我的应用程序也必须在10.4上运行。
最佳答案
我现在有一个USB设备的工作Mac驱动程序,该设备需要通过中断端点进行通信。这是我的做法:
最终,最适合我的方法是选项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
)触摸扩展文件夹,以便下次重新启动时将强制重建缓存(
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缓冲区,final参数是要传递到回调中的可选上下文对象,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)
{
//...
}
请注意,要接收这些回调,运行循环必须正在运行(see this link for more information about the CFRunLoop)。一种实现方法是在调用异步读取或写入方法后调用
CFRunLoopRun()
,此时运行循环运行时主线程将阻塞。处理完回调后,您可以调用CFRunLoopStop(CFRunLoopGetCurrent())
停止运行循环并将执行交还给主线程。另一种选择(我在我的代码中执行)是将上下文对象(在下面的代码示例中称为“ request”)传递到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);
}
这样做的好处是,如果您有其他使用运行循环的线程,则在另一个线程停止运行循环的情况下,您不会过早退出...
我希望这对人们有帮助。为了解决这个问题,我不得不从许多不完整的资源中寻求帮助,这需要大量的工作才能正常运行...
关于c++ - 在Mac上读写USB(HID)中断端点,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/21637506/