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
うーん、今回もGitHubを見てもらった方がよいかなあ。(最近手抜きが多い。。。)
inquiry.js というファイルです。
以下の2つのnpm モジュールを利用しています。
・bluetooth-hci-socket
・debug
Bluetoothをご存じの方であれば、以下のコマンドとイベントを使います。
・Inquiry Command(OCF=0x0001)
・Inquiry Complete Event(Event Code=0x01)
・Inquiry Result Event(Event Code=0x02)
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
const Inquery = require('./inquiry');
const inquiry = new Inquery();
async function inquiry_device(){
return new Promise((resolve, reject) =>{
var local_address = null;
var remote_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リモコン操作用のライブラリ
それでは、肝心のWiiリモコン操作です。
ネイティブライブラリの力を借ります。まず、準備。
$ npm install -g node-gyp
$ npm install nan
node-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というファイルです。(GitHub参照)
やっていることは、
・Node.jsとC言語の呼び出しの橋渡し
・socket.connectで、L2CAPプロトコルの接続(2つのPSM)
・socket.readでブロッキングモードで受信待ちしていったん関数戻り、受信したらコールバック呼び出し
これで、build\Release\binding.node
というのが出来上がります。
後はこれを使いやすいように、jsファイルでくるみます。受信呼び出しを繰り返し呼ばないといけないように作っています。
'use strict';
var EventEmitter = require('events').EventEmitter;
var binding = require('./build/Release/binding.node');
const WIIREMOTE_RUMBLE_MASK = 0x01;
const WIIREMOTE_LED_MASK = 0xf0;
class WiiRemocon extends EventEmitter{
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 = new binding.BtL2capHid();
}
addr2bin(address){
return Buffer.from(address.split(':').reverse().join(''), 'hex');
}
addr2str(address){
return address.toString('hex').match(/.{1,2}/g).reverse().join(':');
}
connect(addr, retry = 2){
console.log('connect');
return new Promise((resolve, reject) =>{
this.l2cap.connect(addr, retry, (err, result) =>{
if( err )
return reject(err);
this.startRead();
resolve(result);
});
})
}
async readAsync(){
return new Promise((resolve, reject) =>{
this.l2cap.read((err, data) => {
if (err)
return reject(err);
resolve(data);
});
});
}
startRead(){
console.log('startRead');
return new Promise(async (resolve, reject) =>{
do{
try{
var result = await this.readAsync();
this.emit("data", result);
}catch(error){
console.error(error);
return resolve(error);
}
}while(true);
});
}
setReport( id, value ){
console.log('setReport called');
var param = Buffer.alloc(3);
param.writeUInt8(0xa2, 0);
param.writeUInt8(id, 1);
param.writeUInt8(value, 2);
console.log('setReport:' + param.toString('hex'));
return this.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 );
return this.setReport(0x11, this.cur_rumble_led);
}
setRumble( rumble ){
this.cur_rumble_led = ( this.cur_rumble_led & ~WIIREMOTE_RUMBLE_MASK ) | ( rumble & WIIREMOTE_RUMBLE_MASK );
return this.setReport(0x11, cur_rumble_led);
}
setDataReportingMode(mode){
var param = 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'));
return this.l2cap.write(0, param);
}
}
module.exports = WiiRemocon;
あとは、こんな感じで使います。
node起動時に、引数にWiiリモコンのBluetoothのMacアドレスを指定します。「XX:XX:XX:XX:XX:XX」という形式です。
const WiiRemocon = require('./wiiremocon');
var wii = new WiiRemocon();
async function wiiremote_monitoring(remote_address){
wii = new WiiRemocon();
wii.on("data", data =>{
console.log(data);
});
await wii.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ボードなども試してみようと思います。
(続編はこちら)
WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(1/2)
WiiリモコンをGamepadにして、HTML5で使う
以上