/** * @file app_cli.c * @brief CLI UART frontend: an independent task receives command lines from * the USART1 RX interrupt queue, handles line editing (backspace, * history, TAB completion, Ctrl+Z) and hands finished lines to the * transport-agnostic CLI core (cli_core.c). Command table and command * handlers live in cli_core.c. * * 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 "cli_core.h" #include "bsp_usart.h" #include "app_tasks.h" #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_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 /** * @brief Frontend-local output: always USART1 DMA TX, independent of the * switchable cli_core output callback (used for prompt/echo/redraw). */ static void cli_uart_write(const char* str) { bsp_usart_write_dma((const uint8_t*)str, (uint16_t)strlen(str)); } /* ------------------------------------------------------------------ */ /* 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_uart_write("\r" CLI_PROMPT); for (i = 0; i < shownLen; i++) { cli_uart_write(" "); } cli_uart_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; uint32_t count = cli_cmd_count(); /* Only complete the first token (no space typed yet) */ if (memchr(line, ' ', len) != NULL) { return len; } line[len] = '\0'; for (i = 0; i < count; i++) { const char* name = cli_cmd_name(i); if (strncmp(name, line, len) == 0) { if (matchCount == 0) { strcpy(common, name); commonLen = (uint16_t)strlen(common); } else { /* shrink common to the shared prefix */ while (commonLen > len && strncmp(common, 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_uart_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_uart_write("\r\n"); for (i = 0; i < count; i++) { const char* name = cli_cmd_name(i); if (strncmp(name, line, len) == 0) { cli_uart_write(name); cli_uart_write(" "); } } cli_uart_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_uart_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_uart_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_uart_write("\r\n"); line[len] = '\0'; cli_history_add(line); /* copy before dispatch tokenizes in place */ cli_set_output(NULL); /* UART frontend: default output */ (void)cli_exec_line(line); cli_uart_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_uart_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_uart_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); }