JavaScript
Chrome
BLE
WebBluetooth
microbit

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

前回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

以上