在 Mac 上读取和写入 USB (HID) 中断端点
我正在尝试与一个相当特定的 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 上运行。
I am attempting to communicate with a rather specific USB device and developing both Windows and Mac code to do so.
The device is a USB device with a HID interface (class 3) with two endpoints, an interrupt input and an interrupt output. The nature of the device is such that data is sent out from the device on the input endpoint only when data is requested from the host: the host sends it data which the device responds to on its input interrupt endpoint. Getting data to the device (a write) is much more simple...
The code for Windows is rather straight-forward: I get a handle to the device and then call either ReadFile or WriteFile. Apparently much of the underlying asynchronous behavior is abstracted out. It appears to work fine.
On Mac, however, it is a bit stickier. I have tried a number of things, none which have been fully successful, but here are the two things which seemed most promising...
1.) Attempt to get access to the device (as USB) via IOUSBInterfaceInterface, iterate through the endpoints to determine the input and output endpoints, and (hopefully) use ReadPipe and WritePipe to communicate. Unfortunately I am unable to open the interface once I have it, with the return value (kIOReturnExclusiveAccess) noting that something already has the device open exclusively. I have tried using IOUSBinterfaceInterface183, so that I could call USBInterfaceOpenSeize, but that results in the same return error value.
--- update 7/30/2010 ---
Apparently, the Apple IOUSBHIDDriver matches early to the device and this is what likely is preventing opening the IOUSBInterfaceInterface. From some digging about it seems that the common way to prevent the IOUSBHIDDriver from matching is to write a code-less kext (kernel extension) with a higher probe score. This would match early, preventing the IOUSBHIDDriver from opening the device, and should, in theory, permit me to open the interface and to write and read to endpoints directly. This is OK, but I would much prefer not having to install something additional on the user machine. If anyone knows of a solid alternative I would be thankful for the information.
2.) Open the device as an IOHIDDeviceInterface122 (or later). To read, I set up an async port, event source and callback method to be called when data is ready - when data is sent from the device on the input interrupt endpoint. However, to write the data — that the device needs — to initialize a response I can't find a way. I'm stumped. setReport typically writes to the control endpoint, plus I need a write that does not expect any direct response, no blocking.
I have looked around online and have tried many things, but none of them is giving me success. Any advice? I can not use much of the Apple HIDManager code since much of that is 10.5+ and my application must work on 10.4 as well.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
发布评论
评论(3)
在读了几次这个问题并思考了一下之后,我想到了另一种模拟阻塞读取行为的解决方案,但使用 HID 管理器而不是替换它。
阻塞读取函数可以为设备注册一个输入回调,在当前运行循环上注册设备,然后通过调用 CFRunLoopRun() 进行阻塞。然后,输入回调可以将报告复制到共享缓冲区中并调用 CFRunLoopStop(),这会导致 CFRunLoopRun() 返回,从而解除对 read() 的阻塞。然后,read()可以将报告返回给调用者。
我能想到的第一个问题是设备已经在运行循环上调度的情况。在读取功能中调度然后取消调度设备可能会产生不利影响。但只有当应用程序尝试在同一设备上同时使用同步和异步调用时,这才会成为问题。
我想到的第二件事是调用代码已经有一个正在运行的运行循环的情况(例如 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 等)。我找到了该设备并使用了 POSIX api。
//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 和读/写..
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);
}
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
我现在有一个可正常工作的 USB 设备 Mac 驱动程序,需要通过中断端点进行通信。我是这样做的:
最终对我来说效果好的方法是选项 1(如上所述)。如前所述,我在打开设备的 COM 样式 IOUSBInterfaceInterface 时遇到问题。随着时间的推移,我们逐渐清楚这是由于 HIDManager 捕获设备造成的。一旦捕获设备,我就无法从 HIDManager 夺取设备的控制权(甚至 USBInterfaceOpenSeize 调用或 USBDeviceOpenSeize 调用也不起作用)。
为了控制设备,我需要在 HIDManager 之前抓取它。解决这个问题的方法是编写一个无代码 kext(内核扩展)。 kext 本质上是一个位于系统/库/扩展中的包,其中包含(通常)plist(属性列表)和(偶尔)内核级驱动程序以及其他项目。就我而言,我只需要 plist,它会向内核提供有关其匹配的设备的指令。如果数据给出的探测分数高于 HIDManager,那么我基本上可以捕获该设备并使用用户空间驱动程序与其进行通信。
编写的 kext plist 修改了一些特定于项目的细节,如下所示:
idVendor 和 idProduct 值赋予 kext 特异性并充分增加其探测分数。
为了使用 kext,需要完成以下操作(我的安装程序将为客户端执行此操作):
sudo chown root:wheel DemiUSBDevice.kext
sudo cp DemiUSBDevice.kext /System/Library/Extensions
)sudo kextload - vt /System/Library/Extensions/DemiUSBDevice.kext
)sudo touch /System/Library/Extensions
)此时系统应该使用 kext 来阻止 HIDManager 捕获我的设备。现在,该怎么办呢?如何写入和读取它?
以下是我的代码的一些简化片段,减去任何错误处理,说明了解决方案。在能够对设备执行任何操作之前,应用程序需要知道设备何时连接(和分离)。请注意,这仅用于说明目的 - 有些变量是类级别的,有些是全局变量等。以下是设置附加/分离事件的初始化代码:
在此有两个方法用作回调初始化代码:device_detach_callback和device_attach_callback(均在静态方法中声明)。 device_detach_callback 很简单:
device_attach_callback 是大部分魔法发生的地方。在我的代码中,我将其分解为多个方法,但在这里我将其作为一个大的整体方法呈现...:
此时我们应该有中断端点的数量和设备的开放 IOUSBInterfaceInterface。数据的异步写入可以通过调用类似的方法来完成:
其中 data 是要写入的数据的 char 缓冲区,最后一个参数是要传递到回调中的可选上下文对象,device_write_completion 是具有以下一般形式的静态方法:
从中断端点读取的内容类似:
其中 device_read_completion 具有以下形式:
请注意,要接收这些回调,运行循环必须正在运行 (请参阅此链接以获取有关 CFRunLoop 的更多信息)。实现此目的的一种方法是在调用异步读取或写入方法后调用 CFRunLoopRun(),此时主线程会在运行循环运行时阻塞。处理回调后,您可以调用 CFRunLoopStop(CFRunLoopGetCurrent()) 来停止运行循环并将执行返回到主线程。
另一种替代方法(我在代码中执行的操作)是将上下文对象(在以下代码示例中名为“request”)传递到 WritePipeAsync/ReadPipeAsync 方法中 - 该对象包含一个布尔完成标志(在本示例中名为“is_done”) 。调用读/写方法后,不调用
CFRunLoopRun()
,而是可以执行类似以下内容:这样做的好处是,如果您有其他线程使用运行循环,则不会过早地执行exit 如果另一个线程停止运行循环...
我希望这对人们有帮助。我不得不从许多不完整的来源中获取信息来解决这个问题,这需要大量的工作才能正常运行......
I have now a working Mac driver to a USB device that requires communication through interrupt endpoints. Here is how I did it:
Ultimately the method that worked well for me was option 1 (noted above). As noted, I was having issues opening the COM-style IOUSBInterfaceInterface to the device. It became clear over time that this was due to the HIDManager capturing the device. I was unable to wrest control of the device from the HIDManager once it was captured (not even the USBInterfaceOpenSeize call or the USBDeviceOpenSeize calls would work).
To take control of the device I needed to grab it before the HIDManager. The solution to this was to write a codeless kext (kernel extension). A kext is essentially a bundle that sits in System/Library/Extensions that contains (usually) a plist (property list) and (occasionally) a kernel-level driver, among other items. In my case I wanted only the plist, which would give the instructions to the kernel on what devices it matches. If the data gives a higher probe score than the HIDManager then I could essentially capture the device and use a user-space driver to communicate with it.
The kext plist written, with some project-specific details modified, is as follows:
The idVendor and idProduct values give the kext specificity and increase its probe score sufficiently.
In order to use the kext, the following things need to be done (which my installer will do for clients):
sudo chown root:wheel DemiUSBDevice.kext
)sudo cp DemiUSBDevice.kext /System/Library/Extensions
)sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext
)sudo touch /System/Library/Extensions
)At this point the system should use the kext to keep the HIDManager from capturing my device. Now, what to do with it? How to write to and read from it?
Following are some simplified snippets of my code, minus any error handling, that illustrate the solution. Before being able to do anything with the device, the application needs to know when the device attaches (and detaches). Note that this is merely for purposes of illustration — some of the variables are class-level, some are global, etc. Here is the initialization code that sets the attach/detach events up:
There are two methods that are used as callbacks in this initialization code: device_detach_callback and device_attach_callback (both declared at static methods). device_detach_callback is straightforward:
device_attach_callback is where most of the magic happens. In my code I have this broken into multiple methods, but here I'll present it as a big monolithic method...:
At this point we should have the numbers of the interrupt endpoints and an open IOUSBInterfaceInterface to the device. An asynchronous writing of data can be done by calling something like:
where data is a char buffer of data to write, the final parameter is an optional context object to pass into the callback, and device_write_completion is a static method with the following general form:
reading from the interrupt endpoint is similar:
where device_read_completion is of the following form:
Note that to receive these callbacks the run loop must be running (see this link for more information about the CFRunLoop). One way to achieve this is to call
CFRunLoopRun()
after calling the async read or write methods at which point the main thread blocks while the run loop runs. After handling your callback you can callCFRunLoopStop(CFRunLoopGetCurrent())
to stop the run loop and hand execution back to the main thread.Another alternative (which I do in my code) is to pass a context object (named 'request' in the following code sample) into the WritePipeAsync/ReadPipeAsync methods - this object contains a boolean completion flag (named 'is_done' in this example). After calling the read/write method, instead of calling
CFRunLoopRun()
, something like the following can be executed:This has the benefit that if you have other threads that use the run loop you won't prematurely exit should another thread stop the run loop...
I hope that this is helpful to people. I had to pull from many incomplete sources to solve this problem and this required considerable work to get running well...