Improve heartbeat indication and docs
This commit is contained in:
@@ -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)
|
||||
|
||||
26
main/main.c
26
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user