Pythonでデバイスを制御しよう

第2回:プッシュボタンを扱う(4) イベント駆動でプッシュボタンに反応する

プッシュボタンを扱う(1) Pythonでプッシュボタンを扱うには
プッシュボタンを扱う(2) 誤動作の原因となるチャタリングを防止する
プッシュボタンを扱う(3) ポーリング制御でプッシュボタンに反応する

 

「プッシュボタンを扱う」のパート4では、タクトスイッチなどのボタンに合わせて処理をする際にポーリング制御と並んでよく用いられるイベント駆動の制御方法を解説します。
パート3では無限ループしながら自分でボタンの動きを監視するポーリング制御を紹介しました。今回のイベント駆動はボタンの状態が変化した時に、あらかじめ登録した関数が自動的に呼び出される方法です。
イベント駆動はソフトウェアとの相性が良い制御方法であり、OSが搭載されたパソコンなどで動作するソフトウェアは一般的にこの方法でコーティングを進めます。
直感的で便利な手法ですが注意点もあります。今回は、イベント駆動で処理を実装する際のポイントをPythonのプログラムを作成しながら解説していきます。

control-device-with-python-vol2-01-01

 

目次

  1. イベント駆動とは
  2. コールバック関数をイベント駆動で呼び出す
    1. 2.1. 本プログラムの構成
    2. 2.2. コールバック関数でボタンの処理を実装するPythonプログラム p4-1.py
    3. 2.3. プログラムの説明
  3. イベント駆動の欠点
    1. 3.1. イベント駆動の欠点とは?
    2. 3.2. 本プログラムの構成
    3. 3.3. コールバック関数内で時間のかかる処理を行うPythonプログラム p4-2.py
    4. 3.4. プログラムの説明
    5. 3.5. プログラムの実行と検証
  4. ポーリング制御とイベント駆動を組み合わせる
    1. 4.1. 状態遷移を使った制御とは
    2. 4.2. 本プログラムの構成
    3. 4.3. ポーリング制御とイベント駆動を組み合わせたPythonのプログラム p4-3.py
    4. 4.4. プログラムの説明
    5. 4.5. プログラムの実行と検証
  5. まとめ

 

イベント駆動とは

パート3では無限ループしながら自分でボタンの動きを監視するポーリング制御を紹介しました。一定の間隔で繰り返されるループの中で「ボタンAが押された時はこの処理を実行」というように、必要な数だけ条件判定が並ぶのが特徴です。
一方、今回紹介するイベント駆動は実行する処理をあらかじめ関数として実装・システムに登録し、ボタンの状態が変化した時に呼び出す方法です。この自動的に呼び出される関数を「コールバック関数」と呼びます。

イベント駆動の概要

 

イベント駆動とは、「きっかけ」と「動作」を関連付けるコーディング手法とも言えるでしょう。
この「きっかけ」をプログラムでは「イベント」と呼びます。例えば「ボタンが押されたら」といった特定のイベントと「メッセージを表示する」などのイベントに応じて実行する処理を関連付けてプログラミングするので「イベント駆動」と呼ばれます。

ラズパイでGPIOピンのイベントを発生させるのはOSにインストールされたデバイスドライバです。通常通りにラズパイをセットアップすれば、このデバイスドライバはインストールされています。
本記事では、このデバイスドライバへのアクセスを簡略化するライブラリに「RPi.GPIO」を使用します。

プログラムとRPi.GPIO、デバイスドライバの関係

 

プログラムにはGPIOピンの状態が変化した時に実行するコールバック関数を用意し、GPIOピンを初期化する際にRPi.GPIOに登録しておきます。
RPi.GPIO内ではデバイスドライバが発する「GPIOピンの状態が変化したことを伝える通知」を監視し、変化があった際には初期化時に登録したコールバック関数を呼び出します。
この時、主処理で実行中の処理とコールバック関数の呼び出しは非同期です。

 

コールバック関数をイベント駆動で呼び出す

実際にコールバック関数を作成してイベント駆動で実行してみます。

本プログラムの構成

以下がコールバック関数でボタンの処理を実装するPythonのプログラムの構成です。

システム構成

 

このプログラムの動作にはプッシュボタン1個とライブラリ「RPi.GPIO」が必要です。パート1を参考に配線とインストールを行ってください。

コールバック関数でボタンの処理を実装するPythonプログラム p4-1.py

以下がボタンを押した時の処理をコールバック関数で実装するPythonのプログラムです。
このプログラムはパート2の『チャタリングの波形と問題となる動作』で紹介したものと同じです。ただしチャタリング対策として
「bouncetime=200」
としている点に注意してください。

#!/usr/bin/env python

import sys
import time
import datetime

import RPi.GPIO as GPIO


# ボタンは"GPIO5"に接続
BUTTON = 5


# 主処理
def main():
    
    try:
        # 操作対象のピンは「GPIOn」の"n"を指定する
        GPIO.setmode(GPIO.BCM)
        # BUTTONがつながるGPIOピンの動作は「入力」「プルアップあり」
        GPIO.setup(BUTTON, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        
        # 立ち下がり(GPIO.FALLING)を検出する(プルアップなので通常時1/押下時0)
        GPIO.add_event_detect(BUTTON, GPIO.FALLING, bouncetime=200)
        # イベント発生時のコールバック関数を登録
        GPIO.add_event_callback(BUTTON, button_pressed)
        
        # 無限ループ
        while True:
            # 主処理は何もしない
            time.sleep(1)
        
    # キーボード割り込みを捕捉
    except KeyboardInterrupt:
        print("例外'KeyboardInterrupt'を捕捉")
    
    print("処理を終了します")
    
    # GPIOの設定をリセット
    GPIO.cleanup()
    
    return 0


# ボタンAが押された時に呼び出されるコールバック関数
# gpio_no: イベントの原因となったGPIOピンの番号
def button_pressed(gpio_no):
    
    # メッセージを表示
    print_message("ボタンが押されました")


# ターミナル上に「日付 時刻.マイクロ秒: メッセージ」を表示する関数
# message: 表示する「メッセージ」
def print_message(message):
    
    # 現在の日付時刻を取得して「年-月-日 時:分:秒.マイクロ秒」にフォーマット
    now = datetime.datetime.now()
    timestamp = now.strftime("%Y-%m-%d %H:%M:%S.%f")
    
    # 引数で送られたメッセージを表示
    print("{}: {}".format(timestamp, message))


if __name__ == "__main__":
    sys.exit(main())

プログラムの説明

コールバック関数はbutton_pressed()関数です。関数名は自由につけることができますが、引数「gpio_no」は必須です。この関数が呼び出された際にイベントの原因となったGPIOピンの番号が渡されます。
またイベント駆動のポイントは以下の2行です。

# 立ち下がり(GPIO.FALLING)を検出する(プルアップなので通常時1/押下時0)
GPIO.add_event_detect(BUTTON, GPIO.FALLING, bouncetime=200)
# イベント発生時のコールバック関数を登録
GPIO.add_event_callback(BUTTON, button_pressed)

add_event_detect()関数で何を検出してイベントにするかを設定します。引数は以下の通りです。

引数 内容
BUTTON GPIOピンの番号(プログラムの冒頭で「BUTTON = 5」を設定)
GPIO.FALLING 信号の立ち下がりを検出した際にイベントを発生
bouncetime=200 イベント発生後、200ミリ秒間は続くイベントを無視

今回はプッシュボタンをGPIO5に接続しているので5を指定します。またGPIO5はプルアップに設定しているため、ボタンを押した瞬間が立ち下がり(GPIO.FALLING)です。

イベントの立ち上がりと立ち下がり

 

GPIO5はチャタリング対策としてイベント発生後、200ミリ秒間は続くイベントを無視します。ただし、この設定で対処できるのはボタンを押した時のチャタリングに限ります。

200ミリ秒間は続くイベントを無視

 

この数字を大きくすればボタンを離した時のチャタリングも無視できますが、連続でボタンを押しても無視するため「ボタンの反応が鈍い」と感じるようになる可能性があります。
この問題についてはのちほど『ポーリング制御とイベント駆動を組み合わせる』で状態遷移を使った対処方法を紹介します。

使用した関数の詳細な説明はRPi.GPIOライブラリの公式ドキュメント(英語)を参照してください。

プログラムを終了させるためのKeyboardInterruptの扱いはパート3のポーリング制御と同じです。
メッセージを表示するための自作関数「print_message()」はパート2で紹介したプログラムでも使用しました。引数で指定された文字列にタイムスタンプを付加してターミナル上に表示します。

 

イベント駆動の欠点

前述の通りイベント駆動は「必要な時に必要な処理を起動する」方法なので無駄がないように見えますが欠点もあります。

イベント駆動の欠点とは?

イベント駆動は必要な時に処理を起動する無駄のない手法です。
しかしながらRPi.GPIOには
「イベントの実行中は、ほかのイベントがブロックされる」
という欠点があります(検証に使用したRPi.GPIOのバージョンは0.7.0)。

イベント処理中はほかのイベントがブロックされる

 

これはコールバック関数内で時間のかかる処理を行うと、それが終了するまで、ほかのコールバック関数が呼び出されないことを意味します。
試しにコールバック関数内で終了までに10秒かかる処理を行うプログラムを作成し、処理中にボタンを何度か押してみる実験を行ってみましょう。

本プログラムの構成

以下がコールバック関数内で実行に10秒かかる処理を行うPythonのプログラムの構成です。

システム構成

 

このプログラムの動作にはプッシュボタン2個とライブラリ「RPi.GPIO」が必要です。パート1を参考に配線とインストールを行ってください。

プログラムは2個のボタンを使用し、それぞれ「ボタンA」「ボタンB」とします。
ボタンAを押すと2秒ごとにメッセージを5回表示する処理が起動されます。この処理は終了まで10秒かかります(2秒×5回=10秒)。
ボタンAの処理中にボタンBを押すとボタンAの処理を途中で打ち切り、ボタンBの処理が開始される動作を想定しています。

コールバック関数内で時間のかかる処理を行うPythonプログラム p4-2.py

以下がコールバック関数内で実行に10秒かかる処理を行うPythonのプログラムです。p4-2.pyの名前で保存してください。
後述するように、このプログラムは想定通りに動きません。

#!/usr/bin/env python

import sys
import time
import datetime

import RPi.GPIO as GPIO


# ボタンは"GPIO5/GPIO6"に接続
BUTTON_A = 5
BUTTON_B = 6

# ボタンAの処理状況
g_button_a = False


# 主処理
def main():
    
    try:
        # 操作対象のピンは「GPIOn」の"n"を指定する
        GPIO.setmode(GPIO.BCM)
        
        # 使用するボタンの情報 [GPIOピン番号, コールバック関数]
        buttons = [
            [BUTTON_A, button_a_pressed],
            [BUTTON_B, button_b_pressed],
        ]
        
        for button in buttons:
            # ボタンがつながるGPIOピンの動作は「入力」「プルアップあり」
            GPIO.setup(button[0], GPIO.IN, pull_up_down=GPIO.PUD_UP)
            
            # 立ち下がり(GPIO.FALLING)を検出する(プルアップなので通常時1/押下時0)
            GPIO.add_event_detect(button[0], GPIO.FALLING, bouncetime=100)
            # イベント発生時のコールバック関数を登録
            GPIO.add_event_callback(button[0], button[1])
        
        # 無限ループ
        while True:
            # 主処理は何もしない
            time.sleep(1)
        
    # キーボード割り込みを捕捉
    except KeyboardInterrupt:
        print("例外'KeyboardInterrupt'を捕捉")
    
    print("処理を終了します")
    
    # GPIOの設定をリセット
    GPIO.cleanup()
    
    return 0


# ボタンAが押された時に呼び出されるコールバック関数
# gpio_no: イベントの原因となったGPIOピンの番号
def button_a_pressed(gpio_no):
    
    # 関数内でグローバル変数を操作
    global g_button_a
    
    # メッセージを表示
    print_message("ボタンAが押されました。時間がかかる処理を開始")
    
    # 時間のかかる処理を行う(2秒ごとにメッセージ×5回)
    g_button_a = True
    loop = 0
    while g_button_a:
        # 100ミリ秒間休止
        time.sleep(0.1)
        
        # 終了判定
        loop += 1
        if 100 < loop:
            g_button_a = False
        
        # 2秒おきにメッセージ
        if loop % 20 == 0:
            print_message("実行中...")
    
    print_message("時間がかかる処理を終了します")


# ボタンBが押された時に呼び出されるコールバック関数
# gpio_no: イベントの原因となったGPIOピンの番号
def button_b_pressed(gpio_no):
    
    # 関数内でグローバル変数を操作
    global g_button_a
    
    # メッセージを表示
    print_message("ボタンBが押されました。処理をキャンセル")
    
    # ボタンAの処理を停止
    g_button_a = False


# ターミナル上に「日付 時刻.マイクロ秒: メッセージ」を表示する関数
# message: 表示する「メッセージ」
def print_message(message):
    
    # 現在の日付時刻を取得して「年-月-日 時:分:秒.マイクロ秒」にフォーマット
    now = datetime.datetime.now()
    timestamp = now.strftime("%Y-%m-%d %H:%M:%S.%f")
    
    # 引数で送られたメッセージを表示
    print("{}: {}".format(timestamp, message))


if __name__ == "__main__":
    sys.exit(main())

プログラムの説明

今回はプッシュボタンを2個使用します。それぞれのボタンの初期設定は概ね同じ手順なのでfor文を使って処理し、2個のボタンで異なる値は二次元配列「buttons」を使って指定します。

ボタンAのコールバック関数内で行う時間のかかる処理を、ボタンBのコールバック関数内から打ち切るため、両者の情報伝達にグローバル変数「g_button_a」を使用します。グローバル変数はプログラム内の至る所からアクセスでき不具合の原因となりやすいので、変数名の頭に「g_」を付加することで識別しやすくします。

プログラムの実行と検証

プログラムを保存したディレクトリに移動したら以下のコマンドでプログラムを起動してください。

./p4-2.py

実行中にボタンAを押すと次のようなメッセージが表示されます。

2021-02-23 17:54:37.790045: ボタンAが押されました。時間がかかる処理を開始
2021-02-23 17:54:39.793643: 実行中...
2021-02-23 17:54:41.797240: 実行中...
2021-02-23 17:54:43.800741: 実行中...
2021-02-23 17:54:45.804307: 実行中...
2021-02-23 17:54:47.807877: 実行中...
2021-02-23 17:54:47.908355: 時間がかかる処理を終了します

次に、ボタンAを押し処理が開始されたことを確認したら、すぐにボタンBを押してください。想定では
「ボタンBが押されました。処理をキャンセル」
と表示されて直ちにボタンAの処理が終了するはずです。
ところが実際は次のようになってしまいます。

2021-02-23 17:55:32.824178: ボタンAが押されました。時間がかかる処理を開始
2021-02-23 17:55:34.827452: 実行中...
2021-02-23 17:55:36.830715: 実行中...
2021-02-23 17:55:38.834176: 実行中...
2021-02-23 17:55:40.837669: 実行中...
2021-02-23 17:55:42.841166: 実行中...
2021-02-23 17:55:42.941546: 時間がかかる処理を終了します
2021-02-23 17:55:42.941756: ボタンBが押されました。処理をキャンセル

これはボタンAのコールバック関数が終了するまで、ボタンBのコールバック関数の呼び出しがブロックされてしまうためです。
ただし、この現象はRPi.GPIO(使用したバージョンは0.7.0)固有の現象であり、イベント駆動自体の欠点ではありません。
しかしながら、イベントによって起動されたコールバック関数内の処理を、ほかのイベントから制御するのは比較的難しい作業です。
これはイベント駆動を実現する仕組みに起因します。イベント駆動では「原因となる事象の検出」と「コールバック関数の呼び出し」にスレッドという技術を利用するのが一般的です。RPi.GPIOはこの部分に一本のスレッドを使用しているため、特定のコールバック関数を実行している間は、ほかのコールバック関数が呼び出されないことになります。
OSが乗っているシステムで本格的に制御を行うためにはスレッドの取り扱いが重要です。スレッドに関しては別の機会に解説します。

次に解説するのは、ポーリング制御とイベント駆動を状態遷移の手法で組み合わせることでこの現象を解決する方法です。この方法はイベント駆動の実装の違いを吸収する方法であり、さまざまなシステムに応用できるでしょう。

 

ポーリング制御とイベント駆動を組み合わせる

イベント駆動ではコールバック関数が呼び出されるタイミングは実装に大きく依存します。本記事で紹介したRPi.GPIOには
「コールバック関数の実行中は、ほかのコールバック関数の呼び出しがブロックされる」
という問題があります。
前項では
「ボタンAで開始する時間のかかる処理を、ボタンBを押してキャンセルする」
の例としてPythonのプログラムを作成して説明しました。

この問題はパート3で紹介した状態遷移の考え方で解決できます。
これは、ポーリング制御とイベント駆動を状態遷移の考え方で組み合わせるような方法であり、非常に応用しやすい手法です。
状態遷移の説明はパート3も合わせて確認してください。

状態遷移を使った制御とは

状態遷移を使った制御とは
「あらかじめ定義したシステムの取り得る状態をコントロールすることで一連の処理を実現」
する手法で実際の電子機器の制御にも用いられています。
今回実現したい動作は
「ボタンAで開始する時間のかかる処理を、ボタンBを押してキャンセルする」
です。
そこで今回は「待機中」「処理中」「復帰中」の三つの状態を定義し、状態とそれに関連する動作を次のように定義します。

状態 動作
待機中 ボタンAが押されるのを監視
処理中 時間がかかる処理を実行。ボタンBが押されるのを監視
復帰中 連続して処理が起動しないための保護時間。ボタンの入力は受け付けない

プログラムの動作を開始する「ボタンA」は待機中のみ、動作をキャンセルする「ボタンB」は処理中のみ入力を受け付けます。
復帰中はボタンを連打された際などに、連続して処理を起動しない時間(保護時間)を確保するための状態です。

本プログラムの構成

ポーリング制御とイベント駆動を状態遷移の考え方で組み合わせるPythonのプログラムも、前の『イベント駆動の欠点』と全く同じ構成です。

このプログラムの動作にはプッシュボタン2個とライブラリ「RPi.GPIO」が必要です。パート1を参考に配線とインストールを行ってください。

プログラムは2個のボタンを使用し、それぞれ「ボタンA」「ボタンB」とします。
ボタンAを押すと2秒ごとにメッセージを5回表示する処理が起動します。この処理は終了まで10秒かかります(2秒×5回=10秒)。その間にボタンBを押すとボタンAの処理を途中で打ち切る想定です。

ポーリング制御とイベント駆動を組み合わせたPythonのプログラム p4-3.py

ポーリング制御とイベント駆動を状態遷移の考え方で組み合わせるPythonのプログラムです。p4-3.pyの名前で保存してください。

#!/usr/bin/env python

import sys
import time
import datetime

import RPi.GPIO as GPIO


# ボタンは"GPIO5/GPIO6"に接続
BUTTON_A = 5
BUTTON_B = 6

# ボタンA/ボタンBが押されたことを主処理に伝える
g_button_a = False
g_button_b = False


# 主処理
def main():
    
    # 関数内でグローバル変数を操作
    global g_button_a
    global g_button_b
    
    # 現在の状態を管理
    status = 0
    
    # 復帰中に経過時間を計測する
    resume = 0
    
    # 処理中に経過時間を計測する
    loop = 0
    
    try:
        # 操作対象のピンは「GPIOn」の"n"を指定する
        GPIO.setmode(GPIO.BCM)
        
        # 使用するボタンの情報 [GPIOピン番号, コールバック関数]
        buttons = [
            [BUTTON_A, button_a_pressed],
            [BUTTON_B, button_b_pressed],
        ]
        
        for button in buttons:
            # ボタンがつながるGPIOピンの動作は「入力」「プルアップあり」
            GPIO.setup(button[0], GPIO.IN, pull_up_down=GPIO.PUD_UP)
            
            # 立ち下がり(GPIO.FALLING)を検出する(プルアップなので通常時1/押下時0)
            GPIO.add_event_detect(button[0], GPIO.FALLING, bouncetime=100)
            # イベント発生時のコールバック関数を登録
            GPIO.add_event_callback(button[0], button[1])
        
        # 無限ループ
        while True:
            # 100ミリ秒間休止
            time.sleep(0.1)
            
            # 「待機中」はボタンAを確認して処理を起動する
            if status == 0:
                if g_button_a:
                    # 状態を「処理中」に
                    status = 1
                    print_message("ボタンAが押されました。時間がかかる処理を開始")
            
            # 「処理中」はボタンBを確認して処理をキャンセル
            elif status == 1:
                loop += 1
                if g_button_b:
                    # キャンセル。状態を「復帰中」に
                    status = 2
                    loop = 0
                    print_message("ボタンBが押されました。処理をキャンセル")
                elif 100 < loop:
                    # 終了。状態を「復帰中」に
                    status = 2
                    loop = 0
                    print_message("時間がかかる処理を終了します")
                elif loop % 20 == 0:
                    # 2秒おきにメッセージ
                    print_message("実行中...")
            
            # 「復帰中」は1秒経過するのを待つ
            elif status == 2:
                # ループの回数をカウント
                resume += 1
                if 10 <= resume:
                    # 1秒経過したので状態を「待機中」に
                    status = 0
                    resume = 0
                    print_message("1秒経過しました。待機中に遷移")
            
            # ボタンが押されたことは上のif文で確認済み
            g_button_a = False
            g_button_b = False
    
    # キーボード割り込みを捕捉
    except KeyboardInterrupt:
        print("例外'KeyboardInterrupt'を捕捉")
    
    print("処理を終了します")
    
    # GPIOの設定をリセット
    GPIO.cleanup()
    
    return 0


# ボタンAが押された時に呼び出されるコールバック関数
# gpio_no: イベントの原因となったGPIOピンの番号
def button_a_pressed(gpio_no):
    
    # 関数内でグローバル変数を操作
    global g_button_a
    g_button_a = True


# ボタンBが押された時に呼び出されるコールバック関数
# gpio_no: イベントの原因となったGPIOピンの番号
def button_b_pressed(gpio_no):
    
    # 関数内でグローバル変数を操作
    global g_button_b
    g_button_b = True


# ターミナル上に「日付 時刻.マイクロ秒: メッセージ」を表示する関数
# message: 表示する「メッセージ」
def print_message(message):
    
    # 現在の日付時刻を取得して「年-月-日 時:分:秒.マイクロ秒」にフォーマット
    now = datetime.datetime.now()
    timestamp = now.strftime("%Y-%m-%d %H:%M:%S.%f")
    
    # 引数で送られたメッセージを表示
    print("{}: {}".format(timestamp, message))


if __name__ == "__main__":
    sys.exit(main())

プログラムの説明

今回もプッシュボタンを2個使用します。それぞれのボタンの初期設定は概ね同じ手順なのでfor文を使って処理し、2個のボタンで異なる値は二次元配列「buttons」を使って指定します。

前回と大きく異なるのは、時間のかかる処理をボタンAのコールバック関数内で行わない点です。ボタンBも前回はコールバック関数内でボタンAの処理をキャンセルしようとしましたがこれも行いません。
ボタンA、ボタンBのコールバック関数「button_a_pressed()・button_b_pressed()」ではそれぞれグローバル変数「g_button_a・g_button_b」にTrueを設定するだけです。
これによってコールバック関数の実行がほかに及ぼす影響を最小限にできます。一方でコールバック関数の呼び出しで通知されるボタンの変化は、グローバル変数「g_button_a・g_button_b」の値の変化として主処理の無限ループに伝えられます。
主処理の無限ループでボタンが押されたことが判別できれば、あとはパート3で紹介したポーリング制御と状態遷移を組み合わせた時と同じように制御できます。
ただし前回は「待機中」「復帰中」の二つの状態でしたが、今回はそれに「処理中」が加わります。それぞれの状態と動作の関係は前述した『状態遷移とは』の通りです。
変数「status」で状態を管理し、必ず0(待機中)・1(処理中)・2(復帰中)のいずれかが格納されるようにコーディングします。また無限ループの中ではif文を使用してそれぞれの状態に相応しい動作を実装します。
処理中・復帰中はループの回数によって経過時間を計測するため、それぞれ変数「loop・resume」を使用します。

この制御方法では無限ループ内の処理が大きくなりがちです。関数内の処理はできるだけ小さくすることが定石ですが、この部分に限っては、ここに処理を集中させた方が「システムの全体像を把握しやすくなる」といった利点も生まれます。とはいえ、関数が大きくなることで不具合の原因につながるのも間違いないので、遷移をコントロールする処理は無限ループの中に、具体的な細かい処理は関数化して外に置き、無限ループ内から呼び出すといった工夫は必要です。

プログラムの実行と検証

プログラムを保存したディレクトリに移動したら以下のコマンドでプログラムを起動してください。

./p4-3.py

前回同様、ボタンAを押し処理が開始されたのを確認したら、すぐにボタンBを押してください。想定では
「ボタンBが押されました。処理をキャンセル」
と表示されて直ちにボタンAの処理が終了するはずです。

2021-02-23 18:24:57.220281: ボタンAが押されました。時間がかかる処理を開始
2021-02-23 18:24:59.223910: 実行中...
2021-02-23 18:25:01.227507: 実行中...
2021-02-23 18:25:02.429844: ボタンBが押されました。処理をキャンセル
2021-02-23 18:25:03.431755: 1秒経過しました。待機中に遷移

今回は想定通りの動作になったはずです。ボタンA、ボタンB共に長押しや連打しても不必要に反応しません。
ただし、今回のイベント駆動では、ボタンが押された状態しか検出されません(プルアップ+立ち下がり検出)。そのためパート3で紹介したプログラムのように「復帰中」でボタンが離されたことを検出できないので、単に1秒間、ボタンを受け付けないようにしています。

 

まとめ

イベント駆動での処理実装は、OSがシステムを管理するパソコンでは一般的な方法です。ラズパイでもライブラリ「RPi.GPIO」を使用することで簡単に実装できます。
イベント駆動は「きっかけ」と「動作」を関連させるコーディング手法であり、プログラムの構造が分かりやすい反面、各動作間の同期が取りにくいといった不便な点もあります。
状況に応じてポーリング制御や状態遷移などと組み合わせる必要があり応用力の問われる手法かもしれません。不安な方は前回(パート3)の記事も合わせて確認してください。
次回以降のパート5・パート6ではプログラムの安定稼働には欠かせない例外処理について説明します。

 

今回の連載の流れ

プッシュボタンを扱う(1) Pythonでプッシュボタンを扱うには
プッシュボタンを扱う(2) 誤動作の原因となるチャタリングを防止する
プッシュボタンを扱う(3) ポーリング制御でプッシュボタンに反応する
プッシュボタンを扱う(4) イベント駆動でプッシュボタンに反応する(今回)
プッシュボタンを扱う(5) プログラムをストップさせる例外を捕捉して処理する
プッシュボタンを扱う(6) 「raise Exception」で積極的に例外を利用する

エレクトロニクスやメカトロニクスを愛するみなさんに、深く愛されるサイトを目指してDevice Plusを運営中。

https://deviceplus.jp

学生ロボコン2017 出場ロボット解剖計画