415 lines
12 KiB
C
415 lines
12 KiB
C
/**
|
|
* @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 <string.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_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);
|
|
}
|