4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

M5Stack UIFlowのBLE UARTでChromeと接続し環境センサー値をGoogle Chartsで表示

Last updated at Posted at 2021-01-10

#概要

以前「M5GO(M5Stack)とChromeをBLEで接続し環境センサー値をGoogle Chartsで表示」で Arduino で作成したプログラムで Chrome との BLE による接続を試しましたが、M5Stack Fire/Core2 では UIFlow (最新版は 1.7.1) で BLE が使えるようになったので、今回は UIFlow の Blockly を使って Chrome との BLE による接続を試しました。

  • M5Stack で付属の環境センサー(DHT12、BMP280)から値を取得
  • M5Stack と Chrome を LE で接続
  • M5Stack から BLE の Notify にて環境センサー値を Chrome へ通知
  • Chrome側で取得した環境センサー値を Google Charts で表示

おまけとして Blockly で使用している ble_uart モジュールではなく、より基本的な bluetooth モジュールを使って同様の機能を実装してみました。

#環境

  • M5Stack Fire (UIFlow 1.7.1)
  • iMac (macOS Catalina)
  • Google Chrome (87.0.4280.141)

#実行結果

###実行手順

  1. M5Stack Fire側のプログラムを実行
  2. HTML を直接 Chrome で開く
  3. BLE での接続用ボタン「BLE接続」をクリック(ユーザ操作無しでの接続は不可)
  4. 「BLE Env. Sensor」とペア設定
  5. M5Stack から BLE経由 で送られてくる気温・湿度・気圧値が Google Charts で表示される

###M5Stack の表示
センサーが不調のようで湿度が低すぎ。実際には40%程度。
「気温」「気圧」を「氣温」「氣压」としているのは lcd.FONT_UNICODE が中国語用のフォントで「気」や「圧」等、一部の日本語の漢字が表示できないためです。(ちなみに M5Stack Core2 ではこれらの日本語の漢字も表示可能です)
IMG_5907.jpeg

###Chrome の表示
スクリーンショット 2021-01-10 21.58.39.png

#プログラム
###UIFlow の Blockly で作成したプログラム

スクリーンショット 2021-01-10 21.48.52.png
スクリーンショット 2021-01-10 21.49.12.png

###Chrome側プログラム
以前のものとの主な変更点は Service/Characteristic の UUID を UARTのものに変えた程度。

<html>
    <head>
        <title>気温・湿度・気圧計 - Google Charts</title>
        <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
        <script type="text/javascript">
            google.charts.load('current', {'packages':['gauge', 'corechart']});
            google.charts.setOnLoadCallback(chartSetup);

            var optionGaugeT = {
                    width: 250, height: 250,
                    min: -10, max: 50,
                    majorTicks: ["-10", "0", "10", "20", "30", "40", "50"],
                    minorTicks: 5
                };
            var optionGaugeH = {
                    width: 250, height: 250,
                    min: 0, max: 100,
                    majorTicks: ["0", "20", "40", "60", "80", "100"],
                    minorTicks: 4
                };
            var optionGaugeP = {
                    width: 250, height: 250,
                    min: 940, max: 1060,
                    majorTicks: ["940", "960", "980", "1000", "1020", "1040", "1060"],
                    minorTicks: 4
                };
            var optionLineTH = {
                    title: '気温&湿度の変化',
                    width: 1000,
                    height: 300,
                    colors: ['#DC3912', '#3366CC'],
                    // Gives each series an axis that matches the vAxes number below.
                    series: {
                        0: {targetAxisIndex: 0},
                        1: {targetAxisIndex: 1}
                    },
                    vAxes: {
                      // Adds titles to each axis.
                        0: {title: '気温',
                            format: '#.#℃'},
                        1: {title: '湿度',
                            format: '#.#%'}
                    },
                    hAxis: {
                        format: 'HH:mm:ss'
                    },
                    vAxis: {
                        titleTextStyle: {italic: false}
                    }
                };

            var optionLineP = {
                    title: '気圧の変化',
                    width: 1000,
                    height: 300,
                    // Gives each series an axis that matches the vAxes number below.
                    series: {
                        0: {targetAxisIndex: 0}
                    },
                    vAxes: {
                      // Adds titles to each axis.
                        0: {title: '気圧',
                            format: '#.#hPa'}
                    },
                    hAxis: {
                        format: 'HH:mm:ss'
                    },
                    vAxis: {
                        titleTextStyle: {italic: false}
                    }
                };

            var dataGaugeT;
            var dataGaugeH;
            var dataGaugeP;
            var dataLineTH;
            var dataLineP;

            var chartGaugeT;
            var chartGaugeH;
            var chartGaugeP;
            var chartLineTH;
            var chartLineP;

            var formatterT;
            var formatterH;
            var formatterP;

            function chartSetup() {
                dataGaugeT = google.visualization.arrayToDataTable([
                        ['Label', 'Value'],
                        ['気温', 0]
                    ]);
                dataGaugeH = google.visualization.arrayToDataTable([
                        ['Label', 'Value'],
                        ['湿度', 0]
                    ]);
                dataGaugeP = google.visualization.arrayToDataTable([
                        ['Label', 'Value'],
                        ['気圧', 0]
                  ]);

                dataLineTH = new google.visualization.DataTable();
                dataLineTH.addColumn('datetime', '日時');
                dataLineTH.addColumn('number',   '気温');
                dataLineTH.addColumn('number',   '湿度');

                dataLineP = new google.visualization.DataTable();
                dataLineP.addColumn('datetime', '日時');
                dataLineP.addColumn('number',   '気圧');

                chartGaugeT = new google.visualization.Gauge(document.getElementById('gauge_chart_temperature'));
                chartGaugeH = new google.visualization.Gauge(document.getElementById('gauge_chart_humidity'));
                chartGaugeP = new google.visualization.Gauge(document.getElementById('gauge_chart_pressure'));
                chartLineTH = new google.visualization.LineChart(document.getElementById('line_chart_temp_hum'));
                chartLineP  = new google.visualization.LineChart(document.getElementById('line_chart_pressure'));

                formatterT = new google.visualization.NumberFormat({suffix:'', fractionDigits:1});
                formatterH = new google.visualization.NumberFormat({suffix:' %', fractionDigits:1});
                formatterP = new google.visualization.NumberFormat({suffix:' hPa', fractionDigits:1});

                updateCharts(undefined, undefined, undefined);
            }

            function updateCharts(t, h, p) {
                serverTime = new Date();
                //document.getElementById('datetime').innerHTML=serverTime.toLocaleString();
                document.getElementById('datetime').innerHTML =
                                    serverTime.getFullYear() + '/'
                            +  ('0' + (serverTime.getMonth() + 1)).slice(-2) + '/'
                            +  ('0' +  serverTime.getDate()).slice(-2) + ' '
                            +  ('0' +  serverTime.getHours()).slice(-2) + ':'
                            +  ('0' +  serverTime.getMinutes()).slice(-2) + ':'
                            +  ('0' +  serverTime.getSeconds()).slice(-2);

                if (t === undefined) {
                    optionLineTH['title'] = '気温&湿度の変化';
                    optionLineP['title']  = '気圧の変化';
                } else {
                    dataGaugeT.setValue(0, 1, t);
                    dataGaugeH.setValue(0, 1, h);
                    dataGaugeP.setValue(0, 1, p);

                    dataLineTH.addRow([serverTime, t, h / 100])
                    if (dataLineTH.getNumberOfRows() > 600)
                        dataLineTH.removeRow(0)

                    dataLineP.addRow([serverTime, p])
                    if (dataLineP.getNumberOfRows() > 600)
                        dataLineP.removeRow(0)

                    optionLineTH['title'] = '気温&湿度の変化  '
                                        + dataLineTH.getValue(0, 0).toLocaleString()
                                        + ' - '
                                        + dataLineTH.getValue(dataLineTH.getNumberOfRows()-1, 0).toLocaleString();

                    optionLineP['title'] = '気圧の変化  '
                                        + dataLineP.getValue(0, 0).toLocaleString()
                                        + ' - '
                                        + dataLineP.getValue(dataLineP.getNumberOfRows()-1, 0).toLocaleString();
                }
                formatterT.format(dataGaugeT, 1);
                formatterH.format(dataGaugeH, 1);
                formatterP.format(dataGaugeP, 1);

                chartGaugeT.draw(dataGaugeT, optionGaugeT);
                chartGaugeH.draw(dataGaugeH, optionGaugeH);
                chartGaugeP.draw(dataGaugeP, optionGaugeP);
                chartLineTH.draw(dataLineTH, optionLineTH);
                chartLineP.draw(dataLineP, optionLineP);
            }

            var bluetoothDevice;
            var characteristic;
            var service;

            var SERVICE_UUID        = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
            var CHARACTERISTIC_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';

            function toggle_connection() {
                if (!bluetoothDevice || !bluetoothDevice.gatt.connected)
                    connect();
                else
                    disconnect();
            }

            //BEL接続処理
            function connect() {
                let options = {};

                options.filters = [
                    {services: [SERVICE_UUID]},
                    {name: "BLE Env. Sensor"}
                ];

                navigator.bluetooth.requestDevice(options)
                .then(device => {
                    bluetoothDevice = device;
                    console.log("device", device);
                    return device.gatt.connect();
                })
                .then(server =>{
                    console.log("server", server)
                    return server.getPrimaryService(SERVICE_UUID);
                })
                .then(serv => {
                    service = serv;
                    console.log("service", service)
                    return service.getCharacteristic(CHARACTERISTIC_UUID)
                })
                .then(chara => {
                    document.getElementById('toggle_connection').innerHTML = "BLE 切断";
                    console.log("characteristic", chara)
                    characteristic = chara;
                    characteristic.addEventListener('characteristicvaluechanged', onRecvSensorData);
                            characteristic.startNotifications();
                })
                .catch(error => {
                    console.log(error);
                });
            }

            //BEL切断処理
            function disconnect() {
                document.getElementById('toggle_connection').innerHTML = "BLE 接続";
                if (!bluetoothDevice || !bluetoothDevice.gatt.connected) return;
                bluetoothDevice.gatt.disconnect();
            }

            function onRecvSensorData(event) {
                let characteristic = event.target;
                let value = characteristic.value;
                let decoder = new TextDecoder('utf-8');

                ble_env = decoder.decode(value).split(',')
                // ble_env[0] Temperature from DHT12
                // ble_env[1] Humidity    from DHT12
                // ble_env[2] Pressure    from BMP280
                console.log(ble_env[0] + ', ' + ble_env[1] + ', ' + ble_env[2])

                updateCharts(Number(ble_env[0]), Number(ble_env[1]), Number(ble_env[2]));
            }

       </script>
    </head>
    <body>
        <div style="width: 1000px; margin: 0 auto;">
            <div style="text-align: center;">
                <button id="toggle_connection" style="font-size: 120%; font-family: sans-serif;" onClick="toggle_connection()">BLE 接続</button>
            </div>
            <div id="datetime" style="text-align: center; font-size: 300%; font-family: sans-serif;"></div>
            <div style=" display:flex;justify-content: center;">
                <div id="gauge_chart_temperature"></div>
                <div id="gauge_chart_humidity"></div>
                <div id="gauge_chart_pressure"></div>
            </div>
            <div id="line_chart_temp_hum"></div>
            <div id="line_chart_pressure"></div>
        </div>
    </body>
</html>

###UIFlow で生成された MicroPython プログラム

from m5stack import *
from m5ui import *
from uiflow import *
from ble import ble_uart
import time
import unit

setScreenColor(0x222222)
env0 = unit.get(unit.ENV, unit.PORTA)

t = None
h = None
p = None

label0 = M5TextBox(160, 50, "Text", lcd.FONT_DejaVu24, 0xFFFFFF, rotate=0)
label1 = M5TextBox(160, 100, "Text", lcd.FONT_DejaVu24, 0xFFFFFF, rotate=0)
label2 = M5TextBox(160, 150, "Text", lcd.FONT_DejaVu24, 0xFFFFFF, rotate=0)
label3 = M5TextBox(50, 50, "氣温", lcd.FONT_UNICODE, 0xFFFFFF, rotate=0)
label4 = M5TextBox(50, 100, "湿度", lcd.FONT_UNICODE, 0xFFFFFF, rotate=0)
label5 = M5TextBox(50, 150, "氣压", lcd.FONT_UNICODE, 0xFFFFFF, rotate=0)

uart_ble = ble_uart.init('BLE Env. Sensor')
while True:
  t = env0.temperature
  h = env0.humidity
  p = env0.pressure
  label0.setText(str(t))
  label1.setText(str(h))
  label2.setText(str(p))
  uart_ble.write(','.join([str(t), str(h), str(p)]))
  wait(1)

###bluetooth モジュールを使って実装した MicroPython プログラム

以下は同様の機能を公式の micropython のサンプルプログラム ble_uart_peripheral.py を元に修正したもので、
from ble_advertisingfrom ble.ble_advertising とすることでイベント発生時の処理はそのまま動かすことができ、接続時・切断時の処理を追加したりできます。

from m5stack import *
from m5ui import *
from uiflow import *
import unit

import bluetooth
#from ble_advertising import advertising_payload
#UIFlow では ble_advertising を ble.ble_advertising に修正する必要あり
from ble.ble_advertising import advertising_payload

from micropython import const

_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)

_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)

_UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX = (
    bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"),
    _FLAG_NOTIFY,
)
_UART_RX = (
    bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"),
    _FLAG_WRITE,
)
_UART_SERVICE = (
    _UART_UUID,
    (_UART_TX, _UART_RX),
)

# org.bluetooth.characteristic.gap.appearance.xml
_ADV_APPEARANCE_GENERIC_COMPUTER = const(128)


class BLEUART:
    def __init__(self, ble, name="mpy-uart", rxbuf=100):
        self._ble = ble
        self._ble.active(True)
        self._ble.irq(self._irq)
        ((self._tx_handle, self._rx_handle),) = self._ble.gatts_register_services((_UART_SERVICE,))
        # Increase the size of the rx buffer and enable append mode.
        self._ble.gatts_set_buffer(self._rx_handle, rxbuf, True)
        self._connections = set()
        self._rx_buffer = bytearray()
        self._handler = None
        # Optionally add services=[_UART_UUID], but this is likely to make the payload too large.
        self._payload = advertising_payload(name=name, appearance=_ADV_APPEARANCE_GENERIC_COMPUTER)
        self._advertise()

    def irq(self, handler):
        self._handler = handler

    def _irq(self, event, data):
        # Track connections so we can send notifications.
        if event == _IRQ_CENTRAL_CONNECT:
            label4.setText("接續")
            conn_handle, _, _ = data
            self._connections.add(conn_handle)
        elif event == _IRQ_CENTRAL_DISCONNECT:
            conn_handle, _, _ = data
            if conn_handle in self._connections:
                label4.setText("切断")
                self._connections.remove(conn_handle)
            # Start advertising again to allow a new connection.
            self._advertise()
        elif event == _IRQ_GATTS_WRITE:
            conn_handle, value_handle = data
            if conn_handle in self._connections and value_handle == self._rx_handle:
                self._rx_buffer += self._ble.gatts_read(self._rx_handle)
                if self._handler:
                    self._handler()

    def any(self):
        return len(self._rx_buffer)

    def read(self, sz=None):
        if not sz:
            sz = len(self._rx_buffer)
        result = self._rx_buffer[0:sz]
        self._rx_buffer = self._rx_buffer[sz:]
        return result

    def write(self, data):
        for conn_handle in self._connections:
            self._ble.gatts_notify(conn_handle, self._tx_handle, data)

    def close(self):
        for conn_handle in self._connections:
            self._ble.gap_disconnect(conn_handle)
        self._connections.clear()

    def _advertise(self, interval_us=500000):
        self._ble.gap_advertise(interval_us, adv_data=self._payload)


def demo():
    env0 = unit.get(unit.ENV, unit.PORTA)

    ble = bluetooth.BLE()
    uart = BLEUART(ble, 'BLE Env. Sensor')

    def on_rx():
        print("rx: ", uart.read().decode().strip())

    uart.irq(handler=on_rx)

    while True:
      t = env0.temperature
      h = env0.humidity
      p = env0.pressure
      label5.setText(str(t))
      label6.setText(str(h))
      label7.setText(str(p))
      uart.write(','.join([str(t), str(h), str(p)]))
      wait(1)

    #uart.close()

setScreenColor(0x222222)
label0 = M5TextBox( 50, 40*1, "状態", lcd.FONT_UNICODE, 0xFFFFFF, rotate=0)
label1 = M5TextBox( 50, 40*2, "氣温", lcd.FONT_UNICODE, 0xFFFFFF, rotate=0)
label2 = M5TextBox( 50, 40*3, "湿度", lcd.FONT_UNICODE, 0xFFFFFF, rotate=0)
label3 = M5TextBox( 50, 40*4, "氣压", lcd.FONT_UNICODE, 0xFFFFFF, rotate=0)
label4 = M5TextBox(150, 40*1, "切断", lcd.FONT_UNICODE, 0xFFFFFF, rotate=0)
label5 = M5TextBox(150, 40*2, "", lcd.FONT_DejaVu24, 0xFFFFFF, rotate=0)
label6 = M5TextBox(150, 40*3, "", lcd.FONT_DejaVu24, 0xFFFFFF, rotate=0)
label7 = M5TextBox(150, 40*4, "", lcd.FONT_DejaVu24, 0xFFFFFF, rotate=0)
demo()
#
#UIFlow から動かした場合 __name__ は "flow.m5cloud" になる
#if __name__ == "__main__":
#    demo()

以下のように UIFlow の ble_uart モジュールの中を覗いてみると、中身は ble_uart_peripheral.py と同じような気がします。

>>> from ble import ble_uart
>>> help(ble_uart)
object <module 'ble.ble_uart' from 'ble/ble_uart.py'> is of type module
  const -- <function>
  BLEUART -- <class 'BLEUART'>
  advertising_payload -- <function advertising_payload at 0x3f81bae0>
  _UART_UUID -- UUID128('6e400001-b5a3-f393-e0a9-e50e24dcca9e')
  __file__ -- ble/ble_uart.py
  init -- <function init at 0x3f81bde0>
  __name__ -- ble.ble_uart
  bluetooth -- <module 'ubluetooth'>
  _UART_SERVICE -- (UUID128('6e400001-b5a3-f393-e0a9-e50e24dcca9e'), ((UUID128('6e400003-b5a3-f393-e0a9-e50e24dcca9e'), 16), (UUID128('6e400002-b5a3-f393-e0a9-e50e24dcca9e'), 8)))
  _UART_RX -- (UUID128('6e400002-b5a3-f393-e0a9-e50e24dcca9e'), 8)
  _ble_uart -- None
  _UART_TX -- (UUID128('6e400003-b5a3-f393-e0a9-e50e24dcca9e'), 16)
>>>
>>> help(ble_uart.BLEUART)
object <class 'BLEUART'> is of type type
  any -- <function any at 0x3f81be50>
  __qualname__ -- BLEUART
  irq -- <function irq at 0x3f81bd60>
  __module__ -- ble.ble_uart
  read -- <function read at 0x3f81be80>
  rename -- <function rename at 0x3f81c0b0>
  close -- <function close at 0x3f81c070>
  write -- <function write at 0x3f81c060>
  _irq -- <function _irq at 0x3f81be70>
  _advertise -- <function _advertise at 0x3f81c090>
  __init__ -- <function __init__ at 0x3f81be30>
>>> 
4
4
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
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?