Files
watch-watch/main/uart_mux.c

521 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Developed by TComLab
* Version: v0.1
* Date: 2025-12-15
*/
#include "uart_mux.h"
#include <ctype.h>
#include <string.h>
#include <limits.h>
#include "dcdc_controller.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_check.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#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
#endif
#ifndef CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC
#define CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC 60
#endif
#if CONFIG_WATCH_UART_MUX_ENABLED
#define UART_MUX_MAX_CHANNELS CONFIG_WATCH_UART_MUX_CHANNELS
static const char *TAG = "uart_mux";
static const gpio_num_t s_select_pins[3] = {
CONFIG_WATCH_UART_MUX_SEL_A0,
CONFIG_WATCH_UART_MUX_SEL_A1,
CONFIG_WATCH_UART_MUX_SEL_A2,
};
static SemaphoreHandle_t s_mutex;
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];
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.
static esp_err_t uart_mux_select_locked(size_t channel)
{
if (channel >= UART_MUX_MAX_CHANNELS) {
return ESP_ERR_INVALID_ARG;
}
if (channel == s_active_channel) {
return ESP_OK;
}
for (int bit = 0; bit < 3; ++bit) {
int level = (channel >> bit) & 0x1;
ESP_RETURN_ON_ERROR(gpio_set_level(s_select_pins[bit], level), TAG,
"GPIO set failed");
}
s_active_channel = channel;
s_last_heartbeat_us[channel] = esp_timer_get_time();
return ESP_OK;
}
// Періодично опитує всі канали, щоб зчитати heartbeat та перезапускає DCDC,
// якщо канал «мовчить» довше за CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC.
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(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) {
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) {
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_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) {
uart_mux_restart_channel(ch);
}
}
}
}
#endif // CONFIG_WATCH_UART_MUX_ENABLED
// Налаштовує GPIO-вибірники, драйвер UART та створює watchdog-задачу для
// мультиплексора; повторний виклик просто повертає ESP_OK.
esp_err_t uart_mux_init(void)
{
#if !CONFIG_WATCH_UART_MUX_ENABLED
return ESP_ERR_NOT_SUPPORTED;
#else
if (s_initialized) {
return ESP_OK;
}
gpio_config_t io_conf = {
.intr_type = GPIO_INTR_DISABLE,
.mode = GPIO_MODE_OUTPUT,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pin_bit_mask = 0,
};
for (int i = 0; i < 3; ++i) {
io_conf.pin_bit_mask = 1ULL << s_select_pins[i];
ESP_RETURN_ON_ERROR(gpio_config(&io_conf), TAG, "GPIO config failed");
ESP_RETURN_ON_ERROR(gpio_set_level(s_select_pins[i], 0), TAG, "GPIO init level failed");
}
s_active_channel = 0;
uart_config_t uart_cfg = {
.baud_rate = CONFIG_WATCH_UART_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB,
};
ESP_RETURN_ON_ERROR(uart_param_config(CONFIG_WATCH_UART_PORT, &uart_cfg), TAG, "UART config failed");
ESP_RETURN_ON_ERROR(uart_set_pin(CONFIG_WATCH_UART_PORT,
CONFIG_WATCH_UART_TX_GPIO,
CONFIG_WATCH_UART_RX_GPIO,
UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE),
TAG, "UART pin assign failed");
ESP_RETURN_ON_ERROR(uart_driver_install(CONFIG_WATCH_UART_PORT,
CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN * 2,
CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN * 2,
0, NULL, 0),
TAG, "UART driver install failed");
s_mutex = xSemaphoreCreateMutex();
ESP_RETURN_ON_FALSE(s_mutex, ESP_ERR_NO_MEM, TAG, "mutex alloc failed");
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;
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) {
return ESP_ERR_NO_MEM;
}
s_initialized = true;
ESP_LOGI(TAG, "UART мультиплексор активовано, каналів: %d", UART_MUX_MAX_CHANNELS);
return ESP_OK;
#endif
}
// Повертає ознаку ініціалізації модулю UART мультиплексора.
bool uart_mux_ready(void)
{
#if CONFIG_WATCH_UART_MUX_ENABLED
return s_initialized;
#else
return false;
#endif
}
// Кількість доступних каналів, визначених у конфігурації.
size_t uart_mux_channel_count(void)
{
return CONFIG_WATCH_UART_MUX_CHANNELS;
}
// Перемикається на канал, передає буфер даних через загальний UART та
// захищає доступ до шини мьютексом, щоб уникнути гонок між задачами.
esp_err_t uart_mux_write(size_t channel, const uint8_t *data, size_t length, TickType_t timeout)
{
#if !CONFIG_WATCH_UART_MUX_ENABLED
return ESP_ERR_NOT_SUPPORTED;
#else
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (channel >= UART_MUX_MAX_CHANNELS) {
return ESP_ERR_INVALID_ARG;
}
if (!data || length == 0) {
return ESP_OK;
}
if (xSemaphoreTake(s_mutex, timeout) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
esp_err_t err = uart_mux_select_locked(channel);
if (err == ESP_OK) {
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);
return err;
#endif
}
// Читає дані з вказаного каналу, оновлюючи heartbeat під час успішного
// зчитування, і повертає кількість байтів через out_length.
esp_err_t uart_mux_read(size_t channel, uint8_t *buffer, size_t buffer_size, size_t *out_length, TickType_t timeout)
{
#if !CONFIG_WATCH_UART_MUX_ENABLED
return ESP_ERR_NOT_SUPPORTED;
#else
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
if (channel >= UART_MUX_MAX_CHANNELS || !buffer || buffer_size == 0) {
return ESP_ERR_INVALID_ARG;
}
if (xSemaphoreTake(s_mutex, timeout) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
esp_err_t err = uart_mux_select_locked(channel);
if (err == ESP_OK) {
int read = uart_read_bytes(CONFIG_WATCH_UART_PORT, buffer, buffer_size, timeout);
if (read < 0) {
err = ESP_FAIL;
} else {
if (out_length) {
*out_length = (size_t)read;
}
if (read > 0) {
s_last_heartbeat_us[channel] = esp_timer_get_time();
}
}
}
xSemaphoreGive(s_mutex);
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
}