Cấu hình Wi-Fi cho ESP32 bằng điện thoại, không nạp lại code: tự làm trang cài đặt như WiFiManager
- 1. ESP32 chọn chế độ khi khởi động
- 2. Chuẩn bị
- 3. Chương trình
- 4. Cài Wi-Fi trên điện thoại
- 5. Giải thích các phần chính
- Lưu cấu hình bằng Preferences
- Trang cài đặt tự bật lên (captive portal)
- Vừa phát mạng, vừa quét mạng
- Ghép vào chương trình của mình
- 6. Kết quả
- 7. Bảo mật và lưu ý
- 8. Xử lý lỗi thường gặp
- 9. Xem thêm
ESP32 tự phát mạng Wi-Fi khi chưa có cấu hình, điện thoại vào là bật trang chọn mạng, lưu bằng Preferences; chỉ dùng thư viện có sẵn, giữ BOOT 3 giây để xóa.
Hầu hết chương trình ESP32 ghi thẳng tên và mật khẩu Wi-Fi vào code. Mang mạch sang nhà khác, đổi modem hay đổi mật khẩu là phải cắm máy tính nạp lại. Thiết bị bán cho người khác thì càng không thể làm vậy. Cách thường dùng là cho ESP32 tự phát một mạng Wi-Fi riêng khi chưa có cấu hình: điện thoại vào mạng đó, một trang chọn mạng tự bật lên, người dùng chọn Wi-Fi nhà mình, nhập mật khẩu là xong. Bài này tự làm chức năng đó bằng các thư viện có sẵn trong gói esp32, không cần thư viện WiFiManager, và giải thích từng phần để dễ ghép vào chương trình của mình.
1. ESP32 chọn chế độ khi khởi động

- Có cấu hình và vào được Wi-Fi: chạy chương trình chính như bình thường.
- Chưa có cấu hình, hoặc 20 giây không vào được: chuyển sang chế độ cài đặt, phát mạng Wi-Fi tên PYWORLD kèm 4 ký tự cuối của địa chỉ MAC, ví dụ
PYWORLD-B2C3, để nhiều mạch không trùng tên. - Đang ở chế độ cài đặt mà có cấu hình cũ: sau 5 phút tự khởi động lại thử Wi-Fi cũ. Trường hợp hay gặp là mất điện cả nhà, ESP32 khởi động nhanh hơn modem nên lúc đầu không vào được.
- Giữ nút BOOT 3 giây khi mạch đang chạy: xóa cấu hình, quay về chế độ cài đặt.
2. Chuẩn bị
- ESP32 DevKit V1 30 chân (có sẵn nút BOOT), hoặc bản 38 chân.
- Dây USB nạp code và một điện thoại.
Bài không cần nối thêm linh kiện. Khi lắp vào hộp, có thể đưa nút xóa cấu hình ra ngoài vỏ: nối một nút nhấn từ GPIO0 xuống GND song song với nút BOOT.
3. Chương trình
Chỉ dùng WiFi, WebServer, DNSServer, Preferences có sẵn trong gói esp32 (bài dùng phiên bản 3.3.10). Không cần sửa gì trước khi nạp; đổi MAT_KHAU_AP nếu muốn mật khẩu mạng cài đặt khác.
// PYWORLD.VN - Cấu hình Wi-Fi cho ESP32 bằng điện thoại, không phải nạp lại chương trình
// Chưa có Wi-Fi: ESP32 phát mạng riêng, điện thoại vào là tự mở trang chọn mạng; giữ nút BOOT 3 giây để xóa cấu hình
// Chỉ dùng WiFi, WebServer, DNSServer, Preferences có sẵn trong gói esp32
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <Preferences.h>
const char *MAT_KHAU_AP = "pyworld123"; // Mật khẩu mạng cài đặt (ít nhất 8 ký tự)
const uint8_t NUT_XOA = 0; // Nút BOOT trên ESP32 DevKit (GPIO0)
const unsigned long GIU_XOA_MS = 3000; // Giữ 3 giây khi mạch đang chạy thì xóa cấu hình
const unsigned long CHO_KET_NOI_MS = 20000; // Chờ vào Wi-Fi tối đa 20 giây
const unsigned long CAI_DAT_TOI_DA_MS = 300000; // Có cấu hình cũ mà ở chế độ cài đặt quá 5 phút: khởi động lại thử tiếp
WebServer server(80);
DNSServer dns;
Preferences pref;
bool cheDoCaiDat = false;
unsigned long lucVaoCaiDat = 0;
String ssidLuu, matKhauLuu, dsMang;
String maHoaHtml(const String &s) {
String r;
for (char c : s) {
if (c == '<') r += "<";
else if (c == '>') r += ">";
else if (c == '&') r += "&";
else if (c == '"') r += """;
else r += c;
}
return r;
}
void docCauHinh() {
pref.begin("wifi", true); // true: chỉ đọc
ssidLuu = pref.getString("ssid", "");
matKhauLuu = pref.getString("pass", "");
pref.end();
}
void luuCauHinh(const String &ssid, const String &pass) {
pref.begin("wifi", false);
pref.putString("ssid", ssid);
pref.putString("pass", pass);
pref.end();
}
void xoaCauHinh() {
pref.begin("wifi", false);
pref.clear();
pref.end();
}
bool thuKetNoi(const String &ssid, const String &pass) {
Serial.printf("Dang vao Wi-Fi \"%s\"", ssid.c_str());
WiFi.mode(WIFI_STA);
WiFi.begin(ssid.c_str(), pass.c_str());
unsigned long batDau = millis();
while (WiFi.status() != WL_CONNECTED && millis() - batDau < CHO_KET_NOI_MS) {
delay(500);
Serial.print(".");
}
Serial.println();
return WiFi.status() == WL_CONNECTED;
}
// Quét các mạng xung quanh, dựng sẵn danh sách <option> cho trang cài đặt
void quetMang() {
int n = WiFi.scanNetworks();
dsMang = "";
for (int i = 0; i < n; i++) {
String ten = maHoaHtml(WiFi.SSID(i));
if (ten.length() == 0) continue; // Mạng ẩn tên: nhập tay ở ô bên dưới
dsMang += "<option value=\"" + ten + "\">" + ten + " (" + String(WiFi.RSSI(i)) + " dBm" +
(WiFi.encryptionType(i) == WIFI_AUTH_OPEN ? ", mở" : "") + ")</option>";
}
WiFi.scanDelete();
Serial.printf("Tim thay %d mang\n", n);
}
void trangCaiDat() {
String h = F("<!doctype html><html lang=\"vi\"><head><meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Cài đặt Wi-Fi</title>"
"<style>body{font-family:Arial,sans-serif;background:#eef2f7;margin:0;padding:16px}"
"form{background:#fff;border-radius:12px;padding:16px}label{display:block;margin:12px 0 4px}"
"select,input{width:100%;box-sizing:border-box;font-size:16px;padding:10px}"
"button{margin-top:16px;width:100%;padding:12px;font-size:17px;border:0;border-radius:8px;"
"background:#1f5fae;color:#fff}</style></head><body><h2>Chọn Wi-Fi cho ESP32</h2>"
"<form method=\"POST\" action=\"/luu\"><label>Mạng Wi-Fi 2,4 GHz</label><select name=\"s\">");
h += dsMang;
h += F("</select><label>Hoặc gõ tên mạng (mạng ẩn tên)</label><input name=\"s2\" maxlength=\"32\">"
"<label>Mật khẩu</label><input name=\"p\" type=\"password\" maxlength=\"64\">"
"<button type=\"submit\">Lưu và kết nối</button></form>"
"<p><a href=\"/quet\">Quét lại</a></p></body></html>");
server.send(200, "text/html; charset=utf-8", h);
}
void xuLyLuu() {
String ssid = server.arg("s2").length() ? server.arg("s2") : server.arg("s");
String pass = server.arg("p");
if (ssid.length() == 0) {
server.send(400, "text/plain; charset=utf-8", "Chưa chọn mạng Wi-Fi");
return;
}
luuCauHinh(ssid, pass);
Serial.printf("Da luu Wi-Fi \"%s\", khoi dong lai\n", ssid.c_str());
server.send(200, "text/html; charset=utf-8",
"<meta charset=\"utf-8\"><h2>Đã lưu.</h2><p>ESP32 khởi động lại và vào mạng " + maHoaHtml(ssid) +
". Nếu sau 30 giây mạng cài đặt lại hiện ra, mật khẩu có thể sai.</p>");
delay(1500);
ESP.restart();
}
void batCheDoCaiDat() {
cheDoCaiDat = true;
lucVaoCaiDat = millis();
WiFi.disconnect(true);
WiFi.mode(WIFI_AP_STA); // Vừa phát mạng cài đặt, vừa quét được các mạng xung quanh
String mac = WiFi.macAddress();
String tenAp = "PYWORLD-" + mac.substring(12, 14) + mac.substring(15, 17); // Thêm 4 ký tự cuối của MAC
WiFi.softAP(tenAp.c_str(), MAT_KHAU_AP);
dns.start(53, "*", WiFi.softAPIP()); // Mọi tên miền đều trỏ về ESP32: điện thoại tự mở trang cài đặt
quetMang();
server.on("/", HTTP_GET, trangCaiDat);
server.on("/luu", HTTP_POST, xuLyLuu);
server.on("/quet", HTTP_GET, []() {
quetMang();
server.sendHeader("Location", "/");
server.send(302, "text/plain", "");
});
server.onNotFound([]() { // Trang kiểm tra Internet của điện thoại cũng được chuyển về trang cài đặt
server.sendHeader("Location", String("http://") + WiFi.softAPIP().toString() + "/");
server.send(302, "text/plain", "");
});
server.begin();
Serial.printf("Che do cai dat: vao Wi-Fi \"%s\", mat khau %s, mo http://%s\n", tenAp.c_str(), MAT_KHAU_AP,
WiFi.softAPIP().toString().c_str());
}
void batCheDoChay() {
Serial.printf("Da vao Wi-Fi, IP %s\n", WiFi.localIP().toString().c_str());
server.on("/", HTTP_GET, []() {
server.send(200, "text/html; charset=utf-8",
"<meta charset=\"utf-8\"><h2>ESP32 đang chạy</h2><p>Wi-Fi: " + maHoaHtml(WiFi.SSID()) +
", " + String(WiFi.RSSI()) + " dBm</p>");
});
server.begin();
}
void kiemTraNutXoa() {
static unsigned long lucNhan = 0;
if (digitalRead(NUT_XOA) == LOW) {
if (lucNhan == 0) lucNhan = millis();
if (millis() - lucNhan > GIU_XOA_MS) {
Serial.println("Xoa cau hinh Wi-Fi, khoi dong lai");
xoaCauHinh();
delay(200);
ESP.restart();
}
} else {
lucNhan = 0;
}
}
void setup() {
Serial.begin(115200);
pinMode(NUT_XOA, INPUT_PULLUP);
docCauHinh();
if (ssidLuu.length() && thuKetNoi(ssidLuu, matKhauLuu)) {
batCheDoChay();
} else {
Serial.println(ssidLuu.length() ? "Khong vao duoc Wi-Fi da luu" : "Chua co cau hinh Wi-Fi");
batCheDoCaiDat();
}
}
void loop() {
if (cheDoCaiDat) {
dns.processNextRequest();
if (ssidLuu.length() && millis() - lucVaoCaiDat > CAI_DAT_TOI_DA_MS) {
Serial.println("Thu lai Wi-Fi da luu");
ESP.restart();
}
}
server.handleClient();
kiemTraNutXoa();
// Chương trình chính (đọc cảm biến, điều khiển relay…) đặt ở đây, chỉ chạy khi !cheDoCaiDat nếu cần mạng
}
4. Cài Wi-Fi trên điện thoại

- Nạp chương trình, mở Serial Monitor: dòng "Che do cai dat" cho biết tên mạng và mật khẩu. Trên điện thoại, vào cài đặt Wi-Fi, chọn mạng có tên bắt đầu bằng
PYWORLD-, nhập mật khẩupyworld123. - Điện thoại báo "Đăng nhập vào mạng" (tên gọi tùy hãng); bấm vào để mở trang cài đặt. Không thấy thông báo thì mở trình duyệt, gõ
192.168.4.1. Điện thoại có thể hỏi "mạng không có Internet, có giữ kết nối không": chọn giữ. - Chọn mạng Wi-Fi nhà mình trong danh sách (mạng 2,4 GHz), nhập mật khẩu, bấm Lưu và kết nối. Mạng ẩn tên thì gõ tên vào ô bên dưới.
- ESP32 lưu cấu hình và khởi động lại. Mạng cài đặt PYWORLD biến mất là ESP32 đã vào được Wi-Fi nhà.
5. Giải thích các phần chính

Lưu cấu hình bằng Preferences
Tên mạng và mật khẩu lưu vào vùng nhớ wifi của thư viện Preferences, nằm trong phân vùng NVS của bộ nhớ flash. Dữ liệu còn nguyên khi mất điện và khi nạp lại chương trình bằng Arduino IDE (trừ khi bật mục Erase All Flash Before Sketch Upload trong menu Tools). Cách dùng Preferences xem bài lưu dữ liệu vào flash bằng Preferences.
Trang cài đặt tự bật lên (captive portal)
Khi vào một mạng Wi-Fi, điện thoại thử mở một địa chỉ kiểm tra Internet của hãng. Ở chế độ cài đặt, ESP32 chạy máy chủ DNS trả mọi tên miền về chính nó (dns.start(53, "*", WiFi.softAPIP())), và máy chủ web trả mã chuyển hướng 302 cho mọi địa chỉ lạ (server.onNotFound). Điện thoại thấy trang kiểm tra bị chuyển hướng thì hiểu là mạng cần đăng nhập và tự mở trang cài đặt. Ở gói esp32 3.x, máy chủ DNS chạy nền; lệnh dns.processNextRequest() trong vòng lặp chỉ còn để tương thích với bản cũ.
Vừa phát mạng, vừa quét mạng
WiFi.mode(WIFI_AP_STA) cho ESP32 vừa phát mạng cài đặt vừa quét được các mạng xung quanh để đưa vào danh sách. Tên mạng được đổi các ký tự < > & " trước khi ghép vào trang (hàm maHoaHtml), tránh tên mạng lạ làm hỏng trang.
Ghép vào chương trình của mình
Giữ nguyên các hàm cấu hình. Phần việc chính (đọc cảm biến, điều khiển relay, gửi dữ liệu) đặt trong loop(); việc cần mạng chỉ chạy khi cheDoCaiDat là false. Trong batCheDoChay() thêm các địa chỉ web của chương trình chính, ví dụ trang điều khiển relay như bài trang web cập nhật tức thời cùng loạt.
6. Kết quả

Từ lần khởi động sau, ESP32 vào thẳng Wi-Fi đã lưu. Mở địa chỉ IP in trên Serial Monitor bằng trình duyệt sẽ thấy trang "ESP32 đang chạy" kèm tên mạng và cường độ sóng.
7. Bảo mật và lưu ý
- Đổi mật khẩu mạng cài đặt: mật khẩu mặc định ai đọc bài này cũng biết. Thiết bị dùng thật nên đặt mật khẩu riêng, ghi trên nhãn dán ở vỏ máy.
- Mật khẩu Wi-Fi lưu dạng chữ thường trong flash: ai cầm được mạch và đọc flash thì đọc được. Đó là mức chấp nhận được cho đồ dùng trong nhà, không dùng cách này để lưu mật khẩu quan trọng khác.
- Nút BOOT: giữ khi mạch đang chạy để xóa cấu hình. Giữ nút BOOT ngay lúc cắm điện thì ESP32 vào chế độ nạp chương trình chứ không chạy.
- Chỉ Wi-Fi 2,4 GHz: ESP32 không thấy mạng 5 GHz. Modem phát chung một tên cho cả hai băng tần thường vẫn vào được.
8. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Không thấy mạng cài đặt PYWORLD | ESP32 đã vào được Wi-Fi cũ (đang ở chế độ chạy) | Giữ BOOT 3 giây để xóa cấu hình |
| Vào mạng cài đặt nhưng trang không tự mở | Điện thoại đang dùng 4G song song, hoặc trình duyệt nhớ trang cũ | Tắt dữ liệu di động, gõ 192.168.4.1 trong trình duyệt |
| Danh sách không có mạng nhà mình | Modem chỉ phát 5 GHz, ở xa, hoặc mạng ẩn tên | Bấm "Quét lại", gõ tên vào ô mạng ẩn tên, bật băng tần 2,4 GHz trên modem |
| Lưu xong, 30 giây sau mạng cài đặt PYWORLD lại hiện ra | Sai mật khẩu Wi-Fi | Vào lại trang cài đặt, nhập lại mật khẩu (phân biệt chữ hoa, thường) |
| Mất điện cả nhà, có điện lại mà ESP32 vẫn ở chế độ cài đặt | ESP32 khởi động nhanh hơn modem | Chờ: sau 5 phút ESP32 tự khởi động lại và vào Wi-Fi cũ; có thể tăng CHO_KET_NOI_MS |
| Nạp lại chương trình thì mất cấu hình | Đang bật xóa toàn bộ flash khi nạp | Tắt Tools > Erase All Flash Before Sketch Upload |
9. Xem thêm
- Kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh và phát Wi-Fi riêng
- Lưu dữ liệu khi mất điện bằng Preferences
- Nạp chương trình cho ESP32 qua Wi-Fi (OTA)
- Hộp mica cho ESP32 DevKit 30 chân
- Các bài cùng loạt: trang web điều khiển cập nhật tức thời, điều khiển relay bằng ứng dụng Blynk IoT.
Xem thêm