Raspberry Piにシャットダウン・スイッチを接続

はじめに

2016年11月、Raspberry Pi2にシャットダウン・スイッチを接続しました。

Raspberry Pi2

Raspberry Pi2

Raspberry Pi2には、電源スイッチがありません。

  • パワーオンは、USB電源をONするとOSが起動します
  • パワーオフは、シャットダウンコマンドでOSを停止後にUSB電源をOFFします

パワーオフする時に、ターミナルを開いて シャットダウンコマンドを入力するのが面倒な場合があります。しかし、シャットダウンせずにUSB電源をOFFにするとSDカードを壊す可能性が高いです。

そこで、簡単な電子工作でRaspberry PiのGPIOポートを使用してタクトスイッチとLEDを接続して、タクトスイッチを長押しすると自動的に シャットダウンするようにしました。

制御ソフトはPythonで記述して、バックグラウンドで実行させるためにデーモン化してシステム起動時に自動で起動するようにしました。

Raspbianでデーモンを作成するには、2種類の方法があります。

  1. init.d で管理しているサービス (Wheezy以前の旧システム)
  2. systemdで管理しているサービス (Jessie以降の新システム)

Raspberry Pi2のRaspbian jessie OSを使用しているので、今回は systemd の管理でデーモン化しました。

 

pi@pi2note:~ $ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 8.0 (jessie)
Release:        8.0
Codename:       jessie

シャットダウン・スイッチの仕様

シャットダウン・スイッチの仕様を下記のように決めました。スイッチを一度押しただけでシャットダウンすると、誤って押してしまうことがありますので3秒間の長押しで シャットダウンします。また、スイッチが押されたことをLEDの点滅で確認できるようにします。

  1. システム起動時にデーモンとして自動起動するとLEDを2秒間点灯します
  2. タクトスイッチをONするとLEDが点滅し、点滅間隔が少しずつ短くなります
  3. 3秒間押し続けるとシャットダウンします
  4. 3秒以内にタクトスイッチをOFFすると、LEDは消灯しシャットダウンしません

電子工作編

手軽に電子工作して動作確認を行うために、電子部品を差し込んでジャンパーワイヤで接続するブレッドボードというものがあります。ブレッドボードにT型接続基板を装着して、40ピンのリボンケーブルでRaspberry PiのGPIOポートコネクタと接続すると、ブレッドボードの上でGPIOとの接続ができます。

ブレッドボードで電子工作

ブレッドボードで電子工作

 

GPIO出力でLEDを点滅

GPIOの21ピンの出力でLEDを点灯、または消灯する回路図を下記に示します。

LED出力の回路図

LED出力の回路図

 

  • GPIO出力を「1」にすると、GPIO出力の電圧は3.3Vとなり電流が抵抗とLEDに流れてLEDが点灯します
  • GPIO出力を「0」にすると、GPIO出力の電圧は0.0Vとなり電流が流れないのでLEDは消灯します

スイッチ状態をGPIOに入力

スイッチ状態をGPIOの26番ピンに入力する回路図を下記に示します。GPIOは内部にプルアップ抵抗とプルダウン抵抗を内蔵しており、設定により有効にすることができます。この接続では、内蔵プルダウン抵抗を有効にして使用します。

スイッチ入力の回路図

スイッチ入力の回路図

  • スイッチがONの時は、3.3Vの電圧がかかりGPIOに「1」が入力されます。プルダウン抵抗があるので、3.3VとGNDがショートすることはありません
  • スイッチがOFFの時は、プルダウン抵抗によりGND方向につながりGPIOに「0」が入力されます

回路の接続方法

ブレッドボードで回路を接続する方法を下記に示します。ブレッドボードの左右に配置されている赤色と青色の2列は縦1列がつながっています。青色側をマイナス、赤色側をプラスとして使用します。

電子回路の接続

電子回路の接続

LEDの出力回路は右側半分で接続しています。GPIOの21ピンの出力を抵抗(330Ω)とLEDに接続します。LEDは極性がありますので足の長い方のアノード側をプラス側、カソード側をマイナス側に接続します。

スイッチ状態の入力回路は左側半分で接続しています。3.3VとGPIOの26番ピンをタクトスイッチに接続します。タクトスイッチは4ピン構成で下記のような構造になっています。1ピンと2ピン、3ピンと4ピンは内部でつながっています。ボタンを押すと、1ピンと3ピン、2ピンと4ピンが通電する仕組みとなっています。

タクトスイッチの構造

タクトスイッチの構造

ソフトウェア編

PythonのGPIO制御用ライブラリ(RPi.GPIO)をインストールします。

sudo apt-get install python-rpi.gpio

/usr/local/sbin に、シャットダウン・スイッチを制御するPythonスクリプトを作成します。

この領域は管理者権限が必要なので、「sudo su」コマンドで管理者権限モードになり操作します。

pi@pi2note:/usr/local/sbin $ cat shutdownswd.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

u""" SW ON で shutdown するデーモン

 デーモンが起動すると LED が2秒間点灯します
 SW ON で LEDが点滅して 3秒間押し続けると shutdown します
 3秒以内に SW OFF すると LED は消灯し shutdown しません
"""
__author__  = 'Yuki'
__version__ = '0.1'

#
import RPi.GPIO as GPIO
import time
import sys
import os
from threading import (Event, Thread)

FILE_PID = '/var/run/shutdownswd.pid'
OUT_LED = 21      # GPIO OUT
INP_SW1 = 26      # GPIO INP

req_halt_event = Event()

#
def cb_check_sw(ch):
    u"""  3秒間 SW ON のチェック コールバック関数

    3秒 LED 点滅の間 SW ON が継続するとHALT要求イベントを通知する
    """

    for dout in [1,1,1,0,0,1,1,0,0,1,0,1,0,1,0]:
        GPIO.output(OUT_LED ,dout)
        time.sleep(0.2)           # 0.2s × 15回 = 3秒
        sw = GPIO.input(ch)
        if sw == 0:
            break
    GPIO.output(OUT_LED ,0)
    if sw == 1:
        req_halt_event.set()       # ---> Set Event req HALT

#
def deamon_main():
    u""" SW ON が3秒間継続すると shutdown するデーモンメイン

    1. デーモン起動すると 2秒間 LED を点灯します
    2. SW 立ち上がりエッジ検出イベントを登録します
    3. イベント検出時、SW状態チェックコールバック関数を呼び出します
    4. HALT要求イベントを待ちます
    5. shutdown します
    """

    GPIO.setwarnings(False)
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(OUT_LED,GPIO.OUT)
    GPIO.setup(INP_SW1,GPIO.IN ,pull_up_down=GPIO.PUD_DOWN)

    GPIO.output(OUT_LED, 1)
    time.sleep(2)
    GPIO.output(OUT_LED, 0)

    GPIO.add_event_detect  (INP_SW1 ,GPIO.RISING ,bouncetime=200)
    GPIO.add_event_callback(INP_SW1 ,cb_check_sw)

    req_halt_event.wait()         # <-- Wait Event req HALT
    req_halt_event.clear()

    GPIO.remove_event_detect(INP_SW1)
    GPIO.cleanup(INP_SW1)
    GPIO.cleanup(OUT_LED)

    os.system("shutdown -h now")
    sys.exit()

#
def deamon_fork():
    u""" 子プロセスを生成して、デーモンメインを呼び出す

    fork() でプロセスのコピーを作成して、pidを記録して親プロセスを終了する
    子プロセスは、デーモンメインを呼び出す
    """

    pid = os.fork()
    if pid > 0:                      # Parent
        fp = open(FILE_PID,'w')
        fp.write(str(pid)+"\n")
        fp.close()
        sys.exit()                   # Parent Exit

    if pid == 0:                     # Child
        deamon_main()

#
if __name__=='__main__':
    deamon_fork()

Pythonコメントによるドキュメント

このPythonスクリプトは、docstring形式でコメントを記述していますのでコメントからドキュメントを生成することができます。

pythonを起動して、対話形式でimport した後に、helpコマンドでドキュメントが出力されます。

pi@pi2note:/usr/local/sbin $ python
Python 2.7.9 (default, Sep 17 2016, 20:26:04)
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import shutdownswd
>>> help(shutdownswd)
shutdownswdドキュメント

shutdownswdドキュメント

ライブラリのインポート

GPIOを使用するために、GPIOライブラリをインポートします。その他に、time,sys,os ライブラリもインポートします。イベント通知を行うので、Event系もインポートします。

import RPi.GPIO as GPIO
import time
import sys
import os
from threading import (Event, Thread)

GPIOのチャンネル番号定義

GPIOのチャンネル番号を定義します。LED出力を21番ピン、スイッチ入力を26番ピンとします。

OUT_LED = 21      # GPIO OUT
INP_SW1 = 26      # GPIO INP

GPIO関係

deamon_main()関数の中を見てゆきます。

チャンネルの指定方法

2種類のチャンネルの指定モードがあり選択します。

  • GPIO.BOARD : 物理的なGPIOのボード番号(P1から順番にボードに並ぶ番号)
  • GPIO.BCM : GPIOのピン番号
GPIO.setmode(GPIO.BCM)

今回は、GPIOのピン番号を選択します。

チャンネルのモード設定

チャンネルの入出力モードを設定します。

  • LEDのチャンネルを GPIO.OUT で出力モードにします
  • SWのチャンネルを GPIO.IN で入力モードにします

入力モードの時、pull_up_downパラメータを指定することにより、プルアップ抵抗(GPIO.PUD_UP)、またはプルダウン抵抗(GPIO.PUD_DOWN)を有効にすることができます。今回の回路では、GPIO入力チャンネルにプルダウン抵抗を使用します。

GPIO.setup(OUT_LED,GPIO.OUT)
GPIO.setup(INP_SW1,GPIO.IN ,pull_up_down=GPIO.PUD_DOWN)

LEDの制御

LEDの制御は、GPIO.output()関数でGPIOチャンネルに「1:点灯」/「0:消灯」を出力します。

デーモンが起動したので、2秒間LEDを点灯します。

  • OUT_LEDチャンネルに「1」を出力してLEDを点灯します
  • 2秒間スリープして時間待ちします
  • OUT_LEDチャンネルに「0」を出力してLEDを消灯します
GPIO.output(OUT_LED, 1)
time.sleep(2)
GPIO.output(OUT_LED, 0)

スイッチの入力

スイッチの入力は、GPIO.input()関数でGPIOチャンネルから入力します。

スイッチ状態のエッジを検出して、イベントを発生させることもできます。
「add_event_detect()」関数で、INP_SW1チャンネルのエッジ検出イベントを登録します。

検出するエッジの指定は下記3種類です。

  •  GPIO.RISING (立ち上がりエッジ)
  •  GPIO.FALLING (立ち下がりエッジ)
  •  GPIO.BOTH (立ち上がりまたは立ち下がりエッジ)

今回は、タクトスイッチを押すと電気信号がLOWからHIGHになりますので、立ち上がりエッジを検出します。

スイッチ入力を行う時は、チャタリングが発生します。チャタリングとは、オン・オフを切り替える時にオン・オフが細かく繰り返される現象のことです。このため、立ち上がりエッジが何回も発生してしまいます。このチャタリングを防止するために、bouncetime パラメータを指定します。これにより、エッジ検出イベント発生直後からバウンス時間に指定された時間に発生した他のエッジ検出イベントが無視されるため、イベントが複数回発生するのを避けることができます。

「add_event_callback()」関数で、イベントが発生した場合に呼び出すコールバック関数を登録します。コールバック関数は、別スレッドで実行されます。

GPIO.add_event_detect  (INP_SW1 ,GPIO.RISING ,bouncetime=200)
GPIO.add_event_callback(INP_SW1 ,cb_check_sw)

このイベント登録により、タクトスイッチの立ち上がりイベントを検出してコールバック関数をスレッドとして実行します。

シャットダウン要求のイベント通知

コールバック関数スレッドでシャットダウンの条件判断を行い、デーモンメイン関数にシャットダウン要求イベント通知を行います。

初期化で、req_halt_eventイベント変数を定義します。

req_halt_event = Event()

デーモンメイン関数では、req_halt_event.wait()でシャットダウン要求イベントを待ちます。

  • シャットダウン要求イベントを受けるまで、待ち状態になります
  • シャットダウン要求イベントを受信した後は、req_halt_event.clear()でイベントをクリアします
req_halt_event.wait()         # <-- Wait Event req HALT
req_halt_event.clear()

コールバック関数スレッドではシャットダウン条件となった時に、req_halt_event.set() でシャットダウン要求イベントを通知します。

req_halt_event.set()       # ---> Set Event req HALT

GPIO入力のイベント検知の解除

タクトスイッチの立ち上がりイベント検知を解除します。

GPIO.remove_event_detect(INP_SW1)

GPIOの終了処理

cleanup()関数で、使用したチャンネルの終了処理をします。

GPIO.cleanup(INP_SW1)
GPIO.cleanup(OUT_LED)

シャットダウンの実行

OSのshutdownコマンドを実行して、システムをシャットダウンします。
その後、exit()でデーモンを終了させます。

os.system("shutdown -h now")
sys.exit()

コールバック関数部

タクトスイッチの立ち上がりエッジを検出した時にコールバックされるcb_check_sw()関数の中を見てゆきます。

スイッチ長押しチェック

LEDを点滅させながらスイッチの状態を調べます。0.2秒スリープしながら15回ループします。ループごとに、OUT_LEDチャンネルに「1」か「0」を出力してLEDを点滅させます。点滅パターンは、for文のリストで指定します。スリープ後にINP_SW1チャンネルのスイッチの状態を変数swに入力します。

  • 変数 sw=0の時は、途中でスイッチをオフしたのでループを脱出します
  • 変数 sw=1の時は、ループを継続します

15回すべてが「1」の時は、最終的に変数 swは「1」となります。ループ終了後、OUT_LEDチャンネルに「0」を出力してLEDを消灯します。

for dout in [1,1,1,0,0,1,1,0,0,1,0,1,0,1,0]:
    GPIO.output(OUT_LED ,dout)
    time.sleep(0.2)           # 0.2s × 15回 = 3秒
    sw = GPIO.input(ch)
    if sw == 0:
        break
GPIO.output(OUT_LED ,0)

シャットダウン要求イベント通知

変数 sw=1の時は、3秒間スイッチがオンなので、シャットダウン要求イベントを通知します。

if sw == 1:
    req_halt_event.set()       # ---> Set Event req HALT

デーモンプロセスの生成部

deamon_fork()を見ていきます。

最初に、fork()関数によりプロセスを複製して新しいプロセスを生成します。戻り値は、プロセスIDで変数pidに代入しますす。

  • 変数 pid > 0 の時は、親プロセスでpidは分岐した子プロセスの番号なのでファイルに記録して終了します
  • 変数 pid = 0 の時は、子プロセスで deamon_main()関数でデーモンのメイン処理を行います。
FILE_PID = '/var/run/shutdownswd.pid'

    pid = os.fork()
    if pid > 0:                      # Parent
        fp = open(FILE_PID,'w')
        fp.write(str(pid)+"\n")
        fp.close()
        sys.exit()                   # Parent Exit

    if pid == 0:                     # Child
        deamon_main()

systemdによるデーモン設定ファイル

デーモンを登録するためにサービス設定ファイルを作成します。サービス名を「shutdownswd」とします。ファイルの設置場所は、下記のどちらかとなります。

  • /etc/systemd/system/shutdownswd.service
  • /usr/lib/systemd/system/shutdownswd.service

前者はユーザー設定用で、後者はシステム設定用に使用されるようなので、今回は前者に設定しました。

pi@pi2note:~ $ sudo cat /etc/systemd/system/shutdownswd.service
[Unit]
Description = shutdown switch daemon

[Service]
ExecStart = /usr/local/sbin/shutdownswd.py
Restart = always
Type = forking
PIDFile=/var/run/shutdownswd.pid

[Install]
WantedBy = multi-user.target

Unit セクション

[Unit]セクションは、systemdの管理単位です。

Descriptionは、Unitの説明を記述します。
その他に、依存関係や起動順について記載できますが、今回の使用では単独で使用するので何も記述しません。

Service セクション

[Service]セクションは、サービスの制御について記述します。

  • ExecStartは、サービス起動コマンドのパス名を記述します
  • ExecReloadは、サービスリロードコマンドのパス名を記述します(今回は指定しません)
  • ExecStopは、サービス停止コマンドのパス名を記述します(今回は指定しません)
  • Restartは、サービスプロセス停止時の再起動条件で「always:常に再起動を試みる」とします
  • Typeは、サービスプロセスの起動完了の判定方法で、「forking」とします
  • PIDFileは、プロセスIDのパス名を記述します

Install セクション

[Install]セクションは、Unitを有効にした時の動作を記述します。

WantedByは、有効時にこのUnitの.wantsディレクトリにリンクを作成します。

multi-user.targetは、デーモンのrunレベルでrunレベル2~4に相当します。

サービス設定ファイルを記述したら、サービスの再読み込みをします。

sudo systemctl daemon-reload

systemdによるデーモン起動

  • サービスのスタート
sudo systemctl start shutdownswd
  • サービスの状態を確認
sudo systemctl status shutdownswd
  • システム起動時に自動的サービス起動を有効にする
sudo systemctl enable shutdownswd
  • システム起動時に自動的サービス起動を無効にする
sudo systemctl disable shutdownswd

最後に

shutdownswdサービスを有効にして、リブートして実行してみます。

リブート直後に、LEDが2秒間点灯したので、デーモンが動作開始したことがわかります。タクトスイッチをオンにすると、LEDが点滅し少しずつ点滅間隔が短くなります。タクトスイッチをオフすると、LEDは消灯します。今度は、タクトスイッチを3秒間長押します。シャットダウンが開始されました。

ターミナルを開いて、 shutdownコマンドを入力しなくてもシャットダウンできるので便利になりました。

そのうちに、ブレットボードに組んだ回路をRaspberry Pi2に組み込もうと考えています。