
ESP32 OTA qua mạng LAN (ArduinoOTA)
Nạp firmware cho ESP32 qua WiFi trong cùng mạng LAN bằng thư viện ArduinoOTA có sẵn, không cần cắm cáp USB, không cần máy chủ cloud hay Internet.
OTA (Over-The-Air) là kỹ thuật nạp firmware không dây, rất hữu ích khi ESP32 đã lắp đặt cố định, khó tiếp cận cổng USB.
Bài này dùng thư viện ArduinoOTA có sẵn trong ESP32 Arduino core để nạp code qua mạng LAN nội bộ: máy tính và ESP32 kết nối chung một WiFi, Arduino IDE/PlatformIO tự phát hiện ESP32 như một "Network Port" nhờ mDNS và nạp trực tiếp — không cần máy chủ OTA riêng, không cần Internet.
Đây là điểm khác biệt quan trọng so với project "ESP32 Cloud OTA" đã có trên maker.iotlabs.vn (project đó nạp firmware qua máy chủ trên Internet, dùng khi thiết bị ở xa, không cùng mạng LAN với người phát triển). Bài học cũng nhấn mạnh việc đặt mật khẩu OTA và giới hạn dùng trong mạng tin cậy khi phát triển/demo.
Hướng dẫn chi tiết
Nạp firmware ESP32 qua WiFi trong cùng mạng LAN bằng ArduinoOTA — khác với ESP32 Cloud OTA (qua Internet). 3 code mẫu compile pass qua arduino-cli.
1. Giới thiệu
OTA (Over-The-Air) là kỹ thuật nạp firmware không dây cho vi điều khiển, rất hữu ích khi ESP32 đã lắp đặt cố định trong hộp kín hoặc khó tiếp cận cổng USB. Bài này dùng thư viện ArduinoOTA có sẵn trong ESP32 Arduino core (không cần cài thêm) để nạp firmware qua mạng LAN nội bộ: máy tính và ESP32 kết nối chung một mạng WiFi, Arduino IDE/PlatformIO tự phát hiện ESP32 như một "Network Port" nhờ giao thức mDNS và nạp trực tiếp qua giao thức espota — không cần máy chủ OTA riêng, không cần Internet.
Khác biệt quan trọng với project "ESP32 Cloud OTA" đã có trên maker.iotlabs.vn: project đó nạp firmware qua một máy chủ trên Internet (thiết bị tự tải bản cập nhật từ URL từ xa), phù hợp khi thiết bị đã triển khai ở hiện trường, không cùng mạng LAN với người phát triển. Bài này ngược lại — chỉ hoạt động khi PC và ESP32 cùng một mạng LAN/WiFi, phù hợp giai đoạn phát triển/debug tại chỗ, không cần hạ tầng máy chủ.
Lưu ý an toàn: ArduinoOTA mặc định không có xác thực mạnh — bài này luôn đặt ArduinoOTA.setPassword(), và chỉ nên dùng trong mạng LAN tin cậy khi phát triển/demo, không dùng cho sản phẩm thương mại public tiếp xúc mạng không tin cậy.
2. Linh kiện cần có
Linh kiện | Số lượng | Ghi chú |
|---|---|---|
ESP32 DevKit V4 (board_esp32_devkitc) | 1 | Board duy nhất cần có — WiFi đã tích hợp sẵn, không cần module ngoài |
3. Sơ đồ kết nối
Bài demo board-only: không có module ngoài, không cần đấu dây. ESP32 DevKit chỉ cần cấp nguồn (USB lần nạp đầu tiên) và kết nối WiFi — các lần nạp OTA sau đó hoàn toàn không dây.
4. Code ví dụ #1 — ArduinoOTA đầy đủ với retry WiFi và heartbeat
Kết nối WiFi có timeout + tự động thử lại định kỳ (không dùng while vô hạn chặn chương trình), cấu hình đầy đủ callback OTA (onStart/onEnd/onProgress/onError), và log heartbeat mỗi 5 giây để biết thiết bị còn sống trong lúc chờ OTA.
/*
ESP32 OTA qua mang LAN (ArduinoOTA)
Board: ESP32 DevKit V4 (esp32:esp32:esp32)
Demo nap firmware qua WiFi trong CUNG mang LAN, dung thu vien
ArduinoOTA (co san trong ESP32 Arduino core - khong can cai them).
Khac voi "ESP32 Cloud OTA" (nap qua Internet/may chu cloud):
o day PC va ESP32 phai CUNG mot mang WiFi noi bo, Arduino IDE/
PlatformIO se thay ESP32 nhu mot "Network Port" va nap truc tiep,
khong can may chu OTA rieng, khong can Internet.
LUU Y AN TOAN: ArduinoOTA mac dinh khong yeu cau xac thuc manh -
nen dat ArduinoOTA.setPassword() (da lam ben duoi) va chi dung
trong mang LAN tin cay khi phat trien/demo, khong dung cho san
pham thuong mai public.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <ArduinoOTA.h>
// TODO: thay bang thong tin WiFi that cua ban truoc khi nap.
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* OTA_HOSTNAME = "esp32-lan-ota-demo";
const char* OTA_PASSWORD = "iotlabs-ota-2026"; // doi mat khau nay khi dung that
const uint32_t WIFI_CONNECT_TIMEOUT_MS = 15000;
const uint32_t WIFI_RETRY_INTERVAL_MS = 10000;
unsigned long lastWifiRetryAt = 0;
bool connectWiFi() {
Serial.printf("Dang ket noi WiFi SSID \"%s\"...\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < WIFI_CONNECT_TIMEOUT_MS) {
delay(300);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("WiFi da ket noi. IP: %s\n", WiFi.localIP().toString().c_str());
return true;
}
Serial.println("[WARN] Ket noi WiFi that bai (timeout) - se thu lai dinh ky trong loop().");
return false;
}
void setupOTA() {
ArduinoOTA.setHostname(OTA_HOSTNAME);
ArduinoOTA.setPassword(OTA_PASSWORD);
ArduinoOTA
.onStart([]() {
String type = (ArduinoOTA.getCommand() == U_FLASH) ? "sketch" : "filesystem";
Serial.println("[OTA] Bat dau cap nhat: " + type);
})
.onEnd([]() {
Serial.println("\n[OTA] Cap nhat hoan tat, chuan bi reboot...");
})
.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("[OTA] Tien do: %u%%\r", (progress / (total / 100)));
})
.onError([](ota_error_t error) {
Serial.printf("[OTA] Loi [%u]: ", error);
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
});
ArduinoOTA.begin();
Serial.printf("[OTA] San sang. Hostname: %s.local (mDNS)\n", OTA_HOSTNAME);
}
void setup() {
Serial.begin(115200);
delay(200);
Serial.println();
Serial.println("=== ESP32 LAN OTA (ArduinoOTA) Demo boot ===");
if (connectWiFi()) {
setupOTA();
}
}
void loop() {
if (WiFi.status() == WL_CONNECTED) {
// ArduinoOTA.handle() phai duoc goi lien tuc, khong dung delay() dai
// trong loop() de khong bo lo yeu cau OTA tu may tinh.
ArduinoOTA.handle();
} else {
// Mat WiFi -> thu ket noi lai dinh ky, KHONG while() vo han chan chuong trinh.
unsigned long now = millis();
if (now - lastWifiRetryAt >= WIFI_RETRY_INTERVAL_MS) {
lastWifiRetryAt = now;
Serial.println("[WiFi] Mat ket noi, thu ket noi lai...");
if (connectWiFi()) {
setupOTA();
}
}
}
// Telemetry chu ky de biet thiet bi van song trong luc cho OTA.
static unsigned long lastHeartbeatAt = 0;
if (millis() - lastHeartbeatAt >= 5000) {
lastHeartbeatAt = millis();
Serial.printf("[HEARTBEAT] uptime=%lus wifi=%s ip=%s\n",
millis() / 1000,
WiFi.status() == WL_CONNECTED ? "connected" : "disconnected",
WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString().c_str() : "-");
}
}
5. Code ví dụ #2 — Cấu hình ArduinoOTA tối giản
Bản rút gọn chỉ giữ lại phần bắt buộc để bật OTA qua LAN — phù hợp khi đã quen luồng WiFi.begin() và chỉ cần nhớ "công thức" bật ArduinoOTA nhanh cho demo.
/*
ESP32 LAN OTA - Vi du 2: Cau hinh ArduinoOTA toi gian
Board: ESP32 DevKit V4
Ban rut gon cua vi du 1 - chi giu lai phan bat buoc de bat OTA
qua LAN, phu hop khi ban da quen WiFi.begin() va chi can nho
"cong thuc" ArduinoOTA. Dung cho du an demo nhanh, khong co
retry/heartbeat nhu vi du 1 (khuyen nghi dung ban day du trong
du an thuc te).
*/
#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoOTA.h>
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Dang ket noi WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nIP: %s\n", WiFi.localIP().toString().c_str());
// Cau hinh toi gian: hostname + password, khong can callback chi tiet.
ArduinoOTA.setHostname("esp32-lan-ota-minimal");
ArduinoOTA.setPassword("iotlabs-ota-2026");
ArduinoOTA.begin();
Serial.println("ArduinoOTA san sang - vao Arduino IDE > Tools > Port de chon port mang.");
}
void loop() {
ArduinoOTA.handle();
}
6. Code ví dụ #3 — Cấu hình PlatformIO để Upload qua mạng (espota)
Với PlatformIO, đổi upload_protocol sang espota và trỏ upload_port vào địa chỉ IP của ESP32 trên LAN (in ra Serial ở lần nạp USB đầu tiên) để nút Upload gửi firmware qua WiFi thay vì USB.
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
; --- Nap qua USB (mac dinh) ---
; upload_protocol = esptool
; --- Nap qua mang LAN bang ArduinoOTA (espota) ---
; Sau khi da nap 1 lan qua USB voi firmware co ArduinoOTA.begin(),
; doi upload_protocol + upload_port sang IP cua ESP32 tren LAN
; roi bam Upload nhu binh thuong - PlatformIO se nap qua WiFi.
upload_protocol = espota
upload_port = 192.168.1.50 ; thay bang IP thuc te cua ESP32
upload_flags =
--port=3232
--auth=iotlabs-ota-2026 ; phai khop OTA_PASSWORD trong code
7. Lỗi thường gặp
Lỗi | Nguyên nhân | Cách sửa |
|---|---|---|
Arduino IDE không thấy ESP32 xuất hiện ở mục Port (Network) | PC và ESP32 không cùng mạng WiFi/LAN, hoặc mDNS bị chặn bởi router/firewall/VLAN cô lập thiết bị | Đảm bảo PC và ESP32 kết nối cùng 1 router/SSID, tắt tính năng cô lập client (AP/client isolation) trên router nếu có |
OTA báo lỗi | Mật khẩu đặt trong | Đồng bộ đúng 1 mật khẩu ở cả code và công cụ nạp; đổi mật khẩu mặc định trong bài trước khi dùng thật |
Nạp OTA lần đầu luôn thất bại | ArduinoOTA chỉ hoạt động khi ESP32 đã có sẵn firmware chứa | Luôn nạp lần đầu tiên qua cáp USB với firmware đã tích hợp ArduinoOTA, các lần sau mới nạp OTA được |
ESP32 "treo", không nhận OTA dù vẫn thấy trên mạng (ping được) | Trong | Không dùng |
8. Tổng kết
ArduinoOTA giúp nạp firmware cho ESP32 hoàn toàn qua WiFi trong cùng mạng LAN, không cần cáp USB và không cần hạ tầng máy chủ, khác với OTA qua Cloud (dùng khi thiết bị ở xa, không cùng mạng người phát triển).
Ba điểm cốt lõi:
(1) firmware đầu tiên bắt buộc phải nạp qua USB để "gieo mầm" ArduinoOTA
(2) luôn gọi
ArduinoOTA.handle()đều đặn trongloop()không bị chặn3) luôn đặt mật khẩu OTA và chỉ dùng trong mạng tin cậy.
Bước tiếp theo tự nhiên: kết hợp với các project ESP32 khác (cảm biến, actuator) để có thể cập nhật logic điều khiển từ xa mà không cần tháo thiết bị ra khỏi vị trí lắp đặt.