我现在有一个可工作的Mac驱动程序,用于需要通过中断端点进行通信的USB设备。以下是我的做法:
最终对我有效的方法是选项1(如上所述)。正如所指出的,我在打开COM-style 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复制到Extensions (
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;
result = IOMasterPort(bootstrap_port, &master_port);
matching_dict = IOServiceMatching(kIOUSBDeviceClassName);
CFDictionarySetValue(matching_dict, CFSTR(kUSBVendorID),
CFNumberCreate(kCFAllocatorDefault, kCFNumberInt32Type, &vendor_id));
CFDictionarySetValue(matching_dict, CFSTR(kUSBProductID),
CFNumberCreate(kCFAllocatorDefault, kCFNumberInt32Type, &product_id));
notify_port = IONotificationPortCreate(master_port);
run_loop_source = IONotificationPortGetRunLoopSource(notify_port);
CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source,
kCFRunLoopDefaultMode);
matching_dict = (CFMutableDictionaryRef)CFRetain(matching_dict);
result = IOServiceAddMatchingNotification(notify_port,
kIOTerminatedNotification, matching_dict, device_detach_callback,
NULL, &removed_iter);
device_detach_callback(NULL, removed_iter);
result = IOServiceAddMatchingNotification(notify_port,
kIOFirstMatchNotification, matching_dict, device_attach_callback,
NULL, &g_added_iter);
if (result)
{
throw Exception("Unable to add attach notification callback.");
}
device_attach_callback(NULL, added_iter);
service();
}
在这个初始化代码中,有两种作为回调的方法:device_detach_callback和device_attach_callback(均声明为静态方法)。device_detach_callback比较简单:
void DemiUSBDevice::device_detach_callback(void* context, io_iterator_t iterator)
{
IOReturn result;
io_service_t obj;
while ((obj = IOIteratorNext(iterator)))
{
result = IOObjectRelease(obj);
}
}
device_attach_callback 是大部分魔法发生的地方。在我的代码中,我把它分成了多个方法,但这里我将其作为一个大的单一方法呈现...
void DemiUSBDevice::device_attach_callback(void * context,
io_iterator_t iterator)
request.bInterfaceClass = kIOUSBFindInterfaceDontCare;
request.bInterfaceSubClass = kIOUSBFindInterfaceDontCare;
request.bInterfaceProtocol = kIOUSBFindInterfaceDontCare;
request.bAlternateSetting = kIOUSBFindInterfaceDontCare;
result = &intf);
IODestroyPlugInInterface(plugin);
result =
此时,我们应该已经获取了中断端点的数字和一个打开的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);
}
这样做的好处是,如果你有其他使用运行循环的线程,当另一个线程停止运行循环时,你不会过早地退出...
我希望这对人们有所帮助。我不得不从许多不完整的来源中获取信息来解决这个问题,这需要相当大的工作量才能使其正常运行...