この記事でやること
- Raspberry Pi Pico Wをwifiのアクセスポイント(AP、親機)として動作させる
- WEBサーバー機能を稼働させてクライアントのブラウザ経由で通信する
- 単純にアクセスポイントとして機能させた場合、接続後にクライアント側でブラウザのURLバーにサーバーのIPアドレスを入力しなければアクセスを開始できない
- サーバーのIPアドレスを入力するのが面倒なので、クライアントが接続すると同時に特定のwebページに自動的にアクセスするように仕向ける
使うもの
- Raspberry Pi Pico W
- Arduino IDE(1.8.13、Windows10)
Arduino IDEで使うライブラリはRaspberry Pi公式のArduino Mbed OS RP2040ではなく、Earle F. Philhower, III氏の ボードライブラリ(v3.1.0で確認) を使用します。
ライブラリのインストール方法は省略します。
書き込み時はボード選択をRaspberry Pi Pico Wにしてください。
確認したクライアント環境
- Windows10(+Chrome)
- Android11
クライアントの挙動はOSに依存します。
Windowsではブラウザが起動します。
Androidではブラウザではなくキャプティブポータル用のアプリが立ち上がるようです。さしあたっての操作はブラウザとの違いはないですが、ブラウザの機能を使いたい場合は改めてブラウザからアクセスしなおした方がいいかもしれません。
MacやiOSでは確認していないです。
仕組み
- サーバー側のDNS機能とクライアント側のキャプティブポータルの機能を使います。
- キャプティブポータルとは、公衆wifiなどで接続後にログインを求めるような場合に認証画面に誘導する仕組み(らしい)です。クライアント側がwifi接続後にキャプティブポータルにアクセスしようとする動作を利用します。
- クライアントのOSはキャプティブポータルのページが存在するかどうか確かめるアクセスを試みますが、DNSサーバー側はそのアクセスをwebサーバーのIPアドレスに名前解決し、不明なページへのアクセスをリダイレクトさせてwebサーバーの任意のページに誘導します。
- OSはキャプティブポータルからの応答をブラウザにつなぐので、ユーザーは通常のWEBアクセスに近い状態でアクセスが始まります(OSによって挙動が異なります)。
- wifiの接続操作をしたあと、上記の流れが自動で実行されるため、ユーザーがサーバーのIPアドレスを調べて入力する必要がありません。
操作手順
- wifi接続するクライアント機器の他のネットワーク接続を切っておく
- コードを書き込んだRaspberry Pi Pico Wに給電する
- PCとUSB接続している場合はシリアルポートを開いておく(状態がわかるため)
- クライアント機器からwifiに接続する
- サンプルコードそのままの場合は SSID「hogehoge」、PASS「fugafuga」 です
- クライアント機器の画面上で自動的にwebページが開けば成功
注意
クライアント機器が他のネットワークに接続している場合はうまく動作確認できない場合があります。
モバイルネットワークや有線LANとの接続を切り、wifiの自動接続をオフにしておくとスムーズです。
コードについて
このコードの重要な部分は
dnsServer.start(DNS_PORT, "*", ipAdr);
の部分です。
あらゆるドメインの名前解決をwebサーバに仕向けます。キャプティブポータルを確認するURLはOS依存になり、かつネットワーク内に名前解決の必要な機器がないのでこの処理で済みます。
setup1()
とloop1()
は動作状況をシリアルモニタで確認する目的に動かしているだけです。
コードの作成にはEarle F. Philhower, III氏のライブラリ内のサンプルコード CaptivePortalAdvanced を参考にしながら、今回使用する部分だけピックアップして最低限の処理になるようコードを作っています。
また、ESP32は使ったことないですが、ライブラリの多くの部分が共通なようなのでESP32でも動かもしれません。
コード
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <DNSServer.h>
#ifndef STASSID
#define STASSID "hogehoge" //SSID name, change if you need
#define STAPSK "fugafuga" //wifi password, change if you need
#endif
const char* ssid = STASSID;
const char* password = STAPSK;
WebServer server(80);
const byte DNS_PORT = 53;
DNSServer dnsServer;
void handleRoot() { //" http://192.168.46.1/ "
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1");
server.send(200, "text/plain", "This page is Root.\r\n");
Serial.println("A client access to ROOT.");
}
void handleRedirect() { //" http://192.168.46.1/redirect "
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1");
server.send(200, "text/html", "Click <a href='/'>Here</a> to Root page.\r\n");
Serial.println("A client access to REDIRECT Page.");
}
void handleNotFound() { // All not fond page in " http://192.168.46.1/ " domain.
String redirectPage = "http://" + WiFi.localIP().toString() + "/redirect";
server.sendHeader("Location", redirectPage);
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1");
server.send(302, "text/plain", "");
Serial.println("A client access to NotFound Page.");
}
void setup(void) {
Serial.begin(115200);
WiFi.mode(WIFI_AP);
IPAddress ipAdr = IPAddress(192, 168, 46, 1);// Server IP address, change if you need
IPAddress ipSubnet = IPAddress(255, 255, 255, 0);
WiFi.softAPConfig(ipAdr,ipAdr,ipSubnet);
WiFi.softAP(ssid, password);
dnsServer.start(DNS_PORT, "*", ipAdr); //CaptivePortal will redirect to handleNotFound().
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
server.on("/", handleRoot);
server.on("/redirect", handleRedirect);
server.onNotFound(handleNotFound);
server.begin();
}
void loop(void) {
dnsServer.processNextRequest();
server.handleClient();
}
void setup1(void) {
while (WiFi.status() != WL_CONNECTED) { delay(500); }
}
void loop1(void) {
Serial.print(" || Wifi AP <SSID> : "); Serial.print(WiFi.SSID());
Serial.print(" , <PASS> : "); Serial.print(password);
Serial.print(" , <IP address> : "); Serial.print(WiFi.localIP());
Serial.print(", <getTime> : "); Serial.println(WiFi.getTime()); //return millis()
delay(5000);
}
おわりに
調べながら動作確認ベースでコーディングしたので、記事内の説明はニュアンス等が間違ってる可能性があります。
インターネットに接続するネットワークとして期待するとあまり良い挙動ではないですが、クローズドネットワークのアクセスポイントであることを考えると、望ましくはないもののそれほど問題ではない動きかなと思います。