Improve watchdog monitoring and CLI

This commit is contained in:
2025-12-19 11:01:33 +02:00
parent 89ded8b119
commit e9933da1a4
11 changed files with 970 additions and 285 deletions

View File

@@ -22,22 +22,142 @@
#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)
#define WS2812_ANIM_REFRESH pdMS_TO_TICKS(100)
#define WS2812_ALERT_BLINK_PERIOD pdMS_TO_TICKS(200)
#define WS2812_ALERT_GAP_PERIOD pdMS_TO_TICKS(2000)
#define WS2812_VPN_ALERT_BLINKS 2
#define WS2812_APP_ALERT_BLINKS 3
#define WS2812_VPN_SECTION_TICKS (WS2812_ALERT_BLINK_PERIOD * 2U * WS2812_VPN_ALERT_BLINKS)
#define WS2812_APP_SECTION_TICKS (WS2812_ALERT_BLINK_PERIOD * 2U * WS2812_APP_ALERT_BLINKS)
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 bool s_channel_enabled[WS2812_STATUS_LED_COUNT];
static bool s_vpn_state[WS2812_STATUS_LED_COUNT];
static bool s_app_state[WS2812_STATUS_LED_COUNT];
static SemaphoreHandle_t s_ws_lock;
static TimerHandle_t s_animation_timer;
static bool s_error_state;
static bool s_startup_hold;
static TickType_t s_startup_expire_tick;
static TickType_t s_alert_cycle_epoch;
static size_t s_active_vpn_alerts;
static size_t s_active_app_alerts;
static void ws2812_polling_timer_cb(TimerHandle_t timer);
static esp_err_t ws2812_status_apply(void);
static void ws2812_animation_timer_cb(TimerHandle_t timer);
static void ws2812_status_compute_color(size_t index,
TickType_t now_ticks,
uint8_t *r,
uint8_t *g,
uint8_t *b);
static bool ws2812_recalculate_alert_counts(void);
static void ws2812_animation_timer_cb(TimerHandle_t timer)
{
(void)timer;
ws2812_status_apply();
}
static bool ws2812_startup_hold_active(TickType_t now_ticks)
{
if (!s_startup_hold) {
return false;
}
if (now_ticks >= s_startup_expire_tick) {
s_startup_hold = false;
return false;
}
return true;
}
static void ws2812_status_compute_color(size_t index,
TickType_t now_ticks,
uint8_t *r,
uint8_t *g,
uint8_t *b)
{
if (!r || !g || !b || index >= WS2812_STATUS_LED_COUNT) {
return;
}
*r = 0;
*g = 0;
*b = 0;
if (s_error_state) {
*r = 40;
return;
}
if (ws2812_startup_hold_active(now_ticks)) {
*g = 40;
return;
}
if (!s_channel_enabled[index]) {
return;
}
const bool vpn_alert = !s_vpn_state[index];
const bool app_alert = !s_app_state[index];
if (!vpn_alert && !app_alert) {
return;
}
const bool vpn_global = (s_active_vpn_alerts > 0);
const bool app_global = (s_active_app_alerts > 0);
TickType_t cycle_ticks = 0;
if (vpn_global) {
cycle_ticks += WS2812_VPN_SECTION_TICKS + WS2812_ALERT_GAP_PERIOD;
}
if (app_global) {
cycle_ticks += WS2812_APP_SECTION_TICKS + WS2812_ALERT_GAP_PERIOD;
}
if (cycle_ticks == 0) {
return;
}
TickType_t blink_period = WS2812_ALERT_BLINK_PERIOD ? WS2812_ALERT_BLINK_PERIOD : 1;
TickType_t cycle_pos = (now_ticks - s_alert_cycle_epoch) % cycle_ticks;
if (vpn_global) {
if (cycle_pos < WS2812_VPN_SECTION_TICKS) {
if (vpn_alert) {
const bool on = ((cycle_pos / blink_period) % 2U) == 0U;
if (on) {
*r = 90;
}
}
return;
}
cycle_pos -= WS2812_VPN_SECTION_TICKS;
if (cycle_pos < WS2812_ALERT_GAP_PERIOD) {
return;
}
cycle_pos -= WS2812_ALERT_GAP_PERIOD;
}
if (app_global) {
if (cycle_pos < WS2812_APP_SECTION_TICKS) {
if (app_alert) {
const bool on = ((cycle_pos / blink_period) % 2U) == 0U;
if (on) {
*r = 80;
*g = 80;
}
}
return;
}
cycle_pos -= WS2812_APP_SECTION_TICKS;
if (cycle_pos < WS2812_ALERT_GAP_PERIOD) {
return;
}
}
}
static esp_err_t ws2812_status_apply(void)
{
@@ -47,32 +167,15 @@ static esp_err_t ws2812_status_apply(void)
if (!s_ws_lock) {
return ESP_ERR_INVALID_STATE;
}
if (xSemaphoreTake(s_ws_lock, portMAX_DELAY) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
const TickType_t now_ticks = xTaskGetTickCount();
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_led_state[i]) {
b = 5;
} else if (s_ack_state[i]) {
g = (s_active_channel == i) ? 60 : 25;
} else {
b = (s_active_channel == i) ? 60 : 25;
}
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)
ws2812_status_compute_color(i, now_ticks, &r, &g, &b);
err = led_strip_set_pixel(s_strip, i, g, r, b);
}
if (err == ESP_OK) {
@@ -95,7 +198,6 @@ esp_err_t ws2812_status_init(void)
.led_model = LED_MODEL_WS2812,
.flags.invert_out = false,
};
led_strip_rmt_config_t rmt_cfg = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = WS2812_STATUS_RESOLUTION_HZ,
@@ -111,204 +213,142 @@ esp_err_t ws2812_status_init(void)
s_ws_lock = xSemaphoreCreateMutex();
ESP_RETURN_ON_FALSE(s_ws_lock, ESP_ERR_NO_MEM, TAG, "mutex alloc failed");
const TickType_t now = xTaskGetTickCount();
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_channel_enabled[i] = false;
s_vpn_state[i] = true;
s_app_state[i] = true;
}
s_active_channel = SIZE_MAX;
s_error_state = false;
s_startup_hold = false;
s_startup_expire_tick = 0;
s_alert_cycle_epoch = now;
(void)ws2812_recalculate_alert_counts();
if (!s_animation_timer) {
s_animation_timer = xTimerCreate("ws2812_anim",
WS2812_ANIM_REFRESH,
pdTRUE,
NULL,
ws2812_animation_timer_cb);
ESP_RETURN_ON_FALSE(s_animation_timer, ESP_ERR_NO_MEM, TAG, "anim timer alloc failed");
ESP_RETURN_ON_FALSE(xTimerStart(s_animation_timer, 0) == pdPASS,
ESP_FAIL,
TAG,
"anim timer start failed");
}
(void)ws2812_recalculate_alert_counts();
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 (!s_strip) {
return ESP_ERR_INVALID_STATE;
}
s_channel_enabled[channel] = enabled;
if (!enabled) {
s_ack_state[channel] = false;
s_vpn_state[channel] = true;
s_app_state[channel] = true;
}
return ws2812_status_apply();
}
esp_err_t ws2812_status_mark_active(size_t channel)
{
if (channel >= WS2812_STATUS_LED_COUNT) {
return ESP_ERR_INVALID_ARG;
}
s_active_channel = channel;
return ws2812_status_apply();
}
esp_err_t ws2812_status_clear_active(void)
{
s_active_channel = SIZE_MAX;
return ws2812_status_apply();
}
esp_err_t ws2812_status_set_error(bool has_error)
{
s_error_state = has_error;
if (has_error) {
s_active_channel = SIZE_MAX;
if (!s_strip) {
return ESP_ERR_INVALID_STATE;
}
s_error_state = has_error;
return ws2812_status_apply();
}
esp_err_t ws2812_status_set_ack_state(size_t channel, bool received)
esp_err_t ws2812_status_set_service_state(size_t channel, bool vpn_ok, bool app_ok)
{
if (channel >= WS2812_STATUS_LED_COUNT) {
return ESP_ERR_INVALID_ARG;
}
s_ack_state[channel] = received;
if (!s_strip) {
return ESP_ERR_INVALID_STATE;
}
bool changed = false;
if (s_vpn_state[channel] != vpn_ok) {
s_vpn_state[channel] = vpn_ok;
changed = true;
}
if (s_app_state[channel] != app_ok) {
s_app_state[channel] = app_ok;
changed = true;
}
if (changed) {
(void)ws2812_recalculate_alert_counts();
s_alert_cycle_epoch = xTaskGetTickCount();
}
return ws2812_status_apply();
}
esp_err_t ws2812_status_refresh_from_dcdc(void)
{
if (!s_strip) {
return ESP_ERR_INVALID_STATE;
}
const size_t available_channels = dcdc_channel_count();
const size_t count = available_channels < WS2812_STATUS_LED_COUNT
? available_channels
: WS2812_STATUS_LED_COUNT;
for (size_t i = 0; i < count; ++i) {
bool enabled = dcdc_get_state(i);
s_led_state[i] = enabled;
if (!enabled) {
s_ack_state[i] = false;
}
s_channel_enabled[i] = dcdc_get_state(i);
}
for (size_t i = count; i < WS2812_STATUS_LED_COUNT; ++i) {
s_led_state[i] = false;
s_ack_state[i] = false;
s_channel_enabled[i] = false;
s_vpn_state[i] = true;
s_app_state[i] = true;
}
(void)ws2812_recalculate_alert_counts();
s_alert_cycle_epoch = xTaskGetTickCount();
return ws2812_status_apply();
}
esp_err_t ws2812_status_play_bringup_animation(size_t cycles, uint32_t step_delay_ms)
esp_err_t ws2812_status_set_startup_hold(uint32_t duration_ms)
{
if (!s_strip) {
return ESP_ERR_INVALID_STATE;
}
if (cycles == 0) {
cycles = 1;
}
if (step_delay_ms == 0) {
step_delay_ms = 150;
if (duration_ms == 0) {
s_startup_hold = false;
s_startup_expire_tick = 0;
return ws2812_status_apply();
}
bool saved_led_state[WS2812_STATUS_LED_COUNT];
TickType_t duration_ticks = pdMS_TO_TICKS(duration_ms);
if (duration_ticks == 0) {
duration_ticks = 1;
}
s_startup_hold = true;
s_startup_expire_tick = xTaskGetTickCount() + duration_ticks;
return ws2812_status_apply();
}
static bool ws2812_recalculate_alert_counts(void)
{
size_t vpn = 0;
size_t app = 0;
for (size_t i = 0; i < WS2812_STATUS_LED_COUNT; ++i) {
saved_led_state[i] = s_led_state[i];
}
const bool saved_error = s_error_state;
const size_t saved_active = s_active_channel;
s_error_state = false;
s_active_channel = SIZE_MAX;
const TickType_t delay_ticks = pdMS_TO_TICKS(step_delay_ms);
esp_err_t err = ESP_OK;
for (size_t cycle = 0; cycle < cycles && err == ESP_OK; ++cycle) {
for (size_t led = 0; led < WS2812_STATUS_LED_COUNT; ++led) {
for (size_t idx = 0; idx < WS2812_STATUS_LED_COUNT; ++idx) {
const uint8_t g = (idx == led) ? 35 : 0;
err = led_strip_set_pixel(s_strip, idx, 0, g, 0);
if (err != ESP_OK) {
break;
}
}
if (err != ESP_OK) {
break;
}
err = led_strip_refresh(s_strip);
if (err != ESP_OK) {
break;
}
vTaskDelay(delay_ticks);
if (!s_channel_enabled[i]) {
continue;
}
if (!s_vpn_state[i]) {
++vpn;
}
if (!s_app_state[i]) {
++app;
}
}
for (size_t i = 0; i < WS2812_STATUS_LED_COUNT; ++i) {
s_led_state[i] = saved_led_state[i];
}
s_error_state = saved_error;
s_active_channel = saved_active;
esp_err_t restore_err = ws2812_status_apply();
if (err != ESP_OK) {
return err;
}
return restore_err;
const bool changed = (vpn != s_active_vpn_alerts) || (app != s_active_app_alerts);
s_active_vpn_alerts = vpn;
s_active_app_alerts = app;
return changed;
}