Files
watch-watch/main/usb_cdc_cli.c

531 lines
18 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

#include "usb_cdc_cli.h"
#include <ctype.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#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"
#include "sdkconfig.h"
#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
#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_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)
{
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;
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),
(unsigned)stats.missed_heartbeats,
(unsigned)stats.restart_count);
}
}
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;
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 В, %.3f А, %.3f Вт",
(unsigned)channel, reading->voltage_v, reading->current_a, reading->power_w);
}
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 <channel> <text> | uart read <channel> [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 - показати стан усіх каналів 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 ||
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 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;
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_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;
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;
}
static void usb_cli_handle_reset(void)
{
usb_cli_printf("\r\nПерезавантаження пристрою...\r\n");
vTaskDelay(pdMS_TO_TICKS(100));
esp_restart();
}