LoginSignup
5
6

More than 3 years have passed since last update.

M5GO(M5Stack)とMac(Python)をBLEで接続し環境センサー値をGoogle Chartsで表示

Posted at

概要

  • 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_Env_BLE_Notify.png
(センサーのDHT12が不調のようで湿度の値が90%と高くなっていますが、実際は55%程度です)

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()
5
6
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
5
6