# Описание компонента ate0005 ## Общие сведения **ATE0005** — это прошивка ESP32 для ESP-IDF, реализующая универсальный оптический двухканальный угломер/тахометр. Приложение измеряет угол поворота оси и/или скорость её вращения по двум независимым каналам A и B, подключённым к оптическим энкодерам, и выводит результат на LCD-экран 16x2. Управление приборами осуществляется поворотным энкодером MODE с кнопкой. ### Основные возможности 1. **Два независимых канала** — каналы A и B работают с собственными оптическими энкодерами. 2. **Четыре режима работы** — `ANGLE/ANGLE`, `TACHO/TACHO`, `ANGLE/TACHO`, `REMOTE`. 3. **Измерение угла** — текущий угол с разрешением 0.1°, сброс длительным нажатием кнопки. 4. **Измерение скорости** — обороты в минуту (RPM) с обновлением каждые 100 мс. 5. **LCD 16x2 через PCF8574** — заставка, анимированный прогресс-бар загрузки, живой вывод значений. 6. **Меню на поворотном энкодере** — выбор режима поворотом, вход/выход коротким нажатием. 7. **WiFi AP+STA** — точка доступа для локального доступа и подключение к внешней сети. 8. **OTA-обновления** — встроенный веб-сервер с самопроверкой образа и откатом (rollback). 9. **Удалённый syslog** — периодическая отправка статистики задач и счётчиков ошибок драйверов. 10. **FreeRTOS-многозадачность** — отдельные задачи на каждый канал, энкодер, расширитель и статистику. ## Архитектура прошивки Приложение состоит из единственного исходного файла `main/ate0005.c` и заголовка конфигурации `main/ate0005.h`. Вся прикладная логика построена вокруг компонентов проекта (`zh_*`), которые выполняют непосредственную работу с железом; `ate0005.c` отвечает за инициализацию, коммутацию режимов и вывод. Точка входа `app_main()` выполняет последовательную инициализацию подсистем: 1. `nvs_flash_init()` с очисткой и повторной инициализацией при пустом разделе или несовпадении версии. 2. `esp_event_loop_create_default()` — общая петля событий. 3. `_zh_pcf8574_init()` — шина I2C, расширитель PCF8574, драйвер LCD, мьютекс экрана. 4. `_zh_system_load()` — заставка и прогресс-бар загрузки. 5. `_zh_encoder_init()` — энкодер выбора режима с обработчиком событий. 6. `_zh_timer_init()` — программный таймер сброса угла. 7. `_zh_a_inclinometer_init()`, `_zh_b_inclinometer_init()` — угломеры каналов A и B. 8. `_zh_wifi_init()` — WiFi в режиме AP+STA. 9. `_zh_webserver_init()` — HTTP-сервер с OTA-страницей. 10. `_zh_espnow_init()` — заготовка под ESP-NOW (пока пуста). После инициализации выполняется самопроверка OTA: если запущенный образ находится в состоянии `ESP_OTA_IMG_PENDING_VERIFY`, задача ждёт 60 секунд и помечает образ валидным через `esp_ota_mark_app_valid_cancel_rollback()`. ### Задачи FreeRTOS | Задача | Приоритет | Стек, слов | Назначение | | -------- | ----------- | ------------ | ------------ | | `_zh_a_inclinometer_processing_task` | `tskIDLE_PRIORITY + 5` | `configMINIMAL_STACK_SIZE + 512` | Вывод угла канала A | | `_zh_b_inclinometer_processing_task` | `tskIDLE_PRIORITY + 5` | `configMINIMAL_STACK_SIZE + 512` | Вывод угла канала B | | `_zh_a_tachometer_processing_task` | `tskIDLE_PRIORITY + 5` | `configMINIMAL_STACK_SIZE + 512` | Вывод RPM канала A | | `_zh_b_tachometer_processing_task` | `tskIDLE_PRIORITY + 5` | `configMINIMAL_STACK_SIZE + 512` | Вывод RPM канала B | | `_zh_encoder_event_handler` | обрабатывается в задаче `zh_encoder` | — | События энкодера и кнопки | | `_zh_pcf8574_*` (внутри `zh_pcf8574`) | `tskIDLE_PRIORITY + 4` | `configMINIMAL_STACK_SIZE` | Обмен с расширителем LCD | | `_zh_stats_processing_task` | `tskIDLE_PRIORITY` | `configMINIMAL_STACK_SIZE + 1280` | Отправка статистики в syslog | Все задачи создаются через `xTaskCreatePinnedToCore()` с `tskNO_AFFINITY`, то есть планировщик сам выбирает ядро. ## Внутреннее устройство ### Структура `_task_stats_t` Хранит минимальный свободный объём стека (high water mark) каждой измерительной задачи. Статистическая задача читает эти значения для отправки в syslog. | Поле | Тип | Описание | | ------ | ----- | ---------- | | `a_inclinometer_task_min_stack_size` | `uint32_t` | Минимальный свободный стек задачи угломера A | | `b_inclinometer_task_min_stack_size` | `uint32_t` | Минимальный свободный стек задачи угломера B | | `a_tachometer_task_min_stack_size` | `uint32_t` | Минимальный свободный стек задачи тахометра A | | `b_tachometer_task_min_stack_size` | `uint32_t` | Минимальный свободный стек задачи тахометра B | Статический экземпляр `_task_stats` инициализируется полными размерами стеков, чтобы до первого замера статистика не давала ложных предупреждений. ### Глобальные дескрипторы и флаги | Переменная | Тип | Описание | | ------------ | ----- | ---------- | | `_http_server_handle` | `httpd_handle_t` | Дескриптор HTTP-сервера | | `_lcd_handle` | `zh_pcf8574_handle_t *` | Расширитель PCF8574 | | `_lcd_160x_handle` | `zh_160x_i2c_handle_t *` | Драйвер LCD 16x2 | | `_mode_encoder_handle` | `zh_encoder_handle_t *` | Энкодер выбора режима | | `_a_inclinometer_handle`, `_b_inclinometer_handle` | `zh_inclinometer_handle_t *` | Угломеры каналов A/B | | `_a_tachometer_handle`, `_b_tachometer_handle` | `zh_tachometer_handle_t *` | Тахометры каналов A/B | | `_inclinometer_reset_timer_handle` | `esp_timer_handle_t` | Таймер сброса угла | | `_lcd_mutex` | `SemaphoreHandle_t` | Мьютекс доступа к LCD | | `_is_menu_mode` | `volatile bool` | Флаг нахождения в меню выбора режима | | `_is_reset_mode` | `volatile bool` | Флаг нажатия кнопки (идёт отсчёт долгого нажатия) | | `_is_reset_occurred` | `volatile bool` | Признак того, что сброс уже выполнен, чтобы отпустить кнопку «вхолостую» | | `_current_menu_value` | `volatile uint8_t` | Индекс выбранного режима | | `_wifi_reconnect_retry_num` | `volatile uint8_t` | Счётчик попыток переподключения STA | | `_is_wifi_connected` | `volatile bool` | Флаг подключения STA | Все разделяемые между задачами и обработчиками прерываний флаги объявлены `volatile`, поскольку изменяются из разных контекстов без блокировок. ### Константы конфигурации Режимы работы заданы константами и подписями меню: | Константа | Значение | Подпись меню | | ----------- | ---------- | -------------- | | `MODE_ANGLE_ANGLE` | `0` | `"ANGLE/ANGLE"` | | `MODE_TACHO_TACHO` | `1` | `"TACHO/TACHO"` | | `MODE_ANGLE_TACHO` | `2` | `"ANGLE/TACHO"` | | `MODE_REMOTE` | `3` | `"REMOTE"` | Массив `_main_menu[]` хранит подписи в порядке их индексов; энкодер режима ограничен диапазоном `0 .. sizeof(_main_menu)/sizeof(_main_menu[0]) - 1`. ### Распиновка GPIO | Периферия | Вывод A | Вывод B | Вывод S | Примечание | | ----------- | --------- | --------- | --------- | ------------ | | I2C-шина | `SDA = GPIO_NUM_21` | `SCL = GPIO_NUM_22` | — | Внутренние подтяжки включены | | Расширитель LCD | адрес `0x20` | — | — | PCF8574 (U2) | | Энкодер MODE | `GPIO_NUM_27` | `GPIO_NUM_26` | `GPIO_NUM_25` | Разъём EN1, экземпляр `1` | | Оптический энкодер A | `GPIO_NUM_14` | `GPIO_NUM_13` | — | Разъём EN2 | | Оптический энкодер B | `GPIO_NUM_18` | `GPIO_NUM_19` | — | Разъём EN3 | Оптическое устройство каждого канала общее для угломера и тахометра этого канала: они не могут работать одновременно на одном канале. ## API (интерфейс) Прикладной код `ate0005.c` не экспортирует публичных функций — это законченное приложение, а не библиотека. Все функции модуля объявлены `static` и вызываются только из `app_main()` и обработчиков событий. Ниже приведена их роль как внутренняя справка. ### Инициализация подсистем | Функция | Описание | | --------- | ---------- | | `app_main()` | Точка входа: NVS, событийная петля, LCD, энкодер, таймер, угломеры, WiFi, веб-сервер, ESP-NOW, самопроверка OTA. | | `_zh_pcf8574_init()` | Создаёт шину I2C, инициализирует PCF8574 и LCD 16x2, создаёт мьютекс экрана. | | `_zh_encoder_init()` | Регистрирует обработчик энкодера, задаёт GPIO, диапазон и шаг, сбрасывает позицию в 0. | | `_zh_timer_init()` | Создаёт одноразовый программный таймер сброса угла. | | `_zh_a_inclinometer_init()` | Инициализирует угломер канала A и создаёт его задачу. | | `_zh_b_inclinometer_init()` | Инициализирует угломер канала B и создаёт его задачу. | | `_zh_a_tachometer_init()` | Инициализирует тахометр канала A и создаёт его задачу. | | `_zh_b_tachometer_init()` | Инициализирует тахометр канала B и создаёт его задачу. | | `_zh_wifi_init()` | Настраивает AP и STA, регистрирует обработчики событий WiFi/IP, запускает WiFi. | | `_zh_webserver_init()` | Запускает HTTP-сервер и регистрирует OTA-страницу на `"/"`. | | `_zh_espnow_init()` | Заготовка под ESP-NOW, тело пусто. | ### Обработчики событий | Функция | Описание | | --------- | ---------- | | `_zh_encoder_event_handler()` | Обрабатывает нажатие/отпускание кнопки и поворот энкодера: переключение меню, сброс угла. | | `_zh_wifi_event_handler()` | Подключение STA, переподключение с ограничением попыток, инициализация syslog и создание задачи статистики при получении IP. | | `_zh_espnow_event_handler()` | Заготовка под ESP-NOW, тело пусто. | | `_zh_inclinometer_reset_timer_on_alarm_cb()` | Колбэк долгого нажатия: сбрасывает угол активных угломеров текущего режима. | ### Задачи обработки и вывода | Функция | Описание | | --------- | ---------- | | `_zh_a_inclinometer_processing_task()` | Выводит угол канала A на 2-ю строку LCD с 1 знаком после запятой. | | `_zh_b_inclinometer_processing_task()` | Выводит угол канала B на 2-ю строку LCD с 1 знаком после запятой. | | `_zh_a_tachometer_processing_task()` | Выводит RPM канала A на 2-ю строку LCD. | | `_zh_b_tachometer_processing_task()` | Выводит RPM канала B на 2-ю строку LCD. | | `_zh_stats_processing_task()` | Периодически отправляет свободный стек задач и счётчики ошибок в syslog. | ### Служебные функции вывода | Функция | Описание | |---------|----------| | `_zh_system_load()` | Показывает заставку с названием и версией, затем прогресс-бар загрузки. | ## Потокобезопасность 1. **Мьютекс `_lcd_mutex`** — защищает последовательности обращений к LCD (`set_cursor` → `print_*`) от одновременного доступа нескольких задач. Захватывается на время вывода значения и освобождается сразу после. 2. **`volatile` флаги состояния** — `_is_menu_mode`, `_is_reset_mode`, `_is_reset_occurred`, `_current_menu_value` изменяются из обработчика событий энкодера и читаются из задач и колбэка таймера; прямой доступ без блокировки допустим для однобайтовых флагов. 3. **`_task_stats`** — каждая измерительная задача пишет только своё поле, читатель (статистическая задача) только читает; гонка исключена за счёт разделения полей. 4. **Событийная модель `zh_encoder`** — прерывания энкодера постят события в очередь, обработка идёт в задаче компонента, а не в ISR. ## Алгоритмы ### Переключение режима работы 1. Короткое нажатие кнопки энкодера инвертирует `_is_menu_mode`. 2. При входе в меню текущие активные устройства режима останавливаются: удаляются соответствующие задачи через `vTaskDelete()`, затем вызываются `zh_inclinometer_deinit()` / `zh_tachometer_deinit()`. 3. Включается курсор LCD, очищается 2-я строка, на 1-й отображается текущая позиция меню. 4. При выходе из меню курсор выключается, инициализируются устройства выбранного режима и запускаются их задачи. 5. Поворот энкодера в меню обновляет `_current_menu_value` и подпись режима; вне меню позиция энкодера принудительно возвращается к `_current_menu_value` через `zh_encoder_set()`. ### Сброс угла долгим нажатием 1. При нажатии кнопки запускается одноразовый таймер на 2 секунды и выставляется `_is_reset_mode`. 2. Если кнопка отпущена раньше 2 секунд, таймер останавливается, событие считается коротким нажатием (переключение меню). 3. Если таймер сработал, колбэк выставляет `_is_reset_occurred` и вызывает `zh_inclinometer_reset()` для активных угломеров режима (в `ANGLE/ANGLE` — оба, в `ANGLE/TACHO` — только A). 4. Последующее отпускание кнопки игнорируется, если сброс уже произошёл (`_is_reset_occurred == true`). ### Обновление значения на LCD 1. Задача кэширует предыдущее показание (`prev_angle` / `prev_rpm`). 2. Каждые 100 мс читается новое значение через `zh_inclinometer_get()` / `zh_tachometer_get()`. 3. Значение перерисовывается только при изменении: сначала область значения затишается пробелами, затем печатается новое число. Это снижает нагрузку на I2C и мигание экрана. ### Статистика и syslog 1. Задача статистики создаётся только после получения STA-адреса (`IP_EVENT_STA_GOT_IP`). 2. Каждые ~60 секунд (с промежуточными задержками по 500 мс) отправляются сообщения о свободном стеке каждой задачи и о счётчиках ошибок `zh_encoder` и `zh_pcf8574`. 3. Уровень `ZH_WARNING` ставится, если свободный стек задачи меньше 100 слов или счётчик ошибок ненулевой; иначе `ZH_INFO`. 4. При разрыве WiFi задача статистики удаляется, syslog деинициализируется. ## Примеры использования ### Инициализация угломера канала ```c #include "zh_inclinometer.h" static zh_inclinometer_handle_t *_a_inclinometer_handle = NULL; void app_main(void) { zh_inclinometer_init_config_t config = ZH_INCLINOMETER_INIT_CONFIG_DEFAULT(); config.a_gpio_number = A_OPTICAL_DEVICE_A_GPIO; // GPIO_NUM_14 config.b_gpio_number = A_OPTICAL_DEVICE_B_GPIO; // GPIO_NUM_13 config.encoder_pulses = 3600; // импульсов на оборот zh_inclinometer_init(&config, &_a_inclinometer_handle); // ... использование ... zh_inclinometer_deinit(&_a_inclinometer_handle); // освобождение ресурсов } ``` ### Вывод угла на LCD под мьютексом ```c #include "zh_160x_i2c.h" static zh_160x_i2c_handle_t *_lcd_160x_handle = NULL; static SemaphoreHandle_t _lcd_mutex = NULL; void app_main(void) { _lcd_mutex = xSemaphoreCreateMutex(); float current_angle = 0.0; for (;;) { // читаем угол и выводим только при изменении zh_inclinometer_get(&_a_inclinometer_handle, ¤t_angle); xSemaphoreTake(_lcd_mutex, portMAX_DELAY); zh_160x_set_cursor(&_lcd_160x_handle, 1, 2); zh_160x_print_float(&_lcd_160x_handle, current_angle, 1); xSemaphoreGive(_lcd_mutex); vTaskDelay(100 / portTICK_PERIOD_MS); } vSemaphoreDelete(_lcd_mutex); } ``` ### Инициализация удалённого syslog ```c #include "zh_syslog.h" void app_main(void) { zh_syslog_init_config_t config = ZH_SYSLOG_INIT_CONFIG_DEFAULT(); strcpy(config.syslog_ip, SYSLOG_IP); // адрес сервера из ate0005.h esp_err_t err = zh_syslog_init(&config); if (err != ESP_OK) { printf("Syslog initialization error %s\n", esp_err_to_name(err)); return; } zh_syslog_send(ZH_KERN, ZH_INFO, "ATE0005", "zh_syslog", "Stats task started.\n"); zh_syslog_deinit(); // освобождение ресурсов } ``` ## Конфигурация сборки Прошивка опирается на следующие параметры `sdkconfig.defaults`: | Параметр | Значение | Назначение | | ---------- | ---------- | ------------ | | `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` | `y` | Разрешает OTA-откат при непройденной самопроверке | | `CONFIG_BOOTLOADER_LOG_LEVEL_NONE` | `y` | Отключает лог загрузчика | | `CONFIG_ESPTOOLPY_FLASHSIZE_8MB` | `8MB` | Размер флеш под две OTA-партиции | | `CONFIG_PARTITION_TABLE_CUSTOM` | `y` | Используется `partitions.csv` | | `CONFIG_COMPILER_OPTIMIZATION_SIZE` | `y` | Оптимизация по размеру | | `CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240` | `240` | Максимальная частота CPU | | `CONFIG_FREERTOS_HZ` | `1000` | Тик 1 мс для точных задержек | | `CONFIG_LOG_DEFAULT_LEVEL_NONE` | `y` | Отключает ESP-IDF лог по умолчанию | ### Партиции (`partitions.csv`) | Имя | Тип | Подтип | Размер | Назначение | | ----- | ----- | -------- | -------- | ------------ | | `nvs` | data | nvs | `0x6000` | Хранилище настроек | | `otadata` | data | ota | `0x2000` | Состояние OTA | | `phy_init` | data | phy | `0x1000` | Калибровка PHY | | `ota_0` | app | ota_0 | 3500K | Образ прошивки A | | `ota_1` | app | ota_1 | 3500K | Образ прошивки B | | `storage` | data | fat | 1M | Файловое хранилище | ## Ошибки Прикладной код не возвращает пользовательских кодов ошибок и полагается на коды ESP-IDF, транслируемые компонентами `zh_*`. Ниже типовые коды, встречающиеся в путях инициализации. | Код | Описание | | ----- | ---------- | | `ESP_OK` | Операция выполнена успешно | | `ESP_ERR_INVALID_ARG` | Неверный аргумент функции (например, нулевой дескриптор) | | `ESP_ERR_NO_MEM` | Недостаточно памяти | | `ESP_ERR_NOT_FOUND` | Устройство или партиция не найдены | | `ESP_ERR_TIMEOUT` | Таймаут операции (I2C, WiFi) | | `ESP_ERR_NVS_NO_FREE_PAGES` | Раздел NVS переполнен (требует очистки) | | `ESP_ERR_NVS_NEW_VERSION_FOUND` | Версия формата NVS не совпадает (требует очистки) | ## Ограничения 1. **Режим `REMOTE` не реализован** — функции `_zh_espnow_init()` и `_zh_espnow_event_handler()` имеют пустое тело, поэтому в этом режиме на экране выводится `NOT CONNECTED`. 2. **Общее оптическое устройство на канал** — угломер и тахометр одного канала используют одни GPIO, поэтому не могут работать одновременно; при переключении режима предыдущие устройства деинициализируются. 3. **WiFi-кредиты зашиты в прошивку** — `WIFI_STA_SSID` и `WIFI_STA_PASS` заданы константами в `ate0005.h` и требуют пересборки для смены сети. 4. **Жёсткая распиновка GPIO** — номера выводов соответствуют разъёмам платы ATE0005 и не должны меняться без аппаратной ревизии. 5. **Самопроверка OTA блокирует на 60 секунд** — при первом запуске после OTA-обновления `app_main()` выдерживает задержку перед подтверждением образа. 6. **Порог предупреждения стека — 100 слов** — статистика выдаёт `ZH_WARNING` при свободном стеке ниже 100 слов; значение подобрано под конкретные размеры стеков задач. ## Требования для сборки - ESP-IDF версии 5.0 или выше. - FreeRTOS (входит в ESP-IDF). - Компоненты проекта: `zh_160x_i2c`, `zh_encoder`, `zh_inclinometer`, `zh_tachometer`, `zh_espnow`, `zh_ota_server`, `zh_syslog`. - Встроенные компоненты ESP-IDF: `esp_wifi`, `nvs_flash`, `esp_timer`, `esp_app_format`. - Флеш-память 8 МБ под две OTA-партиции по 3500K. ## Установка ```bash cd your_projects_folder git clone https://github.com/aZholtikov/ate0005 cd ate0005 idf.py set-target esp32 idf.py build idf.py -p /dev/ttyUSB0 flash monitor ``` Зависимости-компоненты подтягиваются автоматически из `main/idf_component.yml` при первой сборке. Для подключения в собственном коде используется единый заголовок конфигурации: ```c #include "ate0005.h" ```