上回聊到程式開發的老鳥魔咒,提到我在寫 ESP32/ESP8266 程式時有個心魔無法克服 - 幾乎所有 Arduino 範例都把 WiFi 基地台的 SSID 跟密碼寫死在程式裡,讓我頻頻呼喊花惹發?把密碼用明碼寫進程式有安全疑慮並有改密碼要重新編譯的後遺症,嚴重違背了我的信仰。如果是 C#,我用左手花十分鐘就能寫出安全又好維護的做法,但這是他 X 的 C++ 啊! 我像沒了鎚子的雷神索爾,體驗如何當一個心很大想很多但能力超鳥的平凡人。

在 ESP 無線網路設定,其實已有一些程式庫想解決在程式寫死網路設定的問題,做法多半是在執行階段提供 Web 介面輸入 SSID 及密碼,這很接近我心中理想的做法。其運作原理是善用 ESP8266/ESP32 能在 AP 模式與 STA(Station) 模式間切換的能力,AP 模式是指由 ESP 模組擔任無線基地台,連上它可形成封閉的區域網路;STA 模式則是 ESP 用 SSID 及密碼連上其他基地台取得 IP,之後便可上網,也能跑伺服器對區域網路提供服務。(延伸閱讀:How to Set an ESP32 Access Point (AP) for Web Server)

因此,當 ESP 未設定 SSID 或 SSID/密碼有誤無法連上其他無線基地台時,自動切換成 AP 模式並啟動一個小網站,我們從手機或筆電的無線 AP 清單找到並連上它,就能登入設定 WiFi SSID 跟密碼的小網站,設好 SSID 及密碼,ESP 再切換 STA 模式,連上第三方 AP 取得 IP,連上作業環境的無線網路執行日常作業。

實踐這種想法的程式庫還不少:

有許多不但介面華麗功能還很強大,像是支援mDNS(不需註冊 DNS 伺服器,透過網路廣播方式找到 xxx.local 主機)、Capative Portal(連上 ESP 執行的 AP,不管輸入什麼網址都會導到 ESP 上的網站伺服器)、掃瞄並列出範圍內的無線基地台... 等等,對我來說這些功能太多了也太複雜。我反而想要一輕巧、簡單、安全,可中文化的版本,於是我犯了全天下程式開發人員都會犯的錯,對! 我造了自己想要的輪子。

我構想的運作流程如下:

  1. 開機後先重連上次使用的無線網路
  2. 若成功連上,就執行原本的流程,不需要執下面的步驟
  3. 若從未設定過或原本設的 SSID 與密碼失效,則進入 WiFi 設定介面模式
  4. WiFi 設定介面很簡單,一個 SSID、一個密碼欄位(一定要 type="password" 呀! 不少程式庫用明碼欄,位再次踩到我的紅線,也是讓我想自己寫旳原因之一)、一個儲存設定鈕,一個重新啟動鈕
  5. 輸入 SSID、密碼後按【儲存設定】,ESP 會嘗試連線該 AP 取得 IP 並顯示出來,如果 IP 是透過 DHCP 指派,稍後才知道該用什麼 IP 連上 ESP
  6. 確試 SSID 與密碼可用後,按下【重新啟動】讓 ESP 重啟,回到步驟 1,這次 ESP 會連上基地台取得 IP 進入正常流程

在開發過程中,我再次摔進「想套用 C# 設計技巧讓架構好擴充易維護,但對映到 C++ 不知如何下手」的深坑,大半天爬不出來,連續幾個週末花了超過十個小時查資料,修修改改,終於寫出一個自己能接受的版本。

我用了物件導向技巧,寫了一個 GuineapigWiFiConfig 類別,提供全域物件 - WiFiConfig。主程式引用 Guineapig.WiFiConfig.h,在 setup() 時呼叫 WiFiConfig.connectWiFi(),它會用上次設定的 SSID 跟密碼試連無線網路,若成功取得 IP 傳回 true,繼續正常流程;若無線網路設定無效,則進入 AP 模式,啟動一個 WiFi 設定網站讓使用使用者設定 SSID、密碼。程式範例如下:

#include <Arduino.h>
#include "Guineapig.WiFiConfig.h"

#pragma region Async Web Server ***************************
#include <ESPAsyncWebServer.h>
#ifdef ESP32
#include <WiFi.h>
#else
#include <ESP8266WiFi.h>
#endif
AsyncWebServer webServer(80);
#pragma endregion *****************************************

bool resetWifiFlag = false;

void setup()
{
  Serial.begin(115200);
  //嘗試連上無線網路,若成功傳回 true 繼續作業;若失敗則啟用 AP 模式讓使用者連上來設定網路
  if (WiFiConfig.connectWiFi())
  {
    pinMode(LED_BUILTIN, OUTPUT);
    //建立一個小網站讓使用者開關LED燈及清除網路設定(方便實驗觀察用,一般應用時不太需要)
    webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
      request->send(200, "text/html", R"===(
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    a { /** 略 **/ }
  </style>
</head>
<body>
  <a href='/led/on' target=hiddenFrame>LED ON</a>
  <a href='/led/off' target=hiddenFrame>LED OFF</a>
  <a href='/reset-wifi'>RESET WIFI</a>
  <iframe name=hiddenFrame style='display:none'></iframe>
</body>
</html>
)===");
    });
    //點燈
    webServer.on("/led/on", HTTP_GET, [](AsyncWebServerRequest *request) {
      digitalWrite(LED_BUILTIN, LOW);
      request->send(200, "text/plain", "OK");
    });
    //關燈
    webServer.on("/led/off", HTTP_GET, [](AsyncWebServerRequest *request) {
      digitalWrite(LED_BUILTIN, HIGH);
      request->send(200, "text/plain", "OK");
    });
    //重設網路
    webServer.on("/reset-wifi", HTTP_GET, [](AsyncWebServerRequest *request) {
      resetWifiFlag = true;
      request->send(200, "text/plain", "Reset and rebooting...");
    });
    webServer.begin();
  }
}

void loop()
{
  if (resetWifiFlag)
  {
    delay(3000);
    WiFiConfig.clearWiFiConfig();
  }
  delay(500);
}

第一次使用或上次設的 SSID 或密碼失效時,ESP 會進入 AP 模式,由 Serial Monitor 可以看到進入 AP 模式訊息、AP 名稱及 IP:

無網基地台清單也可以看到名為 ESP-xxxx 的基地台:

連上 ESP-xxxx AP,瀏覽器開啟 192.168.4.1 可看到 WiFi 設定頁面:

操作流程如下,設好 SSID、密碼,儲存設定後按重新啟動,ESP 會重開機,之後瀏覽器改切回正常無線網路就能連上 ESP 服務器

偵錯訊息預設只輸出到 Serial,而我上週為 SSD1306 OLED 加入的自動捲動文字顯示功能派上用場,WiFiConfig.logCallback 有個 logCallback 可以指定自訂偵錯誤訊輸出函式,我將偵錯誤訊息用 ttyPrintln() 輸出到 OLED:


void printOled(String msg)
{
  OLED.ttyPrint(msg);
  OLED.display();
}

void setup() 
{
    //...略...
    
    //將偵測訊息輸出導一份到 OLED
    WiFiConfig.logCallback = printOled;
}

薑! 薑! 薑! 講~~~ 比用 Serial Monitor 方便多了。

程式碼還在早期發展階段,可能會有大幅修改,我打算等成熟點再放上 Github。如果有人現在想當白老鼠或想參考程式寫法,我先放了一份在 Gist - Guineapig.WiFiConfig.hGuineapig.WiFiConfig.cpp

經過幾個星期的掙扎,終於擠出一些自己覺得還行的東西,這也讓重當菜鳥的我復習了程式開發基本技巧,像是別一口氣寫一大段程式期待它會動,一旦出問題會不知從何查起,甚至查錯方向白花時間。要用 Bottom-Up 方式,先確認基本環節沒問題,再往上疊加把系統搭起來。這些是我在教新同學時常耳提面命的基本技巧,但自己仗著對 C#/.NET/JavaScript 熟,則習慣一次寫完開測,反正有問題很快能抓出來;來到陌生環境,我卻把這種老司機專屬的水溝蓋跑法給帶過來,難怪要摔到鼻青臉腫。老鳥當久了,偶爾受受這種刺激也好,也算另一種「跨出舒適圈」吧!

Trying to implement my version of self WiFi setting web UI on ESP8266/ESP32 to prevent from storing SSID and password in code.


Comments

# by J.C.

您好,編譯的過程一直欠缺檔案與發生錯誤,可以分享完整的程式碼嗎? 感謝。

# by J.C.

ESP8266

# by Jeffrey

to J.C., OK,待我整理一下,近期再放上 Github。

# by Nelson

這個可以透過 SmartConfig跟WiFiMulti搞定,SmartConfig是神器!

# by Jeffrey

to Nelson, 感謝分享,又學到新知了。

# by 胖子

最近也是苦惱跟您相同的問題,只是我沒能力解決,剛好瀏覽到您的文章,請問大師,您放在GitHub的完整連結在哪裡?可否分享?我目前只找到您網頁分享的Guineapig.WiFiConfig.h、Guineapig.WiFiConfig.cpp 這兩個檔案

# by Jeffrey

to 胖子,過幾天我會整理一個較乾淨(無 OLED 顯示,純 Serial 輸出偵錯)的可執行範例,再上傳 github,屆時會再發文說明。

# by Jeffrey

to 胖子,範例程式來了 https://blog.darkthread.net/blog/esp-wifi-conf-demo/

# by shaman

esp_wifi_get_config() 的 return 值能拿來判別 ssid/password 已經儲存在 nvs 嗎?我發現 ESP_OK,而隨後它也確實連接上 ssid,但是在當下輸出 wifi_config_t 的 sta.ssid 部分,卻發現其內容是空字串。

# by Jeffrey

to shaman, 有人回報類似問題 https://github.com/espressif/arduino-esp32/issues/1743 疑似 IDF Bug,討論串有 Workaround,可參考看看。

# by J

想請問html有被R"===()==="包起來 請問這段是什麼意思 謝謝

# by Jeffrey

to J, 這是 C 的一種字串表示法,術語是 Raw String Literal https://www.geeksforgeeks.org/raw-string-literal-c/,兩個括號中間可以包含 "、換行等字元不用寫成 \",\n。

# by EvenChen

ESP8266(nodemcuv2,實際開發板是NodeMCU Lua WIFI V3)SSID跟PSK似乎都是空白無法儲存,一直跳192.168.4.1,ESP32(esp32doit-devkit-v1)則沒問題。 另外,AP模式SSID應該是你重新算出來的(沒有全大寫、沒補0),其實不用這麼麻煩,預設即可: WiFi.softAP(WiFi.softAPSSID().c_str()); 理論上給NULL應該也可以: WiFi.softAP(NULL); 但有些裝置會看得到SSID但無法正常連線,也許是WiFi晶片太舊?可以參考看看

# by Lulu

請問連上 ESP後要如何自動跳出 192.168.4.1的網頁?

# by Jeffrey

to Lulu, 指連上後不管輸入什麼都連到設定畫面嗎?這種做法的術語叫 Captive Portal,你可以用關鍵字查到相關程式庫

# by Lulu

Hi Jeffrey, 是的我指的是Captive Portal 有辦法加入至您寫的Guineapig.WiFiConfig程式庫嗎?

# by Jeffrey

to Lulu,引用另一篇文章提過的心路歷程:https://blog.darkthread.net/blog/esp-wifi-conf-demo/ 在決定自己動手寫之前,我試用過五種程式庫,其中不乏介面華麗功能還很強大的,像是支援mDNS(不需註冊 DNS 伺服器,透過網路廣播方式找到 xxx.local 主機)、Captive Portal(連上 ESP 執行的 AP,不管輸入什麼網址都會導到 ESP 上的網站伺服器)、掃瞄並列出範圍內的無線基地台... 等等,但對我來說功能太多也太複雜。我只想要一個輕巧、簡單、安全,可中文化的版本,夠用就好。 程式庫旨在追求簡單輕巧,結果又包進一堆複雜機制違反初衷,若 Captive Portal 是你的主要需求,引用對映的程式庫應是較有效率的做法。

# by Kyle

最近剛跑來玩Arduino 也是遇到這個有一些類似的問題。 因為之後燒好的ESP可能不會再與電腦連線,甚至都封裝了。 感覺是不是可以透過這個方式架設一個ESP的AP主機,之後封裝的ESP都預設為這個路徑。 這樣未來不管是更改SSID 還是 密碼應該都可以透過這個AP來作設定或是更新。

# by Makodo

Dear 版主: 是否可以當您的小白鼠, 參考您這個專案的所有檔案嗎?

# by Jeffrey

to Makodo, 我有包成程式庫了也有應用範例,可以參考這篇:https://blog.darkthread.net/blog/esp-wifi-conf-demo/

Post a comment