🌐

Raspberry Pi Pico Wで無線Lチカの発展版

2023/07/09に公開
2

背景

Rapberry Pi Pico W(以下Pico W)で無線Lチカをされている例は多いですが、多くはadafruit_httpserverなどを利用して、Pico W側にwebサーバーの機能を持たせているように思います。しかし現実的には、外部にwebサーバーを置き、そこにPico Wや手元のPC・スマホなどからアクセスするほうが自然だと思います。というわけで今回は、FastAPIを使って簡易なAPIサーバーを立てながら、Reactを使って簡易なGUIを作り、Raspberry Pi Pico Wを無線でLチカしてみました。同じように自前でwebサーバーを作ってみたい方の参考になれば幸いです。(動作を保証するものではありませんが、そこまでバージョンとかは意識しなくても動くと思います。)

バックエンド

APIサーバーはFast APIというPythonのフレームワークを使って作成しました。選定理由としては、使用した経験が少しあったのと、2020~2021くらいには既にある程度流行ってた技術なので、github copilotやchatgptなどのコード補完や生成の精度が高そうというのもありました。

今回は、簡単に/ledというエンドポイントを作り、そこにGETPUTの2種類のメソッドを定義して、今ledをonにするかoffにするかの命令がどっちになっているのかを、取得したり設定したりできるようにしました。

以下のようなjson形式でリクエストを受けたり、返したりしています。

{"led":"on"}

言い訳

わざわざDBを作るのが面倒だったのでled_onというグローバル変数を一個置きました。また、Raspberry Pi Pico W のCORS設定をどうしたら良いか分からなかったので、結局どこからでもAPIにアクセス出来るようにしました。(セキュリティ的には良くないです。)

コードは以下の通りです。

main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import  BaseModel

class Item(BaseModel):
    led:str


app = FastAPI()


app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


led_on=True

@app.put("/led")
async def set_led(led:Item):
    global led_on
    status=led.led
    if(status=="on"):
        led_on=True
        print(led_on)
    elif(status=="off"):
        led_on=False
        print(led_on)
    else:
        return {"error": "invalid led value"}
    return {"led": status} 

@app.get("/led")
async def get_led():
    global led_on
    if(led_on):
        return {"led": "on"}
    else:
        return {"led": "off"}

フロントエンド

フロントエンドには、React×Typescriptを採用しました。これも経験が少しあるからなのですが、じつはNext JSを使っていた経験もあり、データのfetchを高速にいい感じにやってくれそうな気がしたのでNext JSを最初は使おうとしたのですが、キャッシュ周りの仕様が最近変化したために、上手く動かすことが出来なかったので、React×Typescriptを採用しました。また、個人的に好きなTailwind CSSも使いました。(素人意見ですがネット上のサンプルをコピペしたり、場合によってはCopilotが提案してくれるclassNameを脳死で使ってももそこそこのデザインになってくれるので、個人開発にはもってこいだと思います)

仕様

4秒ごとにリロードして、GET /ledして状態を監視しています。また、on/offボタンでPUT /ledを投げるようにしています。今の表示項目をコンポーネント化して、カードを並べるデザインに今後しようかなと考えています。
また.envにAPIサーバーを立てているPCのipv4アドレス+ポート番号を設定しています。

.env
REACT_APP_API_BASE_URL = http://xxx.xx.xx.x:8000

以下にコードを載せときます。

/src/App.ts
import React, { useState, useEffect } from 'react';

interface LedData {
  led: string;
}

function App(): JSX.Element {
  const [led, setLed] = useState<string>("off");
  const [error, setError] = useState<string | null>(null);

  const apiBaseurl=process.env.REACT_APP_API_BASE_URL;
  console.log(apiBaseurl);

  useEffect(() => {
    const reloadInterval = setInterval(() => {window.location.reload(); }, 4000);
    return () => {clearInterval(reloadInterval); };
  }, []);

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch(apiBaseurl+"/led", {
          method: "GET",
        });

        if (response.ok) {
          const data: LedData = await response.json();
          setLed(data.led);
          console.log(data);
        } else {
          throw new Error("Network response was not ok.");
        }
      } catch (error: any) {
        console.error("Error:", error);
        setError(error.message);
      }
    }

    fetchData();
  }, [apiBaseurl]);

  return (
    <>
    <div className="max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
      {error ? <h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">{error}</h5>:<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">LEDは現在{led}です</h5> }
      <button
        type="button"
        className="text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 font-medium rounded-full text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
        onClick={async () => {
          try {
            const response = await fetch(apiBaseurl+"/led", {
              method: "PUT",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ led: "on" }),
            });

            if (!response.ok) {
              throw new Error("Network response was not ok.");
            }
          } catch (error: any) {
            console.error("Error:", error);
            setError(error.message);
          }
        }}
      >
        ON
      </button>
      <button
        type='button'
        className="py-2.5 px-5 mr-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
        onClick={async () => {
          try {
            const response = await fetch(apiBaseurl+"/led", {
              method: "PUT",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({ led: "off" }),
            });

            if (!response.ok) {
              throw new Error("Network response was not ok.");
            }
          } catch (error: any) {
            console.error("Error:", error);
            setError(error.message);
          }
        }}
      >
        OFF
      </button>
    </div>
      
    </>
  );
}

export default App;

Pico W

言語はCircuitPythonを利用しています。adafruit_requestsというライブラリを使うと簡単にAPIリクエストを投げることが出来ますし、jsonのparseも簡単に出来ます。個人的には、もっとCircuitPython流行って欲しい。。。

setting.tomlに、SSIDやPassWord、APIサーバーを立てているPCのipv4アドレス+ポート番号を設定しておくことで、os.getenvから取得できます。
設定ファイルの例を示しておきます。

setting.toml
CIRCUITPY_WIFI_SSID = "your-ssid"
CIRCUITPY_WIFI_PASSWORD = "your-ssid-password"
IPV4_ADDERSS = "http://xxx.xx.xx.x:8000"

コードを以下に載せておきます。

code.py
import os
import time
import ssl
import wifi
import socketpool
import microcontroller
import digitalio
from board import *
from lib import adafruit_requests


led=digitalio.DigitalInOut(LED)
led.direction=digitalio.Direction.OUTPUT
led.value=False

#  FastAPI URL
api_url=str(os.getenv('IPV4_ADDERSS'))+"/led"
print(api_url)

#  connect to SSID
wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD'))

pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())

while True:
    try:
        print("Fetching text from %s" % api_url)
        response = requests.get(api_url)
        res_json=response.json()
        if(res_json['led'] == 'on'):
            print("LED turn on")
            led.value=True
        elif(res_json['led'] == 'off'):
            print("LED turn off")
            led.value=False
    
        response.close()
        time.sleep(5)
    except Exception as e:
        print("Error:\n", str(e))
        print("Resetting microcontroller in 10 seconds")
        time.sleep(30)


まとめ

正直私はどの分野も少しかじっているだけなので、用語の誤りなどあればコメントいただけると幸いです。動作確認の動画もあるんですが、色々と映ってしまっているのでまた時間があるときにデモを撮って載せようと思います。また、今後センサーデータやRSSIなどの取得も考えているので更新するかもしれません。

参考

https://learn.adafruit.com/pico-w-wifi-with-circuitpython/overview

Discussion

apolliapolli

code.pyの17行目
api_url=str(os.getenv('IPV4_ADDERSS'))+"/led"
についてですが、アドレスの先頭に http:// をつけないとエラーが出て動かないようです。

api_url="http://"+str(os.getenv('IPV4_ADDERSS'))+"/led"

sou1649sou1649

ご意見ありがとうございます。書き方が略しすぎており、すみませんでした。設定ファイルの例を追加しておきました。