MacでのUSB(HID)割り込みエンドポイントの読み取りと書き込み

c++ hid kernel-extension macos usb
MacでのUSB(HID)割り込みエンドポイントの読み取りと書き込み

かなり具体的なUSBデバイスと通信し、そのためのWindowsコードとMacコードの両方を開発しようとしています。

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

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

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

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

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

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

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

  20  10


ベストアンサー

現在、割り込みエンドポイントを介した通信を必要とするUSB​​デバイスへの動作するMacドライバーがあります。 これが私のやり方です。

最終的に、私にとってうまくいった方法はオプション1(上記)です。 前述のように、デバイスへのCOMスタイルのIOUSBInterfaceInterfaceを開くときに問題が発生していました。 これは、HIDManagerがデバイスをキャプチャしたことが原因であることが明らかになりました。 HIDManagerからキャプチャしたデバイスの制御を取得できませんでした(USBInterfaceOpenSeize呼び出しやUSBDeviceOpenSeize呼び出しも機能しませんでした)。

デバイスを制御するには、HIDManagerの前にデバイスを取得する必要がありました。 これに対する解決策は、コードレスkext(カーネル拡張)を書くことでした。 kextは基本的に、System / Library / Extensionsにあるバンドルであり、(通常)plist(プロパティリスト)および(場合によって)カーネルレベルのドライバーなどを含みます。 私の場合は、plistのみが必要でした。plistは、一致するデバイスに関する指示をカーネルに提供します。 データがHIDManagerより高い_probeスコア_を示す場合、基本的にデバイスをキャプチャし、ユーザースペースドライバーを使用して通信できます。

いくつかのプロジェクト固有の詳細を変更した、書かれたkext plistは次のとおりです。

    OSBundleLibraries

        com.apple.iokit.IOUSBFamily
        1.8
        com.apple.kernel.libkern
        6.0

    CFBundleDevelopmentRegion
    English
    CFBundleGetInfoString
    Demi USB Device
    CFBundleIdentifier
    com.demiart.mydevice
    CFBundleInfoDictionaryVersion
    6.0
    CFBundleName
    Demi USB Device
    CFBundlePackageType
    KEXT
    CFBundleSignature
    ????
    CFBundleVersion
    1.0.0
    IOKitPersonalities

        Device Driver

            CFBundleIdentifier
            com.apple.kernel.iokit
            IOClass
            IOService
            IOProviderClass
            IOUSBInterface
            idProduct
            12345
            idVendor
            67890
            bConfigurationValue
            1
            bInterfaceNumber
            0


    OSBundleRequired
    Local-Root

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. 拡張機能フォルダをタッチして、次回の再起動で強制的に
    キャッシュの再構築( sudo touch / System / Library / Extensions

この時点で、システムはkextを使用して、HIDManagerがデバイスをキャプチャしないようにする必要があります。 さて、それをどうするか? 読み書きする方法は?

以下は、コードを簡単に抜粋したもので、エラー処理を除いたもので、ソリューションを示しています。 デバイスで何かを行う前に、アプリケーションはデバイスがいつ接続(および切断)されるかを知る必要があります。 これは単に説明を目的としたものであることに注意してください。変数の一部はクラスレベル、一部はグローバルなどです。 アタッチ/デタッチイベントを設定する初期化コードは次のとおりです。

#include
#include
#include
#include

#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();
}

この初期化コードでコールバックとして使用される2つのメソッドがあります: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バッファーであり、最後のパラメーターはコールバックに渡すオプションのコンテキストオブジェクトであり、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)
{
  //...
}

これらのコールバックを受信するには、実行ループが実行されている必要があります(http://developer.apple.com/mac/library/documentation/CoreFoundation/Reference/CFRunLoopRef/Reference/reference.html[CFRunLoopの詳細については、このリンクを参照してください])。 これを実現する1つの方法は、実行ループの実行中にメインスレッドがブロックする非同期読み取りまたは書き込みメソッドを呼び出した後に `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);
}

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

これが人々に役立つことを願っています。 私はこの問題を解決するために多くの不完全なソースから引き出さなければなりませんでした、そして、これはうまく動くためにかなりの仕事を必要としました…​

30


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

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

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

2番目に思い浮かぶのは、呼び出し元のコードで既に実行ループが実行されている場合です(たとえば、CocoaやQtアプリ)。 ただし、CFRunLoopStop()のドキュメントは、CFRunLoopRun()のネストされた呼び出しが適切に処理されることを示しているようです。 だから、それは大丈夫です。

これに対応する簡単なコードを次に示します。 HID Libraryに似たようなものを実装しましたが、うまく機能しているようですが、広範囲にはテストしていません。

/* 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(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;
}

2


私はこの同じ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および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);
  }

2


タイトルとURLをコピーしました