ラズパイPicoW Wi-Fi通信、遠隔操作&表示 MicroPython編

ラズパイPico W Wi-Fi遠隔操作MicroPythonアイキャッチ

Raspberry Pi PicoW のWi-Fi無線通信機能を使用して、パソコンやスマホのブラウザから遠隔操作、データ表示する方法を詳しく紹介します。

今回はプログラム言語は「MicroPython」開発環境は「Thonny」を使用します。
一般的に公開されているWi-Fi遠隔操作、データ表示のチュートリアルを使用して、コピペによる動作確認とWi-Fi接続の詳細な説明から、より実用的な使用方法(表示データ自動更新)まで紹介します。

動作確認に使用するブラウザページ(html、JavaScript、CSS)は「ChatGPT」にお任せして、「MicroPython」を使用したWi-Fi無線通信について理解して、自由に使いこなせるようになりましょう。


「CircuitPython」や「C言語(Arduinoコマンド)」を使用した。PicoWの使用方法は、以下のリンクで詳しく紹介しています。

ラズパイPicoW Wi-Fi通信、遠隔操作&表示 CircuitPython編
Raspberry Pi PicoWのWi-Fi通信機能を使用してサーバー機能を利用した遠隔操作、データ監視する方法をCircuitPythonで詳しく紹介します。
ラズパイPicoW Wi-Fi通信、遠隔操作&表示 C言語(Arduinoコマンド)編
Wi-Fi通信機能を使用してPicoWをサーバーに設定して遠隔操作&表示する方法を詳しく紹介します。動作確認のブラウザページはChatGPTに全部書いてもらいましょう。

「ラズパイPico W」の基本仕様、開発環境については以下のリンクで詳しく紹介しています。

ラズパイPico/PicoWの使い方を3つの開発環境Python、ArduinoIDE、PlatformIOで紹介
Raspberry Pi Pico/Pico Wの使い方を端子配列からPython(MicroPython)とC言語の開発環境、Lチカ方法まで紹介。PythonはTonny、C言語はArduinoIDEとPlatformIOの3種類で詳しく紹介します。
スポンサーリンク

1.ラズパイPico Wとは

「Raspberry Pi PicoW」とは、イギリスを拠点とする慈善団体によって若者のプログラミング学習を目的に開発されたもので、他の「Raspberry Piシリーズ」とは異なり、OSの機能はありませんが、電源投入ですぐに使用でき、スイッチやLEDランプ、モーター、各種センサー、通信機器を接続して、それらを制御するプログラムを簡単に作成して動作確認できるため、組み込み(制御)系プログラミングの学習に最適です。

「Pico W」にはWi-Fi機能があり、Wi-Fi通信を利用したインターネット経由のデータ収集や遠隔操作等のアイデアを簡単に試すことができます。

端子配列は下画像のようになります(公式サイトより抜粋

ラズパイPico/Pico Wの端子配列
スポンサーリンク

2.開発環境について

・MicroPythonとは

「MicroPython」とはマイコンボード(マイクロコントローラ)上での動作に最適化された「Python」と互換性のあるプログラミング言語です。

「Python」とほぼ同じように機能しますが、いくつかの制限があり「Python」の標準ライブラリの一部はサポートされていません。しかし、文法や構文は「Python」と同じため、理解しやすく、マイコンボード上で動作する「Python」によって、「IoT(Internet of Things)」デバイスや組み込みシステムの開発が容易になります。

マイコンボードを使用して、LED、温度センサー、モーター、Wi-Fiモジュールなどを動作させながら「Python」の学習ができるため、初心者にとっても有用です。

・Thonnyとは

「Thonny」とは「ラズベリーパイ」の上位機種にも標準でインストールされている「Python」のプログラム作成、デバッグ、実行を行うことがでる統合開発環境(IDE)です。

簡単なデバッグ機能やエラー情報の確認ができるため、学習におけるストレスを最小限に抑えることができ「Python」プログラマーにとって非常に役立つ開発環境と言えます。

エディタとしての機能は物足りないですが、手軽に導入できて特に初心者の方がお試しするには最適のツールと思います。


「MicroPython」を使用した開発環境「Thonny」の使用方法は以下のリンクで詳しく紹介しています。

ラズパイPicoの使い方 MicroPython&開発環境Thonny、SSLエラーの対処方法も紹介
Raspberry Pi Picoので開発環境Thonnyを使用した「Python(MicroPython)」でのプログラミング方法について初期設定からパッケージ(ライブラリ)の追加、動作確認の方法まで詳しく紹介します。
スポンサーリンク

3.Wi-Fi接続「基本編」

・動作紹介

「Raspberry Pi PicoW」のWi-Fi通信機能の基本的な使用方法は、英国の慈善団体「Raspberry Pi Foundation」の公式HP(以下リンク先)で詳しく紹介されています。

https://projects.raspberrypi.org/en/projects/get-started-pico-w/0

リンク先の手順通りに進めれば、Wi-Fi通信の手順確認から動作確認まで進める事ができますが、英文での解説で、プログラムも部分的に書かれているだけのため、全体のプログラムをサンプルプログラムとして以下にまとめました。

このサンプルプログラムを使用して各動作について詳しく紹介します。

サンプルプログラムを開発環境「Thonny」にコピペで貼り付けて実行すると以下のように「シェル」部に「PicoW」の「IPアドレス(下画像では「192.168.0.9」)」が表示されます。

ラズパイPico W Wi-Fi遠隔操作MicroPython

パソコンやスマホのブラウザのアドレスバーに「IPアドレス(ここでは192.168.0.9)」を入力すると、下画像のような画面が表示されます。

ラズパイPico W Wi-Fi遠隔操作MicroPython

表示画面の「Ligt on/Light off」ボタンで「PicoW」本体のLEDをON/OFFさせることができます。
その下にはLEDのON/OFF状態が表示され、その下にはPicoW本体の温度センサの値が表示されます。

表示内容はページが再読み込みされた時にしか更新されません。
応用編ではページを自動更新する方法も紹介しています。

・サンプルプログラム(コピペ)

サンプルプログラムは以下になります。コピペで貼り付けて実行してください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

8,9行目の「ssid」と「password」は、お使いのネットワーク環境に合わせて設定してください
import network            # ネットワーク処理用ライブラリ
import socket             # ソケット処理用ライブラリ
from time import sleep    # スリープ処理用ライブラリ
from picozero import pico_temp_sensor, pico_led   # GPIO制御用ライブラリ
import machine            # マイクロコントローラー制御用ライブラリ

# ネットワーク(Wi-Fiルーター)の SSID とパスワードを設定
ssid = '自宅のネットワーク環境のSSIDを設定'       
password = '自宅のネットワーク環境のパスワードを設定'

# Wi-Fi 接続実行関数   
def connect():
    wlan = network.WLAN(network.STA_IF)      # WLANオブジェクトを作成
    wlan.active(True)                        # WLANインタフェースを有効化
    wlan.connect(ssid, password)             # 指定されたSSIDとパスワードでWi-Fiに接続する

    while wlan.isconnected() == False:       # Wi-Fi接続が確立されるまで待機
        print('Waiting for connection...')
        sleep(1)

    print(wlan.ifconfig())                   # Wi-Fi接続情報を全て出力
    ip = wlan.ifconfig()[0]                  # IPアドレスのみを取得
    print(f'Connected IP: {ip}')             # IPアドレスを出力
    return ip                                # IPアドレスを返す

#  ソケットを開く関数
def open_socket(ip):
    address = (ip, 80)             # IPアドレスとポート番号のタプルを作成
    connection = socket.socket()   # ソケットオブジェクトを作成
    connection.bind(address)       # IPアドレスとポート番号をバインド
    connection.listen(1)           # 接続待機
    print(connection)              # ソケットオブジェクトを出力
    return connection              # ソケットオブジェクトを返す

# Webページを準備
def webpage(temperature, state):
    html = f"""
        <!DOCTYPE html>
        <html>
        <form action="./lighton">
        <input type="submit" value="Light on" />
        </form>
        <form action="./lightoff">
        <input type="submit" value="Light off" />
        </form>
        <p>LED is {state}</p>
        <p>Temperature is {temperature}</p>
        </body>
        </html>
        """
    return str(html)

# HTTPリクエストを処理する関数
def serve(connection):
    # 初期状態を設定
    state = 'OFF'
    pico_led.off()
    temperature = 0

    # 接続待機とHTTPリクエスト処理
    while True:
        client = connection.accept()[0]    # クライアントからの接続要求を受け入れる
        request = client.recv(1024)        # クライアントからのHTTPリクエストを受信する
        request = str(request)             # バイト文字列を文字列に変換する
        try:
            request = request.split()[1]   # HTTPリクエストの2番目の要素(URL)を抽出する
        except IndexError:
            pass

        # URLによってLEDを制御する
        if request == '/lighton?':    # 抽出したURLが「/lighton?」なら
            pico_led.on()             # 本体LEDをオンにする
            state = 'ON'              # 本体LED状態を'ON'に設定
        elif request =='/lightoff?':  # 抽出したURLが「/lightoff?」なら
            pico_led.off()            # 本体LEDをオフにする
            state = 'OFF'             # 本体LED状態を'OFF'に設定

        # 本体温度を取得し、Webページを生成してクライアントに送信する
        temperature = pico_temp_sensor.temp # 温度センサーから温度データを取得する
        html = webpage(temperature, state)  # 温度値とステータスをHTMLページに埋め込む
        client.send(html) # クライアントにHTMLページを送信する
        client.close()    # クライアント接続を閉じる


# メイン処理
try:
    ip = connect()                  # Wi-Fiに接続し、IPアドレスを取得する
    connection = open_socket(ip)    # IPアドレスを使用してソケットを開く
    serve(connection)               # HTTPリクエストを処理する
except KeyboardInterrupt:
    machine.reset()                 # 停止(Ctrl-C)が押されたときにPicoを再起動する

・プログラムの詳細(Wi-Fi接続手順)

「基本編」のプログラムの詳細は以下のようになります。
以下リンクの「Raspberry Pi Foundation」のチュートリアルを参考に、Wi-Fi接続の手順からページの読み込みまでサンプルプログラムから抜粋して詳しく紹介します。

https://projects.raspberrypi.org/en/projects/get-started-pico-w/0

  1. 必要なライブラリ、パッケージの準備
  2. ネットワークのSSID、パスワードの設定
  3. Wi-Fi接続実行、IPアドレスの取得
  4. ソケットを開く
  5. Webページの準備
  6. HTTPリクエストの応答処理
  7. メイン処理

1.必要なライブラリ、パッケージの準備

まずは必要なライブラリとパッケージを準備します。
WiFi接続や「PicoW」本体の制御にに必要なライブラリを以下のように準備します。

import network            # ネットワーク処理用ライブラリ
import socket             # ソケット処理用ライブラリ
from time import sleep    # スリープ処理用ライブラリ
from picozero import pico_temp_sensor, pico_led   # GPIO制御用ライブラリ
import machine            # マイクロコントローラー制御用ライブラリ
この中で「picozero」は本体LEDや温度センサーの読み取りに必要なパッケージのため、別途「Thonny」の[ツール]→[パッケージを管理]から検索して、インストールしておきましょう

「Tonny」を使用した「MicroPython」のパッケージのインストール方法は以下のリンクを参考にしてください。

ラズパイPicoの使い方 MicroPython&開発環境Thonny、SSLエラーの対処方法も紹介
Raspberry Pi Picoので開発環境Thonnyを使用した「Python(MicroPython)」でのプログラミング方法について初期設定からパッケージ(ライブラリ)の追加、動作確認の方法まで詳しく紹介します。

2.ネットワークのSSID、パスワードの設定

次に、自宅のネットワーク環境に合わせて、以下の部分に「SSID」と「パスワード」を設定します。

# ネットワーク(Wi-Fiルーター)の SSID とパスワードを設定
ssid = '自宅のネットワーク環境のSSIDを設定'       
password = '自宅のネットワーク環境のパスワードを設定'

3.Wi-Fi接続実行、IPアドレスの取得

Wi-Fi接続を行うには以下の関数のような手順で行います。
先に設定した「SSID」と「パスワード」を使用して、Wi-Fiネットワークへの接続が開始され、接続が完了すると「IPアドレス」が割り当てられます。

ここではWi-Fiネットワークへの接続と「IPアドレス」の取得を行っています。

# Wi-Fi 接続実行関数   
def connect():
    wlan = network.WLAN(network.STA_IF)      # WLANオブジェクトを作成
    wlan.active(True)                        # WLANインタフェースを有効化
    wlan.connect(ssid, password)             # 指定されたSSIDとパスワードでWi-Fiに接続する

    while wlan.isconnected() == False:       # Wi-Fi接続が確立されるまで待機
        print('Waiting for connection...')
        sleep(1)

    print(wlan.ifconfig())                   # Wi-Fi接続情報を全て出力
    ip = wlan.ifconfig()[0]                  # IPアドレスのみを取得
    print(f'Connected IP: {ip}')             # IPアドレスを出力
    return ip                                # IPアドレスを返す

4.ソケットを開く

「ソケット」とは、サーバーがクライアントに応答する方法で、Webページの表示にも利用されます。
今回はサーバー側が「Raspberry Pi PicoW」でクライアント側はスマホやパソコン等の「ブラウザ」になります。

「ソケット」は「IPアドレス」と「ポート番号」を指定して開く必要があり「ポート番号」はサーバーの識別に使用されます。
「ポート番号」は「IPアドレス」に続けて何も指定しなければ通常ポート「80」が使われます。

以下の関数を実行することで「PicoW」 が「ポート80」 で「IPアドレス」への接続に応答するようになります。
これで、接続された「ブラウザ」でWebページを表示する(HTMLコードの提供を開始)準備ができます。

#  ソケットを開く関数
def open_socket(ip):
    address = (ip, 80)             # IPアドレスとポート番号のタプルを作成
    connection = socket.socket()   # ソケットオブジェクトを作成
    connection.bind(address)       # IPアドレスとポート番号をバインド
    connection.listen(1)           # 接続待機
    print(connection)              # ソケットオブジェクトを出力
    return connection              # ソケットオブジェクトを返す

5.Webページの準備

以下の関数ではブラウザで表示する Webページを作成します。
Webページは「htmlコード」を文字列として変数に格納して準備しておきます。

文字列を指定する時には「’’’(三重引用符)」を使用します。
複数行の文字列を「’’’」で挟んだ間に記入することでテキスト全体を1つの文字列として扱うことができます。

さらに「f’’’」のように「f」をつけることで文字列内に「変数」を埋め込むこともできます。
この場合の変数は「中括弧{ }」で囲んで指定します。

以下のコードでは「{state}」と「{temperature}」で使用しており、Webページが呼び出されるたびに、その時の変数の状態を渡して表示させることができます。

# Webページを準備
def webpage(temperature, state):
    html = f"""
        <!DOCTYPE html>
        <html>
        <form action="./lighton">
        <input type="submit" value="Light on" />
        </form>
        <form action="./lightoff">
        <input type="submit" value="Light off" />
        </form>
        <p>LED is {state}</p>
        <p>Temperature is {temperature}</p>
        </body>
        </html>
        """
    return str(html)
文字列として格納するWebページのコードについては、事前にそれ単体で正しく表示されるかをWebブラウザで確認しておきましょう。

6.HTTPリクエストの応答処理

最後に以下の関数を実行することで、サーバー側でクライアント側からのリクエストを常に待ち受ける、接続待機状態となります。

リクエストがあった時には、リクエストに応じた処理を実行し、文字列として格納したWebページを提供します。

# HTTPリクエストを処理する関数
def serve(connection):
    # 初期状態を設定
    state = 'OFF'
    pico_led.off()
    temperature = 0

    # 接続待機とHTTPリクエスト処理
    while True:
        client = connection.accept()[0]    # クライアントからの接続要求を受け入れる
        request = client.recv(1024)        # クライアントからのHTTPリクエストを受信する
        request = str(request)             # バイト文字列を文字列に変換する
        try:
            request = request.split()[1]   # HTTPリクエストの2番目の要素(URL)を抽出する
        except IndexError:
            pass

        # URLによってLEDを制御する
        if request == '/lighton?':    # 抽出したURLが「/lighton?」なら
            pico_led.on()             # 本体LEDをオンにする
            state = 'ON'              # 本体LED状態を'ON'に設定
        elif request =='/lightoff?':  # 抽出したURLが「/lightoff?」なら
            pico_led.off()            # 本体LEDをオフにする
            state = 'OFF'             # 本体LED状態を'OFF'に設定

        # 本体温度を取得し、Webページを生成してクライアントに送信する
        temperature = pico_temp_sensor.temp # 温度センサーから温度データを取得する
        html = webpage(temperature, state)  # 温度値とステータスをHTMLページに埋め込む
        client.send(html) # クライアントにHTMLページを送信する
        client.close()    # クライアント接続を閉じる

ブラウザから「IPアドレス(ルート)」にアクセスすると、文字列として格納しておいたWebページが読み込まれ、以下のように表示されます。

ラズパイPico W Wi-Fi遠隔操作MicroPython

ブラウザ上にはボタンが2つあり、ボタンを押すとそれぞれ以下のような動作になります。

  • [Light on]ボタン:「IPアドレス/lighton?」でサーバーにリクエストを送信し、サーバーがこれを受信した場合は本体LEDを点灯させ、LEDの状態変数「state」を「’ON’」にします。
  • [Light off]ボタン:「IPアドレス/lightoff?」でサーバーにリクエストを送信し、サーバーがこれを受信した場合は本体LEDを消灯させ、LEDの状態変数「state」を「’OFF’」にします。
「LED is 〜」には本体LEDの状態、「Temperature is 〜」には本体温度が表示されますが、この値はページが更新されないと変わりません。
「応用編」のサンプルプログラムではページを更新せずに、定期的に表示を更新する方法を詳しく紹介しています。

7.メイン処理

メインの処理は以下になります。

「connect()」関数でW-Fi接続を実行し「IPアドレス」を取得します。
取得した「IPアドレス」で「ソケット」を開き、リクエストに応答する準備をします。
「serve(connection)」関数を実行することで待機状態となり、受信したリクエストに応じて処理を実行できるようになります。

# メイン処理
try:
    ip = connect()                  # Wi-Fiに接続し、IPアドレスを取得する
    connection = open_socket(ip)    # IPアドレスを使用してソケットを開く
    serve(connection)               # HTTPリクエストを処理する
except KeyboardInterrupt:
    machine.reset()                 # 停止(Ctrl-C)が押されたときにPicoを再起動する

4.Wi-Fi接続「応用編」

先に紹介した「基本編」ではWebページの表示データの値は、ページを更新しないと変更されません。
定期的にブラウザページを自動更新するようにしても良いですが、頻繁にページを再読み込みするのは効率的ではありません。

そこで「応用編」ではWebページを再読み込みせずに、表示データの値を定期的に更新して表示させる方法について、詳しく紹介します。

・動作紹介

動作については「基本編」と同じように、この下のサンプルプログラムを「Thonny」にコピペして実行すると、シェル部に「IPアドレス(下画像では192.168.0.9)」が表示されます。

ラズパイPico W Wi-Fi遠隔操作MicroPython

この「IPアドレス」をブラウザのアドレスバーに入力すると、以下のようなページが表示されます。

ラズパイPico W Wi-Fi遠隔操作MicroPython
「基本編」と機能的には同じですが「応用編」ではボタンを押すたびにページが更新されることはありません。
本体温度も1秒ごとに自動で更新されて表示されます。
この動作を実現するためには、Webページを「html」だけでなく「JavaScript」を使用して作成します。

・サンプルプログラム(コピペ)

サンプルプログラムは以下になります。コピペで貼り付けて実行してください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

8,9行目の「ssid」と「password」は、自宅のネットワーク環境に合わせて設定してください
import network            # ネットワーク処理用ライブラリ
import socket             # ソケット処理用ライブラリ
from time import sleep    # スリープ処理用ライブラリ
from picozero import pico_temp_sensor, pico_led   # GPIO制御用ライブラリ
import machine            # マイクロコントローラー制御用ライブラリ

# ネットワーク(Wi-Fiルーター)の SSID とパスワードを設定
ssid = '自宅のネットワーク環境のSSIDを設定'       
password = '自宅のネットワーク環境のパスワードを設定'

# Wi-Fi 接続実行関数   
def connect():
    wlan = network.WLAN(network.STA_IF)      # WLANオブジェクトを作成
    wlan.active(True)                        # WLANインタフェースを有効化
    wlan.connect(ssid, password)             # 指定されたSSIDとパスワードでWi-Fiに接続する

    while wlan.isconnected() == False:       # Wi-Fi接続が確立されるまで待機
        print('Waiting for connection...')
        sleep(1)

    print(wlan.ifconfig())                   # Wi-Fi 接続情報を全て出力(IPアドレス、サブネットマスク、 ゲートウェイ、DNS)
    ip = wlan.ifconfig()[0]                  # IPアドレスのみを取得
    print(f'Connected IP: {ip}')             # IPアドレスを出力
    return ip                                # IPアドレスを返す

#  ソケットを開く関数
def open_socket(ip):
    address = (ip, 80)             # IPアドレスとポート番号のタプルを作成
    connection = socket.socket()   # ソケットオブジェクトを作成
    connection.bind(address)       # IPアドレスとポート番号をバインド
    connection.listen(1)           # 接続待機
    print(connection)              # ソケットオブジェクトを出力
    return connection              # ソケットオブジェクトを返す

# Webページを準備
def webpage():
    html = '''
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <title>PicoW Controller</title>
        <style>
          #container {display: flex;justify-content: center;align-items: center;flex-direction: column;margin-top: 50px;}
          h1 {color: #666;}
          .button {width: 100px;height: 50px;border-radius: 5px;margin: 10px;cursor: pointer;font-size: 20px;font-weight: bold;color: white;text-align: center;line-height: 50px;}
          #on-button {background-color: green;}
          #off-button {background-color: red;}
          input[type="text"] {width: 200px;padding: 5px;font-size: 20px;text-align: center;margin: 20px;}
        </style>
      </head>
      <body>
        <div id="container">
          <h1>PicoW Controller</h1>
          <hr>
          <div>
            <button id="on-button" class="button">ON</button>
            <button id="off-button" class="button">OFF</button>
          </div>
           <div><p id="led_state"> </p></div>
          <input type="text" placeholder="Temperature" />
        </div>

        <script>
          const onButton = document.getElementById("on-button");
          const offButton = document.getElementById("off-button");
          const ledState = document.getElementById("led_state");
          const textBox = document.querySelector("input[type='text']");

          onButton.addEventListener("click", async () => {
            const response = await fetch("/lighton");
            const text = await response.text();
            ledState.textContent = text;
            console.log(text);
          });

          offButton.addEventListener("click", async () => {
            const response = await fetch("/lightoff");
            const text = await response.text();
            ledState.textContent = text;
            console.log(text);
          });

          setInterval(async () => {
            const response = await fetch("/get/data");
            const text = await response.text();
            textBox.value =   "本体温度:" + text;
          }, 1000);
        </script>
      </body>
    </html>
    '''
    return html

# Wi-Fi接続実行
try:
    ip = connect()       # Wi-Fi接続を実行してIPアドレスを取得
    connection = open_socket(ip)    # IPアドレスでソケットを開く
    html = webpage()     # htmlデータ読み込み
except (KeyboardInterrupt, OSError) as e:
    machine.reset()      # 停止またはOSエラーで本体リセット

# サーバー応答処理
while True:
    try:
        client = connection.accept()[0]    # クライアントからの接続要求を受け入れる
        request = client.recv(1024)        # クライアントからのHTTPリクエストを受信する
        request = str(request)             # バイト文字列を文字列に変換する
        try:                               # リクエストが存在しない場合に発生する可能性のあるIndexErrorをキャッチし、例外を無視する
            request = request.split()[1]   # 受信したデータからURLを抽出
        except IndexError:
            pass
        
        # ヘッダー情報を返す
        client.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        # ルートアクセスでhtmlデータを返す
        if request == '/':            # 抽出したURLが「/」なら
            client.send(html)         # Webページを返す
            print('Root Access')
        # 本体 LED ON
        elif request == '/lighton':   # 抽出したURLが「/lighton」なら
            pico_led.on()             # 本体LEDをオンにする
            client.send('LED_ON!')    # 文字列「LED_ON!」を返す
            print('LED_ON!')
        # 本体 LED OFF
        elif request == '/lightoff':  # 抽出したURLが「/lighton」なら
            pico_led.off()            # 本体LEDをオフにする
            client.send('LED_OFF!')   # 文字列「LED_OFF!」を返す
            print('LED_OFF!')
        # 本体温度を取得
        elif request == '/get/data':
            temperature = pico_temp_sensor.temp # 温度センサーから温度データを取得する
            temperature = "{:.1f}".format(temperature)  # 温度データを小数点1桁の文字列に変換
            client.send(temperature)            # 温度データを返す
            print('Temp:', temperature)
        client.close()  # クライアント接続を閉じる
        
    except KeyboardInterrupt:
        machine.reset()       # 停止(Ctrl-C)が押されたときにPicoを再起動する
    except OSError as e:
        client.close()        # OSエラーで接続を閉じる
        print('connection closed')

・プログラムの詳細(Webページ自動更新)

「応用編」のプログラムの詳細は以下のようになります。
Wi-Fiの接続設定は「基本編」とほぼ同じですが「Webページ」が大きく違うのと、「サーバー応答処理」が少し異なります。

  1. Wi-Fi接続の準備
  2. Webページの準備
  3. Wi-Fi接続の実行
  4. サーバー応答処理

1.Wi-Fi接続の準備

Wi-Fi接続の準備は「基本編」と同じように以下の手順で行いますが、ここは「基本編」と全く同じです。
 ①必要なライブラリ、パッケージの準備
 ②ネットワークのSSID、パスワードの設定
 ③Wi-Fi接続実行、IPアドレスの取得
 ④ソケットを開く

2.Webページの準備

Webページは「基本編」と同じように「htmlコード」を文字列として変数に格納して準備しておきます。(「応用編」は「CSS」と「JavaScript」を含みます。)

「基本編」と違い、文字列を「f」付の「’’’(三重引用符)」で挟むのではなく「f」を付けずに「’’’」だけで挟んで記入します。
これでテキスト全体を1つの純粋な文字列として扱うことができます。

「f」を付けて「f’’’ 〜 ’’’」で挟んで指定すると、その間の「中括弧{ }」で挟まれた文字列は「Python」コードから読み込まれるため「CSS」や「JavaScript」を含む文字列はエラーになります。

応用編」のWebページでは以下コードの 30〜55行目のように「JavaScript」の「FetchAPI」を使用してリクエストを送信し、受信したレスポンスデータによってWebページの表示を部分的に更新できるようにしています。

# Webページを準備
def webpage():
    html = '''
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <title>PicoW Controller</title>
        <style>
          #container {display: flex;justify-content: center;align-items: center;flex-direction: column;margin-top: 50px;}
          h1 {color: #666;}
          .button {width: 100px;height: 50px;border-radius: 5px;margin: 10px;cursor: pointer;font-size: 20px;font-weight: bold;color: white;text-align: center;line-height: 50px;}
          #on-button {background-color: green;}
          #off-button {background-color: red;}
          input[type="text"] {width: 200px;padding: 5px;font-size: 20px;text-align: center;margin: 20px;}
        </style>
      </head>
      <body>
        <div id="container">
          <h1>PicoW Controller</h1>
          <hr>
          <div>
            <button id="on-button" class="button">ON</button>
            <button id="off-button" class="button">OFF</button>
          </div>
           <div><p id="led_state"> </p></div>
          <input type="text" placeholder="Temperature" />
        </div>

        <script>
          const onButton = document.getElementById("on-button");
          const offButton = document.getElementById("off-button");
          const ledState = document.getElementById("led_state");
          const textBox = document.querySelector("input[type='text']");

          onButton.addEventListener("click", async () => {
            const response = await fetch("/lighton");
            const text = await response.text();
            ledState.textContent = text;
            console.log(text);
          });

          offButton.addEventListener("click", async () => {
            const response = await fetch("/lightoff");
            const text = await response.text();
            ledState.textContent = text;
            console.log(text);
          });

          setInterval(async () => {
            const response = await fetch("/get/data");
            const text = await response.text();
            textBox.value =   "本体温度:" + text;
          }, 1000);
        </script>
      </body>
    </html>
    '''
    return html

3.Wi-Fi接続の実行

Webページの準備ができたら以下のように「connect()」関数でW-Fi接続を実行し、取得した「IPアドレス」で「ソケット」を開き、Webページを読み込んでおきます。

# Wi-Fi接続実行
try:
    ip = connect()       # Wi-Fi接続を実行してIPアドレスを取得
    connection = open_socket(ip)    # IPアドレスでソケットを開く
    html = webpage()     # htmlデータ読み込み
except (KeyboardInterrupt, OSError) as e:
    machine.reset()      # 停止またはOSエラーで本体リセット

4.サーバー応答処理

最後に以下を実行することで、サーバー側でクライアント側からのリクエストを常に待ち受ける、接続待機状態となります。

「基本編」と異なるのは、リクエストがある毎に毎回Webページを返すのではなく、ルート(IPアドレス)アクセス時にだけWebページを返すようにしています。

その他のリクエストに対しても、リクエストに応じた処理だけを実行します。

# サーバー応答処理
while True:
    try:
        client = connection.accept()[0]    # クライアントからの接続要求を受け入れる
        request = client.recv(1024)        # クライアントからのHTTPリクエストを受信する
        request = str(request)             # バイト文字列を文字列に変換する
        try:                               # リクエストが存在しない場合に発生する可能性のあるIndexErrorをキャッチし、例外を無視する
            request = request.split()[1]   # 受信したデータからURLを抽出
        except IndexError:
            pass
        
        # ヘッダー情報を返す
        client.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        # ルートアクセスでhtmlデータを返す
        if request == '/':            # 抽出したURLが「/」なら
            client.send(html)         # Webページを返す
            print('Root Access')
        # 本体 LED ON
        elif request == '/lighton':   # 抽出したURLが「/lighton」なら
            pico_led.on()             # 本体LEDをオンにする
            client.send('LED_ON!')    # 文字列「LED_ON!」を返す
            print('LED_ON!')
        # 本体 LED OFF
        elif request == '/lightoff':  # 抽出したURLが「/lighton」なら
            pico_led.off()            # 本体LEDをオフにする
            client.send('LED_OFF!')   # 文字列「LED_OFF!」を返す
            print('LED_OFF!')
        # 本体温度を取得
        elif request == '/get/data':
            temperature = pico_temp_sensor.temp # 温度センサーから温度データを取得する
            temperature = "{:.1f}".format(temperature)  # 温度データを小数点1桁の文字列に変換
            client.send(temperature)            # 温度データを返す
            print('Temp:', temperature)
        client.close()  # クライアント接続を閉じる
        
    except KeyboardInterrupt:
        machine.reset()       # 停止(Ctrl-C)が押されたときにPicoを再起動する
    except OSError as e:
        client.close()        # OSエラーで接続を閉じる
        print('connection closed')

ブラウザから「IPアドレス(ルート)」にアクセスすると、文字列(html)として格納しておいたWebページが読み込まれ、以下のように表示されます。

ラズパイPico W Wi-Fi遠隔操作MicroPython

ブラウザ上にはボタンが2つあり、ボタンを押すとそれぞれ以下のような動作になります。

  • [ON]ボタン:「IPアドレス/lighton」でサーバーにリクエストを送信し、サーバーがこれを受信した場合は本体LEDを点灯させ、レスポンスとして文字列「LED_ON!」を返します。
  • [OFF]ボタン:「IPアドレス/lightoff」でサーバーにリクエストを送信し、サーバーがこれを受信した場合は本体LEDを消灯させ、レスポンスとして文字列「LED_OFF!」を返します。

本体温度の表示は「JavaScript」の「setInterval関数」を使用して、1秒毎にサーバー側へリクエストを送信することで、レスポンスデータを取得して表示を更新します。

「JavaScript」を使用することで動きのあるWebページを作成でき、IoTデバイスのデータ表示やグラフ表示等ができる操作画面も作ることができます。
「JavaScript」を使用したWebページの作成は私は苦手ですが・・・最近ではChatGPTが書いてくれるので楽になりました♪

今回使用したWebページも1回の質問でほぼ書いてくれるので、次はこの方法について紹介します。

5.動作確認用のWebページの作成はChatGPTにおまかせ

「JavaScript」を使用したWebページは「ChatGPT」にお任せしましょう。
質問の仕方によっては1回で目的の動作をするコードを書いてくれます。

今回使用した「応用編」のサンプルプログラムのWebページは以下の質問でほぼ書いてくれるので試してみましょう。


「ChatGPT」への質問は以下のように行いました。

以下の仕様を満たすHTMLタグをCSS、JavaScriptを含めて書いて
1. ブラウザにONボタン(内容:ON、色:緑色)とOFFボタン(内容:OFF、色:赤色)を横並びに配置して、その下に隙間を開けて、LEDの状態を表示する段落を配置(初期値は空白)して、その下に隙間を開けて、項目を温度表示としたテキストボックスを配置して、全体を上中央に表示する。
2. ONボタンを押すと非同期通信のfetchで”/lighton”へリクエストを送信し、受信したレスポンスをテキストで取得して、LEDの状態を表示する段落の内容を取得したテキストの内容で更新、かつコンソールログに表示する。
3. OFFボタンを押すと非同期通信のfetchで”/lightoff”へリクエストを送信し、受信したレスポンスをテキストで取得して、LEDの状態を表示する段落の内容を取得したテキストの内容で更新、かつコンソールログに表示する。
4. 1秒ごとに”/get/data”へリクエストを送信し、レスポンスを温度表示用のテキストボックスに表示する。

「ChatGPT」の回答は以下のようになりました。

以下は、上記の仕様に従ってHTML、CSS、JavaScriptを書いた例です。
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>LEDコントロールパネル</title>
    <style>
        body {
            text-align: center;  /* このCSSだけ後から追加 */
        }
        button {
            display: inline-block;
            padding: 10px 20px;
            font-size: 16px;
            border-radius: 5px;
            cursor: pointer;
        }
        button.on {
            background-color: green;
            color: white;
        }
        button.off {
            background-color: red;
            color: white;
        }
        p#led-state {
            margin-top: 20px;
            font-size: 18px;
        }
    </style>
</head>
<body>
    <div style="display:flex; justify-content:center;">
        <button id="on-button" class="on">ON</button>
        <button id="off-button" class="off">OFF</button>
    </div>
    <p id="led-state"></p>
    <div style="margin-top:20px; display:flex; justify-content:center;">
        <label for="temperature-box">温度表示:</label>
        <input type="text" id="temperature-box">
    </div>

    <script>
        const onButton = document.querySelector('#on-button');
        const offButton = document.querySelector('#off-button');
        const ledStatePara = document.querySelector('#led-state');
        const tempBox = document.querySelector('#temperature-box');

        onButton.addEventListener('click', () => {
            fetch('/lighton')
                .then(response => response.text())
                .then(data => {
                    ledStatePara.textContent = data;
                    console.log(data);
                });
        });

        offButton.addEventListener('click', () => {
            fetch('/lightoff')
                .then(response => response.text())
                .then(data => {
                    ledStatePara.textContent = data;
                    console.log(data);
                });
        });

        setInterval(() => {
            fetch('/get/data')
                .then(response => response.text())
                .then(data => {
                    tempBox.value = data;
                });
        }, 1000);
    </script>
</body>
</html>
以上が、仕様に沿ったHTML、CSS、JavaScriptの例です。このコードを実行すると、上部に「ON」と「OFF」のボタンがあり、下部にLEDステータス表示パラグラフ、および温度表示用のテキストボックスがあります。ユーザーは、ボタンをクリックして、LEDの状態を制御でき、1秒ごとに温度データが自動的に更新されます。

以上のような回答になりました。


このコードを「応用編」のサンプルプログラムの「webpage()関数」の中の「html = ”'” 〜 “””」の中で指定した文字列と置き換えて実行すると、以下のようなWebページが表示されました。

ラズパイPico W Wi-Fi遠隔操作MicroPython

残念ながら「LED_ON!」の表示だけ左端に表示されたため、上コードの中の「 7〜9行目」の「CSS」だけ後から追加しています。

修正したものを実行すると下画像のようになり、思い通りのWebページになり問題なく動作しました。

ラズパイPico W Wi-Fi遠隔操作MicroPython
「ChatGPT」への質問を正しくしてやれば1回で使えるコードを書いてくれます。
動かなかったとしても、症状や発生したエラーを続けて聞けば正しいものに書き換えてくれます。
今回使用した「ChatGPT」のAIは「ChatGPT-3.5 Turbo」です。
無料の「Text-davinci-003」では、「html」「JavaScript」「CSS」を個別で回答してきたので、それぞれを自分で組み合わせる必要がありました。
回答のコードは聞くごとに微妙に異なります。何度か聞いて一番良いものを使用しましょう。

ChatGPTを使用したプログラミングについては、以下のリンクで紹介しています。

Arduinoプログラミング初心者必見!ChatGPTがもたらす新しい可能性
ChatGPTがプログラミングの考え方を変えました「こんなプログラムを書いて」と聞けばコード作成から説明、エラーの指摘までしてくれる。ChatGPTがもたらす新しい可能性を探ってみたいと思います。

6.本体に別ファイルで保存したWebページを読み込む方法

動作確認用のWebページは「Python」のコードの中に文字列として埋め込みましたが「Raspberry Pi Pico W」本体内に保存しておくことで、別ファイルとして読み込んで表示させることもできます。

別ファイルとして保存しておくと、Webページを個別で動作確認や編集をすることが簡単にできるため、毎回「Python」コードに文字列として埋め込む手間が省けて便利です。


Webページを別ファイルとして保存するには「Thonny」を使用して以下のように行います。

まずは下画像のように[新規ファイル]をクリックすると「<untitled>」タブが表示されます

ラズパイPico W Wi-Fi遠隔操作MicroPython

次に下画像のようにWebページのコードを「<untitled>」内に作成またはコピペして[保存]ボタンをクリックします。

ラズパイPico W Wi-Fi遠隔操作MicroPython

下画像のように、保存場所を聞いてくるので[Raspberry Pi Pico]を選択します。

ラズパイPico W Wi-Fi遠隔操作MicroPython

下画像のようなウインドウが表示されたら「ファイル名」を「index.html」として[OK]ボタンをクリックします。

ラズパイPico W Wi-Fi遠隔操作MicroPython

次に「応用編」のサンプルプログラムの「webpage()関数」を下コードと置き換えてから実行します。

以下のコードが実行されると「PicoW」本体内に保存された「index.html」ファイルが読み込まれて、ファイルの内容が文字列として変数htmlに格納され、これをレスポンスとして返すことで、Webページとして表示させることができるようになります。

# 外部HTMLファイルの読み込み
def webpage():
    f = open('/index.html', 'r') # PicoW内に保存されたHTMLファイルを開く
    html = f.read()              # コンテンツを読み込む
    f.close()                    # ファイルを閉じる
    return html

7.まとめ

「Raspberry Pi Pico W」 のWi-Fi無線通信機能を使用して、パソコンやスマホのブラウザから遠隔操作、データ表示する方法を「MicroPython」で詳しく紹介しました。

最初の動作確認は「Raspberry Pi Foundation」の公式HPを参考に行いましたが、Webページの表示内容を更新するにはページの再読み込みが必要だったので、ここでは「基本編」として紹介しました。

実際にはWebページの表示内容は自動で更新されないと実用的ではないため「応用編」として「JavaScript」の「FetchAPI」の非同期通信でデータの送受信を行い、ページを自動更新する方法も紹介しました。

Webページのコードは「Pyhton」コード内に文字列として埋め込むこともできますが、別ファイルとして保存しておいた方が、Webページの動作確認や編集が容易に行えるため、その方法も紹介しました。

動作確認に使用するブラウザページ(html、JavaScript、CSS)は「ChatGPT」にお任せすれば比較的簡単に作成することができるので活用していきましょう。

これで「Raspberry Pi Pico W」のWi-Fi無線機能を「MicroPython」で自由に使えるようになりました。今後はこれを利用したIoTデバイスとして、データ収集や遠隔操作、監視等の方法も紹介していきたいと思います。

「CircuitPython」や「C言語(Arduinoコマンド)」を使用した遠隔操作、データ表示の方法も以下のリンクで詳しく紹介しています。

ラズパイPicoW Wi-Fi通信、遠隔操作&表示 CircuitPython編
Raspberry Pi PicoWのWi-Fi通信機能を使用してサーバー機能を利用した遠隔操作、データ監視する方法をCircuitPythonで詳しく紹介します。
ラズパイPicoW Wi-Fi通信、遠隔操作&表示 C言語(Arduinoコマンド)編
Wi-Fi通信機能を使用してPicoWをサーバーに設定して遠隔操作&表示する方法を詳しく紹介します。動作確認のブラウザページはChatGPTに全部書いてもらいましょう。

コメント

  1. kyoppe より:

    RaspPi PicoW の勉強の為、拝見させて頂いています。
    応用編のsample2をコピーして、PicoWにmain.pyとして動作させていますが
    長時間動作(1日程度)させていると、PICO W(WebServer)に接続できなくなります。
    尚、PICO Wを固定IPとする為、15行目に下記を追記しております。

    wlan.ifconfig((‘192.168.0.61’, ‘255.255.255.0’, ‘192.168.0.1’, ‘192.168.0.1’))

    何かヒントを頂けると助かります。

    • logikara より:

      kyoppeさん初めまして、読んでいただきありがとうございます。
      同じように固定IPで動作させてしばらく様子を見てますが、とりあえず接続できています。
      こちらも1日放置して様子を見てみます。

      接続できない状態でコマンドプロンプト(MACならターミナル)から「ping 192.168.0.61」を入力すると応答は返ってきますでしょうか?
      返ってこない場合はラズパイサーバーが何らかの原因で停止してます。
      この時ラズパイ自体が生きているかどうかを確認するため、本体LEDを常にフリッカさせる処理を入れて様子をみてみてはいかがでしょうか?
      フリッカも止まっていればラズパイ自体が停止しているので、ネットワークというよりもハードの問題か、電源の問題かもしれません。
      パソコンと接続して放置した時、シリアルモニタ(Thonnyならシェル)の温度表示がどのタイミングで表示されなくなるでしょう?何かエラーは出てないでしょうか?

      ひとまず思いつくのはこの辺りですが、こちらでも同様のエラーが出るようであればもう少し探ってみたいと思います。

      • kyoppe より:

        早速の返信を頂きながら、返信せずすみませんでした。

        先日の質問後、接続できない現象発生していおりません。現在も1週間程、電源入れたまま、1日数回、複数の端末(PC1台、スマホ2台)からアクセスしていますが、正常に接続出来ています。外出先からVPN接続した場合も正常に接続できています。

        引き続き、連続動作させて様子を見てみます。
        問題発生時は、また質問させて頂きますので、その際は、よろしくお願いいたします。

        • logikara より:

          返信に関してはお気になさらず^^結果ご連絡いただけただけでありがたいですよ♪

          あの時はこちらも2日程放置してみたのですが特に問題なく動いていたので、一時的なネットワークのトラブルがあったんでしょうかね。
          サンプルプログラムについは実際の運用上の問題には気づかないこともあるので、また何あったら教えてください。

タイトルとURLをコピーしました