/** * @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 "bsp_usart.h" #include "bsp_led.h" #include "app_tasks.h" #include #include #include #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_write("N32WB031 FreeRTOS + UART(DMA) + LED + CLI demo\r\n"); cli_printf("SystemCoreClock = %lu Hz\r\n", (unsigned long)SystemCoreClock); } /** * @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> ": 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> \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> \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> \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/clock info", CmdVersion}, {"log", "log [on|off]: active printing switch (default off)", CmdLog}, {"led", "led <1|2> : 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); }