M5Stack Core2でWiFiに接続する（WiFi設定ウィザード作成）

M5Stack Core2 を用いて，WiFi にマイコンを接続しましょう。

:::note info
本記事では M5Stack Core2 を使用していますが，他のシリーズでも対応するライブラリがインストールしてあれば同じようにできるはずです（保証はできませんが...）。
また，本記事中では客観的に見ると好まれない表現があるかもしれません。
何かお気づきの際は，コメント等で教えていただけると幸いです。
:::

# 前提
以下を前提に，進んでいきます。
- 使用IDE：Arduino IDE
- Arduino IDE に，M5Stack Core2 のライブラリが追加されている（ない場合：[環境構築編](https://qiita.com/sssscccc210/items/bc7c76801c8d8121a2a9)を参照）
- 書いたプログラムをマイコン（M5Stack Core2）上で実行できる（できない場合：[環境構築編](https://qiita.com/sssscccc210/items/bc7c76801c8d8121a2a9)を参照）

# 接続フローについて
よくプログラムに直接 SSID, パスワードを記述して WiFi と接続する記事がありますが，本記事では，汎用性高く，ユーザーがスマホ等から接続設定を行えるようにしてみましょう（<ins>プログラム中に情報を埋め込まない</ins>でWiFiに接続します）。

以下のフローチャートの動作になるように，開発を行います。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/b7c4e413-9ef7-150f-51ea-e4f14049d766.png">

実は，これを実現するためのプログラムを，M5Stack の方で用意してくださっています。ですので，そちらをありがたく使わせていただきます。

# WiFi設定のExampleプログラムを実行してみる
:::note info
後ほど，Exampleプログラムの修正を行います。「修正なんかやってないで，早く動作させたい！」という方は，下に示す[最終コード](#最終コード)を利用してください。
:::

`File > Examples > Examples from Custom Libraries`の，`M5Core2 > Advanced > WiFi > WiFiSetting`を開きます。
<img width="550px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/676b51fc-6842-fc2e-e73d-788e4d604f0b.png">
ちなみに，このソースコードは，[GitHub（公式レポジトリ）](https://github.com/m5stack/M5Core2/tree/master/examples/Advanced/WIFI/WiFiSetting)からも確認できます。
[MITライセンス](https://github.com/m5stack/M5Core2/blob/master/LICENSE)で公開されています。ありがたき幸せ。

`WiFiSetting.ino`が開かれます。
<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/5d3304ce-1392-5f41-4274-9e3378896826.png">
何も変更を加えずに，これを実行してみましょう。

## 実行結果
黄色の小さいメッセージが表示されます。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/4ded4d8b-ff6a-8258-d1d4-dc4cfe93c857.png">

## アクセスポイントに接続する
この時，[最初に示したフローチャート](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/b7c4e413-9ef7-150f-51ea-e4f14049d766.png)を辿ると，「**セットアップモード**（AP）」に入っています。APはアクセスポイントのことで，Wi-Fiの電波を送受信する機器と説明されるようです（ちょっと違いますが，今はルーターとして機能していると思ってください）。
つまり，今，このマイコンはルーター（みたいなもの）です。ですから，この「ルーター」に接続してみましょう。
```
Starting Web Server at: 192.168.4.1
Starting Access Point at "M5STACK_SETUP"
```
とあるので，SSIDが`M5STACK_SETUP`となっているWiFiに接続します（パスワードフリーです）。
<img width="200px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/15e56175-2619-b364-ce17-d5d36d15fc58.png">

## WiFi接続設定を行う
接続後，ブラウザから「192.168.4.1」にアクセスしましょう。
ものすごくシンプルなページが表示されます。

`Wi-Fi Settings`をクリックしましょう。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/b8d050da-61db-630c-3642-0d39f2735f6c.png">

マイコンが後ほど接続する（本物の）ルーターの認証情報を入力します。SSIDを選択し，パスワードを入力後，`送信`をクリックしましょう。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/af3462e9-cae1-4a02-d8c9-a2f99f12e487.png">
送信後，「Setup complete.」というページが表示されます。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/5196e4dd-5f11-9139-0d99-7911f295b070.png">

入力情報を本体のストレージ（NVS）に保存し，M5Stack が再起動します（APモードが終了するので，`M5STACK_SETUP`からの接続も切れます）。

## 再起動後...
前項での操作が成功していれば，
```
WIFI-SSID: [謎の文字]
WIFI-PASSWD: [謎の文字]
Waiting for Wi-Fi connection...
Connected!
Starting Web Server at 192.168.11.24
```
のようなメッセージが表示されます。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/938232e3-b5fa-ac13-07ad-c385806a6dbf.png">

:::note warn
本操作に失敗している場合，`Starting Access Point at "M5STACK_SETUP"`が再度表示されます。通常はWiFiのパスワード打ち間違えです。
[「アクセスポイントに接続する」](#アクセスポイントに接続する)からやり直してください。
:::

:::note info
[謎の文字]なんか表示されず，正常に表示されるよ！？という方もいるでしょう。これについては，[[謎の文字]を修正](#謎の文字を修正)項を参照してください。
:::


今度は，[最初に示したフローチャート](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/b7c4e413-9ef7-150f-51ea-e4f14049d766.png)を辿ると，「**稼働モード**（STA）」に入っています。STAは，ステーションモードのことで，端末（無線LAN子機）としてWi-Fiアクセスポイント（AP）に接続するモードと説明されるようです（先程はルーター扱いでしたが，今度はスマホやPCのような，ルーターに接続する側となります）。
（私の場合）`192.168.11.24`にアクセスできるようなので，先ほどと同様，このipアドレスにアクセスしてみます。

::: note info
マイコンが接続している WiFi と操作端末が接続している WiFi が一致していることをご確認ください。
:::

うまく表示されていますね。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/eec2dd97-c9f0-32a2-3027-c5459472e5fb.png">

`Reset Wi-Fi Settings`をクリックすると，保存されているWiFi設定を消去することができます。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/910139ab-3473-eb6f-00d4-ecfcd384e19f.png">

消去が完了すると，再起動します（再度使うには，[アクセスポイントに接続する](#アクセスポイントに接続する)からやり直しとなります）。

# プログラムを修正する

## Webページを日本語対応する
現状，ipアドレスにアクセスして表示されるWebページですが，日本語が文字化けします。Webページのエンコードを`UTF-8`に設定してあげましょう。
`makePage`関数を修正します。
```diff_cpp:WiFiSetting.ino
String makePage(String title, String contents) {
-   String s = "<!DOCTYPE html><html><head>";
+   String s = "<!DOCTYPE html><html lang='ja'><head>";
    s += "<meta name=\"viewport\" "
         "content=\"width=device-width,user-scalable=0\">"
+        "<meta charset='UTF-8'>";
    s += "<title>";
    s += title;
-   s += "</title></head><body>";
+   s += "</title></head><body style='font-family: SF Pro JP, SF Pro Text, SF Pro Icons, Hiragino Kaku Gothic Pro, ヒラギノ角ゴ Pro W3,"
+        "メイリオ, Meiryo, ＭＳ Ｐゴシック, Helvetica Neue, Helvetica, Arial, sans-serif'>";
    s += contents;
    s += "</body></html>";
    return s;
}
```
<details><summary>コピー用はこちら</summary>

```cpp:WiFiSetting.ino
String makePage(String title, String contents) {
    String s = "<!DOCTYPE html><html lang='ja'><head>";
    s += "<meta name='viewport' content='width=device-width,user-scalable=0'>"
         "<meta charset='UTF-8'><title>";
    s += title;
    s += "</title></head><body style='font-family: SF Pro JP, SF Pro Text, SF Pro Icons, Hiragino Kaku Gothic Pro, ヒラギノ角ゴ Pro W3,"
         "メイリオ, Meiryo, ＭＳ Ｐゴシック, Helvetica Neue, Helvetica, Arial, sans-serif'>";
    s += contents;
    s += "</body></html>";
    return s;
}

```

</details>


## ページを日本語にする
もう少し詳しく，かつ日本語のページを表示させましょう。ここは完全に個人の好みで修正しています。必要なら参照ください。
`startWebServer`関数を修正します。
```diff_cpp:WiFiSetting.ino
void startWebServer() {  // Open the web service.  打开Web服务
    if (settingMode) {  // If the setting mode is on.  如果设置模式处于开启状态
        M5.Lcd.print("Starting Web Server at: ");
        M5.Lcd.print(
            WiFi.softAPIP());  // Output AP address (you can change the address
                               // you want through apIP at the beginning).
                               // 输出AP地址(可通过开头的apIP更改自己想要的地址)
        webServer.on(
            "/settings", []() {  // AP web interface settings.  AP网页界面设置
                String s =
-                   "<h1>Wi-Fi Settings</h1><p>Please enter your password by "
-                   "selecting the SSID.</p>";
+                   "<h1>Wi-Fi 設定</h1><p>以下からSSIDを選び，そのパスワードを入力して「送信」ボタンを押してください。</p>";
                s += "<form method=\"get\" action=\"setap\"><label>SSID: "
                     "</label><select name=\"ssid\">";
                s += ssidList;
-               s += "</select><br>Password: <input name=\"pass\" length=64 "
+               s += "</select><br>パスワード: <input name=\"pass\" length=64 "
                     "type=\"password\"><input type=\"submit\"></form>";
                webServer.send(200, "text/html", makePage("Wi-Fi Settings", s));
            });
        webServer.on("/setap", []() {
            String ssid = urlDecode(webServer.arg("ssid"));
            M5.Lcd.printf("SSID: %s\n", ssid);
            String pass = urlDecode(webServer.arg("pass"));
            M5.Lcd.printf("Password: %s\n\nWriting SSID to EEPROM...\n", pass);

            // Store wifi config.  存储wifi配置信息
            M5.Lcd.println("Writing Password to nvr...");
            preferences.putString("WIFI_SSID", ssid);
            preferences.putString("WIFI_PASSWD", pass);

            M5.Lcd.println("Write nvr done!");
            String s =
-               "<h1>Setup complete.</h1><p>device will be connected to \"";
+               "<h1>WiFi設定を保存しました</h1><p>2秒後に再起動し，\"";
            s += ssid;
-           s += "\" after the restart.";
+           s += "\"に自動で接続されます。<br>操作端末との接続も切れます。</p>";
            webServer.send(200, "text/html", makePage("Wi-Fi Settings", s));
            delay(2000);
            ESP.restart();  // Restart MPU.  重启MPU
        });
        webServer.onNotFound([]() {
            String s =
-               "<h1>AP mode</h1><p><a href=\"/settings\">Wi-Fi "
-               "Settings</a></p>";
+               "<h1>WiFi設定ウィザード</h1><p>ようこそ。以下からWiFi接続設定を開始してください。</p>"
+               "<p><a href='/settings'>Wi-Fi設定を始める</a></p>";
            webServer.send(200, "text/html", makePage("AP mode", s));
        });
    } else {  // If the setting mode is off.  如果设置模式处于关闭状态
        M5.Lcd.print("Starting Web Server at ");
        M5.Lcd.println(WiFi.localIP());
        webServer.on("/", []() {  // AP web interface settings.  AP网页界面设置
            String s =
-               "<h1>STA mode</h1><p><a href=\"/reset\">Reset Wi-Fi "
-               "Settings</a></p>";
+               "<h1>稼働モード</h1><p><a href=\"/reset\">Wi-Fi設定を削除する</a></p>";
            webServer.send(200, "text/html", makePage("STA mode", s));
        });
        webServer.on("/reset", []() {
            // reset the wifi config
            preferences.remove("WIFI_SSID");
            preferences.remove("WIFI_PASSWD");
            String s =
-               "<h1>Wi-Fi settings was reset.</h1><p>Please reset device.</p>";
+               "<h1>Wi-Fi設定を削除しました</h1><p>2秒後にデバイスが再起動します。</p>";
            webServer.send(200, "text/html",
                           makePage("Reset Wi-Fi Settings", s));
            delay(2000);
            ESP.restart();
        });
    }
    webServer.begin();  // Start web service.  开启web服务
}
```

<details><summary>コピー用はこちら</summary>

```cpp:WiFiSetting.ino
void startWebServer() {  // Open the web service.  打开Web服务
    if (settingMode) {  // If the setting mode is on.  如果设置模式处于开启状态
        M5.Lcd.print("Starting Web Server at: ");
        M5.Lcd.print(
            WiFi.softAPIP());  // Output AP address (you can change the address
                               // you want through apIP at the beginning).
                               // 输出AP地址(可通过开头的apIP更改自己想要的地址)
        webServer.on(
            "/settings", []() {  // AP web interface settings.  AP网页界面设置
                String s =
                    "<h1>Wi-Fi 設定</h1><p>以下からSSIDを選び，そのパスワードを入力して「送信」ボタンを押してください。</p>";
                s += "<form method=\"get\" action=\"setap\"><label>SSID: "
                     "</label><select name=\"ssid\">";
                s += ssidList;
                s += "</select><br>パスワード: <input name=\"pass\" length=64 "
                     "type=\"password\"><input type=\"submit\"></form>";
                webServer.send(200, "text/html", makePage("Wi-Fi Settings", s));
            });
        webServer.on("/setap", []() {
            String ssid = urlDecode(webServer.arg("ssid"));
            M5.Lcd.printf("SSID: %s\n", ssid);
            String pass = urlDecode(webServer.arg("pass"));
            M5.Lcd.printf("Password: %s\n\nWriting SSID to EEPROM...\n", pass);

            // Store wifi config.  存储wifi配置信息
            M5.Lcd.println("Writing Password to nvr...");
            preferences.putString("WIFI_SSID", ssid);
            preferences.putString("WIFI_PASSWD", pass);

            M5.Lcd.println("Write nvr done!");
            String s =
                "<h1>WiFi設定を保存しました</h1><p>2秒後に再起動し，\"";
            s += ssid;
            s += "\"に自動で接続されます。<br>操作端末との接続も切れます。</p>";
            webServer.send(200, "text/html", makePage("Wi-Fi Settings", s));
            delay(2000);
            ESP.restart();  // Restart MPU.  重启MPU
        });
        webServer.onNotFound([]() {
            String s =
                "<h1>WiFi設定ウィザード</h1><p>ようこそ。以下からWiFi接続設定を開始してください。</p>"
                "<p><a href='/settings'>Wi-Fi設定を始める</a></p>";
            webServer.send(200, "text/html", makePage("AP mode", s));
        });
    } else {  // If the setting mode is off.  如果设置模式处于关闭状态
        M5.Lcd.print("Starting Web Server at ");
        M5.Lcd.println(WiFi.localIP());
        webServer.on("/", []() {  // AP web interface settings.  AP网页界面设置
            String s =
                "<h1>稼働モード</h1><p><a href=\"/reset\">Wi-Fi設定を削除する</a></p>";
            webServer.send(200, "text/html", makePage("STA mode", s));
        });
        webServer.on("/reset", []() {
            // reset the wifi config
            preferences.remove("WIFI_SSID");
            preferences.remove("WIFI_PASSWD");
            String s =
                "<h1>Wi-Fi設定を削除しました</h1><p>2秒後にデバイスが再起動します。</p>";
            webServer.send(200, "text/html",
                           makePage("Reset Wi-Fi Settings", s));
            delay(2000);
            ESP.restart();
        });
    }
    webServer.begin();  // Start web service.  开启web服务
}
```

</details>

この修正で，日本語のページも正常に表示されるようになりました。
<img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/f3df7893-ffb7-3555-2468-f1dcfafcd8ce.png">


## [謎の文字]を修正
[ここ](#再起動後)のところで，`WIFI-SSID`や`WIFI-PASSWD`のとこが正しく表示されず，変な表示となっています。これ，検証をしてみると，それぞれ**13字**までは正常に表示され，**14字以降**はこのように[謎の文字]が表示されてしまうようです（原因は全くわからん！！）。
SSID・PASSWDともに`1234567890abc`
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/d1148711-9599-c149-ddad-be3f6acf5a17.png">

SSIDは`1234567890abc`，PASSWDは`1234567890abcd`
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/f59701b4-cb3d-a74f-bf4e-dd6b682c63c9.png">

SSID・PASSWDともに`1234567890abcd`
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/e3c3981e-b150-e157-a442-4daf8e64d63f.png">

これの原因は，プログラムが足りていない箇所があります。
まず，`startWebServer`関数です。
```diff_cpp:WiFiSetting.ino
void startWebServer() {  // Open the web service.  打开Web服务
    if (settingMode) {  // If the setting mode is on.  如果设置模式处于开启状态
        M5.Lcd.print("Starting Web Server at: ");
        M5.Lcd.print(
            WiFi.softAPIP());  // Output AP address (you can change the address
                               // you want through apIP at the beginning).
                               // 输出AP地址(可通过开头的apIP更改自己想要的地址)
        webServer.on(
            "/settings", []() {  // AP web interface settings.  AP网页界面设置
                String s =
                    "<h1>Wi-Fi 設定</h1><p>以下からSSIDを選び，そのパスワードを入力して「送信」ボタンを押してください。</p>";
                s += "<form method=\"get\" action=\"setap\"><label>SSID: "
                     "</label><select name=\"ssid\">";
                s += ssidList;
                s += "</select><br>パスワード: <input name=\"pass\" length=64 "
                     "type=\"password\"><input type=\"submit\"></form>";
                webServer.send(200, "text/html", makePage("Wi-Fi Settings", s));
            });
        webServer.on("/setap", []() {
            String ssid = urlDecode(webServer.arg("ssid"));
-           M5.Lcd.printf("SSID: %s\n", ssid);
+           M5.Lcd.printf("SSID: %s\n", ssid.c_str());
            String pass = urlDecode(webServer.arg("pass"));
-           M5.Lcd.printf("Password: %s\n\nWriting SSID to EEPROM...\n", pass);
+           M5.Lcd.printf("Password: %s\n\nWriting SSID to EEPROM...\n", pass.c_str());

            // Store wifi config.  存储wifi配置信息
            M5.Lcd.println("Writing Password to nvr...");
            preferences.putString("WIFI_SSID", ssid);
            preferences.putString("WIFI_PASSWD", pass);

            M5.Lcd.println("Write nvr done!");
            String s =
                "<h1>WiFi設定を保存しました</h1><p>2秒後に再起動し，\"";
            s += ssid;
            s += "\"に自動で接続されます。<br>操作端末との接続も切れます。</p>";
            webServer.send(200, "text/html", makePage("Wi-Fi Settings", s));
            delay(2000);
            ESP.restart();  // Restart MPU.  重启MPU
        });
        webServer.onNotFound([]() {
            String s =
                "<h1>WiFi設定ウィザード</h1><p>ようこそ。以下からWiFi接続設定を開始してください。</p>"
                "<p><a href='/settings'>Wi-Fi設定を始める</a></p>";
            webServer.send(200, "text/html", makePage("AP mode", s));
        });
    } else {  // If the setting mode is off.  如果设置模式处于关闭状态
        M5.Lcd.print("Starting Web Server at ");
        M5.Lcd.println(WiFi.localIP());
        webServer.on("/", []() {  // AP web interface settings.  AP网页界面设置
            String s =
                "<h1>稼働モード</h1><p><a href=\"/reset\">Wi-Fi設定を削除する</a></p>";
            webServer.send(200, "text/html", makePage("STA mode", s));
        });
        webServer.on("/reset", []() {
            // reset the wifi config
            preferences.remove("WIFI_SSID");
            preferences.remove("WIFI_PASSWD");
            String s =
                "<h1>Wi-Fi設定を削除しました</h1><p>2秒後にデバイスが再起動します。</p>";
            webServer.send(200, "text/html",
                           makePage("Reset Wi-Fi Settings", s));
            delay(2000);
            ESP.restart();
        });
    }
    webServer.begin();  // Start web service.  开启web服务
}
```

続いて，`restoreConfig`関数です。
```diff_cpp:WiFiSetting.ino
boolean restoreConfig() { /* Check whether there is wifi configuration information
                     storage, if there is 1 return, if no return 0.
                          检测是否有wifi配置信息存储,若有返回1,无返回0 */
    wifi_ssid     = preferences.getString("WIFI_SSID");
    wifi_password = preferences.getString("WIFI_PASSWD");
    M5.Lcd.printf(
        "WIFI-SSID: %s\n",
-       wifi_ssid);  // Screen print format string.  屏幕打印格式化字符串
+       wifi_ssid.c_str());  // Screen print format string.  屏幕打印格式化字符串
-   M5.Lcd.printf("WIFI-PASSWD: %s\n", wifi_password);
+   M5.Lcd.printf("WIFI-PASSWD: %s\n", wifi_password.c_str());
    WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str());

    if (wifi_ssid.length() > 0) {
        return true;
    } else {
        return false;
    }
}
```

要するに，`c_str()`で string を従来の，C言語文字配列へのポインタで`printf`に渡す必要があったようです。（もう一回言いますが，何故13字までは上手く行ったのか，よくわかりません。有識者の方，教えてください...）

今度は上手く`1234567890abcd`が表示されました。
<img width="250px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/7ed2e5bf-ca81-37ea-fabe-f8cd333d1c33.png">



# 最終コード
オリジナルはプロトタイプ宣言を含んでいましたが，ここではプロトタイプ宣言を消す代わりに関数の位置を上下ひっくり返しました。
```cpp:WiFiSetting.ino
/*
*******************************************************************************
* Copyright (c) 2021 by M5Stack
*                  Equipped with M5Core2 sample source code
*                          配套  M5Core2 示例源代码
* Visit for more information: https://docs.m5stack.com/en/core/core2
* 获取更多资料请访问: https://docs.m5stack.com/zh_CN/core/core2
*
* Describe: WiFi connect.  wifi连接
* Date: 2021/7/27
*******************************************************************************
*/
#include <M5Core2.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiClient.h>
#include "WebServer.h"
#include <Preferences.h>

const IPAddress apIP(
    192, 168, 4, 1);  // Define the address of the wireless AP. 定义无线AP的地址
const char* apSSID = "M5STACK_SETUP";  // Define the name of the created
                                       // hotspot.  定义创建热点的名称
boolean settingMode;
String ssidList;
String wifi_ssid;  // Store the name of the wireless network. 存储无线网络的名称
String wifi_password;  // Store the password of the wireless network.
                       // 存储无线网络的密码

// DNSServer dnsServer;.  webServer的类
WebServer webServer(80);

// wifi config store.  wifi配置存储的类
Preferences preferences;


String makePage(String title, String contents) {
    String s = "<!DOCTYPE html><html lang='ja'><head>";
    s += "<meta name='viewport' content='width=device-width,user-scalable=0'>"
         "<meta charset='UTF-8'><title>";
    s += title;
    s += "</title></head><body style='font-family: SF Pro JP, SF Pro Text, SF Pro Icons, Hiragino Kaku Gothic Pro, ヒラギノ角ゴ Pro W3,"
         "メイリオ, Meiryo, ＭＳ Ｐゴシック, Helvetica Neue, Helvetica, Arial, sans-serif'>";
    s += contents;
    s += "</body></html>";
    return s;
}

String urlDecode(String input) {
    String s = input;
    s.replace("%20", " ");
    s.replace("+", " ");
    s.replace("%21", "!");
    s.replace("%22", "\"");
    s.replace("%23", "#");
    s.replace("%24", "$");
    s.replace("%25", "%");
    s.replace("%26", "&");
    s.replace("%27", "\'");
    s.replace("%28", "(");
    s.replace("%29", ")");
    s.replace("%30", "*");
    s.replace("%31", "+");
    s.replace("%2C", ",");
    s.replace("%2E", ".");
    s.replace("%2F", "/");
    s.replace("%2C", ",");
    s.replace("%3A", ":");
    s.replace("%3A", ";");
    s.replace("%3C", "<");
    s.replace("%3D", "=");
    s.replace("%3E", ">");
    s.replace("%3F", "?");
    s.replace("%40", "@");
    s.replace("%5B", "[");
    s.replace("%5C", "\\");
    s.replace("%5D", "]");
    s.replace("%5E", "^");
    s.replace("%5F", "-");
    s.replace("%60", "`");
    return s;
}

void startWebServer() {  // Open the web service.  打开Web服务
    if (settingMode) {  // If the setting mode is on.  如果设置模式处于开启状态
        M5.Lcd.print("Starting Web Server at: ");
        M5.Lcd.print(
            WiFi.softAPIP());  // Output AP address (you can change the address
                               // you want through apIP at the beginning).
                               // 输出AP地址(可通过开头的apIP更改自己想要的地址)
        webServer.on(
            "/settings", []() {  // AP web interface settings.  AP网页界面设置
                String s =
                    "<h1>Wi-Fi 設定</h1><p>以下からSSIDを選び，そのパスワードを入力して「送信」ボタンを押してください。</p>";
                s += "<form method=\"get\" action=\"setap\"><label>SSID: "
                     "</label><select name=\"ssid\">";
                s += ssidList;
                s += "</select><br>パスワード: <input name=\"pass\" length=64 "
                     "type=\"password\"><input type=\"submit\"></form>";
                webServer.send(200, "text/html", makePage("Wi-Fi Settings", s));
            });
        webServer.on("/setap", []() {
            String ssid = urlDecode(webServer.arg("ssid"));
            M5.Lcd.printf("SSID: %s\n", ssid.c_str());
            String pass = urlDecode(webServer.arg("pass"));
            M5.Lcd.printf("Password: %s\n\nWriting SSID to EEPROM...\n", pass.c_str());

            // Store wifi config.  存储wifi配置信息
            M5.Lcd.println("Writing Password to nvr...");
            preferences.putString("WIFI_SSID", ssid);
            preferences.putString("WIFI_PASSWD", pass);

            M5.Lcd.println("Write nvr done!");
            String s =
                "<h1>WiFi設定を保存しました</h1><p>2秒後に再起動し，\"";
            s += ssid;
            s += "\"に自動で接続されます。<br>操作端末との接続も切れます。</p>";
            webServer.send(200, "text/html", makePage("Wi-Fi Settings", s));
            delay(2000);
            ESP.restart();  // Restart MPU.  重启MPU
        });
        webServer.onNotFound([]() {
            String s =
                "<h1>WiFi設定ウィザード</h1><p>ようこそ。以下からWiFi接続設定を開始してください。</p>"
                "<p><a href='/settings'>Wi-Fi設定を始める</a></p>";
            webServer.send(200, "text/html", makePage("AP mode", s));
        });
    } else {  // If the setting mode is off.  如果设置模式处于关闭状态
        M5.Lcd.print("Starting Web Server at ");
        M5.Lcd.println(WiFi.localIP());
        webServer.on("/", []() {  // AP web interface settings.  AP网页界面设置
            String s =
                "<h1>稼働モード</h1><p><a href=\"/reset\">Wi-Fi設定を削除する</a></p>";
            webServer.send(200, "text/html", makePage("STA mode", s));
        });
        webServer.on("/reset", []() {
            // reset the wifi config
            preferences.remove("WIFI_SSID");
            preferences.remove("WIFI_PASSWD");
            String s =
                "<h1>Wi-Fi設定を削除しました</h1><p>2秒後にデバイスが再起動します。</p>";
            webServer.send(200, "text/html",
                           makePage("Reset Wi-Fi Settings", s));
            delay(2000);
            ESP.restart();
        });
    }
    webServer.begin();  // Start web service.  开启web服务
}

boolean restoreConfig() { /* Check whether there is wifi configuration information
                     storage, if there is 1 return, if no return 0.
                          检测是否有wifi配置信息存储,若有返回1,无返回0 */
    wifi_ssid     = preferences.getString("WIFI_SSID");
    wifi_password = preferences.getString("WIFI_PASSWD");
    M5.Lcd.printf(
        "WIFI-SSID: %s\n",
        wifi_ssid.c_str());  // Screen print format string.  屏幕打印格式化字符串
    M5.Lcd.printf("WIFI-PASSWD: %s\n", wifi_password.c_str());
    WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str());

    if (wifi_ssid.length() > 0) {
        return true;
    } else {
        return false;
    }
}

boolean checkConnection() {  // Check wifi connection.  检测wifi连接情况
    int count = 0;           // count.  计数
    M5.Lcd.print("Waiting for Wi-Fi connection");
    while (
        count <
        30) { /* If you fail to connect to wifi within 30*350ms
                 (10.5s), return false; otherwise return true.
                            若在30*500ms(15s)内未能连上wifi,返回false;否则返回true
               */
        if (WiFi.status() == WL_CONNECTED) {
            M5.Lcd.printf("\nConnected!\n");
            return (true);
        }
        delay(350);
        M5.Lcd.print(".");
        count++;
    }
    M5.Lcd.println("Timed out.");
    return false;
}

void setupMode() {
    WiFi.mode(WIFI_MODE_STA);  // Set Wi-Fi mode to WIFI_MODE_STA.
                               // 设置Wi-Fi模式为WIFI_MODE_STA
    WiFi.disconnect();         // Disconnect wifi connection.  断开wifi连接
    delay(100);
    int n = WiFi.scanNetworks();  // Store the number of wifi scanned into n.
                                  // 将扫描到的wifi个数存储到n中
    delay(100);
    M5.Lcd.println("");
    for (int i = 0; i < n; ++i) {  // Save each wifi name scanned to ssidList.
                                   // 将扫描到的每个wifi名称保存到ssidList中
        ssidList += "<option value=\"";
        ssidList += WiFi.SSID(i);
        ssidList += "\">";
        ssidList += WiFi.SSID(i);
        ssidList += "</option>";
    }
    delay(100);
    WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
    WiFi.softAP(apSSID);  // Turn on Ap mode.  开启Ap模式
    WiFi.mode(WIFI_MODE_AP);  // Set WiFi to soft-AP mode. 设置WiFi为soft-AP模式
    startWebServer();         // Open the web service.  打开Web服务
    M5.Lcd.printf("\nStarting Access Point at \"%s\"\n\n", apSSID);
}

void setup() {
    M5.begin();  // Init M5Core2.  初始化M5Core2
    M5.Lcd.setTextColor(
        YELLOW);  // Set the font color to yellow.  设置字体颜色为黄色
    preferences.begin("wifi-config");
    delay(10);
    if (restoreConfig()) {  // Check if wifi configuration information has been
                            // stored.  检测是否已存储wifi配置信息
        if (checkConnection()) {  // Check wifi connection.  检测wifi连接情况
            settingMode = false;  // Turn off setting mode.  关闭设置模式
            startWebServer();  // Turn on network service.  开启网络服务
            return;            // Exit setup().  退出setup()
        }
    }
    settingMode =
        true;  // If there is no stored wifi configuration information, turn on
               // the setting mode.  若没有已存储的wifi配置信息,则开启设置模式
    setupMode();
}

void loop() {
    if (settingMode) {
    }
    webServer
        .handleClient();  // Check that there is no facility to send requests to
                          // the M5StickC Web server over the network.
                          // 检查有没有设备通过网络向M5Core2网络服务器发送请求
}
```

## 関数を結びつけたフローチャート
上のプログラムはM5Stackの方で丁寧に英語でのコメントを残してくださっているので，プログラムを順に追っていけばどこで何をやっているのかわかると思います。
参考までに，最初に提示したフローチャートとプログラム内のどこの関数が結びついているか，を明記したフローチャートを下に載せておきます。
<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981603/366d5180-08ff-4273-e6e0-a05418bb3c75.png">

## 一応解説
基本的にはプログラム上のコメント，上のフローチャートを見てください。ここでは，いろいろ初めての方用に，特に触れてこなかった部分の解説を残しておきます。

### makePage関数
この関数が何をしているか，htmlでページを作ったことがある方はわかると思います。
```cpp
String s = "<!DOCTYPE html><html lang='ja'><head>";
s += "<meta name='viewport' content='width=device-width,user-scalable=0'>"
    "<meta charset='UTF-8'><title>";
s += title;
s += "</title></head><body style='font-family: SF Pro JP, SF Pro Text, SF Pro Icons, Hiragino Kaku Gothic Pro, ヒラギノ角ゴ Pro W3,"
     "メイリオ, Meiryo, ＭＳ Ｐゴシック, Helvetica Neue, Helvetica, Arial, sans-serif'>";
s += contents;
s += "</body></html>";
return s;
```
これ，こういう書き方になるとわかりづらいですね。見慣れているhtmlのコードに直してみましょう。
```html
<!DOCTYPE html>
<html lang='ja'>
    <head>
        <meta name='viewport' content='width=device-width,user-scalable=0'>
        <meta charset='UTF-8'>
        <title>【title】</title>
    </head>
    <body style='font-family: SF Pro JP, SF Pro Text, SF Pro Icons, Hiragino Kaku Gothic Pro, ヒラギノ角ゴ Pro W3, メイリオ, Meiryo, ＭＳ Ｐゴシック, Helvetica Neue, Helvetica, Arial, sans-serif'>
        【contents】
    </body>
</html>
```
何も難しいことやっていませんでしたね。単純に，上のコードの改行がないものを生成するだけの関数でした。

### startWebServer関数
<!--`M5.Lcd.print(WiFi.softAPIP());`はAPモードにおけるIPアドレスを出力します。
`M5.Lcd.println(WiFi.localIP());`はSTAモードにおけるIPアドレスを出力します。-->
`webServer.on("/xx", [](){FUNC});`は`IPアドレス/xx`にアクセスされた時，`FUNC`部を実行します。
`webServer.send`はクライアント（操作端末）にレスポンスを送信します。本関数内では，`200`（リクエスト成功）というレスポンスとともに，`text/html`形式のデータを送り付けます。要するに，新たなhtmlコードを送りつけてクライアント側に新たなページを表示させるわけです。

### Preferencesとは
Preferences は，NVS（不揮発性ストレージ）領域にデータを保存・読み取りができるものです。
`Preferences preferences`を定義のもと...
1. `preferences.begin("wifi-config");`でデータセットに移動
1. `preferences.getString("WIFI_SSID");`でStringデータを取得（`wifi-config/WIFI_SSID`内の）
1. `preferences.putString("WIFI_SSID", ssid);`でStringデータを保存（`wifi-config/WIFI_SSID`内に）
1. `preferences.remove("WIFI_SSID");`でデータを消去（`wifi-config/WIFI_SSID`内の）

詳しい情報に関しては，
https://docs.espressif.com/projects/arduino-esp32/en/latest/tutorials/preferences.html 
にあります。

### WiFiについて
`WiFi.`の部分に関しては，公式ドキュメント
https://docs.m5stack.com/ja/arduino/m5core/wifi 
にサンプルとともに概要が示されているので，こちらを参照ください。

# 結果
- SSIDやパスワードが14字以上でも，正常にLCDに表示された
- IPアドレスにアクセスして表示されるページが日本語に対応した
- スマホ等から設定したWiFiに接続できた

本記事は以上です。お疲れ様でした。

---
**参考文献**
- [アクセスポイントとは？ルーターとの違いやおすすめのアクセスポイントも紹介](https://www.commufa.jp/article/0137)
- [ステーションモード](https://e-words.jp/w/%E3%82%B9%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%A2%E3%83%BC%E3%83%89.html)

<!--https://github.com/espressif/arduino-esp32/blob/master/libraries/HTTPClient/src/HTTPClient.h-->