WiiリモコンをNode.jsから操ってみよう

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公開まで〜](https://qiita.com/castaneai/items/f82b2523bcb603e42ea7)

本来であれば、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

```js:inquiry_test.js
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の設定ファイルを作成します。

```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というファイルです。（GitHub参照）

やっていることは、
・Node.jsとC言語の呼び出しの橋渡し
・socket.connectで、L2CAPプロトコルの接続(2つのPSM)
・socket.readでブロッキングモードで受信待ちしていったん関数戻り、受信したらコールバック呼び出し

これで、```build\Release\binding.node```というのが出来上がります。
後はこれを使いやすいように、jsファイルでくるみます。受信呼び出しを繰り返し呼ばないといけないように作っています。

```js:wiiremocon.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」という形式です。

```js:wiiremocon_test.js
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)](https://qiita.com/poruruba/items/0bc3ebd90f221c293d7f)
[WiiリモコンをGamepadにして、HTML5で使う](https://qiita.com/poruruba/items/9e4c1b438ab9283fa391)

以上