/** * @file app_cli.c * @brief CLI frontend: an independent task receives command lines from the * USART1 RX interrupt queue (BLE downlink bytes are injected into the * same queue via AppCli_InputBytes), handles line editing (backspace, * history, TAB completion) and hands finished lines to the * transport-agnostic CLI core (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_ota.h" #include "app_wdt.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->" /* UART OTA binary mode: raw 0xCA frames in, framed OTA_RSP out (no echo). * Auto-exit after this much inactivity (ms). Must exceed the host's worst- * case retry window (ble_ota_update.py: 3 tries x 2 s) so a lost frame on a * flaky link does not drop the session before the host resends. */ #define OTA_MODE_IDLE_MS 10000u #define OTA_MODE_POLL_MS 100u static uint8_t s_otaMode; /** * @brief Switch the UART frontend into OTA binary frame mode. Called by the * "ota" CLI command (any CLI context; takes effect in CliTask). */ void app_cli_ota_mode_enter(void) { s_otaMode = 1; } /* Key codes */ #define CLI_KEY_TAB 0x09 #define CLI_KEY_ESC 0x1B #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. * TAB completes the command name, UP/DOWN arrows walk the history. * In OTA binary mode (CLI "ota"), bytes feed the OTA frame channel * raw - no echo/editing; the mode exits on 10s of inactivity (or on * device reset after a successful OTA_END). */ 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) */ uint32_t otaIdleMs = 0; uint8_t ch; (void)argument; cli_uart_write("\r\nCLI ready. Type 'help' for commands.\r\n" CLI_PROMPT); for (;;) { app_wdt_heartbeat(); /* OTA binary frame mode: raw bytes to the OTA channel. * otaIdleMs is (re)armed on every entry - a leftover count from a * previous session must not trip an instant idle timeout. */ otaIdleMs = 0; while (s_otaMode) { app_wdt_heartbeat(); if (bsp_usart_read_byte(&ch, OTA_MODE_POLL_MS) != 0) { if (app_ota_chan_rx_uart(ch) != 0) { /* OTA_ABORT: explicit exit back to the text CLI */ s_otaMode = 0; bsp_usart_tx_lock(); cli_uart_write("\r\n[ota] aborted, back to CLI\r\n" CLI_PROMPT); bsp_usart_tx_unlock(); } otaIdleMs = 0; continue; } otaIdleMs += OTA_MODE_POLL_MS; /* A byte gap this long mid-frame means the rest was lost on the * wire: drop the partial frame so a host resend re-syncs */ app_ota_chan_idle(); if (otaIdleMs >= OTA_MODE_IDLE_MS) { s_otaMode = 0; app_ota_chan_idle(); app_ota_abort(); /* discard a half-finished session */ bsp_usart_tx_lock(); cli_uart_write("\r\n[ota] idle timeout, back to CLI\r\n" CLI_PROMPT); bsp_usart_tx_unlock(); } } 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_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); /* default dual output (UART + BLE) */ (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 Feed CLI input bytes from a non-UART source (BLE downlink). * Runs in the BLE task context; bytes enter the same RX queue. */ void AppCli_InputBytes(const uint8_t* data, uint16_t len) { bsp_usart_rx_inject(data, len); } /** * @brief Create the CLI task. */ void AppCli_Init(void) { xTaskCreate(CliTask, "CLI", CLI_TASK_STACK, NULL, CLI_TASK_PRIO, NULL); }