M5GO(M5Stack)をWeb Serverにして環境センサー値をGoogle Chartsで表示

【2022/03/02追記】
最近 LGTM を頂いたので最新の M5Stack の UI.Flow Firmware (v1.9.2) で動くかどうか確認しました。
そのまま動かそうとすると以下の問題があります。
- ```from microWebSrv import MicroWebSrv``` でエラー ```ImportError: no module named 'microWebSrv'```
「microWebSrv」というモジュールは存在しますが MicroWebSrv 配下に移動しているためエラーになってしまいます。また、microWebSrv 内で microWebSocket というモジュールもインポートしているため、単に
```
from MicroWebSrv.microWebSrv import MicroWebSrv
```
としても microWebSrv 内で microWebSocket を参照することができません。対応としてはプログラムの先頭で以下のように MicroWebSrv 配下をライブラリのパスに追加することで参照可能となります。
```
import sys
sys.path.append('MicroWebSrv')
```
使用されている microWebSrv.py のソースは以下になります。

https://github.com/m5stack/M5Stack_MicroPython/blob/master/MicroPython_BUILD/components/micropython/esp32/modules/microWebSrv.py

- ```import logging``` でエラー ```ImportError: no module named 'logging'```
Firmware のバージョンアップで logging モジュールが無くなってしまいました。単純な確認用のログ出力に使っているだけなので、print() での置き換えで充分でしょうし、logging モジュールを使いたい場合は以下の logging.py 追加するということも可能です。

https://github.com/m5stack/M5Stack_MicroPython/blob/master/MicroPython_BUILD/components/micropython/esp32/modules/logging.py


【2020/02/16更新】最新 Firmware（M5 UI.Flow v1.4.4）の MicroPython に合わせて修正

- wifisetup → wifiCfg (Firmware v1.3で変更)
- 各関数内で追加の import や global 宣言が不要になっていたので、関連するコメントおよび宣言を削除(Firmware v1.1.2で変更)


# 概要

- M5GO(M5Stack)のクラウド環境M5Flow経由でプログラムを実行する
- M5GO付属の環境センサー（DHT12、BMP280）から気温・湿度・気圧を取得
- M5GOの液晶画面にQRコードでアクセス用URLを表示する
- M5GO(M5Stack)をWeb Serverとして使用しWeb Socketで取得値を返す
- ブラウザ側ではGoogle Chartsを使用して気温・湿度・気圧をそれぞれメータ表示

## 環境

- M5GO (Firmware M5 UI.Flow v1.4.4)
- M5Flow を使ってクラウド側から実行

## 実行結果

M5GOの画面上に用事されたアクセス用QRコードを使ってWebページを開くとGoogle Chartsを使って気温・湿度・気圧が表示される。

![IMG_0113-1.png](https://qiita-image-store.s3.amazonaws.com/0/133064/63a35d29-74b1-77ca-ea9b-27b07d501c17.png)


## プログラム（含indexページ）

#### 特記事項
- M5Flow上でMicroPythonのプログラムを書く際、各関数内で使用するグローバル変数は global によって明記する必要がありました。（ただし、APP.LISTに登録して実行する場合はその制約は無し）
- M5GO付属の環境センサーはDHT12(気温・湿度)とBMP280(気温・気圧)でどちらも気温を測定できますが、値を比較するとBMP280の方がより実際の気温に近い値が得られているようなので、BMP280の気温を使用しています。
- 使用した Web Server/Web Socket のライブラリは複数スレッドでの使用が可能なものですが、スレッドを有効にして実行すると落ちてしまうので、スレッドを無効にしています。

```python:main.py
from m5stack     import lcd
from microWebSrv import MicroWebSrv
from dht12       import DHT12
from bmp280      import BMP280
from machine     import Timer
import network
import i2c_bus
import json
import logging

def _httpHandlerIndexGet(httpClient, httpResponse):
    httpResponse.WriteResponseOk(headers		 = None,
                                 contentType	 = "text/html",
                                 contentCharset  = "UTF-8",
                                 content 		 = index_content)

def _acceptWebSocketCallback(webSocket, httpClient) :
    logger.debug("WS ACCEPT")
    webSocket.RecvTextCallback   = _recvTextCallback
    webSocket.ClosedCallback 	 = _closedCallback
    ws_list.append(webSocket)

def _recvTextCallback(webSocket, msg) :
    logger.debug("WS RECV TEXT : %s" % msg)

def _closedCallback(webSocket) :
    logger.debug("WS CLOSED")
    ws_list.remove(webSocket)

def send_env_data(timer):
    env_data = {}  # Store data in dict
    try:
        # DHT12から気温と湿度を取得
        # ただし気温に関してはBMP280の方がより正しい値が取得できているようなので
        # DHT12の気温は使用していない
        dht12.measure()
        dht12_h = dht12.humidity()
        dht12_t = dht12.temperature()

        # BMP280から気温と気圧を取得
        bmp280_t, bmp280_p = bmp280.values

        # 液晶にも表示する
        lcd.print('{:.2f}C  '.format(bmp280_t),   5, 20) # BMP280の気温
        lcd.print('{:.2f}%  '.format(dht12_t),    5, 50) # DHT12の湿度
        lcd.print('{:.2f}hPa  '.format(bmp280_p), 5, 80) # BMP280の気圧

        env_data['temperature'] = bmp280_t
        env_data['humidity']    = dht12_t
        env_data['pressure']    = bmp280_p
        logger.debug(env_data)
        for ws in ws_list:
            if not ws.IsClosed():
                # ブラウザへはjson形式にて送信
                if not ws.SendText(json.dumps(env_data)):
                    logger.error('webSocket.SendText error')
    except Exception as e:
        # 時々I2C bus error が起きる。
        logger.error('Exception: ', e)
    gc.collect()


# M5 UI.Flow からの実行ではなく、APP.LIST に登録して実行する場合は、
# プログラム内でネットワーク接続を行う必要がある。
# Connect network
# 
# UI.Flow 1.2 以前は以下
# import wifisetup
# wifisetup.auto_connect()
#
# UI.Flow 1.3 以降は以下
import wifiCfg
wifiCfg.autoConnect(lcdShow=True)

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

lcd.setBrightness(255)
lcd.font(lcd.FONT_DejaVu18)
lcd.clear()

i2c    = i2c_bus.get(i2c_bus.M_BUS)
dht12  = DHT12(i2c)
bmp280 = BMP280(i2c)

sta_if = network.WLAN(network.STA_IF)
url = 'http://' + sta_if.ifconfig()[0] # ip address
logger.info(url)
lcd.qrcode(url, 135, 20, 175)

ws_list = [] # Active WebSocket list
timer = Timer(1)
timer.init(period=1000, mode=Timer.PERIODIC, callback=send_env_data)

routeHandlers = [("/", "GET", _httpHandlerIndexGet)]

srv = MicroWebSrv(routeHandlers=routeHandlers)
srv.MaxWebSocketRecvLen     = 256
# スレッドを使用するとエラーが発生するため使用しない
srv.WebSocketThreaded       = False
srv.AcceptWebSocketCallback = _acceptWebSocketCallback

index_content = """\
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <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 dataGaugeT;
            var dataGaugeH;
            var dataGaugeP;
            
            var chartGaugeT;
            var chartGaugeH;
            var chartGaugeP;
            
            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]
                    ]);
                
                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'));
                
                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();
                
                if (t !== undefined) {
                    dataGaugeT.setValue(0, 1, t);
                    dataGaugeH.setValue(0, 1, h);
                    dataGaugeP.setValue(0, 1, p);
                }
                formatterT.format(dataGaugeT, 1);
                formatterH.format(dataGaugeH, 1);
                formatterP.format(dataGaugeP, 1);
                
                chartGaugeT.draw(dataGaugeT, optionGaugeT);
                chartGaugeH.draw(dataGaugeH, optionGaugeH);
                chartGaugeP.draw(dataGaugeP, optionGaugeP);
            }

            var websocket;
            var sendTimer;
            function dummySend() {
                if (websocket.readyState === WebSocket.OPEN) {
                    console.log("send test");
                    websocket.send("test");
                }
            }
            function connectM5GO()
            {
                var wsUri           = "ws://" + window.location.hostname;
                console.log("Connection to " + wsUri + "...");
                websocket           = new WebSocket(wsUri);
                websocket.onopen    = function(event) { onOpen    (event) };
                websocket.onclose   = function(event) { onClose   (event) };
                websocket.onmessage = function(event) { onMessage (event) };
                websocket.onerror   = function(event) { onError   (event) };
            }
            function onOpen(event)
            {
                console.log("on Open");
                document.getElementById('websocket_status').innerHTML = "M5GO接続状態：接続";

                sendTimer = setInterval(function() {
                    dummySend();
                }, 10000);
            }
            function onClose(event)
            {
                clearInterval(sendTimer);
                console.log("on Close");
                document.getElementById('websocket_status').innerHTML = "M5GO接続状態：切断";
            }
            function onMessage(event)
            {
                console.log("on Message: " + event.data);
                var env_data = JSON.parse(event.data);
                updateCharts(Number(env_data.temperature), Number(env_data.humidity), Number(env_data.pressure));
            }
            function onError(event)
            {
                console.log("on Error: " + event.data);
            }
            window.addEventListener("load", connectM5GO, false);
       </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="websocket_status" style="text-align: center; font-family: sans-serif;">M5GO接続状態：切断</div>
        </div>
    </body>
</html>
"""

# スレッドを使用するとエラーが発生するため使用しない
srv.Start(threaded=False)
```