Wiiリモコンは、中古で入手しやすく、機能も豊富なので、入力デバイスとしてはうってつけです。
接続もBluetoothなので、プロトコルさえわかれば、操れそうです。
ことの発端は、かの神モジュール「noble」を勉強のためソースコードを見ていたのですが、自分でも操ってみようと思い、そこで思いついたのがWiiリモコンでした。
毎度の通り、ソースコードもろもろを、GitHubに上げておきます。
poruruba/WiiRemocon
https://github.com/poruruba/WiiRemocon
※たぶん、Linuxでしか動かないと思います。
Wiiリモコンのプロトコル
ここにすべて書いてあります!
WiiBrew:Wiimote
http://wiibrew.org/wiki/Wiimote
Wiiリモコンとは、BluetoothのL2CAPプロトコルで通信します。
HIDとして見えるので、PSM=0x0011(HID Control)とPSM=0x0013 (HID Interrupt)の2つのコネクションを張る必要があります。
L2CAPプロトコルの接続には、Linuxのsocket関数を使うのですが、node-gypでネイティブ実装しました。
こちらを参考にさせていただきました。あと、node-bluetooth-hci-socketも。
Node.jsのネイティブ拡張を作ってみよう 〜NAN, 非同期処理, npm公開まで〜
本来であれば、HCI Commandの「Create Connection Command」や、L2CAPの「Connection Request」や「Configuration Request」の処理をする必要がありますが、socket関数が内部で処理してくれます。
接続した後は、HID Interruptの通信路に、Wiiリモコンの通信データ(HIDのレポート)が永遠と飛んできます。
Wiiリモコンの発見
Wiiリモコンは、①と②のマークのボタンを同時に押すとLEDが点滅して、Discoveryモードになって発見できる状態になります。
発見には、BluetoothのHCI Command Packetを使います。そのレイヤの操作に以下のモジュールを使っています。nobleの中でnpmモジュール化していただいているものです。
noble/node-bluetooth-hci-socket
https://github.com/noble/node-bluetooth-hci-socket
Bluetoothをご存じの方であれば、以下のコマンドとイベントを使います。
・Inquiry Command(OCF=0x0001)
・Inquiry Complete Event(Event Code=0x01)
・Inquiry Result Event(Event Code=0x02)
Wiiリモコンの操作ライブラリ
うーん、今回もGitHubを見てもらった方がよいかなあ。(最近手抜きが多い。。。)
inquiry.js というファイルです。
以下の2つのnpm モジュールを利用しています。
・bluetooth-hci-socket
・debug
BLUETOOTH SPECIFICATION
7 HCI COMMANDS AND EVENTS
7.1 LINK CONTROL COMMANDS
と
7.7 EVENTS
の辺りを見れば、大体わかります。
パケットフォーマットは、
Figure 5.1 HCI Command Packet
にあります。ただし、socket関数を使う場合は、先頭1バイトに0x01を入れる必要があるようです。
使い方は以下の感じ。
ただし、これの実行には、ルート権限が必要です。ルート権限不要としたい場合は以下を参照してください。
https://github.com/noble/noble#running-without-rootsudo
inquiry_test.js
constInquery=require('./inquiry');constinquiry=newInquery();asyncfunctioninquiry_device(){returnnewPromise((resolve,reject)=>{varlocal_address=null;varremote_address=null;inquiry.on("initialized",(address)=>{console.log("local: "+address);local_address=address;inquiry.inquiry(10,1);});inquiry.on("inquiryResult",(address)=>{console.log("remote: "+address);remote_address=address;});inquiry.on("inquiryComplete",(status)=>{console.log("status: "+status);inquiry.stop();resolve({local:local_address,remote:remote_address});});inquiry.init();})}inquiry_device().then(result=>{console.log(result);}).catch(error=>{console.error(error);});
Wiiリモコン操作用のライブラリ
ネイティブライブラリの力を借ります。まず、準備。
$ npm install -g node-gyp
$ npm install nan
node-gypの設定ファイルを作成します。
building.gyp
{"targets":[{"target_name":"binding","sources":["src/BtL2capHid.cpp"],'link_settings':{'libraries':['-lbluetooth',],},"include_dirs":["<!(node -e \"require('nan')\")"]}]}
以下のように準備して、コンパイル
$ node-gyp configure
$ node-gyp build
またしても、ソース割愛。BtL2capHid.cppというファイルです。
やっていることは、
・Node.jsとC言語の呼び出しの橋渡し
・socket.connectで、L2CAPプロトコルの接続(2つのPSM)
・socket.readでブロッキングモードで受信待ちしていったん関数戻り、受信したらコールバック呼び出し
これで、build\Release\binding.node
というのが出来上がります。
後はこれを使いやすいように、jsファイルでくるみます。受信呼び出しを繰り返し呼ばないといけないように作っています。
wiiremocon.js
'use strict';varEventEmitter=require('events').EventEmitter;varbinding=require('./build/Release/binding.node');constWIIREMOTE_RUMBLE_MASK=0x01;constWIIREMOTE_LED_MASK=0xf0;classWiiRemoconextendsEventEmitter{constructor(){super();this.WIIREMOTE_LED_BIT0=0x80;this.WIIREMOTE_LED_BIT1=0x40;this.WIIREMOTE_LED_BIT2=0x20;this.WIIREMOTE_LED_BIT3=0x10;this.cur_rumble_led=0x00;this.l2cap=newbinding.BtL2capHid();}addr2bin(address){returnBuffer.from(address.split(':').reverse().join(''),'hex');}addr2str(address){returnaddress.toString('hex').match(/.{1,2}/g).reverse().join(':');}connect(addr,retry=2){console.log('connect');returnnewPromise((resolve,reject)=>{this.l2cap.connect(addr,retry,(err,result)=>{if(err)returnreject(err);this.startRead();resolve(result);});})}asyncreadAsync(){returnnewPromise((resolve,reject)=>{this.l2cap.read((err,data)=>{if(err)returnreject(err);resolve(data);});});}startRead(){console.log('startRead');returnnewPromise(async(resolve,reject)=>{do{try{varresult=awaitthis.readAsync();this.emit("data",result);}catch(error){console.error(error);returnresolve(error);}}while(true);});}setReport(id,value){console.log('setReport called');varparam=Buffer.alloc(3);param.writeUInt8(0xa2,0);param.writeUInt8(id,1);param.writeUInt8(value,2);console.log('setReport:'+param.toString('hex'));returnthis.l2cap.write(0,param);}setLed(led_mask,led_val){this.cur_rumble_led=(this.cur_rumble_led&~(led_mask&WIIREMOTE_LED_MASK))|(led_val&WIIREMOTE_LED_MASK);returnthis.setReport(0x11,this.cur_rumble_led);}setRumble(rumble){this.cur_rumble_led=(this.cur_rumble_led&~WIIREMOTE_RUMBLE_MASK)|(rumble&WIIREMOTE_RUMBLE_MASK);returnthis.setReport(0x11,cur_rumble_led);}setDataReportingMode(mode){varparam=Buffer.alloc(4);param.writeUInt8(0xa2,0);param.writeUInt8(0x12,1);param.writeUInt8(0x00,2);param.writeUInt8(mode,3);console.log('setDataReportingMode:'+param.toString('hex'));returnthis.l2cap.write(0,param);}}module.exports=WiiRemocon;
あとは、こんな感じで使います。
node起動時に、引数にWiiリモコンのBluetoothのMacアドレスを指定します。「XX:XX:XX:XX:XX:XX」という形式です。
wiiremocon_test.js
constWiiRemocon=require('./wiiremocon');varwii=newWiiRemocon();asyncfunctionwiiremote_monitoring(remote_address){wii=newWiiRemocon();wii.on("data",data=>{console.log(data);});awaitwii.connect(wii.addr2bin(remote_address));wii.setLed(wii.WIIREMOTE_LED_BIT0|wii.WIIREMOTE_LED_BIT1|wii.WIIREMOTE_LED_BIT2|wii.WIIREMOTE_LED_BIT3,0);}wiiremote_monitoring(process.argv[2]).catch(error=>{console.error(error);});
wii.on(“data”, function(data)) のところに、Wiiからボタン等の状態が送られてきます。
ボタンを押したときにイベントデータが送られてきますが、setDataReportingModeで例えば0x31を指定してモードを変更すれば、加速度などがひっきりなしに送られてきます。
終わりに
あとは、Node.js上でいろいろいじれそうです。
WiiヌンチャクやWii Fitボードなども試してみようと思います。
以上