diff --git a/Kconfig.projbuild b/Kconfig.projbuild index de37e56..6a3415b 100644 --- a/Kconfig.projbuild +++ b/Kconfig.projbuild @@ -24,3 +24,104 @@ config WATCH_WS2812_RMT_RESOLUTION 10 МГц = 0.1 мкс на імпульс. endmenu + +menu "INA226 моніторинг" + +config WATCH_INA226_ENABLED + bool "Увімкнути моніторинг INA226" + default y + help + Якщо увімкнено, ESP32-S3 опитує датчики INA226 для вимірювання + напруги, струму та потужності кожного каналу. + +config WATCH_INA226_I2C_PORT + int "I2C порт" + range 0 1 + default 0 + +config WATCH_INA226_I2C_SDA + int "GPIO SDA" + range 0 48 + default 6 + +config WATCH_INA226_I2C_SCL + int "GPIO SCL" + range 0 48 + default 7 + +config WATCH_INA226_I2C_FREQ_HZ + int "Швидкість I2C (Гц)" + range 10000 1000000 + default 400000 + +config WATCH_INA226_SHUNT_MILLIOHM + int "Опір шунта (мОм)" + range 1 500 + default 10 + +config WATCH_INA226_CURRENT_LSB_uA + int "Крок струму (мкА/LSB)" + range 10 10000 + default 100 + +config WATCH_INA226_SAMPLE_INTERVAL_MS + int "Інтервал опитування (мс)" + range 50 5000 + default 500 + +config WATCH_INA226_ADDR + hex "Адреса INA226 (загальна шина)" + default 0x40 + +endmenu + +menu "UART мультиплексор" + +config WATCH_UART_MUX_ENABLED + bool "Увімкнути взаємодію з Raspberry Pi через UART" + default y + +config WATCH_UART_MUX_CHANNELS + int "Кількість каналів (Raspberry Pi)" + range 1 8 + default 5 + +config WATCH_UART_PORT + int "Номер UART" + range 0 2 + default 1 + +config WATCH_UART_BAUD + int "Швидкість UART, біт/с" + default 115200 + +config WATCH_UART_TX_GPIO + int "GPIO TX" + default 17 + +config WATCH_UART_RX_GPIO + int "GPIO RX" + default 16 + +config WATCH_UART_MUX_SEL_A0 + int "GPIO A0" + default 9 + +config WATCH_UART_MUX_SEL_A1 + int "GPIO A1" + default 10 + +config WATCH_UART_MUX_SEL_A2 + int "GPIO A2" + default 11 + +config WATCH_UART_MUX_DEFAULT_READ_LEN + int "Типова довжина читання (байт)" + default 128 + +config WATCH_UART_HEARTBEAT_TIMEOUT_SEC + int "Тайм-аут heartbeat (сек.)" + range 5 600 + default 60 + +endmenu diff --git a/README.md b/README.md index e137a7e..72a9da0 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,31 @@ watch-watch — вбудована система на ESP32-S3 для нагл - **Керування каналами живлення**: 5 незалежних ліній `EN` (GPIO 2, 4, 5, 18, 19), які можна увімкнути, вимкнути або перемкнути з коду чи CLI. - **Послідовний автотест**: у `app_main` реалізовано базову логіку — канали вмикаються по черзі з інтервалом 4 с, що дозволяє перевірити всі DC/DC. - **Світлодіодний індикатор стану**: п’ять WS2812 (GPIO 8) показують роботу каналів — активний канал підсвічується яскраво-зеленим, увімкнені/вимкнені відображаються зеленим/синім, помилки — червоним. +- **Моніторинг навантаження**: датчики INA226 вимірюють напругу, струм та потужність кожного каналу, інформація потрапляє в лог і CLI. +- **UART взаємодія з Raspberry Pi**: один UART через мультиплексор (A0/A1/A2) ділиться між п’ятьма Pi, дозволяючи надсилати службові повідомлення або обмінюватися даними. - **Нативний USB-CLI**: ESP32-S3 підключається до Raspberry Pi 5 по USB і стає CDC ACM пристроєм; командний інтерфейс дозволяє керувати каналами та дивитись стан у реальному часі. - **Модульна архітектура**: окремі компоненти `dcdc_controller` і `usb_cdc_cli` спрощують розширення (телеметрія, автоматизація, протоколи зв’язку). ## Структура проєкту ``` +├── CMakeLists.txt +├── Kconfig.projbuild # меню і параметри WATCH_WS2812_* +├── README.md +├── sdkconfig # збережені налаштування menuconfig +├── dependencies.lock ├── main -│ ├── main.c // базова логіка, ініціалізація модулів -│ ├── dcdc_controller.c/.h // API керування DC/DC каналами -│ ├── usb_cdc_cli.c/.h // TinyUSB CLI для Raspberry Pi 5 -│ └── idf_component.yml // залежність на espressif/esp_tinyusb -├── sdkconfig // конфігурація (опції TinyUSB увімкнені) -└── README.md // цей файл +│ ├── CMakeLists.txt +│ ├── idf_component.yml # залежності: esp_tinyusb, led_strip +│ ├── main.c # головний цикл, тестова логіка DC/DC +│ ├── dcdc_controller.c/.h # керування GPIO EN +│ ├── usb_cdc_cli.c/.h # CLI по USB CDC +│ ├── ws2812_status.c/.h # індикація стану на WS2812 +│ ├── ina226_monitor.c/.h # вимірювання напруги/струму/потужності +│ └── uart_mux.c/.h # UART взаємодія з 5 Raspberry Pi +├── managed_components +│ ├── espressif__esp_tinyusb # бібліотека TinyUSB від Espressif +│ └── espressif__led_strip # драйвер керування WS2812 (RMT/SPI) +└── .vscode / .devcontainer / .clangd # допоміжні файли середовища розробки ``` ## GPIO-призначення каналів @@ -39,6 +52,25 @@ watch-watch — вбудована система на ESP32-S3 для нагл - Колірні алгоритми можна кастомізувати у `main/ws2812_status.c`. - GPIO, кількість діодів та тактову частоту RMT можна змінити через `idf.py menuconfig` (розділ *Налаштування watch-watch*). +## Моніторинг живлення (INA226) +- Один INA226 підключений до загальної шини живлення (I2C порт 0, GPIO 6/7; адреса налаштовується параметром `WATCH_INA226_ADDR`). +- Модуль `ina226_monitor` вимірює сумарну напругу, струм і розраховує потужність — ці значення фіксуються в логах і використовуються для телеметрії. +- Команда CLI `sense` показує поточні показники для всієї системи (канал не вказується, бо датчик один). +- Конфігурація I2C, адреси, шунта та кроку струму знаходиться в `menuconfig → INA226 моніторинг`. + +## UART взаємодія та heartbeat +- Загальний UART (типово UART1, GPIO17/16) підключений до аналогового мультиплексора, лінії адреси `A0/A1/A2` (GPIO 9/10/11) вибирають одну з Raspberry Pi. +- Модуль `uart_mux` серіалізує доступ до UART, надає API для `uart_mux_write/read` і періодично опитує UART на наявність heartbeat. +- Після кожного вимірювання INA226 ESP32-S3 відправляє поточну телеметрію (`PWR `) до активної Raspberry Pi. +- Якщо heartbeat від Pi не надходить протягом `CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC` (за замовчуванням 60 с), відповідний канал живлення вимикається й знову вмикається для примусового перезапуску. +- Команди CLI `uart send` / `uart read` дозволяють вручну надсилати/читати повідомлення, а в `app_main` можна реалізувати власні протоколи синхронізації. + +## UART взаємодія з Raspberry Pi +- Шина UART (типово UART1, TX=GPIO17, RX=GPIO16) підключена до аналогового мультиплексора з адресними лініями A0/A1/A2 (GPIO 9/10/11), що дозволяє вибирати одну з 5 Raspberry Pi. +- Модуль `uart_mux` гарантує серійний доступ: перед операцією він виставляє двійковий код каналу на A0-A2 та блокує UART м’ютексом. +- У `app_main` після вимірювань кожному Pi відправляється телеметрія (`CHx V mA`), а через CLI можна виконати `uart send ` або `uart read [len]`. +- Усі параметри (порт, швидкість, GPIO) доступні в `menuconfig → UART мультиплексор`. + ## USB CDC CLI Після підключення ESP32-S3 до Raspberry Pi 5 з’являється USB-пристрій (CDC ACM). У CLI доступні команди: @@ -49,6 +81,11 @@ watch-watch — вбудована система на ESP32-S3 для нагл | `enable ` | увімкнути канал `n` (0..4) | | `disable ` | вимкнути канал `n` | | `toggle ` | перемкнути канал `n` | +| `sense` | виміряти загальну напругу/струм/потужність | +| `uart send ` | надіслати повідомлення у Raspberry Pi `n` | +| `uart read [len]` | прочитати відповідь від Raspberry Pi `n` | +| `uart send ` | відправити текст у Raspberry Pi №n | +| `uart read [len]` | прочитати дані з цільового Pi | CLI з’являється після того, як Raspberry Pi встановить DTR (наприклад, через `screen`, `picocom` або власний скрипт). diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 7f62ef2..3e3bb21 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -2,4 +2,6 @@ idf_component_register(SRCS "main.c" "dcdc_controller.c" "usb_cdc_cli.c" "ws2812_status.c" + "ina226_monitor.c" + "uart_mux.c" INCLUDE_DIRS ".") diff --git a/main/ina226_monitor.c b/main/ina226_monitor.c new file mode 100644 index 0000000..8960632 --- /dev/null +++ b/main/ina226_monitor.c @@ -0,0 +1,172 @@ +#include "ina226_monitor.h" + +#include + +#include "driver/i2c.h" +#include "esp_check.h" +#include "esp_log.h" +#include "sdkconfig.h" + +#ifndef CONFIG_WATCH_INA226_ENABLED +#define CONFIG_WATCH_INA226_ENABLED 0 +#endif +#ifndef CONFIG_WATCH_INA226_I2C_PORT +#define CONFIG_WATCH_INA226_I2C_PORT 0 +#endif +#ifndef CONFIG_WATCH_INA226_I2C_SDA +#define CONFIG_WATCH_INA226_I2C_SDA 6 +#endif +#ifndef CONFIG_WATCH_INA226_I2C_SCL +#define CONFIG_WATCH_INA226_I2C_SCL 7 +#endif +#ifndef CONFIG_WATCH_INA226_I2C_FREQ_HZ +#define CONFIG_WATCH_INA226_I2C_FREQ_HZ 400000 +#endif +#ifndef CONFIG_WATCH_INA226_CURRENT_LSB_uA +#define CONFIG_WATCH_INA226_CURRENT_LSB_uA 100 +#endif +#ifndef CONFIG_WATCH_INA226_SHUNT_MILLIOHM +#define CONFIG_WATCH_INA226_SHUNT_MILLIOHM 10 +#endif +#ifndef CONFIG_WATCH_INA226_ADDR +#define CONFIG_WATCH_INA226_ADDR 0x40 +#endif + +#define INA226_REG_CONFIG 0x00 +#define INA226_REG_BUS 0x02 +#define INA226_REG_CURRENT 0x04 +#define INA226_REG_CALIBRATION 0x05 + +#define INA226_CONFIG_AVG_16 (0x04 << 9) +#define INA226_CONFIG_VBUS_1100US (0x04 << 6) +#define INA226_CONFIG_VSH_1100US (0x04 << 3) +#define INA226_CONFIG_MODE_SHUNT_BUS_CONT 0x07 + +#if CONFIG_WATCH_INA226_ENABLED +static const char *TAG = "ina226"; +static bool s_initialized; +static float s_current_lsb_ma; +static uint8_t s_address = CONFIG_WATCH_INA226_ADDR; +static ina226_reading_t s_last_reading; +#endif + +esp_err_t ina226_monitor_init(void) +{ +#if !CONFIG_WATCH_INA226_ENABLED + return ESP_ERR_NOT_SUPPORTED; +#else + if (s_initialized) { + return ESP_OK; + } + + i2c_config_t config = { + .mode = I2C_MODE_MASTER, + .sda_io_num = CONFIG_WATCH_INA226_I2C_SDA, + .scl_io_num = CONFIG_WATCH_INA226_I2C_SCL, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .master.clk_speed = CONFIG_WATCH_INA226_I2C_FREQ_HZ, + }; + ESP_RETURN_ON_ERROR(i2c_param_config(CONFIG_WATCH_INA226_I2C_PORT, &config), TAG, "i2c config failed"); + ESP_RETURN_ON_ERROR(i2c_driver_install(CONFIG_WATCH_INA226_I2C_PORT, config.mode, 0, 0, 0), + TAG, "i2c driver install failed"); + + double current_lsb_a = ((double)CONFIG_WATCH_INA226_CURRENT_LSB_uA) / 1000000.0; + double shunt_ohms = ((double)CONFIG_WATCH_INA226_SHUNT_MILLIOHM) / 1000.0; + double calibration = 0.00512 / (current_lsb_a * shunt_ohms); + if (calibration > 0xFFFF) calibration = 0xFFFF; + uint16_t calibration_value = (uint16_t)calibration; + s_current_lsb_ma = (float)current_lsb_a * 1000.0f; + + const uint16_t config_value = INA226_CONFIG_AVG_16 | + INA226_CONFIG_VBUS_1100US | + INA226_CONFIG_VSH_1100US | + INA226_CONFIG_MODE_SHUNT_BUS_CONT; + + uint8_t payload_cfg[3] = { INA226_REG_CONFIG, (uint8_t)(config_value >> 8), (uint8_t)(config_value & 0xFF) }; + ESP_RETURN_ON_ERROR(i2c_master_write_to_device(CONFIG_WATCH_INA226_I2C_PORT, + s_address, payload_cfg, sizeof(payload_cfg), + pdMS_TO_TICKS(100)), + TAG, "config write failed"); + + uint8_t payload_cal[3] = { INA226_REG_CALIBRATION, (uint8_t)(calibration_value >> 8), + (uint8_t)(calibration_value & 0xFF) }; + ESP_RETURN_ON_ERROR(i2c_master_write_to_device(CONFIG_WATCH_INA226_I2C_PORT, + s_address, payload_cal, sizeof(payload_cal), + pdMS_TO_TICKS(100)), + TAG, "calibration write failed"); + + s_last_reading = (ina226_reading_t){0}; + s_initialized = true; + ESP_LOGI(TAG, "INA226 ініціалізовано (адреса 0x%02X)", s_address); + return ESP_OK; +#endif +} + +bool ina226_monitor_ready(void) +{ +#if CONFIG_WATCH_INA226_ENABLED + return s_initialized; +#else + return false; +#endif +} + +size_t ina226_monitor_channel_count(void) +{ + return 1; +} + +#if CONFIG_WATCH_INA226_ENABLED +static esp_err_t ina226_read_register(uint8_t reg, uint16_t *out_value) +{ + uint8_t value[2]; + esp_err_t err = i2c_master_write_read_device(CONFIG_WATCH_INA226_I2C_PORT, + s_address, ®, 1, value, sizeof(value), + pdMS_TO_TICKS(100)); + if (err != ESP_OK) { + return err; + } + *out_value = ((uint16_t)value[0] << 8) | value[1]; + return ESP_OK; +} +#endif + +esp_err_t ina226_monitor_sample(ina226_reading_t *out_reading) +{ +#if !CONFIG_WATCH_INA226_ENABLED + return ESP_ERR_NOT_SUPPORTED; +#else + if (!s_initialized) { + return ESP_ERR_INVALID_STATE; + } + uint16_t bus_raw = 0; + uint16_t current_raw = 0; + + ESP_RETURN_ON_ERROR(ina226_read_register(INA226_REG_BUS, &bus_raw), TAG, "bus read failed"); + ESP_RETURN_ON_ERROR(ina226_read_register(INA226_REG_CURRENT, ¤t_raw), TAG, "current read failed"); + + float voltage_v = (float)bus_raw * 1.25f / 1000.0f; + float current_ma = (int16_t)current_raw * s_current_lsb_ma; + float power_mw = voltage_v * current_ma; + + s_last_reading = (ina226_reading_t){ + .voltage_v = voltage_v, + .current_ma = current_ma, + .power_mw = power_mw, + }; + if (out_reading) { + *out_reading = s_last_reading; + } + return ESP_OK; +#endif +} + +const ina226_reading_t *ina226_monitor_get_last(void) +{ +#if CONFIG_WATCH_INA226_ENABLED + return &s_last_reading; +#else + return NULL; +#endif +} diff --git a/main/ina226_monitor.h b/main/ina226_monitor.h new file mode 100644 index 0000000..a7957ba --- /dev/null +++ b/main/ina226_monitor.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include "esp_err.h" + +typedef struct { + float voltage_v; + float current_ma; + float power_mw; +} ina226_reading_t; + +esp_err_t ina226_monitor_init(void); +bool ina226_monitor_ready(void); +size_t ina226_monitor_channel_count(void); +esp_err_t ina226_monitor_sample(ina226_reading_t *out_reading); +const ina226_reading_t *ina226_monitor_get_last(void); diff --git a/main/main.c b/main/main.c index cc91976..975e89b 100644 --- a/main/main.c +++ b/main/main.c @@ -5,6 +5,8 @@ #include "esp_log.h" #include "dcdc_controller.h" +#include "ina226_monitor.h" +#include "uart_mux.h" #include "usb_cdc_cli.h" #include "ws2812_status.h" @@ -25,6 +27,19 @@ void app_main(void) ESP_LOGW(TAG, "WS2812 статусний індикатор недоступний"); } + if (ina226_monitor_init() == ESP_OK) { + ESP_LOGI(TAG, "INA226 моніторинг активовано"); + ina226_monitor_sample(NULL); + } else { + ESP_LOGW(TAG, "Моніторинг навантаження недоступний"); + } + + if (uart_mux_init() == ESP_OK) { + ESP_LOGI(TAG, "UART мультиплексор активовано"); + } else { + ESP_LOGW(TAG, "UART мультиплексор недоступний"); + } + if (usb_cdc_cli_init() != ESP_OK) { ESP_LOGW(TAG, "USB CDC CLI недоступний"); } else { @@ -48,6 +63,19 @@ void app_main(void) ws2812_status_mark_active(ch); vTaskDelay(on_time); + ina226_reading_t reading = {0}; + if (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()) { + char msg[64]; + int len = snprintf(msg, sizeof(msg), "PWR %.2fV %.0fmA\r\n", + reading.voltage_v, reading.current_ma); + uart_mux_write(ch, (const uint8_t *)msg, len, pdMS_TO_TICKS(100)); + } + prev_channel = ch; } } diff --git a/main/uart_mux.c b/main/uart_mux.c new file mode 100644 index 0000000..2d5cd8a --- /dev/null +++ b/main/uart_mux.c @@ -0,0 +1,233 @@ +#include "uart_mux.h" + +#include + +#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" + +#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 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; +} + +static void uart_mux_watchdog_task(void *arg) +{ + const TickType_t poll_interval = pdMS_TO_TICKS(1000); + const TickType_t read_timeout = pdMS_TO_TICKS(10); + const int64_t timeout_us = (int64_t)CONFIG_WATCH_UART_HEARTBEAT_TIMEOUT_SEC * 1000000LL; + uint8_t buffer[CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN]; + + while (true) { + vTaskDelay(poll_interval); + int64_t now = esp_timer_get_time(); + for (size_t ch = 0; ch < UART_MUX_MAX_CHANNELS; ++ch) { + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(20)) == pdTRUE) { + 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; + } + } + xSemaphoreGive(s_mutex); + } + + 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); + dcdc_disable(ch); + vTaskDelay(pdMS_TO_TICKS(2000)); + dcdc_enable(ch); + s_last_heartbeat_us[ch] = esp_timer_get_time(); + } + } + } +} + +#endif // CONFIG_WATCH_UART_MUX_ENABLED + +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; + } + + 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 +} + +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; +} + +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; + } + } + xSemaphoreGive(s_mutex); + return err; +#endif +} + +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 +} diff --git a/main/uart_mux.h b/main/uart_mux.h new file mode 100644 index 0000000..79bfd0d --- /dev/null +++ b/main/uart_mux.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "freertos/FreeRTOS.h" +#include "esp_err.h" + +esp_err_t uart_mux_init(void); +bool uart_mux_ready(void); +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); +esp_err_t uart_mux_read(size_t channel, uint8_t *buffer, size_t buffer_size, size_t *out_length, TickType_t timeout); diff --git a/main/usb_cdc_cli.c b/main/usb_cdc_cli.c index 82cff95..9d68e84 100644 --- a/main/usb_cdc_cli.c +++ b/main/usb_cdc_cli.c @@ -18,6 +18,8 @@ #include "tinyusb.h" #include "tusb_cdc_acm.h" #include "ws2812_status.h" +#include "ina226_monitor.h" +#include "uart_mux.h" #define CLI_LINE_MAX_LEN 128 #define CLI_QUEUE_LEN 8 @@ -25,6 +27,9 @@ #ifndef CONFIG_TINYUSB_CDC_RX_BUFSIZE #define CONFIG_TINYUSB_CDC_RX_BUFSIZE 64 #endif +#ifndef CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN +#define CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN 128 +#endif typedef struct { size_t length; @@ -147,6 +152,84 @@ static void usb_cli_handle_switch(const char *cmd, char *args) } } +static void usb_cli_print_measurement(size_t channel, const ina226_reading_t *reading) +{ + usb_cli_printf("\r\nCH%u: %.2f В, %.1f мА, %.1f мВт", + (unsigned)channel, reading->voltage_v, reading->current_ma, reading->power_mw); +} + +static void usb_cli_handle_sense(char *args) +{ + if (!ina226_monitor_ready()) { + usb_cli_printf("\r\nМоніторинг INA226 недоступний\r\n"); + return; + } + + ina226_reading_t reading = {0}; + if (ina226_monitor_sample(&reading) == ESP_OK) { + usb_cli_print_measurement(0, &reading); + } else { + usb_cli_printf("\r\nНе вдалося зчитати дані з INA226\r\n"); + } +} + +static void usb_cli_handle_uart(char *args) +{ + if (!uart_mux_ready()) { + usb_cli_printf("\r\nUART мультиплексор недоступний\r\n"); + return; + } + + char *ctx = NULL; + char *action = strtok_r(args, " ", &ctx); + if (!action) { + usb_cli_printf("\r\nUART usage: uart send | uart read [len]\r\n"); + return; + } + + char *channel_str = strtok_r(NULL, " ", &ctx); + dcdc_channel_t channel; + if (!usb_cli_parse_channel(channel_str, &channel)) { + usb_cli_printf("\r\nНевірний номер каналу\r\n"); + return; + } + + if (strcasecmp(action, "send") == 0) { + char *payload = ctx; + if (!payload || *payload == '\0') { + usb_cli_printf("\r\nНемає даних для відправлення\r\n"); + return; + } + size_t len = strlen(payload); + if (uart_mux_write(channel, (const uint8_t *)payload, len, pdMS_TO_TICKS(200)) == ESP_OK) { + usb_cli_printf("\r\nВідправлено %u байт\r\n", (unsigned)len); + } else { + usb_cli_printf("\r\nПомилка відправлення UART\r\n"); + } + } else if (strcasecmp(action, "read") == 0) { + char *len_str = ctx ? strtok_r(NULL, " ", &ctx) : NULL; + size_t req_len = len_str ? strtoul(len_str, NULL, 10) : CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN; + if (req_len == 0) { + req_len = CONFIG_WATCH_UART_MUX_DEFAULT_READ_LEN; + } + if (req_len > 256) { + req_len = 256; + } + uint8_t buffer[256]; + size_t received = 0; + if (uart_mux_read(channel, buffer, req_len, &received, pdMS_TO_TICKS(200)) == ESP_OK && received > 0) { + usb_cli_printf("\r\nCH%u UART (%u байт): ", (unsigned)channel, (unsigned)received); + for (size_t i = 0; i < received; ++i) { + usb_cli_write_raw((char *)&buffer[i], 1); + } + } else { + usb_cli_printf("\r\nДані відсутні\r\n"); + } + } else { + usb_cli_printf("\r\nНевірна дія '%s'\r\n", action); + } +} + static void usb_cli_process_line(char *line) { char *trimmed = usb_cli_trim(line); @@ -167,13 +250,20 @@ static void usb_cli_process_line(char *line) " status - стан всіх каналів\r\n" " enable - увімкнути канал n\r\n" " disable - вимкнути канал n\r\n" - " toggle - перемкнути канал n\r\n"); + " toggle - перемкнути канал n\r\n" + " sense [n] - показати напругу/струм/потужність (опц. канал)\r\n" + " uart send - відправити повідомлення Pi n\r\n" + " uart read [len] - прочитати дані з Pi n\r\n"); } else if (strcasecmp(cmd, "status") == 0) { usb_cli_print_status(); } else if (strcasecmp(cmd, "enable") == 0 || strcasecmp(cmd, "disable") == 0 || strcasecmp(cmd, "toggle") == 0) { usb_cli_handle_switch(cmd, save_ptr); + } else if (strcasecmp(cmd, "sense") == 0) { + usb_cli_handle_sense(save_ptr); + } else if (strcasecmp(cmd, "uart") == 0) { + usb_cli_handle_uart(save_ptr); } else { usb_cli_printf("\r\nНевідома команда '%s'\r\n", cmd); }