ABEJA Tech Blog

中の人の興味のある情報を発信していきます

ESP32 x Prometheusで温度・湿度・気圧データを蓄積・可視化する

はじめに

ABEJA大田黒です。これはABEJAアドベントカレンダー2024の5日目の記事です。去年のアドベントカレンダーではこんな記事を書いていました。ハードウェアもソフトウェアも大好きな開発グループのマネージャーです。 tech-blog.abeja.asia

今回は、部屋の温度と湿度と気圧データを時系列で蓄積し、分析できる仕組みをサクッと自作したので、その話ができればと思います。

作ったもの

Fig ミニPCとセンサー

Fig Grafanaでのデータ可視化画面

ESP32 x BME280 ハードウェア準備

近年、ESP32を搭載した開発ボードが手に入りやすくなりました。中学生のお小遣いでも購入できる金額感になっています。ですが、今回はあえてESP32を搭載した基板を作成してみることにしました。※既製品を使った方がコスト・品質共に良いです。

akizukidenshi.com

また、温度、湿度、気圧のセンシングにはBME280を採用しました。このセンサーは、以下の特徴を持つため、非常に扱いやすいです。

  • 3つの物理量(温度・湿度・気圧)を1つのデバイスで取得可能
  • I2CまたはSPI通信をサポートしており、データのやり取りが簡単

akizukidenshi.com

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.org

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_clientstart_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は、テクノロジーの社会実装に取り組んでいます。 技術はもちろん、技術をどのようにして社会やビジネスに組み込んでいくかを考えるのが好きな方は、下記採用ページからエントリーください! (新卒の方やインターンシップのエントリーもお待ちしております!)

careers.abejainc.com

特に下記ポジションの募集を強化しています!ぜひご覧ください!

プラットフォームグループ:シニアソフトウェアエンジニア | 株式会社ABEJA

トランスフォーメーション領域:ソフトウェアエンジニア(リードクラス) | 株式会社ABEJA

トランスフォーメーション領域:データサイエンティスト(シニアクラス) | 株式会社ABEJA