はじめに
ABEJA大田黒です。これはABEJAアドベントカレンダー2024の5日目の記事です。去年のアドベントカレンダーではこんな記事を書いていました。ハードウェアもソフトウェアも大好きな開発グループのマネージャーです。 tech-blog.abeja.asia
今回は、部屋の温度と湿度と気圧データを時系列で蓄積し、分析できる仕組みをサクッと自作したので、その話ができればと思います。
作ったもの
Fig ミニPCとセンサー
Fig Grafanaでのデータ可視化画面
ESP32 x BME280 ハードウェア準備
近年、ESP32を搭載した開発ボードが手に入りやすくなりました。中学生のお小遣いでも購入できる金額感になっています。ですが、今回はあえてESP32を搭載した基板を作成してみることにしました。※既製品を使った方がコスト・品質共に良いです。
また、温度、湿度、気圧のセンシングにはBME280を採用しました。このセンサーは、以下の特徴を持つため、非常に扱いやすいです。
- 3つの物理量(温度・湿度・気圧)を1つのデバイスで取得可能
- I2CまたはSPI通信をサポートしており、データのやり取りが簡単
ESP32を用いた回路設計
今回、基板設計にKiCad 8を利用しました。学生時代はEagleを使っていましたが、どちらも使いやすくていい感じですね。始めてKiCadを使いましたが、情報が多くインターネット上にあり、スムーズに使い始めることができました。
回路にはType-Cコネクタ+USBシリアル変換IC(FT232RL)+ESP32本体+リセット回路+I/Oコネクタが搭載されており、ESP32を動かす為の標準的な回路構成になっています。
Fig X: 自作回路 (回路図)
Fig X: 自作回路 (ボード) ※ボードデザインはご愛嬌、ということで....
Fig X: 自作回路 (3Dモデル)
FIg X: Elecrowでの基板発注
部品実装(リフロー)
Elecrowに発注してから大体10日前後で基板が到着しました。クリームはんだとチップ部品を乗せてリフロー。ArduinoIDEを用いて書き込みテストをしました。(一発で動いたので良かった・・・)
Fig 実際に届いた基板
Fig 秋月電子での部品購入
Fig クリームはんだとチップ部品を乗せる
Fig 部品を乗せてリフロー
Fig 書き込み&動作テスト
センサー仮組み
Fig BME280接続
ESP32ソフトウェア準備
昔Arduino IDEを長らく使っていましたが、今回はPlatformIOという環境を使ってVS Code上で開発をしました。今回は、定期的にBME280で測定したデータをシリアル通信経由で送信するプログラムを作りました。Adafruit様のライブラリを使用しています。
platformio.ini
; platformio.ini [env:esp32dev] platform = espressif32 board = esp32dev framework = arduino monitor_speed = 115200 lib_deps = adafruit/Adafruit Unified Sensor@^1.1.14 adafruit/Adafruit BME280 Library@^2.2.4
src/main.cpp
#include <Wire.h> #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> Adafruit_BME280 bme; float temp; float pressure; float humid; void setup() { Serial.begin(115200); bool status; status = bme.begin(0x76); while (!status) { Serial.println("Failed to open BME280"); delay(1000); } } void loop() { temp=bme.readTemperature(); pressure=bme.readPressure() / 100.0; humid=bme.readHumidity(); Serial.print("TEMP="); Serial.print(temp); Serial.print(";"); Serial.print("PRESSURE="); Serial.print(pressure); Serial.print(";"); Serial.print("HUMIDITY="); Serial.print(humid); Serial.print(";"); Serial.println(); delay(1000); }
データ処理部分
データをPrometheusで格納しGrafanaで表示するにあたって、下記のような仕組みを作りました。
Fig データの流れ
pyserial
を用いてESP32からのシリアル通信データを受信。パースして後段のRedisにいれる。Redisをメッセージキユーとして簡易利用
- RabbitMQでも何でもOK
Flaskを用いて、Redisの中身を返却する自作Exporterを作成
- Prometheusが認識できるフォーマットが返却できるならFastAPIでも何でもOK
初期フェーズの試行錯誤では、シリアル通信部分とWebアプリケーションフレームワークを分けていませんでした。ところが、噛み合わせ問題(おそらくGILに起因??)にあたってしまい、うまくデータのやり取りができない事がありました。シリアル通信部分とWebアプリケーションフレームワークを分離した結果、うまくいったので上記のような構成にしました。
受信部 (receive.py)
シリアル通信の受信内容をパース&メッセージキュー(Redis)に格納するために、下記のようなコードを作りました。
import traceback import redis import serial import time # 動作環境やESP32ボード側に合わせる port = '/dev/ttyUSB0' baudrate = 115200 # パースしてRedisにつっこむ def write(r, s): key, value = s.split('=') if key == 'TEMP': LATEST_TEMP = float(value) r.set('temp', LATEST_TEMP) if key == 'PRESSURE': LATEST_PRESSURE = float(value) r.set('pressure', LATEST_PRESSURE) if key == 'HUMIDITY': LATEST_HUMIDITY = float(value) r.set('humidity', LATEST_HUMIDITY) print(key, value) def main(r): with serial.Serial(port, baudrate, timeout=1) as ser: print(f'Listening on {port} at {baudrate} baud...') while True: if ser.in_waiting <= 0: continue data = ser.readline().decode('utf-8').strip() print(f'Received: [{data}]') for s in data.split(';'): if s == '': continue try: write(r, s) except Exception: traceback.print_exc() time.sleep(0.1) if __name__ == '__main__': r = redis.Redis(host='localhost', port=6379, db=0) main(r)
自作Exporter (exporter.py)
メッセージキューに格納されたデータをPrometheus側に渡す為に、下記のようなメトリクスデータ取得用エンドポイントを作成しました。
- 依存パッケージ
追記:prometheus_client
にstart_http_server
が用意されているので、Flaskいらなかったかもしれない・・・
from flask import Flask, Response from prometheus_client import Gauge, generate_latest import logging import redis r = redis.Redis(host='localhost', port=6379, db=0) logger = logging.getLogger(__name__) app = Flask(__name__) CONTENT_TYPE_LATEST = str('text/plain; version=0.0.4; charset=utf-8') ROOM_TEMP_GAUGE = Gauge( 'current_room_temp', 'Room Temp', ['site_name'] ) ROOM_PRESSURE_GAUGE = Gauge( 'current_room_pressure', 'Room Pressure', ['site_name'] ) ROOM_HUMIDITY_GAUGE = Gauge( 'current_room_humidity', 'Room Humidity', ['site_name'] ) @app.route('/metrics', methods=['GET']) def get_data(): """Returns all data as plaintext.""" label = 'my-house' value = r.get('temp') if value is not None: value = float(value) ROOM_TEMP_GAUGE.labels(label).set(value) value = r.get('pressure') if value is not None: value = float(value) ROOM_PRESSURE_GAUGE.labels(label).set(value) value = r.get('humidity') if value is not None: value = float(value) ROOM_HUMIDITY_GAUGE.labels(label).set(value) return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0')
Prometheus x Grafana x Redisの立ち上げ
今回、Linux搭載ミニPCを準備し、下記のようなコンテナを動かしています。
prometheus.ymlの準備
global: scrape_interval: 15s scrape_configs: - job_name: 'prometheus' scrape_interval: 5s static_configs: - targets: ['192.168.XXX.XXX:5000']
docker-compose.yamlの準備
services: prometheus: image: prom/prometheus container_name: prometheus restart: unless-stopped ports: - '9090:9090' volumes: - './prometheus.yml:/etc/prometheus/prometheus.yml' - 'prometheus-data:/prometheus' networks: - monitoring command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--storage.tsdb.retention.time=365d' - '--web.console.libraries=/usr/share/prometheus/console_libraries' - '--web.console.templates=/usr/share/prometheus/consoles' grafana: image: grafana/grafana-oss container_name: grafana restart: unless-stopped ports: - '3011:3000' volumes: - 'grafana-data:/var/lib/grafana' networks: - monitoring redis: image: "redis:latest" container_name: redis restart: unless-stopped ports: - "6379:6379" volumes: - "redis-data:/data" networks: - monitoring networks: monitoring: driver: bridge volumes: prometheus-data: {} grafana-data: {} redis-data: {}
コンテナ群の立ち上げ
edge@edge-Default-string:~/prometheus$ docker compose up [+] Running 3/0 ✔ Container prometheus Running 0.0s ✔ Container redis Running 0.0s ✔ Container grafana Running 0.0s Attaching to grafana, prometheus, redis edge@edge-Default-string:~/prometheus$ docker compose ps NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS grafana grafana/grafana-oss "/run.sh" grafana 2 months ago Up 7 weeks 0.0.0.0:3011->3000/tcp, :::3011->3000/tcp prometheus prom/prometheus "/bin/prometheus --c…" prometheus 4 days ago Up 4 days 0.0.0.0:9090->9090/tcp, :::9090->9090/tcp redis redis:latest "docker-entrypoint.s…" redis 2 months ago Up 7 weeks 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp
最後に
今回の記事では、部屋の温度・湿度・気圧をモニタリングする仕組みについて解説しました。夏頃に作成したので、台風一過を見守ったりしてました。あれから数ヶ月立ちますが、まだ元気に動いています。
PrometheusやGrafanaは、サーバー監視の文脈で語られることが多いです。温度などの物理量も一種のメトリクスとみなすと、こういったユースケースで使うのも悪くなさそうですね。「時系列データを蓄積して、サクっと見たい」というシーンにおいては、悪くない選択肢であると感じました。
We Are Hiring!
ABEJAは、テクノロジーの社会実装に取り組んでいます。 技術はもちろん、技術をどのようにして社会やビジネスに組み込んでいくかを考えるのが好きな方は、下記採用ページからエントリーください! (新卒の方やインターンシップのエントリーもお待ちしております!)
特に下記ポジションの募集を強化しています!ぜひご覧ください!
プラットフォームグループ:シニアソフトウェアエンジニア | 株式会社ABEJA