3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-21

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

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

ソースコード

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

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

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