obnizOSがM5StickCに対応したので試してみた

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ページです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/cc373c7b-4c7a-8cd7-0640-7366092f6ccf.png)

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などのコンソールを接続する必要があります。

# ソースコード

以下のソースコードを、どこかにホスティングして、ブラウザからアクセスします。

```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://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です。少々長いですが。

```js:start.js
'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>
```