#概要
以前「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)
#実行結果
###実行手順
- M5Stack Fire側のプログラムを実行
- HTML を直接 Chrome で開く
- BLE での接続用ボタン「BLE接続」をクリック(ユーザ操作無しでの接続は不可)
- 「BLE Env. Sensor」とペア設定
- M5Stack から BLE経由 で送られてくる気温・湿度・気圧値が Google Charts で表示される
###M5Stack の表示
センサーが不調のようで湿度が低すぎ。実際には40%程度。
「気温」「気圧」を「氣温」「氣压」としているのは lcd.FONT_UNICODE が中国語用のフォントで「気」や「圧」等、一部の日本語の漢字が表示できないためです。(ちなみに M5Stack Core2 ではこれらの日本語の漢字も表示可能です)
#プログラム
###UIFlow の Blockly で作成したプログラム
###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_advertising
を from 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>
>>>