2022-03-24

使用 ESP8266 NodeMCU 連接 WiFi 及建立 HTTP伺服器 遙距控制電子裝置

上次使用藍牙無線控制電子裝置,但藍牙仍然只能作有限距離的無線控制
如果可以使用網絡,接通網絡後,即使用在地球另一邊都能夠遙距控制

連接 WiFi

見下文
ESP8266 NodeMCU 本身已經內置 WiFi 模組,因此具備 WiFi 功能
而且安裝 ESP8266 NodeMCU 開發模組時,亦包含很多 ESP8266函式庫

編寫 WiFi 連接程式
#include <ESP8266WebServer.h>

void setup() {
	Serial.begin(9600);
	Serial.println("Connecting WiFi...");
	WiFi.begin("SSID", "password"); // replace a valid SSID and password
	while (WiFi.status() != WL_CONNECTED) {
		Serial.print(".");
		delay(1000);
	}
	Serial.println("WiFi connected");
	Serial.print("IP: ");
	Serial.println(WiFi.localIP());
}

void loop() {
	delay(1);
}

見下文
ESP8266 NodeMCU 連接 WiFi ,會獲派 IP地址

建立 HTTP伺服器

連接 WiFi 的測試成功後,可以編寫 HTTP伺服器 程式
#include <ESP8266WebServer.h>

const byte LED_WIFI = 2;
const byte LED_BOARD = 16;
ESP8266WebServer server = ESP8266WebServer(80);

const char* html PROGMEM = R"EOF(<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <title>ESP8266 LED Control</title>
    </head>
    <body>
        <div><a href="/led-wifi">LED WiFi</a></div>
        <div><a href="/led-board">LED Board</a></div>
    </body>
</html>)EOF";

void setup() {
	Serial.begin(9600);
	pinMode(LED_WIFI, OUTPUT);
	digitalWrite(LED_WIFI, HIGH);
	pinMode(LED_BOARD, OUTPUT);
	digitalWrite(LED_BOARD, HIGH);
	Serial.println("Connecting WiFi...");
	WiFi.begin("SSID", "password"); // replace a valid SSID and password
	while (WiFi.status() != WL_CONNECTED) {
		Serial.print(".");
		delay(1000);
	}
	Serial.println("WiFi connected");
	Serial.print("Visit: http://");
	Serial.println(WiFi.localIP());
	server.on("/", [] {
		server.send(200, "text/html", html);
	});
	server.on("/led-wifi", [] {
		String value = server.arg("value");
		if (value == "0") {
			digitalWrite(LED_WIFI, HIGH);
		} else if (value == "1") {
			digitalWrite(LED_WIFI, LOW);
		} else {
			digitalWrite(LED_WIFI, !digitalRead(LED_WIFI));
		}
		server.sendHeader("Location", "/");
		server.send(302);
	});
	server.on("/led-board", [] {
		String value = server.arg("value");
		if (value == "0") {
			digitalWrite(LED_BOARD, HIGH);
		} else if (value == "1") {
			digitalWrite(LED_BOARD, LOW);
		} else {
			digitalWrite(LED_BOARD, !digitalRead(LED_BOARD));
		}
		server.sendHeader("Location", "/");
		server.send(302);
	});
	server.begin();
}

void loop() {
	server.handleClient();
}


見下文
連接 WiFi 後,便會建立 HTTP伺服器 ,可以在網頁瀏覽器載入該 IP地址

網頁內容

見下文
見下文
在下只是使用很基本的 HTML元素 設計
按下連結,可以控制對應電子裝置

見下文
既然可以使用網頁控制,即是可以使用 Curl 等指令,發送 HTTP Request 控制
可以使用指令,亦表示可以自行製作自動化操作

使用 WiFiManager 連接 WiFi

將 SSID 及 密碼 編寫在程式,顯然有安全隱患
可以使用 WiFiManager函式庫 讓 ESP8266 NodeMCU 成為 存取點(Access Point)
先加入到 ESP8266 NodeMCU 的 存取點,並以互動方式控制 ESP8266 NodeMCU 選擇 SSID 及 輸入密碼 來連接 WiFi

見下文
見下文
Sketch > Include Library > Manager Libraries... 搜找 WiFiManager
由於有很多相似名稱及功能的函式庫,但文中所使用的是由 Tzapu Tablatronix 所製作的 WiFiManager

#include <WiFiManager.h>
#include <ESP8266WebServer.h>

const byte LED_WIFI = 2;
const byte LED_BOARD = 16;
ESP8266WebServer server = ESP8266WebServer(80);

const char* html PROGMEM = R"EOF(<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <title>ESP8266 LED Control</title>
    </head>
    <body>
        <div><a href="/led-wifi">LED WiFi</a></div>
        <div><a href="/led-board">LED Board</a></div>
    </body>
</html>)EOF";

void setup() {
	Serial.begin(9600);
	pinMode(LED_WIFI, OUTPUT);
	digitalWrite(LED_WIFI, HIGH);
	pinMode(LED_BOARD, OUTPUT);
	digitalWrite(LED_BOARD, HIGH);
	WiFiManager wfm = WiFiManager();
	wfm.resetSettings();
	wfm.autoConnect("SSID", "password"); // replace your SSID and password for ESP8826 Web Server Portal
	server.on("/", [] {
		server.send(200, "text/html", html);
	});
	server.on("/led-wifi", [] {
		String value = server.arg("value");
		if (value == "0") {
			digitalWrite(LED_WIFI, HIGH);
		} else if (value == "1") {
			digitalWrite(LED_WIFI, LOW);
		} else {
			digitalWrite(LED_WIFI, !digitalRead(LED_WIFI));
		}
		server.sendHeader("Location", "/");
		server.send(302);
	});
	server.on("/led-board", [] {
		String value = server.arg("value");
		if (value == "0") {
			digitalWrite(LED_BOARD, HIGH);
		} else if (value == "1") {
			digitalWrite(LED_BOARD, LOW);
		} else {
			digitalWrite(LED_BOARD, !digitalRead(LED_BOARD));
		}
		server.sendHeader("Location", "/");
		server.send(302);
	});
	server.begin();
}

void loop() {
	server.handleClient();
}

見下文
在 WiFi 中可以尋找到 ESP8266 Web Server Portal 的 WiFi 訊號

見下文
輸入設定的密碼加入到存取點

見下文
見下文
加入到 ESP8266 Web Server Portal 在 Arduino IDE 的 Serial Monitor 會顯示 預設閘道(Default Gateway) 的 IP地址
在下多次測試都是 192.168.4.1
注意:由於加入到 Portal 的裝置會暫時無法連接網絡,如果當時正在下載檔案便不要測試

見下文
使用網頁瀏覽器便可以顯示 WiFiManager 的設定頁面
Exit 就是離開操作,在下並不在此測試

見下文
Update頁面 可以將 WiFiManager 升級,但在下暫時沒有使用此方法升級

見下文
見下文
見下文
Info頁面 顯示 ESP8266 NodeMCU 當前的資料及狀態

見下文
見下文
見下文
Configure WiFi頁面 控制 ESP8266 Web Server Portal 連接其他 WiFi訊號
選擇 SSID 及 輸入密碼後,按 Save按鈕 便設定完成

見下文
連接後,在 Arduino IDE 的 Serial Monitor 會顯示已經連接的 SSID 及會顯示 ESP8266 Web Server Portal 連接 WiFi 後的 IP地址

見下文
見下文
結果與直接寫 SSID 及 密碼 相同

使用 MDNS 設定主機名

多重廣播DNS (Multicast DNS (MDNS)) 可以讓 ESP8266 NodeMCU HTTP伺服器 設定主機名,方便載入頁面
即使加入到不同的網絡,都可以使用相同的主機名來存取頁面,不需要記下複鎖的 IP地址

#include <WiFiManager.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

const byte LED_WIFI = 2;
const byte LED_BOARD = 16;
ESP8266WebServer server = ESP8266WebServer(80);

const char* html PROGMEM = R"EOF(<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <title>ESP8266 LED Control</title>
    </head>
    <body>
        <div><a href="/led-wifi">LED WiFi</a></div>
        <div><a href="/led-board">LED Board</a></div>
    </body>
</html>)EOF";

void setup() {
	Serial.begin(9600);
	pinMode(LED_WIFI, OUTPUT);
	digitalWrite(LED_WIFI, HIGH);
	pinMode(LED_BOARD, OUTPUT);
	digitalWrite(LED_BOARD, HIGH);
	WiFiManager wfm = WiFiManager();
	wfm.resetSettings();
	wfm.autoConnect("SSID", "password"); // replace your SSID and password for ESP8826 Web Server Portal
	MDNS.begin("hostname"); // replace your hostname for ESP8826 Web Server Portal
	server.on("/", [] {
		server.send(200, "text/html", html);
	});
	server.on("/led-wifi", [] {
		String value = server.arg("value");
		if (value == "0") {
			digitalWrite(LED_WIFI, HIGH);
		} else if (value == "1") {
			digitalWrite(LED_WIFI, LOW);
		} else {
			digitalWrite(LED_WIFI, !digitalRead(LED_WIFI));
		}
		server.sendHeader("Location", "/");
		server.send(302);
	});
	server.on("/led-board", [] {
		String value = server.arg("value");
		if (value == "0") {
			digitalWrite(LED_BOARD, HIGH);
		} else if (value == "1") {
			digitalWrite(LED_BOARD, LOW);
		} else {
			digitalWrite(LED_BOARD, !digitalRead(LED_BOARD));
		}
		server.sendHeader("Location", "/");
		server.send(302);
	});
	server.begin();
}

void loop() {
	server.handleClient();
    MDNS.update();
}

見下文
見下文
MDNS運作後,可以使用
ping hostname.local

確認主機名能否連接

見下文
可以使用主機名連接頁面

使用 SSL

如果只是使用 HTTP ,資料並沒有加密保障,資料很容易被竊取
ESP8266 提供 ESP8266WebServerSecure 以建立具備 SSL 的 HTTP伺服器
#include <WiFiManager.h>
#include <ESP8266WebServerSecure.h>
#include <ESP8266mDNS.h>

const byte LED_WIFI = 2;
const byte LED_BOARD = 16;
ESP8266WebServerSecure server = ESP8266WebServerSecure(443);

const char* html PROGMEM = R"EOF(<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <title>ESP8266 LED Control</title>
    </head>
    <body>
        <div><a href="/led-wifi">LED WiFi</a></div>
        <div><a href="/led-board">LED Board</a></div>
    </body>
</html>)EOF";

const char* publicCert PROGMEM = R"EOF(-----BEGIN CERTIFICATE-----
paste your public cert here
-----END CERTIFICATE-----)EOF";

const char* privateKey PROGMEM = R"EOF(-----BEGIN PRIVATE KEY-----
paste your private key here
-----END PRIVATE KEY-----)EOF";

void setup() {
	Serial.begin(9600);
	pinMode(LED_WIFI, OUTPUT);
	digitalWrite(LED_WIFI, HIGH);
	pinMode(LED_BOARD, OUTPUT);
	digitalWrite(LED_BOARD, HIGH);
	WiFiManager wfm = WiFiManager();
	wfm.resetSettings();
	wfm.autoConnect("SSID", "password"); // replace your SSID and password for ESP8826 Web Server Portal
	MDNS.begin("hostname"); // replace your hostname for ESP8826 Web Server Portal
    server.getServer().setRSACert(new X509List(publicCert), new PrivateKey(privateKey));
	server.on("/", [] {
		server.send(200, "text/html", html);
	});
	server.on("/led-wifi", [] {
		String value = server.arg("value");
		if (value == "0") {
			digitalWrite(LED_WIFI, HIGH);
		} else if (value == "1") {
			digitalWrite(LED_WIFI, LOW);
		} else {
			digitalWrite(LED_WIFI, !digitalRead(LED_WIFI));
		}
		server.sendHeader("Location", "/");
		server.send(302);
	});
	server.on("/led-board", [] {
		String value = server.arg("value");
		if (value == "0") {
			digitalWrite(LED_BOARD, HIGH);
		} else if (value == "1") {
			digitalWrite(LED_BOARD, LOW);
		} else {
			digitalWrite(LED_BOARD, !digitalRead(LED_BOARD));
		}
		server.sendHeader("Location", "/");
		server.send(302);
	});
	server.begin();
}

void loop() {
	server.handleClient();
    MDNS.update();
}

見下文
要以 SSL 建立 HTTP侵服器 需要建立 憑證及私鑰 ,在 Terminal 輸入
openssl req -x509 -nodes -newkey rsa:2048 -keyout "private.key" -out "public.crt"

執行時,要填寫的資料全部可以留空白
執行後,會建立 public.crt 憑證 及 private.key 私鑰
ESP8266WebServerSecure 最多支援 RSA 2048 ,如果使用 RSA 4096 則無法使用

見下文
見下文
使用 cat 指令 (或其他純文字軟件) 開啟及將內容複製

見下文
見下文
如果不想太複雜,可以開啟範本 File > Examples > ESP8266WebServer > HelloServerBearSSL
serverCertserverKey 複製到閣下的 ESP8266 HTTP伺服器 使用亦可以
但效慮到安全性問題,最好還是閣下自行建立 憑證及私鑰

見下文
見下文
由於憑證及私鑰並非由認證的網絡安全組織發佈,因此網頁瀏覽器認為是不安全憑證
接受風險並繼續 即可 (不同網頁瀏覽器操作會略有不同)

見下文
進入頁面,與先前的頁面其實相同
但由於受 SSL 保護,內容可以降低被中途竊取的風險

見下文
同樣由於使用自行建立 憑證及私鑰, Curl 無法通過安全認證
使用 Curl 時,需要附加 --insecure 才能執行
(!! (兩個感嘆號) 是重覆上次指令的指令)

使用登入認證

雖然使用 SSL 可以降低資料被竊取的風險,但操作不需要安全認證便可以執行
如果操作連結洩漏,即使使用 SSL 都無法得到保護
#include <WiFiManager.h>
#include <ESP8266WebServerSecure.h>
#include <ESP8266mDNS.h>

const byte LED_WIFI = 2;
const byte LED_BOARD = 16;
ESP8266WebServerSecure server = ESP8266WebServerSecure(443);

const char* html PROGMEM = R"EOF(<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
        <title>ESP8266 LED Control</title>
    </head>
    <body>
        <div><a href="/led-wifi">LED WiFi</a></div>
        <div><a href="/led-board">LED Board</a></div>
    </body>
</html>)EOF";

const char* publicCert PROGMEM = R"EOF(-----BEGIN CERTIFICATE-----
paste your public cert here
-----END CERTIFICATE-----)EOF";

const char* privateKey PROGMEM = R"EOF(-----BEGIN PRIVATE KEY-----
paste your private key here
-----END PRIVATE KEY-----)EOF";

const char* username = "username";
const char* password = "password";
const char* failMessage = "401 Unauthorized"; // replace your unauthorzied message

void setup() {
	Serial.begin(9600);
	pinMode(LED_WIFI, OUTPUT);
	digitalWrite(LED_WIFI, HIGH);
	pinMode(LED_BOARD, OUTPUT);
	digitalWrite(LED_BOARD, HIGH);
	WiFiManager wfm = WiFiManager();
	wfm.resetSettings();
	wfm.autoConnect("SSID", "password"); // replace your SSID and password for ESP8826 Web Server Portal
	MDNS.begin("hostname"); // replace your hostname for ESP8826 Web Server Portal
    server.getServer().setRSACert(new X509List(publicCert), new PrivateKey(privateKey));
	server.on("/", [] {
		server.send(200, "text/html", html);
	});
	server.on("/led-wifi", [] {
		if (server.authenticate(username, password)) {
			String value = server.arg("value");
			if (value == "0") {
				digitalWrite(LED_WIFI, HIGH);
			} else if (value == "1") {
				digitalWrite(LED_WIFI, LOW);
			} else {
				digitalWrite(LED_WIFI, !digitalRead(LED_WIFI));
			}
			server.sendHeader("Location", "/");
			server.send(302);
		} else {
			return server.requestAuthentication(BASIC_AUTH, NULL, failMessage);
		}
	});
	server.on("/led-board", [] {
		if (server.authenticate(username, password)) {
			String value = server.arg("value");
			if (value == "0") {
				digitalWrite(LED_BOARD, HIGH);
			} else if (value == "1") {
				digitalWrite(LED_BOARD, LOW);
			} else {
				digitalWrite(LED_BOARD, !digitalRead(LED_BOARD));
			}
			server.sendHeader("Location", "/");
			server.send(302);
		} else {
			return server.requestAuthentication(BASIC_AUTH, NULL, failMessage);
		}
	});
	server.begin();
}

void loop() {
	server.handleClient();
    MDNS.update();
}

見下文
載入操作頁面時,會顯示 基本認證 (Basic Authorization) 程序

見下文
如果取消或登入錯誤,會顯示登入錯誤的訊息

見下文
如果登入正確,便會執行操作

見下文
如果使用 Curl 操作,由於需要登入認證才能執行,未經登入認證會出錯
可以使用 -u username:password 的參數登入認證,例如:
curl -u "username:password" "http://hostname.local/led-wifi" --insecure

如果使用傳統 HTTP請求,則要使用 基本認證標頭 ,即是 Authorization: Basic base64("username:password")
curl --header "Authorization: Basic "`printf "username:password" | base64` "http://hostname.local/led-wifi" --insecure

當然還可以使用 URL 語法
curl "http://username:password@hostname.local/led-wifi" --insecure

雖然這種語法最方便,但亦是最不安全,部分網頁瀏覽器可能會保存登入資料,因此要小心使用
測試時最好在受信任的網絡下使用 無痕瀏覽 (Incognito Mode 或 Private Browsing 或 InPrivate)
避免明文密碼保存在網頁瀏覽器的歷史紀錄中

補充資料

server.on() 第二個參數是指派 callback function
在下編寫的
server.on("/", [] {
	server.send(200, "text/html", html);
});

可以改寫成
void htmlIndex() {
	server.send(200, "text/html", html);
}

server.on("/", htmlIndex);

如果 server.on() 需要多次使用相同的 callback function,可以將參數獨立成一個 void function
可以省卻程式碼,亦方便修改內容

總結

最初只是使用 ESP8266 NodeMCU 的內置 WiFi 功能,製作一個可以透過網頁遙距控制電子裝置
因此查看製作及建立 ESP8266 的 HTTP伺服器 後便完成測試,但發現越多越多需要的功能可以添加,因此增加了製作的時間

要將 SSID 及 密碼 直接寫在程式碼,覺得有點危險,尤其實要製作教學時,擔心不慎將重要的資料洩泄
因此尋找到使用 WiFiManager 可以建立 存取點 ,便不需要將 SSID 及 密碼 編寫在代碼中
每次都要查看 Arduino IDE 的 Serial Monitor 確認 ESP8266 的 IP地址,才能進入 HTTP伺服器
而且進入其他 WiFi 時的 IP地址 很大機會不相同,又要查看 Serial Monitor ,非常麻煩

因此嘗試設定 ESP8266 的 主機名,最初在網上查看需要使用 WiFi.hostname("hostname") 但都無法達到效果
使用 MDNS 都無法使用,原來需要在 void loop 中加入 MDNS.update() 才能保持 MDNS 運作

最後使用具備 SSL 的 HTTP伺服器 及 設定登入認證 提升安全性

ESP8266 NodeMCU 還提供 FLASH按鈕,可以讓 ESP8266 Web Server 以 on demand 的方式起動,而不需要額外配件

如果閣下預計會以 Rest Server 或 API 方式執行操作,其實可以不需要自動返回主頁,簡化設計

測試登入認證時最好使用 無痕模式 (Incognito Mode)
避免登入後要關閉瀏覽器才能登出,影響閣下瀏覽網頁的體驗

使用 SSL 建立的 HTTP伺服器,執行操作的回應時間比較慢
而且回應速度並不固定,3秒至30秒不等

參考資料

沒有留言 :

張貼留言