Improve watchdog monitoring and CLI
This commit is contained in:
38
README.md
38
README.md
@@ -4,8 +4,8 @@ watch-watch — вбудована система на ESP32-S3 для нагл
|
||||
|
||||
## Основні можливості
|
||||
- **Керування каналами живлення**: 5 незалежних ліній `EN` (GPIO 2, 4, 5, 18, 19), які можна увімкнути, вимкнути або перемкнути з коду чи CLI.
|
||||
- **Послідовний автотест**: у `app_main` реалізовано базову логіку — канали вмикаються по черзі з інтервалом 4 с, що дозволяє перевірити всі DC/DC.
|
||||
- **Світлодіодний індикатор стану**: п’ять WS2812 (GPIO 8) показують роботу каналів — активний канал підсвічується яскраво-зеленим, увімкнені/вимкнені відображаються зеленим/синім, помилки — червоним.
|
||||
- **Послідовний автотест**: у `app_main` реалізовано базову логіку — канали вмикаються по черзі з інтервалом 3 с, що дозволяє перевірити всі DC/DC без стрибків споживання.
|
||||
- **Світлодіодний індикатор стану**: п’ять WS2812 (GPIO 8) світяться зеленим під час стартової затримки, після чого кожен канал сигналізує лише про дві події — відсутність VPN (два червоних блимання) та падіння APP (три жовтих блимання).
|
||||
- **Моніторинг навантаження**: датчики INA226 вимірюють напругу, струм та потужність кожного каналу, інформація потрапляє в лог і CLI.
|
||||
- **UART взаємодія з Raspberry Pi**: один UART через мультиплексор (A0/A1/A2) ділиться між п’ятьма Pi, дозволяючи надсилати службові повідомлення або обмінюватися даними.
|
||||
- **Нативний USB-CLI**: ESP32-S3 підключається до Raspberry Pi 5 по USB і стає CDC ACM пристроєм; командний інтерфейс дозволяє керувати каналами та дивитись стан у реальному часі.
|
||||
@@ -47,12 +47,12 @@ watch-watch — вбудована система на ESP32-S3 для нагл
|
||||
|
||||
## Світлодіоди стану
|
||||
- IC WS2812 підключений до GPIO 8 (один ланцюг із 5 діодів).
|
||||
- Модуль `ws2812_status` показує одночасно три аспекти стану:
|
||||
- **Живлення**: якщо канал вимкнений, сегмент горить тьмяно-синім; увімкнений канал — зеленим/синім залежно від зв’язку.
|
||||
- **UART-зв’язок**: після кожного опитування Raspberry Pi канал, що відповів (`{"hb":2}`), стає зеленим; якщо відповідей не було, він переходить у синій. Таким чином можна одразу бачити, хто не відповідає.
|
||||
- **Активний канал / опитування**: канал, який зараз опитує watchdog або проходить тестовий цикл, підсвічується яскравішим тоном; поки триває читання UART, колір м’яко блимає, щоб показати активність, але не приховати власний статус.
|
||||
- За критичної помилки (наприклад, DCDC не ініціалізувався) всі індикатори стають червоними.
|
||||
- Колірні алгоритми можна кастомізувати у `main/ws2812_status.c`, там же знаходиться API `ws2812_status_set_ack_state()` для відображення acknowledge.
|
||||
- Після старту всі п’ять діодів світяться сталим зеленим протягом затримки `heartbeat_start_delay_sec`, щоб показати фазу ініціалізації.
|
||||
- Після завершення затримки вся стрічка гасне, а кожен канал індикує лише два типи подій:
|
||||
- VPN=0 — два червоних блимання по 200 мс із паузою між циклами;
|
||||
- APP=0 — три жовтих блимання по 200 мс.
|
||||
Якщо активні обидва попередження, вони програються послідовно (спочатку VPN, потім APP) із паузою 2 с між послідовностями.
|
||||
- При критичній помилці (наприклад, DCDC не ініціалізувався) всі індикатори залишаються червоними.
|
||||
- GPIO, кількість діодів та тактову частоту RMT можна змінити через `idf.py menuconfig` (розділ *Налаштування watch-watch*).
|
||||
|
||||
## Моніторинг живлення (INA226)
|
||||
@@ -87,11 +87,29 @@ watch-watch — вбудована система на ESP32-S3 для нагл
|
||||
| `sense` | виміряти загальну напругу/струм/потужність |
|
||||
| `uart send <n> <msg>` | надіслати повідомлення у Raspberry Pi `n` |
|
||||
| `uart read <n> [len]` | прочитати відповідь від Raspberry Pi `n` |
|
||||
| `uart send <n> <msg>` | відправити текст у Raspberry Pi №n |
|
||||
| `uart read <n> [len]` | прочитати дані з цільового Pi |
|
||||
| `config show` | переглянути таймінги heartbeat/DCDC |
|
||||
| `config set …` | змінити та зберегти таймінги / моніторинг |
|
||||
| `reset` | м’яке перезавантаження ESP32-S3 |
|
||||
| `bootloader` | перезавантажити ESP32-S3 у ROM bootloader для `esptool.py` |
|
||||
|
||||
CLI з’являється після того, як Raspberry Pi встановить DTR (наприклад, через `screen`, `picocom` або власний скрипт).
|
||||
|
||||
### Керування таймінгами heartbeat
|
||||
Команда `config` дозволяє зберігати параметри роботи watchdog та циклу heartbeat у NVS, і вони одразу набувають чинності без перезбирання прошивки:
|
||||
|
||||
1. `hb_period` — інтервал (сек) між опитуваннями/heartbeat у головному циклі.
|
||||
2. `dcdc_off` — тривалість вимкнення каналу DCDC при автоматичному перезапуску (сек).
|
||||
3. `hb_start` — затримка перед стартом опитування після завантаження (сек); застосовується як у головному циклі, так і у watchdog-завданні.
|
||||
4. `hb_monitor` — значення `1` вмикає моніторинг heartbeat (за замовчуванням), `0` вимикає лише контроль/рестарти каналів, але тестові повідомлення heartbeat продовжують відправлятися.
|
||||
5. `hb_miss` — кількість послідовних запитів без відповіді перед автоматичним перезапуском каналу (за замовчуванням 3). Значення зберігає watchdog та використовується в CLI для відображення статистики пропусків/рестартів.
|
||||
|
||||
### Прошивка без натискання BOOT
|
||||
1. Під’єднайтесь до CLI та виконайте команду `bootloader`. ESP32-S3 перезавантажиться у ROM bootloader, а на хості з’явиться USB-пристрій `USB JTAG/serial`.
|
||||
2. На Raspberry Pi запустіть `esptool.py` або `idf.py flash` і вкажіть новий порт (`/dev/ttyACM*` або `/dev/cu.usbmodem*`). Наприклад:
|
||||
`esptool.py --chip esp32s3 --port /dev/ttyACM0 --before usb_reset --after no_reset write_flash 0x0 build/watch-watch.bin`
|
||||
3. Після завершення прошивки виконайте `esptool.py --after hard_reset reset` або просто перезавантажте живлення — пристрій вийде з bootloader і повернеться до нормальної роботи.
|
||||
Таким чином процедура оновлення доступна без натискання кнопки BOOT і може виконуватись безпосередньо з підключеної Raspberry Pi.
|
||||
|
||||
## Збірка та конфігурація
|
||||
1. Встановіть ESP-IDF v5.5.1 (шлях `IDF_PATH` має вказувати на `/Users/tarassivas/esp/v5.5.1/esp-idf`).
|
||||
2. Один раз виконайте `idf.py reconfigure`, щоб підвантажити залежності з `idf_component.yml`.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
idf_component_register(SRCS "main.c"
|
||||
idf_component_register(SRCS "ws2812_status.c" "watch_config.c" "usb_cdc_cli.c" "main.c"
|
||||
"dcdc_controller.c"
|
||||
"usb_cdc_log.c"
|
||||
"ws2812_status.c"
|
||||
|
||||
"ina226_monitor.c"
|
||||
"uart_mux.c"
|
||||
INCLUDE_DIRS ".")
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES nvs_flash driver esp_timer)
|
||||
|
||||
158
main/main.c
158
main/main.c
@@ -5,45 +5,80 @@
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "dcdc_controller.h"
|
||||
#include "ina226_monitor.h"
|
||||
#include "uart_mux.h"
|
||||
#include "usb_cdc_cli.h"
|
||||
#include "usb_cdc_log.h"
|
||||
#include "watch_config.h"
|
||||
#include "ws2812_status.h"
|
||||
|
||||
static const char *TAG = "watch-watch";
|
||||
static const char HB_MESSAGE[] = "{\"hb\":1}\r\n";
|
||||
static const char HB_MESSAGE[] = "{\"cmd\":\"status\"}\r\n";
|
||||
|
||||
static int noop_vprintf(const char *fmt, va_list args)
|
||||
{
|
||||
(void)fmt;
|
||||
(void)args;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
if (usb_cdc_log_init() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "USB CDC лог недоступний");
|
||||
esp_log_set_vprintf(noop_vprintf);
|
||||
|
||||
if (watch_config_init() != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Не вдалося ініціалізувати конфігурацію");
|
||||
} else {
|
||||
ESP_LOGI(TAG, "USB CDC лог активовано");
|
||||
ESP_LOGI(TAG, "Конфігурацію завантажено");
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(2000)); // Затримка для стабілізації живлення після перезавантаження
|
||||
|
||||
if (usb_cdc_cli_init() == ESP_OK) {
|
||||
ESP_LOGI(TAG, "USB CDC CLI активовано");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Не вдалося запустити USB CLI");
|
||||
}
|
||||
|
||||
bool ws_ready = false;
|
||||
if (ws2812_status_init() == ESP_OK) {
|
||||
ws_ready = true;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "WS2812 статусний індикатор недоступний");
|
||||
}
|
||||
|
||||
const watch_config_t *cfg = watch_config_get();
|
||||
const uint32_t start_delay_ms = cfg->heartbeat_start_delay_sec * 1000U;
|
||||
TickType_t start_delay = pdMS_TO_TICKS(start_delay_ms);
|
||||
if (start_delay > 0) {
|
||||
ESP_LOGI(TAG, "Очікування %u с перед стартом опитування", cfg->heartbeat_start_delay_sec);
|
||||
if (ws_ready) {
|
||||
ws2812_status_set_startup_hold(start_delay_ms);
|
||||
}
|
||||
vTaskDelay(start_delay);
|
||||
if (ws_ready) {
|
||||
ws2812_status_set_startup_hold(0);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Запуск watch-watch systems");
|
||||
if (dcdc_init() != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Помилка ініціалізації DCDC контролера");
|
||||
ws2812_status_init();
|
||||
ws2812_status_set_error(true);
|
||||
if (ws_ready) {
|
||||
ws2812_status_set_error(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ws2812_status_init() == ESP_OK) {
|
||||
if (ws_ready) {
|
||||
ws2812_status_refresh_from_dcdc();
|
||||
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));
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "WS2812 статусний індикатор недоступний");
|
||||
}
|
||||
|
||||
if (ina226_monitor_init() == ESP_OK) {
|
||||
@@ -59,36 +94,97 @@ void app_main(void)
|
||||
ESP_LOGW(TAG, "UART мультиплексор недоступний");
|
||||
}
|
||||
|
||||
|
||||
|
||||
ESP_LOGI(TAG, "Початок послідовного ввімкнення каналів з інтервалом 4 с");
|
||||
const TickType_t on_time = pdMS_TO_TICKS(4000);
|
||||
const TickType_t mux_timeout = pdMS_TO_TICKS(600);
|
||||
TickType_t channel_next_hb[DCDC_CHANNEL_COUNT] = {0};
|
||||
bool channel_powered[DCDC_CHANNEL_COUNT] = {false};
|
||||
const TickType_t power_on_stagger = pdMS_TO_TICKS(3000);
|
||||
ESP_LOGI(TAG, "Початок циклічного опитування всіх каналів");
|
||||
|
||||
while (true) {
|
||||
for (size_t ch = 0; ch < dcdc_channel_count(); ++ch) {
|
||||
if (!dcdc_get_state(ch)) {
|
||||
cfg = watch_config_get();
|
||||
TickType_t hb_period = pdMS_TO_TICKS(cfg->heartbeat_period_sec * 1000U);
|
||||
if (hb_period == 0) {
|
||||
hb_period = 1;
|
||||
}
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
size_t channels = dcdc_channel_count();
|
||||
for (size_t ch = 0; ch < channels; ++ch) {
|
||||
bool powered = dcdc_get_state(ch);
|
||||
if (!powered) {
|
||||
channel_powered[ch] = false;
|
||||
ESP_LOGI(TAG, "-> Ввімкнення каналу %d", (int)ch);
|
||||
dcdc_enable(ch);
|
||||
ws2812_status_set_channel_state(ch, true);
|
||||
if (dcdc_enable(ch) == ESP_OK) {
|
||||
powered = true;
|
||||
channel_powered[ch] = true;
|
||||
channel_next_hb[ch] = now + hb_period;
|
||||
if (ws_ready) {
|
||||
ws2812_status_set_channel_state(ch, true);
|
||||
}
|
||||
vTaskDelay(power_on_stagger);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Не вдалося ввімкнути канал %d", (int)ch);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!channel_powered[ch]) {
|
||||
channel_powered[ch] = true;
|
||||
channel_next_hb[ch] = now + hb_period;
|
||||
continue;
|
||||
}
|
||||
|
||||
now = xTaskGetTickCount();
|
||||
if (now < channel_next_hb[ch]) {
|
||||
continue;
|
||||
}
|
||||
ws2812_status_mark_active(ch);
|
||||
vTaskDelay(on_time);
|
||||
|
||||
ina226_reading_t reading = {0};
|
||||
if (ina226_monitor_sample(&reading) == ESP_OK) {
|
||||
if (ina226_monitor_ready() && ina226_monitor_sample(&reading) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Живлення: %.2f В, %.1f мА, %.1f мВт",
|
||||
reading.voltage_v, reading.current_ma, reading.power_mw);
|
||||
}
|
||||
|
||||
if (uart_mux_ready()) {
|
||||
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);
|
||||
esp_err_t tx_err = uart_mux_write(ch,
|
||||
(const uint8_t *)HB_MESSAGE,
|
||||
sizeof(HB_MESSAGE) - 1,
|
||||
mux_timeout);
|
||||
if (tx_err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Не вдалося надіслати heartbeat на канал %d: %s",
|
||||
(int)ch, esp_err_to_name(tx_err));
|
||||
} else {
|
||||
uint8_t rx_buffer[CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN];
|
||||
size_t received = 0;
|
||||
esp_err_t rx_err = uart_mux_read(ch,
|
||||
rx_buffer,
|
||||
sizeof(rx_buffer),
|
||||
&received,
|
||||
mux_timeout);
|
||||
if (rx_err == ESP_OK) {
|
||||
if (received > 0) {
|
||||
ESP_LOGI(TAG, "RX CH%u (%u байт після heartbeat)", (unsigned)ch, (unsigned)received);
|
||||
ESP_LOG_BUFFER_HEX_LEVEL(TAG, rx_buffer, received, ESP_LOG_INFO);
|
||||
uart_mux_process_rx(ch, rx_buffer, received);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Канал %d не відповів даними на heartbeat", (int)ch);
|
||||
uart_mux_report_miss(ch);
|
||||
}
|
||||
} else if (rx_err == ESP_ERR_TIMEOUT) {
|
||||
ESP_LOGW(TAG, "Час очікування відповіді з каналу %d перевищено", (int)ch);
|
||||
uart_mux_report_miss(ch);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Помилка читання відповіді CH%u: %s",
|
||||
(unsigned)ch, esp_err_to_name(rx_err));
|
||||
uart_mux_report_miss(ch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "UART мультиплексор недоступний, очікування...");
|
||||
}
|
||||
|
||||
channel_next_hb[ch] = now + hb_period;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
}
|
||||
|
||||
290
main/uart_mux.c
290
main/uart_mux.c
@@ -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
|
||||
}
|
||||
|
||||
@@ -7,10 +7,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t missed_heartbeats;
|
||||
uint32_t restart_count;
|
||||
} uart_mux_channel_stats_t;
|
||||
|
||||
// Ініціалізує апаратний мультиплексор UART: GPIO, UART драйвер та watchdog.
|
||||
esp_err_t uart_mux_init(void);
|
||||
// Повертає true, якщо драйвер готовий приймати виклики.
|
||||
@@ -21,3 +27,9 @@ size_t uart_mux_channel_count(void);
|
||||
esp_err_t uart_mux_write(size_t channel, const uint8_t *data, size_t length, TickType_t timeout);
|
||||
// Зчитує дані з каналу, повертаючи кількість байтів через 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);
|
||||
// Повідомляє драйверу про дані, зчитані поза watchdog-ом (для оновлення станів).
|
||||
void uart_mux_process_rx(size_t channel, const uint8_t *data, size_t length);
|
||||
// Повідомляє про пропущений heartbeat (використовується основним циклом).
|
||||
void uart_mux_report_miss(size_t channel);
|
||||
// Повертає статистику по каналу (кількість пропущених відповідей та рестартів).
|
||||
void uart_mux_get_channel_stats(size_t channel, uart_mux_channel_stats_t *out_stats);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "dcdc_controller.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/task.h"
|
||||
@@ -18,8 +19,12 @@
|
||||
#include "tinyusb.h"
|
||||
#include "tusb_cdc_acm.h"
|
||||
#include "ws2812_status.h"
|
||||
#include "watch_config.h"
|
||||
#include "esp_system.h"
|
||||
#include "ina226_monitor.h"
|
||||
#include "uart_mux.h"
|
||||
#include "soc/rtc_cntl_reg.h"
|
||||
#include "soc/soc.h"
|
||||
|
||||
#define CLI_LINE_MAX_LEN 128
|
||||
#define CLI_QUEUE_LEN 8
|
||||
@@ -45,6 +50,10 @@ static bool s_host_ready;
|
||||
static void usb_cli_task(void *arg);
|
||||
static void usb_cli_process_line(char *line);
|
||||
static void usb_cli_prompt(void);
|
||||
static void usb_cli_enter_bootloader(void);
|
||||
static void usb_cli_handle_config(char *args);
|
||||
static void usb_cli_print_config(void);
|
||||
static void usb_cli_handle_reset(void);
|
||||
|
||||
static void usb_cli_write_raw(const char *data, size_t len)
|
||||
{
|
||||
@@ -112,10 +121,16 @@ static void usb_cli_print_status(void)
|
||||
usb_cli_printf("\r\nСтан каналів:\r\n");
|
||||
for (size_t i = 0; i < dcdc_channel_count(); ++i) {
|
||||
dcdc_channel_t ch = (dcdc_channel_t)i;
|
||||
usb_cli_printf(" - CH%u: %s (GPIO %d)\r\n",
|
||||
uart_mux_channel_stats_t stats = {0};
|
||||
if (uart_mux_ready()) {
|
||||
uart_mux_get_channel_stats(i, &stats);
|
||||
}
|
||||
usb_cli_printf(" - CH%u: %s (GPIO %d) | miss=%u restart=%u\r\n",
|
||||
(unsigned)i,
|
||||
dcdc_get_state(ch) ? "ON" : "OFF",
|
||||
(int)dcdc_get_gpio(ch));
|
||||
(int)dcdc_get_gpio(ch),
|
||||
(unsigned)stats.missed_heartbeats,
|
||||
(unsigned)stats.restart_count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +139,18 @@ static void usb_cli_prompt(void)
|
||||
usb_cli_write_raw(PROMPT, strlen(PROMPT));
|
||||
}
|
||||
|
||||
static void usb_cli_enter_bootloader(void)
|
||||
{
|
||||
usb_cli_printf("\r\nПерехід у режим прошивки esptool...\r\n"
|
||||
"Після перезавантаження з’явиться ROM-порт USB CDC/Serial.\r\n"
|
||||
"Запустіть esptool.py або idf.py flash та прошийте пристрій. "
|
||||
"Для виходу з bootloader виконайте 'esptool.py --after hard_reset reset' або перезавантажте живлення.\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
tinyusb_driver_uninstall();
|
||||
REG_WRITE(RTC_CNTL_OPTION1_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT);
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
static void usb_cli_handle_switch(const char *cmd, char *args)
|
||||
{
|
||||
dcdc_channel_t channel;
|
||||
@@ -246,14 +273,22 @@ static void usb_cli_process_line(char *line)
|
||||
if (strcasecmp(cmd, "help") == 0) {
|
||||
usb_cli_printf(
|
||||
"\r\nДоступні команди:\r\n"
|
||||
" help - показати довідку\r\n"
|
||||
" status - стан всіх каналів\r\n"
|
||||
" enable <n> - увімкнути канал n\r\n"
|
||||
" disable <n> - вимкнути канал n\r\n"
|
||||
" toggle <n> - перемкнути канал n\r\n"
|
||||
" sense [n] - показати напругу/струм/потужність (опц. канал)\r\n"
|
||||
" uart send <n> <msg> - відправити повідомлення Pi n\r\n"
|
||||
" uart read <n> [len] - прочитати дані з Pi n\r\n");
|
||||
" help - показати цю довідку\r\n"
|
||||
" status - показати стан усіх каналів DCDC\r\n"
|
||||
" enable <n> - увімкнути канал n (0..4)\r\n"
|
||||
" disable <n> - вимкнути канал n\r\n"
|
||||
" toggle <n> - перемкнути канал n\r\n"
|
||||
" sense - виміряти напругу/струм/потужність INA226\r\n"
|
||||
" uart send <n> <msg> - надіслати текстове повідомлення в Raspberry Pi n\r\n"
|
||||
" uart read <n> [len] - прочитати до [len] байт відповіді від Raspberry Pi n\r\n"
|
||||
" config show - показати збережені таймінги heartbeat/DCDC\r\n"
|
||||
" config set hb_period <сек> - змінити інтервал відправки heartbeat\r\n"
|
||||
" config set dcdc_off <сек> - задати тривалість вимкнення DCDC при рестарті\r\n"
|
||||
" config set hb_start <сек> - налаштувати затримку перед стартом опитування після boot\r\n"
|
||||
" config set hb_monitor <0|1> - увімкнути/вимкнути контроль відповіді heartbeat\r\n"
|
||||
" config set hb_miss <шт> - кількість пропусків відповіді до рестарту каналу\r\n"
|
||||
" reset - м'яко перезавантажити ESP32-S3\r\n"
|
||||
" bootloader - перезавантажити ESP32-S3 у ROM bootloader для esptool\r\n");
|
||||
} else if (strcasecmp(cmd, "status") == 0) {
|
||||
usb_cli_print_status();
|
||||
} else if (strcasecmp(cmd, "enable") == 0 ||
|
||||
@@ -264,11 +299,109 @@ static void usb_cli_process_line(char *line)
|
||||
usb_cli_handle_sense(save_ptr);
|
||||
} else if (strcasecmp(cmd, "uart") == 0) {
|
||||
usb_cli_handle_uart(save_ptr);
|
||||
} else if (strcasecmp(cmd, "config") == 0) {
|
||||
usb_cli_handle_config(save_ptr);
|
||||
} else if (strcasecmp(cmd, "reset") == 0) {
|
||||
usb_cli_handle_reset();
|
||||
} else if (strcasecmp(cmd, "bootloader") == 0) {
|
||||
usb_cli_enter_bootloader();
|
||||
} else {
|
||||
usb_cli_printf("\r\nНевідома команда '%s'\r\n", cmd);
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_cli_print_config(void)
|
||||
{
|
||||
const watch_config_t *cfg = watch_config_get();
|
||||
usb_cli_printf(
|
||||
"\r\nПоточні налаштування:\r\n"
|
||||
" hb_period: %u с\r\n"
|
||||
" dcdc_off: %u с\r\n"
|
||||
" hb_start_delay: %u с\r\n"
|
||||
" hb_monitor: %s\r\n"
|
||||
" hb_miss_limit: %u зап.\r\n",
|
||||
(unsigned)cfg->heartbeat_period_sec,
|
||||
(unsigned)cfg->dcdc_restart_off_sec,
|
||||
(unsigned)cfg->heartbeat_start_delay_sec,
|
||||
cfg->heartbeat_monitor_enabled ? "on" : "off",
|
||||
(unsigned)cfg->heartbeat_miss_limit);
|
||||
}
|
||||
|
||||
static void usb_cli_handle_config(char *args)
|
||||
{
|
||||
if (!args || *args == '\0') {
|
||||
usb_cli_print_config();
|
||||
return;
|
||||
}
|
||||
|
||||
char *ctx = NULL;
|
||||
char *action = strtok_r(args, " ", &ctx);
|
||||
if (!action || strcasecmp(action, "show") == 0) {
|
||||
usb_cli_print_config();
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcasecmp(action, "set") != 0) {
|
||||
usb_cli_printf("\r\nВикористання: config show | config set <hb_period|dcdc_off|hb_start|hb_monitor|hb_miss> <знач>\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
char *param = strtok_r(NULL, " ", &ctx);
|
||||
char *value_str = strtok_r(NULL, " ", &ctx);
|
||||
if (!param || !value_str) {
|
||||
usb_cli_printf("\r\nВкажіть параметр і значення в секундах\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t value = (uint32_t)strtoul(value_str, NULL, 10);
|
||||
|
||||
watch_config_t new_cfg = *watch_config_get();
|
||||
if (strcasecmp(param, "hb_period") == 0) {
|
||||
if (value == 0) {
|
||||
usb_cli_printf("\r\nЗначення має бути більше нуля\r\n");
|
||||
return;
|
||||
}
|
||||
new_cfg.heartbeat_period_sec = value;
|
||||
} else if (strcasecmp(param, "dcdc_off") == 0) {
|
||||
if (value == 0) {
|
||||
usb_cli_printf("\r\nЗначення має бути більше нуля\r\n");
|
||||
return;
|
||||
}
|
||||
new_cfg.dcdc_restart_off_sec = value;
|
||||
} else if (strcasecmp(param, "hb_start") == 0 ||
|
||||
strcasecmp(param, "hb_start_delay") == 0) {
|
||||
if (value == 0) {
|
||||
usb_cli_printf("\r\nЗначення має бути більше нуля\r\n");
|
||||
return;
|
||||
}
|
||||
new_cfg.heartbeat_start_delay_sec = value;
|
||||
} else if (strcasecmp(param, "hb_monitor") == 0) {
|
||||
if (value != 0 && value != 1) {
|
||||
usb_cli_printf("\r\nhb_monitor приймає 0 або 1\r\n");
|
||||
return;
|
||||
}
|
||||
new_cfg.heartbeat_monitor_enabled = (value != 0);
|
||||
} else if (strcasecmp(param, "hb_miss") == 0 ||
|
||||
strcasecmp(param, "hb_miss_limit") == 0) {
|
||||
if (value == 0) {
|
||||
usb_cli_printf("\r\nhb_miss має бути більше нуля\r\n");
|
||||
return;
|
||||
}
|
||||
new_cfg.heartbeat_miss_limit = value;
|
||||
} else {
|
||||
usb_cli_printf("\r\nНевідомий параметр '%s'\r\n", param);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t err = watch_config_save(&new_cfg);
|
||||
if (err == ESP_OK) {
|
||||
usb_cli_printf("\r\nПараметр оновлено\r\n");
|
||||
usb_cli_print_config();
|
||||
} else {
|
||||
usb_cli_printf("\r\nПомилка збереження конфігурації: %s\r\n", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_cli_task(void *arg)
|
||||
{
|
||||
usb_cli_msg_t msg;
|
||||
@@ -285,14 +418,17 @@ static void usb_cli_task(void *arg)
|
||||
usb_cli_process_line(line);
|
||||
line_len = 0;
|
||||
}
|
||||
usb_cli_write_raw("\r\n", 2);
|
||||
usb_cli_prompt();
|
||||
} else if (ch == 0x7F || ch == '\b') {
|
||||
if (line_len > 0) {
|
||||
line_len--;
|
||||
usb_cli_write_raw("\b \b", 3);
|
||||
}
|
||||
} else if (isprint((unsigned char)ch)) {
|
||||
if (line_len < CLI_LINE_MAX_LEN - 1) {
|
||||
line[line_len++] = ch;
|
||||
usb_cli_write_raw(&ch, 1);
|
||||
} else {
|
||||
usb_cli_printf("\r\nРядок занадто довгий\r\n");
|
||||
line_len = 0;
|
||||
@@ -386,3 +522,9 @@ esp_err_t usb_cdc_cli_init(void)
|
||||
ESP_LOGI(TAG, "USB CDC CLI ініціалізовано");
|
||||
return ESP_OK;
|
||||
}
|
||||
static void usb_cli_handle_reset(void)
|
||||
{
|
||||
usb_cli_printf("\r\nПерезавантаження пристрою...\r\n");
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "usb_cdc_log.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "esp_check.h"
|
||||
@@ -13,28 +12,6 @@ static const char *TAG = "usb_log";
|
||||
static bool s_initialized;
|
||||
static bool s_host_ready;
|
||||
|
||||
static int usb_cdc_vprintf(const char *fmt, va_list args)
|
||||
{
|
||||
if (s_host_ready) {
|
||||
char buffer[256];
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int len = vsnprintf(buffer, sizeof(buffer), fmt, args_copy);
|
||||
va_end(args_copy);
|
||||
if (len > 0) {
|
||||
if (len >= (int)sizeof(buffer)) {
|
||||
len = sizeof(buffer) - 1;
|
||||
}
|
||||
if (tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0, (const uint8_t *)buffer, len) == ESP_OK) {
|
||||
tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, 0);
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void usb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event)
|
||||
{
|
||||
(void)itf;
|
||||
@@ -83,8 +60,7 @@ esp_err_t usb_cdc_log_init(void)
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(tusb_cdc_acm_init(&acm_cfg), TAG, "CDC init failed");
|
||||
|
||||
esp_log_set_vprintf(usb_cdc_vprintf);
|
||||
s_initialized = true;
|
||||
ESP_LOGI(TAG, "ESP_LOG перенаправлено в USB CDC");
|
||||
ESP_LOGI(TAG, "USB CDC лог ініціалізовано (перенаправлення ESP_LOG вимкнено)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
141
main/watch_config.c
Normal file
141
main/watch_config.c
Normal file
@@ -0,0 +1,141 @@
|
||||
#include "watch_config.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#define WATCH_CONFIG_NAMESPACE "watchcfg"
|
||||
#define KEY_HB_PERIOD "hb_period"
|
||||
#define KEY_DCDC_OFF "dcdc_off"
|
||||
#define KEY_HB_START "hb_start"
|
||||
#define KEY_HB_MON "hb_mon"
|
||||
#define KEY_HB_MISS "hb_miss"
|
||||
|
||||
#define DEFAULT_HB_PERIOD_SEC 4U
|
||||
#define DEFAULT_DCDC_OFF_SEC 2U
|
||||
#define DEFAULT_HB_START_SEC 2U
|
||||
#define DEFAULT_HB_MISS_LIMIT 3U
|
||||
|
||||
static watch_config_t s_config = {
|
||||
.heartbeat_period_sec = DEFAULT_HB_PERIOD_SEC,
|
||||
.dcdc_restart_off_sec = DEFAULT_DCDC_OFF_SEC,
|
||||
.heartbeat_start_delay_sec = DEFAULT_HB_START_SEC,
|
||||
.heartbeat_monitor_enabled = true,
|
||||
.heartbeat_miss_limit = DEFAULT_HB_MISS_LIMIT,
|
||||
};
|
||||
static const char *TAG = "watch_cfg";
|
||||
static bool s_initialized;
|
||||
|
||||
static void watch_config_apply_defaults(void)
|
||||
{
|
||||
s_config.heartbeat_period_sec = DEFAULT_HB_PERIOD_SEC;
|
||||
s_config.dcdc_restart_off_sec = DEFAULT_DCDC_OFF_SEC;
|
||||
s_config.heartbeat_start_delay_sec = DEFAULT_HB_START_SEC;
|
||||
s_config.heartbeat_monitor_enabled = true;
|
||||
s_config.heartbeat_miss_limit = DEFAULT_HB_MISS_LIMIT;
|
||||
}
|
||||
|
||||
static void watch_config_clamp(watch_config_t *cfg)
|
||||
{
|
||||
if (!cfg->heartbeat_period_sec) {
|
||||
cfg->heartbeat_period_sec = DEFAULT_HB_PERIOD_SEC;
|
||||
}
|
||||
if (!cfg->dcdc_restart_off_sec) {
|
||||
cfg->dcdc_restart_off_sec = DEFAULT_DCDC_OFF_SEC;
|
||||
}
|
||||
if (!cfg->heartbeat_start_delay_sec) {
|
||||
cfg->heartbeat_start_delay_sec = DEFAULT_HB_START_SEC;
|
||||
}
|
||||
cfg->heartbeat_monitor_enabled = cfg->heartbeat_monitor_enabled ? true : false;
|
||||
if (!cfg->heartbeat_miss_limit) {
|
||||
cfg->heartbeat_miss_limit = DEFAULT_HB_MISS_LIMIT;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t watch_config_init(void)
|
||||
{
|
||||
if (s_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_RETURN_ON_ERROR(nvs_flash_erase(), TAG, "NVS erase failed");
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_RETURN_ON_ERROR(err, TAG, "NVS init failed");
|
||||
|
||||
watch_config_apply_defaults();
|
||||
|
||||
nvs_handle_t handle = 0;
|
||||
err = nvs_open(WATCH_CONFIG_NAMESPACE, NVS_READWRITE, &handle);
|
||||
ESP_RETURN_ON_ERROR(err, TAG, "NVS open failed");
|
||||
|
||||
uint32_t value = 0;
|
||||
if (nvs_get_u32(handle, KEY_HB_PERIOD, &value) == ESP_OK && value > 0) {
|
||||
s_config.heartbeat_period_sec = value;
|
||||
}
|
||||
if (nvs_get_u32(handle, KEY_DCDC_OFF, &value) == ESP_OK && value > 0) {
|
||||
s_config.dcdc_restart_off_sec = value;
|
||||
}
|
||||
if (nvs_get_u32(handle, KEY_HB_START, &value) == ESP_OK && value > 0) {
|
||||
s_config.heartbeat_start_delay_sec = value;
|
||||
}
|
||||
uint8_t hb_mon = 1;
|
||||
if (nvs_get_u8(handle, KEY_HB_MON, &hb_mon) == ESP_OK) {
|
||||
s_config.heartbeat_monitor_enabled = (hb_mon != 0);
|
||||
}
|
||||
if (nvs_get_u32(handle, KEY_HB_MISS, &value) == ESP_OK && value > 0) {
|
||||
s_config.heartbeat_miss_limit = value;
|
||||
}
|
||||
|
||||
nvs_close(handle);
|
||||
s_initialized = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
const watch_config_t *watch_config_get(void)
|
||||
{
|
||||
return &s_config;
|
||||
}
|
||||
|
||||
esp_err_t watch_config_save(const watch_config_t *cfg)
|
||||
{
|
||||
if (!cfg) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
watch_config_t tmp = *cfg;
|
||||
watch_config_clamp(&tmp);
|
||||
|
||||
nvs_handle_t handle = 0;
|
||||
ESP_RETURN_ON_ERROR(nvs_open(WATCH_CONFIG_NAMESPACE, NVS_READWRITE, &handle),
|
||||
TAG, "NVS open failed");
|
||||
|
||||
esp_err_t err = nvs_set_u32(handle, KEY_HB_PERIOD, tmp.heartbeat_period_sec);
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_set_u32(handle, KEY_DCDC_OFF, tmp.dcdc_restart_off_sec);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_set_u32(handle, KEY_HB_START, tmp.heartbeat_start_delay_sec);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_set_u8(handle, KEY_HB_MON, tmp.heartbeat_monitor_enabled ? 1 : 0);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_set_u32(handle, KEY_HB_MISS, tmp.heartbeat_miss_limit);
|
||||
}
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_commit(handle);
|
||||
}
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
s_config = tmp;
|
||||
return ESP_OK;
|
||||
}
|
||||
18
main/watch_config.h
Normal file
18
main/watch_config.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t heartbeat_period_sec;
|
||||
uint32_t dcdc_restart_off_sec;
|
||||
uint32_t heartbeat_start_delay_sec;
|
||||
bool heartbeat_monitor_enabled;
|
||||
uint32_t heartbeat_miss_limit;
|
||||
} watch_config_t;
|
||||
|
||||
esp_err_t watch_config_init(void);
|
||||
const watch_config_t *watch_config_get(void);
|
||||
esp_err_t watch_config_save(const watch_config_t *cfg);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
|
||||
esp_err_t ws2812_status_init(void);
|
||||
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_set_service_state(size_t channel, bool vpn_ok, bool app_ok);
|
||||
esp_err_t ws2812_status_set_startup_hold(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