2
3

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 5 years have passed since last update.

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

Last updated at Posted at 2019-03-25

前回TIが販売している小型で電池駆動のSensorTagをChromeブラウザから接続してセンサー情報を表示してみました。
 TI SensorTagをブラウザから使う(1):with Chromeブラウザ
 TI SensorTagをブラウザから使う(2):with LINE Things

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

MakeCodeでmicro:bitをSensorTag化する

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

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

image.png

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

image.png

「ずっと」のブロックはいらないので右クリックで削除します。
右上の歯車をクリックして、拡張機能を選択します。

image.png

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

image.png

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

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

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

image.png

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

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

image.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

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

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

image.png

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

image.png

以上

2
3
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
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?