Python で Raspberry Pi の 複数のGPIOピンに効率的よくシェルスクリプトを割当てる方法を紹介いたします。
以下、Raspberry Pi は ラズパイとして話を進めます。
下記のようなラズパイを組み込んだシステムのボタン処理の効率化についての説明になります。
実行環境
- ラズパイゼロ(WH)、ラズパイ3、4 ※ラズパイPicoは対象外
- システムライブラリ pipgio がインストール済みであること
- python仮想環境からpythonを実行する (python-3.7.2 〜 3.8.x)
- pythonライブラリ pigpio がインストール済みであること
リソースの構成
raspi_pigpio
├── bin
│ ├── buttons_startup.sh
│ ├── kill_display_process.sh
│ ├── restart_udpmon.sh
│ ├── show_weather_forecast.sh
│ └── pigpio
│ ├── ButtonStartServices.py
│ ├── ShowWeatherForecast.py
│ └── conf
│ └── conf_buttonstart.json
└── datas
└── WeatherForecast.txt
1. 実装方法
1-1. GPIOピンと起動するシェルスクリプトの対応付け
{
"actions": [
"showWeatherForecast",
"killDisplayAndMelodyProcess",
"restartUdpmon"
],
"showWeatherForecast": {
"PIN": 18,
"PUD_down": true,
"enable": true,
"script": "/home/pi/bin/show_weather_forecast.sh"
},
"killDisplayAndMelodyProcess": {
"PIN": 12,
"PUD_down": true,
"enable": true,
"script": "/home/pi/bin/kill_display_process.sh"
},
"restartUdpmon": {
"PIN": 24,
"PUD_down": true,
"enable": true,
"script": "/home/pi/bin/restart_udpmon.sh"
}
}
1-2. ボタンサービス実行シェルスクリプト
#!/bin/bash
. $HOME/py_venv/py37_pigpio/bin/activate
python $HOME/bin/pigpio/ButtonStartServices.py
deactivate
1-3. Pythonスクリプト
1-3-1. インポート・グローバル変数定義
import logging
import os
import signal
import subprocess
import time
import threading
import pigpio
import util.file_util as FU
from log import logsetting
"""
Buttons startup srcipt on service
"""
base_dir = os.path.abspath(os.path.dirname(__file__))
logger = logsetting.create_logger("service_buttonstart")
# pigpio オブジェクト
pi = None
# conf_buttonstart.jsonを読み込んだ辞書オブジェクト
conf_app = None
# GPIOピンコールバックリスト
cb_list = []
isLogLevelDebug = False
# Prevent chattering
prev_tick = 0
THRESHOLD_DIFF_TICK = 500000
1-3-2. シャットダウンフックとクリーンアップ処理
def detect_signal(signum, frame):
"""
Detect shutdown, and execute cleanup.
:param signum: Signal number
:param frame: frame
"""
logger.info("signum: {}, frame: {}".format(signum, frame))
if signum == signal.SIGTERM:
# signal shutdown or kill
cleanup()
# Current process terminate
exit(0)
def cleanup():
for _cb in cb_list:
_cb.cancel()
pi.stop()
1-3-3. サブプロセス起動処理
def subproc_start(script):
script_name = os.path.basename(script)
try:
logger.info("Subprocess {} start.".format(script_name))
exec_status = subprocess.run([script])
logger.info("Subprocess {} terminated: {}".format(script_name, exec_status))
except Exception as ex:
logger.warning(ex)
1-3-4. ボタン(GPIOピン)押下時の処理
- (A) ボタン押下時のチャタリング検知処理
- (B) アクションリストのループ処理
アクションピンとボタンのGPIOピンが一致したら、対応するシェルスクリプトをサブプロセスとして起動する
def change_actions(gpio_pin, level, tick):
# (A) ボタン押下時のチャタリング検知処理
global prev_tick
logger.info("pin: {}, level: {}, tick: {}".format(gpio_pin, level, tick))
if prev_tick != 0:
tick_diff = pigpio.tickDiff(prev_tick, tick)
if isLogLevelDebug:
logger.debug("tick_diff:{}".format(tick_diff))
if tick_diff < THRESHOLD_DIFF_TICK:
logger.info("tick_diff:{} < {} return".format(tick_diff, THRESHOLD_DIFF_TICK))
return
prev_tick = tick
# (B) ボタン(GPIOピン)に割り当てられたシェルスクリプトを起動する処理
for action in conf_app["actions"]:
button_service = conf_app[action]
act_pin = button_service["PIN"]
if act_pin == gpio_pin and button_service["enable"]:
script = button_service["script"]
script_name = os.path.basename(script)
subprc_thread = threading.Thread(target=subproc_start, args=(script,))
logger.info("Subprocess {} thread start".format(script_name))
subprc_thread.start()
1-3-5. GPIOピンと対応するコールバックの関連付け処理
設定ファイルにピン設定と起動するシェルスクリプトのリストを定義することにより、GPIOピンの初期化とコールバックの定義が効率的にできるようになります。
※処理するボタンが増えてもこの関数は修正なしにそのまま使えます
def setup_gpio():
global cb_list
for action in conf_app["actions"]:
button_service = conf_app[action]
if isLogLevelDebug:
logger.debug("{}: {}".format(action, button_service))
act_pin = button_service["PIN"]
act_PUD_down = button_service["PUD_down"]
pi.set_mode(act_pin, pigpio.INPUT)
# PullUp か PullDownによりピンの設定変える
if act_PUD_down:
pi.write(act_pin, pigpio.LOW)
pi.set_pull_up_down(act_pin, pigpio.PUD_DOWN)
cb_action = pi.callback(act_pin, pigpio.RISING_EDGE, change_actions)
else:
pi.write(act_pin, pigpio.HIGH)
pi.set_pull_up_down(act_pin, pigpio.PUD_UP)
cb_action = pi.callback(act_pin, pigpio.FALLING_EDGE, change_actions)
# Callback
cb_list.append(cb_action)
if isLogLevelDebug:
logger.debug("Callback list: {}".format(cb_list))
1-3-6. メイン処理
- pigpioオブジェクト生成
- シャットダウンフックの定義
- GPIOピンと起動するシェルスクリプトの対応付け設定ファイル読込み
- GPIOピンの初期化とコールバックリスト生成
- 無限ループ (1秒間スリープする)
このスクリプトはシステムサービスから呼び出されることを前提に作られています。
if __name__ == '__main__':
pi = pigpio.pi()
if not pi.connected:
logger.warning("pigpiod not stated!")
exit(1)
# Check shutdown signal
signal.signal(signal.SIGTERM, detect_signal)
isLogLevelDebug = logger.getEffectiveLevel() <= logging.DEBUG
# GPIOピンと起動するシェルスクリプトの対応付け設定ファイルを読み込む
conf_app = FU.read_json(os.path.join(base_dir, "conf", "conf_buttonstart.json"))
if isLogLevelDebug:
logger.debug(conf_app)
# GPIOピンと対応するコールバックの関連付け処理
setup_gpio()
try:
# 無限ルーブ
while True:
time.sleep(1.0)
except KeyboardInterrupt:
pass
finally:
cleanup()
【参考】システムサービス設定ファイル
[Unit]
Description=Button start Specific application subprocess.
After=newtwork-online.target
[Service]
Type=simple
ExecStart=/home/pi/bin/buttons_startup.sh
User=pi
[Install]
WantedBy=multi-user.target
ボタン起動開始サービスの登録処理とサービス起動
$ sudo cp button_start.service /etc/systemd/system
$ sudo systemctl enable button_start.service
$ sudo systemctl start button_start.service
pythonアプリケーションのシステムサービス化に関しては下記 Qiita投稿記事をご覧ください。
(Qiita) RaspberryPi Pythonアプリケーションをシステムサービス化する
2. ボタン押下時に起動されるシェルスクリプト
2-1. 天気予報表示
#!/bin/bash
# Prevent double execute.
pid_file="${PATH_DATAS}/.forecast"
if [ -e "$pid_file" ]; then
exit 0
fi
touch "$pid_file"
. $HOME/py_venv/py37_pigpio/bin/activate
python $HOME/bin/pigpio/ShowWeatherForecast.py
deactivate
【参考】ソースのみ掲載します
pythonスクリプトが読み込むファイルの内容は以下の通りです。
※1 cron で定期的に気象情報サイトからデータを取得し先頭に追記する
※2 pythonスクリプトは先頭行を表示する。
2023-11-29 12:00:00,11月29日 札幌市豊平区 雪のち曇 −3〜−1℃
2023-11-29 10:00:00,11月29日 札幌市豊平区 雪のち曇 −3〜0℃
2023-11-29 08:00:00,11月29日 札幌市豊平区 晴時々雪 −3〜0℃
2023-11-29 04:00:00,11月29日 札幌市豊平区 曇時々雪 −2〜0℃
import logging
import os
import subprocess
from urllib.parse import quote_plus
import util.file_util as FU
from log import logsetting
"""
当日の最新の天気予報をサブプロセスでOLEDに出力する
[ファイル]
$PATH_DATAS/WeatherForecast.txt
"""
logger = logsetting.create_logger("main_app")
isLogLevelDebug = False
if __name__ == '__main__':
isLogLevelDebug = logger.getEffectiveLevel() <= logging.DEBUG
home_datas_dir = os.environ.get("PATH_DATAS", "/home/pi/datas")
conf_app = FU.read_json(os.path.join(home_datas_dir, "conf_weatherforecast.json"))
# 天気予報ファイル
filename = conf_app["filename"]
file_path = os.path.join(home_datas_dir, filename)
encoded = ''
if os.path.exists(file_path):
today_list = FU.read_text(file_path)
if len(today_list) > 0:
# 先頭が最新
latest = today_list[0]
if isLogLevelDebug:
logger.debug("latest: {}".format(latest))
# 末尾の出力メッセージ
latest = latest.split(",")
encoded = quote_plus(latest[1])
if isLogLevelDebug:
logger.debug(encoded)
script = conf_app["displayMessage"]["script"]
if len(encoded) > 0:
exec_status = subprocess.run([script, "--encoded-message", encoded, "--has-list"])
else:
# 取得データが無い場合のメッセージ
encoded = conf_app["displayMessage"]["emptyData"]
encoded = quote_plus(encoded)
exec_status = subprocess.run([script, "--encoded-message", encoded])
logger.info("Subprocess DisplayMessage terminated: {}".format(exec_status))
2-2. 表示ボタン起動のプロセス削除
#!/bin/bash
bak_ifs=$IFS
IFS=
# メロディー鳴動プロセス削除
for ps_line in $(ps aux | grep PlayMelody | grep -v grep)
do
echo $ps_line | awk '{ print $2 }' | xargs kill
done
sleep 1
# OLED出力示プロセス削除 ※天気予報表示
for ps_line in $(ps aux | grep DisplayMessageToOLED | grep -v grep)
do
echo $ps_line | awk '{ print $2 }' | xargs kill
done
IFS=$bak_ifs
PATH_DATAS=/home/pi/datas
pid_forecast="${PATH_DATAS}/.forecast"
if [ -e "$pid_forecast" ]; then
rm -f "$pid_forecast"
fi
pid_display="${PATH_DATAS}/.display"
if [ -e "$pid_display" ]; then
rm -f "$pid_display"
fi
【参考】下記 DisplayMessageToOLED.py
の全てのプロセスを削除する
$ ps aux | grep "\.py" | grep -v grep
root 281 0.0 2.2 24808 9996 ? Sl 08:46 0:04 python /home/pi/bin/pigpio/SwitchToPoweroff.py --poweroff-pin 21 --ledblink-pin 20
pi 336 0.0 2.2 24536 9760 ? Sl 08:46 0:03 python /home/pi/bin/pigpio/ButtonStartServices.py
pi 346 0.0 3.2 71900 14204 ? Sl 08:46 0:03 python /home/pi/bin/pigpio/UDPClientFromWeatherSensor.py --udp-port 2222 --brightness-pin 17
pi 366 0.0 4.4 62480 19576 ? Sl 08:46 0:14 python /home/pi/webapp/run.py
pi 1292 0.0 3.7 74172 16624 ? Sl 12:29 0:03 python /home/pi/bin/pigpio/DisplayMessageToOLED.py --encoded-message %E9%9B%BB%E6%B1%A0%E5%88%87%E3%82%8C12%3A29
pi 1313 0.0 3.7 74172 16544 ? Sl 12:41 0:02 python /home/pi/bin/pigpio/DisplayMessageToOLED.py --encoded-message %E9%9B%BB%E6%B1%A0%E5%88%87%E3%82%8C12%3A41
pi 1333 0.0 3.8 74172 16772 ? Sl 12:53 0:02 python /home/pi/bin/pigpio/DisplayMessageToOLED.py --encoded-message %E9%9B%BB%E6%B1%A0%E5%88%87%E3%82%8C12%3A53
2-3. UDPモニターサービス再起動
#!/bin/bash
my_passwd=your_raspi_passwd
echo $my_passwd | {
sudo --stdin systemctl stop udp-weather-mon.service
sudo systemctl start udp-weather-mon.service
}
3. 天気予報表示プロセス起動ボタン押下
白いボタンを押さない限り延々と表示を繰り返します。
白いボタンを押すと天気予報表示プロセスが削除され表示が消えます。
実行ログ
# 黄色いボタン押下
2023-11-29 14:04:48 INFO ButtonStartServices.py(61)[change_actions] pin: 18, level: 1, tick: 2019127849
2023-11-29 14:04:48 INFO ButtonStartServices.py(78)[change_actions] Subprocess show_weather_forecast.sh thread start
2023-11-29 14:04:48 INFO ButtonStartServices.py(52)[subproc_start] Subprocess show_weather_forecast.sh start.
# 白いボタン押下: 天気予報表示プロセス削除
2023-11-29 14:05:12 INFO ButtonStartServices.py(61)[change_actions] pin: 12, level: 1, tick: 2043146570
2023-11-29 14:05:12 INFO ButtonStartServices.py(78)[change_actions] Subprocess kill_display_process.sh thread start
2023-11-29 14:05:12 INFO ButtonStartServices.py(52)[subproc_start] Subprocess kill_display_process.sh start.
2023-11-29 14:05:14 INFO ButtonStartServices.py(54)[subproc_start] Subprocess kill_display_process.sh terminated: CompletedProcess(args=['/home/pi/bin/kill_display_process.sh'], returncode=0)
2023-11-29 14:05:14 INFO ButtonStartServices.py(54)[subproc_start] Subprocess show_weather_forecast.sh terminated: CompletedProcess(args=['/home/pi/bin/show_weather_forecast.sh'], returncode=0)```
4. 結論
ラズパイでボタンに特定のシェルスクリプトを実行させたい場合、GPIOピン設定とシェルスクリプトを紐付けたた設定ファイルを作るとGPIOの初期化処理が効率的になります。
ボタンが増えた場合(逆に減った場合)でも設定ファイルを編集するだけですみます。
※但しボタン開始サービスの再起動は必要
最初の画像で紹介したシステムのソースコード(システムサービス登録含む)は下記GitHubリポジトリで公開しております。
GitHub(pipito-yukio) home_weather_sensors/installer/src/raspi_pigpio