ラズベリーパイの排熱を改善2

ケースの上に穴をあけ、更にヒートシンクを取り付けてもはっきりと温度が下がらなかったラズベリーパイ3ですが、ファンを取り付けてみました。

100均で買った下敷きを切ってそれに取り付けてみました。

これにより、5℃くらい下がりました。ファンの電源を5Vから撮りたかったのですが、LCDが使っていて開いていなかったので、3.3Vからとりました。

対処後の温度グラフは以下のようになりました。対処したのはPi3 Work 緑の線になります。

これを対処したことによって、Low voltage warning「低電圧の警告」が頻繁に表示されるようになってしまいまいました。現在使っているのは5V3AのACアダプタなのですが、LCDとファンも動かすようになったことで容量不足になったのかもしれません。

居間のラズベリーパイ4ですが、ファンの電源を5Vから取っていたのですが、テレビを点けていないときにファンの音がうるさいことに気が付きました。ファンが大きくて冷却効果も高いのか30℃半ばをキープしていたので3.3Vで常時冷やすことにしました。音も気にならなくなり、温度も40℃を割る位でキープしているようです。

現在の課題
1)Pi3 Workの電源容量不足問題
2)常時稼働ラズパイのファンレス化(庭カメラ用ラズパイの直射日光問題含む)

ラズベリーパイの排熱を改善

各ラズベリーパイのCPU温度グラフを見るようになって、庭を撮影しているラズベリーパイの温度が高いのが気になってきました。日が射すと70度を超えます。窓際に置いているため、ケースに直接日光が当たるためだと思い、画用紙で日よけを作ってみました。いくらか効果あるでしょうか?

対策を施したその日からめっきり秋めいてきたので、明確な比較はできませんが70℃を超えることはなくなりました。ある程度の効果はあったと言えるでしょう。

100均で買った箱をケースにしているラズベリーパイですが、60℃超になっていることが多く、ケース上部が暖かくなっているので穴をあけて排熱を良くしました。

結果は上部からの熱の抜けは良くなったような気はするものの、明確に数字に表れたような感じがしません。敢えて言うと1℃くらい低くなった感じです。それではと思い、ヒートシンクを購入して貼り付けてみました。結果は若干効果あり、60℃を超えることが少なくなりました。それでも2~3℃低くなって59℃台です。

50℃をちょっと超えた温度だったPi Zero2は試しにテンキーを取ってみるとそれだけで5℃下がりました。やはり排熱性が悪かったのでしょう。

CPU温度を測ろうとしたのはファンを温度によって回す設定を発見したところから始まったのですが、常時回すようにしたらどうなるのかと思い、ファンの電源をGPIOの5Vから常時給電するように再セッティングしてみました。

すると一気に下がりました。居間に置いたものは35℃位まで下がりました。庭のラズベリーパイはそれより7℃位高いですが、日除けのせいで排熱性が悪くなっているのでしょう。Pi4 Workは3.3Vから給電してみましたが、5Vで給電しているPiよりも7℃位高いです。Youtubeの動画を見ると50℃以上まで温度が上昇します。

上記の処置により、結果としてヒートシンクだけで冷却しているPi3が他を圧倒して熱いという状況になってしまいました。排熱性が悪くて5℃高くなっているPi Zero2も気になります。

各ラズベリーパイの部屋の温度と部屋の温度を計測しグラフに表示

ラズベリーパイ4は他のシリーズに比べ、CPU性能も高く発熱量も多いのでCPUファン付きのケースに入れていました。ファンの電源はGPIOの5Vに接続し、常時回していました。あるとき、ラズベリーパイの設定にCPUが何度になったらファンを回す、という設定があるのに気が付きました。試してみるとちゃんと動作しています。

CPUの温度で動作するという事は、今何度かわかる方法があるはずです。調べてみると以下のコマンドがありました。
$ sudo vcgencmd measure_temp
temp=57.4’C

気が付くと家にあるラズベリーパイは7台にもなっていました。
居間(Living):居間に防犯カメラのモニターとして常時稼働(Pi4:4GB)
玄関(Entrance):玄関の防犯カメラとして常時稼働(Pi3)
玄関2(Entrance-2):側面方向からみた玄関の防犯カメラとして常時稼働(PiZero)
実験用Pi3(Pi3 Work):実験用としているが、2階のカメラモニタとして半常時稼働 (Pi3)
実験用Pi4(Pi4 Work):実験用として必要時稼働 (Pi4:4GB)
テンキー用 (Alexa Key):テンキーによるAlexa定型アクション起動用として常時稼働 (PiZero2)
庭(Garden):たまにやってくる動物を捉えようと設置したカメラ用。常時稼働(Pi4:2GB)

上記のラズベリーパイの全部のCPU温度を時系列でグラフにしてみたくなりました。居間のNature Remo miniにも室温を計測する機能がありました。室温もグラフに加えます。最近2階用にスマートリモコンEZCONを買ったのですが、これには室温計測機能が無いようです。本当はNature Remoを買い増そうかと思ったのですが、去年買った時よりずいぶん高くなっていたので止めて安いものを買ったのです。

毎時0分と30分の30分おきにデータを収集することにしました。プログラムは以下のようになります。
data_collect.py

from paramiko import SSHClient, AutoAddPolicy, SSHException, AuthenticationException, ssh_exception
import socket
import re
from urllib.parse import urlencode
from urllib.request import urlopen, Request
from urllib.error import HTTPError
from json import loads
import os
from datetime import datetime
import schedule
import time

PI_NAME = ['Living', 'Entrance', 'Entrance-2', 'Pi3 Work', 'Pi4 Work', 'Alexa Key', 'Garden', 'Room']
PI_ADR = ['192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx', '192.168.xx.xx']
PORT = 22
USER = 'pi'
PASSWORD = 'xxx'
TIMEOUT = 5
CMD_CPU = 'sudo vcgencmd measure_temp'

api_key = ""
url = "https://api.nature.global/1/devices/"
headers = {
    "accept" :"application/json",
    "Authorization" :"Bearer " + api_key,
}

DATA_FILE = '/var/www/html/temperature.csv'
ERROR_FILE = './errorlog.txt'
DATA_NUM = 96

def savedata(data):
  list = []
  if os.path.exists(DATA_FILE):
    f = open( DATA_FILE, 'r' )
    for line in f.readlines():
      list.append(line)
    else:
      f.close()
  start = 1
  num = len(list)
  if num >= (DATA_NUM + 1):
    start = num - DATA_NUM + 1
  f = open( DATA_FILE, 'w' )
  header = 'time,'
  for i in range(len(PI_NAME)):
    if i != 0:
      header += ','
    header += PI_NAME[i]
  f.write(header + '\n')
  for i in range(start, num):
    f.write( list[i] )
  f.write( datetime.now().strftime('%Y/%m/%d %H:%M,') + data + '\n' )
  f.close()

def errorlog(log):
  f = open(ERROR_FILE, 'a')
  f.write( datetime.now().strftime('%Y/%m/%d %H:%M:%S:') + log + '\n' )
  f.close()

def cpu_temp():
  ret = ''
  ssh = SSHClient()
  for name, adr in zip(PI_NAME, PI_ADR):
    try:
      ssh.set_missing_host_key_policy(AutoAddPolicy())
      ssh.connect(adr, PORT, USER, PASSWORD, timeout=TIMEOUT)
      stdin, stdout, stderr = ssh.exec_command(CMD_CPU)
    except AuthenticationException as e:
      errorlog(name + ':AuthenticationException:' + e)
      ret += ','
    except SSHException as e:
      errorlog(name + ':SSHException:' + e)
      ret += ','
    except ssh_exception.NoValidConnectionsError as e:
      ret += ','
    except socket.timeout:
      ret += ','
    else:
      temp = re.sub('\'C\n', '', re.sub('temp=', '', stdout.readline()))
      ret += str(temp) + ','
    finally:
      ssh.close()
  return ret

def room_temp():
  ret = ''
  request = Request(url, headers=headers)
  try:
    with urlopen(request) as response:
      data_byte = response.read()
      data= loads(data_byte)
      device_info = data[0]["newest_events"]
      ret = str(device_info["te"]["val"])
  except HTTPError as e:
      errorlog('Room:HTTPError:' + e)
      ret = ''
  finally:
      response.close()
  return ret

def correct():
  data1 = cpu_temp()
  data2 = room_temp()
  savedata(data1 + data2)

# every 30minute
schedule.every().day.at('00:00').do(correct)
schedule.every().day.at('00:30').do(correct)
schedule.every().day.at('01:00').do(correct)
schedule.every().day.at('01:30').do(correct)
schedule.every().day.at('02:00').do(correct)
schedule.every().day.at('02:30').do(correct)
schedule.every().day.at('03:00').do(correct)
schedule.every().day.at('03:30').do(correct)
schedule.every().day.at('04:00').do(correct)
schedule.every().day.at('04:30').do(correct)
schedule.every().day.at('05:00').do(correct)
schedule.every().day.at('05:30').do(correct)
schedule.every().day.at('06:00').do(correct)
schedule.every().day.at('06:30').do(correct)
schedule.every().day.at('07:00').do(correct)
schedule.every().day.at('07:30').do(correct)
schedule.every().day.at('08:00').do(correct)
schedule.every().day.at('08:30').do(correct)
schedule.every().day.at('09:00').do(correct)
schedule.every().day.at('09:30').do(correct)
schedule.every().day.at('10:00').do(correct)
schedule.every().day.at('10:30').do(correct)
schedule.every().day.at('11:00').do(correct)
schedule.every().day.at('11:30').do(correct)
schedule.every().day.at('12:00').do(correct)
schedule.every().day.at('12:30').do(correct)
schedule.every().day.at('13:00').do(correct)
schedule.every().day.at('13:30').do(correct)
schedule.every().day.at('14:00').do(correct)
schedule.every().day.at('14:30').do(correct)
schedule.every().day.at('15:00').do(correct)
schedule.every().day.at('15:30').do(correct)
schedule.every().day.at('16:00').do(correct)
schedule.every().day.at('16:30').do(correct)
schedule.every().day.at('17:00').do(correct)
schedule.every().day.at('17:30').do(correct)
schedule.every().day.at('18:00').do(correct)
schedule.every().day.at('18:30').do(correct)
schedule.every().day.at('19:00').do(correct)
schedule.every().day.at('19:30').do(correct)
schedule.every().day.at('20:00').do(correct)
schedule.every().day.at('20:30').do(correct)
schedule.every().day.at('21:00').do(correct)
schedule.every().day.at('21:30').do(correct)
schedule.every().day.at('22:00').do(correct)
schedule.every().day.at('22:30').do(correct)
schedule.every().day.at('23:00').do(correct)
schedule.every().day.at('23:30').do(correct)

while True:
  schedule.run_pending()
  time.sleep(10)

このプログラムを動かすにはライブラリparamikoとurllib、scheduleのインストールが必要になります。インストールは以下のように行います。
$ pip install paramiko⏎
$ pip install urllib3⏎
$ pip install schedule⏎

プログラムの説明
PI_NAME =で温度計測対象の名称を配列で設定しています。
PI_ADR =は各ラズベリーパイのアドレスを配列で定義しています。
PORT =はSSH接続するときのポート番号です。
USER =はSSH接続するときのユーザー名、PASSWORD =はパスワードです。
TIMEOUT =はSSH接続する際のタイムアウト時間となります。全てのラズベリーパイが常時稼働しているわけではないため、応答がなければ早めに非稼働中とします。
api_key =はNature Remoのアクセストークンです。Nature APIを使用するには必要で、以下のサイトで取得します。
https://home.nature.global/
url =はNature APIのURL、headersはNature APIに必要なヘッダー情報となります。
DATA_FILE =はこのプログラムのOUTPUファイルとなります。HTMLでグラフ表示するため、/var/www/html/ディレクトリにCSV形式で保存します。
ERROR_FILE =はエラーが発生した時に内容を出力するファイルになります。
DATA_NUM=は保持する最大のデータ数になります。30分おきに1日48データ収集しますので、2日分のデータを保持する設定になっています。

プログラムは最初起動時にスケジューラで起動する時刻を設定します。
schedule.every().day.at(‘XX:XX’).do(correct)
48行書くのは野暮ったいですね。流石ヘボプログラマーです。
while文で無限ループし、10秒おきに起動判定を行います。

関数correct()では最初に各ラズベリーパイの温度を収集し、次に部屋の温度を収集し、最後にデータを保存します。

各ラズベリーパイの温度を収集する関数cpu_temp()ではSSH接続し、温度を取得するコマンドによりデータを取得しています。取得した文字列は「temp=57.4’C⏎」というフォーマットなので数字だけを取り出すために[temp=[と[‘C⏎]を取り除いています。データをCSVファイルで保存するために,(カンマ)で区切っています。timeout以外の予期せぬエラー(例外)はエラーログファイルに内容を出力しています。(この例外処理は発生したことがないので正常に動作するのか試したことはありません・・・)

関数room_temp()ではNature APIを使いデバイス情報をJSONフォーマットで取得します。{“newest_events”:{“te”:{“val”:}}}に格納されている部屋の温度を取得します。(参照:https://swagger.nature.global/#/default/get_1_devices)

関数savedata()では引数に与えられたデータに時刻を追加してCSVファイルに保存しています。直近96データより古いものは破棄しています。

プログラムが出力するcsvは以下のようになっています。
temperature.csv

time,Living,Entrance,Entrance-2,Pi3 Work,Pi4 Work,Alexa Key,Garden,Room⏎
2022/08/24 11:30,52.1,45.1,50.8,62.3,,53.2,72.5,26.5⏎
2022/08/24 12:00,54.5,47.8,,65.5,,53.2,73.0,27⏎
2022/08/24 12:30,56.9,45.1,,65.0,,53.7,70.1,28⏎
2022/08/24 13:00,55.0,46.2,48.7,62.3,,53.7,68.1,28.5⏎
2022/08/24 13:30,51.6,45.1,51.4,62.8,,53.2,67.2,28⏎
・・・後略・・・

プログラムは居間のラズベリーパイに稼働させることにしました。
apache2をインストールします。
$sudo apt install apache2

ラズベリーパイ起動時にプログラムを開始するようにします。
/etc/rc.localのexit 0より上に以下の1行を挿入し、start.shシェルスクリプトをバックグラウンドで起動します。
sudo sh /home/pi/start.sh &

シェルスクリプトstart.shは以下のようになっています。

#!/usr/bin/bash
sleep 10
cd /home/pi/
python ./data_collect.py

最初に10秒sleepしているのは、これがないとプログラムがエラーとなるためです。どうやらこのプログラムが起動している段階ではネットワーク系のサービスがまだ起動していないらしく、それを避けるために簡易的に10秒間待っています。

データを格納するcsvファイルが作成されるようになりました。
それをグラフ表示するhtmlは以下のようになります。グラフはGoogle Chartを使用しています。
/var/www/html/temperature.html

<!DOCTYPE html>
<html lang='ja'>
<head>
  <meta charset='UTF-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <meta http-equiv="Cache-Control" content="no-cache">
  <title>Temperature Graph</title>
  <link rel="stylesheet" href="css/style.css">
  <script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
  <script type='text/javascript'>
    google.charts.load('current', {packages: ['corechart', 'bar']});
    google.charts.setOnLoadCallback(drawCurveTypes);

    function drawCurveTypes() {
      var req = new XMLHttpRequest();
      req.open('get', 'temperature.csv', true);
      req.send(null);
      req.onload = function() {
        var ma = convertCSVtoArray( req.responseText );
        var data = new google.visualization.arrayToDataTable( ma );
        var options = {
          focusTarget: 'category',
          pointSize: 3,
          pointShape: 'circle',
          chartArea:{left:'5%',width:'75%',height:'85%',top:'5%'},
          backgroundColor: '#C0C0C0'
        };
        var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
        chart.draw(data, options);
      }
    }

    function convertCSVtoArray(str) {
      var result = [];
      tmp = str.split('\n')
      for( var i = 0; i < tmp.length; ++i ) {
        if( tmp[i] == '' ) continue;
        result[i] = tmp[i].split(',');
        if( i != 0) {
          result[i][0] = new Date(result[i][0]);
          for( var j = 1; j < result[i].length; j++) {
            if( result[i][j].length != 0 ) {
              result[i][j] = Number(result[i][j]);
            } else {
              result[i][j] = NaN;
            }
          }
        }
      }
      return result;
    }
  </script>
</head>
<body>
  <p>温度データ</p>
  <div id='chart_div' class='graph-temperature'></div>
 </body>
</html>

/var/www/html/css/style.cssには以下の4行を追加します。ファイルがない場合は追加します。グラフの横幅はブラウザの横幅を目一杯使うようにしています。縦の540ピクセルはラズパイのモニタの設定ではみ出さないで表示で来る最大の大きさを狙って設定しています。

.graph-temperature{
  width: 100%;
  height: 540px;
}

表示されるページは以下のようなイメージになります。グラフ内にマウスポインタを当てると指定したポイントの値を表示します。

庭を写すカメラを制御しているGardenは窓際に置いているのですが、日が射すと温度が70℃以上になっているのがわかります。曇りの日はそれほど高くならないようです。
2階のモニタ用ラズパイはPi3でタッチパネルと一体にセットされて、100均で買ったプラスティックケースに入れていますが、ファンもヒートシンクも付けていないため、60°をちょっと超えてしまっています。

居間のテレビの裏に置いてあるLivingは時々60℃を超えてはファンが回って冷えている感じなのでしょうか。
PiZero2のAlexa Keyは熱対策なしの上に、テンキーで蓋をされているようになっているためか、はたまたZeroよりCPU能力が高く発熱量が多いためか50℃を超えたところで安定しています。
玄関フード内に取り付けたEntrance-2はラズパイZeroですが、これも熱対策は何もしていません。常に気温より20℃くらい高い温度です。
玄関の内側に取り付けたEntranceはPi3ですが、これは全体がヒートシンクになっているというアルミケースを使用しています。常に40℃前半を維持していて素晴らしい冷却能力を示しています。
部屋は暑くなってきたらエアコンを点けているのがわかります。もう秋なのか夜は25℃以下になることもあるようです。