Raspberry Pi Pico WとMicroPythonでWEBサーバーと並行処理をさせてみよう。

2024年9月30日

【 当サイトには広告リンクが含まれています。 】

赤LEDの点滅とThonnyに時刻を表示させながら、ローカルWEBサーバーにスマホなどから要求される、緑LEDの点灯/消灯の指示、ラズパイPicoWの時刻の表示、スイッチ情報の取得を行い、プログラムの並行処理を行います。


非同期処理について

同期処理が処理を一つずつ順番に実行するのに対し、非同期処理は、一つの処理の完了を待たずに、次の処理を始めることができます。

高速で複数の処理を切り替えることで、別々の処理が同時に実行しているかのように見え、並行処理を可能にします。

▶️同期処理

処理の順序 : 1⇒2⇒3


▶️非同期処理

処理の順序 : 1⇒2⇒3⇒1⇒2⇒1


非同期機能使うことで、ラズパイPicoWは、ローカルWEBサーバーへのクライアントからの接続を待っている間に、別の処理を実行することがきます。

今回はMicroPythonのasyncioモジュールにある、「async/await」 構文を使ってシングルスレッド(1つの処理を順番に実行する方式)で非同期処理を行います。

コルーチンといわれる中断・再開ができるルーチンを、「async」構文を使って複数、宣言し、「create_task」構文で実行できる状態にします。

実行できる状態となったコルーチンを、イベントループ(asyncio.get_event_loop)と言われる実行機能で、順番を管理しながら実行していきす。

コルーチン内に実装された「await」 構文により、コルーチンを一時停止させ、別のコルーチンに処理を渡すことで、非同期に処理を行い、並行処理を実現します。


実験準備

実験に必要な環境や部品を準備します。

機器

「ラズパイPicoWを始めよう。」記事で書きました、MicroPythonファームウェアをインストールしたラズパイPicoWと、統合開発環境ThonnyをインストールしたRaspi4Bを準備します。


使う部品

LED(発光ダイオード)、固定抵抗など実験に使う部品を準備します。

部 品 名規 格数 量取扱い店(参考)
LED(赤)3mm 赤色1電子工作ステーション
LED(緑)3mm 緑色1電子工作ステーション
1回路2接点 スライドスイッチSS12D001Amazon
ブレッドボードBB-1021秋月電子通商
カーボン抵抗1/4W 330Ω2電子工作ステーション
ジャンパーワイヤオス-メス
(約20cm)
1電子工作ステーション


配線

準備した機器と部品を、配線リストと配線図を参考にして接続します。

配線リスト

次の配線を行います。

FromTo
ラズパイPicoW(GP26)カーボン抵抗 R1(a)
カーボン抵抗 R1(b)LED(緑)(アノード 足の長い方)
LED(緑)カソード(足の短い方)ラズパイPicoW(GND)
ラズパイPicoW(GP27)カーボン抵抗 R2(a)
カーボン抵抗 R2(b)LED(赤)アノード(アノード 足の長い方)
LED(赤)カソード(足の短い方)ラズパイPicoW(GND)
ラズパイPicoW(GP28)スイッチ(b)
スイッチ(a)ラズパイPicoW(GND)


配線図



並行処理プログラムの実行

赤LEDの点滅とThonnyに時刻を表示させながら、ローカルWEBサーバーにスマホなどから要求される、緑LEDの点灯/消灯の指示、ラズパイPicoWの時刻の表示、スイッチ情報の取得を行い、プログラムの並行処理を確認します。

Raspi4BのThonnyを起動し、次のコードを「エディタ」に入力するか、リストをコピーしてペーストします。


##################################################
#
#      モジュールの読み込み
#
##################################################

#GPIO Pin/ネットワーク
from machine import Pin
import network
import ntptime
import rp2
import time

#ソケット(WEBサーバー)
import socket

#非同期処理
import asyncio


##################################################
#
#      LED、スイッチピン設定
#
##################################################

#緑のLED
led_grn = Pin(26, Pin.OUT)

#赤のLED
led_red = Pin(27, Pin.OUT)

#スイッチ
sw = Pin(28, Pin.IN, Pin.PULL_UP)

#LED消灯
led_grn.value(0)
led_red.value(0)

##################################################
#
#      グローバル変数
#
##################################################

#現在時刻 表示
disp_clock = ""


##################################################
#
#      Wi-Fi接続/時刻パラメータ
#
##################################################

#WiFi接続パラメータ
ssid = "Wi-FiルータのSSIDを設定します。"
password = "Wi-Fiルータのパスワードを設定します。"

#日本標準時(UTC+9時間)
UTC_OFFSET = 9

#NTPサーバ ドメイン
NTP_SRV = "ntp.nict.jp"


##################################################
#
#      【関数】クライエントへのHTML応答メッセージを作成
#
##################################################

def make_msg():
    
    #HTMLメッセージを作成
    html = """
        <!DOCTYPE html>
        <html>
        <head>
            <title>ラズパイPicoW Server</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <style>
                .btnGreen {width: 100px;
                           height: 50px;
                           display: flex;
                           align-items: center;
                           justify-content: center;
                           box-sizing: border-box;
                           background: #4CAF50;
                           color: #fff;
                           text-decoration: none;
                           border-radius: 5px;
                           font-size: 20px;
                           border: 2px solid #000000;
                           }
                           
                .btnOrange {width: 224px;
                           height: 50px;
                           display: flex;
                           align-items: center;
                           justify-content: center;
                           box-sizing: border-box;
                           background: #FFC107;
                           color: #000;
                           text-decoration: none;
                           border-radius: 5px;
                           font-size: 20px;
                           border: 2px solid #000000;
                           }
                           
                .btnBlue {width: 224px;
                           height: 50px;
                           display: flex;
                           align-items: center;
                           justify-content: center;
                           box-sizing: border-box;
                           background: #07FFFF;
                           color: #000;
                           text-decoration: none;
                           border-radius: 5px;
                           font-size: 20px;
                           border: 2px solid #000000;
                           }                           
                           
                           
                           
                .marg      {
                           margin: auto;
                           }
                           
                .marg input{
                           margin: 10px;
                           }                           
                           
            </style>                                                    
        </head>
        <body><center>
            <h2>ラズパイPicoWで並行処理</h2>
            
            <table class="marg">
            <tr>            
              <td>
                <form action="./GRN_ON">
                  <input type="submit" class="btnGreen" value="点灯" />
                </form>
              </td>
            
              <td>
                <form action="./GRN_OFF">
                  <input type="submit" class="btnGreen" value="消灯" />
                </form>
              </td>
            </tr>

            <tr>
              <td colspan="2">
                <form action="./SW_INFO">
                  <input type="submit" class="btnOrange" value="スイッチの状態" />
                </form>
              </td>             
            </tr>

            <tr>
              <td colspan="2">
                <form action="./CLOCK">
                  <input type="submit" class="btnBlue" value="現在時刻" />
                </form>
              </td>             
            </tr>            
            
            </table>            
            
            <p><font size="5">%s</font></p>
        </center></body>
        </html>
        """
    return str(html)

##################################################
#
#      【関数】WiFiに接続
#
##################################################

def wifi_connect():

    #WiFi地域(日本)の設定
    rp2.country('JP')

    #ステーションモードでの接続オブジェクト作成
    wlan = network.WLAN(network.STA_IF)
    
    #ステーションインタフェースの有効化
    wlan.active(True)

    #WiFiの省電力をオフに設定
    wlan.config(pm = 0xa11140)
    
    
    #接続状態確認
    if not wlan.isconnected():      

        #WiFiに接続
        wlan.connect(ssid, password)
        
        
        try:
            #IPアドレス取得待ち
            while wlan.status() != network.STAT_GOT_IP:
                print("接続中・・・")
                time.sleep(1)
                
            #IPアドレス取得    
            ip_add = wlan.ifconfig()[0]
            print(f' {ip_add}に接続しました。')
            
            #ラズパイPicoWのIPアドレスを返す
            return ip_add
        
        #強制中断
        except KeyboardInterrupt:
                  
            print("「Ctrl + c」キーが押されました。")  
            machine.reset()
            


##################################################
#
#      【関数】NTPサーバから日時取得
#
##################################################

def get_ntp_time():

    #NTPサーバ
    ntptime.server = NTP_SRV
    
    #NTPサーバへの接続待ち
    time.sleep(1)
    
    #ローカル時刻をUTC標準時刻に同期
    ntptime.settime()
    
    print ("Connected to", "NTP server.")
      
    #ダミー
    time.sleep(2)

            
##################################################
#
#      【非同期関数】赤LEDを点滅
#
##################################################

async def blnk_red_led():
    
    while True:
        
        #赤LEDを点滅
        led_red.toggle() 
        
        #非同期処理 0.5秒点滅
        await asyncio.sleep(0.5)            
            
            
##################################################
#
#      【非同期関数】クライアントのリクエストを処理
#
##################################################

async def hdl_clnt_req(reader, writer):
    
    global disp_clock
    
    #LED/スイッチ状況/現在時刻 表示
    disp_sts = ""
    
    #HTTPリクエストラインを読み込む
    req_lin = await reader.readline()
    
    #HTTPリクエストヘッダを読み飛ばす
    while await reader.readline() != b"\r\n":
        pass
    
    #URI(/、/GRN_ON?、/GRN_OFF?、/SW_INFO?)を取得
    req = str(req_lin, 'utf-8').split()[1]
    
    #リクエスト処理(緑LED点灯)
    if req == '/GRN_ON?':
        led_grn.value(1)
        disp_sts = "緑:点灯"
        
    #リクエスト処理(緑LED消灯)
    if req == '/GRN_OFF?':
        led_grn.value(0)
        disp_sts = "緑:消灯"
        
    #リクエスト処理(スイッチの状態)
    if req == '/SW_INFO?':
        if sw.value() == 0:
            disp_sts = 'スイッチ:入'
        else:
            disp_sts = 'スイッチ:切'
            

    #リクエスト処理(現在時刻)
    if req == '/CLOCK?':
        disp_sts = disp_clock         

    
    #応答メッセージを作成
    response = make_msg() % disp_sts
    
    #書き込みバッファに格納
    writer.write('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
    writer.write(response)
    
    #応答メッセージの送信完了待ち
    await writer.drain()
    
    #書き込みバッファを閉じる
    writer.close()
    
    #書き込みバッファが閉じるまで待つ
    await writer.wait_closed()
    

##################################################
#
#      【非同期関数】メイン
#
##################################################
    
async def main():
    
    #現在時刻 表示
    global disp_clock
    
    #WiFiに接続
    ip_add = wifi_connect()
    
    #NTPサーバから日時取得
    get_ntp_time()    
    
    #WEBサーバー作成(ラズパイPicoWのIPアドレス、HTTPポート(80))
    srv = asyncio.start_server(hdl_clnt_req, ip_add, 80)
    
    #WEBサーバーを並行処理させるためのタスクを作成
    asyncio.create_task(srv)
    
    #赤LEDを点滅を並行処理させるためのタスクを作成    
    asyncio.create_task(blnk_red_led())
    
    #現在時刻を取得
    while True:
        
        #ループの中で必要なタスクを追加
        
        #ローカル時刻
        lcl_tm =  time.localtime(time.mktime(time.localtime()) + UTC_OFFSET * 60 * 60) 
        
        #日付
        dat = ("%4d/%02d/%02d" % (lcl_tm[0:3]))
    
        #時刻
        tm = ("%2d:%02d:%02d" % (lcl_tm[3:6]))
               
        #現在時刻 表示
        disp_clock = tm
        print(tm)       
        
        await asyncio.sleep(1)
   
    


##################################################
#
#      並行処理開始
#
##################################################

#イベントループを作成(非同期タスクの実行、イベントのスケジューリング)
loop = asyncio.get_event_loop()

#【非同期関数】メインを実行するタスクを作成
loop.create_task(main())

#イベントループを実行
try:
    loop.run_forever()
    
except Exception as e:
    print('Error occured: ', e)
    
#強制中断    
except KeyboardInterrupt:
    
    print("「Ctrl + c」キーが押されました。")
   
    machine.reset()


Thonnyの「F5」キーを押して、並行処理プログラムを実行すると、Thonnyのシェルにネットワークへの接続メッセージが表示され、ラズパイPicoWのIPアドレスが表示されます。

スマホやパソコンのブラウザで、ラズパイPicoWのIPアドレスを指定し、ホームページを閲覧します。

表示された「点灯、消灯」ボタンをタップすると、緑のLEDが点灯、消灯し、「スイッチの状態」ボタンをタップすると、スイッチの入、切の状態、「現在時刻」ボタンをタップするとラズパイPicoWの時刻を表示します。

この処理と同時に、ラズパイPicoWは赤のLEDを連続的に点滅させ、Thonnyに時刻を表示させます。




確認後、Thonnyの「Ctrl + c」キーを押すと、処理が中断しますので、中断後、「Ctrl + F2」キーを押して、プログラムを終了させます。


任意のファイル名でラズパイPicoWに保存します。(ここでは「3_12concurrency.py」で保存しました。)


並行処理プログラムの仕組み

MicroPythonのasyncioモジュールを使って構築した非同期のローカルWEBサーバーは、一度に複数のクライアントを処理することができ、クライアントの接続を待っている間に、赤LEDの点滅や時計表示等、他の処理が実行できる仕組みを簡単に見ていきます。

blnk_red_led()(赤LEDの点滅)関数とhdl_clnt_req()(クライアントのリクエストを処理)はasync defというキーワードで定義された非同期関数で、他のコルーチンと並行処理を実行することができます。

main()(メイン)も非同期関数として定義され、WEBサーバーの作成や、その他の処理をまとめています。

モジュールの読み込み

ネットワークとの接続及び、WEBサーバー構築のモジュールに他に、asyncioモジュールをインポートして、非同期プログラミングを行います。

#GPIO Pin/ネットワーク
from machine import Pin
import network
import ntptime
import rp2
import time

#ソケット(WEBサーバー)
import socket

#非同期処理
import asyncio


LED、スイッチピン設定

緑のLEDと赤のLED及びスイッチを接続するGPピンの指定と、2つのLEDを消灯状態として初期化します。

スイッチを接続するGPピンについては、プルアップの指定をします。

#緑のLED
led_grn = Pin(26, Pin.OUT)

#赤のLED
led_red = Pin(27, Pin.OUT)

#スイッチ
sw = Pin(28, Pin.IN, Pin.PULL_UP)

#LED消灯
led_grn.value(0)
led_red.value(0)


グローバル変数

ローカルWEBサーバーにスマホなどから要求される、ラズパイPicoWの時刻を応答、表示するため、グローバル変数を設定します。

#現在時刻 表示
disp_clock = ""


Wi-Fi接続/時刻パラメータ

Wi-Fiルータに接続するための「SSID」、「パスワード」、協定世界時と日本標準時の時間差やNTPサーバのアドレスを設定します。

#WiFi接続パラメータ
ssid = "Wi-FiルータのSSIDを設定します。"
password = "Wi-Fiルータのパスワードを設定します。"

#日本標準時(UTC+9時間)
UTC_OFFSET = 9

#NTPサーバ ドメイン
NTP_SRV = "ntp.nict.jp"


【関数】クライエントへのHTML応答メッセージを作成

スマホやパソコンブラウザから、ローカルWEBサーバーにアクセスがあった時、関数の呼出し元にHTML応答メッセージを返します。

主なタグ説 明
<style>・・・</style>各ボタンのデザインを設定します。
<form action="./GRN_ON">
 <input type="submit ・・・>
</form>など
設定したボタンがクリックされた時のURLを相対パスで指定します。
(例:192.168.0.xxx/GRN_ON)
<p><font size="5">%s</font></p>「%s」を呼出し元から受け取った情報(スイッチ:入、切等)に置き換えます。


def make_msg():
    
    #HTMLメッセージを作成
    html = """
        <!DOCTYPE html>
        <html>
        <head>
            <title>ラズパイPicoW Server</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <style>
                .btnGreen {width: 100px;
                           height: 50px;
                           display: flex;
                           align-items: center;
                           justify-content: center;
                           box-sizing: border-box;
                           background: #4CAF50;
                           color: #fff;
                           text-decoration: none;
                           border-radius: 5px;
                           font-size: 20px;
                           border: 2px solid #000000;
                           }
               ・・・中略                      
                           
            </style>                                                    
        </head>
        <body><center>
            <h2>ラズパイPicoWで並行処理</h2>
            
            <table class="marg">
            <tr>            
              <td>
                <form action="./GRN_ON">
                  <input type="submit" class="btnGreen" value="点灯" />
                </form>
              </td>
            
               ・・・中略          
            
            </table>            
            
            <p><font size="5">%s</font></p>
        </center></body>
        </html>
        """
    return str(html)


【関数】WiFiに接続

Wi-Fi接続パラメータで設定した「SSID」と「パスワード」を使って、Wi-Fiネットワークに接続し、正常に接続された場合は、関数の呼び出し元に、ラズパイPicoWのIPアドレスを返します。

接続の状況、IPアドレスをThonnyのシェルに表示します。

def wifi_connect():

    #WiFi地域(日本)の設定
    rp2.country('JP')

    #ステーションモードでの接続オブジェクト作成
    wlan = network.WLAN(network.STA_IF)
    
    #ステーションインタフェースの有効化
    wlan.active(True)

    #WiFiの省電力をオフに設定
    wlan.config(pm = 0xa11140)
    
    
    #接続状態確認
    if not wlan.isconnected():      

        #WiFiに接続
        wlan.connect(ssid, password)
        
        
        try:
            #IPアドレス取得待ち
            while wlan.status() != network.STAT_GOT_IP:
                print("接続中・・・")
                time.sleep(1)
                
            #IPアドレス取得    
            ip_add = wlan.ifconfig()[0]
            print(f' {ip_add}に接続しました。')
            
            #ラズパイPicoWのIPアドレスを返す
            return ip_add
        
        #強制中断
        except KeyboardInterrupt:
                  
            print("「Ctrl + c」キーが押されました。")  
            machine.reset()


【関数】NTPサーバから日時取得

Wi-Fi接続/時刻パラメータで設定したNTPサーバから、標準時刻を取得し、ラズパイPicoWのローカル時刻(内臓のリアルタイム・クロック)に同期させます。

def get_ntp_time():

    #NTPサーバ
    ntptime.server = NTP_SRV
    
    #NTPサーバへの接続待ち
    time.sleep(1)
    
    #ローカル時刻をUTC標準時刻に同期
    ntptime.settime()
    
    print ("Connected to", "NTP server.")
      
    #ダミー
    time.sleep(2)


【非同期関数】赤LEDを点滅

赤LEDを点滅させる処理を、「async」構文を使って、コルーチン(非同期関数)として宣言します。

「await asyncio.sleep(0.5)」は0.5秒間、現在の処理を停止し、実行準備中の別のコルーチンに処理を渡します。

async def blnk_red_led():
    
    while True:
        
        #赤LEDを点滅
        led_red.toggle() 
        
        #非同期処理 0.5秒点滅
        await asyncio.sleep(0.5)   


【非同期関数】クライアントのリクエストを処理

ローカルWEBサーバーにスマホなどから要求される処理を、「async」構文を使って、コルーチン(非同期関数)として宣言します。

この関数(hdl_clnt_req)は、WEBサーバー起動後、新しいクライアントコネクションが確立されるたびに呼び出され、引数として「reader」、「writer」を受取ります。

「reader」は非同期にクライアントからデータを、「await reader.readline()」を使って受け取り、「writer」は非同期に応答してクライアントにデータを、「writer.write()」を使って送ります。

async def hdl_clnt_req(reader, writer):
    
    global disp_clock
    
    #LED/スイッチ状況/現在時刻 表示
    disp_sts = ""
    
    #HTTPリクエストラインを読み込む
    req_lin = await reader.readline()
    
    #HTTPリクエストヘッダを読み飛ばす
    while await reader.readline() != b"\r\n":
        pass
    
    #URI(/、/GRN_ON?、/GRN_OFF?、/SW_INFO?)を取得
    req = str(req_lin, 'utf-8').split()[1]
    
    #リクエスト処理(緑LED点灯)
    if req == '/GRN_ON?':
        led_grn.value(1)
        disp_sts = "緑:点灯"
        
    #リクエスト処理(緑LED消灯)
    if req == '/GRN_OFF?':
        led_grn.value(0)
        disp_sts = "緑:消灯"
        
    #リクエスト処理(スイッチの状態)
    if req == '/SW_INFO?':
        if sw.value() == 0:
            disp_sts = 'スイッチ:入'
        else:
            disp_sts = 'スイッチ:切'
            

    #リクエスト処理(現在時刻)
    if req == '/CLOCK?':
        disp_sts = disp_clock         

    
    #応答メッセージを作成
    response = make_msg() % disp_sts
    
    #書き込みバッファに格納
    writer.write('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
    writer.write(response)
    
    #応答メッセージの送信完了待ち
    await writer.drain()
    
    #書き込みバッファを閉じる
    writer.close()
    
    #書き込みバッファが閉じるまで待つ
    await writer.wait_closed()


【非同期関数】メイン

宣言した関数や非同期関数の実行や、WEBサーバーを作成するため、「async」構文を使って、コルーチン(非同期関数)として宣言します。

非同期TCPサーバーを作成する「asyncio.start_server()」関数の引数に、新しいクライアントコネクションが確立されるたびに呼び出される非同期関数(hdl_clnt_req)、ラズパイPicoWのIPアドレス、HTTPポート(80)を指定し、WEBサーバーを作成します。

作成したWEBサーバーを並行処理させるために「asyncio.create_task」関数を使って実行可能状態にします。

赤LEDを点滅も同様に、並行処理させるために「asyncio.create_task」関数を使って実行可能状態にします。

「while」文でThonnyのシェルに1秒ごとに時刻をカウント、表示させる時、「await asyncio.sleep(1)」を使って1秒間、カウントを停止し、実行準備中の別のコルーチンに処理を渡します。

async def main():
    
    #現在時刻 表示
    global disp_clock
    
    #WiFiに接続
    ip_add = wifi_connect()
    
    #NTPサーバから日時取得
    get_ntp_time()    
    
    #WEBサーバー作成(ラズパイPicoWのIPアドレス、HTTPポート(80))
    srv = asyncio.start_server(hdl_clnt_req, ip_add, 80)
    
    #WEBサーバーを並行処理させるためのタスクを作成
    asyncio.create_task(srv)
    
    #赤LEDを点滅を並行処理させるためのタスクを作成    
    asyncio.create_task(blnk_red_led())
    
    #現在時刻を取得
    while True:
        
        #ループの中で必要なタスクを追加
        
        #ローカル時刻
        lcl_tm =  time.localtime(time.mktime(time.localtime()) + UTC_OFFSET * 60 * 60) 
        
        #日付
        dat = ("%4d/%02d/%02d" % (lcl_tm[0:3]))
    
        #時刻
        tm = ("%2d:%02d:%02d" % (lcl_tm[3:6]))
               
        #現在時刻 表示
        disp_clock = tm
        print(tm)       
        
        await asyncio.sleep(1)


並行処理開始

非同期処理の管理と実行のために、「asyncio.get_event_loop」関数でイベントループを作成します。

非同期関数メイン(main)は、「loop.create_task(main())」を使ってイベントループのタスクとして登録、スケジューリングされ、非同期に実行されます。

「 loop.run_forever」関数により、作成したイベントループは限りなく実行されるため、タスクは継続的に処理されます。

これにより、WEBサーバーは実行状態を維持し、クライアントからの要求に応答し続けることができます。

#イベントループを作成(非同期タスクの実行、イベントのスケジューリング)
loop = asyncio.get_event_loop()

#【非同期関数】メインを実行するタスクを作成
loop.create_task(main())

#イベントループを実行
try:
    loop.run_forever()
    
except Exception as e:
    print('Error occured: ', e)
    
#強制中断    
except KeyboardInterrupt:
    
    print("「Ctrl + c」キーが押されました。")
   
    machine.reset()


まとめ

MicroPythonのasyncioモジュールを使って構築した非同期のローカルWEBサーバーは、一度に複数のクライアントを処理することができ、クライアントの接続を待っている間に、赤LEDの点滅や時計表示等、他の処理が実行できることを確認しました。

MicroPythonのasyncioモジュールにある、非同期処理を行うための「async/await」、 「create_task」、「asyncio.get_event_loop」構文等を使い、シングルスレッド(1つの処理を順番に実行する方式)で並行処理ができることを確認しました。