OTA-обновление ESP32 через веб-интерфейс (Web Updater) в Arduino IDE
Одна из лучших особенностей ESP32 заключается в том, что его прошивку можно обновлять по беспроводной сети. Такой вид программирования называется «по воздуху» (Over-The-Air, OTA).
Что такое OTA-программирование в ESP32?
OTA-программирование позволяет обновлять/загружать новую программу на ESP32 через Wi-Fi без необходимости подключать ESP32 к компьютеру через USB.
Функциональность OTA особенно полезна, когда отсутствует физический доступ к модулю ESP. Кроме того, она сокращает время, необходимое для обновления каждого модуля ESP во время обслуживания.
Одно из ключевых преимуществ OTA заключается в том, что из одного центрального места можно отправить обновление на несколько ESP-модулей в одной сети.
Единственный недостаток — вы должны включать OTA-код в каждый скетч, который загружаете, чтобы иметь возможность использовать OTA при следующем обновлении.
Способы реализации OTA в ESP32
Существует два способа реализации OTA-функциональности в ESP32.
Базовое OTA — обновления доставляются с помощью Arduino IDE.
Веб-обновление OTA (Web Updater) — обновления доставляются через веб-браузер.
У каждого способа есть свои преимущества, поэтому вы можете использовать тот, который лучше всего подходит для вашего проекта.
В этом руководстве мы рассмотрим процесс реализации OTA через веб-обновление (Web Updater). Если вас интересует базовое OTA, пожалуйста, ознакомьтесь с руководством ниже.
Представьте, что у вас несколько модулей ESP32 разбросаны по всему дому. Каждый раз вытаскивать их и подключать кабель для обновления программы…
3 простых шага для использования Web Updater OTA с ESP32
Загрузка OTA-программы через последовательный порт: Первый шаг — загрузить скетч, содержащий OTA-прошивку, через последовательный порт. Это обязательный шаг для выполнения последующих обновлений по воздуху.
Доступ к веб-серверу: OTA-скетч создаёт веб-сервер в режиме STA, доступный через веб-браузер. После входа в веб-сервер вы можете загружать новые скетчи.
Загрузка нового скетча по воздуху: Теперь вы можете загружать новые скетчи на ESP32, генерируя и загружая скомпилированный .bin файл через веб-сервер.
Шаг 1: Загрузка OTA-программы через последовательный порт
Поскольку заводской образ ESP32 не обладает возможностью OTA-обновления, сначала необходимо загрузить OTA-прошивку на ESP32 через последовательный интерфейс.
Обязательно сначала обновите прошивку, чтобы в дальнейшем выполнять обновления по воздуху.
Дополнение ESP32 для Arduino IDE включает библиотеку OTA, а также пример OTAWebUpdater. Просто перейдите в File > Examples > ArduinoOTA > OTAWebUpdater.
Пользовательский интерфейс OTA Web Updater выглядит крайне непривлекательно. Поэтому мы доработали код, чтобы улучшить его внешний вид. Для начала подключите ESP32 к компьютеру и загрузите приведённый ниже скетч.
Перед загрузкой скетча необходимо изменить следующие две переменные, указав учётные данные вашей сети, чтобы ESP32 мог подключиться к существующей сети.
const char* ssid = "---";
const char* password = "----";
Когда всё готово, загружайте скетч.
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
const char* host = "esp32";
const char* ssid = "---";
const char* password = "----";
WebServer server(80);
/* Style */
String style =
"<style>#file-input,input{width:100%;height:44px;border-radius:4px;margin:10px auto;font-size:15px}"
"input{background:#f1f1f1;border:0;padding:0 15px}body{background:#3498db;font-family:sans-serif;font-size:14px;color:#777}"
"#file-input{padding:0;border:1px solid #ddd;line-height:44px;text-align:left;display:block;cursor:pointer}"
"#bar,#prgbar{background-color:#f1f1f1;border-radius:10px}#bar{background-color:#3498db;width:0%;height:10px}"
"form{background:#fff;max-width:258px;margin:75px auto;padding:30px;border-radius:5px;text-align:center}"
".btn{background:#3498db;color:#fff;cursor:pointer}</style>";
/* Login page */
String loginIndex =
"<form name=loginForm>"
"<h1>ESP32 Login</h1>"
"<input name=userid placeholder='User ID'> "
"<input name=pwd placeholder=Password type=Password> "
"<input type=submit onclick=check(this.form) class=btn value=Login></form>"
"<script>"
"function check(form) {"
"if(form.userid.value=='admin' && form.pwd.value=='admin')"
"{window.open('/serverIndex')}"
"else"
"{alert('Error Password or Username')}"
"}"
"</script>" + style;
/* Server Index Page */
String serverIndex =
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update' id='file' onchange='sub(this)' style=display:none>"
"<label id='file-input' for='file'> Choose file...</label>"
"<input type='submit' class=btn value='Update'>"
"<br><br>"
"<div id='prg'></div>"
"<br><div id='prgbar'><div id='bar'></div></div><br></form>"
"<script>"
"function sub(obj){"
"var fileName = obj.value.split('\\\\');"
"document.getElementById('file-input').innerHTML = ' '+ fileName[fileName.length-1];"
"};"
"$('form').submit(function(e){"
"e.preventDefault();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
"$.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {"
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;"
"$('#prg').html('progress: ' + Math.round(per*100) + '%');"
"$('#bar').css('width',Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!') "
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>" + style;
/* setup function */
void setup(void) {
Serial.begin(115200);
// Connect to WiFi network
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
/*use mdns for host name resolution*/
if (!MDNS.begin(host)) { //http://esp32.local
Serial.println("Error setting up MDNS responder!");
while (1) {
delay(1000);
}
}
Serial.println("mDNS responder started");
/*return index page which is stored in serverIndex */
server.on("/", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", loginIndex);
});
server.on("/serverIndex", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", serverIndex);
});
/*handling uploading firmware file */
server.on("/update", HTTP_POST, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
/* flashing firmware to ESP*/
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) { //true to set the size to the current progress
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
} else {
Update.printError(Serial);
}
}
});
server.begin();
}
void loop(void) {
server.handleClient();
delay(1);
}
Шаг 2: Доступ к веб-серверу
Скетч OTA Web Updater создаёт веб-сервер в режиме STA, к которому можно обратиться через веб-браузер и использовать для загрузки новых скетчей на ESP32 по воздуху.
Чтобы получить доступ к веб-серверу, откройте монитор последовательного порта на скорости 115200 бод и нажмите кнопку EN на ESP32. Если всё в порядке, отобразится динамический IP-адрес, полученный от вашего маршрутизатора.
Затем откройте браузер и перейдите по IP-адресу, отображённому в мониторе последовательного порта. ESP32 должен показать веб-страницу с запросом данных для входа.
Введите следующий User ID и пароль:
User ID: admin
Password: admin
Если вы хотите изменить User ID и пароль, отредактируйте следующий код в вашем скетче.
"if(form.userid.value=='admin' && form.pwd.value=='admin')"
После входа вы будете перенаправлены на страницу /serverIndex. Эта страница позволяет загружать новые скетчи на ESP32 по воздуху.
Обратите внимание, что новый скетч, который вы хотите загрузить, должен быть в формате .bin (скомпилированный двоичный файл). Как сгенерировать .bin файл вашего скетча, вы узнаете на следующем шаге.
Предупреждение
Страница /serverIndex не защищена функцией входа. Этот недостаток может позволить пользователям получить доступ к системе без авторизации.
Шаг 3: Загрузка нового скетча по воздуху
Теперь давайте загрузим новый скетч по воздуху.
Помните, что вы должны включать OTA-код в каждый скетч, который загружаете. В противном случае вы потеряете возможность OTA и не сможете выполнить следующую загрузку по воздуху. Поэтому рекомендуется модифицировать предыдущий код, включив в него ваш новый код.
В качестве примера мы включим простой скетч Blink в код OTA Web Updater. Не забудьте изменить переменные SSID и password, указав учётные данные вашей сети.
Изменения в программе Web Updater OTA выделены синим цветом.
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
const char* host = "esp32";
const char* ssid = "---";
const char* password = "----";
//variabls for blinking an LED with Millis
const int led = 2; // ESP32 Pin to which onboard LED is connected
unsigned long previousMillis = 0; // will store last time LED was updated
const long interval = 1000; // interval at which to blink (milliseconds)
int ledState = LOW; // ledState used to set the LED
WebServer server(80);
/* Style */
String style =
"<style>#file-input,input{width:100%;height:44px;border-radius:4px;margin:10px auto;font-size:15px}"
"input{background:#f1f1f1;border:0;padding:0 15px}body{background:#3498db;font-family:sans-serif;font-size:14px;color:#777}"
"#file-input{padding:0;border:1px solid #ddd;line-height:44px;text-align:left;display:block;cursor:pointer}"
"#bar,#prgbar{background-color:#f1f1f1;border-radius:10px}#bar{background-color:#3498db;width:0%;height:10px}"
"form{background:#fff;max-width:258px;margin:75px auto;padding:30px;border-radius:5px;text-align:center}"
".btn{background:#3498db;color:#fff;cursor:pointer}</style>";
/* Login page */
String loginIndex =
"<form name=loginForm>"
"<h1>ESP32 Login</h1>"
"<input name=userid placeholder='User ID'> "
"<input name=pwd placeholder=Password type=Password> "
"<input type=submit onclick=check(this.form) class=btn value=Login></form>"
"<script>"
"function check(form) {"
"if(form.userid.value=='admin' && form.pwd.value=='admin')"
"{window.open('/serverIndex')}"
"else"
"{alert('Error Password or Username')}"
"}"
"</script>" + style;
/* Server Index Page */
String serverIndex =
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update' id='file' onchange='sub(this)' style=display:none>"
"<label id='file-input' for='file'> Choose file...</label>"
"<input type='submit' class=btn value='Update'>"
"<br><br>"
"<div id='prg'></div>"
"<br><div id='prgbar'><div id='bar'></div></div><br></form>"
"<script>"
"function sub(obj){"
"var fileName = obj.value.split('\\\\');"
"document.getElementById('file-input').innerHTML = ' '+ fileName[fileName.length-1];"
"};"
"$('form').submit(function(e){"
"e.preventDefault();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
"$.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {"
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;"
"$('#prg').html('progress: ' + Math.round(per*100) + '%');"
"$('#bar').css('width',Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!') "
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>" + style;
/* setup function */
void setup(void) {
pinMode(led, OUTPUT);
Serial.begin(115200);
// Connect to WiFi network
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
/*use mdns for host name resolution*/
if (!MDNS.begin(host)) { //http://esp32.local
Serial.println("Error setting up MDNS responder!");
while (1) {
delay(1000);
}
}
Serial.println("mDNS responder started");
/*return index page which is stored in serverIndex */
server.on("/", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", loginIndex);
});
server.on("/serverIndex", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", serverIndex);
});
/*handling uploading firmware file */
server.on("/update", HTTP_POST, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
/* flashing firmware to ESP*/
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) { //true to set the size to the current progress
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
} else {
Update.printError(Serial);
}
}
});
server.begin();
}
void loop(void) {
server.handleClient();
delay(1);
//loop to blink without delay
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
// save the last time you blinked the LED
previousMillis = currentMillis;
// if the LED is off turn it on and vice-versa:
ledState = not(ledState);
// set the LED with the ledState of the variable:
digitalWrite(led, ledState);
}
}
Примечание
Обратите внимание, что мы не использовали функцию delay() для мигания светодиодом. Это связано с тем, что функция delay() приостанавливает программу. Если следующий OTA-запрос будет сгенерирован, пока ESP32 приостановлен в ожидании завершения delay(), ваша программа пропустит этот запрос.
Генерация .bin файла в Arduino IDE
Чтобы загрузить новый скетч на ESP32, необходимо сначала сгенерировать скомпилированный двоичный файл .bin вашего скетча.
Для этого перейдите в Sketch > Export compiled Binary.
После успешной компиляции скетча файл .bin будет сгенерирован в папке скетча. Чтобы открыть папку скетча, выберите Sketch > Show Sketch Folder.
Загрузка нового скетча по воздуху на ESP32
После генерации файла .bin новый скетч можно загрузить по воздуху на ESP32.
Откройте браузер и перейдите на страницу /serverIndex. Нажмите Choose File… Выберите только что сгенерированный файл .bin и нажмите Update.
Новый скетч будет загружен за считанные секунды.
И встроенный светодиод должен начать мигать.