RaspberryPiPicoWをwifiアクセスポイント(AP)モードで使用して接続後自動でリダイレクトさせる（ArduinoIDE）

# この記事でやること
- **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氏の　[ボードライブラリ](https://github.com/earlephilhower/arduino-pico)（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ページが開けば成功**

:::note warn
注意
**クライアント機器が他のネットワークに接続している場合はうまく動作確認できない**場合があります。
モバイルネットワークや有線LANとの接続を切り、**wifiの自動接続をオフにしておく**とスムーズです。
:::

## コードについて
このコードの重要な部分は
`dnsServer.start(DNS_PORT, "*", ipAdr);`
の部分です。
あらゆるドメインの名前解決をwebサーバに仕向けます。キャプティブポータルを確認するURLはOS依存になり、かつネットワーク内に名前解決の必要な機器がないのでこの処理で済みます。
`setup1()`と`loop1()`は動作状況をシリアルモニタで確認する目的に動かしているだけです。

コードの作成にはEarle F. Philhower, III氏のライブラリ内のサンプルコード　[CaptivePortalAdvanced](https://github.com/earlephilhower/arduino-pico/tree/master/libraries/DNSServer/examples/CaptivePortalAdvanced)　を参考にしながら、今回使用する部分だけピックアップして最低限の処理になるようコードを作っています。

また、ESP32は使ったことないですが、ライブラリの多くの部分が共通なようなのでESP32でも動かもしれません。

# コード
```C++:RP2040_Wificaptiveportal.ino
#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);
}
```

## おわりに
調べながら動作確認ベースでコーディングしたので、記事内の説明はニュアンス等が間違ってる可能性があります。
インターネットに接続するネットワークとして期待するとあまり良い挙動ではないですが、クローズドネットワークのアクセスポイントであることを考えると、望ましくはないもののそれほど問題ではない動きかなと思います。