OTA-обновление ESP32 через веб-интерфейс (Web Updater) в Arduino IDE

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, пожалуйста, ознакомьтесь с руководством ниже.

Базовое OTA-обновление ESP32 «по воздуху» (OTA) в Arduino IDE

Представьте, что у вас несколько модулей ESP32 разбросаны по всему дому. Каждый раз вытаскивать их и подключать кабель для обновления программы…

/lastminuteengineers/esp32-ota-updates-arduino-ide/index

3 простых шага для использования Web Updater OTA с ESP32

  1. Загрузка OTA-программы через последовательный порт: Первый шаг — загрузить скетч, содержащий OTA-прошивку, через последовательный порт. Это обязательный шаг для выполнения последующих обновлений по воздуху.

  2. Доступ к веб-серверу: OTA-скетч создаёт веб-сервер в режиме STA, доступный через веб-браузер. После входа в веб-сервер вы можете загружать новые скетчи.

  3. Загрузка нового скетча по воздуху: Теперь вы можете загружать новые скетчи на ESP32, генерируя и загружая скомпилированный .bin файл через веб-сервер.

Принцип работы OTA-обновления ESP32 через веб-интерфейс

Шаг 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

Затем откройте браузер и перейдите по IP-адресу, отображённому в мониторе последовательного порта. ESP32 должен показать веб-страницу с запросом данных для входа.

Доступ к веб-серверу ESP32 OTA

Введите следующий 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.

Экспорт скомпилированного двоичного файла программы в Arduino IDE

После успешной компиляции скетча файл .bin будет сгенерирован в папке скетча. Чтобы открыть папку скетча, выберите Sketch > Show Sketch Folder.

Открытие папки скетча из Arduino IDE

Загрузка нового скетча по воздуху на ESP32

После генерации файла .bin новый скетч можно загрузить по воздуху на ESP32.

Откройте браузер и перейдите на страницу /serverIndex. Нажмите Choose File… Выберите только что сгенерированный файл .bin и нажмите Update.

Доступ к странице ServerIndex для загрузки OTA-обновлений

Новый скетч будет загружен за считанные секунды.

Загрузка OTA двоичного .bin файла через веб-браузер

И встроенный светодиод должен начать мигать.

Мигание встроенного светодиода ESP32