必要に迫られたので作ってみた。 初めて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操作にdataset
とmysql-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
を参照すべし