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

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

Lưu đồ khởi động: có cấu hình trong bộ nhớ thì thử vào Wi-Fi đã lưu tối đa 20 giây, vào được thì chạy chương trình chính; chưa có hoặc không vào được thì sang chế độ cài đặt phát Wi-Fi PYWORLD, bấm Lưu thì lưu vào Preferences và khởi động lại
ESP32 chọn chế độ chạy hay chế độ cài đặt khi khởi động (hình minh họa).
  • 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ị

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 += "&lt;";
    else if (c == '>') r += "&gt;";
    else if (c == '&') r += "&amp;";
    else if (c == '"') r += "&quot;";
    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

Bốn màn hình điện thoại: chọn mạng PYWORLD-B2C3 trong danh sách Wi-Fi, bấm thông báo đăng nhập vào mạng, trang Chọn Wi-Fi cho ESP32 có danh sách mạng và ô mật khẩu, trang Đã lưu
Bốn bước trên điện thoại (hình minh họa, giao diện điện thoại vẽ kiểu chung).
  1. 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ẩu pyworld123.
  2. Đ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ữ.
  3. 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.
  4. 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

Bảng vùng nhớ wifi trong Preferences gồm khóa ssid và pass; ghi chú NVS còn nguyên khi mất điện; hai dòng dns.start và server.onNotFound tạo trang tự bật lên
Nơi lưu cấu hình và hai dòng làm trang cài đặt tự bật lên.

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ả

Serial Monitor: Chua co cau hinh Wi-Fi, Tim thay 7 mang, che do cai dat vao Wi-Fi PYWORLD-B2C3, Da luu Wi-Fi NhaTang2; sau khi khởi động lại vào Wi-Fi được IP, giữ BOOT xóa cấu hình
Serial Monitor ở tốc độ 115200, lần đầu và sau khi đã lưu (số liệu ví dụ).

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ượngNguyên nhân thường gặpCách xử lý
Không thấy mạng cài đặt PYWORLDESP32 đã 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ìnhModem chỉ phát 5 GHz, ở xa, hoặc mạng ẩn tênBấ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 raSai mật khẩu Wi-FiVà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 đặtESP32 khởi động nhanh hơn modemChờ: 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ạpTắt Tools > Erase All Flash Before Sketch Upload

9. Xem thêm


Tin tức liên quan

Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay
Phát hiện chuyển động bằng cảm biến PIR HC-SR501 với ESP32: tự bật đèn qua relay

Hướng dẫn dùng cảm biến chuyển động PIR HC-SR501 với ESP32: nguyên lý, cách chỉnh module, đọc tín hiệu bằng ngắt và ghép relay làm đèn tự bật khi có người, tự tắt sau 60 giây.

Tự làm máy vẽ (pen plotter) 2 trục bằng ESP32 và động cơ bước, tự viết bộ đọc G-code
Tự làm máy vẽ (pen plotter) 2 trục bằng ESP32 và động cơ bước, tự viết bộ đọc G-code

Tự làm máy vẽ 2 trục với ESP32, hai A4988, NEMA 17 và servo nhấc bút; chương trình tự đọc G-code G0 G1 G2 G3, nội suy Bresenham có tăng tốc, script Python gửi file G-code.

Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano
Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano

Hướng dẫn nối màn hình OLED 0.96 inch SSD1306 (I2C) với Arduino Nano, cài thư viện Adafruit, hiện chữ nhiều cỡ, vẽ thanh mức theo điện áp và lưu ý về bộ nhớ RAM.

GPS NEO-6M với ESP32: đọc câu NMEA, lấy tọa độ, giờ Việt Nam, tốc độ không cần thư viện
GPS NEO-6M với ESP32: đọc câu NMEA, lấy tọa độ, giờ Việt Nam, tốc độ không cần thư viện

GPS làm việc thế nào, module NEO-6M, nối với ESP32 qua UART2, đọc câu NMEA RMC và GGA, kiểm checksum, đổi tọa độ, giờ Việt Nam, tốc độ, tính khoảng cách, không cần TinyGPS.

Đo dòng điện với ESP32: INA219 hay ACS712, đo dòng, điện áp, công suất và mAh không cần thư viện
Đo dòng điện với ESP32: INA219 hay ACS712, đo dòng, điện áp, công suất và mAh không cần thư viện

So sánh INA219 (điện trở shunt, I2C) và ACS712 (cảm biến Hall), nối với ESP32 có cầu phân áp, chương trình đo dòng, điện áp, công suất, điện lượng mAh không cần thư viện, kèm bảng lỗi.

ESP32 điều khiển động cơ DC với driver L298N: đảo chiều, chỉnh tốc độ bằng PWM
ESP32 điều khiển động cơ DC với driver L298N: đảo chiều, chỉnh tốc độ bằng PWM

Hướng dẫn nối ESP32 với driver L298N và động cơ DC giảm tốc: cầu H, đảo chiều, phanh, chỉnh tốc độ bằng PWM, điều khiển bằng lệnh trên Serial Monitor và thí nghiệm STEM.

Hướng dẫn dùng PWM trên ESP32: điều chỉnh độ sáng LED bằng analogWrite và LEDC
Hướng dẫn dùng PWM trên ESP32: điều chỉnh độ sáng LED bằng analogWrite và LEDC

Hướng dẫn tạo PWM trên ESP32 bằng Arduino IDE: analogWrite, ledcAttach và ledcWrite của gói esp32 3.x, chỉnh độ sáng LED bằng biến trở, chọn tần số và độ phân giải.

Hướng dẫn dùng module thẻ nhớ Micro SD với ESP32
Hướng dẫn dùng module thẻ nhớ Micro SD với ESP32

Hướng dẫn nối module thẻ nhớ Micro SD với ESP32 DevKit V1 qua SPI, định dạng thẻ FAT32, chạy code Arduino đọc ghi file, ghi dữ liệu định kỳ ra CSV và xử lý lỗi Card Mount Failed.


Bình luận
Đã thêm vào giỏ hàng