Web Serial APIとCDN版Vue.jsで作る、Raspberry Pi Pico WのWiFi設定ツールの実装例

## 今回の内容

[前回](https://qiita.com/asd_m/items/85b86601af3db2799af7)では、Web Serial APIを介してブラウザとRaspberry Pi Pico Wが通信するJavaScriptコードのクラス化と、CDN版Vue.jsをあえて用いたGUI側の制御を実装しました。  
今回は実際に、Raspberry Pi Pico WのWiFi設定をPCのWebブラウザとUSBシリアル経由で行えるツールを、Vue.jsとWeb Serial APIで構築してみました。  
設定情報は暗号化され、再設定操作も制限されるなど、**実用を見据えた構成**になっています。


### Raspberry Pi Pico W側のMicroPythonコード

まず、Raspberry Pi Pico Wに以下の2つの機能を持たせたコードを実装します：

- 引数として受け取ったWiFi接続情報を保存する  
- 保存された接続情報をもとにWiFiに接続する  

このコードは `boot.py` や `main.py` **ではありません**。もちろん、それらから `import` されて使用されることは想定していますが、これはあくまで独立したモジュールとしての実装です。  
[wifisetting.py](https://github.com/DBA-Z21A/raspico_wifi_set_repl/blob/main/pico/firmware/wifisetting.py)

このコードの主な機能は、まず引数として受け取ったWiFi情報を内部に保存することです。

```python
def save_wifi_config(json_string, filename=_SETTING_FILE_NAME):
    """
    引数で指定された文字列をJSONとして解釈し、パスワードを暗号化してからファイルに保存
    """
    try:
        wifi_config = ujson.loads(json_string)
        key = generate_key()
        if "wifi_password" in wifi_config and wifi_config["wifi_password"]:
            encrypted_password = encrypt_password(wifi_config["wifi_password"], key)
            wifi_config["wifi_password_encrypted"] = encrypted_password
            del wifi_config["wifi_password"]  # 元のパスワードを削除

        with open(filename, "w") as f:
            ujson.dump(wifi_config, f)
        print(f"WiFi configuration saved to '{filename}'.")
        return True, wifi_config
    except Exception as e:
        print(f"Error saving WiFi config: {e}")
        return False, None
```

保存された情報は、以下の関数でWiFi接続に使用されます。

```python
def apply_wifi_config(filename=_SETTING_FILE_NAME):
    """
    ファイルに保存されたWiFi接続情報を用いてWiFiに接続
    """
    try:
        if filename not in os.listdir():
            print(f"Config file '{filename}' not found.")
            return False

        config = ujson.loads(open(filename).read())
        wlan = network.WLAN(network.STA_IF)
        if wlan.isconnected():
            wlan.disconnect()
        print("Disconnect Wi-Fi...")
        wlan.active(True)

        ssid = config.get("wifi_ssid")
        encrypted_password = config.get("wifi_password_encrypted")
        use_static = config.get("use_static_ip", False)
        ip = config.get("static_ip")
        subnet = config.get("subnet_mask")
        gateway = config.get("gateway")
        dns = config.get("dns_server")

        password = None
        if encrypted_password:
            key = generate_key()
            try:
                password = decrypt_password(encrypted_password, key)
            except ValueError as e:
                print(f"Password decryption error: {e}")
                return

        if ssid and password is not None:
            if use_static:
                if ip and subnet and gateway and dns:
                    wlan.ifconfig((ip, subnet, gateway, dns))
                    print(f"Using static IP: {ip}, GW: {gateway}, Subnet: {subnet}, DNS: {dns}")
                else:
                    print("Static IP configuration incomplete.")
            else:
                print("Using DHCP.")

            wlan.connect(ssid, password)
            print(f"Connected to {ssid}.")
            print("IP Address:", wlan.ifconfig()[0])
            return True
        else:
            print("SSID or encrypted password not found in configuration.")
    except Exception as e:
        print(f"Error connecting WiFi: {e}")
    return False
```

この2つの処理そのものは特に目新しいものではありません。Qiitaにも多くの情報があるので、ぜひ他の方の記事も参照してみてください。

そのほか、以下のような付随機能も実装しています：

- WiFi接続パスワードを、Raspberry Pi Pico Wの固有IDをキーとして暗号化  
- アクセス可能なWiFiのSSIDを検索し、電波強度を測定  
- 接続後、導通確認を兼ねてNTPサーバへアクセスし、内部の時計を同期  

暗号化については、Raspberry Pi Pico Wの固有IDをそのままキーに使うと容易に解読されてしまう可能性があります。

そのため、実運用では次のような工夫を加えるとより安全です：

- 固有IDにビットシフトをかける
- 固定のプレフィックス／サフィックスを加える
- `.mpy` にコンパイルして外部からの読み取りを困難にする

こうすることで、仮に機器が盗難されたとしてもWiFi情報の流出を防ぎやすくなります。


### （おまけ）main.py

「このように使用されることを想定している」という意味で、`main.py` も記述してみました。  
[main.py](https://github.com/DBA-Z21A/raspico_wifi_set_repl/blob/main/pico/firmware/main.py)  
内容は非常にシンプルで、以下のような処理をしています：

1. WiFi接続情報がなければ終了  
2. NTPサーバに接続して内部時計を同期  
3. 指定されたURLに1分ごとにアクセスし、結果をprintに出力  
4. ただし、5秒以内にBOOTSELボタンが3回押された場合は「リセット待機モード（LED点滅）」に移行し、この状態でさらに5秒以内に3回押された場合はWiFi接続情報を削除して再起動  

今回の例では、何も接続していないRaspberry Pi Pico Wでも実行可能なようにBOOTSELボタンと本体のLEDのみを使用してみました。
※一応、コード内ではGPIO28にブザーが接続されている事を想定していますが、無くとも動作します
実運用では、GPIOにスライドスイッチやLED、ブザーを接続したりして制御する事が望ましいと思われます。
以上、よろしければ実際の実装の参考にしてみてください。

## HTML側の実装

上記 `wifisetting.py` の関数を、Web Serial APIとREPLを介してブラウザから呼び出すHTMLおよびJavaScriptが以下になります：

- [pico_repl3.html](https://github.com/DBA-Z21A/raspico_wifi_set_repl/blob/main/web/frontend/pico_repl3.html)  
- [WebSerial.js](https://github.com/DBA-Z21A/raspico_wifi_set_repl/blob/main/web/frontend/js/WebSerial.js)（変更なし）  
- [VueWebSerial.js](https://github.com/DBA-Z21A/raspico_wifi_set_repl/blob/main/web/frontend/js/VueWebSerial.js)（変更なし）

注目していただきたいのは後ろの2つです。  
[前回](https://qiita.com/asd_m/items/85b86601af3db2799af7) 実装したJavaScriptコードを再利用しています。

```html
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js",
      "mitt": "https://cdn.jsdelivr.net/npm/mitt@3.0.1/+esm",
      "WebSerial": "./js/WebSerial.js",
      "VueWebSerial": "./js/VueWebSerial.js"
    }
  }
</script>
```

このように `import` 可能とすることで、前回のコードにある `ConsoleLog`, `SentLog`, `ReceivedLog` などの処理はそのまま活かしつつ、今回のWebページで必要な機能だけを追加しています。

```javascript
<script type="module">
  import { createApp, ref } from "vue";
  import { vueWebSerial } from "./js/VueWebSerial.js";
  const app = createApp({
    setup() {
      const { ConsoleLog, SentLog, ReceivedLog, wasConnected, clickStart, clickClose, clickTest } = vueWebSerial();
      const wifiInfo = {
        SSID: "",
        password: "",
        useStaticIP: false,
        staticIP: [0, 0, 0, 0],
        subnetMask: [255, 255, 255, 0],
        gateway: [0, 0, 0, 0],
        DNS: [8, 8, 8, 8],
      };
      const wifi = ref(wifiInfo);
      const showConsoleLog = ref(false);
      const showSentLog = ref(false);
      const showReceivedLog = ref(true);
      const sendWiFiSetting = async function () {
        const divError = document.getElementById("divWiFiInfoError");
        divError.textContent = "";
        if (!wifiInfoValidation()) {
          divError.textContent = "invalid input...";
          return;
        }
        const formattedWifiData = {
          wifi_ssid: wifi.value.SSID,
          wifi_password: wifi.value.password,
          use_static_ip: wifi.value.useStaticIP,
          static_ip: wifi.value.staticIP.join("."),
          subnet_mask: wifi.value.subnetMask.join("."),
          gateway: wifi.value.gateway.join("."),
          dns_server: wifi.value.DNS.join("."),
        };
        console.log(JSON.stringify(formattedWifiData));
        nowSendingToSerial.value = true;
        await globalThis.webSerialManager.sendCommands([
          "import wifisetting",
          "wifisetting.save_wifi_config('" + JSON.stringify(formattedWifiData) + "')",
          "wifisetting.apply_wifi_config()",
        ]);
        nowSendingToSerial.value = false;
      };
      const wifiInfoValidation = function () {
        return wifi.value.SSID && wifi.value.password;
      };
      const doScanSSID = async function () {
        nowSendingToSerial.value = true;
        await globalThis.webSerialManager.sendCommands([
          "import wifisetting",
          "wifisetting.scanSSID()",
          "wifisetting.get_time_from_NTP()",
        ]);
        nowSendingToSerial.value = false;
      };
      const nowSendingToSerial = ref(false);
      return {
        showConsoleLog,
        ConsoleLog,
        showSentLog,
        SentLog,
        showReceivedLog,
        ReceivedLog,
        wasConnected,
        clickStart,
        clickClose,
        clickTest,
        wifi,
        doScanSSID,
        sendWiFiSetting,
        wifiInfoValidation,
        nowSendingToSerial,
      };
    },
  });
  app.mount("#formWebSerial");
</script>
```

ここでは、`sendWiFiSetting`という関数の中で以下の処理を実装しています
 
 - REPL経由でRaspberry Pi Pico WのMicroPythonに先ほど実装した`wifisetting`をimport
 - 画面から入力したWiFi接続文字列をJSONに編集
 - `wifisetting`の関数`save_wifi_config`を実行し、その引数として先ほどのJSONを引き渡し
 
これで、連載の当初に掲げた **「Raspberry Pi Pico WのWiFi接続情報をWebブラウザから設定する」** という機能が実現できました。PC側はすべてHTMLとJavaScriptで構成していますので、（第二回でも言及したように）WiFi接続情報以外の機器固有の情報（例：クラウドへの接続トークンなど）をサーバ側で生成し引き渡すことも可能です。

また、GUI制御についても、この画面で追加した機能以外はすべて前回のVueWebSerial.jsから再利用しています。これで、少なくとも**コードの再利用性**という面では、SFC（Single File Component）形式に劣らない柔軟性をCDN版でも実現できたと自負しています。

## 最後に

全5回にわたり、Web Serial APIとREPLを活用してRaspberry Pi Pico WのWiFi設定を行う仕組みをご紹介してきました。

今回の実装は、IoTデバイスの実運用を視野に入れた構成であり、CDN版Vueによる柔軟なUI構築の一例にもなれば幸いです。

今後もRaspberry Pi Pico Wを活用した実装例を投稿していく予定ですので、ぜひフォロー・ご意見をお寄せいただけると嬉しいです！