MicroPythonでWi-Fiの設定画面(CaptivePortal)を実装する

## はじめに
皆さんはMiroPythonをインストールしたRaspberryPi Pico WやESP32等のマイクロコントローラをWi-Fiに接続して使用するようなシーンにおいて接続したいWi-FiのSSIDやパスワードをソースコードに直書きせず、なんらかの手法で後から自由に変更できるような形で設定したいと思ったことはないでしょうか？私はあります。

アクセスポイントモードで起動したデバイスにスマホ等を接続し表示されたWEBページからSSIDやパスワードを設定する、さらにWEBページのURL等を覚えていなくてもCaptivePortalとして勝手に設定ページが立ち上がる...そんな機能を実装したので紹介します。

#### この記事で触れないこと
RaspberryPi PicoやESP32等へのMicroPythonのインストール、Thonny等の開発環境構築・操作等に関しては触れません。

## wifi-setupライブラリ
#### 概要
前述のとおりMicroPythonにてCaptivePortalを実装し、表示されるページ上でMicroPythonデバイスが接続してほしいWi-FiのSSIDやパスワードを設定できるものです。
※MicroPythonでのCaptivePortal実装には[micropython-captiveportal](https://github.com/metachris/micropython-captiveportal)を大いに参考にしました
※RaspberryPi Pico W & MicroPython v1.23.0で動作確認しています

リポジトリはこちら：

https://github.com/H0ukiStar/micropython-wifi-setup

#### ライブラリのインストール
リポジトリ上の`wifi_setup`フォルダ一式をMicroPythonデバイスの`lib`フォルダ内にコピーします。
```txt:ディレクトリツリー
root
└─lib
    └─wifi_setup
        ├─wifi_setup.py
        ├─__init__.py
        │
        └─web
            ├─error.html
            ├─index.html
            ├─style.css
            └─success.html
```

#### サンプルコードのコピー
起動時に処理される`boot.py`とその処理完了後に実行される`main.py`のサンプルをリポジトリ上の`sample`フォルダ内からコピーします。
※RaspberryPi Pico Wでの利用を想定したサンプルコードとしています

`boot.py`ではボタン(RaspberryPiのBOOTSELボタン)が押下されている場合にWi-Fiの設定を削除するサンプルとなっています。

<details><summary>boot.py サンプルコード</summary>

```python:boot.py
import time

from wifi_setup import WifiConfig

# Sample of deleting settings when some button is pressed at boot.
# For RaspberryPi Pico W
if WifiConfig().check():
    time.sleep_ms(3000)
if rp2.bootsel_button() == 1:
    WifiConfig().delete()
```

</details>

`main.py`ではWi-Fiが未設定の場合はWi-Fi設定用のポータルを起動、設定済みの場合は設定情報に基づいてWi-Fiへの接続とメインルーチンを実行するサンプルとしています。

<details><summary>main.py サンプルコード</summary>

```python:main.py
import time
from network import (
    WLAN,
    STA_IF,
    STAT_GOT_IP,
    STAT_NO_AP_FOUND,
    STAT_CONNECT_FAIL,
    STAT_WRONG_PASSWORD,
)
from machine import reset

from wifi_setup import WifiConfig, WifiSetupPortal

class IoTDevice:
    def execute(self) -> None:
        try:
            if not WifiConfig().check():
                print("start wifi setup portal")
                WifiSetupPortal().execute()
            else:
                print("start iot device")
                if not self._connect_wifi():
                    raise Exception("cannot connect wifi")
                self._main_routine()
        except Exception as e:
            print(f"{e=}")
            time.sleep(1)
            reset()

    def _connect_wifi(self) -> bool:
        ssid: str
        key: str
        ssid, key = WifiConfig().get()
        wlan: WLAN = WLAN(STA_IF)
        wlan.active(True)
        wlan.connect(ssid, key)

        while True:
            time.sleep(1)
            status: int = wlan.status()
            if status == STAT_NO_AP_FOUND or status == STAT_CONNECT_FAIL:
                break
            elif status == STAT_WRONG_PASSWORD:
                print("cannot connect wifi because incorrect password so wifi_config reset")
                WifiConfig().delete()
                reset()
                break
            elif status == STAT_GOT_IP:
                return True
        return False

    def _main_routine(self) -> None:
        # Your main routine here
        print("main routine")

if __name__ == "__main__":
    IoTDevice().execute()
```

</details>

#### スマホからのWi-Fi設定
1. サンプルを実行するとMicroPythonデバイスが無線アクセスポイントモード(デフォルトではSSID：`IoTDeviceSetUp`のセキュリティ無し)で起動します
1. スマホ等を使用し無線アクセスポイントモードで起動してきたMicroPythonデバイスのSSIDに接続します
1. CaptivePortalとして実装しているので(Androidでは)「Wi-Fiネットワークにログイン」の通知及びWi-Fi設定画面へ自動遷移します
1. 表示された`Wi-Fi設定画面`にてMicroPythonデバイスが接続してほしいWi-Fiの`SSID`及び`Password`を入力し、`Submit`します
1. リクエストに成功するとMicroPythonデバイスが再起動し`boot.py`や`main.py`が再度実行され、サンプルコードではWi-Fi設定済み時の処理(無線APへの接続やメインルーチンの実行)が行われます

![一連のスクショ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3870591/30d6e5dc-63cc-bcef-2bf6-d5805edfab82.jpeg)

#### 補足
##### 無線アクセスポイントモード時のSSIDやパスワードのカスタマイズ
MicroPythonデバイスにWi-Fi設定を行う時の無線アクセスポイントの`SSID`や`パスワード`、設定ページの`ドメイン`は`WifiSetupPortal`クラスのインスタンス生成時に指定できます。
```python:カスタマイズ
WifiSetupPortal(
    ssid="SetUpSSID",
    key="SetUpPassword",
    domain="setup.local"
)
```
##### Wi-Fi設定情報へのアクセス
Wi-Fiの設定情報はMicroPythonデバイスのroot直下に`wifi_config.json`として保存され直接のアクセスも可能ですが、`WifiConfig`クラスから操作できます。
* `WifiConfig().get()`：設定された`SSID`と`パスワード`をタプルで返します
* `WifiConfig().check()`：設定ファイルの存在確認と設定情報を正しく読み込み可能か確認し結果をboolで返します
* `WifiConfig().save()`：与えられた`SSID`と`パスワード`を設定ファイルに保存します
* `WifiConfig().delete()`：設定ファイルを削除します

## 最後に
CaptivePortalをWi-Fiを使用しようとする者の認証以外の用途で使用してもいいものか？は悩みましたが、スマホ等で設定画面に自動で遷移するという利便性・魅力の前に考えることをやめて実装しました。

個人の趣味としての電子工作等の用途であればWi-Fi設定のためにアプリ等を必要とせず、自動で画面遷移するためMicroPythonデバイスのIPアドレスやURL等を覚えておく必要がないため便利なんじゃないかなと思います。