Jetson nanoで4ピンPWMファンを自動制御(bash)

# モチベーション
- Jetson Nano A02を手に入れました
- しかしファンがないのでよく落ちる
- 4ピンのPWMファンを買ってみた

- しかしJetson Nanoには自動でPWM制御する機能は付いていない
# これを書いた理由
様々な方が実際にファンの自動制御を行ってます。Pythonで温度を取得し、pwm制御を行うスクリプトがGitHubに公開されていたり...
しかし割と簡略化されてはいますが、ブラックボックス化されていて何やってるかイマイチわからん
Linux内でシステムの自動化をするならPythonよりbash（必ずとは限らない）
あと純粋このシステムだとbashの方がスマート
<br>
なにより、自分で理解して自分で作った方がそれっぽい
この記事を見るよりGitHubに公開されてるものをそのまま使ったほうが早いので、こちらは自分が処理内容を理解するための、そして自分が作ったという優越感に浸るための内容となっています。
# やっていく流れ
1. Jetson Nanoの温度情報を取得する
2. Jetson NanoでPWM制御を行う
3. 取得した温度情報からPWM信号に変換（設定）する
4. bashで1→3→2の流れをスクリプトに起こす
5. systemdに組み込んでデーモン化する
# Jetson Nanoの温度情報を取得する
まず、Jetson Nanoの温度情報を取得します。参考は[こちら](https://karakuri-musha.com/inside-technology/jetson-nano-how-to-use-fan01/#:~:text=%E4%BB%8A%E5%9B%9E%E3%81%AF%E3%80%81Jetson)です。
/sys/devices/virtual/thermalに、それぞれの温度を管理するzoneが格納されています。
```:terminal
$ cd /sys/devices/virtual/thermal/
$ ls
cooling_device0  cooling_device10  cooling_device12  cooling_device14  cooling_device16  
cooling_device3  cooling_device5  cooling_device7  cooling_device9  thermal_zone1  
thermal_zone3  thermal_zone5  cooling_device1  cooling_device11  cooling_device13  
cooling_device15  cooling_device2   cooling_device4  cooling_device6  cooling_device8  
thermal_zone0    thermal_zone2  thermal_zone4  thermal_zone6
```
こんな感じでthermal_zoneが複数あります。参考記事には3つあり、他の記事には8つだったり、私は7つと様々です。
次にこれらの温度情報と、それの属性（何の温度情報なのか）を確認します。
```:terminal
$ cat thermal_zone[0-6]/type
AO-therm
CPU-therm
GPU-therm
PLL-therm
PMIC-Die
thermal-fan-est
iwlwifi_1
$ cat thermal_zone[0-6]/temp
40000
30000
31000
29000
50000
29750
35000
```
このように、それぞれのthermal_zoneに格納されている温度情報とその種別を確認できます。
例えば、thermal_zone1はCPU-thermとあるのでCPU温度を指し、これが30000となっていますが、これは30℃です。
# Jetson NanoのPWMファンを制御する
参考記事は[こちら](https://zenn.dev/sumire0204/articles/52200752e72b17)です。
pwmは0から255までの値を取り、/sys/devices/pwm-fanに含まれるterget_pwmの値によって回転数を制御できます。
```:terminal
$ sudo sh -c 'echo 255 > /sys/devices/pwm-fan/target_pwm'
```
おそらく最大火力なのでものすごい音になるかと思いますが、このコマンドの255を違う値にしてもう一度ターミナルに送信すると回転数が変化します。所感ですが、少しの値だと変化がわからないので、50、100単位で値をずらしてみるといいかもしれません。
# bashスクリプトで5秒おきにpwmを温度情報に基づいて制御する
まず全体のスクリプトを示します。
```:get_temp.sh
#!/bin/bash
#daemon script path: /usr/local/bin/get_temp.sh
#daemon system name: autofan_daemon

#logfile
LOGFILE="/var/log/fan_thermal.log"

#温度を取得するzone
zones=(1 2 5)

# 温度を取得する関数
read_thermal_zone() {
    local zone=$1
    local temp_file="/sys/class/thermal/thermal_zone${zone}/temp"

    if [[ -f $temp_file ]]; then
        temp_millidegree=$(cat $temp_file)
        temp_degree=$(echo "scale=3; $temp_millidegree / 1000" | bc)
    else
        temp_degree="Not available"
    fi

    echo "${temp_degree}"
}

# 温度の平均値を算出する関数
calculate_average_temperature() {
    local sum=0
    local count=0
    for temp in "$@"; do
        if [[ "$temp" != "Not available" ]]; then
            sum=$(echo "$sum + $temp" | bc)
            ((count++))
        fi
    done
    if (( count > 0 )); then
        average=$(echo "scale=3; $sum / $count" | bc)
        echo "$average"
    else
        echo "Not available"
    fi
}

# 温度の平均値からPWM値を決定する関数
determine_pwm_value() {
    local average_temp=$1
    local pwm_value=0

    # ここで段階的に設定
    if [[ "$average_temp" != "Not available" ]]; then
        if (( $(echo "$average_temp < 30" | bc -l) )); then
            pwm_value=0
        elif (( $(echo "$average_temp < 40" | bc -l) )); then
            pwm_value=50
        elif (( $(echo "$average_temp < 50" | bc -l) )); then
            pwm_value=100
        elif (( $(echo "$average_temp < 60" | bc -l) )); then
            pwm_value=150
        else
            pwm_value=255
        fi
    else
        pwm_value=0
    fi

    echo "$pwm_value"
}

# メインスクリプト
while true; do
    temperatures=()
    for zone in "${zones[@]}"; do
        temp=$(read_thermal_zone $zone)
        temperatures+=("$temp")
    done

    average_temp=$(calculate_average_temperature "${temperatures[@]}")
    pwm_value=$(determine_pwm_value "$average_temp")
    echo "$(date): Temp:$average_temp℃ PWM:$pwm_value" >>$LOGFILE

    # Write PWM value to the system
    if [[ -w /sys/devices/pwm-fan/target_pwm ]]; then
        sudo sh -c "echo $pwm_value > /sys/devices/pwm-fan/target_pwm"
    else
        echo "Error: Cannot write to /sys/devices/pwm-fan/target_pwm"
    fi

    sleep 5  # 5秒待機
done
```
このスクリプトは温度を取得する関数、得た温度から代表値(今回は平均値)を算出する関数、代表値をpwm信号に変換する関数を用意し、これを5秒ごとに更新、そのたびにpwm信号を送信するというスクリプトになります。
### 温度を取得する関数
```
# 温度を取得する関数
read_thermal_zone() {
    local zone=$1
    local temp_file="/sys/class/thermal/thermal_zone${zone}/temp"

    if [[ -f $temp_file ]]; then
        temp_millidegree=$(cat $temp_file)
        temp_degree=$(echo "scale=3; $temp_millidegree / 1000" | bc)
    else
        temp_degree="Not available"
    fi

    echo "${temp_degree}"
}
```
先ほど確認した温度情報の種別を見ると
1. CPUの温度情報
2. GPUの温度情報
5. ファンの信号から取得しているヒートシンクの温度情報

が利用する温度情報に該当するので温度の取得するzoneを1,2,5に絞り、℃で扱う温度情報に変換します。
取得できなかった場合は"Not available"という文字列を代入します。
### 温度から代表値を算出する関数
```
# 温度の平均値を算出する関数
calculate_average_temperature() {
    local sum=0
    local count=0
    for temp in "$@"; do
        if [[ "$temp" != "Not available" ]]; then
            sum=$(echo "$sum + $temp" | bc)
            ((count++))
        fi
    done
    if (( count > 0 )); then
        average=$(echo "scale=3; $sum / $count" | bc)
        echo "$average"
    else
        echo "Not available"
    fi
}
```
私は簡単に平均値を利用しました。
得られた温度情報が"Not availavble"ではない場合加算し、カウントします。そして加算された値とカウント数で割ることにより得られた温度情報のみ（1つや2つの場合もある）から平均値を算出できます。
### 代表値からpwm信号に変換する関数
```
# 温度の平均値からPWM値を決定する関数
determine_pwm_value() {
    local average_temp=$1
    local pwm_value=0

    # ここで段階的に設定
    if [[ "$average_temp" != "Not available" ]]; then
        if (( $(echo "$average_temp < 30" | bc -l) )); then
            pwm_value=0
        elif (( $(echo "$average_temp < 40" | bc -l) )); then
            pwm_value=50
        elif (( $(echo "$average_temp < 50" | bc -l) )); then
            pwm_value=100
        elif (( $(echo "$average_temp < 60" | bc -l) )); then
            pwm_value=150
        else
            pwm_value=255
        fi
    else
        pwm_value=0
    fi

    echo "$pwm_value"
}
```
ここら辺は割と好みになるのではないかなと思います。
私は温度の代表値が特定の範囲でpwmを段階的に変化させていますが、例えば最大設定温度から規格化する直線的な変化や、温度の閾値を変化させるなど様々かと思います。
### メインスクリプト
```
#!/bin/bash
#daemon script path: /usr/local/bin/get_temp.sh
#daemon system name: autofan_daemon

#logfile
LOGFILE="/var/log/fan_thermal.log"
#温度を取得するzone
zones=(1 2 5)
```
```
# メインスクリプト
while true; do
    temperatures=()
    for zone in "${zones[@]}"; do
        temp=$(read_thermal_zone $zone)
        temperatures+=("$temp")
    done

    average_temp=$(calculate_average_temperature "${temperatures[@]}")
    pwm_value=$(determine_pwm_value "$average_temp")
    echo "$(date): Temp:$average_temp℃ PWM:$pwm_value" >>$LOGFILE

    # Write PWM value to the system
    if [[ -w /sys/devices/pwm-fan/target_pwm ]]; then
        sudo sh -c "echo $pwm_value > /sys/devices/pwm-fan/target_pwm"
    else
        echo "Error: Cannot write to /sys/devices/pwm-fan/target_pwm"
    fi

    sleep 5  # 5秒待機
done
```
ここでは、上で定義した3つの関数を呼び出し、pwm値を更新します。これを5秒毎に行います。
ログファイルfan_thermal.logを用意し、書き込むことで常時pwm値と平均温度の監視ができます。
また、書き込むpwm値が存在しない場合は書き込まないようにしています。

このスクリプトは、ターミナルで
```:terminal
$ sudo bash get_temp.sh
```
このように入力することで実行できます。
また、ログファイルの書き込みを許可するために
```:terminal
$ sudo chmod 666 /var/log/fan_thermal.log
```
とすることでログが書き込まれるようになります。
# お願いダえもん！常時起動にして！
このbashスクリプトを/usr/local/binに移動し、systemdでデーモン名autofan_daemonを作成します。
```
$ sudo cp get_temp.sh /usr/local/bin
$ cd /etc/systemd/system/
$ sudo vi autofan_daemon.service
```
autofan_daemon.serviceにこのように設定します。
```:autofan_daemon.service
[Unit]
Description=Get Thermal Zone Information and Set PWM
After=network.target

[Service]
ExecStart=/usr/local/bin/get_temp.sh
Restart=on-failure
RestartSec=10
User=root
StandardOutput=journal
StandardError=journal
StartLimitIntervalSec=50
StartLimitBurst=3

[Install]
WantedBy=multi-user.target
```
Userはrootにしないと権限上書き込みができないので気を付けましょう。
そしたらこれを常時起動にして完成です。
```:terminal
$ sudo systemctl start autofan_daemon
$ sudo systemctl enable autofan_daemon
```
ちゃんと再起動してもpwm制御でファンが起動するかと思います。

よきJetsonライフを
## 補足：スクリプトを修正した場合
もしpwm値や回転条件などを変更した場合、/usr/local/binにあるget_temp.shを変更しただけでは何も変化しないので、ダえもんをリロードしてあげてください。
```
$ sudo systemctl daemon-reload
$ sudo systemctl restart autofan_daemon
```
## 参考
https://github.com/Pyrestone/jetson-fan-ctl

https://zenn.dev/sumire0204/articles/52200752e72b17

https://karakuri-musha.com/inside-technology/jetson-nano-how-to-use-fan01/#:~:text=%E4%BB%8A%E5%9B%9E%E3%81%AF%E3%80%81Jetson

https://qiita.com/2626Q/items/c7377ce9f16908ff2604#:~:text=Jetson%20Nan