割り込みエンドポイントを介した通信を必要とする USB デバイスへの Mac ドライバーが動作するようになりました。これが私がやった方法です:
最終的に私にとってうまくいった方法は、オプション 1 (上記) でした。前述のように、COM スタイルの 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 を拡張機能 (
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 がデバイスをキャプチャしないようにする必要があります。さて、それをどうするか?書き込みと読み取りの方法は?
以下は、解決策を示す、エラー処理を除いたコードの簡略化されたスニペットです。デバイスで何かを行う前に、アプリケーションは、デバイスが接続 (および切断) されたときを知る必要があります。これは説明のためだけのものであることに注意してください。変数の一部はクラス レベルであり、一部はグローバルです。アタッチ/デタッチ イベントを設定する初期化コードは次のとおりです。
#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 の 2 つのメソッドがコールバックとして使用されています (どちらも静的メソッドで宣言されています)。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 の詳細については、このリンクを参照してください)。これを実現する 1 つの方法はCFRunLoopRun()
、実行ループの実行中にメイン スレッドがブロックされる時点で、非同期の読み取りまたは書き込みメソッドを呼び出した後に呼び出すことです。コールバックを処理した後、呼び出しCFRunLoopStop(CFRunLoopGetCurrent())
て実行ループを停止し、実行をメイン スレッドに戻すことができます。
もう 1 つの方法 (私のコードではこれを行っています) は、コンテキスト オブジェクト (次のコード サンプルでは「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);
}
これには、実行ループを使用する他のスレッドがある場合、別のスレッドが実行ループを停止しても途中で終了しないという利点があります...
これが人々に役立つことを願っています。この問題を解決するには、多くの不完全なソースから取得する必要があり、これをうまく実行するにはかなりの作業が必要でした...