Compare commits

..
10 Commits
Author SHA1 Message Date
aZholtikov bcd5d5c3f8 doc: added wiki 2026-10-17 20:41:24 +03:00
aZholtikov 51fdf710b8 doc: updated readme 2026-10-17 20:41:08 +03:00
aZholtikov 95ac96f61c style: updated tag 2026-10-04 22:16:23 +03:00
aZholtikov 40f2d290ae perf: updated error checks 2026-10-04 21:10:50 +03:00
aZholtikov e3c55dd432 style: updated comments 2026-09-12 12:39:59 +03:00
aZholtikov ae2828c6bd doc: updated readme 2026-08-30 16:53:29 +03:00
aZholtikov 03b5d12f1b doc: updated comments 2026-08-16 09:13:27 +03:00
aZholtikov b5b27a1937 doc: updated readme 2026-08-16 09:12:31 +03:00
aZholtikov 9521a78d13 doc: updated wiki 2026-08-16 09:08:43 +03:00
aZholtikov 7a6c15193a doc: added idf_component.yml 2026-08-16 08:32:06 +03:00
5 changed files with 272 additions and 27 deletions

No files matched your search

+32 -12
View File
@@ -1,31 +1,41 @@
# ESP32 ESP-IDF component for simple OTA server
## Tested on
## Features
1. [ESP32 ESP-IDF v6.0.0](https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/index.html)
## SAST Tools
[PVS-Studio](https://pvs-studio.com/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source) - static analyzer for C, C++, C#, and Java code.
1. HTTP-based OTA firmware updates via standard HTTP POST requests.
2. Automatic partition management with next available OTA partition detection.
3. Embedded web interface for browser-based firmware uploads.
4. Automatic boot activation and device reboot after successful update.
5. Chunked data reception with 1000-byte blocks for efficient memory usage.
6. Seamless integration with ESP-IDF HTTP server and OTA APIs.
7. Comprehensive error handling with detailed error reporting.
8. Low memory footprint with minimal RAM consumption.
## Using
In an existing project, run the following command to install the component:
```text
```bash
cd ../your_project/components
git clone https://github.com/aZholtikov/zh_ota_server
```
In the application, add the component:
In the application, include the header:
```c
#include "zh_ota_server.h"
```
## Example
## Attention
OTA server by address <http://192.168.4.1/ota>
1. The firmware upload path is fixed to /zh_ota_server and cannot be changed.
2. The zh_ota_server_init function must be called only once during application lifetime.
3. Memory allocated for the URI path is never freed.
4. Flashing an invalid firmware image may render the device unbootable.
## Examples
### Basic Example
```c
#include "nvs_flash.h"
@@ -58,7 +68,17 @@ void app_main(void)
esp_wifi_set_config(WIFI_IF_AP, &ap_config);
esp_wifi_start();
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
httpd_start(&http_server_handle, &config);
zh_ota_server_init(http_server_handle, "/ota");
esp_err_t ret = httpd_start(&http_server_handle, &config);
if (ret != ESP_OK)
{
printf("HTTP server start error: %s\n", esp_err_to_name(ret));
return;
}
ret = zh_ota_server_init(http_server_handle, "/ota");
if (ret != ESP_OK)
{
printf("OTA server init error: %s\n", esp_err_to_name(ret));
return;
}
}
```
+8
View File
@@ -0,0 +1,8 @@
description: ESP32 ESP-IDF component for simple OTA server
url: https://github.com/aZholtikov/zh_ota_server
version: "1.0.0"
license: "Apache-2.0"
maintainers:
- "Alexey Zholtikov <alexey.zholtikov@yandex.ru>"
dependencies:
idf: ">=5.0"
+8 -2
View File
@@ -22,9 +22,14 @@
#pragma once
#include "string.h"
#include "esp_ota_ops.h"
#include "esp_log.h"
#include "esp_http_server.h"
#include "esp_heap_caps.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#ifdef __cplusplus
extern "C"
@@ -38,8 +43,8 @@ extern "C"
* the URI path, and registers the HTTP GET (web interface) and HTTP POST
* (firmware upload) handlers with the provided HTTP server instance.
*
* @param[in] server HTTP server handle obtained from httpd_start().
* @param[in] path URI path for the OTA server (e.g., "/ota").
* @param[in] server HTTP server handle obtained from httpd_start() (must not be NULL)
* @param[in] path URI path for the OTA server (e.g., "/ota")
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if invalid server handle or path
@@ -48,6 +53,7 @@ extern "C"
*
* @note The path string is copied internally and must not be freed
* by the caller.
*
* @warning The memory allocated for the path is never freed.
* Call this function only once during application lifetime.
*/
+196
View File
@@ -0,0 +1,196 @@
# Описание компонента zh_ota_server
## Общие сведения
**zh_ota_server** — это компонент для ESP-IDF, реализующий сервер обновлений прошивки по воздуху (Over-The-Air, OTA) для микроконтроллеров ESP32.
Компонент предоставляет веб-интерфейс для загрузки бинарных файлов прошивки через браузер и автоматически записывает их в свободный OTA-раздел ESP32 с последующей активацией и перезагрузкой устройства.
## Основные возможности
1. **Веб-интерфейс для загрузки** — встроенная HTML-страница для выбора и отправки файла прошивки через браузер.
2. **Автоматическое определение раздела** — компонент сам находит следующий доступный OTA-раздел через API ESP-IDF.
3. **Построчная запись прошивки** — данные принимаются блоками по 1000 байт для экономии оперативной памяти.
4. **Автоматическая активация и перезагрузка** — после успешной записи прошивки раздел становится главным, и устройство перезагружается.
5. **Интеграция с ESP-IDF HTTP server** — компонент использует встроенный HTTP-сервер ESP-IDF и регистрирует свои обработчики как стандартные URI-хендлеры.
6. **Встроенная HTML-страница** — веб-интерфейс хранится как встроенный массив байт (EMBED_FILES), не требуя внешней файловой системы.
7. **Детальное логирование ошибок** — при ошибках логируются файл, строка и код возврата ESP-IDF.
## Внутреннее устройство
### Глобальные переменные
| Переменная | Тип | Описание |
|------------|-----|----------|
| `_ota_path` | `char *` | Выделенная динамически память для URI-пути OTA-сервера (не освобождается) |
| `_ota_ws_path` | `const char *` | URI-путь для POST-обработчика (`"/zh_ota_server"`) |
| `_is_initialized` | `volatile bool` | Флаг инициализации компонента, предотвращающий повторный вызов `zh_ota_server_init()` |
### Структуры обработчиков
| Поле | Тип | Описание |
|------|-----|----------|
| `_ota_page.uri` | `char *` | URI-путь для GET-запроса (устанавливается при инициализации) |
| `_ota_page.method` | `httpd_method_t` | HTTP-метод: `HTTP_GET` |
| `_ota_page.handler` | `httpd_req_handler_t` | Указатель на `_ota_page_handler()` |
| `_ota_page_ws.uri` | `char *` | URI-путь для POST-запроса: `"/zh_ota_server"` |
| `_ota_page_ws.method` | `httpd_method_t` | HTTP-метод: `HTTP_POST` |
| `_ota_page_ws.handler` | `httpd_req_handler_t` | Указатель на `_ota_ws_page_handler()` |
### Встроенные ресурсы
HTML-страница `zh_ota_server.html` встраивается в прошивку через механизм `EMBED_FILES` CMake. Доступна через внешние символы:
| Символ | Описание |
|--------|----------|
| `zh_ota_server_html_start[]` | Начало встроенного HTML-массива |
| `zh_ota_server_html_end[]` | Конец встроенного HTML-массива |
Размер страницы вычисляется как `zh_ota_server_html_end - zh_ota_server_html_start`.
### Управление памятью
При инициализации `zh_ota_path` выделяется через `heap_caps_calloc()` с флагом `MALLOC_CAP_8BIT`. Память **не освобождается** никогда — это осознанный выбор, так как функция инициализации может быть вызвана только один раз за время работы приложения.
## API (интерфейс)
### Инициализация
| Функция | Описание |
|---------|----------|
| `zh_ota_server_init()` | Инициализирует и регистрирует обработчики OTA-сервера в предоставленном экземпляре HTTP-сервера. Принимает handle сервера (`httpd_handle_t`) и URI-путь (например, `"/ota"`). Возвращает `ESP_OK` при успехе, `ESP_ERR_INVALID_ARG` при некорректных аргументах, `ESP_ERR_NO_MEM` при нехватке памяти, `ESP_FAIL` при ошибке регистрации обработчика. |
## Потокобезопасность
Компонент **не является потокобезопасным**. Глобальные переменные `_ota_path` и `_is_initialized` защищены только проверкой на повторную инициализацию (`_is_initialized == false`), но не мьютексами. Обработчики HTTP-запросов вызываются в контексте потока HTTP-сервера и не используют механизмы синхронизации.
## Алгоритмы
### Обработка OTA-обновления (`_ota_ws_page_handler`)
1. Выделение буфера размером **1000 байт** для приёма данных.
2. Определение следующего доступного OTA-раздела через `esp_ota_get_next_update_partition(NULL)`.
3. Начало записи в раздел через `esp_ota_begin()` с размером `OTA_SIZE_UNKNOWN`.
4. **Цикл приёма данных:**
- Приём данных из HTTP-запроса через `httpd_req_recv()` блоками до `sizeof(buf)` (1000 байт).
- При таймауте сокета (`HTTPD_SOCK_ERR_TIMEOUT`) цикл продолжается без действия.
- При ошибке приёма отправляется HTTP 500 и вызывается `esp_ota_abort()`.
- Запись данных в flash через `esp_ota_write()`; при ошибке отправляется HTTP 500 и вызывается `esp_ota_abort()`.
- Уменьшение счётчика оставшихся байт (`remaining -= recv_len`).
5. Завершение записи через `esp_ota_end()` (валидация образа).
6. Установка раздела как загрузочного через `esp_ota_set_boot_partition()`.
7. Отправка HTTP-ответа `"Firmware update complete, rebooting now!\n"`.
8. Задержка **500 мс** через `vTaskDelay()` и вызов `esp_restart()` для перезагрузки.
## Примеры использования
### Базовый пример
```c
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "zh_ota_server.h"
static httpd_handle_t http_server_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_ota_server", ESP_LOG_ERROR);
nvs_flash_init();
esp_netif_init();
esp_event_loop_run(NULL, NULL);
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
httpd_start(&http_server_handle, &config);
zh_ota_server_init(http_server_handle, "/ota");
}
```
### Пример с Wi-Fi в режиме точки доступа
```c
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "zh_ota_server.h"
#define WIFI_SSID "ESP32-OTA"
#define WIFI_PASS "password123"
static httpd_handle_t http_server_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_ota_server", ESP_LOG_DEBUG);
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
wifi_init_config_t wifi_config = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wifi_config);
wifi_config_t ap_config = {
.ap = {
.ssid = WIFI_SSID,
.password = WIFI_PASS,
.max_connection = 4,
.authmode = WIFI_AUTH_WPA2_PSK,
},
};
esp_wifi_set_mode(WIFI_MODE_AP);
esp_wifi_set_config(WIFI_IF_AP, &ap_config);
esp_wifi_start();
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
httpd_start(&http_server_handle, &config);
zh_ota_server_init(http_server_handle, "/ota");
}
```
## Логирование
Компонент использует ESP-IDF логирование с тегом `"zh_ota_server"`.
Макрос `ZH_LOGI` — для информационных сообщений. Макрос `ZH_LOGE` — для ошибок, автоматически добавляет имя файла, номер строки и строковое представление кода ошибки ESP-IDF.
```c
esp_log_level_set("zh_ota_server", ESP_LOG_DEBUG);
```
## Ошибки
| Код | Описание |
|-----|----------|
| `ESP_OK` | Успешное выполнение |
| `ESP_ERR_INVALID_ARG` | Неверный аргумент: NULL-указатель на сервер или путь |
| `ESP_ERR_INVALID_STATE` | Повторная инициализация: компонент уже был инициализирован |
| `ESP_ERR_NO_MEM` | Нехватка памяти при выделении для URI-пути |
| `ESP_FAIL` | Ошибка регистрации URI-обработчика, записи в flash, завершения OTA или установки загрузочного раздела |
## Ограничения
1. **Однократная инициализация** — `zh_ota_server_init()` можно вызвать только один раз за время работы приложения. Повторный вызов вернёт `ESP_ERR_INVALID_STATE`.
2. **Утечка памяти URI-пути** — память, выделенная для `_ota_path`, никогда не освобождается.
3. **Риск «окирпичивания» устройства** — запись невалидного образа прошивки сделает устройство не загружаемым. Необходимо иметь механизм восстановления (например, USB-прошивку).
4. **Отсутствие потокобезопасности** — компонент не защищён мьютексами; одновременный доступ из нескольких потоков может привести к неопределённому поведению.
5. **Размер буфера 1000 байт** — фиксированный размер буфера приёма может быть недостаточен для оптимальной производительности при больших объёмах данных.
6. **Отсутствие контрольной суммы** — валидация образа выполняется только через `esp_ota_end()`, что не гарантирует корректность прошивки на уровне содержимого.
## Требования для сборки
- ESP-IDF (фреймворк для ESP32)
- `esp_http_server` — встроенный HTTP-сервер ESP-IDF
- `app_update` — модуль ESP-IDF для управления OTA-разделами и загрузкой
- FreeRTOS (входит в состав ESP-IDF)
## Установка
```bash
cd ваш_проект/components
git clone https://github.com/aZholtikov/zh_ota_server
```
```c
#include "zh_ota_server.h"
```
+28 -13
View File
@@ -13,17 +13,26 @@ static const char *TAG = "zh_ota_server";
return err; \
}
#define ZH_ERROR_CHECK_CONT(cond, cleanup, msg, ...) \
if (!(cond)) \
{ \
ZH_LOGE(msg, ESP_FAIL, ##__VA_ARGS__); \
cleanup; \
}
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
static char *_ota_path = NULL; /*!< Internal storage for the OTA server URI path */
static char *_ota_ws_path = "/zh_ota_server"; /*!< Fixed URI path for WebSocket-style firmware upload */
static char *_ota_path = NULL;
static const char *_ota_ws_path = "/zh_ota_server";
volatile static bool _is_initialized = false;
/**
* @brief HTTP GET handler for the OTA web interface.
*
* Serves the embedded HTML page for firmware upload via HTTP response.
*
* @param req HTTP request structure.
* @param req HTTP request structure
*
* @return ESP_OK on success
* @return ESP_FAIL if failed to send the response
@@ -37,7 +46,7 @@ static esp_err_t _ota_page_handler(httpd_req_t *req);
* available OTA partition, and activates it as the new boot partition.
* The device reboots automatically after a successful update.
*
* @param req HTTP request containing the firmware binary payload.
* @param req HTTP request containing the firmware binary payload
*
* @return ESP_OK on success
* @return ESP_FAIL if OTA operations or HTTP response failed
@@ -51,13 +60,13 @@ static esp_err_t _ota_ws_page_handler(httpd_req_t *req);
extern const uint8_t zh_ota_server_html_start[] asm("_binary_zh_ota_server_html_start");
extern const uint8_t zh_ota_server_html_end[] asm("_binary_zh_ota_server_html_end");
httpd_uri_t _ota_page = {
static httpd_uri_t _ota_page = {
.uri = NULL,
.method = HTTP_GET,
.handler = _ota_page_handler,
.user_ctx = NULL};
httpd_uri_t _ota_page_ws = {
static httpd_uri_t _ota_page_ws = {
.uri = NULL,
.method = HTTP_POST,
.handler = _ota_ws_page_handler,
@@ -67,13 +76,17 @@ esp_err_t zh_ota_server_init(httpd_handle_t server, const char *path)
{
ZH_LOGI("OTA server initialization started.");
ZH_ERROR_CHECK(server != NULL && path != NULL, ESP_ERR_INVALID_ARG, NULL, "OTA server initialization failed. Invalid argument.");
ZH_ERROR_CHECK(_is_initialized == false, ESP_ERR_INVALID_STATE, NULL, "OTA server initialization failed. OTA server already initialized.");
_ota_path = (char *)heap_caps_calloc(1, strlen(path) + 1, MALLOC_CAP_8BIT);
ZH_ERROR_CHECK(_ota_path != NULL, ESP_ERR_NO_MEM, NULL, "OTA server initialization failed. Memory allocation fail or no free memory in the heap.");
strcpy(_ota_path, path);
_ota_page.uri = _ota_path;
_ota_page_ws.uri = _ota_ws_path;
ZH_ERROR_CHECK(httpd_register_uri_handler(server, &_ota_page) == ESP_OK, ESP_FAIL, heap_caps_free(_ota_path), "OTA server initialization failed. Register uri handler failed.");
ZH_ERROR_CHECK(httpd_register_uri_handler(server, &_ota_page_ws) == ESP_OK, ESP_FAIL, heap_caps_free(_ota_path), "OTA server initialization failed. Register uri handler failed.");
ZH_ERROR_CHECK(httpd_register_uri_handler(server, &_ota_page) == ESP_OK, ESP_FAIL, heap_caps_free(_ota_path); _ota_path = NULL; _ota_page.uri = NULL, "OTA server initialization failed. Register uri handler failed.");
ZH_ERROR_CHECK(httpd_register_uri_handler(server, &_ota_page_ws) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(httpd_unregister_uri_handler(server, _ota_page.uri, HTTP_GET) == ESP_OK, NULL, "Unregister uri handler failed.")};
heap_caps_free(_ota_path); _ota_path = NULL; _ota_page.uri = NULL, "OTA server initialization failed. Register uri handler failed.");
_is_initialized = true;
ZH_LOGI("OTA server initialization completed successfully.");
return ESP_OK;
}
@@ -84,7 +97,7 @@ esp_err_t _ota_page_handler(httpd_req_t *req)
return ESP_OK;
}
esp_err_t _ota_ws_page_handler(httpd_req_t *req) // -V2008
esp_err_t _ota_ws_page_handler(httpd_req_t *req)
{
char buf[1000] = {0};
esp_ota_handle_t ota_handle = {0};
@@ -101,17 +114,19 @@ esp_err_t _ota_ws_page_handler(httpd_req_t *req) // -V2008
}
else if (recv_len <= 0)
{
ZH_ERROR_CHECK(httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Protocol Error") == ESP_OK, ESP_FAIL, NULL, "HTTP server internal error.");
ZH_ERROR_CHECK_CONT(httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Protocol Error") == ESP_OK, NULL, "HTTP server internal error.");
ZH_ERROR_CHECK_CONT(esp_ota_abort(ota_handle) == ESP_OK, NULL, "OTA abort failed.");
return ESP_FAIL;
}
ZH_ERROR_CHECK(esp_ota_write(ota_handle, (const void *)buf, recv_len) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Flash error.") == ESP_OK, ESP_FAIL, NULL, "HTTP server internal error.")}, "OTA write failed.");
{ZH_ERROR_CHECK_CONT(httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Flash error.") == ESP_OK, NULL, "HTTP server internal error.")};
{ZH_ERROR_CHECK_CONT(esp_ota_abort(ota_handle) == ESP_OK, NULL, "OTA abort failed.")}, "OTA write failed.");
remaining -= recv_len;
}
ZH_ERROR_CHECK(esp_ota_end(ota_handle) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Validation / Activation Error.") == ESP_OK, ESP_FAIL, NULL, "HTTP server internal error.")}, "OTA end failed.");
{ZH_ERROR_CHECK_CONT(httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Validation / Activation Error.") == ESP_OK, NULL, "HTTP server internal error.")}, "OTA end failed.");
ZH_ERROR_CHECK(esp_ota_set_boot_partition(ota_partition) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Validation / Activation Error.") == ESP_OK, ESP_FAIL, NULL, "HTTP server internal error.")}, "OTA set boot partition failed.");
{ZH_ERROR_CHECK_CONT(httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Validation / Activation Error.") == ESP_OK, NULL, "HTTP server internal error.")}, "OTA set boot partition failed.");
ZH_ERROR_CHECK(httpd_resp_sendstr(req, "Firmware update complete, rebooting now!\n") == ESP_OK, ESP_FAIL, NULL, "HTTP server internal error.");
vTaskDelay(500 / portTICK_PERIOD_MS);
esp_restart();