#include "usb_cdc_cli.h" #include #include #include #include #include #include #include #include "dcdc_controller.h" #include "esp_check.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/task.h" #include "sdkconfig.h" #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 #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; uint8_t data[CONFIG_TINYUSB_CDC_RX_BUFSIZE]; } usb_cli_msg_t; static const char *TAG = "usb_cli"; static QueueHandle_t s_cli_queue; static bool s_cli_initialized; 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_write_raw(const char *data, size_t len) { if (!s_host_ready || len == 0) { return; } if (tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0, (uint8_t *)data, len) == ESP_OK) { esp_err_t err = tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, 0); if (err != ESP_OK) { ESP_LOGW(TAG, "CDC flush error: %s", esp_err_to_name(err)); } } } static void usb_cli_printf(const char *fmt, ...) { if (!s_host_ready) { return; } char buffer[192]; va_list args; va_start(args, fmt); int len = vsnprintf(buffer, sizeof(buffer), fmt, args); va_end(args); if (len > 0) { if (len >= (int)sizeof(buffer)) { len = (int)sizeof(buffer) - 1; } usb_cli_write_raw(buffer, len); } } static const char *PROMPT = "\r\nwatch> "; static char *usb_cli_trim(char *str) { while (*str && isspace((unsigned char)*str)) { str++; } char *end = str + strlen(str); while (end > str && isspace((unsigned char)*(end - 1))) { *(--end) = '\0'; } return str; } static bool usb_cli_parse_channel(const char *token, dcdc_channel_t *channel) { if (!token) { return false; } char *end = NULL; long val = strtol(token, &end, 10); if (end == token || val < 0 || val >= DCDC_CHANNEL_COUNT) { return false; } *channel = (dcdc_channel_t)val; return true; } 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", (unsigned)i, dcdc_get_state(ch) ? "ON" : "OFF", (int)dcdc_get_gpio(ch)); } } static void usb_cli_prompt(void) { usb_cli_write_raw(PROMPT, strlen(PROMPT)); } static void usb_cli_handle_switch(const char *cmd, char *args) { dcdc_channel_t channel; char *ctx = NULL; char *channel_str = strtok_r(args, " ", &ctx); if (!usb_cli_parse_channel(channel_str, &channel)) { usb_cli_printf("\r\nНевірний номер каналу\r\n"); return; } esp_err_t err = ESP_OK; if (strcasecmp(cmd, "enable") == 0) { err = dcdc_enable(channel); } else if (strcasecmp(cmd, "disable") == 0) { err = dcdc_disable(channel); } else { err = dcdc_toggle(channel); } if (err != ESP_OK) { usb_cli_printf("\r\nПомилка керування каналом: %s\r\n", esp_err_to_name(err)); } else { bool state = dcdc_get_state(channel); usb_cli_printf("\r\nКанал %d -> %s\r\n", channel, state ? "ON" : "OFF"); ws2812_status_set_channel_state(channel, state); } } 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); if (*trimmed == '\0') { return; } char *save_ptr = NULL; char *cmd = strtok_r(trimmed, " ", &save_ptr); if (!cmd) { return; } if (strcasecmp(cmd, "help") == 0) { usb_cli_printf( "\r\nДоступні команди:\r\n" " help - показати довідку\r\n" " status - стан всіх каналів\r\n" " enable - увімкнути канал n\r\n" " disable - вимкнути канал 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); } } static void usb_cli_task(void *arg) { usb_cli_msg_t msg; char line[CLI_LINE_MAX_LEN]; size_t line_len = 0; while (xQueueReceive(s_cli_queue, &msg, portMAX_DELAY)) { for (size_t i = 0; i < msg.length; ++i) { char ch = (char)msg.data[i]; if (ch == '\r' || ch == '\n') { if (line_len > 0) { line[line_len] = '\0'; usb_cli_process_line(line); line_len = 0; } usb_cli_prompt(); } else if (ch == 0x7F || ch == '\b') { if (line_len > 0) { line_len--; } } else if (isprint((unsigned char)ch)) { if (line_len < CLI_LINE_MAX_LEN - 1) { line[line_len++] = ch; } else { usb_cli_printf("\r\nРядок занадто довгий\r\n"); line_len = 0; usb_cli_prompt(); } } } } } static void usb_cdc_rx_callback(int itf, cdcacm_event_t *event) { if (!s_cli_queue) { return; } usb_cli_msg_t msg = { 0 }; size_t rx_size = 0; esp_err_t ret = tinyusb_cdcacm_read(itf, msg.data, sizeof(msg.data), &rx_size); if (ret == ESP_OK && rx_size > 0) { msg.length = rx_size; if (xQueueSend(s_cli_queue, &msg, 0) != pdTRUE) { ESP_LOGW(TAG, "CLI queue overflow, втрачено %u байт", (unsigned)rx_size); } } } static void usb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) { const bool dtr = event->line_state_changed_data.dtr; const bool rts = event->line_state_changed_data.rts; ESP_LOGI(TAG, "CDC лінія %d змінилась: DTR=%d RTS=%d", itf, dtr, rts); s_host_ready = dtr; if (s_host_ready) { usb_cli_printf("\r\nwatch-watch CLI готовий. Надрукуйте 'help'.\r\n"); usb_cli_prompt(); } } esp_err_t usb_cdc_cli_init(void) { if (s_cli_initialized) { return ESP_OK; } s_cli_queue = xQueueCreate(CLI_QUEUE_LEN, sizeof(usb_cli_msg_t)); if (!s_cli_queue) { return ESP_ERR_NO_MEM; } if (xTaskCreate(usb_cli_task, "usb_cli", 4096, NULL, 5, NULL) != pdPASS) { vQueueDelete(s_cli_queue); s_cli_queue = NULL; return ESP_ERR_NO_MEM; } const tinyusb_config_t tusb_cfg = { .device_descriptor = NULL, .string_descriptor = NULL, .external_phy = false, #if (TUD_OPT_HIGH_SPEED) .fs_configuration_descriptor = NULL, .hs_configuration_descriptor = NULL, .qualifier_descriptor = NULL, #else .configuration_descriptor = NULL, #endif }; ESP_RETURN_ON_ERROR(tinyusb_driver_install(&tusb_cfg), TAG, "TinyUSB init failed"); const tinyusb_config_cdcacm_t acm_cfg = { .usb_dev = TINYUSB_USBDEV_0, .cdc_port = TINYUSB_CDC_ACM_0, .rx_unread_buf_sz = CONFIG_TINYUSB_CDC_RX_BUFSIZE, .callback_rx = usb_cdc_rx_callback, .callback_rx_wanted_char = NULL, .callback_line_state_changed = NULL, .callback_line_coding_changed = NULL, }; ESP_RETURN_ON_ERROR(tusb_cdc_acm_init(&acm_cfg), TAG, "CDC init failed"); ESP_RETURN_ON_ERROR( tinyusb_cdcacm_register_callback(TINYUSB_CDC_ACM_0, CDC_EVENT_LINE_STATE_CHANGED, usb_cdc_line_state_changed_callback), TAG, "Line state callback error"); s_cli_initialized = true; ESP_LOGI(TAG, "USB CDC CLI ініціалізовано"); return ESP_OK; }