2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-04

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

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ファイルでくるみます。受信呼び出しを繰り返し呼ばないといけないように作っています。

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」という形式です。

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)
WiiリモコンをGamepadにして、HTML5で使う

以上

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?