今回の内容
前回では、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
このコードの主な機能は、まず引数として受け取ったWiFi情報を内部に保存することです。
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接続に使用されます。
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
内容は非常にシンプルで、以下のような処理をしています:
- WiFi接続情報がなければ終了
- NTPサーバに接続して内部時計を同期
- 指定されたURLに1分ごとにアクセスし、結果をprintに出力
- ただし、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
- WebSerial.js(変更なし)
- VueWebSerial.js(変更なし)
注目していただきたいのは後ろの2つです。
前回 実装したJavaScriptコードを再利用しています。
<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ページで必要な機能だけを追加しています。
<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を活用した実装例を投稿していく予定ですので、ぜひフォロー・ご意見をお寄せいただけると嬉しいです!