ESP32: WiFi Manager với thư viện AsyncWebServer
Trong bài viết này, bạn sẽ được hướng dẫn cách sử dụng WiFi Manager với thư viện ESPAsyncWebServer thay vì cách thông thường, giúp thiết bị có thể được cấu hình WiFi một cách dễ dàng mà không cần khai báo cố định trong mã nguồn. Với WiFi Manager, thiết bị sẽ tự động kết nối vào mạng WiFi lần trước đó hoặc phát ra một mạng WiFi của riêng nó (chế độ Access Point), để người dùng có thể kết nối vào và cấu hình mạng WiFi mới cho thiết bị.
Cách hoạt động của thư viện WiFi Manager
Bạn hãy xem sơ đồ bên dưới đây để hiểu rõ hơn cách hoạt động của thư viện WiFi Manager mà chúng ta sẽ áp dụng trong bài hướng dẫn này.
- Khi ESP32 khởi động, nó sẽ tìm kiếm các file ssid.txt, pass.txt và ip.txt (1);
- Nếu các file này không tồn tại hoặc không có dữ liệu (2) (thiết bị chạy lần đầu tiên sau khi nạp code), ESp32 sẽ bật mode Access Point và phát ra một mạng WiFi của riêng mình (3);
- Người dùng sử dụng các thiết bị như máy tính hoặc điện thoại để kết nối vào mạng WiFi này và truy cập vào địa chỉ IP 192.168.4.1 để mở trang web cấu hình WiFi cho thiết bị (4);
- Các thông tin cấu hình trong form như SSID, password, và địa chỉ IP address sẽ được lưu vào các file tương ứng trên ESP32 như: ssid.txt, pass.txt, and ip.txt (5);
- Sau đó ESP32 khởi động lại (6);
- Sau khi khởi động, các file cấu hình này có chứa dữ liệu nên ESP32 sẽ đọc và cố gắng kết nối đến mạng WiFi đã lưu (7);
- Nếu kết nối thành công, quá trình kết nối WiFi hoàn tất và thiết bị sẽ thực hiện các tác vụ khác như trong chương trình được nạp (9). Ngược lại nếu không thành công, ESP32 sẽ chuyển về mode Access Point và phát ra mạng WiFi như bước số 3 ở trên .
Để demo tính năng WiFi Manager, chúng ta sẽ setup một web server cho phép điều khiển đèn LED trên board ESP32 (D13). Bạn có thể sử dụng WiFi Manager với thư viện ESPAsyncWebServer cho các dự án cần ESP32 kết nối với mạng WiFi.
Yêu cầu
Để thực hiện theo bài hướng dẫn này, bạn cần cài đặt Arduino IDE và add-on hỗ trợ lập trình cho ESP32 ở bài viết này.
Cài đặt thư viện trong Arduino IDE
Bạn cần cài đặt thêm các thư viện sau trong Arduino IDE để setup web server
- ESPAsyncWebServer (.zip folder)
- AsyncTCP (.zip folder)
Thư viện ESPAsyncWebServer, AsynTCP, và ESPAsyncTCP không có sẵn trong Arduino Library Manager, bạn cần phải download về máy và cài đặt trong Arduino thông qua menu Sketch > Include Library > Add .zip Library.
Filesystem Uploader
Ngoài ra, bạn cũng cần cài đặt thêm một plugin cho Arduino IDE có tên là ESP32 Uploader.
Tổ chức các file
Để giúp tổ chức các file của dự án một cách ngăn nắp và dễ hiểu, chúng ta sẽ tạo ra 4 file khác nhau sau đây cho chức năng web server:
- Arduino sketch chứa chương trình Arduino chính;
- index.html: chứa nội dung của trang web để điều khiển đèn led;
- style.css: chứa các format css của trang web;
- wifimanager.html: chứa trang web hiển thị ra giao diện cho phép người dùng cấu hình mạng WiFi.
Các file HTML và CSS sẽ được chứa chung trong thư mục data trong thư mục của dự án. Chúng ta cần upload tất cả các file này vào filesystem của ESP32 (SPIFFS).
Bạn có thể download toàn bộ file của dự án này ở đây:
Tạo các file HTML
Trong dự án này, chúng ta sẽ cần 2 file HTML. Một file là chứa giao diện web để điều khiển đèn LED trên board (index.html) và file còn lại để hiển thị giao diện cấu hình WiFi (wifimanager.html).
index.html
<!DOCTYPE html> <html> <head> <title>ESP WEB SERVER</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" href="style.css"> <link rel="icon" type="image/png" href="favicon.png"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"> </head> <body> <div class="topnav"> <h1>ESP WEB SERVER</h1> </div> <div class="content"> <div class="card-grid"> <div class="card"> <p class="card-title"><i class="fas fa-lightbulb"></i> GPIO 2</p> <p> <a href="on"><button class="button-on">ON</button></a> <a href="off"><button class="button-off">OFF</button></a> </p> <p class="state">State: %STATE%</p> </div> </div> </div> </body> </html>
Chúng ta sẽ không đi vào tìm hiểu kỹ nội dung các file HTML này vì nó nằm ngoài phạm vi của bài viết.
wifimanager.html
Trang web cấu hình WiFi cho WiFi Manager sẽ trông giống như sau:
Copy nội dung sau vào file wifimanager.html của bạn. File này bao gồm 1 form, 3 trường nhập dữ liệu và một 1 nút Submit để lưu dữ liệu.
<!DOCTYPE html> <html> <head> <title>ESP Wi-Fi Manager</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" href="data:,"> <link rel="stylesheet" type="text/css" href="style.css"> </head> <body> <div class="topnav"> <h1>ESP Wi-Fi Manager</h1> </div> <div class="content"> <div class="card-grid"> <div class="card"> <form action="/" method="POST"> <p> <label for="ssid">SSID</label> <input type="text" id ="ssid" name="ssid"><br> <label for="pass">Password</label> <input type="text" id ="pass" name="pass"><br> <label for="ip">IP Address</label> <input type="text" id ="ip" name="ip" value="192.168.1.200"><br> <label for="gateway">Gateway Address</label> <input type="text" id ="gateway" name="gateway" value="192.168.1.1"><br> <input type ="submit" value ="Submit"> </p> </form> </div> </div> </div> </body> </html>
File CSS
Copy nội dung sau vào file style.css của bạn.
html { font-family: Arial, Helvetica, sans-serif; display: inline-block; text-align: center; } h1 { font-size: 1.8rem; color: white; } p { font-size: 1.4rem; } .topnav { overflow: hidden; background-color: #0A1128; } body { margin: 0; } .content { padding: 5%; } .card-grid { max-width: 800px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); } .card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); } .card-title { font-size: 1.2rem; font-weight: bold; color: #034078 } input[type=submit] { border: none; color: #FEFCFB; background-color: #034078; padding: 15px 15px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; width: 100px; margin-right: 10px; border-radius: 4px; transition-duration: 0.4s; } input[type=submit]:hover { background-color: #1282A2; } input[type=text], input[type=number], select { width: 50%; padding: 12px 20px; margin: 18px; display: inline-block; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } label { font-size: 1.2rem; } .value{ font-size: 1.2rem; color: #1282A2; } .state { font-size: 1.2rem; color: #1282A2; } button { border: none; color: #FEFCFB; padding: 15px 32px; text-align: center; font-size: 16px; width: 100px; border-radius: 4px; transition-duration: 0.4s; } .button-on { background-color: #034078; } .button-on:hover { background-color: #1282A2; } .button-off { background-color: #858585; } .button-off:hover { background-color: #252524; }
Thiết lập Web Server
Chương trình của chúng ta sẽ như sau:
#include <Arduino.h> #include <WiFi.h> #include <ESPAsyncWebServer.h> #include <AsyncTCP.h> #include "SPIFFS.h" // Create AsyncWebServer object on port 80 AsyncWebServer server(80); // Search for parameter in HTTP POST request const char* PARAM_INPUT_1 = "ssid"; const char* PARAM_INPUT_2 = "pass"; const char* PARAM_INPUT_3 = "ip"; const char* PARAM_INPUT_4 = "gateway"; //Variables to save values from HTML form String ssid; String pass; String ip; String gateway; // File paths to save input values permanently const char* ssidPath = "/ssid.txt"; const char* passPath = "/pass.txt"; const char* ipPath = "/ip.txt"; const char* gatewayPath = "/gateway.txt"; IPAddress localIP; //IPAddress localIP(192, 168, 1, 200); // hardcoded // Set your Gateway IP address IPAddress localGateway; //IPAddress localGateway(192, 168, 1, 1); //hardcoded IPAddress subnet(255, 255, 0, 0); // Timer variables unsigned long previousMillis = 0; const long interval = 10000; // interval to wait for Wi-Fi connection (milliseconds) // Set LED GPIO const int ledPin = 2; // Stores LED state String ledState; // Initialize SPIFFS void initSPIFFS() { if (!SPIFFS.begin(true)) { Serial.println("An error has occurred while mounting SPIFFS"); } Serial.println("SPIFFS mounted successfully"); } // Read File from SPIFFS String readFile(fs::FS &fs, const char * path){ Serial.printf("Reading file: %s\r\n", path); File file = fs.open(path); if(!file || file.isDirectory()){ Serial.println("- failed to open file for reading"); return String(); } String fileContent; while(file.available()){ fileContent = file.readStringUntil('\n'); break; } return fileContent; } // Write file to SPIFFS void writeFile(fs::FS &fs, const char * path, const char * message){ Serial.printf("Writing file: %s\r\n", path); File file = fs.open(path, FILE_WRITE); if(!file){ Serial.println("- failed to open file for writing"); return; } if(file.print(message)){ Serial.println("- file written"); } else { Serial.println("- write failed"); } } // Initialize WiFi bool initWiFi() { if(ssid=="" || ip==""){ Serial.println("Undefined SSID or IP address."); return false; } WiFi.mode(WIFI_STA); localIP.fromString(ip.c_str()); localGateway.fromString(gateway.c_str()); if (!WiFi.config(localIP, localGateway, subnet)){ Serial.println("STA Failed to configure"); return false; } WiFi.begin(ssid.c_str(), pass.c_str()); Serial.println("Connecting to WiFi..."); unsigned long currentMillis = millis(); previousMillis = currentMillis; while(WiFi.status() != WL_CONNECTED) { currentMillis = millis(); if (currentMillis - previousMillis >= interval) { Serial.println("Failed to connect."); return false; } } Serial.println(WiFi.localIP()); return true; } // Replaces placeholder with LED state value String processor(const String& var) { if(var == "STATE") { if(digitalRead(ledPin)) { ledState = "ON"; } else { ledState = "OFF"; } return ledState; } return String(); } void setup() { // Serial port for debugging purposes Serial.begin(115200); initSPIFFS(); // Set GPIO 2 as an OUTPUT pinMode(ledPin, OUTPUT); digitalWrite(ledPin, LOW); // Load values saved in SPIFFS ssid = readFile(SPIFFS, ssidPath); pass = readFile(SPIFFS, passPath); ip = readFile(SPIFFS, ipPath); gateway = readFile (SPIFFS, gatewayPath); Serial.println(ssid); Serial.println(pass); Serial.println(ip); Serial.println(gateway); if(initWiFi()) { // Route for root / web page server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(SPIFFS, "/index.html", "text/html", false, processor); }); server.serveStatic("/", SPIFFS, "/"); // Route to set GPIO state to HIGH server.on("/on", HTTP_GET, [](AsyncWebServerRequest *request) { digitalWrite(ledPin, HIGH); request->send(SPIFFS, "/index.html", "text/html", false, processor); }); // Route to set GPIO state to LOW server.on("/off", HTTP_GET, [](AsyncWebServerRequest *request) { digitalWrite(ledPin, LOW); request->send(SPIFFS, "/index.html", "text/html", false, processor); }); server.begin(); } else { // Connect to Wi-Fi network with SSID and password Serial.println("Setting AP (Access Point)"); // NULL sets an open Access Point WiFi.softAP("ESP-WIFI-MANAGER", NULL); IPAddress IP = WiFi.softAPIP(); Serial.print("AP IP address: "); Serial.println(IP); // Web Server Root URL server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/wifimanager.html", "text/html"); }); server.serveStatic("/", SPIFFS, "/"); server.on("/", HTTP_POST, [](AsyncWebServerRequest *request) { int params = request->params(); for(int i=0;i<params;i++){ AsyncWebParameter* p = request->getParam(i); if(p->isPost()){ // HTTP POST ssid value if (p->name() == PARAM_INPUT_1) { ssid = p->value().c_str(); Serial.print("SSID set to: "); Serial.println(ssid); // Write file to save value writeFile(SPIFFS, ssidPath, ssid.c_str()); } // HTTP POST pass value if (p->name() == PARAM_INPUT_2) { pass = p->value().c_str(); Serial.print("Password set to: "); Serial.println(pass); // Write file to save value writeFile(SPIFFS, passPath, pass.c_str()); } // HTTP POST ip value if (p->name() == PARAM_INPUT_3) { ip = p->value().c_str(); Serial.print("IP Address set to: "); Serial.println(ip); // Write file to save value writeFile(SPIFFS, ipPath, ip.c_str()); } // HTTP POST gateway value if (p->name() == PARAM_INPUT_4) { gateway = p->value().c_str(); Serial.print("Gateway set to: "); Serial.println(gateway); // Write file to save value writeFile(SPIFFS, gatewayPath, gateway.c_str()); } //Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); } } request->send(200, "text/plain", "Done. ESP will restart, connect to your router and go to IP address: " + ip); delay(3000); ESP.restart(); }); server.begin(); } } void loop() { }
Giải thích chương trình
Các biến sau để lưu các thông tin về mạng WiFi như SSID, password, địa chỉ IP, và gateway sau khi người dùng điền vào form và nhấn nút submit.
// Search for parameter in HTTP POST request const char* PARAM_INPUT_1 = "ssid"; const char* PARAM_INPUT_2 = "pass"; const char* PARAM_INPUT_3 = "ip"; const char* PARAM_INPUT_4 = "gateway";
Các biến ssid, pass, ip, và gateway dùng để lưu các giá trị nhận được.
//Variables to save values from HTML form String ssid; String pass; String ip; String gateway;
Đường dẫn chứa các file cấu hình WiFi trong file system của ESP32.
// File paths to save input values permanently const char* ssidPath = "/ssid.txt"; const char* passPath = "/pass.txt"; const char* ipPath = "/ip.txt"; const char* gatewayPath = "/gateway.txt";
Các thông tin về gateway và submit. Bạn có thể bỏ qua nếu muốn các thông tin này sẽ được tự động lấy tùy theo từng mạng WiFi.
IPAddress localIP; //IPAddress localIP(192, 168, 1, 200); // hardcoded // Set your Gateway IP address IPAddress localGateway; //IPAddress localGateway(192, 168, 1, 1); //hardcoded IPAddress subnet(255, 255, 0, 0);
initWiFi()
Hàm initWiFi() trả về giá trị boolean (đúng hoặc sai) tùy theo trạng thái kết nối WiFi thành công thay thất bại của ESP32.
bool initWiFi() { if(ssid=="" || ip==""){ Serial.println("Undefined SSID or IP address."); return false; } WiFi.mode(WIFI_STA); localIP.fromString(ip.c_str()); if (!WiFi.config(localIP, gateway, subnet)){ Serial.println("STA Failed to configure"); return false; } WiFi.begin(ssid.c_str(), pass.c_str()); Serial.println("Connecting to WiFi..."); unsigned long currentMillis = millis(); previousMillis = currentMillis; while(WiFi.status() != WL_CONNECTED) { currentMillis = millis(); if (currentMillis - previousMillis >= interval) { Serial.println("Failed to connect."); return false; } } Serial.println(WiFi.localIP()); return true; }
Trước tiên, kiểm tra ssid và ip có rỗng không. Nếu thông tin là rỗng thì không thể kết nối với WiFi được và trả về false ngay.
if(ssid=="" || ip==""){
Nếu không rỗng, cho ESp32 kết nối với mạng WiFi có SSID và password đã lưu.
WiFi.mode(WIFI_STA); localIP.fromString(ip.c_str()); if (!WiFi.config(localIP, gateway, subnet)){ Serial.println("STA Failed to configure"); return false; } WiFi.begin(ssid.c_str(), pass.c_str()); Serial.println("Connecting to WiFi...");
Nếu không thể kết nối WiFi trong 10 giây, trả về false.
unsigned long currentMillis = millis(); previousMillis = currentMillis; while(WiFi.status() != WL_CONNECTED) { currentMillis = millis(); if (currentMillis - previousMillis >= interval) { Serial.println("Failed to connect."); return false; }
Nếu kết nối thành công thì trả về true.
return true;
setup()
Trong hàm setup(), bắt đầu đọc các file chứa SSID, password, IP address, và gateway.
ssid = readFile(LittleFS, ssidPath); pass = readFile(LittleFS, passPath); ip = readFile(LittleFS, ipPath); gateway = readFile (LittleFS, gatewayPath);
Nếu ESP32 kết nối WiFi thành công (hàm initWiFi() trả về true), bắt đầu xử lý các yêu cầu gửi đến web server:
if(initWiFi()) { // Route for root / web page server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(SPIFFS, "/index.html", "text/html", false, processor); }); server.serveStatic("/", SPIFFS, "/"); // Route to set GPIO state to HIGH server.on("/on", HTTP_GET, [](AsyncWebServerRequest *request) { digitalWrite(ledPin, HIGH); request->send(SPIFFS, "/index.html", "text/html", false, processor); }); // Route to set GPIO state to LOW server.on("/off", HTTP_GET, [](AsyncWebServerRequest *request) { digitalWrite(ledPin, LOW); request->send(SPIFFS, "/index.html", "text/html", false, processor); }); server.begin(); }
Nếu kết nối bị thất bại, hàm initWiFi() trả về false, ESP32 sẽ bật mode access point và phát ra một mạng WiFi có tên là ESP-WIFI-MANAGER không có password bằng hàm softAP():
else { // Connect to Wi-Fi network with SSID and password Serial.println("Setting AP (Access Point)"); // NULL sets an open Access Point WiFi.softAP("ESP-WIFI-MANAGER", NULL); IPAddress IP = WiFi.softAPIP(); Serial.print("AP IP address: "); Serial.println(IP);
Khi chúng ta sử dụng máy tính hoặc điện thoại kết nối vào mạng WiFi này và truy cập địa chỉ 192.168.4.1, ESP32 sẽ hiển thị trang web để cấu hình mạng WiFibằng file wifimanager.html file.
// Web Server Root URL server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/wifimanager.html", "text/html"); });
Chúng ta cũng cần xử lý yêu cầu khi người dùng điền form và nhấn nút Submit để lưu thông tin WiFi là lưu xuống các file có tên tương ứng.
server.on("/", HTTP_POST, [](AsyncWebServerRequest *request) { int params = request->params(); for(int i=0;i<params;i++){ AsyncWebParameter* p = request->getParam(i); if(p->isPost()){ // HTTP POST ssid value if (p->name() == PARAM_INPUT_1) { ssid = p->value().c_str(); Serial.print("SSID set to: "); Serial.println(ssid); // Write file to save value writeFile(SPIFFS, ssidPath, ssid.c_str()); } // HTTP POST pass value if (p->name() == PARAM_INPUT_2) { pass = p->value().c_str(); Serial.print("Password set to: "); Serial.println(pass); // Write file to save value writeFile(SPIFFS, passPath, pass.c_str()); } // HTTP POST ip value if (p->name() == PARAM_INPUT_3) { ip = p->value().c_str(); Serial.print("IP Address set to: "); Serial.println(ip); // Write file to save value writeFile(SPIFFS, ipPath, ip.c_str()); } // HTTP POST gateway value if (p->name() == PARAM_INPUT_4) { gateway = p->value().c_str(); Serial.print("Gateway set to: "); Serial.println(gateway); // Write file to save value writeFile(SPIFFS, gatewayPath, gateway.c_str()); } //Serial.printf("POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); } }
Sau khi submit form, ESP32 xuất ra mã code 200 báo hiệu xử lý thành công và dòng thông báo cho người dùng biết kết quả:
request->send(200, "text/plain", "Done. ESP will restart, connect to your router and go to IP address: " + ip);
Sau 3 giây, ESP32 khởi động lại bằng hàm ESP.restart():
delay(3000); ESP.restart();
Uploading Code và các file dữ liệu
Upload các file trong thư mục data vào ESP32bằng menu Tools > ESP32 Sketch Data Upload.
Sau khi upload các file data thành công, bạn hãy upload chương trình vào board ESP32.
Kết quả
Sau khi upload thành công các file data và chương trình, bạn mở cửa sổ Serial Monitor và quan sát kết quả khi chạy lần đầu và sau khi cấu hình WiFi.
Kết nối máy tính hoặc điện thoại vào mạng WiFi Access Point của ESP32 để cấu hình WiFi
Mở trình duyệt và truy cập 192.168.4.1. Trang web cấu hình WiFi sẽ hiện ra.
Nhập thông tin như: SSID và Password và địa chỉ IP address.
Sau khi ESP32 reset và kết nối WiFi thành công, bạn có thể mở trang web với địa chỉ IP của ESP32 để điều khiển đèn LED trên board:
Lời kết
Bài viết trên đã hướng dẫn bạn cách cấu hình WiFi Manager để phục vụ cho các dự án Web Server cùng mạch ESP. Với hướng dẫn trên, bạn có thể dễ dàng kết nối mạch ESP tới bất kỳ mạng nào. Chúc các bạn thành công!