22

私はかなり特定のUSBデバイスと通信しようとしており、そのためにWindowsとMacの両方のコードを開発しています。

このデバイスは、割り込み入力と割り込み出力の2つのエンドポイントを持つHIDインターフェイス(クラス3)を備えたUSBデバイスです。デバイスの性質上、データはホストから要求された場合にのみ入力エンドポイントでデバイスから送信されます。ホストは、デバイスが入力割り込みエンドポイントで応答するデータをデバイスに送信します。デバイスへのデータの取得(書き込み)ははるかに簡単です...

Windowsのコードはかなり単純です。デバイスへのハンドルを取得してから、ReadFileまたはWriteFileのいずれかを呼び出します。明らかに、基礎となる非同期動作の多くは抽象化されています。正常に動作しているようです。

ただし、Macでは少し粘着性があります。私はいくつかのことを試しましたが、どれも完全に成功していませんが、ここに最も有望と思われる2つのことがあります...

1.)IOUSBInterfaceInterfaceを介して(USBとして)デバイスへのアクセスを試み、エンドポイントを反復処理して入力エンドポイントと出力エンドポイントを決定し、(うまくいけば)ReadPipeとWritePipeを使用して通信します。残念ながら、一度インターフェイスを開くと、戻り値(kIOReturnExclusiveAccess)で、デバイスがすでに排他的に開いていることが示されているため、インターフェイスを開くことができません。USBInterfaceOpenSeizeを呼び出すことができるように、IOUSBinterfaceInterface183を使用してみましたが、同じ戻りエラー値が返されます。

---アップデート2010年7月30日---
どうやら、Apple IOUSBHIDDriverはデバイスと早期に一致し、これがIOUSBInterfaceInterfaceを開くのを妨げている可能性があります。いくつかの掘り下げから、IOUSBHIDDriverが一致しないようにする一般的な方法は、より高いプローブスコアを持つコードレスkext(カーネル拡張)を作成することであるように思われます。これは早期に一致し、IOUSBHIDDriverがデバイスを開くのを防ぎ、理論的には、インターフェイスを開いてエンドポイントに直接読み書きできるようにする必要があります。これは問題ありませんが、ユーザーのマシンに何かを追加でインストールする必要はありません。誰かが確かな代替案を知っているなら、私はその情報に感謝するでしょう。

2.)デバイスをIOHIDDeviceInterface122(またはそれ以降)として開きます。読み取るために、データの準備ができたとき、つまり入力割り込みエンドポイントのデバイスからデータが送信されたときに呼び出される非同期ポート、イベントソース、およびコールバックメソッドを設定しました。ただし、デバイスが必要とするデータを書き込んで応答を初期化する方法が見つかりません。私は困惑しています。setReportは通常、コントロールエンドポイントに書き込みます。さらに、直接応答やブロッキングを予期しない書き込みが必要です。

私はオンラインで周りを見回して多くのことを試しましたが、どれも私に成功をもたらしていません。何かアドバイス?Apple HIDManagerコードの多くは10.5以降であり、アプリケーションは10.4でも動作する必要があるため、使用できません。

4

3 に答える 3

36

割り込みエンドポイントを介した通信を必要とする 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 を使用するには、次のことを行う必要があります (私のインストーラーがクライアントに対して行います)。

  1. 所有者を root:wheel に変更します ( sudo chown root:wheel DemiUSBDevice.kext)
  2. kext を拡張機能 ( sudo cp DemiUSBDevice.kext /System/Library/Extensions)にコピーします。
  3. kextloadユーティリティを呼び出して、再起動せずにすぐに使用できるように kext をロードします ( sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext) 。
  4. 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);
}

これには、実行ループを使用する他のスレッドがある場合、別のスレッドが実行ループを停止しても途中で終了しないという利点があります...

これが人々に役立つことを願っています。この問題を解決するには、多くの不完全なソースから取得する必要があり、これをうまく実行するにはかなりの作業が必要でした...

于 2010-08-20T00:11:00.493 に答える
2

これと同じkIOReturnExclusiveAccessに遭遇しました。それと戦う代わりに(kextを構築するなど)。デバイスを見つけて、POSIXAPIを使用しました。

//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およびread/writeを呼び出すことができます。

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);
  }
于 2013-01-16T15:22:10.450 に答える
2

この質問を数回読んで少し考えた後、ブロッキング読み取り動作をエミュレートする別の解決策を考えましたが、HID マネージャーを置き換える代わりに使用しました。

ブロック読み取り関数は、デバイスの入力コールバックを登録し、現在の実行ループにデバイスを登録してから、CFRunLoopRun() を呼び出してブロックできます。その後、入力コールバックはレポートを共有バッファにコピーし、CFRunLoopStop() を呼び出すことができます。これにより、CFRunLoopRun() が返され、read() のブロックが解除されます。次に、read() はレポートを呼び出し元に返すことができます。

私が考えることができる最初の問題は、デバイスが既に実行ループでスケジュールされている場合です。読み取り関数でデバイスをスケジュールしてからスケジュール解除すると、悪影響が生じる可能性があります。ただし、これが問題になるのは、アプリケーションが同じデバイスで同期呼び出しと非同期呼び出しの両方を使用しようとしている場合だけです。

2 番目に思いつくのは、呼び出し元のコードで既に実行ループが実行されている場合です (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;
}
于 2011-05-04T05:20:06.217 に答える