LEDの色をWebに反映するには

  • ラズパイを使用してPythonで次のことができるようにします。
  • LEDを緑、黄、赤3色用意し、タクトスイッチを押すたびに色を切り替えます。
  • 現在ラズパイで点灯しているLEDの色をWebから確認できるようにします。

本稿の回路とプログラムの骨格は以下の書籍を参考にしました。
「プログラミングで遊ぼう!」
 日経ソフトウエア/編 日経BP社 2015.8
  第1部 作って楽しむ!ラズパイで電子工作
   Part 2 スイッチを接続してWebアプリと連動させる

検証環境で使用した部品とは

デバイス/素子数量備考
ラズパイ 4 モデルB1いずれの機種でも可能と思われす。
LED 緑1直径5mm
LED 黄1直径5mm
LED 赤1直径5mm
タクトスイッチ1
抵抗 330Ω1
抵抗 1KΩ1

どのように考えて回路をつくったか

処理概要とブレッドボード配線図

タクトスイッチのON/OFFをGPIO22の状態で検出します。ONのときはGPIO22はLOW、OFFのときはHIGHです。
ON/OFFを検知したタイミングでLEDに接続したGPIO17,18,28に対してHIGH/LOWを切り替えることにより、交通信号機と同じ順番でLEDを点けます。今回はLEDを同時に1つしか点灯させないので、抵抗(330Ω)は1つで済みます。

ブレッドボードは省略します。

接続先1接続経路接続先2
3.3Vリード線(赤)抵抗(1KΩ)
抵抗(1KΩ)タクトスイッチ
タクトスイッチリード線(黒)GND
タクトスイッチリード線(青)GPIO 22
GPIO 17リード線(緑)LED(緑)
GPIO 18リード線(黄)LED(黄)
GPIO 28リード線(オレンジ)LED(赤)
LED(緑、黄、赤)抵抗(330Ω)
抵抗(330Ω)リード線(黒)GND

完成図のイメージ

一部リード線の色が上記と異なります。

Pythonプログラム

プログラムの概要

プログラムで行っていることは大きく、二つの処理に分かれます。
①スイッチとLEDを扱う処理
②Webに情報を表示する処理
この二つを「スレッド」と呼ばれる並行的に実行される処理(非同期処理)にします。これにより、無限ループ処理を複数実行することができます。
①と②をもう少し詳しく説明します。
①はスイッチの操作を検出して、現在点いているLEDを消して次のLEDを点ける処理です。
②はブラウザからの更新要求を受け取って、現在どのLEDが点いているかをページに反映させます。
このあと、プログラムの詳細を解説します。

# Display the color of a lit LED on a web page
# 2023.09.13 by Seshat

import threading    # To perform ON/OFF detection of switches in parallel
import time         # To synchronize events by waiting
import RPi.GPIO as GPIO # For handling GPIO
from webob import Request, Response # To communicate via the web

gpio22 = 15 # for Switch
# It makes sense that PIN numbers are sequential
gpio17 = 11 # for G
gpio18 = 12 # for Y
gpio27 = 13 # for R

stat = gpio17    # LED's info [Global]
color = 'GREEN'    # LED's info [Global]

# Each time a button is pressed, the three LEDs light up in sequence
# like a traffic light
class Sw(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
 
        GPIO.setmode(GPIO.BOARD)
        GPIO.setup(gpio22, GPIO.IN)
        GPIO.setup(gpio17, GPIO.OUT)
        GPIO.setup(gpio18, GPIO.OUT)
        GPIO.setup(gpio27, GPIO.OUT)
        GPIO.output(gpio17, GPIO.HIGH);
        GPIO.output(gpio18, GPIO.LOW);
        GPIO.output(gpio27, GPIO.LOW);

        self.daemon = True
        self.start()

    def run(self):
        global stat     # Pin number to which the lit LED is connected
        global color    # Color of lit LEDs

        # At the moment the button is pressed,
        # the LED that was lit turns off and the next LED is turned on.
        while True:
            # Wait for the switch to be pressed
            while GPIO.input(gpio22) == True:
                time.sleep(0.01)
    
            # print('Turn ON')

            # Wait for the unstable state (chatter) when the
            # button is pressed to stabilize
            time.sleep(0.02)
    
            # Turn off the currently lit LED
            GPIO.output(stat, GPIO.LOW);
            # Select the LED to light up
            stat += 1
            if stat > gpio27:
                stat = gpio17
    
            # Change the color of the LED
            GPIO.output(stat, GPIO.HIGH);
            # Set the color of the currently lit LED
            if stat == gpio17:
                color = 'GREEN'
            elif stat == gpio18:
                color = 'YELLOW'
            else:
                color = 'RED'
    
            # Wait until your hand is off the switch
            while GPIO.input(gpio22) == False:
                time.sleep(0.01)
                
            # print('Turn OFF')

            # Wait for the unstable state (chatter) when the
            # button is released to stabilize
            time.sleep(0.02)

# Returns the color of the currently lit LED in html in response
# to a web page update request
class WebSrv(object):
    def __call__(self, environ, start_response):
        global stat
        req = Request(environ)
        html = "<p>LED color=%s</p>"
        resp = Response(html % color)
        return resp(environ, start_response)

srv = WebSrv()
sw = Sw()

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    port = 8080

    httpd = make_server('', port, srv)
    print('Serving HTTP on port' , port)
    httpd.serve_forever()

プログラムを実行するには

コードを記述したファイル名を”ledClr2web.py”とします。
Raspberry Pi OSなどの端末から、次のように実行します。

python3 ledClr2web.py

まず、LEDの緑が点灯するはずです。
次に、タクトスイッチを押すたびに、黄、赤と変化するのを確かめましょう。
次に他のPCやスマホなどからブラウザを使って、ラズパイにアクセスし、何が表示されているかを確認します。
その際、別の端末を使うなどしてラズパイのIPアドレスを調べておきます。ifconfigやipコマンドでわかります。
ラズパイのWebサーバにアクセスするときに指定するポート番号は「8080」です。
ブラウザから、以下のように入力すると、ラズパイのWebページが表示され、現在のLEDの色が英語で表示されます。

ラズパイのIPアドレス:8080

ターミナルでは以下のようなメッセージが表示されているはずです。

$ python3 ledClr2web.py 
Serving HTTP on port 8080
192.168.1.13 - - [16/Sep/2023 18:25:45] "GET / HTTP/1.1" 200 23
192.168.1.13 - - [16/Sep/2023 18:25:45] "GET / HTTP/1.1" 200 23
192.168.1.13 - - [16/Sep/2023 18:25:45] "GET /favicon.ico HTTP/1.1" 200 23

ブラウザの情報を最新にするにはF5キーなどを押してページを更新します。
操作は次の流れになります。

  1. スイッチを押してLEDの色を変える
  2. ブラウザをF5などで更新して最新の情報にする

プログラムを終了するには、端末から[Ctrl]+[C]を入力します。以下のようなメッセージが表示されて、プログラムは終了します。

^CTraceback (most recent call last):
  File "/home/linux/worx/Python/sw_web/ledClr2web.py", line 99, in <module>
    httpd.serve_forever()
  File "/usr/lib/python3.9/socketserver.py", line 232, in serve_forever
    ready = selector.select(poll_interval)
  File "/usr/lib/python3.9/selectors.py", line 416, in select
    fd_event_list = self._selector.poll(timeout)
KeyboardInterrupt

結局プログラムで何をしているか

以下では、プログラムを短く切り取って詳しい説明をします。

import threading    # To perform ON/OFF detection of switches in parallel
import time         # To synchronize events by waiting
import RPi.GPIO as GPIO # For handling GPIO
from webob import Request, Response # To communicate via the web

次の機能が使えるようにインポートしています。

  1. スイッチを扱う処理をスレッドにする(threading)
  2. イベント待ちのため「時間」に関する機能(time)を取り込む
  3. GPIOを使用可能にする
  4. Webサービスとしてブラウザの要求に対して応答する
gpio22 = 15 # for Switch
# It makes sense that PIN numbers are sequential
gpio17 = 11 # for G
gpio18 = 12 # for Y
gpio27 = 13 # for R

stat = gpio17    # LED's info [Global]
color = 'GREEN'    # LED's info [Global]

ラズパイのGPIOで使用するピン名とピン番号、接続する素子の一覧は以下の通りです。

ピン名ピン番号用途
GPIO2215スイッチのON/OFFを検出
GPIO1711LED(緑)の点灯・消灯
GPIO1812LED(黄)の点灯・消灯
GPIO2713LED(赤)の点灯・消灯

変数”stat”は現在HIGH(点灯させている)状態のピン番号を保存します。初期値は緑であるgpio17です。
“color”は現在点灯しているLEDの色を格納します。初期値は”GREEN”です。

次からのコード”Sw”クラスは、ボタンが押される都度、3色のLEDを信号機のように順番に点灯させる処理です。

# Each time a button is pressed, the three LEDs light up in sequence
# like a traffic light
class Sw(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

上記では、スレッドの初期化を行っています。

        GPIO.setmode(GPIO.BOARD)
        GPIO.setup(gpio22, GPIO.IN)
        GPIO.setup(gpio17, GPIO.OUT)
        GPIO.setup(gpio18, GPIO.OUT)
        GPIO.setup(gpio27, GPIO.OUT)
        GPIO.output(gpio17, GPIO.LOW);
        GPIO.output(gpio18, GPIO.LOW);
        GPIO.output(gpio27, GPIO.LOW);

上の1行目はGPIOをピン番号で扱うための設定です。
2行目はスイッチのON/OFFという、入力(IN)情報をGPIO22で扱うための設定です。
3-5行はLEDをON/OFFするために、各ポートを出力(OUT)に使用する設定です。setup()の第1引数はピン番号、第2引数はINもしくはOUTを指定します。
6-8行では初期設定として緑のLEDを点灯(HIGH)し、それ以外のLEDを消灯(LOW)しておきます。output()の第1引数はピン番号、第2引数はHIGHもしくはLOWです。

        self.daemon = True
        self.start()

上記の処理でスレッドを開始しています。
くれぐれも、GPIOの初期化処理の前(2つ前のスレッドの初期化処理の直後)に、上記を実行してはなりません。タイミングによっては、GPIO.setmode()実行前にスレッドが実行され、GPIOが初期化されていないことが原因でエラーになってしまいます。

   def run(self):
        global stat     # Pin number to which the lit LED is connected
        global color    # Color of lit LEDs

このように宣言することにより、”stat”と”color”がSwクラスから参照できるようになります。

次からいよいよボタンが押された時の処理です。一言で「ボタンが押された」といっても、ボタンを押したままになっている瞬間と、ボタンから手が離れた瞬間に分けられます。それぞれに対して処理を行います。

        # At the moment the button is pressed,
        # the LED that was lit turns off and the next LED is turned on.
        while True:
	        # Wait for the switch to be pressed
	        while GPIO.input(gpio22) == True:
	            time.sleep(0.01)
	
	        # print('Turn ON')

3行目のwhile文から永久ループとなります。
5行目のwhile文はボタンが押されるのを待っている処理です。正確には0.01秒ごとに、ボタンが押されていない状態かチェックし、押されていないとまた0.01秒待つ処理を繰り返しています。
ボタンが押された瞬間に、while文から抜けて、下の処理に移ります。
ここで、回路は3.3Vの電源から1kΩの抵抗を経てスイッチに接続しています。スイッチは押されていないと電流は流れないので、GPIO22には3.3Vがかかったままになります。そのため、
GPIO.input(gpio22)
の結果はHIGHすなわちTrueとなります。
ボタンが押された瞬間、電流がGNDに流れるため、電圧は降下し、LowすなわちFalseになるというわけです。

            # Wait for the unstable state (chatter) when the
            # button is pressed to stabilize
	        time.sleep(0.02)

実は、スイッチは押した瞬間、超短時間でON/OFFが繰り返されるという不安定な時間帯が生じています(チャタリング)。ここでは、この不安定な状態が収まるのを待っています。

	        # Turn off the currently lit LED
	        GPIO.output(stat, GPIO.LOW);

ボタンが押されたので、次のLEDを点灯する前に、現在点灯しているLEDを消灯します。

        # Select the LED to light up
        stat += 1
        if stat > gpio27:
            stat = gpio17

上記では順番が次のLEDを点灯させる(HIGHにする)ためのGPIOポートを”stat”に格納します。GPIO名の変数に格納されているのはピン番号です。LEDに使用するピン番号は11-13の連番にしています。そのため、1プラスし、結果が14以上の場合のみ、先頭の11番に戻す処理を行っています。

	        # Change the color of the LED
	        GPIO.output(stat, GPIO.HIGH);

ここでやっと、次のLEDを点灯させることができます。

	        # Set the color of the currently lit LED
	        if stat == gpio17:
	            color = 'GREEN'
	        elif stat == gpio18:
	            color = 'YELLOW'
	        else:
	            color = 'RED'

今点灯したばかりのLEDのピン番号が入っている”stat”から、何色を点灯させたかを調べて、変数”color”に色を代入します。

	        # Wait until your hand is off the switch
	        while GPIO.input(gpio22) == False:
	            time.sleep(0.01)
	            
	        # print('Turn OFF')

押されたボタンから手が離れ、OFFになるのを待ちます。
人間がボタンを一瞬でON/OFFしたと思っていても、実はコンピュータにとっては長い時間ONのままになっているのです。
ここで、回路は3.3Vの電源から1kΩの抵抗を経てスイッチに接続しています。スイッチは押されており、電流はグランド(GND)に流れるので、電圧が降下します。
そのため、
GPIO.input(gpio22)
の結果はLOWすなわちFalseとなります。
スイッチが手から離れると、電流は流れなくなるため、電圧は3.3Vに戻りHIGHになるのでしたね。

            # Wait for the unstable state (chatter) when the
            # button is released to stabilize
	        time.sleep(0.02)

ボタンを離した時も、押したとき同様に不安定な状態になることがあるため、安定するのを待ちます。

# Returns the color of the currently lit LED in html in response
# to a web page update request
class WebSrv(object):
    def __call__(self, environ, start_response):
        global stat
        req = Request(environ)
        html = "<p>LED Color=%s</p>"
        resp = Response(html % color)
        return resp(environ, start_response)

”WebSrv()クラスでは、ブラウザからの要求を受けて、現在点灯しているLEDの色(color)を表示しています。

srv = WebSrv()
sw = Sw()

Webの情報を更新する処理:WebSrv()と、スイッチを読み取ってLEDを制御する処理:Sw()は並行して実行されます。

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    port = 8080

    httpd = make_server('', port, srv)
    print('Serving HTTP on port' , port)
    httpd.serve_forever()

上記の処理で、Webサーバはポート番号8080を使用する設定にしています。Webブラウザに表示するにあたって、Webサーバのポート番号を8080に設定しています。

残された課題とは

上記プログラムを終了力後に再度起動すると、GPIO関連のワーニングが複数出るようになります。下はそのうちの一つです。

/home/linux/worx/Python/sw_web/ledClr2web.py:26: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  GPIO.setup(gpio17, GPIO.OUT)

ワーニングメッセージの意味はざっくり、次のような感じです。
「GPIO17(ピン番号11)は誰か使用中です。このプログラムは、使用中のGPIO17を使っちゃいます。警告を無効にするには、GPIO.setwarnings(False) を使用してくださいね。」
このワーニングは本来、Pythonのtry-exceptという仕組みを使って、プログラムの終了時にGPIOを初期化する処理を実行することにより、取り除くのがセオリーですが、本プログラムでは終了処理が呼ばれませんでした。原因はあくまでも推測ですが、Web機能かスレッド機能を使用することでなにか制約が発生するのかもしれません。

さいごに

本稿ではスイッチによってLEDの信号機を制御しました。本物の信号機のように一定時間で色が切り替わるようにすることも可能です。またスイッチを横断歩道にある歩行者用ボタンとみなして、横断歩道を渡るときの信号機の挙動をさせることも可能です。

今回紹介した工作とプログラムは、ラズパイをIoTとして活用した際に、センサーなどで得た情報をWebから閲覧できるようにするといった応用ができます。
みなさんも本稿のプログラムを参考に、様々なシーンでラズパイをお楽しみください。