Joy-Conの入力をSwiftを使ってMacで受け取る

  • このエントリーをはてなブックマークに追加

はじめに

この記事は、香川大学工学部のプログラミングサークル SLP(学生プログラミング研究所)のアドベントカレンダー 1日目の記事です。サークル生やOBのみなさんが繋いでいきますので、そちらもぜひ目を通していただけると嬉しいです。
SLP Advent Calendar 2018

さて、タイトルにもありますが、Joy-Conの入力をMacで受け取るということで、記事を書いていきます。Unityを使って入力を受け取るのは簡単ですが、今回は、Swiftを使ってドライバっぽいのを作って、もっとちゃんと値を受け取りたいと思います。

ちなみに、コードをいくつかに分けて説明していきますが、最後にまとめたものも載せます。コピペする場合や、場所がわからなくなった場合は、そちらも確認してみてください。

注意事項と言い訳

マークダウン部分の表示が、テーマの関係であまり強調されていないため、かなり見辛いかもです。
ごめんなさい。

昔、Xamarinで、ライブラリを bind して C# で書こうとした名残なんですが、ところどころ、引数に突然説明もなく、0や1が現れますが、本当は、それぞれに使うように定義された定数があります。直してないです。ごめんなさい。

Joy-Con

そもそも、Joy-Conとは、某ゲーム会社の販売しているコントローラですね。

Wikipediaを見た感じ、Bluetoothで通信してるみたいだったので、試しにやってみたらMacでも問題なく接続できました。ただ、Joy-Conは、Bluetooth 3.0で、BLE (Bluetooth Low Energy)は使えないです。ですので、USB-HIDとして接続して値の受け取ってみます 

SwiftでUSB-HID

USB-HIDを扱うためには、IOKit.hitをimportします。IOHIDManagerでマッチングして、Callbackを受け取ればいけそうです。マッチングには、「vendor id」と「product id」を指定した方が無難です。これは、接続したあと、Macの「システム情報」アプリで調べることができます。

「ハードウェア」->「Bluetooth」の、デバイスのところを見ます、右手側のJoy-Conを使う場合、「Joy-Con (R)」とあり、そこの「製造元ID」が「vendor id」で、「製品ID」が「product id」です。僕が見たところ、それぞれ「0x057E」「0x2007」となっていました。

IOHIDManagerでデバイスとマッチング

まずは、managerとデバイスのIDを定義します。今回はJoy-Con (R)を使います。

import IOKit.hid

let manager = IOHIDManagerCreate(kCFAllocatorDefault, 0)
let matching = [kIOHIDVendorIDKey: 0x057E, kIOHIDProductIDKey: 0x2007]

次にマッチングさせて、そのCallback関数を作成します。
IOHIDManagerSetDeviceMatching(manager, matching as CFDictionary?) を使って、先ほど定義したIDのものだけを検索するようにします。そうしたら、
IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) を使って、クライアント側のrun loopにmanagerを登録します。このmanagerが、デバイスを探したり、コネクションを確立したりしてくれます。

次に、マッチングしたときの呼ばれる、IOHIDDeviceCallback の関数を用意します。今回は、matchingCallback とします。Callbackの中では、
IOHIDDeviceOpen(device, 1) でDeviceとのコネクションを確立し、
IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) を用いて、クライアント側のrun loopにデバイスを登録します。

この関数を IOHIDManagerRegisterDeviceMatchingCallback(manager, matchingCallback, nil) で登録します。

IOHIDManagerSetDeviceMatching(manager, matching as CFDictionary?)
IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)

let matchingCallback: IOHIDDeviceCallback = {context, result, sender, device in

    IOHIDDeviceOpen(device, 1)
    IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)
}

IOHIDManagerRegisterDeviceMatchingCallback(manager, matchingCallback, nil)

IOHIDManager 関連の仕上げ

マッチングした時のCallbackと同様に、外れた時の関数も用意します。今回は、特に実装はありませんが、外れた時に通知する場合などは、ここに実装すればいいかなと思います。同様に、Callback関数を登録してあげます。

最後に、CFRunLoopRun() で、run loopを走らせ続けてあげれば、Bluetoothで接続されているJoy-Conと、コネクションを確立するところまでやってくれます。

最後に、 IOHIDManagerUnscheduleFromRunLoop(manager, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) を使って、run loopのスケジュールから外すようにしておくとお行儀がいいと思います。

let removeCallback: IOHIDDeviceCallback = {context, result, sender, device in
    // do something
}
IOHIDManagerRegisterDeviceRemovalCallback(manager, removeCallback, nil)

CFRunLoopRun()

IOHIDManagerUnscheduleFromRunLoop(manager, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue)

 Joy-Conからの入力を出力する

さて、ここまでで、Bluetoothで接続したJoy-Conとのコネクションを確立するところまでは完了しました。あとは、デバイスからの入力を受け取る部分の実装です。ここから先は、マッチングのCallback関数の中で実装します。なお、Data型を使うので、Foundation のimportが必要です。 

再びCallback関数を作ります。今度は、Joy-Conから送られてくるデータを受け取るための IOHIDReportCallback の関数です。
送られてくるデータは、report の変数に格納されます。これは、 UnsafeMutablePointer<UInt8> です。今回は、4byte分あれば、大丈夫なので、 Int32 に直してあげて出力してみます。(4byteで大丈夫な理由は、このあと出てきます。)
出力が、10進数だと見辛かったので、16進数で出力します。

 最後に、今作ったCallback関数の登録です。この時、デバイスから送られてくるデータを保存するバッファを事前に割り当てておき、そのポインタを渡す必要があります。適当な値で試してみたところ、4byte分あれば欲しいデータがありそうでした。

そのため、 let report = UnsafeMutablePointer.allocate(capacity: 4) で、バッファを確保します。
IOHIDDeviceRegisterInputReportCallback(device, report, 4, reportCallback, nil) で、バッファのポインタと、同様に長さ指定し、Callback関数を登録します。

let reportCallback : IOHIDReportCallback = { context, result, sender, type, reportId, report, reportLength in
        
    let data = Data(bytes: report, count: reportLength)
    let code = Int32(bigEndian: data.withUnsafeBytes { $0.pointee })
    print(String(format:"%x", code))
}

let report = UnsafeMutablePointer.allocate(capacity: 4)
IOHIDDeviceRegisterInputReportCallback(device, report, 4, reportCallback, nil)

実行してみる

実行して結果を見てみます。ボタンを押すときと、放す時に、それぞれデータが送られてきているみたいです。また、それぞれのボタンのデータは足し算されていて、何かしらの押すイベントと放すイベントが起こった時に、現在の状態が送るようになっているようです。

ちなみに、他のボタンも問題なく動かせます。一番右がスティックの状態を表していて、ニュートラルが8、左を0として、時計回りに8等分で 0〜7に変化します。

残念ながら、スティックを軽く倒したときや、ジャイロの値を取ることはできませんでした。

3f010008    # <- Aを押した
3f000008    # <- Aを放した
3f040008    # <- Bを押した
3f000008    # <- Bを放した
3f080008    # <- Yを押した
3f000008    # <- Yを放した
3f020008    # <- Xを押した
3f000008    # <- Xを放した
3f010008    # <- Aを押した
3f050008    # <- Aを押したままBも押した
3f040008    # <- Bを押したままAを放した
3f000008    # <- Bを放した

まとめ

Swift書いたり、組み込みっぽいことをするのは、これがほとんど初めてだったので、色々と苦戦しましたが、なんとか入力が取れました。これを使って何かしら作れたらいいなと思っています。
なお、改良途中のものが、GitHubにも置いてあります。
https://github.com/hukurou-s/Joy-Con-Input

長々とお付き合いいただきありがとうござました。

以下、今回のソースコードをまとめたものです。

import Foundation
import IOKit.hid

let manager = IOHIDManagerCreate(kCFAllocatorDefault, 0)
let matching = [kIOHIDVendorIDKey: 0x057E, kIOHIDProductIDKey: 0x2007]

IOHIDManagerSetDeviceMatching(manager, matching as CFDictionary?)
IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)

let matchingCallback: IOHIDDeviceCallback = {context, result, sender, device in

    IOHIDDeviceOpen(device, 1)
    IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)

    let reportCallback : IOHIDReportCallback = { context, result, sender, type, reportId, report, reportLength in
        let data = Data(bytes: report, count: reportLength)
        let code = Int32(bigEndian: data.withUnsafeBytes { $0.pointee })
        print(String(format:"%x", code))
    }

    let report = UnsafeMutablePointer<UInt8>.allocate(capacity: 4)
    IOHIDDeviceRegisterInputReportCallback(device, report, 4, reportCallback, nil)
}

IOHIDManagerRegisterDeviceMatchingCallback(manager, matchingCallback, nil)

let removeCallback: IOHIDDeviceCallback = {context, result, sender, device in
    // do something
}
IOHIDManagerRegisterDeviceRemovalCallback(manager, removeCallback, nil)

CFRunLoopRun()

IOHIDManagerUnscheduleFromRunLoop(manager, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue)
  • このエントリーをはてなブックマークに追加