586 lines
15 KiB
C

/**
* @file app_cli.c
* @brief CLI framework: an independent task receives command lines from the
* USART1 RX interrupt queue, parses and executes them, and answers
* through USART1 DMA TX.
*
* Line terminators: "\r\n", "\n\r", "\r" and "\n" are all accepted.
* Editing: backspace (0x08 / 0x7F) erases the last character.
*/
#include "app_cli.h"
#include "app_version.h"
#include "bsp_usart.h"
#include "bsp_led.h"
#include "app_tasks.h"
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include "FreeRTOS.h"
#include "task.h"
#define CLI_TASK_STACK 1024 /* in words (printf needs more stack) */
#define CLI_TASK_PRIO 3
#define CLI_LINE_BUF_SIZE 64
#define CLI_TX_BUF_SIZE 96
#define CLI_MAX_ARGS 8
#define CLI_HISTORY_SIZE 8 /* number of remembered command lines */
#define CLI_PROMPT "caiic->"
/* Key codes */
#define CLI_KEY_TAB 0x09
#define CLI_KEY_ESC 0x1B
#define CLI_KEY_CTRL_Z 0x1A
#define CLI_KEY_BACKSPACE 0x08
#define CLI_KEY_DEL 0x7F
typedef void (*CliCmdHandler_t)(int argc, char* argv[]);
typedef struct
{
const char* name;
const char* help;
CliCmdHandler_t handler;
} CliCmd_t;
static char s_txBuf[CLI_TX_BUF_SIZE];
/**
* @brief Send a string through USART1 DMA TX.
* @note Strings must contain explicit "\r\n" (no auto CR like fputc).
*/
static void cli_write(const char* str)
{
bsp_usart_write_dma((const uint8_t*)str, (uint16_t)strlen(str));
}
/**
* @brief Formatted CLI output through USART1 DMA TX.
*/
static void cli_printf(const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
vsnprintf(s_txBuf, CLI_TX_BUF_SIZE, fmt, args);
va_end(args);
s_txBuf[CLI_TX_BUF_SIZE - 1] = '\0';
cli_write(s_txBuf);
}
/* ------------------------------------------------------------------ */
/* Built-in commands */
/* ------------------------------------------------------------------ */
static void CmdHelp(int argc, char* argv[]);
/**
* @brief "version": show firmware and clock info.
*/
static void CmdVersion(int argc, char* argv[])
{
(void)argc;
(void)argv;
cli_printf("firmware version: %s\r\n", APP_FW_VERSION);
cli_write("N32WB031 FreeRTOS + UART(DMA) + LED + CLI demo\r\n");
cli_printf("SystemCoreClock = %lu Hz\r\n", (unsigned long)SystemCoreClock);
}
/**
* @brief "sysinfo": show FreeRTOS runtime info (uptime, tasks, heap,
* per-task state/priority/stack high water mark).
*/
static void CmdSysInfo(int argc, char* argv[])
{
static char s_taskList[384]; /* vTaskList output buffer */
uint32_t ticks = (uint32_t)xTaskGetTickCount();
(void)argc;
(void)argv;
cli_printf("uptime: %lu ticks (%lu s)\r\n",
(unsigned long)ticks, (unsigned long)(ticks / configTICK_RATE_HZ));
cli_printf("tasks: %u\r\n", (unsigned int)uxTaskGetNumberOfTasks());
cli_printf("heap: total %u B, free %u B, min ever free %u B\r\n",
(unsigned int)configTOTAL_HEAP_SIZE,
(unsigned int)xPortGetFreeHeapSize(),
(unsigned int)xPortGetMinimumEverFreeHeapSize());
/* vTaskList lines already end with "\r\n"; stack column is in words */
cli_write("name state prio stack(w) num\r\n");
s_taskList[0] = '\0';
vTaskList(s_taskList);
cli_write(s_taskList);
}
/**
* @brief "log [on|off]": show or set the active log printing switch.
*/
static void CmdLog(int argc, char* argv[])
{
if (argc < 2)
{
cli_printf("log is %s\r\n", AppTasks_GetLogEnabled() ? "on" : "off");
return;
}
if (strcmp(argv[1], "on") == 0)
{
AppTasks_SetLogEnabled(1);
}
else if (strcmp(argv[1], "off") == 0)
{
AppTasks_SetLogEnabled(0);
}
else
{
cli_write("usage: log [on|off]\r\n");
return;
}
cli_printf("log set to %s\r\n", AppTasks_GetLogEnabled() ? "on" : "off");
}
/**
* @brief "led <1|2> <on|off|toggle>": control a board LED.
*/
static void CmdLed(int argc, char* argv[])
{
GPIO_Module* port;
uint16_t pin;
if (argc < 3)
{
cli_write("usage: led <1|2> <on|off|toggle>\r\n");
return;
}
if (argv[1][0] == '1' && argv[1][1] == '\0')
{
port = LED1_PORT;
pin = LED1_PIN;
}
else if (argv[1][0] == '2' && argv[1][1] == '\0')
{
port = LED2_PORT;
pin = LED2_PIN;
}
else
{
cli_write("usage: led <1|2> <on|off|toggle>\r\n");
return;
}
if (strcmp(argv[2], "on") == 0)
{
bsp_led_on(port, pin);
}
else if (strcmp(argv[2], "off") == 0)
{
bsp_led_off(port, pin);
}
else if (strcmp(argv[2], "toggle") == 0)
{
bsp_led_toggle(port, pin);
}
else
{
cli_write("usage: led <1|2> <on|off|toggle>\r\n");
return;
}
cli_printf("led %s %s done\r\n", argv[1], argv[2]);
}
/* Command table; add new commands here */
static const CliCmd_t s_cmds[] = {
{"help", "list all commands", CmdHelp},
{"version", "show firmware version", CmdVersion},
{"sysinfo", "show FreeRTOS runtime info (uptime/tasks/heap/task list)", CmdSysInfo},
{"log", "log [on|off]: active printing switch (default off)", CmdLog},
{"led", "led <1|2> <on|off|toggle>: control board LED", CmdLed},
};
#define CLI_CMD_COUNT (sizeof(s_cmds) / sizeof(s_cmds[0]))
/**
* @brief "help": list all registered commands.
*/
static void CmdHelp(int argc, char* argv[])
{
uint32_t i;
(void)argc;
(void)argv;
cli_write("commands:\r\n");
for (i = 0; i < CLI_CMD_COUNT; i++)
{
cli_printf(" %-8s %s\r\n", s_cmds[i].name, s_cmds[i].help);
}
cli_write("keys: TAB = complete, UP/DOWN = history, Ctrl+Z = log off\r\n");
}
/* ------------------------------------------------------------------ */
/* Line parsing / dispatch */
/* ------------------------------------------------------------------ */
/**
* @brief Tokenize a received line in place and run the matching command.
*/
static void cli_dispatch(char* line)
{
char* argv[CLI_MAX_ARGS];
int argc = 0;
char* p = line;
uint32_t i;
/* Split on spaces/tabs */
while (*p != '\0' && argc < CLI_MAX_ARGS)
{
while (*p == ' ' || *p == '\t')
{
p++;
}
if (*p == '\0')
{
break;
}
argv[argc++] = p;
while (*p != '\0' && *p != ' ' && *p != '\t')
{
p++;
}
if (*p != '\0')
{
*p++ = '\0';
}
}
if (argc == 0)
{
return;
}
for (i = 0; i < CLI_CMD_COUNT; i++)
{
if (strcmp(argv[0], s_cmds[i].name) == 0)
{
s_cmds[i].handler(argc, argv);
return;
}
}
cli_printf("unknown command: %s (try 'help')\r\n", argv[0]);
}
/* ------------------------------------------------------------------ */
/* Command history (ring buffer) */
/* ------------------------------------------------------------------ */
static char s_history[CLI_HISTORY_SIZE][CLI_LINE_BUF_SIZE];
static uint8_t s_histCount = 0; /* number of valid entries */
static uint8_t s_histHead = 0; /* next write position */
/**
* @brief Store a command line in the history ring (skips empty lines and
* repeats of the most recent entry).
*/
static void cli_history_add(const char* line)
{
if (line[0] == '\0')
{
return;
}
if (s_histCount > 0)
{
uint8_t last = (s_histHead + CLI_HISTORY_SIZE - 1) % CLI_HISTORY_SIZE;
if (strcmp(s_history[last], line) == 0)
{
return;
}
}
strcpy(s_history[s_histHead], line);
s_histHead = (s_histHead + 1) % CLI_HISTORY_SIZE;
if (s_histCount < CLI_HISTORY_SIZE)
{
s_histCount++;
}
}
/**
* @brief Get a history entry; nav = 0 is the newest entry.
*/
static const char* cli_history_get(int nav)
{
int idx = ((int)s_histHead - 1 - nav + CLI_HISTORY_SIZE) % CLI_HISTORY_SIZE;
return s_history[idx];
}
/* ------------------------------------------------------------------ */
/* Line redraw / tab completion */
/* ------------------------------------------------------------------ */
/**
* @brief Redraw the input line: erase `shownLen` chars, print prompt + line.
* @return new shown length
*/
static uint16_t cli_redraw_line(const char* line, uint16_t shownLen)
{
uint16_t i;
uint16_t len = (uint16_t)strlen(line);
cli_write("\r" CLI_PROMPT);
for (i = 0; i < shownLen; i++)
{
cli_write(" ");
}
cli_write("\r" CLI_PROMPT);
if (len > 0)
{
bsp_usart_write_dma((const uint8_t*)line, len);
}
return len;
}
/**
* @brief Tab completion on the first token of the line.
* Single match: complete it and append a space.
* Multiple matches: extend to the common prefix, or list candidates.
*/
static uint16_t cli_tab_complete(char* line, uint16_t len)
{
char common[CLI_LINE_BUF_SIZE];
uint16_t commonLen = 0;
int matchCount = 0;
uint32_t i;
/* Only complete the first token (no space typed yet) */
if (memchr(line, ' ', len) != NULL)
{
return len;
}
line[len] = '\0';
for (i = 0; i < CLI_CMD_COUNT; i++)
{
if (strncmp(s_cmds[i].name, line, len) == 0)
{
if (matchCount == 0)
{
strcpy(common, s_cmds[i].name);
commonLen = (uint16_t)strlen(common);
}
else
{
/* shrink common to the shared prefix */
while (commonLen > len && strncmp(common, s_cmds[i].name, commonLen) != 0)
{
commonLen--;
}
common[commonLen] = '\0';
}
matchCount++;
}
}
if (matchCount == 0)
{
return len;
}
if (matchCount == 1)
{
/* complete the command name + one space */
while (len < CLI_LINE_BUF_SIZE - 1 && len < commonLen)
{
bsp_usart_write_dma((const uint8_t*)&common[len], 1);
line[len] = common[len];
len++;
}
if (len < CLI_LINE_BUF_SIZE - 1)
{
line[len] = ' ';
len++;
cli_write(" ");
}
return len;
}
if (commonLen > len)
{
/* extend to the common prefix */
while (len < CLI_LINE_BUF_SIZE - 1 && len < commonLen)
{
bsp_usart_write_dma((const uint8_t*)&common[len], 1);
line[len] = common[len];
len++;
}
}
else
{
/* ambiguous: list the candidates and redraw */
cli_write("\r\n");
for (i = 0; i < CLI_CMD_COUNT; i++)
{
if (strncmp(s_cmds[i].name, line, len) == 0)
{
cli_write(s_cmds[i].name);
cli_write(" ");
}
}
cli_write("\r\n");
cli_redraw_line(line, len);
}
return len;
}
/**
* @brief CLI task: receive bytes, edit the line, dispatch on terminator.
* Extra keys: Ctrl+Z turns off active log printing, TAB completes
* the command name, UP/DOWN arrows walk the command history.
*/
static void CliTask(void* argument)
{
static char line[CLI_LINE_BUF_SIZE];
uint16_t len = 0;
uint16_t shownLen = 0; /* chars currently displayed after the prompt */
uint8_t swallow = 0; /* complementary terminator to swallow once */
uint8_t escState = 0; /* 0=normal, 1=got ESC, 2=got ESC '[' */
int histNav = -1; /* -1 = editing, 0.. = history entry (0=newest) */
uint8_t ch;
(void)argument;
cli_write("\r\nCLI ready. Type 'help' for commands.\r\n" CLI_PROMPT);
for (;;)
{
if (bsp_usart_read_byte(&ch, BSP_USART_WAIT_FOREVER) == 0)
{
continue;
}
/* Swallow the second half of a "\r\n" / "\n\r" pair */
if (swallow != 0)
{
uint8_t exp = swallow;
swallow = 0;
if (ch == exp)
{
continue;
}
}
/* Escape sequences (arrow keys: ESC '[' 'A'/'B') */
if (escState == 1)
{
escState = (ch == '[') ? 2 : 0;
continue;
}
if (escState == 2)
{
escState = 0;
if (ch == 'A') /* UP: older history entry */
{
if (histNav + 1 < (int)s_histCount)
{
histNav++;
strcpy(line, cli_history_get(histNav));
shownLen = cli_redraw_line(line, shownLen);
len = (uint16_t)strlen(line);
}
}
else if (ch == 'B') /* DOWN: newer entry / back to empty line */
{
if (histNav >= 0)
{
histNav--;
if (histNav >= 0)
{
strcpy(line, cli_history_get(histNav));
}
else
{
line[0] = '\0';
}
shownLen = cli_redraw_line(line, shownLen);
len = (uint16_t)strlen(line);
}
}
/* other escape sequences are ignored */
continue;
}
if (ch == CLI_KEY_ESC)
{
escState = 1;
continue;
}
if (ch == CLI_KEY_CTRL_Z)
{
/* Ctrl+Z: turn off active log printing */
AppTasks_SetLogEnabled(0);
cli_write("\r\n[log off]\r\n");
shownLen = cli_redraw_line(line, len);
continue;
}
if (ch == CLI_KEY_TAB)
{
bsp_usart_tx_lock();
len = cli_tab_complete(line, len);
shownLen = len;
bsp_usart_tx_unlock();
continue;
}
if (ch == '\r' || ch == '\n')
{
/* Any of "\r", "\n", "\r\n", "\n\r" terminates the line */
swallow = (ch == '\r') ? (uint8_t)'\n' : (uint8_t)'\r';
bsp_usart_tx_lock();
cli_write("\r\n");
line[len] = '\0';
cli_history_add(line); /* copy before dispatch tokenizes in place */
cli_dispatch(line);
cli_write(CLI_PROMPT);
bsp_usart_tx_unlock();
len = 0;
shownLen = 0;
histNav = -1;
}
else if (ch == CLI_KEY_BACKSPACE || ch == CLI_KEY_DEL)
{
if (len > 0)
{
len--;
cli_write("\b \b");
shownLen = len;
}
}
else if (ch >= 0x20 && ch < 0x7F)
{
if (len < CLI_LINE_BUF_SIZE - 1)
{
line[len++] = (char)ch;
bsp_usart_write_dma(&ch, 1); /* echo */
shownLen = len;
}
else
{
cli_write("\a"); /* line full: bell */
}
}
/* other control characters are ignored */
}
}
/**
* @brief Create the CLI task.
*/
void AppCli_Init(void)
{
xTaskCreate(CliTask, "CLI", CLI_TASK_STACK, NULL, CLI_TASK_PRIO, NULL);
}