RaspberryPIのCPU温度を定期的に記録してチャート表示する

必要に迫られたので作ってみた。 初めてbackendにflaskを使ったので、それをサービスとして動作させるまでの手順を残しておく。

スクリプトや設定ファイルの設置パスはここでは/home/pi/projects/tempとして作業する。

リポジトリを公開しています

今回使う環境とアプリのバージョン

定期的にCPU温度の取得しDBへ登録、Webサーバーでそれを公開するという流れで、今回使うものは以下の通り。

$ uname -a
Linux raspberrypi 5.15.79-v7l+ #1600 SMP Fri Nov 18 18:23:26 GMT 2022 armv7l GNU/Linux
$ python --version
Python 3.11.0
$ mariadb --version
mariadb  Ver 15.1 Distrib 10.3.36-MariaDB, for debian-linux-gnueabihf (armv8l) using readline 5.2
$ nginx -v
nginx version: nginx/1.14.2

必要なPythonモジュールのインストール

DB操作にdatasetmysql-connector、Webサイトのルーティングにflaskを使う。 複数バージョンのpythonが導入されている環境では、pythonのーmオプション経由でpipを動かした方がうっかり別バージョンのpipに導入されてModuleNotFoundErrorに悩まされずに済む。

$ python -m pip install dataset
$ python -m pip install mysql-connector
$ python -m pip install flask

CPU温度を取得するスクリプト

CPU温度を取得しDBへ登録するだけのスクリプトで、これはcronで定期的に呼び出す。DB接続用の設定は参照用のスクリプトからも利用する為に外部のsettings.jsonにまとめ、cronからの実行時にはコマンドライン引数で設定ファイルのパスを指定する。

$ vi temp_recorder.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import json
import dataset
import datetime
from subprocess import getoutput

args = sys.argv
setttings_path = './settings.json'
if len(args) == 2:
    setttings_path = args[1]

settings = json.load(open(setttings_path, 'r'))

def cpu_temp():
    temp = getoutput('vcgencmd measure_temp')
    temp2 = temp.split('=')
    Cputemp = temp2[1].split("'")
    return float(Cputemp[0])

def main():
    db = dataset.connect(f"{settings['adaptor']}://{settings['user']}:{settings['password']}@{settings['host']}/{settings['dbname']}")
    temp_record = db["temp_record"]
    temp_record.create_column('recorded_at', db.types.datetime, unique=True, nullable=False, default='NOW()')
    temp_record.insert({"temp": cpu_temp()})

if __name__ == '__main__':
    main()

これをcronで5分毎(任意)に実行する。cronは実行ユーザーのホームディレクトリをカレントとして動作するが、念のためフルパスで指定するのが良い。

$ touch temp_recorder.log
$ crontab -e
*/5 * * * * /home/pi/projects/temp/temp_recorder.py /home/pi/projects/temp/settings.json >> /home/pi/projects/temp/temp_recorder.log 2>&1

データ表示用WEBサイトの準備

まずflask側のスクリプトを用意する。

$ vi app.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

import os
import json
import dataset
from flask import Flask

settings = json.load(open('./settings.json', 'r'))

app = Flask(__name__, static_folder='static', static_url_path='/')
app.config['SECRET_KEY'] = os.urandom(24)
app.config['JSON_AS_ASCII'] = False

@app.route('/')
def index():
    return app.send_static_file('index.html')

@app.route('/timeline')
def timeline():
    res = []
    db = dataset.connect(f"{settings['adaptor']}://{settings['user']}:{settings['password']}@{settings['host']}/{settings['dbname']}")
    temp_record = db["temp_record"]
    results = temp_record.find(order_by=["-recorded_at"])
    for record in results:
        res.append({'recorded_at': record['recorded_at'], 'temp': record['temp']})
    return res

if __name__ == "__main__":
    app.run(port=5000, debug=True)

本システムではDBに記録されているログをブラウザ側でチャートに変換するため、ここではインデックスページとログを返すAPIの記述のみを行う。Flaskで実行する為スクリプトファイルの名前はデフォルトに合わせておくのが楽だろう。

次はindex.html

$ vi static/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CPU Temp</title>
    <script type="module" src="view.js" async defer></script>
    <style>body {background-color: black;}</style>
</head>
<body>
    <canvas id="timeline"></canvas>
</body>
</html>

カレントディレクトリに直接作っても良いが、一応Flaskの流儀に合わせカレントに作ったstaticディレクトリ内に作成する。

次はチャート表示を行うJavaScript。

$ vi static/view.js
addEventListener("load", () => {
    let nowTime = undefined;
    const f = timestamp => {
        if (nowTime === undefined) nowTime = timestamp;
        const elapse = timestamp - nowTime;
        if (elapse >= 1000 * 60 * 5) {
            updateGraph();
            nowTime = timestamp;
        }
        requestAnimationFrame(f);
    };
    updateGraph();
    requestAnimationFrame(f);
});

function updateGraph() {
    fetch("./timeline")
        .then(r => r.json())
        .then(json => {
            const timeline = document.getElementById("timeline");
            const width = (60 / 5) * 24;
            const height = 100;
            const scale = 4;
            timeline.width = width * scale;
            timeline.height = height * scale;
            timeline.style.maxWidth = "100%";

            const ctx = timeline.getContext("2d");
            if (!ctx) throw new TypeError("No Context");

            const bgColor = "rgb(4, 4, 8)";
            const gridColor = "rgb(32, 32, 64)";
            const textColor = "white";

            ctx.fillStyle = bgColor;
            ctx.fillRect(0, 0, width * scale, height * scale);

            // horizon line
            ctx.strokeStyle = gridColor;
            for (let y = 0; y <= height; y += 10) {
                ctx.beginPath();
                ctx.moveTo(0, y * scale);
                ctx.lineTo(width * scale, y * scale);
                ctx.stroke();

                ctx.fillStyle = textColor;
                ctx.fillText(`${("" + (height - y)).padStart(3)}℃`, 8, y * scale);
            }

            // vertical line
            let x = 0;
            let hour = -1;
            for (const record of json) {
                const xx = (width - x) * scale;

                if (record.temp >= 40) {
                    ctx.strokeStyle = record.temp >= 60 ? `orange` : `yellow`;
                    ctx.beginPath();
                    ctx.moveTo(xx, 0);
                    ctx.lineTo(xx, height * scale);
                    ctx.stroke();
                }

                const date = new Date(record.recorded_at.replace(/^(.+) GMT/, "$1"));
                const nowHours = date.getHours();
                if (hour != nowHours) {
                    const dateStr = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
                    const timeStr = `${("" + date.getHours()).padStart(2, '0')}:${("" + date.getMinutes()).padStart(2, '0')}`;

                    ctx.strokeStyle = gridColor;
                    ctx.beginPath();
                    ctx.moveTo(xx, 0);
                    ctx.lineTo(xx, height * scale);
                    ctx.stroke();

                    ctx.save();
                    ctx.translate(xx - 10, 0);
                    ctx.rotate(90 * Math.PI / 180);
                    ctx.fillText(`${dateStr} ${timeStr}`, 0, 0);
                    ctx.restore();

                    hour = nowHours;
                }
                ++x;
                if (width - x < 0)
                    break;
            }

            // temp chart
            x = 0;
            ctx.strokeStyle = "red";
            ctx.beginPath();
            for (const record of json) {
                const xx = (width - x) * scale;
                const yy = (height - record.temp) * scale;
                if (x === 0)
                    ctx.moveTo(xx, yy);
                else
                    ctx.lineTo(xx, yy);
                ++x;
                if (width - x < 0)
                    break;
            }
            ctx.stroke();
        })
        .catch(alert);
}

最後にNginxでこれらを表示するための設定を行う。

$ sudo vi /etc/nginx/sites-enabled/temp_view.conf
server {
    listen 80;
    listen [::]:80;

    root /home/pi/projects/temp;

    access_log /var/log/nginx/temp.access.log;
    error_log /var/log/nginx/temp.error.log;

    location / {
        proxy_pass http://localhost:5000;
    }
}

flaskを起動し、ブラウザからIPアドレス直打ちするなどしてチャートが表示されることを確認する。

$ python -m flask run

※当然ながらログが溜まっていなければチャートは表示されない。

サービスとして登録する

電源ON/OFFに合わせてサービスの起動・停止が行われるようにする場合はsystemd関連のファイルを所定の場所に設置する。

まず起動時のパラメータなどの環境変数を/etc/defaultに作成する。

$ sudo vi /etc/default/temp_view
FLASK_APP="/home/pi/projects/temp/app.py"
FLASK_RUN_PORT=5000
LC_CTYPE="en_US.UTF-8"

そしてサービスとしての定義ファイルを/etc/systemd/systemに作成する

$ sudo vi /etc/systemd/system/temp_view.service
[Unit]
Description=CPU Temp Monitor
After=network.target

[Service]
User=pi
Group=pi
EnvironmentFile=/etc/default/temp_view
WorkingDirectory=/home/pi/projects/temp
ExecStart=/usr/bin/python -m flask run

[Install]
WantedBy=multi-user.target

これでsystemctlコマンドで操作が行えるようになる。

$ sudo systemctl enable temp_view

systemctlで使用する主なサブコマンドについては--helpを参照すべし