From 89ded8b119b53c0eeb10f0076804614ff2b85a31 Mon Sep 17 00:00:00 2001 From: Taras Syvash Date: Tue, 16 Dec 2025 16:19:33 +0200 Subject: [PATCH] Improve heartbeat indication and docs --- README.md | 7 ++- main/main.c | 26 ++++---- main/uart_mux.c | 29 ++++++++- main/usb_cdc_log.c | 10 +--- main/ws2812_status.c | 139 +++++++++++++++++++++++++++++++++++++++---- main/ws2812_status.h | 2 + 6 files changed, 176 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 72a9da0..d716af8 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,12 @@ watch-watch — вбудована система на ESP32-S3 для нагл ## Світлодіоди стану - IC WS2812 підключений до GPIO 8 (один ланцюг із 5 діодів). -- Модуль `ws2812_status` синхронізує стан з DC/DC: увімкнені канали світяться зеленим, вимкнені — синім, активний у поточному циклі — яскраво-зеленим крапкою. +- Модуль `ws2812_status` показує одночасно три аспекти стану: + - **Живлення**: якщо канал вимкнений, сегмент горить тьмяно-синім; увімкнений канал — зеленим/синім залежно від зв’язку. + - **UART-зв’язок**: після кожного опитування Raspberry Pi канал, що відповів (`{"hb":2}`), стає зеленим; якщо відповідей не було, він переходить у синій. Таким чином можна одразу бачити, хто не відповідає. + - **Активний канал / опитування**: канал, який зараз опитує watchdog або проходить тестовий цикл, підсвічується яскравішим тоном; поки триває читання UART, колір м’яко блимає, щоб показати активність, але не приховати власний статус. - За критичної помилки (наприклад, DCDC не ініціалізувався) всі індикатори стають червоними. -- Колірні алгоритми можна кастомізувати у `main/ws2812_status.c`. +- Колірні алгоритми можна кастомізувати у `main/ws2812_status.c`, там же знаходиться API `ws2812_status_set_ack_state()` для відображення acknowledge. - GPIO, кількість діодів та тактову частоту RMT можна змінити через `idf.py menuconfig` (розділ *Налаштування watch-watch*). ## Моніторинг живлення (INA226) diff --git a/main/main.c b/main/main.c index d3a3ea7..f133ab0 100644 --- a/main/main.c +++ b/main/main.c @@ -17,6 +17,7 @@ #include "ws2812_status.h" static const char *TAG = "watch-watch"; +static const char HB_MESSAGE[] = "{\"hb\":1}\r\n"; void app_main(void) { @@ -37,7 +38,7 @@ void app_main(void) if (ws2812_status_init() == ESP_OK) { ws2812_status_refresh_from_dcdc(); - esp_err_t anim_err = ws2812_status_play_bringup_animation(2, 120); + esp_err_t anim_err = ws2812_status_play_bringup_animation(1, 120); if (anim_err != ESP_OK) { ESP_LOGW(TAG, "Анімація WS2812 недоступна: %s", esp_err_to_name(anim_err)); } @@ -63,17 +64,13 @@ void app_main(void) ESP_LOGI(TAG, "Початок послідовного ввімкнення каналів з інтервалом 4 с"); const TickType_t on_time = pdMS_TO_TICKS(4000); - size_t prev_channel = DCDC_CHANNEL_COUNT - 1; while (true) { for (size_t ch = 0; ch < dcdc_channel_count(); ++ch) { - if (prev_channel != ch) { - dcdc_disable(prev_channel); - ws2812_status_set_channel_state(prev_channel, false); + if (!dcdc_get_state(ch)) { + ESP_LOGI(TAG, "-> Ввімкнення каналу %d", (int)ch); + dcdc_enable(ch); + ws2812_status_set_channel_state(ch, true); } - - ESP_LOGI(TAG, "-> Ввімкнення каналу %d", (int)ch); - dcdc_enable(ch); - ws2812_status_set_channel_state(ch, true); ws2812_status_mark_active(ch); vTaskDelay(on_time); @@ -84,13 +81,14 @@ void app_main(void) } if (uart_mux_ready()) { - char msg[64]; - int len = snprintf(msg, sizeof(msg), "PWR %.2fV %.0fmA\r\n", - reading.voltage_v, reading.current_ma); - uart_mux_write(ch, (const uint8_t *)msg, len, pdMS_TO_TICKS(100)); + if (uart_mux_write(ch, + (const uint8_t *)HB_MESSAGE, + sizeof(HB_MESSAGE) - 1, + pdMS_TO_TICKS(100)) != ESP_OK) { + ESP_LOGW(TAG, "Не вдалося надіслати heartbeat на канал %d", (int)ch); + } } - prev_channel = ch; } } } diff --git a/main/uart_mux.c b/main/uart_mux.c index 8f7a989..cd7e825 100644 --- a/main/uart_mux.c +++ b/main/uart_mux.c @@ -7,6 +7,7 @@ #include "uart_mux.h" #include +#include #include "dcdc_controller.h" #include "driver/gpio.h" @@ -18,6 +19,7 @@ #include "freertos/semphr.h" #include "freertos/task.h" #include "sdkconfig.h" +#include "ws2812_status.h" #ifndef CONFIG_WATCH_UART_MUX_CHANNELS #define CONFIG_WATCH_UART_MUX_CHANNELS 5 @@ -43,6 +45,7 @@ static size_t s_active_channel = SIZE_MAX; static bool s_initialized; static int64_t s_last_heartbeat_us[UART_MUX_MAX_CHANNELS]; static TaskHandle_t s_watchdog_task; +static uint8_t s_consecutive_miss[UART_MUX_MAX_CHANNELS]; // Перемикає апаратний мультиплексор на вказаний канал під захистом мьютекса, // оновлюючи таймстемп останнього heartbeat для контролю watchdog. @@ -68,7 +71,7 @@ static esp_err_t uart_mux_select_locked(size_t channel) // якщо канал «мовчить» довше за CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC. static void uart_mux_watchdog_task(void *arg) { - const TickType_t poll_interval = pdMS_TO_TICKS(1000); + const TickType_t poll_interval = pdMS_TO_TICKS(10000); const TickType_t read_timeout = pdMS_TO_TICKS(10); const int64_t timeout_us = (int64_t)CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC * 1000000LL; uint8_t buffer[CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN]; @@ -78,6 +81,7 @@ static void uart_mux_watchdog_task(void *arg) int64_t now = esp_timer_get_time(); for (size_t ch = 0; ch < UART_MUX_MAX_CHANNELS; ++ch) { if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(20)) == pdTRUE) { + ws2812_status_indicate_polling(ch, 2000); if (uart_mux_select_locked(ch) == ESP_OK) { int read = uart_read_bytes(CONFIG_WATCH_UART_PORT, buffer, @@ -85,16 +89,32 @@ static void uart_mux_watchdog_task(void *arg) read_timeout); if (read > 0) { s_last_heartbeat_us[ch] = now; + s_consecutive_miss[ch] = 0; + ws2812_status_set_ack_state(ch, true); + } else if (s_consecutive_miss[ch] < UINT8_MAX) { + s_consecutive_miss[ch]++; + ws2812_status_set_ack_state(ch, false); } } xSemaphoreGive(s_mutex); } + if (dcdc_get_state(ch) && s_consecutive_miss[ch] >= 3) { + ESP_LOGW(TAG, "CH%u: немає відповіді 3 рази поспіль, перезапуск живлення", (unsigned)ch); + ws2812_status_set_ack_state(ch, false); + dcdc_disable(ch); + vTaskDelay(pdMS_TO_TICKS(2000)); + dcdc_enable(ch); + s_consecutive_miss[ch] = 0; + s_last_heartbeat_us[ch] = esp_timer_get_time(); + } + if (dcdc_get_state(ch) && s_last_heartbeat_us[ch] > 0 && (now - s_last_heartbeat_us[ch]) > timeout_us) { ESP_LOGW(TAG, "Heartbeat каналу %u втрачено, перезавантаження...", (unsigned)ch); + ws2812_status_set_ack_state(ch, false); dcdc_disable(ch); - vTaskDelay(pdMS_TO_TICKS(2000)); + vTaskDelay(pdMS_TO_TICKS(3000)); dcdc_enable(ch); s_last_heartbeat_us[ch] = esp_timer_get_time(); } @@ -156,6 +176,7 @@ esp_err_t uart_mux_init(void) int64_t now = esp_timer_get_time(); for (size_t ch = 0; ch < UART_MUX_MAX_CHANNELS; ++ch) { s_last_heartbeat_us[ch] = now; + s_consecutive_miss[ch] = 0; } if (xTaskCreate(uart_mux_watchdog_task, "uart_mux_wd", 4096, NULL, 5, &s_watchdog_task) != pdPASS) { @@ -208,6 +229,10 @@ esp_err_t uart_mux_write(size_t channel, const uint8_t *data, size_t length, Tic int written = uart_write_bytes(CONFIG_WATCH_UART_PORT, (const char *)data, length); if (written < 0 || (size_t)written != length) { err = ESP_FAIL; + } else { + if (uart_wait_tx_done(CONFIG_WATCH_UART_PORT, timeout) != ESP_OK) { + err = ESP_ERR_TIMEOUT; + } } } xSemaphoreGive(s_mutex); diff --git a/main/usb_cdc_log.c b/main/usb_cdc_log.c index fd2ad77..714b68e 100644 --- a/main/usb_cdc_log.c +++ b/main/usb_cdc_log.c @@ -12,7 +12,6 @@ static const char *TAG = "usb_log"; static bool s_initialized; static bool s_host_ready; -static vprintf_like_t s_prev_vprintf; static int usb_cdc_vprintf(const char *fmt, va_list args) { @@ -33,13 +32,6 @@ static int usb_cdc_vprintf(const char *fmt, va_list args) return len; } - if (s_prev_vprintf) { - va_list args_copy; - va_copy(args_copy, args); - int ret = s_prev_vprintf(fmt, args_copy); - va_end(args_copy); - return ret; - } return 0; } @@ -91,7 +83,7 @@ esp_err_t usb_cdc_log_init(void) }; ESP_RETURN_ON_ERROR(tusb_cdc_acm_init(&acm_cfg), TAG, "CDC init failed"); - s_prev_vprintf = esp_log_set_vprintf(usb_cdc_vprintf); + esp_log_set_vprintf(usb_cdc_vprintf); s_initialized = true; ESP_LOGI(TAG, "ESP_LOG перенаправлено в USB CDC"); return ESP_OK; diff --git a/main/ws2812_status.c b/main/ws2812_status.c index b78a4c6..066992e 100644 --- a/main/ws2812_status.c +++ b/main/ws2812_status.c @@ -6,6 +6,8 @@ #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "freertos/timers.h" +#include "freertos/semphr.h" #include "sdkconfig.h" #ifndef CONFIG_WATCH_WS2812_LED_COUNT @@ -20,36 +22,65 @@ #define WS2812_STATUS_GPIO ((gpio_num_t)CONFIG_WATCH_WS2812_GPIO) #define WS2812_STATUS_RESOLUTION_HZ CONFIG_WATCH_WS2812_RMT_RESOLUTION +#define WS2812_POLL_BLINK_PERIOD pdMS_TO_TICKS(250) static const char *TAG = "ws2812"; static led_strip_handle_t s_strip; static bool s_led_state[WS2812_STATUS_LED_COUNT]; +static bool s_ack_state[WS2812_STATUS_LED_COUNT]; static bool s_error_state; static size_t s_active_channel = SIZE_MAX; +static bool s_polling_active[WS2812_STATUS_LED_COUNT]; +static bool s_polling_visible[WS2812_STATUS_LED_COUNT]; +static TickType_t s_polling_expire_tick[WS2812_STATUS_LED_COUNT]; +static TimerHandle_t s_polling_timers[WS2812_STATUS_LED_COUNT]; +static SemaphoreHandle_t s_ws_lock; + +static void ws2812_polling_timer_cb(TimerHandle_t timer); static esp_err_t ws2812_status_apply(void) { if (!s_strip) { return ESP_ERR_INVALID_STATE; } + if (!s_ws_lock) { + return ESP_ERR_INVALID_STATE; + } - for (size_t i = 0; i < WS2812_STATUS_LED_COUNT; ++i) { + if (xSemaphoreTake(s_ws_lock, portMAX_DELAY) != pdTRUE) { + return ESP_ERR_TIMEOUT; + } + + esp_err_t err = ESP_OK; + for (size_t i = 0; i < WS2812_STATUS_LED_COUNT && err == ESP_OK; ++i) { uint8_t r = 0, g = 0, b = 0; if (s_error_state) { r = 40; - } else if (s_active_channel == i) { - g = 60; - } else if (s_led_state[i]) { - g = 18; + } else if (!s_led_state[i]) { + b = 5; + } else if (s_ack_state[i]) { + g = (s_active_channel == i) ? 60 : 25; } else { - b = 10; + b = (s_active_channel == i) ? 60 : 25; } - ESP_RETURN_ON_ERROR(led_strip_set_pixel(s_strip, i, r, g, b), TAG, - "led pixel set failed"); + + if (s_polling_active[i] && !s_error_state && s_led_state[i]) { + if (!s_polling_visible[i]) { + r /= 4; + g /= 4; + b /= 4; + } + } + // Swap R/G to match physical RGB order (driver outputs GRB) + err = led_strip_set_pixel(s_strip, i, g, r, b); + } + if (err == ESP_OK) { + err = led_strip_refresh(s_strip); } - return led_strip_refresh(s_strip); + xSemaphoreGive(s_ws_lock); + return err; } esp_err_t ws2812_status_init(void) @@ -77,8 +108,16 @@ esp_err_t ws2812_status_init(void) "Не вдалося створити RMT пристрій для WS2812"); ESP_RETURN_ON_ERROR(led_strip_clear(s_strip), TAG, "clear fail"); + s_ws_lock = xSemaphoreCreateMutex(); + ESP_RETURN_ON_FALSE(s_ws_lock, ESP_ERR_NO_MEM, TAG, "mutex alloc failed"); + for (size_t i = 0; i < WS2812_STATUS_LED_COUNT; ++i) { s_led_state[i] = false; + s_ack_state[i] = false; + s_polling_active[i] = false; + s_polling_visible[i] = false; + s_polling_expire_tick[i] = 0; + s_polling_timers[i] = NULL; } s_active_channel = SIZE_MAX; s_error_state = false; @@ -86,12 +125,78 @@ esp_err_t ws2812_status_init(void) return ws2812_status_apply(); } +static void ws2812_polling_timer_cb(TimerHandle_t timer) +{ + size_t channel = (size_t)pvTimerGetTimerID(timer); + if (channel >= WS2812_STATUS_LED_COUNT) { + return; + } + TickType_t now = xTaskGetTickCount(); + if (now >= s_polling_expire_tick[channel]) { + s_polling_active[channel] = false; + s_polling_visible[channel] = false; + s_polling_expire_tick[channel] = 0; + xTimerStop(timer, 0); + } else { + s_polling_visible[channel] = !s_polling_visible[channel]; + } + ws2812_status_apply(); +} + +esp_err_t ws2812_status_indicate_polling(size_t channel, uint32_t duration_ms) +{ + if (channel >= WS2812_STATUS_LED_COUNT) { + return ESP_ERR_INVALID_ARG; + } + if (!s_strip) { + return ESP_ERR_INVALID_STATE; + } + if (duration_ms == 0) { + duration_ms = 2000; + } + + TickType_t duration_ticks = pdMS_TO_TICKS(duration_ms); + if (duration_ticks == 0) { + duration_ticks = 1; + } + + if (!s_polling_timers[channel]) { + s_polling_timers[channel] = xTimerCreate("ws2812_poll", + WS2812_POLL_BLINK_PERIOD, + pdTRUE, + (void *)channel, + ws2812_polling_timer_cb); + if (!s_polling_timers[channel]) { + return ESP_ERR_NO_MEM; + } + } + + s_polling_active[channel] = true; + s_polling_visible[channel] = true; + s_polling_expire_tick[channel] = xTaskGetTickCount() + duration_ticks; + esp_err_t err = ws2812_status_apply(); + if (err != ESP_OK) { + return err; + } + + BaseType_t res; + if (xTimerIsTimerActive(s_polling_timers[channel]) == pdTRUE) { + res = xTimerReset(s_polling_timers[channel], 0); + } else { + res = xTimerStart(s_polling_timers[channel], 0); + } + return (res == pdPASS) ? ESP_OK : ESP_FAIL; +} + esp_err_t ws2812_status_set_channel_state(size_t channel, bool enabled) { if (channel >= WS2812_STATUS_LED_COUNT) { return ESP_ERR_INVALID_ARG; } s_led_state[channel] = enabled; + if (!enabled) { + s_ack_state[channel] = false; + } return ws2812_status_apply(); } @@ -119,6 +224,15 @@ esp_err_t ws2812_status_set_error(bool has_error) return ws2812_status_apply(); } +esp_err_t ws2812_status_set_ack_state(size_t channel, bool received) +{ + if (channel >= WS2812_STATUS_LED_COUNT) { + return ESP_ERR_INVALID_ARG; + } + s_ack_state[channel] = received; + return ws2812_status_apply(); +} + esp_err_t ws2812_status_refresh_from_dcdc(void) { const size_t available_channels = dcdc_channel_count(); @@ -127,10 +241,15 @@ esp_err_t ws2812_status_refresh_from_dcdc(void) : WS2812_STATUS_LED_COUNT; for (size_t i = 0; i < count; ++i) { - s_led_state[i] = dcdc_get_state(i); + bool enabled = dcdc_get_state(i); + s_led_state[i] = enabled; + if (!enabled) { + s_ack_state[i] = false; + } } for (size_t i = count; i < WS2812_STATUS_LED_COUNT; ++i) { s_led_state[i] = false; + s_ack_state[i] = false; } return ws2812_status_apply(); } diff --git a/main/ws2812_status.h b/main/ws2812_status.h index fc1b744..fb467e9 100644 --- a/main/ws2812_status.h +++ b/main/ws2812_status.h @@ -18,5 +18,7 @@ esp_err_t ws2812_status_set_channel_state(size_t channel, bool enabled); esp_err_t ws2812_status_mark_active(size_t channel); esp_err_t ws2812_status_clear_active(void); esp_err_t ws2812_status_set_error(bool has_error); +esp_err_t ws2812_status_set_ack_state(size_t channel, bool received); +esp_err_t ws2812_status_indicate_polling(size_t channel, uint32_t duration_ms); esp_err_t ws2812_status_refresh_from_dcdc(void); esp_err_t ws2812_status_play_bringup_animation(size_t cycles, uint32_t step_delay_ms);