📝

M5StickC plus2とセンサーで環境データを取得してみた

2024/03/28に公開

はじめに

旅行・出張で長期間自宅を離れる時、自室の水槽の状態が気になる、ということで外出先からでも環境データを取得できるシステムを作りたいと思ったのが発端で今回タイトルの通りとなりました。
最終的には、外出先からスマートフォンで環境データや画像の取得、季節変動を再現するような長期間での水温・照度の管理、自動給餌等を行いたい、となんとなく思っていますが、今回は1歩目として水温を取得して可視化するまでをやってみました。

設計

今回の「水温を取得して自宅PC上でデータ収集可視化を行う」という単純な要件に対して、
マイコンの選択としては、センサー・電子回路等に関する経験と知識がゼロの人間でも使いやすそうな、比較的安価でセンサーモジュールの選択肢も多いM5StickC plus2を選択しました。
DBとダッシュボードツールについては、以下記事を参考にInfluxDBとGrafanaを利用することとし、
https://zenn.dev/tanny/articles/a5c0fa5c2230a7
全体の構成図としてはこのようになりました。

データ収集可視化基盤はDockerコンテナ上に構築するとして、以上それぞれの役割をまとめると
・水温センサーから取得した値をM5StickC plus2からUDPでpython-api-proxyに送る
・python-api-proxyはUDPパケットを読み取りInfluxDBのAPIを叩く
・InfluxDBでデータを保存
・grafanaでデータを可視化
となります。

準備

マイコンとセンサー、各種パーツが必要となったので、以下を準備しました。

  1. マイコン: M5StackC plus2 ¥3,938
  2. 水温センサー: Gravity - 防水温度センサキット (DS18B20) ¥1,430
  3. ケーブル: GROVE - 4ピン - ジャンパオスケーブル(5本セット)¥396

計 ¥5,764

構築

M5stickC plus2

基本的にはArduinoIDEのスケッチ例を参考にプログラムを書きました。水温の測定値を10秒に1回UDPで送信するという処理ですが、Wi-Fiの接続に関する部分やM5stickの液晶に測定値・IPアドレスを表示する部分も含め、コードは以下の通りです。

temp_udp.ino
#include <M5Unified.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <M5GFX.h>
#include <M5_DLight.h>

#define ONE_WIRE_BUS 33
#define TIME_TO_SLEEP 10    //データ送信間隔(秒)

WiFiUDP wifiUdp; 
const char *pc_addr = "192.168.1.100";  //送信先アドレス
const int pc_port = 5000; //送信先のポート
const int my_port = 50008;  //自身のポート


OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

WiFiClient client;
M5_DLight sensor;

const char* ssid = "************";
const char* password = "***********";

void setup() {
  M5.begin();
  M5.Lcd.setTextSize(2);
  M5.Lcd.setRotation(3);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED){
      delay(500);
      M5.Lcd.print('.');
  }
  M5.Lcd.print("\r\nWiFi connected\r\nIP address:\r\n ");
  M5.Lcd.println(WiFi.localIP());

  Serial.println("Dallas Temperature IC Control Library Demo");
  sensors.begin();
  sensor.begin(&Wire, 0, 26);
  sensor.setMode(CONTINUOUSLY_H_RESOLUTION_MODE);
}

void loop() { 
  sensors.requestTemperatures(); // Send the command to get temperatures
  float temp = sensors.getTempCByIndex(0);
  if(temp != DEVICE_DISCONNECTED_C) {
    /* LCD表示 */
    M5.Lcd.setCursor(40, 80);
    M5.Lcd.printf("Temp: %4.1f 'C\r\n", temp);
    /* UDP送信 */
    wifiUdp.beginPacket(pc_addr, pc_port);
    wifiUdp.printf("{ \"temp\": %4.1f}", temp);
    wifiUdp.endPacket();
  } else {
    Serial.println("Error: Could not read temperature data");
  }


  delay(TIME_TO_SLEEP * 1000);
}

python-api-proxy

M5Stickより5000番ポートに送信されたデータを読み取り、ラインプロトコル形式でInfluxDBに投げます。
API部分については公式のクライアントライブラリが提供されており、サンプルコードも掲載されているため、UDPで送られてくるデータを読み取りラインプロトコル形式にする部分を追加するだけで実装できました。

api_proxy.py
import time
import influxdb_client
from influxdb_client.client.write_api import SYNCHRONOUS
import socket
import json

bucket = "my-bucket"
org = "my-org"
token = "my-token"
url="http://influxdb:8086"
address = ('', 5000)
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #udpという名前のUDPソケット生成
print('create socket')
udp.bind(address) #udpというソケットにaddressを紐づける

client = influxdb_client.InfluxDBClient(url=url,
                                        token=token,
                                        org=org
)
write_api = client.write_api(write_options=SYNCHRONOUS)


while True:
    try:
        print('Waiting message')
        message, cli_addr = udp.recvfrom(1024)
        message = message.decode(encoding='utf-8')
        print(f'Received message is [{message}]')
        d = json.loads(message)
        p = influxdb_client.Point("aquarium").tag("location", "tank_1").field("Temperature", d["temp"])
        write_api.write(bucket=bucket, org=org, record=p)
        print('Send data to influxdb')

        # Clientが受信待ちになるまで待つため
        time.sleep(1)

        # Clientへ受信完了messageを送信
        print('Send response to Client')
        udp.sendto('Success to receive message'.encode(encoding='utf-8'), cli_addr)

    except KeyboardInterrupt:
        print ('\n . . .\n')
        udp.close()
        break

print("Finish_udp")

InfluxDB, grafana

API Proxyは実装できたので、次はInfluxDBとgrafanaの構築です。
これらはapi_proxyを実行するためのコンテナも含めて以下のようにdocker-compose.ymlを用意してコンテナを作成するのみです。
基本的に各コンテナのIPアドレスを指定する必要はありませんが、grafanaとInfluxDBの連携においてIPアドレスを入力する部分があるため、コンテナを作成するたびにIPアドレスを確認するのが不便であったことから今回はIPアドレスを固定しました。

docker-compose.yml
version: "3"
services:
  python3:
    restart: always
    build:
      context: .
      dockerfile: Dockerfile
    container_name: 'python-api-proxy'
    ports:
      - 5000:5000/udp
    working_dir: '/root/opt'
    tty: true
    volumes:
      - ./opt:/root/opt
    networks:
      db_network:
        ipv4_address: 172.25.1.100

  influxdb:
    image: influxdb:2.6.1
    container_name: influxdb
    volumes:
      - ./docker/influxdb/data:/var/lib/influxdb2
      - ./docker/influxdb/config:/etc/influxdb2
    ports:
      - 8086:8086
    networks:
      db_network:
        ipv4_address: 172.25.1.101
    environment:
      - DOCKER_INFLUXDB_INIT_MODE=setup
      - DOCKER_INFLUXDB_INIT_USERNAME=admin
      - DOCKER_INFLUXDB_INIT_PASSWORD=password
      - DOCKER_INFLUXDB_INIT_ORG=my-org
      - DOCKER_INFLUXDB_INIT_BUCKET=my-bucket
      - INFLUXDB_HTTP_FLUX_ENABLED=true
      - DOCKER_INFLUXDB_INIT_RETENTION=1w
      - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-token

  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - 3000:3000
    volumes:
      - ./docker/grafana/data:/var/lib/grafana
    depends_on:
      - influxdb
    networks:
      db_network:
        ipv4_address: 172.25.1.102
    environment:
      - GF_SERVER_ROOT_URL=http://localhost:8080
      - GF_SECURITY_ADMIN_PASSWORD=admin

volumes:
  grafana-volume:
  influxdb-volume:

networks:
  db_network:
    driver: bridge
    name: db_network
    ipam:
      driver: default
      config:
        - subnet: 172.25.1.0/24

api_proxy.pyを実行するコンテナのDockerfileは以下です。コンテナ起動と同時にapi_proxy.pyが実行されるようにしています。

Dockerfile
FROM python:3.11
USER root

RUN pip install --upgrade pip

COPY requirements.txt /root/opt/
RUN pip install -r /root/opt/requirements.txt
ENTRYPOINT ["python", "api_proxy.py"]
requirement.txt
influxdb-client

初め、コンテナ作成直後にコンテナ'python-api-proxy'が停止してしまう問題にぶつかりました。どうやらコンテナは起動した後にすぐに終了するのが通常の挙動であり、

ENTRYPOINT ["python", "api_proxy.py"]

と指定したとしても、この場合api_proxy.pyを実行するとコンテナが停止するようです。
したがって、docker-compose.ymlに

tty: true

を追加しました。

ダッシュボードの作成

前述した手順までで既にデータがInfluxDBに継続的に保存されているので、grafanaとInfluxDBを連携し、grafanaでダッシュボードを作成します。
ブラウザから
InfluxDBは http://localhost:8086
grafanaは http://localhost:3000
でそれぞれアクセス可能です。
まず、InfluxDBにログインします。USERNAME/PASSWORDは、docker-compose.ymlで指定したadmin/passwordを入力します。
次にbucketから下図のようにmeasurement, field, locationを選択してsubmitボタンを押すと、データが保存されていることが確認できます。InfluxDBでもデータ表示期間の選択等は一定程度のカスタマイズは可能となっています。

InfluxDBにデータが保存されていることが確認出来たら、次にgrafanaとInfluxDBの連携を行います。
grafanaにログインし、Data Sourcesを選択します。入力値は以下の通りです。

当初URLの入力値を http://localhost:8086としたところ、データの連携ができずに詰まりました。
ブラウザからInfluxDBにアクセスする場合は、ホストの8086番ポートを通してInfluxDBのコンテナの8086番ポートにアクセスするため当然接続可能ですが、このURLの値に関してはgrafanaのコンテナ内からのアクセスになるため、http://localhost:8086とすると、grafanaのコンテナ内の8086番ポートへのアクセスとなりInfluxDBのコンテナに接続できません。この場合は、同一のdocker networkに属するコンテナ間の通信であるため、URLにはInfluxDBのコンテナのIPアドレス172.25.1.101の8086番ポートを指定することでデータ連携が可能になります。

InfluxDBとの連携が完了したらあとは適当にダッシュボードをいじることで無事このような水温の可視化が可能となりました。

さいごに

今回は、センサーで取得したデータを可視化するところまでを一通り行い、勉強になる部分は非常に多かったですが、実用性という視点で見ると、ただ水温を水槽の横のPCで見られるだけという大げさな水温計でしかない状態です。今後、センサーの追加やヒーター・ライトとの連携、自動給餌、映像取得といった機能の追加や、データ収集基盤をAWS等クラウド上に構築するといった方向で進めていきたいと思います。

Discussion