Как прошить ESP32 “по воздуху” (OTA)

          На практике часто возникает необходимость обновить прошивку для устройства Интернета вещей (IoT) на базе ESP32. Допустим, прибор установлен под потолком и скрыт подвесной панелью. Если подобных изделий много и требуется частое обновление, то для таких случаев существует технология обновления прошивки «по воздуху»: Over-The-Air (OTA). В рамках данной статьи мы подробно рассмотрим инструкции как прошить ESP32 через Arduino IDE и через веб-интерфейс с применением данной технологии.


Содержание



Общее описание

          OTA – это технология обновления встроенного программного обеспечения (ПО) микроконтроллерного устройства через беспроводную сеть (Wi-Fi, Bluetooth, мобильная связь и т.д.), при которой новый бинарный файл записывается во flash-память контроллера без физического проводного подключения.

          У ESP32 во flash-памяти есть два раздела: с текущим работающим ПО и резервный. При обновлении «по воздуху» новый бинарный файл загружается в резервный раздел, после чего система меняет загрузочный указатель и перезагружается.

          Программа с OTA занимает много flash-памяти. Для NodeMCU-32S (ESP-WROOM-32, 4 МБ памяти) минимальная программа уже потребляет более 70%. В Arduino IDE в меню «Инструменты» можно найти (но не для всех моделей микроконтроллеров) пункт «Partition Scheme» (в старой версии IDE назывался «Схема разделов»). Там выбирают размер flash-памяти под прошивку: стандартный вариант Default_4MB_with_spiffs, а для OTA лучше выбрать Minimal_SPIFFS, поскольку он освобождает максимум места под код, убирая файловую систему. Вариант No_OTA не подойдёт, так как при нём обновление «по воздуху» работать не будет.

          Чтобы микроконтроллер мог принимать OTA-обновления, в самом встроенном ПО должен быть код, который запускает специальный сервер. Библиотека ArduinoOTA поднимает TCP-сервер на порту 3232 и слушает команды от Arduino IDE. Когда среда отправляет бинарный файл, этот сервер принимает его и сохраняет в резервный раздел. Первая прошивка всегда заливается по USB, а все последующие – уже «по воздуху».


Прошивка через ArduinoOTA

          Чтобы прошить ESP32 «по воздуху», нужно выполнить следующие шаги:

1) Убедитесь, что среда программирования Arduino IDE настроена на работу с Вашей платой

          Откройте настройки среды, выбрав Файл –> Параметры –> Дополнительные ссылки для менеджера плат. Там должна быть указана ссылка:­

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

          В менеджере плат выберите конкретно Вашу модель. Классическая NodeMCU-32S на базе модуля ESP-WROOM-32 (на 30 или 38 ножек):

          Или же ESP32-C3 Super Mini:

2) Установить библиотеку ArduinoOTA

          Установить библиотеку ArduinoOTA можно двумя способами:

– скачать архив с репозитория на GitHub. Распаковать его, папку переименовать (удалить в конце «master»). После папку поместить в директорию с библиотеками среды программирования (по умолчанию имеет адрес C:\Users\имя_пользователя\Documents\Arduino\libraries). После этих манипуляций перезагрузить среду, если до этого она была открыта;

– установить при помощи менеджера библиотек:

          Перезагрузив среду программирования, открываем базовый пример из библиотеки ArduinoOTA – BasicOTA:

          В ранних версиях библиотеки, кроме этого примера, был файл OTAWebUpdater, который поднимал локальный веб-сервер, позволявший загружать в контроллер файл с новым ПО через веб-интерфейс в браузере на компьютере. Данный метод рассматривается ниже в разделе «OTA-обновление через веб-интерфейс».

3) Сохраните пример BasicOTA отдельно под новым именем

          Скетч содержит код, который запускает специальный сервер, который потом позволит ESP32 принимать ПО по беспроводному каналу.

          Укажите данные для Вашей Wi-Fi сети, к которой будет подключаться компьютер и отладочная плата:

const char *ssid = "имя_сети";
const char *password = "пароль_к_сети";

4) Скомпилируйте скетч и прошейте плату контроллера через USB-кабель

          Завершив загрузку, откройте монитор порта, чтобы убедиться, что контроллер подключился к указанной Вами сети Wi-Fi.

          Если Вы работаете с ESP32-C3, то проверьте, что в меню «Инструменты» среды программирования раздел «USB CDC On Boot» имеет значение «Enabled». Это активирует виртуальный COM-порт для встроенного в контроллер USB-порт, что позволит читать сообщения через монитор порта:

          Если плата не будет подключаться к беспроводной сети, то в мониторе порта постоянно будет отображаться соответствующее сообщение:

          При успешном соединении будет указан IP-адрес Вашей платы:

5) Пояснения к базовой части кода

          Вы сохранили пример BasicOTA как отдельный файл, с которым можно дальше работать. Программа содержит необходимые директивы для загрузчика, и они должны быть теперь всегда во всех версиях скетчей, которые будут в будущем загружаться на плату. То есть объявленный в коде OTA-сервер является обязательным компонентом для каждой версии ПО, которая предназначена для беспроводного обновления.

          После перезагрузки контроллер выполняет активную прошивку. И если в ней отсутствует код обработки запросов, то устройство теряет способность принимать последующие обновления по сети, что вынуждает вновь подключаться через USB-кабель к плате для восстановления.

          В коде программы обязательно должны присутствовать следующие строки:

– подключение библиотек

#include <WiFi.h>	// подключение к беспроводной сети Wi-Fi
#include <ESPmDNS.h>	// обнаружение ESP32 в сети Wi-Fi для Arduino IDE
#include <NetworkUp.h>	// внутренняя работа ArduinoOTA
#include <ArduinoOTA.h>	

– функция инициализации OTA-сервера

ArduinoOTA.begin();

– вызов обработчика в главном бесконечном цикле loop()

ArduinoOTA.handle();

          В примере BasicOTA есть вот такой фрагмент:

ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH) {
        type = "sketch";
      } else {  // U_SPIFFS
        type = "filesystem";
      }

      // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%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");
      }
    });

          Представленные методы (onStart, onEnd, onProgress, onError) – обработчики событий (колбэк-функции) OTA-процесса, которые вызываются автоматически в разные моменты обновления. Они нужны для мониторинга и отладки. Благодаря им в мониторе порта видны сообщения, указывающие на прохождение всех этапов процесса перепрошивки. Без них обновление всё равно будет работать, поэтому они необязательны.

          В базовом примере также есть вот такой фрагмент кода:

  // Port defaults to 3232
  // ArduinoOTA.setPort(3232);

  // Hostname defaults to esp3232-[MAC]
  // ArduinoOTA.setHostname("myesp32");

  // No authentication by default
  // ArduinoOTA.setPassword("admin");

  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");

          Это настройки OTA-сервера. Они закомментированы, потому что эти настройки уже работают по умолчанию. При необходимости, их можно изменить по своему усмотрению:

Читайте также:  Подключение 16-битного АЦП AD7683 к ESP32

– setPort(3232): порт, через который среда программирования подключается к микроконтроллеру для заливки кода;

– setHostName(«myesp32»): имя устройства в сети. По умолчанию имеет название «esp3232» с припиской MAC-адреса;

– setPassword(«admin»): это пароль для защиты от случайного или чужого ПО. По умолчанию пароля нет;

– setPasswordHash(«21232f…»):  это тот же пароль, но в зашифрованном виде (используется MD5-хеш).

          Для экспериментов можно ничего не менять.

6) Добавьте Вашу логику в базовый код

          В отдельно сохранённый Вами файл добавим новую логику, которая явно будет показывать, что контроллер действительно был «прошит по воздуху». Подключим к отладочной плате OLED-дисплей и добавим в код команду, которая выводит на экран какую-нибудь надпись.

          В базовом примере в функции setup() прописано, что при инициализации микроконтроллера осуществляется подключение к беспроводной сети. Но если нужная точка доступа не будет найдена, то система будет постоянно пытаться переподключаться:

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }

          Поэтому предлагается выполнять проверку, которая не будет блокировать работу программы. Причём не только в рамках инициализации setup(), но и в главном бесконечном цикле loop(). При инициализации система пробует подключиться, и если не получается, выдаёт сообщение на дисплей и продолжает работу далее, переходя в главный бесконечный цикл. В нём каждые 60 секунд осуществляется попытка переподключиться.

          В примере BasicOTA есть фрагмент, в котором описаны обработчики событий (onStart, onEnd, onProgress, onError). Сообщения от них выводятся через монитор порта только тогда, когда микроконтроллер подключен по USB-кабелю и выбран физический COM-порт. Если используется OTA-загрузка (выбран IP-адрес из списка портов), эти сообщения не отображаются, поскольку монитор порта не работает с сетевыми подключениями (поэтому данную часть кода можно удалить):

          Учитывая все указанные нюансы, уберём обработчики событий и добавим механизм неблокирующего автоматического переподключения к Wi-Fi. Ниже представлен предлагаемый вариант кода, который можно уже использовать как основу для будущих учебных проектов:

#include <WiFi.h>          // Для работы с Wi-Fi
#include <ESPmDNS.h>       // Для обнаружения устройства в сети по имени (mDNS)
#include <NetworkUdp.h>    // Для UDP-соединений (используется OTA)
#include <ArduinoOTA.h>    // Библиотека для обновления прошивки по воздуху

#include <Wire.h>          // Для I2C-шины (подключение OLED)
#include <Adafruit_GFX.h>  // Графическая библиотека для дисплеев
#include <Adafruit_SSD1306.h> // Драйвер для OLED-дисплея SSD1306

// Настройки Wi-Fi сети
const char *ssid = "имя_сети";
const char *password = "пароль_сети";

// Настройки OLED-дисплея (размеры 128x64, I2C-адрес 0x3C)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Глобальные переменные для управления Wi-Fi
bool wifiConnected = false;                  // Флаг: подключены ли к Wi-Fi
unsigned long lastWiFiCheck = 0;             // Время последней проверки Wi-Fi
const unsigned long CHECK_INTERVAL = 60000;  // Интервал проверки: 60 секунд

// Функция первичной инициализации контроллера
void setup() {

  // Инициализация OLED-дисплея
  // Если дисплей не отвечает по адресу 0x3C - зависаем в бесконечном цикле
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    while (true) {
      delay(100);
    }
  }

  // Настройка встроенного светодиода как выхода
  pinMode(LED_BUILTIN, OUTPUT);

  // Вывод на OLED: "Initialization"
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Initialization");
  display.display();

  // Попытка подключения к Wi-Fi (неблокирующий режим)
  WiFi.mode(WIFI_STA);          // Режим клиента
  WiFi.begin(ssid, password);   // Запуск подключения

  // Ждём до 5 секунд (20 попыток по 250 мс)
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(250);
    attempts++;
    display.print(".");         // Визуальный индикатор ожидания на OLED
    display.display();
  }

  // Проверка результата подключения
  if (WiFi.status() == WL_CONNECTED) {
    wifiConnected = true;

    // Вывод информации об успешном подключении на OLED
    display.setCursor(0, 20);
    display.println("Wi-Fi connected!");
    display.setCursor(0, 40);
    display.println("IP address: ");
    display.setCursor(0, 60);
    display.println(WiFi.localIP());
    display.display();

    // Запуск OTA-сервера
    ArduinoOTA.begin();
  } else {
    wifiConnected = false;

    // Вывод сообщения об ошибке подключения
    display.setCursor(0, 20);
    display.println("WiFi not connected.");
    display.setCursor(0, 40);
    display.println("Will retry later.");
    display.display();
  }

  // Основной экран (выводится всегда, независимо от Wi-Fi)
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("SPRYTRON.RU");
  display.setCursor(0, 20);
  display.println("Ready to work!");

  if (!wifiConnected) {
    display.setCursor(0, 40);
    display.println("No Wi-Fi");
  } else {
    display.setCursor(0, 40);
    display.println("Wi-Fi OK");
  }
  display.display();

  // Запоминаем время последней проверки Wi-Fi
  lastWiFiCheck = millis();
}

// Главный бесконечный цикл
void loop() {

  // 1. Обработка OTA-событий (только если есть Wi-Fi)
  if (wifiConnected) {
    ArduinoOTA.handle();   // Слушает входящие запросы на обновление
  }

  // 2. Периодическая проверка Wi-Fi (каждые 60 секунд)
  unsigned long currentMillis = millis();
  if (currentMillis - lastWiFiCheck >= CHECK_INTERVAL) {
    lastWiFiCheck = currentMillis;

    // 2.1 Если соединение потеряно - пытаемся переподключиться
    if (WiFi.status() != WL_CONNECTED) {

      // Вывод на OLED: "Wi-Fi lost! Reconnecting..."
      display.clearDisplay();
      display.setTextSize(1);
      display.setTextColor(SSD1306_WHITE);
      display.setCursor(0, 0);
      display.println("Wi-Fi lost!");
      display.setCursor(0, 20);
      display.println("Reconnecting...");

      wifiConnected = false;

      display.setCursor(0, 40);
      display.println("No Wi-Fi");
      display.display();

      WiFi.reconnect();   // Переподключение
    }

    // 2.2 Если раньше не было сети, но теперь она появилась
    else if (!wifiConnected) {
      if (WiFi.status() == WL_CONNECTED) {
        wifiConnected = true;

        // Вывод на OLED: "Wi-Fi reconnected!"
        display.clearDisplay();
        display.setTextSize(1);
        display.setTextColor(SSD1306_WHITE);
        display.setCursor(0, 0);
        display.println("Wi-Fi reconnected!");
        display.setCursor(0, 40);
        display.println("Wi-Fi OK");
        display.display();

        // Запуск OTA (если ещё не запущен)
        ArduinoOTA.begin();
      }
    }
  }

  // 3. Основной код приложения.
  // Работает всегда, даже без Wi-Fi
  digitalWrite(LED_BUILTIN, HIGH);   // Включаем встроенный светодиод
  delay(500);
  digitalWrite(LED_BUILTIN, LOW);    // Выключаем встроенный светодиод
  delay(500);
}

7) Подайте питание от внешнего источника и подключитесь по Wi-Fi

Читайте также:  Датчик атмосферного давления BMP580 и ESP32

          Используйте тот же USB-кабель, подключив к зарядному устройству для смартфона на 5 В.

          Я использовал смартфон как точку доступа. Соответственно, чётко будет видно, какие устройства подключены.

          Подаём питание на плату. Ждём, пока загрузится и подключится к Wi-Fi (фиксируем это на смартфоне):

          Далее подключаем ноутбук (b-bb101) к специально созданной точке доступа:

          В меню «Tools/Инструменты» в разделе «Port/Порт» отобразятся сетевое имя платы и её IP-адрес в локальной сети. Если в списке портов ничего не отобразится, то перезагрузите среду программирования после того, как сама плата загрузится, будучи запитанной от отдельного блока питания.

8) Загрузите Ваш новый скетч в плату «по воздуху»

          При попытке загрузить ПО на плату появится диалоговое окно, в котором потребуется ввести пароль аутентификации ArduinoOTA.

          По умолчанию пароля нет, и в предложенном скетче ничего не объявлялось:

// При желании можно добавить пароль аутентификации доступа к OTA-серверу
// No authentication by default
ArduinoOTA.setPassword("admin");

          Если нет пароля, то достаточно просто ввести хотя бы 1 символ (поле нельзя оставить пустым), чтобы активировать кнопку «UPLOAD»:

          В случае успешной загрузки контроллер начнёт исполнять новый скетч. При инициализации на OLED-дисплее отобразится соответствующий процесс, включающий в себя подключение к точке доступа:

          Если в течение 5 секунд не удастся подключиться, то программа не останавливается и выполняется дальше, указав, что не удалось установить связь:

          Если же соединение произведено успешно, то отобразится соответствующее сообщение:

          В программе предусмотрена проверка через каждые 60 секунд: есть ли соединение. При разрыве соединения выполняется попытка переподключения:

          В случае успешного восстановления будет отображено соответствующее сообщение:

          Данный алгоритм позволит изделию выполнять основные функции вне зависимости от состояния подключения к беспроводной сети.


OTA-обновление через веб-интерфейс

          Рассмотрим альтернативный вариант, который подразумевает беспроводную заливку скетча, используя веб-интерфейс:

1) Установите библиотеку ElegantOTA от Ayush Sharma:

          Если не устанавливается через встроенный менеджер, скачайте архив с репозитория на GitHub.

2) Добавить в Ваш скетч следующие строки:

– объявление библиотек

#include <WiFi.h>
#include <WebServer.h>  // Синхронный веб-сервер, предустановлен в Arduino IDE
#include <ElegantOTA.h> // Сама библиотека OTA

– параметры сети

const char* ssid = "имя_сети";
const char* password = "пароль_сети";

– создание веб-сервера

WebServer server(80); // Создаем веб-сервер на 80-м порту

– подключение к точке доступа

// Подключение к Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected!");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

– запуск сервера

ElegantOTA.begin(&server);
server.begin();

– обработка запросов в бесконечном цикле loop()

void loop() {
  // Обработка клиентов веб-сервера (синхронный режим)
  server.handleClient();
  ElegantOTA.loop(); // обработки OTA-запросов
}

          Каждая новая прошивка, которую вы загружаете через веб-интерфейс, сама должна содержать код для OTA. Если вы загрузите ПО без библиотеки ElegantOTA и вызова ElegantOTA.loop(), то устройство потеряет способность к обновлению “по воздуху”, и вам снова придется подключать его по USB для восстановления.

          В примере используется синхронный веб-сервер (webServer), и для учебных целей данный вариант проще и полностью работоспособен. Но в реальной практике рекомендуется использовать асинхронную версию (AsyncWebServer).

3) Минимальный базовый скетч для заливки по USB-кабелю представлен ниже:

#include <WiFi.h>          // Для работы с Wi-Fi
#include <WebServer.h>     // Синхронный веб-сервер (нужен для ElegantOTA)
#include <ElegantOTA.h>    // Библиотека для OTA через веб-интерфейс


#include <Wire.h>          // Для I2C-шины (подключение OLED)
#include <Adafruit_GFX.h>  // Графическая библиотека для дисплеев
#include <Adafruit_SSD1306.h> // Драйвер для OLED-дисплея SSD1306

// Настройки Wi-Fi сети
const char* ssid = "OTA_update_test";        // Имя твоей Wi-Fi сети
const char* password = "evangelion_2015";     // Пароль от неё

// Создаём веб-сервер на порту 80
WebServer server(80);


// Функция setup() - инициализация контроллера при включении
void setup() {
  // Запускаем отладку через USB (можно удалить после настройки)
  Serial.begin(115200);
  Serial.println("Loading...");

  // Подключаемся к Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("\nWi-Fi connected!");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  // Запускаем ElegantOTA
  ElegantOTA.begin(&server);
  // Если требуется поставить пароль на вход в веб-интерфейс:
  // ElegantOTA.setAuth("admin", "password");

  // Запускаем веб-сервер
  server.begin();

  Serial.println("HTTP server started!");
  Serial.println("Open http://" + WiFi.localIP().toString() + "/update");
}

// Функция loop() - главный бесконечный цикл
void loop() {
  // Обработка клиентов веб-сервера (синхронный режим)
  server.handleClient();

  // Обработка OTA-запросов
  ElegantOTA.loop();

  // Здесь может быть Ваш основной код
  // (он будет работать даже во время OTA-обновления)
}

          Загрузите его через USB-кабель на ESP32. В мониторе порта отобразится IP-адрес отладочной платы:

          В компьютере, который должен быть подключен к той же точке доступа, что и отладочная плата, откройте в браузере веб-страничку по адресу, указанному в мониторе порта:

4) Сохраните файл под новым именем. В него тоже добавим OLED-дисплей и мигание встроенным светодиодом:

#include <WiFi.h>        // Для работы с Wi-Fi
#include <WebServer.h>   // Синхронный веб-сервер (нужен для ElegantOTA)
#include <ElegantOTA.h>  // Библиотека для OTA через веб-интерфейс


#include <Wire.h>              // Для I2C-шины (подключение OLED)
#include <Adafruit_GFX.h>      // Графическая библиотека для дисплеев
#include <Adafruit_SSD1306.h>  // Драйвер для OLED-дисплея SSD1306


// Настройки OLED-дисплея (размеры 128x64, I2C-адрес 0x3C)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);


// Глобальные переменные для управления Wi-Fi
bool wifiConnected = false;                  // Флаг: подключены ли к Wi-Fi
unsigned long lastWiFiCheck = 0;             // Время последней проверки Wi-Fi
const unsigned long CHECK_INTERVAL = 60000;  // Интервал проверки: 60 секунд

// Настройки Wi-Fi сети
const char* ssid = "имя_сети";
const char* password = "пароль_сети";

// Создаём веб-сервер на порту 80
WebServer server(80);

// Функция setup() - инициализация контроллера
void setup() {

  // Инициализация OLED-дисплея
  // Если дисплей не отвечает по адресу 0x3C - зависаем в бесконечном цикле
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    while (true) {
      delay(100);
    }
  }

  // Вывод на OLED: "Initialization"
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Initialization");
  display.display();

  // Настройка встроенного светодиода как выхода
  pinMode(LED_BUILTIN, OUTPUT);

  // Попытка подключения к Wi-Fi (неблокирующий режим)
  WiFi.mode(WIFI_STA);         // Режим клиента
  WiFi.begin(ssid, password);  // Запуск подключения

  // Ждём до 5 секунд (20 попыток по 250 мс)
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(250);
    attempts++;
    display.print(".");  // Визуальный индикатор ожидания на OLED
    display.display();
  }

  // Проверка результата подключения
  if (WiFi.status() == WL_CONNECTED) {
    wifiConnected = true;

    // Вывод информации об успешном подключении на OLED
    display.setCursor(0, 20);
    display.println("Wi-Fi connected!");
    display.setCursor(0, 35);
    display.println("IP address: ");
    display.setCursor(0, 50);
    display.println(WiFi.localIP());
    display.display();

    delay(3000);

    // Запускаем ElegantOTA
    ElegantOTA.begin(&server);

    // При необходимости можно поставить пароль на вход в веб-интерфейс
    // ElegantOTA.setAuth("admin", "password");

    // Запускаем веб-сервер
    server.begin();
  } else {
    wifiConnected = false;

    // Вывод сообщения об ошибке подключения
    display.setCursor(0, 20);
    display.println("WiFi not connected.");
    display.setCursor(0, 40);
    display.println("Will retry later.");
    display.display();

    delay(3000);
  }

  // Основной экран (выводится всегда, независимо от Wi-Fi)
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("SPRYTRON.RU");
  display.setCursor(0, 20);
  display.println("Ready to work!");

  if (!wifiConnected) {
    display.setCursor(0, 40);
    display.println("No Wi-Fi");
  } else {
    display.setCursor(0, 40);
    display.println("Wi-Fi OK");
  }
  display.display();

  // Запоминаем время последней проверки Wi-Fi
  lastWiFiCheck = millis();
}

// Функция loop() - главный бесконечный цикл
void loop() {

  // 1. Обработка OTA-событий (только если есть Wi-Fi)
  if (wifiConnected) {
    server.handleClient();  // Обработка клиентов веб-сервера (синхронный режим)
    ElegantOTA.loop();      // Обработка OTA-запросов
  }

  // 2. Периодическая проверка Wi-Fi (каждые 60 секунд)
  unsigned long currentMillis = millis();
  if (currentMillis - lastWiFiCheck >= CHECK_INTERVAL) {
    lastWiFiCheck = currentMillis;

    // 2.1 Если соединение потеряно — пытаемся переподключиться
    if (WiFi.status() != WL_CONNECTED) {

      // Вывод на OLED: "Wi-Fi lost! Reconnecting..."
      display.clearDisplay();
      display.setTextSize(1);
      display.setTextColor(SSD1306_WHITE);
      display.setCursor(0, 0);
      display.println("Wi-Fi lost!");
      display.setCursor(0, 20);
      display.println("Reconnecting...");

      wifiConnected = false;

      display.setCursor(0, 40);
      display.println("No Wi-Fi");
      display.display();

      WiFi.reconnect();  // Переподключение
    }

    // 2.2 Если раньше не было сети, но теперь она появилась
    else if (!wifiConnected) {
      if (WiFi.status() == WL_CONNECTED) {
        wifiConnected = true;

        // Вывод на OLED: "Wi-Fi reconnected!"
        display.clearDisplay();
        display.setTextSize(1);
        display.setTextColor(SSD1306_WHITE);
        display.setCursor(0, 0);
        display.println("Wi-Fi reconnected!");
        display.setCursor(0, 40);
        display.println("Wi-Fi OK");
        display.display();

        // Запускаем ElegantOTA и веб-сервер, если ещё не запущены
        ElegantOTA.begin(&server);
        server.begin();
      }
    }
  }

  // 3. Основной код приложения.
  // Работает всегда, даже без Wi-Fi
  digitalWrite(LED_BUILTIN, HIGH);  // Включаем встроенный светодиод
  delay(500);
  digitalWrite(LED_BUILTIN, LOW);  // Выключаем встроенный светодиод
  delay(500);
}

5) Компиляция и экспорт bin

Читайте также:  Модуль датчиков BMP280 + AHT20: метеостанция на ESP32

          Скомпилируйте файл, нажав кнопку «Проверить» или комбинацию клавиш Ctrl + R. Если ошибок нет, то создайте bin-файл прошивки, выбрав в меню «Скетч» раздел «Экспортировать скомпилированный бинарный файл» (или комбинация клавиш Alt + Ctrl + S):

          В папке с проектом появится директория build, которая содержит нужный бинарный файл:

6) Проверьте подключение к точке доступа

          Убедитесь, что отладочная плата и компьютер подключены к одной точке доступа. Доступ к Интернету вовсе не обязателен, ведь это чисто локальная связь. Теперь вновь откройте в браузере веб-страницу, нажмите кнопку «Select File» и выберите файл, который оканчивается на «ino.bin»:

          Нажав кнопку «Открыть», автоматически начнётся загрузка:

          Через пару секунд заливка завершится:

          В случае ошибки (обрыв соединения) также будет оповещение на веб-страничке:

7) Последующие изменения в коде потребуют пересборки bin-файла (шаг 5).


Заключение

          Мы рассмотрели два надёжных и актуальных способа беспроводного обновления прошивки для ESP32:

– классический через Arduino IDE (библиотека ArduinoOTA);

– альтернативный через веб-интерфейс (ElegantOTA).

          Самые важные моменты:

1) первая прошивка всегда заливается через USB-кабель;

2) каждая последующая прошивка обязана содержать код OTA-сервера;

3) неблокирующее управление подключением к Wi-Fi повышает автономность делает прибор устойчивым к временным сбоям сети.