gpio-fanの効果を見るため、raspi4の温度を測ってグラフにする

raspi 冷却ファン

概要

  • raspi4のgpio-fanオーバーレイを使って、SoCコアが一定の温度になったら冷却ファンを回す。
  • コア温度の上昇がどの程度なのか、また、ファンの効果がどれほどあるのかを見るために、Google Chartを使ってグラフ化する。
  • ブラウザにグラフを表示する都合で、apache2を導入。

gpio-fanオーバーレイと駆動回路

スケジューラボードの回路の話でも書いたが、raspi4の/boot/config.txtに以下のような行を追加することで、gpio-fanオーバーレイを利用することができる。

dtoverlay=gpio-fan,gpiopin=21,temp=50000

このように指定することで、SoCコアの温度が50℃に達すると、10℃下がるまで GPIO21がHIGH (約 +3.3V) になる。ただ、GPIOピンから流せる電流は規格上16mA以下とする必要があるので、直接冷却ファンを駆動することはできない。このため、電源スケジューラボードには、GPIOのロジックレベルに応じてファンを回すための以下のような回路を用意した。

gpio-fan回路

GPIO21がHIGHになるとトランジスタQ2のベースに電流が流れ込んで、コレクタ-エミッタ間がオンになりファンに電流が流れる。抵抗R4は、ファンに流れる電流を制限するためのもので、現在使っているファンでは約85mAほどの電流が流れる。R5はraspiのGPIOからの電流を制限しており、最大でも 3.3 ÷ 1000 = 3.3mAがトランジスタのベースに流れる。ショットキーダイオードのD4は、モーターを止めるときの起電力に配慮したものだが、不要かもしれない。

温度を収集するスクリプト

以下のシェルスクリプト (temp_monitor.sh) を1分間隔で実行することで、温度をCSVファイルに得ている。

#!/bin/bash

dir=/var/www/temp_monitor/
filename=${dir}temp_monitor.log
bakname=${dir}temp_monitor.bak


line=`date '+%F %H:%M:%S'`","\
`/usr/bin/vcgencmd measure_temp | /usr/bin/tr -cd '0-9.'`","\
`/usr/bin/vcgencmd measure_temp pmic | /usr/bin/tr -cd '0-9.'`","\
`/usr/bin/vcgencmd measure_volts core | /usr/bin/tr -cd '0-9.'`","\
`/usr/bin/vcgencmd get_throttled | /usr/bin/tr -cd ['0-9x\n']`

if [ "$1" = "echo" ]; then
echo $line
exit 0
fi

lines=`/usr/bin/wc -l $filename | /usr/bin/cut -f 1 -d " "`
if [ $lines -gt 2000 ]; then
  /usr/bin/cp -pf $filename $bakname
  /usr/bin/tail -n 1440 $bakname > $filename
fi
echo $line >> ${filename}
exit 0

vcgencmd コマンドはビデオコアの動作状況を得るためのコマンドで、動作内容は以下のドキュメントを参照した。 https://www.raspberrypi.org/documentation/computers/os.html#vcgencmd

このスクリプトでは、コア、PMICの温度の他に、コア電圧と 低電圧警告の有無も取得してCSVファイルに書き出している。temp_monitor.log は以下のような内容になる。

...
2021-12-11 11:05:01,39.9,38.2,0.8600,0x0
2021-12-11 11:06:01,41.3,37.3,0.8600,0x0
2021-12-11 11:07:01,43.3,39.2,0.8600,0x0
2021-12-11 11:08:01,47.2,42.0,0.8600,0x0
2021-12-11 11:09:01,49.1,43.9,0.8600,0x0
2021-12-11 11:10:01,44.8,41.1,0.8600,0x50000
2021-12-11 11:11:01,43.3,39.2,0.8600,0x50000
2021-12-11 11:12:01,42.3,38.2,0.8600,0x50000
...

ファンを回すために過大な負荷をかけたので、ちょうど低電圧警告が生じていた。kern.logを見ると以下のようになっていた。

Dec 11 11:09:17 raspberrypi kernel: [ 2353.868305] Under-voltage detected! (0x00050005)
Dec 11 11:09:23 raspberrypi kernel: [ 2360.108353] Voltage normalised (0x00000000)

低電圧警告が一度でも出ると復帰したあともは0x50000 が記録される。低電圧警告中( throttled 状態) のときは、0x50005  と記録される。

一定の行数に保つ

スクリプトの後半の以下の部分で、CSVファイルの行数が2000行に達したら、最新の1440行に切り詰めている。

lines=`/usr/bin/wc -l $filename | /usr/bin/cut -f 1 -d " "`
if [ $lines -gt 2000 ]; then
  /usr/bin/cp -pf $filename $bakname
  /usr/bin/tail -n 1440 $bakname > $filename
fi

たとえば1441行になったら1440行にするとかやってしまうと、1分おきにファイルの書き直しが生じてしまってナニなので、2000行に達してから書き直すようにした。

このraspiは、電源スケジューラボードに設定したスケジュールに従って稼働するので、実際のところ1日数時間程度しか動かない。このため、2000行に達するまでに2週間程度はかかる。

crontabで実行する

このスクリプトを定期的(1分間隔)で実行するため、ユーザー pi のcrontabに以下のように記述した。

*/1 * * * * /bin/bash temp_monitor.sh 2>&1

グラフ表示

Google Chartを使ってコアとPMICの温度を表示するようにした。室温が低いので、ファンがよく回るようコアに負荷をかけたところ、こんな具合のグラフになった(直近1時間のデータ)。

raspi温度グラフ

コア温度が指定上限に達するとすぐにファンが周りだすので、1分間隔の取得では50℃がファイル記録されることは稀。下限は40℃だが、ファンが止まるとすぐに温度が上がり始めるから、やはり40℃はなかなか記録されない。

raspi温度グラフ

冬なので? 負荷をかけるのをやめたら42℃前後に落ち着いた。夏になったらgpio-fanオーバーレイに与えるパラメータも変更して、ファンが回転を開始する温度を高くした方がいいのだろう。

グラフ表示用のshowlog.html (修正前版)

Google Chartを使ってグラフ化するために以下のようなHTMLファイルを用意した。showlog.htmlは、CSVファイルの temp_monitor.log と同じ /var/www/temp_monitor に置いている。

<HTML>
<head>
<meta charset="UTF-8"/>
<title>Raspi4 internal Temp.</title>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<style>
div#chart_div { height: 500px; width: 700px; border: 1px solid #000000; }
</style>
<script>
google.charts.load('current', {'packages':['line']});

window.onload = function() {
	document.getElementById("range_sel").value = 6;
	update_chart();
}

function show_chart(data, rows) {
	let options = {
		chart: {
			title: "raspi4 Internal Temperature (" + rows + " items)",
			subtitle: 'core and pmic (C)'
		},
		lineWidth: 1, 
		fontSize: 14

	};
	var chart = new google.charts.Line(document.getElementById("chart_div"));
	chart.draw(data, options);
}

function tempchart(disp_period_h) {
	fetch("temp_monitor.log").then(function(response) {
		return response.text().then(function(text) { 
			let lines = text.split('\n');
			if (lines.length < 1) {
				alert('no data');
				return;
			}
			let data = new google.visualization.DataTable();
			data.addRows(lines.length);
			data.addColumn("date", "time");
			data.addColumn("number", "Core");
			data.addColumn("number", "PMIC");
			let rows = 0;
			lines.forEach(function(v, i){
				let csv = v.split(',');
				let tm = new Date(csv[0]);
				if (disp_period_h == 0) {
					data.setValue(i, 0, tm);
					data.setValue(i, 1, csv[1]);
					data.setValue(i, 2, csv[2]);
					rows++
				} else if (tm > Date.now() - (disp_period_h * 60 * 60* 1000)) {
					data.setValue(i, 0, tm);
					data.setValue(i, 1, csv[1]);
					data.setValue(i, 2, csv[2]);
					rows++
				}
			});
			show_chart(data, rows);
		});
	});
}

function update_chart() {
	let period = document.getElementById("range_sel").value;
	tempchart(period);
	return 0;
}
</script>
</head>
<body>
<div  style="margin-left:8px; margin-bottom:12px; font-size:16px;vertical-align: middle;">
Display Range: 
<select id="range_sel" style="font-size:16px; ">
<option value="1">1 hour</option>
<option value="2">2 hours</option>
<option value="4">4 hours</option>
<option value="6">6 hours</option>
<option value="8">8 hours</option>
<option value="12">12 hours</option>
<option value="24">24 hours</option>
<option value="48">48 hours</option>
<option value="0">ALL</option>
</select>
<button id="bt_update" type="button" onclick="return update_chart();">Update</button>
</div>
<div id="chart_div" style="display: block;"></div>
</body>
</html>

※ このhtmlの show_chart() に含まれているスクリプトには、Material Chartと従来同様の “Classic” Line Chartの記述が混在していて、うまくありません。グラフとしては上に載せたように表示されてますが、options 内の lineWidth: 指定が無視されるなどの問題がありました。”Classic” LineChart に書き直したものを投稿の最後に追加しました。

function tempchart() が主要部分で、csvファイルを fetch() して得たresponse.text() を行ごとに分け、各行ごとにCSVを分解してコア温度とPMIC温度のみを google.visualization.DataTable() のインスタンスにセットしている。このときに、現在時刻からどれだけ遡って表示するのか ( disp_period_h に時間単位で指定) に応じて、セットするデータを選択している。

HTMLのselect/option で表示する時間を指定できるようにしている。前にも書いたように必ずしも連続したデータではないのだが、Google Chartがうまいこと表示してくれる。

Apache2が必要

RHELみたいにhttpdを導入するんだろうと思ったら、Apache2だったし、confディレクトリの構成も違っててちょっと戸惑った。中身は同じなんだろうけど。

以下の temp_monitor.conf  を、/etc/apach2/conf-enabled に格納することで、同じネットワークのPCから利用できるようにした。

Alias /temp_monitor /var/www/temp_monitor

<Location /temp_monitor>
    Order deny,allow
    Allow from all
    Allow from 127.0.0.1
</Location>

シスログについて

関係ない話に思えるが、crontabを使うようになり、しかも1分間隔で実行させると、syslogファイルなどには以下のようにcronログが大量に発生する。

....
Dec 11 12:00:01 raspberrypi CRON[3769]: (pi) CMD (/bin/bash temp_monitor.sh 2>&1)
Dec 11 12:01:01 raspberrypi CRON[3856]: (pi) CMD (/bin/bash temp_monitor.sh 2>&1)
Dec 11 12:02:01 raspberrypi CRON[3880]: (pi) CMD (/bin/bash temp_monitor.sh 2>&1)
Dec 11 12:03:01 raspberrypi CRON[3903]: (pi) CMD (/bin/bash temp_monitor.sh 2>&1)
Dec 11 12:04:01 raspberrypi CRON[3927]: (pi) CMD (/bin/bash temp_monitor.sh 2>&1)
Dec 11 12:05:01 raspberrypi CRON[3952]: (pi) CMD (/bin/bash temp_monitor.sh 2>&1)
....

また、auth.log ファイルにも以下のようなログが残る。

...
Dec 11 11:44:01 raspberrypi CRON[2056]: pam_unix(cron:session): session opened for user pi by (uid=0)
Dec 11 11:44:01 raspberrypi CRON[2056]: pam_unix(cron:session): session closed for user pi
Dec 11 11:45:01 raspberrypi CRON[2076]: pam_unix(cron:session): session opened for user root by (uid=0)
...

これはちょっと迷惑なので、/etc/rsyslog.conf を編集して「隔離」することにした。ついでに、rngd などあまり注目していないデーモンのログも隔離したり、snmpdが実行時に出すエラーを排除したりして、rsyslog.conf の主要部分は以下のようになった。

#
# First some standard log files.  Log by facility.
#
if $programname == 'snmpd' and $msg contains 'statfs' then stop
if $syslogtag startswith "CRON" then {
        action(type="omfile" file="/var/log/cron.log")
        stop
}
if $syslogtag startswith "rngd" then {
        action(type="omfile" file="/var/log/rngd.log")
        stop
}
auth,authpriv.*                 /var/log/auth.log
*.*;auth,authpriv.none;kern.none        -/var/log/syslog
#cron.*                         /var/log/cron.log
# daemon.*                     -/var/log/daemon.log
kern.*                          -/var/log/kern.log
# lpr.*                                -/var/log/lpr.log
mail.*                          -/var/log/mail.log
user.*                          -/var/log/user.log

その他、messagesやdebug ファイルへの出力も止めた。上記のように設定することで、syslog ファイルの内容はかなりさっぱりしたものになった。

rsyslog.conf の設定内容については、  https://www.rsyslog.com/doc/v8-stable/configuration/ を参照した。

logrotateも設定変更

また、/etc/logrotate.d/rsyslog を変更し、cron.log は初期設定のweeklyローテーションで4週間保存ではなく、syslogと同様に毎日ローテーションで7日間保存とした。追加したrngd.log も同様。

いちおう動作状況は見えるようにしておきたいが、さほど注目していないので、といった設定。

きょうのまとめ

電源スケジューラのプログラムの話を書くためにプログラムを読み返していると、どうしてもプログラム自体を直してしまって進まない。

以前、ESP-WROOM02と温度センサーで取得したデータをグラフ化するときもGoogle Chartを使ったが、何年か経っているので書き方が若干変わったようだった。

Apache2を入れたついでに、懐かしのmrtgも入れたりしてみたが、もうちょっとraspiらしいこと? してみたくはあります。

Google Chart を表示するコードに問題があったので修正版を作りました。

追記 “Classic”なLineChart版

Google Chartには、Material Design と呼ばれる新しいチャート描画オプション(新しいといっても2014年~)と、従来からの “Classic” Chart があって、おもに Options と 最初にロードするGoogle Visualization API のパッケージ名が異なっている。

最初に作ったグラフは、別のMaterial Chartから流用したものだったので、それらのための指定が混在してしまい、なんか妙なグラフになっていた。

パッケージ名

  • Classic版ではgoogle.charts.load(“current”, {packages: [“corechart”]});
  • Material版では、 google.charts.load(‘current’, {‘packages’:[‘line’]});
グラフ

以前のグラフと比べると、subtitle がなくなったが、lineWidth指定は有効になって線が細くなった。またチャート領域として指定したdiv内の実際のチャートの占める位置も明示的に指定している(そうしないと、隙間が大きかったので)。

raspi温度グラフ

前にやったのと同様に、raspi4に過大な負荷をかけてgpio-fan をむりやり回してみたところ。現在は、55℃でファンが回るようにしている。

負荷を解除してしばらくすると、以下のように平熱?に戻った。

raspi温度グラフ
showlog.html (Classic Line Chart版)

上に載せた 中途半端なMaterial 版での指定はコメントとして残した。

<HTML>
<head>
<meta charset="UTF-8"/>
<title>Raspi4 internal Temp.</title>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<style>
div#chart_div { height: 500px; width: 700px; border: 1px solid #000000; }
</style>
<script>
//google.charts.load('current', {'packages':['line']});
google.charts.load("current", {packages: ["corechart"]});
window.onload = function() {
    document.getElementById("range_sel").value = 6;
    update_chart();
}

function show_chart(data, rows) {
    let options = {
//      chart: {
//          title: "raspi4 Internal Temperature (" + rows + " items)",
//          subtitle: 'core and pmic (C)'
//      },
        title: "raspi4 Internal Temperature (" + rows + " items)",
        titleTextStyle: {fontSize: 18, italic: 'yes'},
        lineWidth: 1,
        fontSize: 14,
        chartArea: { left: 40, top: 60, width: '80%', height: '75%'}

    };
    var chart = new google.visualization.LineChart(document.getElementById("chart_div"));
//  var chart = new google.charts.Line(document.getElementById("chart_div"));
    chart.draw(data, options);
//  chart.draw(data, google.charts.Line.convertOptions(options));
}
function tempchart(disp_period_h) {
    fetch("temp_monitor.log").then(function(response) {
        return response.text().then(function(text) {
            let lines = text.split('\n');
            if (lines.length < 1) {
                alert('no data');
                return;
            }
            let data = new google.visualization.DataTable();
            data.addRows(lines.length);
            data.addColumn("date", "time");
            data.addColumn("number", "Core");
            data.addColumn("number", "PMIC");
            let rows = 0;
            lines.forEach(function(v, i){
                let csv = v.split(',');
                let t = csv[0].split(' ');
                let tm = new Date(t[0] + 'T' + t[1] + '.000+09:00');

                if (disp_period_h == 0) {
                    data.setValue(i, 0, tm);
                    data.setValue(i, 1, csv[1]);
                    data.setValue(i, 2, csv[2]);
                    rows++
                } else if (tm.getTime() > (Date.now() - (disp_period_h * 60 * 60* 1000))) {
                    data.setValue(i, 0, tm);
                    data.setValue(i, 1, csv[1]);
                    data.setValue(i, 2, csv[2]);
                    rows++
                }
            });
            show_chart(data, rows);
        });
    });
}
function update_chart() {
    let period = document.getElementById("range_sel").value;
    tempchart(period);
    return 0;
}

</script>
</head>
<body>
<div  style="margin-left:8px; margin-bottom:12px; font-size:16px;vertical-align: middle;">
Display Range:
<select id="range_sel" style="font-size:16px; ">
<option value="1">1 hour</option>
<option value="2">2 hours</option>
<option value="4">4 hours</option>
<option value="6">6 hours</option>
<option value="8">8 hours</option>
<option value="12">12 hours</option>
<option value="24">24 hours</option>
<option value="48">48 hours</option>
<option value="0">ALL</option>
</select>
<button id="bt_update" type="button" onclick="return update_chart();">Update</button>
</div>

<div id="chart_div" style="display: block;"></div>

</body>
</html>