OTA-обновление ESP32: прошивка по Wi-Fi без кабеля
Пока плата лежит на столе, кабель не мешает. Как только она оказывается в корпусе под потолком или внутри робота, каждое исправление превращается в разборку. OTA — Over The Air — позволяет заливать прошивку по Wi-Fi. Разбираем три способа и главное требование, без которого ни один из них не заработает.
Примеры проверены на пакете плат esp32 by Espressif Systems 3.x.
Сначала — схема разделов
Обновление по воздуху устроено так: новая прошивка пишется во второй раздел приложения, и только после успешной записи загрузчик переключается на него. Значит, разделов приложения должно быть два.
Откройте Tools → Partition Scheme и убедитесь, что выбранная схема поддерживает OTA:
| Схема | Приложение | OTA |
|---|---|---|
| Default 4MB with spiffs | 2 × 1,25 МБ | Да |
| Minimal SPIFFS (1.9MB APP) | 2 × 1,875 МБ | Да |
| No FS 4MB (2MB APP x2) | 2 × 2 МБ | Да |
| No OTA (2MB APP/2MB SPIFFS) | 2 МБ | Нет |
| Huge APP (3MB No OTA/1MB SPIFFS) | 3 МБ | Нет |
Со схемой «Huge APP» или любой «No OTA» скетч скомпилируется и зальётся по кабелю
как обычно, а вот попытка обновиться по воздуху упадёт уже во время работы: второго
раздела нет, и Update.begin() вернёт ошибку No suitable partition found. Это
частая и неочевидная причина «OTA не работает».
Отсюда же следует, что доступный размер прошивки равен размеру одного раздела, а не их сумме. Если скетч не влезает в 1,25 МБ схемы по умолчанию, берите Minimal SPIFFS — 1,875 МБ на приложение ценой урезанной до 128 КБ файловой системы. Это последний вариант, где OTA ещё есть. Подробнее про разделы — в статье Хранение данных на ESP32.
Способ 1: ArduinoOTA — заливка прямо из среды
Самый удобный вариант для разработки: плата появляется в списке портов Arduino IDE как сетевой порт, и заливка идёт привычной кнопкой Upload.
#include <WiFi.h>
#include <ESPmDNS.h>
#include <NetworkUdp.h>
#include <ArduinoOTA.h>
const char* ssid = "MyNetwork";
const char* password = "MyPassword";
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("Не удалось подключиться, перезагрузка");
delay(5000);
ESP.restart();
}
ArduinoOTA.setHostname("roboarm");
ArduinoOTA.setPassword("secret");
ArduinoOTA
.onStart([]() {
Serial.println("Начало обновления");
})
.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Прогресс: %u%%\n", progress / (total / 100));
})
.onEnd([]() {
Serial.println("\nГотово");
})
.onError([](ota_error_t error) {
Serial.printf("Ошибка [%u]\n", error);
});
ArduinoOTA.begin();
Serial.println(WiFi.localIP());
}
void loop() {
ArduinoOTA.handle(); // обязательно вызывать в каждом проходе
}
После первой заливки по кабелю плата появится в Tools → Port в разделе сетевых
портов под именем, заданным в setHostname(). Дальше обновляйте как обычно.
В ядре 3.x подключается NetworkUdp.h, а не WiFiUdp.h, как было в версии 2.x. Если
переносите старый скетч и получаете ошибку о ненайденном заголовке — дело именно в
этом.
Вызов ArduinoOTA.handle() должен выполняться регулярно. Если loop() надолго
блокируется, плата перестанет отвечать на запросы обновления — ещё одна причина
писать неблокирующий код.
Способ 2: загрузка через веб-страницу
Когда устройство уже у пользователя, среда разработки недоступна. Тогда прошивку загружают через браузер: страница с полем выбора файла и кнопкой.
Писать это руками не нужно — в ядре есть готовая библиотека, которая навешивает обработчик на существующий веб-сервер:
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPUpdateServer.h>
WebServer server(80);
HTTPUpdateServer updateServer;
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin("MyNetwork", "MyPassword");
while (WiFi.status() != WL_CONNECTED) delay(500);
server.on("/", []() {
server.send(200, "text/html", "<a href='/update'>Обновить прошивку</a>");
});
updateServer.setup(&server, "/update", "admin", "secret");
server.begin();
Serial.print("Откройте http://");
Serial.print(WiFi.localIP());
Serial.println("/update");
}
void loop() {
server.handleClient();
}
Готовый файл прошивки берётся из Arduino IDE: Sketch → Export Compiled Binary,
затем ищите .bin в папке скетча.
Логин и пароль в setup() — не формальность: без них любой в той же сети сможет
залить на устройство что угодно.
Способ 3: обновление с сервера
Для парка устройств удобнее, когда плата сама проверяет наличие новой версии и скачивает её:
#include <WiFi.h>
#include <HTTPUpdate.h>
void checkForUpdate() {
NetworkClient client;
httpUpdate.onProgress([](int current, int total) {
Serial.printf("Загружено %d из %d\n", current, total);
});
t_httpUpdate_return result =
httpUpdate.update(client, "http://example.com/firmware/robot.bin");
switch (result) {
case HTTP_UPDATE_OK:
Serial.println("Обновлено, перезагрузка");
break;
case HTTP_UPDATE_NO_UPDATES:
Serial.println("Обновлений нет");
break;
case HTTP_UPDATE_FAILED:
Serial.printf("Ошибка (%d): %s\n",
httpUpdate.getLastError(),
httpUpdate.getLastErrorString().c_str());
break;
}
}
Обратите внимание на имена: класс называется HTTPUpdate, готовый экземпляр —
httpUpdate со строчной буквы. В экосистеме ESP8266 он назывался иначе
(ESPhttpUpdate), и примеры оттуда не подойдут.
По умолчанию после успешного обновления плата перезагружается сама. Отключается это
вызовом httpUpdate.rebootOnUpdate(false).
Откат на рабочую версию
Что если новая прошивка окажется нерабочей и устройство перестанет выходить в сеть? На этот случай есть механизм отката: свежезалитый образ считается непроверенным, и если он не подтвердит свою работоспособность до перезагрузки, загрузчик вернётся на предыдущий.
В Arduino этот механизм уже включён, и ядро подтверждает образ автоматически ещё
до вызова setup(). Специально ничего делать не нужно — базовая защита работает
из коробки.
Но автоматическое подтверждение происходит слишком рано: прошивка объявляется рабочей до того, как выяснится, подключается ли она к сети. Чтобы отложить решение, ядро предлагает переопределить две слабые функции:
#include <esp_ota_ops.h>
// откладываем автоматическое подтверждение
bool verifyRollbackLater() {
return true;
}
void setup() {
Serial.begin(115200);
WiFi.begin("MyNetwork", "MyPassword");
if (WiFi.waitForConnectResult() == WL_CONNECTED) {
esp_ota_mark_app_valid_cancel_rollback(); // самопроверка пройдена
Serial.println("Прошивка подтверждена");
} else {
esp_ota_mark_app_invalid_rollback_and_reboot(); // откат, не возвращается
}
}
Теперь прошивка, которая не смогла подключиться к сети, автоматически откатится на предыдущую рабочую версию. Для устройства в труднодоступном месте это разница между поездкой с ноутбуком и отсутствием проблемы.
Безопасность
OTA — это удалённая замена кода на вашем устройстве. Примеры выше рассчитаны на доверенную домашнюю сеть; для устройства, доступного из интернета, их недостаточно.
MD5 — это не защита. Методы setMD5sum() у httpUpdate и setMD5() у Update
ловят повреждение файла при передаче, и только. Против злоумышленника они бесполезны:
раз прошивка идёт по обычному HTTP, подменить вместе с ней и контрольную сумму —
задача того же уровня сложности. То же с HTTP Basic Auth: пароль передаётся открытым
текстом и перехватывается вместе со всем остальным.
Что действительно защищает:
- HTTPS с проверкой сертификата вместо HTTP — тогда подменить содержимое по пути
нельзя. Используйте
NetworkClientSecureс закреплённым корневым сертификатом. - Подпись образа. ESP-IDF поддерживает проверку цифровой подписи прошивки перед установкой, а Secure Boot — ещё и на уровне загрузчика. Это единственный механизм, который защищает, даже если канал скомпрометирован.
- Пароль в любом случае, и у
ArduinoOTA, и у веб-интерфейса — он отсекает случайный доступ соседа по сети. - Не оставляйте OTA включённым «на всякий случай» в устройстве, которое не планируется обновлять.
Простое правило: если устройство доступно только из домашней сети и обновляете его вы — примеров выше достаточно. Если оно стоит у заказчика или смотрит в интернет, поднимайте HTTPS и подпись образа, иначе OTA превращается из удобства в способ залить на устройство чужой код.
Частые проблемы
Плата не появляется в списке сетевых портов. Проверьте, что компьютер и плата в
одной сети, что вызывается ArduinoOTA.handle() в loop(), и что mDNS работает —
в некоторых сетях он блокируется, тогда вместо имени указывайте IP-адрес вручную.
Обновление начинается и обрывается. Чаще всего это loop(), который блокируется
надолго. Или нехватка места: прошивка должна поместиться в один раздел.
No suitable partition found. Выбрана схема разделов без OTA, см. начало статьи.
После обновления плата перезагружается по кругу. Новая прошивка нерабочая — именно для этого случая нужен механизм отката выше.
Что дальше
- Схемы разделов подробно — Хранение данных на ESP32.
- Веб-интерфейс, к которому крепится страница обновления — Веб-сервер на ESP32.
- Надёжное подключение к сети — Подключение ESP32 к Wi-Fi.