obnizのページを見ていたら、M5StickC用のobnizOSの説明があったので、試してみました。
勉強がてら、以下のページにある「BLEセントラルコンソール」をM5stickCに移植してみました。
BLEセントラルコンソール
https://obniz.io/ja/webapp/7
BLEセントラルコンソール
ブラウザから、M5StickCのBLEセントラル機能を使って、周辺のBLEデバイスを操作します。
(ESP32の機能を使っているので、M5StickCでなくても動きます)
以下の機能を有しています。
・obniz_idを入力して、obnizに接続します。
・BLEデバイスを探索します。
・BLEデバイスに接続し、PrimaryServiceをDiscoveryします。
・各PrimaryServiceにあるCharacteristicを一覧表示します。
・各Characteristicに対して、Read/Writeします。
こんな感じのWebページです。
obnizにはBLEセントラルのJavascript APIがあるので、そこまで高度な知識がなくても実装できました。
また、オリジナルのBLEセントラルコンソール のソースコードが非常に参考になりました。こちらのコードを8割がた流用させていただいています。
今回再構築したのは、私がVue使いであるためです。
ということで、以下の技術を使っています。
- Vue
- Bootstrap(v3.4.1)
- アロー関数などの最新Javascript
M5StickCへのobnizOSの書き込み
以下に記載の通りにやれば、特に詰まることはありませんでした。
https://obniz.io/ja/doc/obnizos/os_install
ただし、書き込み時に失敗することがあり、ボーレートを落とすことで成功しました。
obniz_cli flashos -b 115200
書き込みが完了し、再起動すると、デバイスキーとお近くのWiFiのSSID/パスワードを入力する必要があります。
上記のためには、Teratermなどのコンソールを接続する必要があります。
ソースコード
以下のソースコードを、どこかにホスティングして、ブラウザからアクセスします。
<!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://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
<!-- Latest compiled and minified JavaScript -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
<title>BLEセントラルコンソール | obniz App</title>
<script src="https://unpkg.com/obniz/obniz.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/m5stickcjs/m5stickc.js"></script>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="obniz-debug"></div>
<br>
<div id="top" class="container">
<h1>BLE Central Console</h1>
<br>
<div class="form-inline">
<label>obniz_id</label> <input type="text" class="form-control" v-model="obniz_id"> <button class="btn btn-default" v-on:click="obniz_connect()">Connect</button><br>
<label>firmware version</label> {{firmware_ver}}
</div>
<div class="row">
<div class="col-md-6">
<h3>Devices</h3>
<select class="form-control" v-model="select_device" v-on:change="device_change()" size="8">
<option v-for="(device, index) in devices" v-bind:value="device">{{device.address + (device.localName ? ' (' + device.localName + ')' : '')}}</option>
</select>
<button class="btn btn-default" v-on:click="device_clear()">Clear</button>
<button class="btn btn-primary" v-on:click="device_connect()" v-if="!isConnected">Connect</button>
<button class="btn btn-primary" v-on:click="device_disconnect()" v-else>Disonnect</button>
<h3>Services</h3>
<select class="form-control" v-model="select_service" v-on:change="service_change()" size="8">
<option v-for="(service, index) in services" v-bind:value="service">{{service.uuid}}</option>
</select>
<h3>Characteristics</h3>
<select class="form-control" v-on:change="characteristic_change()" v-model="select_characteristic" size="6">
<option v-for="(characteristic, index) in characteristics" v-bind:value="characteristic">{{characteristic.uuid}}</option>
</select>
</div>
<div class="col-md-6">
<h3>Detail</h3>
<div class="panel panel-default">
<div class="panel-body">
<div v-if="detail_mode=='device'">
<label>device address</label> {{select_device.address}}<br>
<label>rssi</label> {{select_device.rssi}}<br>
<label>advertise data raw</label> {{array2string(select_device.adv_data)}}<br>
<label>scan response data raw</label> {{array2string(select_device.scan_resp)}}<br>
<label>device meanings</label>
<ul>
<li v-for="(meaning, index) in device_meanings">
<label>{{meaning.title}}</label> {{meaning.infomations}}
</li>
</ul>
</div>
<div v-if="detail_mode=='service'">
<label>service uuid</label> {{select_service.uuid}}<br>
<label>service name</label> {{service_name}} (defined by <a href="https://www.bluetooth.com/specifications/gatt/services">Bluetooth specification</a>)<br>
</div>
<div v-if="detail_mode=='characteristic'">
<label>characteristic uuid</label> {{select_characteristic.uuid}}<br>
<label>characteristic name</label> {{characteristic_name}} (defined by <a href="https://www.bluetooth.com/specifications/gatt/characteristics">Bluetooth specification</a>)<br>
<div class="form-inline">
<label>value type</label>
<label class="radio-inline"><input type="radio" value="binary" v-model="value_type" v-on:change="change_type()" checked>binary</label>
<label class="radio-inline"><input type="radio" value="text" v-model="value_type" v-on:change="change_type()">text</label>
</div>
<label>value</label><br>
<button class="btn btn-primary" v-on:click="characteristic_read">read</button> {{characteristic_read_value}}<br>
<button class="btn btn-primary" v-on:click="characteristic_write">write</button> <input type="text" class="form-control" v-model="characteristic_write_value" placeholder="hex string (ex: f94c8c...) "/><br>
</div>
</div>
</div>
</div>
</div>
<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/start.js"></script>
</body>
こちらがJavascriptです。少々長いですが。
'use strict';
var obniz = null;
var currentPeripheral = null;
var vue_options = {
el: "#top",
data: {
progress_title: '',
obniz_id: "",
firmware_ver: '',
isConnected : false,
detail_mode: 'none',
select_device: null,
devices: [],
select_service: null,
services: [],
select_characteristic: null,
characteristics: [],
characteristic_read_data: null,
characteristic_read_value: '',
characteristic_write_value: '',
device_meanings: [],
value_type: 'binary',
},
computed: {
},
methods: {
// CharacteristicのWriteボタンを押下したとき
characteristic_write: function(){
let chara = this.select_characteristic;
console.log("write value on charactaristic (" + chara.uuid + ")");
let valString = this.characteristic_write_value;
let data;
if (this.value_type === "binary") {
data = splitByLength(valString, 2).map((elm) => {
return parseInt(elm, 16)
});
} else {
data = [];
for (let i = 0; i < valString.length; i++) {
data.push(valString.charCodeAt(i));
}
}
chara.write(data);
chara.onwrite = (results) => {
chara.read();
}
},
// CharacteristicのReadボタンを押下したとき
characteristic_read: function(){
let chara = this.select_characteristic;
console.log("read value on charactaristic (" + chara.uuid + ")");
chara.read();
},
// CharacteristicからReadした値をページに反映したい時
change_type: function(){
var data = this.characteristic_read_data;
let str;
if (this.value_type === "binary") {
str = "0x" + data.map((elm) => {
return elm.toString(16).padStart(2, "0")
}).join("");
if (str.length === 2) {
str = "null";
}
} else {
str = String.fromCharCode.apply(null, data);
if (str.length === 0) {
str = "null";
} else {
str = '"' + str + '"';
}
}
this.characteristic_read_value = str;
},
// CharacteristicのUUIDから名称を検索
showDetailCharacteristic: function(chara) {
let name = characteristicUuidList[parseInt(chara.uuid, 16)];
if( name )
this.characteristic_name = name;
else
this.characteristic_name = '';
},
// Characteristicが選択されたとき
characteristic_change: function(){
let chara = this.select_characteristic;
this.showDetailCharacteristic(chara);
this.characteristic_read_value = '';
this.characteristic_write_value = '';
this.detail_mode = "characteristic";
chara.read();
console.log("read value on charactaristic (" + chara.uuid + ")");
chara.onread = (data) => {
// BLEデバイスからのRead完了時
let currentChara = this.select_characteristic;
if (chara === currentChara) {
this.characteristic_read_data = data;
this.change_type();
}
}
},
// ServiceのUUIDから名前を検索
showDetailService: function(service){
let name = serviceUuidList[parseInt(service.uuid, 16)];
if( name )
this.service_name = name;
else
this.service_name = '';
},
// ServiceにあるCharacteristicを探索
findCharacteristics: function(service) {
console.log("discovering characteristics on service(" + service.uuid + ")");
this.characteristics = [];
service.discoverAllCharacteristics();
service.ondiscovercharacteristic = (chara) => {
this.characteristics.push(chara);
}
},
// Serviceが選択されたとき
service_change: function(){
this.showDetailService(this.select_service);
this.findCharacteristics(this.select_service);
this.detail_mode = "service";
},
// DeviceのPrimaryServiceを探索
findService: function() {
console.log("discovering services on device(" + splitByLength(currentPeripheral.address, 2).join(":") + ")");
this.services = [];
currentPeripheral.discoverAllServices();
currentPeripheral.ondiscoverservice = (service) => {
this.services.push(service);
};
},
// DeviceへのConnectボタンが押下されたとき
device_connect: function(){
let device = this.select_device;
device.onconnect = () => {
// BLEデバイスの接続時
console.log("connected to " + splitByLength(device.address, 2).join(":"));
this.isConnected = true;
currentPeripheral = device;
obniz.led.on();
this.findService();
};
device.ondisconnect = () => {
// BLEデバイスの切断時
console.log("disconnected from " + splitByLength(device.address, 2).join(":"));
this.services = [];
this.characteristics = [];
this.isConnected = false;
currentPeripheral = null;
obniz.led.off();
console.log("start ble scan repeatly");
this.startScanRepeatly();
};
obniz.ble.scan.end();
device.connect();
console.log("connecting to " + splitByLength(device.address, 2).join(":"));
},
// Deviceへの接続の切断
device_disconnect: function(){
currentPeripheral.disconnect();
},
// AdvertiseDataの解析
showDetailDevice: function(peripheral) {
this.device_meanings = [];
peripheral.analyseAdvertisement();
for (let row of peripheral.advertise_data_rows) {
let data = advDataAnalyze(row);
this.device_meanings.push( data );
}
},
// デバイスが選択されたとき
device_change: function(){
this.showDetailDevice(this.select_device);
this.detail_mode = "device";
},
// デバイス一覧をクリア
device_clear: function(){
if (this.isConnected)
return;
obniz.ble.scan.end();
this.devices = [];
console.log("clear all device data and rescan");
obniz.ble.scan.start({duration: 30});
},
// obniz接続完了後の初期処理
setup: function(){
obniz.ble.scan.onfind = (peripheral) =>{
// BLEデバイスの発見時
if (undefined === this.devices.find((elm) => {
return elm.address === peripheral.address
})) {
let address = splitByLength(peripheral.address, 2).join(":");
console.log("find new peripheral : " + address + (peripheral.localName ? "(" + peripheral.localName + ")" : ""));
this.devices.push(peripheral);
}
};
},
// BLEデバイスのスキャンの継続
startScanRepeatly: function() {
obniz.ble.scan.end();
if (!this.isConnected) {
console.log("scan repeating");
obniz.ble.scan.start({duration: 30});
setTimeout(this.startScanRepeatly, 35 * 1000);
}
},
// obnizデバイスの接続
obniz_connect: function(){
// obniz = new Obniz(this.obniz_id);
obniz = new M5StickC(this.obniz_id);
obniz.onconnect = async () => {
// obnizデバイスの接続時
console.log("obniz connected.");
this.firmware_ver = obniz.firmware_ver;
this.setup();
console.log("start ble scan repeatly");
this.startScanRepeatly();
};
},
array2string: function(err){
return array2string(err);
}
},
created: function(){
},
mounted: function(){
}
};
var vue = new Vue( vue_options );
function splitByLength(str, length) {
let resultArr = [];
if (!str || !length || length < 1) {
return resultArr;
}
let index = 0;
let start = index;
let end = start + length;
while (start < str.length) {
resultArr[index] = str.substring(start, end);
index++;
start = end;
end = start + length;
}
return resultArr;
}
function array2string(arr) {
if (!arr || !Array.isArray(arr)) {
return "undefined";
}
if (arr.length === 0) {
return "[ ]";
}
return "[" + arr.map((elm) => {
return "0x" + parseInt(elm).toString(16).padStart(2, "0");
}).join(", ") + "]";
}
// 以降は、解析表示用です。
function advDataAnalyze(row) {
let title;
let infomations = [];
let bytes = row.slice(1);
switch (row[0]) {
case 0x01:
title = "Flags";
let data = {
0x01: "LE Limited Discoverable Mode",
0x02: "LE General Discoverable Mode",
0x04: "BR/EDR Not Supported ",
0x08: "Simultaneous LE and BR/EDR to Same Device Capa- ble (Controller)",
0x10: "Simultaneous LE and BR/EDR to Same Device Capa- ble (Host)",
0x20: "unknown flag - 0x20",
0x40: "unknown flag - 0x40",
0x80: "unknown flag - 0x80",
};
for (let key in data) {
if (parseInt(key) & bytes[0]) {
infomations.push(data[key]);
}
}
break;
case 0x02: // Incomplete List of 16-bit Service Class UUID
case 0x03: // Complete List of 16-bit Service Class UUIDs
title = "16-bit Service UUIDs";
for (let j = 0; j < bytes.length; j += 2) {
let uuid = bytes.slice(j, j + 2).toString('hex').match(/.{1,2}/g).reverse().join('');
infomations.push("uuid - " + uuid);
}
break;
case 0x06: // Incomplete List of 128-bit Service Class UUIDs
case 0x07: // Complete List of 128-bit Service Class UUIDs
title = "128-bit Service UUIDs";
for (let j = 0; j < bytes.length; j += 16) {
let uuid = bytes.slice(j, j + 16).toString('hex').match(/.{1,2}/g).reverse().join('');
infomations.push("uuid - " + uuid);
}
break;
case 0x08: // Shortened Local Name
case 0x09: // Complete Local Name»
title = "Local Name";
infomations.push(String.fromCharCode.apply(null, bytes));
break;
case 0x0a: // Tx Power Level
title = "Tx Power Level";
infomations.push(bytes[0]);
break;
case 0x14: // List of 16 bit solicitation UUIDs
title = "16-bit solicitation UUIDs";
for (let j = 0; j < bytes.length; j += 2) {
let uuid = bytes.slice(j, j + 2).toString('hex').match(/.{1,2}/g).reverse().join('');
infomations.push("uuid - " + uuid);
}
break;
case 0x15: // List of 128 bit solicitation UUIDs
title = "128-bit solicitation UUIDs";
for (let j = 0; j < bytes.length; j += 16) {
let uuid = bytes.slice(j, j + 16).toString('hex').match(/.{1,2}/g).reverse().join('');
infomations.push("uuid - " + uuid);
}
break;
case 0x16: // 16-bit Service Data, there can be multiple occurences
title = "16-bit Service Data";
let serviceDataUuid = bytes.slice(0, 2).toString('hex').match(/.{1,2}/g).reverse().join('');
let serviceData = bytes.slice(2, bytes.length);
infomations.push("uuid - " + serviceDataUuid);
infomations.push("serviceData - " + array2string(serviceData));
break;
case 0x20: // 32-bit Service Data, there can be multiple occurences
title = "32-bit Service Data";
let serviceData32Uuid = bytes.slice(0, 4).toString('hex').match(/.{1,2}/g).reverse().join('');
let serviceData32 = bytes.slice(4, bytes.length);
infomations.push("uuid - " + serviceData32Uuid + "<br/>serviceData - " + array2string(serviceData32));
break;
case 0x21: // 128-bit Service Data, there can be multiple occurences
title = "128-bit Service Data";
let serviceData128Uuid = bytes.slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('');
let serviceData128 = bytes.slice(16, bytes.length);
infomations.push("uuid - " + serviceData128Uuid + "<br/>serviceData - " + array2string(serviceData128));
break;
case 0xff: // 128-bit Service Data, there can be multiple occurences
if (bytes[0] === 0x4c
&& bytes[1] === 0x00
&& bytes[2] === 0x02
&& bytes[3] === 0x15
&& bytes.length === 25) {
title = "Manufacturer Specific Data - iBeacon";
let uuidData = bytes.slice(4, 20);
let uuid = "";
for (let i = 0; i < uuidData.length; i++) {
uuid = uuid + uuidData[i].toString(16).padStart(2, "0");
if (i === (4 - 1) || i === (4 + 2 - 1) || i === (4 + 2 * 2 - 1) || i === (4 + 2 * 3 - 1)) {
uuid += "-";
}
}
let major = "0x" + ((bytes[20] << 8) + bytes[21]).toString(16).padStart(4, "0");
let minor = "0x" + ((bytes[22] << 8) + bytes[23]).toString(16).padStart(4, "0");
let power = "0x" + (bytes[24]).toString(16).padStart(2, "0");
infomations.push("uuid : " + uuid);
infomations.push("major : " + major);
infomations.push("minor : " + minor);
infomations.push("power : " + power);
} else {
title = "Manufacturer Specific Data";
infomations.push(array2string(row.slice(1)));
}
break;
default :
title = "unhandled type";
infomations.push(array2string(row.slice(1)));
break;
}
title += "(0x" + row[0].toString(16).padStart(2, "0") + ")";
return {title, infomations};
}
const serviceUuidList = {
0x1800: "Generic Access",
0x1811: "Alert Notification Service",
0x1815: "Automation IO",
0x180F: "Battery Service",
0x1810: "Blood Pressure",
0x181B: "Body Composition",
0x181E: "Bond Management Service",
0x181F: "Continuous Glucose Monitoring",
0x1805: "Current Time Service",
0x1818: "Cycling Power",
0x1816: "Cycling Speed and Cadence",
0x180A: "Device Information",
0x181A: "Environmental Sensing",
0x1826: "Fitness Machine",
0x1801: "Generic Attribute",
0x1808: "Glucose",
0x1809: "Health Thermometer",
0x180D: "Heart Rate",
0x1823: "HTTP Proxy",
0x1812: "Human Interface Device",
0x1802: "Immediate Alert",
0x1821: "Indoor Positioning",
0x1820: "Internet Protocol Support Service",
0x1803: "Link Loss",
0x1819: "Location and Navigation",
0x1827: "Mesh Provisioning Service",
0x1828: "Mesh Proxy Service",
0x1807: "Next DST Change Service",
0x1825: "Object Transfer Service",
0x180E: "Phone Alert Status Service",
0x1822: "Pulse Oximeter Service",
0x1829: "Reconnection Configuration",
0x1806: "Reference Time Update Service",
0x1814: "Running Speed and Cadence",
0x1813: "Scan Parameters",
0x1824: "Transport Discovery",
0x1804: "Tx Power",
0x181C: "User Data",
0x181D: "Weight Scale",
};
const characteristicUuidList = {
0x2A7E: "Aerobic Heart Rate Lower Limit",
0x2A84: "Aerobic Heart Rate Upper Limit",
0x2A7F: "Aerobic Threshold",
0x2A80: "Age",
0x2A5A: "Aggregate",
0x2A43: "Alert Category ID",
0x2A42: "Alert Category ID Bit Mask",
0x2A06: "Alert Level",
0x2A44: "Alert Notification Control Point",
0x2A3F: "Alert Status",
0x2AB3: "Altitude",
0x2A81: "Anaerobic Heart Rate Lower Limit",
0x2A82: "Anaerobic Heart Rate Upper Limit",
0x2A83: "Anaerobic Threshold",
0x2A58: "Analog",
0x2A59: "Analog Output",
0x2A73: "Apparent Wind Direction",
0x2A72: "Apparent Wind Speed",
0x2A01: "Appearance",
0x2AA3: "Barometric Pressure Trend",
0x2A19: "Battery Level",
0x2A1B: "Battery Level State",
0x2A1A: "Battery Power State",
0x2A49: "Blood Pressure Feature",
0x2A35: "Blood Pressure Measurement",
0x2A9B: "Body Composition Feature",
0x2A9C: "Body Composition Measurement",
0x2A38: "Body Sensor Location",
0x2AA4: "Bond Management Control Point",
0x2AA5: "Bond Management Features",
0x2A22: "Boot Keyboard Input Report",
0x2A32: "Boot Keyboard Output Report",
0x2A33: "Boot Mouse Input Report",
0x2AA6: "Central Address Resolution",
0x2AA8: "CGM Feature",
0x2AA7: "CGM Measurement",
0x2AAB: "CGM Session Run Time",
0x2AAA: "CGM Session Start Time",
0x2AAC: "CGM Specific Ops Control Point",
0x2AA9: "CGM Status",
0x2ACE: "Cross Trainer Data",
0x2A5C: "CSC Feature",
0x2A5B: "CSC Measurement",
0x2A2B: "Current Time",
0x2A66: "Cycling Power Control Point",
0x2A65: "Cycling Power Feature",
0x2A63: "Cycling Power Measurement",
0x2A64: "Cycling Power Vector",
0x2A99: "Database Change Increment",
0x2A85: "Date of Birth",
0x2A86: "Date of Threshold Assessment",
0x2A08: "Date Time",
0x2A0A: "Day Date Time",
0x2A09: "Day of Week",
0x2A7D: "Descriptor Value Changed",
0x2A00: "Device Name",
0x2A7B: "Dew Point",
0x2A56: "Digital",
0x2A57: "Digital Output",
0x2A0D: "DST Offset",
0x2A6C: "Elevation",
0x2A87: "Email Address",
0x2A0B: "Exact Time 100",
0x2A0C: "Exact Time 256",
0x2A88: "Fat Burn Heart Rate Lower Limit",
0x2A89: "Fat Burn Heart Rate Upper Limit",
0x2A26: "Firmware Revision String",
0x2A8A: "First Name",
0x2AD9: "Fitness Machine Control Point",
0x2ACC: "Fitness Machine Feature",
0x2ADA: "Fitness Machine Status",
0x2A8B: "Five Zone Heart Rate Limits",
0x2AB2: "Floor Number",
0x2A8C: "Gender",
0x2A51: "Glucose Feature",
0x2A18: "Glucose Measurement",
0x2A34: "Glucose Measurement Context",
0x2A74: "Gust Factor",
0x2A27: "Hardware Revision String",
0x2A39: "Heart Rate Control Point",
0x2A8D: "Heart Rate Max",
0x2A37: "Heart Rate Measurement",
0x2A7A: "Heat Index",
0x2A8E: "Height",
0x2A4C: "HID Control Point",
0x2A4A: "HID Information",
0x2A8F: "Hip Circumference",
0x2ABA: "HTTP Control Point",
0x2AB9: "HTTP Entity Body",
0x2AB7: "HTTP Headers",
0x2AB8: "HTTP Status Code",
0x2ABB: "HTTPS Security",
0x2A6F: "Humidity",
0x2A2A: "IEEE 11073-20601 Regulatory Certification Data List",
0x2AD2: "Indoor Bike Data",
0x2AAD: "Indoor Positioning Configuration",
0x2A36: "Intermediate Cuff Pressure",
0x2A1E: "Intermediate Temperature",
0x2A77: "Irradiance",
0x2AA2: "Language",
0x2A90: "Last Name",
0x2AAE: "Latitude",
0x2A6B: "LN Control Point",
0x2A6A: "LN Feature",
0x2AB1: "Local East Coordinate",
0x2AB0: "Local North Coordinate",
0x2A0F: "Local Time Information",
0x2A67: "Location and Speed Characteristic",
0x2AB5: "Location Name",
0x2AAF: "Longitude",
0x2A2C: "Magnetic Declination",
0x2AA0: "Magnetic Flux Density - 2D",
0x2AA1: "Magnetic Flux Density - 3D",
0x2A29: "Manufacturer Name String",
0x2A91: "Maximum Recommended Heart Rate",
0x2A21: "Measurement Interval",
0x2A24: "Model Number String",
0x2A68: "Navigation",
0x2A3E: "Network Availability",
0x2A46: "New Alert",
0x2AC5: "Object Action Control Point",
0x2AC8: "Object Changed",
0x2AC1: "Object First-Created",
0x2AC3: "Object ID",
0x2AC2: "Object Last-Modified",
0x2AC6: "Object List Control Point",
0x2AC7: "Object List Filter",
0x2ABE: "Object Name",
0x2AC4: "Object Properties",
0x2AC0: "Object Size",
0x2ABF: "Object Type",
0x2ABD: "OTS Feature",
0x2A04: "Peripheral Preferred Connection Parameters",
0x2A02: "Peripheral Privacy Flag",
0x2A5F: "PLX Continuous Measurement Characteristic",
0x2A60: "PLX Features",
0x2A5E: "PLX Spot-Check Measurement",
0x2A50: "PnP ID",
0x2A75: "Pollen Concentration",
0x2A2F: "Position 2D",
0x2A30: "Position 3D",
0x2A69: "Position Quality",
0x2A6D: "Pressure",
0x2A4E: "Protocol Mode",
0x2A62: "Pulse Oximetry Control Point",
0x2A78: "Rainfall",
0x2B1D: "RC Feature",
0x2B1E: "RC Settings",
0x2A03: "Reconnection Address",
0x2B1F: "Reconnection Configuration Control Point",
0x2A52: "Record Access Control Point",
0x2A14: "Reference Time Information",
0x2A3A: "Removable",
0x2A4D: "Report",
0x2A4B: "Report Map",
0x2AC9: "Resolvable Private Address Only",
0x2A92: "Resting Heart Rate",
0x2A40: "Ringer Control point",
0x2A41: "Ringer Setting",
0x2AD1: "Rower Data",
0x2A54: "RSC Feature",
0x2A53: "RSC Measurement",
0x2A55: "SC Control Point",
0x2A4F: "Scan Interval Window",
0x2A31: "Scan Refresh",
0x2A3C: "Scientific Temperature Celsius",
0x2A10: "Secondary Time Zone",
0x2A5D: "Sensor Location",
0x2A25: "Serial Number String",
0x2A05: "Service Changed",
0x2A3B: "Service Required",
0x2A28: "Software Revision String",
0x2A93: "Sport Type for Aerobic and Anaerobic Thresholds",
0x2AD0: "Stair Climber Data",
0x2ACF: "Step Climber Data",
0x2A3D: "String",
0x2AD7: "Supported Heart Rate Range",
0x2AD5: "Supported Inclination Range",
0x2A47: "Supported New Alert Category",
0x2AD8: "Supported Power Range",
0x2AD6: "Supported Resistance Level Range",
0x2AD4: "Supported Speed Range",
0x2A48: "Supported Unread Alert Category",
0x2A23: "System ID",
0x2ABC: "TDS Control Point",
0x2A6E: "Temperature",
0x2A1F: "Temperature Celsius",
0x2A20: "Temperature Fahrenheit",
0x2A1C: "Temperature Measurement",
0x2A1D: "Temperature Type",
0x2A94: "Three Zone Heart Rate Limits",
0x2A12: "Time Accuracy",
0x2A15: "Time Broadcast",
0x2A13: "Time Source",
0x2A16: "Time Update Control Point",
0x2A17: "Time Update State",
0x2A11: "Time with DST",
0x2A0E: "Time Zone",
0x2AD3: "Training Status",
0x2ACD: "Treadmill Data",
0x2A71: "True Wind Direction",
0x2A70: "True Wind Speed",
0x2A95: "Two Zone Heart Rate Limit",
0x2A07: "Tx Power Level",
0x2AB4: "Uncertainty",
0x2A45: "Unread Alert Status",
0x2AB6: "URI",
0x2A9F: "User Control Point",
0x2A9A: "User Index",
0x2A76: "UV Index",
0x2A96: "VO2 Max",
0x2A97: "Waist Circumference",
0x2A98: "Weight",
0x2A9D: "Weight Measurement",
0x2A9E: "Weight Scale Feature",
0x2A79: "Wind Chill",
};
以下のところでは、M5StickCのクラスを使っています。
// obniz = new Obniz(this.obniz_id);
obniz = new M5StickC(this.obniz_id);
これまでどおり、Obnizを使ってもよいですが、obniz.led.on()/obniz.led.off() を使っています。M5StickCの内蔵LEDを使う場合は、new M5StickCの方を使ってください。
使い方
使い方は簡単です。
「obniz_id」のところに、お手持ちのobnizデバイスのobniz_idを入力して、「Connect」ボタンを押下します。
接続が完了すると、obnizデバイスに書き込んであるファームウェアのバージョンが表示されます。
そうすると、自動的にobnizデバイスの周りにあるBLEデバイスを探索し始めます。
見つかったデバイスは、「Devices」のところに追加されていきます。
Deviceを選択すると、Detailのところに、アドレスやらAdvertiseDataやらScanResponse等々が表示されます。
次に、接続したいBLEデバイスを「Devices」から選択し、Connectボタンを押下します。
そうすると、「Services」のところに、PrimaryServiceの一覧が表示されます。
さらに、「Services」の中のPrimaryServiceを選択すると、それに属するCharacteristicが「Charactersitics」に表示されます。
Characteristicを選択すると、そのCharacteristicにReadし、その値がDetailのところに表示されます。書き込みをしたい場合には、Detailのテキストボックスに16進数文字列で入力して「Write」ボタンを押下します。
補足
以下のページにあるWebアプリが、M5StickCで動かないのは、使っているobniz.jsのバージョンが古いだけなので、単にそこを直せば動くようです。
BLEセントラルコンソール
https://obniz.io/ja/webapp/7
以下の部分です。
<script src="https://unpkg.com/obniz@2.3.0/obniz.js" crossorigin="anonymous"></script>