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

@@ -6,6 +6,7 @@
#include "uart_mux.h"
#include <ctype.h>
#include <string.h>
#include <limits.h>
@@ -20,6 +21,7 @@
#include "freertos/task.h"
#include "sdkconfig.h"
#include "ws2812_status.h"
#include "watch_config.h"
#ifndef CONFIG_WATCH_UART_MUX_CHANNELS
#define CONFIG_WATCH_UART_MUX_CHANNELS 5
@@ -46,6 +48,167 @@ 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];
static bool s_watchdog_armed[UART_MUX_MAX_CHANNELS];
static uint32_t s_total_miss_count[UART_MUX_MAX_CHANNELS];
static uint32_t s_restart_count[UART_MUX_MAX_CHANNELS];
static uint8_t uart_mux_get_miss_limit_from_config(void);
static void uart_mux_restart_channel(size_t channel);
static void uart_mux_record_miss(size_t channel, uint8_t miss_limit);
static TickType_t uart_mux_restart_delay_ticks(void)
{
const watch_config_t *cfg = watch_config_get();
uint32_t sec = cfg ? cfg->dcdc_restart_off_sec : 2U;
if (sec == 0) {
sec = 1;
}
return pdMS_TO_TICKS(sec * 1000U);
}
static uint8_t uart_mux_get_miss_limit_from_config(void)
{
const watch_config_t *cfg = watch_config_get();
uint32_t limit = cfg ? cfg->heartbeat_miss_limit : 3U;
if (limit == 0) {
limit = 3U;
}
if (limit > UINT8_MAX) {
limit = UINT8_MAX;
}
return (uint8_t)limit;
}
static void uart_mux_restart_channel(size_t channel)
{
if (channel >= UART_MUX_MAX_CHANNELS) {
return;
}
ESP_LOGW(TAG, "CH%u: перезапуск живлення після відсутності відповіді", (unsigned)channel);
dcdc_disable(channel);
vTaskDelay(uart_mux_restart_delay_ticks());
dcdc_enable(channel);
s_consecutive_miss[channel] = 0;
s_watchdog_armed[channel] = false;
if (s_restart_count[channel] < UINT32_MAX) {
++s_restart_count[channel];
}
s_last_heartbeat_us[channel] = esp_timer_get_time();
}
static void uart_mux_record_miss(size_t channel, uint8_t miss_limit)
{
if (channel >= UART_MUX_MAX_CHANNELS) {
return;
}
if (!dcdc_get_state(channel)) {
return;
}
if (!s_watchdog_armed[channel]) {
s_watchdog_armed[channel] = true;
s_consecutive_miss[channel] = 0;
}
if (s_total_miss_count[channel] < UINT32_MAX) {
++s_total_miss_count[channel];
}
if (s_consecutive_miss[channel] < UINT8_MAX) {
++s_consecutive_miss[channel];
}
if (miss_limit == 0) {
miss_limit = 1;
}
if (s_consecutive_miss[channel] >= miss_limit) {
uart_mux_restart_channel(channel);
}
}
static bool uart_mux_extract_numeric_field(const uint8_t *data,
size_t length,
const char *key,
int *out_value)
{
if (!data || !key || !out_value) {
return false;
}
const size_t key_len = strlen(key);
if (key_len == 0) {
return false;
}
for (size_t i = 0; i < length; ++i) {
if (data[i] != '"') {
continue;
}
size_t j = i + 1;
if (j + key_len >= length) {
break;
}
if (memcmp(&data[j], key, key_len) != 0) {
continue;
}
j += key_len;
if (j >= length || data[j] != '"') {
continue;
}
++j;
while (j < length && isspace((unsigned char)data[j])) {
++j;
}
if (j >= length || data[j] != ':') {
continue;
}
++j;
while (j < length && isspace((unsigned char)data[j])) {
++j;
}
bool negative = false;
if (j < length && (data[j] == '-' || data[j] == '+')) {
negative = (data[j] == '-');
++j;
}
bool has_digit = false;
int value = 0;
while (j < length && isdigit((unsigned char)data[j])) {
has_digit = true;
value = value * 10 + (data[j] - '0');
++j;
}
if (has_digit) {
*out_value = negative ? -value : value;
return true;
}
}
return false;
}
static void uart_mux_decode_status_payload(const uint8_t *data,
size_t length,
bool *hb_ack,
bool *vpn_ok,
bool *app_ok)
{
int value = 0;
if (hb_ack) {
bool found = uart_mux_extract_numeric_field(data, length, "hb", &value);
*hb_ack = found && value == 2;
}
if (vpn_ok) {
bool found = uart_mux_extract_numeric_field(data, length, "VPN", &value);
*vpn_ok = found && value != 0;
}
if (app_ok) {
bool found = uart_mux_extract_numeric_field(data, length, "APP", &value);
*app_ok = found && value != 0;
}
}
// Перевіряє, чи містить буфер відповідь {"hb":2} від Raspberry Pi.
static bool uart_mux_contains_hb_ack(const uint8_t *data, size_t length)
{
bool ack = false;
uart_mux_decode_status_payload(data, length, &ack, NULL, NULL);
return ack;
}
// Перемикає апаратний мультиплексор на вказаний канал під захистом мьютекса,
// оновлюючи таймстемп останнього heartbeat для контролю watchdog.
@@ -72,51 +235,67 @@ static esp_err_t uart_mux_select_locked(size_t channel)
static void uart_mux_watchdog_task(void *arg)
{
const TickType_t poll_interval = pdMS_TO_TICKS(10000);
const TickType_t read_timeout = pdMS_TO_TICKS(10);
const TickType_t read_timeout = pdMS_TO_TICKS(300);
const int64_t timeout_us = (int64_t)CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC * 1000000LL;
uint8_t buffer[CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN];
const watch_config_t *cfg = watch_config_get();
TickType_t start_delay = pdMS_TO_TICKS(cfg->heartbeat_start_delay_sec * 1000U);
if (start_delay > 0) {
vTaskDelay(start_delay);
}
while (true) {
vTaskDelay(poll_interval);
const watch_config_t *cfg = watch_config_get();
if (!cfg->heartbeat_monitor_enabled) {
continue;
}
uint8_t miss_limit = uart_mux_get_miss_limit_from_config();
int64_t now = esp_timer_get_time();
for (size_t ch = 0; ch < UART_MUX_MAX_CHANNELS; ++ch) {
if (!dcdc_get_state(ch)) {
s_watchdog_armed[ch] = false;
s_consecutive_miss[ch] = 0;
continue;
}
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,
sizeof(buffer),
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);
bool ack = false;
bool vpn_ok = false;
bool app_ok = false;
uart_mux_decode_status_payload(buffer, read, &ack, &vpn_ok, &app_ok);
ESP_LOGI(TAG, "UART0 RX CH%u (%d байт)%s",
(unsigned)ch, read, ack ? " [HB ACK]" : "");
ESP_LOG_BUFFER_HEX_LEVEL(TAG, buffer, read, ESP_LOG_INFO);
if (ack) {
s_last_heartbeat_us[ch] = now;
s_consecutive_miss[ch] = 0;
s_watchdog_armed[ch] = true;
ws2812_status_set_service_state(ch, vpn_ok, app_ok);
} else {
uart_mux_record_miss(ch, miss_limit);
}
} else {
uart_mux_record_miss(ch, miss_limit);
ESP_LOGD(TAG, "UART0 RX CH%u: немає відповіді у watchdog (%u)",
(unsigned)ch, (unsigned)s_consecutive_miss[ch]);
}
}
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_watchdog_armed[ch] && s_consecutive_miss[ch] >= miss_limit) {
uart_mux_restart_channel(ch);
}
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(3000));
dcdc_enable(ch);
s_last_heartbeat_us[ch] = esp_timer_get_time();
uart_mux_restart_channel(ch);
}
}
}
@@ -177,6 +356,9 @@ esp_err_t uart_mux_init(void)
for (size_t ch = 0; ch < UART_MUX_MAX_CHANNELS; ++ch) {
s_last_heartbeat_us[ch] = now;
s_consecutive_miss[ch] = 0;
s_watchdog_armed[ch] = false;
s_total_miss_count[ch] = 0;
s_restart_count[ch] = 0;
}
if (xTaskCreate(uart_mux_watchdog_task, "uart_mux_wd", 4096, NULL, 5, &s_watchdog_task) != pdPASS) {
@@ -198,7 +380,6 @@ bool uart_mux_ready(void)
return false;
#endif
}
// Кількість доступних каналів, визначених у конфігурації.
size_t uart_mux_channel_count(void)
{
@@ -274,3 +455,66 @@ esp_err_t uart_mux_read(size_t channel, uint8_t *buffer, size_t buffer_size, siz
return err;
#endif
}
void uart_mux_process_rx(size_t channel, const uint8_t *data, size_t length)
{
#if CONFIG_WATCH_UART_MUX_ENABLED
if (!s_initialized || channel >= UART_MUX_MAX_CHANNELS || !data || length == 0) {
return;
}
bool ack = false;
bool vpn_ok = false;
bool app_ok = false;
uart_mux_decode_status_payload(data, length, &ack, &vpn_ok, &app_ok);
int64_t now = esp_timer_get_time();
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(10)) == pdTRUE) {
s_last_heartbeat_us[channel] = now;
if (ack) {
s_consecutive_miss[channel] = 0;
s_watchdog_armed[channel] = true;
}
xSemaphoreGive(s_mutex);
}
if (ack) {
ws2812_status_set_service_state(channel, vpn_ok, app_ok);
} else {
uart_mux_record_miss(channel, uart_mux_get_miss_limit_from_config());
}
#else
(void)channel;
(void)data;
(void)length;
#endif
}
void uart_mux_report_miss(size_t channel)
{
#if CONFIG_WATCH_UART_MUX_ENABLED
if (!s_initialized) {
return;
}
uart_mux_record_miss(channel, uart_mux_get_miss_limit_from_config());
#else
(void)channel;
#endif
}
void uart_mux_get_channel_stats(size_t channel, uart_mux_channel_stats_t *out_stats)
{
if (!out_stats) {
return;
}
#if CONFIG_WATCH_UART_MUX_ENABLED
if (!s_initialized || channel >= UART_MUX_MAX_CHANNELS) {
out_stats->missed_heartbeats = 0;
out_stats->restart_count = 0;
return;
}
out_stats->missed_heartbeats = s_total_miss_count[channel];
out_stats->restart_count = s_restart_count[channel];
#else
(void)channel;
out_stats->missed_heartbeats = 0;
out_stats->restart_count = 0;
#endif
}