概要
- M5GOで付属の環境センサー(DHT12、BMP280)から値を取得
- M5GOとMacの接続はBluetooth LEを使用しNotifyで通知
- Mac側はPythonからCoreBluetooth frameworkを使用して接続
- Pythonからpywebviewを使用しGoogle Chartsにより取得値を表示
以前作成した「M5GO(M5Stack)とChromeをBLEで接続し環境センサー値をGoogle Chartsで表示」を
Mac上のPythonでBLEを使用するためのお試しプログラムとして作成しました。
表示部分はChrome版と同様のものでpywebviewを使って表示しています。
環境
- M5GO (Arduino IDE 1.8.7)
- iMac (macOS Mojave)
- Python 3.6.4
- pyobjc-framework-CoreBluetooth 5.2
- pywebview 2.4
実行結果

M5GO側プログラム
「M5GO(M5Stack)とChromeをBLEで接続し環境センサー値をGoogle Chartsで表示」で使用しているものと同じ
Mac側プログラム
M5GO_Env_BLE_Notify.py
# !/usr/bin/env python
# -*- coding: utf-8 -*-
import webview
import signal
from queue import Queue
from threading import Thread
from Foundation import CBCentralManager, CBUUID
from PyObjCTools import AppHelper
M5GO_NAME = 'M5GO Env. Sensor'
M5GO_SERVICE = CBUUID.UUIDWithString_(u'c44205a6-c87c-11e8-a8d5-f2801f1b9fd1')
M5GO_CHARACTERISTIC = CBUUID.UUIDWithString_(u'c442090c-c87c-11e8-a8d5-f2801f1b9fd1')
HTML = """
<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.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);
}
</script>
</head>
<body>
<div style="width: 1000px; margin: 0 auto;">
<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>
"""
class CBCentralManagerDelegate(object):
def __init__(self):
self.peripheral = None
def centralManagerDidUpdateState_(self, manager):
self.manager = manager
manager.scanForPeripheralsWithServices_options_(None, None)
def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, peripheral, data, rssi):
self.peripheral = peripheral
if peripheral.name() is not None:
print('DeviceName:', peripheral.name())
if peripheral.name() == M5GO_NAME:
manager.connectPeripheral_options_(peripheral, None)
manager.stopScan()
def centralManager_didConnectPeripheral_(self, manager, peripheral):
print('Connected:', peripheral.name())
peripheral.setDelegate_(self)
peripheral.discoverServices_([M5GO_SERVICE])
def centralManager_didDisconnectPeripheral_error_(self, manager, peripheral, error):
print('Disconnected:', peripheral.name())
self.peripheral = None
manager.scanForPeripheralsWithServices_options_(None, None)
def peripheral_didDiscoverServices_(self, peripheral, error):
for service in peripheral.services():
if service.UUID() == M5GO_SERVICE:
peripheral.discoverCharacteristics_forService_([M5GO_CHARACTERISTIC], service)
break
def peripheral_didDiscoverCharacteristicsForService_error_(self, peripheral, service, error):
for characteristic in service.characteristics():
if characteristic.UUID() == M5GO_CHARACTERISTIC:
peripheral.setNotifyValue_forCharacteristic_(True, characteristic)
break
def peripheral_didUpdateValueForCharacteristic_error_(self, peripheral, characteristic, error):
if len(characteristic.value()) > 0:
value = characteristic.value().bytes().tobytes().decode('utf8')
print(value)
# ここで直接 webview.evaluate_js() を呼び出すと止まってしまうので、Queue経由で連携
env_data_queue.put(value)
def load_html():
webview.load_html(HTML)
while True:
# BLE経由で通知されたデータをQueue経由で取得
env_data = env_data_queue.get()
env = env_data.split(',')
webview.evaluate_js('updateCharts({},{},{});'.format(env[0],env[1],env[2]))
def signal_handler(signal, frame):
AppHelper.stopEventLoop()
if __name__ == '__main__':
env_data_queue = Queue()
t = Thread(target=load_html)
t.start()
central_manager = CBCentralManager.alloc()
central_manager.initWithDelegate_queue_options_(CBCentralManagerDelegate(), None, None)
# Ctrl+C で停止
signal.signal(signal.SIGINT, signal_handler)
webview.create_window('気温・湿度・気圧計', width=1000, height=1000, debug=True)
# Window が閉じられたら停止
AppHelper.stopEventLoop()