Improve heartbeat indication and docs

This commit is contained in:
2025-12-16 16:19:33 +02:00
parent f3d5e4018b
commit 89ded8b119
6 changed files with 176 additions and 37 deletions

View File

@@ -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)

View File

@@ -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;
}
}
}

View File

@@ -7,6 +7,7 @@
#include "uart_mux.h"
#include <string.h>
#include <limits.h>
#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);

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);