micro:bitをSensorTagとしてブラウザから使う：with Chromeブラウザ

前回TIが販売している小型で電池駆動のSensorTagをChromeブラウザから接続してセンサー情報を表示してみました。
　[TI SensorTagをブラウザから使う(1)：with Chromeブラウザ](https://qiita.com/poruruba/items/16f1a43fdf07c8a03b5b)
　[TI SensorTagをブラウザから使う(2)：with LINE Things](https://qiita.com/poruruba/items/e95d9dd2b93ff875170f)

今回は、TI SensorTagの代わりにmicro:bitにして同じようにセンサー情報を表示してみます。
micro:bitのプログラムには、MicrosoftのMakeCodeを使います。

# MakeCodeでmicro:bitをSensorTag化する

以下のページにアクセスします。

Microsoft MakeCode for micro:bit
　https://makecode.microbit.org/#

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/ac7f39fa-6365-75f0-13bf-63217d19d6ec.png)

新しいプロジェクトを選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/e4d96ea3-beda-83ce-8190-7ad7f276eb05.png)

「ずっと」のブロックはいらないので右クリックで削除します。
右上の歯車をクリックして、拡張機能を選択します。
 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/cd43cf02-aa0c-cf25-f967-c35d16ddb2ce.png)

たくさんあるなかで、「Bluetooth」を選択します。
以下のようなダイアログが表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/bb8a682f-3c99-9a5a-cdeb-38654c15f3f4.png)

拡張機能Radioと拡張機能Bluetoothは排他利用であるため、デフォルトの拡張機能Radioを無効化しますよ、ということなので、赤色の方のボタンを押下します。
そうすると、選択可能なブロック一覧にBluetoothが増えました。

そして、最初だけのブロックに、Bluetoothのブロック群から、以下を追加します。

* Bluetooth 加速度計サービス
* Bluetooth 温度計サービス
* Bluetooth 磁力計サービス
* Bluetooth ボタンサービス

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/a10497ff-cc99-0272-9f33-10fa2ebe6a2e.png)

題名にたとえば「SensorTag」と入力して、出来上がりです。

micro:bitに書き込みます。
micro:bitをUSBケーブルでPCと接続します。そうすると、micro:bitがドライブとしてみえます。
そして、右下の「ダウンロード」ボタンを押下すると、保存先のフォルダ・ファイル名を指定するダイアログが表示されるので、micro:bitのドライブを選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/ea4d266c-4e15-5c4b-6120-fae3d15b1f52.png)

これで書き込みが始まります。micro:bitにあるLEDが点滅するのでわかるかと思います。完了すると、自動的にmicro:bitが再起動して、micro:bitのドライブが再表示されます。

# WebBluetooth APIを使ったSensorTag用クラスを作成する

TI SensorTagの投稿とほぼ同じです。
以下のページを参考にしました。

WebBluetooth API
　https://webbluetoothcg.github.io/web-bluetooth/

Bluetooth Developer Studio Level 3 Profile Report
　https://lancaster-university.github.io/microbit-docs/resources/bluetooth/bluetooth_profile.html


```microbit.js
'use stricts';

const UUID_SERVICE_ACCELEROMETER = 'e95d0753-251d-470a-a062-fa1922dfa9a8';
const UUID_CHAR_ACCELEROMETER_DATA = 'e95dca4b-251d-470a-a062-fa1922dfa9a8';
const UUID_CHAR_ACCELEROMETER_PERIOD = 'e95dfb24-251d-470a-a062-fa1922dfa9a8';

const UUID_SERVICE_MAGNETOMETER = 'e95df2d8-251d-470a-a062-fa1922dfa9a8';
const UUID_CHAR_MAGNETOMETER_DATA = 'e95dfb11-251d-470a-a062-fa1922dfa9a8';
const UUID_CHAR_MAGNETOMETER_BEARING = 'e95d9715-251d-470a-a062-fa1922dfa9a8';

const UUID_SERVICE_TEMPERATURE = 'e95d6100-251d-470a-a062-fa1922dfa9a8';
const UUID_CHAR_TEMPERATURE_DATA = 'e95d9250-251d-470a-a062-fa1922dfa9a8';
const UUID_CHAR_TEMPERATURE_PERIOD = 'e95d1b25-251d-470a-a062-fa1922dfa9a8';

const UUID_SERVICE_BUTTON = 'e95d9882-251d-470a-a062-fa1922dfa9a8';
const UUID_CHAR_BUTTON_A_STATE = 'e95dda90-251d-470a-a062-fa1922dfa9a8';
const UUID_CHAR_BUTTON_B_STATE = 'e95dda91-251d-470a-a062-fa1922dfa9a8';

class Microbit{
    constructor(){
		this.bluetoothDevice = null;
        this.characteristics = new Map();
    }

    is_opened(){
        return this.bluetoothDevice ? true : false;
    }

    open(){
		return this.requestDevice("BBC micro:bit");
    }

    close() {
		if (!this.is_opened())
			throw "Bluetooth Device is not opened";

		return Promise.resolve()
		.then(() =>{
			if (this.bluetoothDevice.gatt.connected) {
				console.log('Execute : disconnect');
                this.bluetoothDevice.gatt.disconnect();
                this.bluetoothDevice = null;
				this.characteristics.clear();
			} else {
                this.bluetoothDevice = null;
				this.characteristics.clear();
				throw "Bluetooth Device is already disconnected";
			}
		});
	}

    setup(){
		console.log('Execute : setup');

		return this.bluetoothDevice.gatt.connect()
		.then(server => {
            return this.setup_accelerometer(server);
        })
		.then(server => {
            return this.setup_magnetometer(server);
        })
        .then(server => {
            return this.setup_temperature(server);
        })
		.then(server => {
            return this.setup_button(server);
        })
		.then(server =>{
			console.log('setup done');
			return this.bluetoothDevice.name;
		});
    }

    set_enable(uuid, enable){
        if (!this.is_opened())
            return;

        if( enable )
            return this.startNotify(uuid);
        else
            return this.stopNotify(uuid);
    }

    set_period(uuid, period){
        return this.characteristics.get(uuid).writeValue(Uint8Array.from([period & 0xff, (period >> 8) & 0xff]));
    }

    set_callback(callback){
        this.callback = callback;
    }

    onDataChanged(event){
        console.log('onDataChanged');
        let characteristic = event.target;
        console.log(characteristic.uuid);

        switch(characteristic.uuid){
            case UUID_CHAR_TEMPERATURE_DATA:{
                var temperature = characteristic.value.getUint8(0);
                if( this.callback ){
                    this.callback({
                        type: 'temperature',
                        temperature: temperature
                    });
                }
                break;
            }
            case UUID_CHAR_ACCELEROMETER_DATA:{
                var xA = characteristic.value.getInt16(0, true) / 1000.0;
                var yA = characteristic.value.getInt16(2, true) / 1000.0;
                var zA = characteristic.value.getInt16(4, true) / 1000.0;

                if( this.callback ){
                    this.callback({
                        type: 'accelerometer',
                        xA: xA,
                        yA: yA,
                        zA: zA,
                    });
                }
                break;
            }
            case UUID_CHAR_MAGNETOMETER_DATA:{
                var xM = characteristic.value.getInt16(0, true);
                var yM = characteristic.value.getInt16(2, true);
                var zM = characteristic.value.getInt16(4, true);

                if( this.callback ){
                    this.callback({
                        type: 'magneto',
                        xM: xM,
                        yM: yM,
                        zM: zM,
                    });
                }
                break;
            }
            case UUID_CHAR_MAGNETOMETER_BEARING:{
                var bearing = characteristic.value.getInt16(0, true);

                if( this.callback ){
                    this.callback({
                        type: 'magneto_bearing',
                        bearing: bearing,
                    });
                }
                break;
            }
            case UUID_CHAR_BUTTON_A_STATE:{
                if( this.callback ){
                    this.callback({
                        type: 'button_a',
                        keys : characteristic.value.getUint8(0)
                    });
                }

                break;
            }            
            case UUID_CHAR_BUTTON_B_STATE:{
                if( this.callback ){
                    this.callback({
                        type: 'button_b',
                        keys : characteristic.value.getUint8(0)
                    });
                }

                break;
            }
            default:
                console.log('Unkown data', characteristic);
                break;
        }
    }

    onDisconnect(event){
		console.log('onDisconnect');
	}

    requestDevice(name){
        console.log('Execute : requestDevice(normal)');
        return navigator.bluetooth.requestDevice({
            filters: [{
               namePrefix: name 
            }],
            optionalServices: [
                UUID_SERVICE_ACCELEROMETER,
                UUID_SERVICE_MAGNETOMETER,
                UUID_SERVICE_TEMPERATURE,
                UUID_SERVICE_BUTTON
            ]
        })
        .then(device => {
            console.log("requestDevice OK");
            this.characteristics.clear();
            this.bluetoothDevice = device;
            this.bluetoothDevice.addEventListener('gattserverdisconnected', (event) => {
                this.onDisconnect(event)
            });
            return this.bluetoothDevice.name;
        });
    }

	setCharacteristic(service, characteristicUuid) {
		return service.getCharacteristic(characteristicUuid)
		.then(characteristic => {
			console.log('setCharacteristic : ' + characteristicUuid);
			this.characteristics.set(characteristicUuid, characteristic);
			return service;
		});
	}

    startNotify(uuid) {
		console.log('Execute : startNotifications');
        var characteristic = this.characteristics.get(uuid);
		if( characteristic === undefined )
			throw "Not Connected";

		characteristic.addEventListener('characteristicvaluechanged', (event) =>{
			this.onDataChanged(event);
		});
		return characteristic.startNotifications();
	}

	stopNotify(uuid){
		console.log('Execute : stopNotifications');
		var characteristic = this.characteristics.get(uuid);
		if( characteristic === undefined )
			throw "Not Connected";

		return characteristic.stopNotifications();
    }
    
    setup_accelerometer(server){
        return server.getPrimaryService(UUID_SERVICE_ACCELEROMETER)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_ACCELEROMETER_DATA),
                this.setCharacteristic(service, UUID_CHAR_ACCELEROMETER_PERIOD),
            ]);
        })
        .then(()=>{
            return server;
        });
    }

    setup_magnetometer(server){
        return server.getPrimaryService(UUID_SERVICE_MAGNETOMETER)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_MAGNETOMETER_DATA),
                this.setCharacteristic(service, UUID_CHAR_MAGNETOMETER_BEARING),
            ]);
        })
        .then(()=>{
            return server;
        });
    }

    setup_temperature(server){
        return server.getPrimaryService(UUID_SERVICE_TEMPERATURE)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_TEMPERATURE_DATA),
                this.setCharacteristic(service, UUID_CHAR_TEMPERATURE_PERIOD),
            ]);
        })
        .then(()=>{
            return server;
        });
    }

    setup_button(server){
        return server.getPrimaryService(UUID_SERVICE_BUTTON)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_BUTTON_A_STATE),
                this.setCharacteristic(service, UUID_CHAR_BUTTON_B_STATE),
            ]);
        })
        .then(values =>{
            return Promise.all([
                this.startNotify(UUID_CHAR_BUTTON_A_STATE),
            ]);
        })
        .then(values =>{
            return Promise.all([
                this.startNotify(UUID_CHAR_BUTTON_B_STATE),
            ]);
        })
        .then(()=>{
            return server;
        });
    }
}
```

open()でmicro:bitを検索して、setup()でCharacteristicを走査します。
計測の開始・終了は、Notificatioinの有効化/無効化で切り替えるので、接続時点では有効にしません。
計測データの受信は、set_callback()で登録したコールバックに渡します。

あとは、このクラスを使ってブラウザで表示します。Vueを使います。

```start.js
'use strict';

var sensortag = new Microbit();

var vue_options = {
    el: "#top",
    data: {
        accelerometer: {},
        temperature: {},
        magneto: {},
        magneto_bearing: {},
        temperature_enable : false,
        accelerometer_enable : false,
        magneto_enable : false,
        magneto_bearing_enable : false,
        button_title: '接続',
        button_a: null,
        button_b: null,

        progress_title: '',
    },
    computed: {
    },
    methods: {
        start: async function(){
            if( !sensortag.is_opened() ){
                try{
                    this.button_title = '接続中';
                    sensortag.set_callback(this.on_receive);
                    await sensortag.open();
                    await sensortag.setup();
                    this.button_title = '切断';
                }catch(error){
                    console.log(error);
                    alert(error);
                    this.button_title = '接続';
                }
            }else{
                try{
                    this.button_title = '切断中';
                    sensortag.close();
                    this.button_title = '接続';
                }catch(error){
                    console.log(error);
                    this.button_title = '切断';
                }
            }
        },
        set_enable: async function(type){
            var uuid;
            var value;
            switch(type){
                case 'temperature':
                    uuid = UUID_CHAR_TEMPERATURE_DATA;
                    value = !this.temperature_enable;
                    break;
                case 'accelerometer':
                    uuid = UUID_CHAR_ACCELEROMETER_DATA;
                    value = !this.accelerometer_enable;
                    break;
                case 'magneto':
                    uuid = UUID_CHAR_MAGNETOMETER_DATA;
                    value = !this.magneto_enable;
                    break;
                case 'magneto_bearing':
                    uuid = UUID_CHAR_MAGNETOMETER_BEARING;
                    value = !this.magneto_bearing_enable;
                    break;
            }
            await sensortag.set_enable(uuid, value);
        },
        on_receive: function(data){
            console.log(data);
            switch( data.type ){
                case 'temperature':
                    this.temperature = data;
                    break;
                case 'accelerometer':
                    this.accelerometer = data;
                    break;
                case 'magneto':
                    this.magneto = data;
                    break;
                case 'button_a':
                    if( data.keys == 0x00 )
                        this.button_a = 'not pressed';
                    else if( data.keys == 0x01 )
                        this.button_a = 'pressed';
                    else if( data.keys == 0x02 )
                        this.button_a = 'long press';
                    else
                        this.button_a = 'unknown';
                    break;
                case 'button_b':
                if( data.keys == 0x00 )
                    this.button_b = 'not pressed';
                else if( data.keys == 0x01 )
                    this.button_b = 'pressed';
                else if( data.keys == 0x02 )
                    this.button_b = 'long press';
                else
                    this.button_b = 'unknown';
            break;
            }
        }
    },
    created: function(){
    },
    mounted: function(){
    }
};
var vue = new Vue( vue_options );
```

以下は、HTMLファイルです。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>micro:bit</title>

  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>micro:bit</h1>

        <button class="btn btn-default" v-on:click="start()">{{button_title}}</button><br>
        <br>
        <input type="checkbox" v-model="temperature_enable" v-on:click="set_enable('temperature')">
        Temperature Sensor<br>
        <label>temperature</label> {{temperature.temperature}}<br>
        <br>
        <input type="checkbox" v-model="accelerometer_enable" v-on:click="set_enable('accelerometer')">
        Accelerometer Sensor<br>
        <label>xA</label> {{accelerometer.xA}}<br>
        <label>yA</label> {{accelerometer.yA}}<br>
        <label>zA</label> {{accelerometer.zA}}<br>
        <br>
        <input type="checkbox" v-model="magneto_enable" v-on:click="set_enable('magneto')">
        Magnetometer Sensor<br>
        <label>xM</label> {{magneto.xM}}<br>
        <label>yM</label> {{magneto.yM}}<br>
        <label>zM</label> {{magneto.zM}}<br>
        <input type="checkbox" v-model="magneto_bearing_enable" v-on:click="set_enable('magneto_bearing')">
        Magnetometer Bearing<br>
        <label>bearing</label> {{magneto_bearing.bearing}}<br>
        <br>
        Keys<br>
        <label>button_a</label> {{button_a}}<br>
        <label>button_b</label> {{button_b}}<br>
        <br>

        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="js/microbit.js"></script>
    <script src="js/start.js"></script>
</body>
```

# Chromeブラウザからmicro:bitに接続する

以上のコンテンツをWebサーバに配置して、Chromeから開いてみましょう。
必ず、HTTPSでアクセスしてください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/75543ebf-95eb-6ad5-1438-3efca6b67ec0.png)

さっそく、「接続」ボタンを押してみましょう。
サービスUUIDの指定ができないので、たくさん出てきてしまいます。以下は、一度接続しているので、わかりやすくなっていますが。

(2019/3/25 追記)
サービスUUIDだけでなく名前でもフィルタリングできました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/f89ec7aa-64ad-493c-1580-4ff7ab48d02d.png)

8秒くらいすると、接続が完了して、ボタンの表示が「切断」に変わります。
あとは、計測したいセンサーのチェックボックスをOnにすれば、計測データが表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/37f25ab9-632c-aab3-313d-c78d356c3b98.png)

以上