WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(2/2)

前回の投稿で、WiiリモコンとヌンチャクとバランスボードをNode.jsで動作させてMQTTに接続するところまでできました。( [WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(1/2)]( https://qiita.com/poruruba/items/0bc3ebd90f221c293d7f) )

今回は、それをブラウザからSubscribeして、ボタンの押下状態を見たり、加速度センサやバランスボードの重量をリアルタイムに表示させたりしてみます。

ソース一式をアップしたGitHubはこちら
　https://github.com/poruruba/WiiRemocon

ブラウザページはこちら
　https://poruruba.github.io/WiiRemocon/html/

# ブラウザからのMQTT接続

以下のライブラリを使います。

Eclipse Paho Javascript Client
　https://www.eclipse.org/paho/clients/js/

こんな感じです。

```js:start.js
    connect_mqtt: function(){
        mqtt_client = new Paho.MQTT.Client(this.mqtt_url, this.client_id );
        mqtt_client.onMessageArrived = this.mqtt_onMessagearrived;
        mqtt_client.onConnectionLost = this.mqtt_onConnectionLost;

        mqtt_client.connect({
            onSuccess: this.mqtt_onConnect
        });
    },

    mqtt_onConnect: function(){
        console.log("MQTT.onConnect");
        this.connected = true;

        wii = new WiiClient(mqtt_client, this.topic_cmd);
        this.init_graph();
        mqtt_client.subscribe(this.topic_evt);
    },

```

#主要なファイル構成

〇js/start.js
　Web画面の制御と、MQTT Subscribeして受信したデータの処理を行います。
　加速度やバランスボードの重量のグラフ表示やヌンチャクのスティックの状態の表示に、Chart.jsを利用しています。また、ボタン押下状態や拡張コントローラ接続状態の表示には、双方向データバインディングが便利なVue.jsを利用しています。また、CSSテンプレートはBootstrap v3.4.1を利用しています。

・Chart.js
　https://www.chartjs.org/

・Vue 2.x
　https://jp.vuejs.org/v2/guide/

・Bootstrap v3.4.1
　https://getbootstrap.com/docs/3.4/

〇js/wiiclient.js
　Wiiリモコンたちに処理を依頼するMQTTのPublish処理をします。また、start.jsで行う受信データの解析に有用な関数群を提供します。


#Javascriptソースコード

メインとなるstart.jsのソースコードです。

```js:start.js
'use strict';

//var vConsole = new VConsole();

const mqtt_url = "【MQTTブローカのURL(WebSocket接続】";
var mqtt_client = null;

const MQTT_CLIENT_ID = "browser";
const MQTT_TOPIC_CMD = 'testwii_cmd';
const MQTT_TOPIC_EVT = 'testwii_evt';

const UPDATE_INTERVAL = 200;

const NUM_OF_DATA = 50;
const NUM_OF_STICK_DATA = 3;

var timer = null;

var wii;

var acc_x, acc_y, acc_z;
var nck_evt;
var blc_evt;
var blc_temperature;

var vue_options = {
    el: "#top",
    data: {
        progress_title: '', // for progress-dialog

        mqtt_url: mqtt_url,
        update_interval: UPDATE_INTERVAL,
        chk_btns: [],
        chk_nck_btns: [],
        battery: 0,
        wii_type: "remocon",
        btaddress: "",
        battery: 0,
        reporting_mode: WIIREMOTE_REPORTID_BTNS,
        flags: [],
        remote_address: "",
        blc_calibration: null,
        blc_battery: false,
        blc_total_weight: 0,
        connected: false,
        leds : [],
        rumble: false,
        topic_cmd: MQTT_TOPIC_CMD,
        topic_evt: MQTT_TOPIC_EVT,
        client_id: MQTT_CLIENT_ID,
    },
    computed: {
    },
    methods: {
        change_rumble_led: function(){
            if(!this.connected)
                return;
            var value = 0;
            if( this.leds[0] ) value |= WIIREMOTE_LED_BIT0;
            if( this.leds[1] ) value |= WIIREMOTE_LED_BIT1;
            if( this.leds[2] ) value |= WIIREMOTE_LED_BIT2;
            if( this.leds[3] ) value |= WIIREMOTE_LED_BIT3;
            if( this.rumble ) value |= WIIREMOTE_RUMBLE_MASK;

            var data = [WIIREMOTE_REPORTID_LED, value ];
            wii.writaValue(data);
        },
        change_reporting_mode: function(){
            var data = [WIIREMOTE_REPORTID_REPORTINGMODE, 0x00, this.reporting_mode ];
            wii.writaValue(data);
        },
        stop_graph: function(){
            if( timer != null ){
                clearTimeout(timer);
                timer = null;
            }
        },
        init_graph: function(){
            if( timer != null ){
                clearTimeout(timer);
                timer = null;
            }

            var labels = [];
            for( var i = 0 ; i < NUM_OF_DATA ; i++ )
                labels.push( -(NUM_OF_DATA - i - 1) * this.update_interval );

            for( var i = 0 ; i < myChart_acc.data.datasets.length ; i++ ){
                myChart_acc.data.datasets[i].data = [];
                for( var j = 0 ; j < NUM_OF_DATA ; j++ )
                    myChart_acc.data.datasets[i].data.push(NaN);
            }
            myChart_acc.data.labels = labels;

            for( var i = 0 ; i < myChart_nck_acc.data.datasets.length ; i++ ){
                myChart_nck_acc.data.datasets[i].data = [];
                for( var j = 0 ; j < NUM_OF_DATA ; j++ )
                    myChart_nck_acc.data.datasets[i].data.push(NaN);
            }
            myChart_nck_acc.data.labels = labels;

            for( var i = 0 ; i < myChart_nck_stk.data.datasets.length ; i++ ){
                myChart_nck_stk.data.datasets[i].data = [];
                for( var j = 0 ; j < NUM_OF_STICK_DATA ; j++ )
                    myChart_nck_stk.data.datasets[i].data.push(NaN);
            }

            timer = setInterval(() =>{
                this.update_graph();
            }, this.update_interval);
        },
        update_graph: function(){
            myChart_acc.data.datasets[0].data.push(acc_x);
            myChart_acc.data.datasets[1].data.push(acc_y);
            myChart_acc.data.datasets[2].data.push(acc_z);
            myChart_acc.data.datasets[0].data.shift();
            myChart_acc.data.datasets[1].data.shift();
            myChart_acc.data.datasets[2].data.shift();
            myChart_acc.update();

            if( nck_evt ){
                myChart_nck_acc.data.datasets[0].data.push(nck_evt.acc_x);
                myChart_nck_acc.data.datasets[1].data.push(nck_evt.acc_y);
                myChart_nck_acc.data.datasets[2].data.push(nck_evt.acc_z);
                myChart_nck_acc.data.datasets[0].data.shift();
                myChart_nck_acc.data.datasets[1].data.shift();
                myChart_nck_acc.data.datasets[2].data.shift();
                myChart_nck_acc.update();

                myChart_nck_stk.data.datasets[0].data.push({ x: nck_evt.stk_x, y: nck_evt.stk_y });
                myChart_nck_stk.data.datasets[0].data.shift();
                myChart_nck_stk.update();
            }

            if( this.blc_calibration && blc_evt ){
                var weight = wii.calcurateBalanceBoard(blc_evt, this.blc_calibration);
                myChart_blc.data.datasets[0].data[0] = weight.topright;
                myChart_blc.data.datasets[1].data[0] = weight.bottomright;
                myChart_blc.data.datasets[2].data[0] = weight.topleft;
                myChart_blc.data.datasets[3].data[0] = weight.bottomleft;
                this.blc_total_weight = weight.total_weight;
                myChart_blc.update();
            }
        },
        mqtt_onMessagearrived: function(message){
            try{
                var topic = message.destinationName;
                if( topic == this.topic_evt){
                    var msg = JSON.parse(message.payloadString);
                    console.log(msg);
                    if( msg.rsp == WIIREMOTE_CMD_EVT){
                        var event = wii.parseReporting(msg.evt);
                        if(event.btns != undefined){
                            for( var i = 0 ; i < 16 ; i++ )
                                this.$set(this.chk_btns, i, (event.btns & (0x0001 << i)) != 0);
                        }
                        if(event.acc_x != undefined) acc_x = event.acc_x;
                        if(event.acc_y != undefined) acc_y = event.acc_y;
                        if(event.acc_z != undefined) acc_z = event.acc_z;
                        if(event.battery != undefined) this.battery = event.battery;
                        if(event.flags != undefined){
                            this.flags[0] = ( event.flags & WIIREMOTE_FLAG_BIT_BATTERY_EMPTY ) ? true : false;     
                            this.flags[1] = ( event.flags & WIIREMOTE_FLAG_BIT_EXTENSION_CONNECTED ) ? true : false;     
                            this.flags[2] = ( event.flags & WIIREMOTE_FLAG_BIT_SPEAKER_ENABLED ) ? true : false;     
                            this.flags[3] = ( event.flags & WIIREMOTE_FLAG_BIT_IR_ENABLED ) ? true : false;     
                        }
                        if(event.report_id == WIIREMOTE_REPORTID_BTNS_EXT8 || event.report_id == WIIREMOTE_REPORTID_BTNS_EXT19 ||
                            event.report_id == WIIREMOTE_REPORTID_BTNS_ACC_EXT16 || event.report_id == WIIREMOTE_REPORTID_BTNS_IR10_EXT9 ||
                            event.report_id == WIIREMOTE_REPORTID_BTNS_ACC_IR10_EXT6 || event.report_id == WIIREMOTE_REPORTID_EXT21)
                        {
                            if( this.wii_type == "remocon"){
                                nck_evt = wii.parseExtension(WIIREMOTE_EXT_TYPE_NUNCHUCK, event.extension);
                                for( var i = 0 ; i < 2 ; i++ )
                                    this.$set(this.chk_nck_btns, i, (nck_evt.btns & (0x0001 << i)) != 0);
                            }else if( this.wii_type = "balance"){
                                blc_evt = wii.parseExtension(WIIREMOTE_EXT_TYPE_BALANCEBOARD, event.extension);
                                if( blc_evt.temperature != undefined) blc_temperature = blc_evt.temperature;
                                if( blc_evt.battery != undefined) this.blc_battery = blc_evt.battery;
                            }
                        }
                    }else
                    if( msg.rsp == WIIREMOTE_CMD_REQ_STATUS){
                        var event = wii.parseReporting(msg.status);
                        console.log(event);
                        if(event.btns != undefined){
                            for( var i = 0 ; i < 16 ; i++ )
                                this.$set(this.chk_btns, i, (event.btns & (0x0001 << i)) != 0);
                        }
                        if(event.acc_x != undefined) acc_x = event.acc_x;
                        if(event.acc_y != undefined) acc_y = event.acc_y;
                        if(event.acc_z != undefined) acc_z = event.acc_z;
                        if(event.battery != undefined) this.battery = event.battery;
                        if(event.flags != undefined){
                            this.flags[0] = ( event.flags & WIIREMOTE_FLAG_BIT_BATTERY_EMPTY ) ? true : false;     
                            this.flags[1] = ( event.flags & WIIREMOTE_FLAG_BIT_EXTENSION_CONNECTED ) ? true : false;     
                            this.flags[2] = ( event.flags & WIIREMOTE_FLAG_BIT_SPEAKER_ENABLED ) ? true : false;     
                            this.flags[3] = ( event.flags & WIIREMOTE_FLAG_BIT_IR_ENABLED ) ? true : false;     
                        }
                    }else
                    if( msg.rsp == WIIREMOTE_CMD_READ_REG_LONG){
                        if( msg.offset == WIIREMOTE_ADDRESS_BALANCE_CALIBRATION){
                            this.blc_calibration = wii.parseBalanceBoardCalibration(msg.value);
                            console.log(this.blc_calibration);
                        }

                    }else
                    if( msg.rsp == WIIREMOTE_CMD_REQ_REMOTE_ADDRESS){
                        if(msg.address)
                            this.remote_address = wii.addr2str(msg.address);
                        else
                            this.remote_address = "";
                    }else if( msg.rsp == WIIREMOTE_CMD_ERR ){
                        console.error("WIIREMOTE_CMD_ERR: " + msg.error);
                    }else{
                        throw 'unknown rsp';
                    }
                }else{
                    console.error('Unknown topic');
                }
            }catch(error){
                console.error(error);
            }
        },
        mqtt_onConnectionLost: function(errorCode, errorMessage){
            console.log("MQTT.onConnectionLost", errorCode, errorMessage);
            this.connected = false;
            this.stop_graph();
        },
        mqtt_onConnect: function(){
            console.log("MQTT.onConnect");
            this.connected = true;

            wii = new WiiClient(mqtt_client, this.topic_cmd);
            this.init_graph();
            mqtt_client.subscribe(this.topic_evt);
        },
        connect_wii: function(){
            if(!this.connected)
                return;
            wii.connect(this.btaddress);
        },
        disconnect_wii: function(){
            wii.disconnect();
        },
        request_status: function(){
            if(!this.connected)
                return;
            wii.requestStatus();
            wii.requestRemoteAddress();
        },
        enable_extension: function(){
            if(!this.connected)
                return;
            wii.enableExtension(true);
        },
        update_calibration: function(){
            if(!this.connected)
                return;
            wii.readRegisterLong(WIIREMOTE_ADDRESS_BALANCE_CALIBRATION, 0x20);
        },
        connect_mqtt: function(){
            mqtt_client = new Paho.MQTT.Client(this.mqtt_url, this.client_id );
            mqtt_client.onMessageArrived = this.mqtt_onMessagearrived;
            mqtt_client.onConnectionLost = this.mqtt_onConnectionLost;

            mqtt_client.connect({
                onSuccess: this.mqtt_onConnect
            });
        },
        disconnect_mqtt: function(){
            if(!this.connected)
                return;
            mqtt_client.disconnect();
        },
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
    }
};
vue_add_methods(vue_options, methods_bootstrap);
vue_add_components(vue_options, components_bootstrap);
var vue = new Vue( vue_options );

var myChart_acc = new Chart( $('#chart_acc')[0].getContext('2d'), {
    type: 'line',
    data: {
        labels: [],
        datasets: [{
            label: "acc_x",
            fill: false,
            data: []
        },{
            label: "acc_y",
            fill: false,
            data: []
        }, {
            label: "acc_z",
            fill: false,
            data: []
        }]
    },
    options: {
        animation: false,
        scales: {
            yAxes: [{
                ticks: {
                    suggestedMax: 255 * 4,
                    suggestedMin: 0,
                }
            }]
        },
        plugins: {
            colorschemes: {
                scheme: 'brewer.Paired12'
            },
        }
    }
});
var myChart_nck_acc = new Chart( $('#chart_nck_acc')[0].getContext('2d'), {
    type: 'line',
    data: {
        labels: [],
        datasets: [{
            label: "acc_x",
            fill: false,
            data: []
        },{
            label: "acc_y",
            fill: false,
            data: []
        }, {
            label: "acc_z",
            fill: false,
            data: []
        }]
    },
    options: {
        animation: false,
        scales: {
            yAxes: [{
                ticks: {
                    suggestedMax: 255 * 4,
                    suggestedMin: 0,
                }
            }]
        },
        plugins: {
            colorschemes: {
                scheme: 'brewer.Paired12'
            },
        }
    }
});
var myChart_nck_stk = new Chart( $('#chart_nck_stk')[0].getContext('2d'), {
    type: 'scatter',
    data: {
        labels: ["nck_stk"],
        datasets: [{
            label: "stick",
            data: []
        }]
    },
    options: {
        scales: {
            xAxes: [{
                ticks: {
                    suggestedMax: 255,
                    suggestedMin: 0,
                }
            }],
            yAxes: [{
                ticks: {
                    suggestedMax: 255,
                    suggestedMin: 0,
                }
            }]
        },
        plugins: {
            colorschemes: {
                scheme: 'brewer.RdYlBu11'
            },
        }
    }
});
var myChart_blc = new Chart( $('#chart_blc')[0].getContext('2d'), {
    type: 'bar',
    data: {
        labels: [],
        datasets: [{
            label: "top_right",
            data: []
        },{
            label: "bottom_right",
            data: []
        },{
            label: "top_left",
            data: []
        }, {
            label: "bottom_left",
            data: []
        }]
    },
    options: {
        animation: false,
        scales: {
            yAxes: [{
                ticks: {
                    suggestedMax: 34,
                    suggestedMin: 0,
                }
            }]
        },
        plugins: {
            colorschemes: {
                scheme: 'brewer.Paired12'
            },
        }
    }
});
```

〇グラフの準備

以下の部分でグラフの準備をしています。あとは、リアルタイムに現在値で反映すればよいだけです。

　var myChart_acc = new Chart( $('#chart_acc')[0].getContext('2d'), {
　var myChart_nck_acc = new Chart( $('#chart_nck_acc')[0].getContext('2d'), {
　var myChart_nck_stk = new Chart( $('#chart_nck_stk')[0].getContext('2d'), {
　var myChart_blc = new Chart( $('#chart_blc')[0].getContext('2d'), {

加速度に線グラフ、ヌンチャクのスティックに散布図、バランスボードの重量に棒グラフを採用しています。
色の選択が面倒なので以下のプラグインを使わせていただきました。

chartjs-plugin-colorschemes
　https://nagix.github.io/chartjs-plugin-colorschemes/ja/

〇グラフ再描画の仕組み

グラフは固定の周期で更新をかけています。
setIntevalを呼び出しておいて、周期的にupdate_graphを呼んでいます。

一方、MQTTで送られてくる受信データであるデータレポーティングは、非同期に受信されてきますので、受信したデータは変数に格納しておきます。

したがって、受信タイミングと描画タイミングは一致していません。もしもっとスムーズに動きを見たければ、再描画間隔を短くしてみてください。デフォルトで200msecにしています。50msecにすると、ヌンチャクのスティックの状態がかなりスムーズに見えるようになります。

〇受信データの解析

MQTTでSubscribeして受信したレポーティングデータは、mqtt_onMessagearrivedが呼び出されて受領できるようにしています。解析のための関数は、wiiclient.jsにまとめておきました。

〇バランスボードの重量の表示

バランスボードの重量の表示には、キャリブレーションデータが必要です。
そのため、生データのデータレポーティングが受信されていても、キャリブレーションデータも受信された状態となって初めてグラフに反映するようにしています。

# Wiiへの要求のためのユーティリティ

Wiiへの要求のための各種ユーティリティです。
さきほど述べましたが、Wiiへの要求のみで、応答はstart.jsの方で処理します。

```js:wiiclient.js
'use strict';

const WIIREMOTE_CMD_EVT = 0x00;
const WIIREMOTE_CMD_ERR = 0xff;
const WIIREMOTE_CMD_CONNECT = 0x01;
const WIIREMOTE_CMD_DISCONNECT = 0x02;
const WIIREMOTE_CMD_WRITE = 0x03;
const WIIREMOTE_CMD_ENABLE_SOUND = 0x04;
const WIIREMOTE_CMD_ENABLE_EXTENSION = 0x05;
const WIIREMOTE_CMD_REQ_REMOTE_ADDRESS = 0x06;
const WIIREMOTE_CMD_READ_REG = 0x07;
const WIIREMOTE_CMD_WRITE_REG = 0x08;
const WIIREMOTE_CMD_REQ_STATUS = 0x09;
const WIIREMOTE_CMD_READ_REG_LONG = 0x0a;

const WIIREMOTE_EXT_TYPE_NUNCHUCK = 0x01;
const WIIREMOTE_EXT_TYPE_BALANCEBOARD = 0x02;

const WIIREMOTE_REPORTID_RUMBLE = 0x10;
const WIIREMOTE_REPORTID_LED = 0x11;
const WIIREMOTE_REPORTID_REPORTINGMODE = 0x12;
const WIIREMOTE_REPORTID_IR_ENABLE = 0x13;
const WIIREMOTE_REPORTID_SPEAKER_ENABLE = 0x14;
const WIIREMOTE_REPORTID_STATUS_REQUEST = 0x15;
const WIIREMOTE_REPORTID_WRITE = 0x16;
const WIIREMOTE_REPORTID_READ = 0x17;
const WIIREMOTE_REPORTID_SPEAKER_DATA = 0x18;
const WIIREMOTE_REPORTID_SPEAKER_MUTE = 0x19;
const WIIREMOTE_REPORTID_IR2_ENABLE = 0x1a;
const WIIREMOTE_REPORTID_STATUS = 0x20;
const WIIREMOTE_REPORTID_READ_DATA = 0x21;
const WIIREMOTE_REPORTID_ACK = 0x22;
const WIIREMOTE_REPORTID_BTNS = 0x30;
const WIIREMOTE_REPORTID_BTNS_ACC = 0x31;
const WIIREMOTE_REPORTID_BTNS_EXT8 = 0x32;
const WIIREMOTE_REPORTID_BTNS_ACC_IR12 = 0x33;
const WIIREMOTE_REPORTID_BTNS_EXT19 = 0x34;
const WIIREMOTE_REPORTID_BTNS_ACC_EXT16 = 0x35;
const WIIREMOTE_REPORTID_BTNS_IR10_EXT9 = 0x36;
const WIIREMOTE_REPORTID_BTNS_ACC_IR10_EXT6 = 0x37;
const WIIREMOTE_REPORTID_EXT21 = 0x3d;

const WIIREMOTE_FLAG_BIT_BATTERY_EMPTY = 0x01;
const WIIREMOTE_FLAG_BIT_EXTENSION_CONNECTED = 0x02;
const WIIREMOTE_FLAG_BIT_SPEAKER_ENABLED = 0x04;
const WIIREMOTE_FLAG_BIT_IR_ENABLED = 0x08;

const WIIREMOTE_RUMBLE_MASK = 0x01;
const WIIREMOTE_LED_MASK = 0xf0;
const WIIREMOTE_LED_BIT0 = 0x80;
const WIIREMOTE_LED_BIT1 = 0x40;
const WIIREMOTE_LED_BIT2 = 0x20;
const WIIREMOTE_LED_BIT3 = 0x10;

const WIIREMOTE_ADDRESS_BALANCE_CALIBRATION = 0xa40020;

class WiiClient {
  constructor(mqtt_client, topic_cmd) {
    this.mqtt_client = mqtt_client;
    this.topic_cmd = topic_cmd;
  }
  
  connect(address, retry = 2){
    var data = {
      cmd: WIIREMOTE_CMD_CONNECT,
      address: this.addr2bin(address),
      retry: retry
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  disconnect(){
    var data = {
      cmd: WIIREMOTE_CMD_DISCONNECT,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  writaValue(value){
    var data = {
      cmd: WIIREMOTE_CMD_WRITE,
      value: value
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  enableSound(enable){
    var data = {
      cmd: WIIREMOTE_CMD_ENABLE_SOUND,
      enable: enable,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  enableExtension(enable){
    var data = {
      cmd: WIIREMOTE_CMD_ENABLE_EXTENSION,
      enable: enable,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  requestRemoteAddress(){
    var data = {
      cmd: WIIREMOTE_CMD_REQ_REMOTE_ADDRESS,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  readRegister(offset, len){
    var data = {
      cmd: WIIREMOTE_CMD_READ_REG,
      offset: offset,
      len: len,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  writeRegister(offset, data){
    var data = {
      cmd: WIIREMOTE_CMD_WRITE_REG,
      offset: offset,
      data: data,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  requestStatus(){
    var data = {
      cmd: WIIREMOTE_CMD_REQ_STATUS,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  readRegisterLong(offset, len){
    var data = {
      cmd: WIIREMOTE_CMD_READ_REG_LONG,
      offset: offset,
      len: len,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  calcurateBalanceBoard(data, base){
    var result = {
      topright: this.calc_balance(data.topright, base.topright ),
      bottomright: this.calc_balance(data.bottomright, base.bottomright ),
      topleft: this.calc_balance(data.topleft, base.topleft ),
      bottomleft: this.calc_balance(data.bottomleft, base.bottomleft ),
    };
    result.total_weight = (result.topright + result.bottomright + result.topleft + result.bottomleft);

    return result;
  }

  calc_balance(value, base){
    if( value <= base[0] ){
      return 0.0;
    }else if( value > base[0] && value <= base[1] ){
      return ((value - base[0]) / (base[1] - base[0])) * 17.0;
    }else if( value > base[0] && value <= base[1] ){
      return (((value - base[1]) / (base[2] - base[1])) * (34.0 - 17.0)) + 17.0;
    }else{
      return 34.0;
    }
  }

  parseBalanceBoardCalibration(data){
    var result = {
      topright: [(data[0x04] << 8) | data[0x05], (data[0x0c] << 8) | data[0x0d], (data[0x14] << 8) | data[0x15]],
      bottomright: [(data[0x06] << 8) | data[0x07], (data[0x0e] << 8) | data[0x0f], (data[0x16] << 8) | data[0x17]],
      topleft: [(data[0x08] << 8) | data[0x09], (data[0x10] << 8) | data[0x11], (data[0x18] << 8) | data[0x19]],
      bottomleft: [(data[0x0a] << 8) | data[0x0b], (data[0x12] << 8) | data[0x13], (data[0x1a] << 8) | data[0x1b]],
    };

    return result;
  }

  parseExtension(type, data) {
    if (type == WIIREMOTE_EXT_TYPE_NUNCHUCK) {
      var value = {
        stk_x: data[0],
        stk_y: data[1],
        acc_x: (data[2] << 2) | ((data[5] >> 2) & 0x03),
        acc_y: (data[3] << 2) | ((data[5] >> 4) & 0x03),
        acc_z: (data[4] << 2) | ((data[5] >> 6) & 0x03),
        btns: (~data[5] & 0x03),
      }
      return value;
    } else
    if (type == WIIREMOTE_EXT_TYPE_BALANCEBOARD) {
      var value = {
        topright: (data[0] << 8) | data[1],
        bottomright: (data[2] << 8) | data[3],
        topleft: (data[4] << 8) | data[5],
        bottomleft: (data[6] << 8) | data[7],
      };
      if (data.length >= 11) {
        value.temperature = data[8];
        value.battery = data[10];
      }
      return value;
    }
  }

  parseReporting(data) {
    if (data[0] == WIIREMOTE_REPORTID_STATUS) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        leds: data[3] & 0xf0,
        flags: data[3] & 0x0f,
        battery: data[6]
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_READ_DATA) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        size: (data[3] >> 4) & 0x0f + 1,
        error: data[3] & 0x0f,
        address: (data[4] << 8) | data[5],
        data: data.slice(6)
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_ACK) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        report: data[3],
        error: data[4]
      };
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS) {
      var report = {
        report_id: data[0],
        btns: ((data[1] << 8) | data[2]),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03),
        acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01),
        acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_EXT8) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        extension: data.slice(3)
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC_IR12) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03),
        acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01),
        acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01),
        ir: data.slice(6),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_EXT19) {
      var report = {
        report_id: data[0],
        btns: ((data[1] << 8) | data[2]),
        extension: data.slice(3),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC_EXT16) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03),
        acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01),
        acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01),
        extension: data.slice(6),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_IR10_EXT9) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        ir: data.slice(3, 3 + 10),
        extension: data.slice(3 + 10),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC_IR10_EXT6) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03),
        acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01),
        acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01),
        ir: data.slice(6, 6 + 10),
        extension: data.slice(6 + 10),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_EXT21) {
      var report = {
        report_id: data[0],
        extension: data
      };
      return report;
    } else
    if (data[0] == 0x3e || data[0] == 0x0f) {
      throw "not supported";
    } else {
      throw "unknown";
    }
  }

  addr2bin(address){
    return bytes_swap(hexs2bytes(address, ':'));
  }
  
  addr2str(address){
    return bytes2hexs(bytes_swap(address), ':');
  }  
}

function hexs2bytes(hexs, sep) {
  hexs = hexs.trim(hexs);
  if( sep == '' ){
      hexs = hexs.replace(/ /g, "");
      var array = [];
      for( var i = 0 ; i < hexs.length / 2 ; i++)
          array[i] = parseInt(hexs.substr(i * 2, 2), 16);
      return array;
  }else{
      return hexs.split(sep).map(function(h) { return parseInt(h, 16) });
  }
}

function bytes2hexs(bytes, sep) {
  var hexs = '';
  for( var i = 0 ; i < bytes.length ; i++ ){
      if( i != 0 )
          hexs += sep;
      var s = bytes[i].toString(16);
      hexs += ((bytes[i]) < 0x10) ? '0'+s : s;
  }
  return hexs;
}

function bytes_swap(bytes){
  for( var i = 0 ; i < bytes.length / 2 ; i++ ){
    var t = bytes[i];
    bytes[i] = bytes[bytes.length - 1 - i];
    bytes[bytes.length - 1 - i] = t;
  }

  return bytes;
}
```


#使い方

まずは、BluetoothでWiiリモコンたちと接続するサーバを立ち上げます。
前回の投稿で示しました。

```
$ node index.js server testwii_cmd testwii_evt
MQTT_CLIENT_ID: server
MQTT_TOPIC_CMD: testwii_cmd
MQTT_TOPIC_EVT: testwii_evt
mqtt.connected.
mqtt.subscribed.
```

これで、testwii_cmdとtestwii_evtというトピック名でMQTTブローカに接続できました。

それでは、まずブラウザから表示させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/bfa2b631-8fd0-aee4-993a-17ee829ad08c.png)

この状態では、まだWiiにもMQTTにも接続されていない状態です。

mqtt_brokerのところに、MQTTブローカのURLを指定してください。Websocket接続のポート番号を指定する必要があります。
topic_cmdとtopic_evtには、サーバが接続したトピック名を指定します。さきのどは、testwii_cmdとtestwii_evtを指定しました。

それでは、MQTT Connectボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/6a3d8785-2ec1-d365-e7f2-1ed5a17d67cd.png)

これで接続はできました。
ですが、まだWiiリモコンは接続していません。
Bluetooth Addressに接続したいWiiリモコンのBluetooth Addressを入力します。
そして、Wiiリモコンの①と②のボタンを両方押して、Discoveryモードにして、Wii Connectボタンを押下します。

そうすると、サーバ側の方で、以下のように表示されて、Wiiリモコンとの接続が完了します。ブラウザからはちょっとわかりにくいので、改善の余地ありですね。。。

```
on.message topic: testwii_cmd message: {"cmd":1,"address":[XX,XX,XX,XX,XX,XX],"retry":2}
Uint8Array [ XX, XX, XX, XX, XX, XX ]
connect called
s_11 connect result=0
s_13 connect result=0
startContinuousRead called
```

この状態で、Wiiリモコンの各ボタンを押してみてください。ブラウザに押したボタンの色が濃くなるのがわかると思います。また、rumbleのチェックボックスをOnにするとWiiリモコンが振動し、LEDの1～4のチェックボックスをOnにするとLEDが点灯するのがわかるかと思います。

この状態は、レポーティングモードがBTNSですので、ボタンの状態しかPublishされてきません。そこで、report_idのところをBTNS_ACCに変更してsetボタンを押してみてください。すると、上側のグラフ(acc_xやacc_y、acc_z)にグラフが表示されます。これが、Wiiリモコンの加速度の状態です。Wiiリモコンを動かしてみるとわかります。

次に、ヌンチャクを動かしてみましょう。
まず、ヌンチャクをWiiリモコンに接続します。そうすると、extension_connectedがtrueに切り替わります。
ヌンチャクの情報の取得には、EXTが付いたreport_idを選択します。BTNS_ACC_EXT16を選択してsetボタンを押下します。グラフが表示されだしましたが、まだ値に動きがありません。それは拡張コントローラを有効にしていないためです。そこで、Enable Extensionボタンを押下すると、動き出します。
ヌンチャクを動かしたり、ヌンチャクのスティックを動かしたり、C/Zボタンを押したりしてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/15dc3841-c6a3-2973-142e-850dd20aa11d.png)

次に、バランスボードを接続してみます。
さきほどのWiiリモコンを接続するサーバは、すでにWiiリモコンを接続済みですので、もう一つサーバを立ち上げます。MQTTの名前が被らないようにします。testwii_cmd2、testwii_evt2にしてみました。

```
$ node index.js server2 testwii_cmd2 testwii_evt2
MQTT_CLIENT_ID: server2
MQTT_TOPIC_CMD: testwii_cmd2
MQTT_TOPIC_EVT: testwii_evt2
mqtt.connected.
mqtt.subscribed.
```

では、ブラウザ側も別のブラウザまたは別タブを立ち上げて表示させます。
今度は、topic_cmdとtopic_evtに先ほど指定したtestwii_cmd2とtestwii_evt2を指定し、client_idに他とかぶらない値を指定して、MQTT Connectボタンを押下します。
そして、Bluetooth Addressには、バランスボードのBluetooth Addressを指定し、wii_typeにはバランスボードを選択しておきます。

それでは、バランスボードの裏面の電池蓋を開けて、Syncボタンを押してから、Wii Connectボタンを押下しましょう。
接続が完了すると、バランスボードのPowerボタンを押すとボタンの色が変わりますし、LED 4のチェックボックスをOnにすると、PowerボタンのLEDが点灯したりします。

次に、report_idをBTNS_EXT19を選択してsetボタンを押下します。
ですが、まだ重量のグラフは表示されません。それはキャリブレーションデータを受信していないためです。Update Calibrationボタンを押下してみてください。
さあ、バランスボードに乗ってみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5108ff57-5f70-2fab-2d6e-6d68cbaab023.png)

以上