- pinset <pxN> [0|1]: 设置 GPIO 输出高低 / 读当前电平 (pa0-6/pb0-13) - Boot/src/boot_cli.c + mcm-ddc-ble/src/cli_core.c 各加一份同名命令 - 对 SWD(PA4/PA5) / 控制台(PB6/PB7) 重配前打印 warning - Boot V1.00.06 -> V1.00.07; APP V1.00.30 -> V1.00.31 (0x0001001F) - docs/开发日志.md: 新增 §59
789 lines
22 KiB
C
789 lines
22 KiB
C
/**
|
|
* @file boot_cli.c
|
|
* @brief Bootloader download-mode CLI: bare-metal polled line editor
|
|
* (echo, backspace, TAB command-name completion, UP/DOWN history)
|
|
* + command table, and the UART OTA binary frame mode.
|
|
*
|
|
* Wire-compatible with the application's UART CLI so that
|
|
* tools/ble_ota_update.py works unchanged against the bootloader:
|
|
* - prompt "caiic->" (the script's PROMPT marker)
|
|
* - "devinfo" prints "cur bank: APP<n>" (the boot's OTA target is the
|
|
* OTHER bank, exactly like the app's "write the inactive bank" rule)
|
|
* - "ota" prints the "[ota] binary mode ON" go-ahead marker, then the
|
|
* stream carries 0xCA OTA frames (engine: shared app_ota.c)
|
|
*
|
|
* No libc printf: cli_printf() below is a minimal formatter (%% %c %s %d
|
|
* %u %x %X, width, '-'/'0' pads, 'l' ignored - all integers are 32-bit).
|
|
*/
|
|
#include "cli_core.h" /* bootloader shim (Boot/src) */
|
|
#include "boot_usart.h"
|
|
#include "boot_version.h"
|
|
#include "app_bootset.h"
|
|
#include "app_ota.h"
|
|
#include "dfu_layout.h"
|
|
#include "boot_crc32.h"
|
|
#include "n32wb03x.h"
|
|
|
|
#include <string.h>
|
|
#include <stddef.h>
|
|
#include <stdarg.h>
|
|
|
|
#define CLI_TX_BUF_SIZE 96
|
|
#define CLI_LINE_SIZE 80
|
|
#define CLI_MAX_ARGS 8
|
|
#define OTA_IDLE_EXIT_MS 3000u /* binary mode fallback exit */
|
|
#define CLI_KEY_TAB 0x09 /* TAB: command-name completion */
|
|
#define CLI_KEY_ESC 0x1B /* ESC: arrow-key escape sequence start */
|
|
#define CLI_HISTORY_SIZE 8 /* number of remembered command lines */
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Output (cli_core.h shim implementation) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void cli_write(const char* str)
|
|
{
|
|
boot_usart_write((const uint8_t*)str, (uint16_t)strlen(str));
|
|
}
|
|
|
|
/** Format v into out (NUL-terminated), least-significant first reversed. */
|
|
static void u32_to_str(uint32_t v, char* out, uint8_t base, int neg)
|
|
{
|
|
char tmp[11]; /* 10 digits + sign */
|
|
uint8_t n = 0;
|
|
|
|
do
|
|
{
|
|
uint32_t d = v % base;
|
|
tmp[n++] = (char)((d < 10u) ? ('0' + d) : ('A' + d - 10u));
|
|
v /= base;
|
|
} while (v != 0u);
|
|
if (neg)
|
|
{
|
|
tmp[n++] = '-';
|
|
}
|
|
while (n != 0u)
|
|
{
|
|
*out++ = tmp[--n];
|
|
}
|
|
*out = '\0';
|
|
}
|
|
|
|
static char* str_pad(char* out, const char* s, int width, uint8_t left, char pad)
|
|
{
|
|
int len = (int)strlen(s);
|
|
|
|
if (!left)
|
|
{
|
|
while (width-- > len)
|
|
{
|
|
*out++ = pad;
|
|
}
|
|
}
|
|
while (*s != '\0')
|
|
{
|
|
*out++ = *s++;
|
|
}
|
|
while (width-- > len)
|
|
{
|
|
*out++ = ' ';
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void cli_printf(const char* fmt, ...)
|
|
{
|
|
static char s_txBuf[CLI_TX_BUF_SIZE];
|
|
char* out = s_txBuf;
|
|
va_list args;
|
|
|
|
va_start(args, fmt);
|
|
while (*fmt != '\0' && out < s_txBuf + CLI_TX_BUF_SIZE - 12)
|
|
{
|
|
char c = *fmt++;
|
|
|
|
if (c != '%')
|
|
{
|
|
*out++ = c;
|
|
continue;
|
|
}
|
|
{
|
|
uint8_t left = 0;
|
|
char pad = ' ';
|
|
int width = 0;
|
|
char num[12];
|
|
char spec;
|
|
|
|
if (*fmt == '-')
|
|
{
|
|
left = 1;
|
|
fmt++;
|
|
}
|
|
if (*fmt == '0')
|
|
{
|
|
pad = '0';
|
|
fmt++;
|
|
}
|
|
while (*fmt >= '0' && *fmt <= '9')
|
|
{
|
|
width = width * 10 + (*fmt++ - '0');
|
|
}
|
|
if (*fmt == 'l')
|
|
{
|
|
fmt++; /* 32-bit already: %lu == %u here */
|
|
}
|
|
spec = *fmt;
|
|
if (spec != '\0')
|
|
{
|
|
fmt++;
|
|
}
|
|
switch (spec)
|
|
{
|
|
case 's':
|
|
out = str_pad(out, va_arg(args, const char*), width, left, ' ');
|
|
break;
|
|
case 'c':
|
|
*out++ = (char)va_arg(args, int);
|
|
break;
|
|
case 'd':
|
|
{
|
|
long v = va_arg(args, long);
|
|
u32_to_str((uint32_t)((v < 0) ? -v : v), num, 10, v < 0);
|
|
out = str_pad(out, num, width, left, pad);
|
|
break;
|
|
}
|
|
case 'u':
|
|
u32_to_str(va_arg(args, unsigned long), num, 10, 0);
|
|
out = str_pad(out, num, width, left, pad);
|
|
break;
|
|
case 'x':
|
|
case 'X':
|
|
u32_to_str(va_arg(args, unsigned long), num, 16, 0);
|
|
out = str_pad(out, num, width, left, pad);
|
|
break;
|
|
case '%':
|
|
*out++ = '%';
|
|
break;
|
|
default:
|
|
*out++ = '%';
|
|
if (spec != '\0')
|
|
{
|
|
*out++ = spec;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
va_end(args);
|
|
*out = '\0';
|
|
cli_write(s_txBuf);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Commands */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
typedef void (*CliCmdHandler_t)(int argc, char* argv[]);
|
|
|
|
typedef struct
|
|
{
|
|
const char* name;
|
|
const char* help;
|
|
CliCmdHandler_t handler;
|
|
} CliCmd_t;
|
|
|
|
static uint8_t s_otaMode; /* 1 = OTA binary frame mode */
|
|
|
|
static void CmdHelp(int argc, char* argv[]);
|
|
|
|
static void CmdVersion(int argc, char* argv[])
|
|
{
|
|
(void)argc;
|
|
(void)argv;
|
|
cli_printf("bootloader version: %s (download mode)\r\n", BOOT_FW_VERSION);
|
|
cli_printf("SystemCoreClock = %lu Hz\r\n", (unsigned long)SystemCoreClock);
|
|
}
|
|
|
|
/**
|
|
* @brief "devinfo": report the bank the NEXT OTA would replace, using the
|
|
* same "cur bank: APP<n>" line the application prints - the PC
|
|
* updater picks the payload for the other bank, which is exactly
|
|
* the boot's OTA target rule (inactive bank; APP1 when the
|
|
* bootsetting record is invalid).
|
|
*/
|
|
static void CmdDevInfo(int argc, char* argv[])
|
|
{
|
|
const caiic_bootsetting_t* bs = (const caiic_bootsetting_t*)CAIIC_BOOTSETTING_ADDR;
|
|
uint32_t active;
|
|
uint8_t bs_ok;
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
bs_ok = (bs->magic == CAIIC_BOOT_MAGIC &&
|
|
bs->crc32 == caiic_crc32((const uint8_t*)bs,
|
|
(uint32_t)offsetof(caiic_bootsetting_t, crc32))) ? 1 : 0;
|
|
active = bs->active_bank;
|
|
|
|
cli_printf("bootloader: %s, download mode\r\n", BOOT_FW_VERSION);
|
|
if (!bs_ok)
|
|
{
|
|
cli_write("bootsetting: INVALID - OTA targets APP1\r\n");
|
|
active = CAIIC_ACTIVE_BANK2; /* report the bank NOT targeted */
|
|
}
|
|
cli_printf("cur bank: APP%lu\r\n", (unsigned long)active);
|
|
cli_printf("uptime: %lu s\r\n", (unsigned long)(boot_millis() / 1000u));
|
|
}
|
|
|
|
/**
|
|
* @brief "ota": enter the OTA binary frame mode (0xCA frames on the raw
|
|
* stream, framed RSP on TX). The marker line is the host's go-ahead.
|
|
* Exits: OTA_ABORT frame, 3 s idle, or reset after OTA_END.
|
|
*/
|
|
static void CmdOta(int argc, char* argv[])
|
|
{
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
cli_write("[ota] binary mode ON - send 0xCA OTA frames now\r\n"
|
|
"(ble_protocol.md section 6.6; OTA_ABORT or 3s idle exits)\r\n");
|
|
s_otaMode = 1;
|
|
}
|
|
|
|
static void CmdReset(int argc, char* argv[])
|
|
{
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
cli_write("resetting...\r\n");
|
|
boot_delay_ms(200u);
|
|
NVIC_SystemReset();
|
|
}
|
|
|
|
static void CmdUartInfo(int argc, char* argv[])
|
|
{
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
cli_write("usart1: TX=PB6 RX=PB7 (AF4)\r\n");
|
|
cli_write("baud: 460800, 8 data bits, no parity, 1 stop bit\r\n");
|
|
cli_write("flow control: none; tx: polled; rx: DMA ring (3KB, polled)\r\n");
|
|
}
|
|
|
|
/**
|
|
* @brief Parse a "pxN" pin name (e.g. "pb0", "PA3", "PB13") into a GPIO port
|
|
* and pin mask. @return 1 on success, 0 on bad name/out-of-range pin.
|
|
*/
|
|
static int pinset_parse(const char* name, GPIO_Module** port, uint16_t* mask)
|
|
{
|
|
const char* p;
|
|
int pin = 0;
|
|
|
|
if (name[0] != 'p' && name[0] != 'P')
|
|
{
|
|
return 0;
|
|
}
|
|
if (name[1] == 'a' || name[1] == 'A')
|
|
{
|
|
*port = GPIOA;
|
|
}
|
|
else if (name[1] == 'b' || name[1] == 'B')
|
|
{
|
|
*port = GPIOB;
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
p = &name[2];
|
|
if (*p < '0' || *p > '9')
|
|
{
|
|
return 0;
|
|
}
|
|
while (*p >= '0' && *p <= '9')
|
|
{
|
|
pin = pin * 10 + (*p - '0');
|
|
if (pin > 15)
|
|
{
|
|
return 0;
|
|
}
|
|
p++;
|
|
}
|
|
if (*p != '\0')
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (*port == GPIOA && pin > 6) /* PA7 not bonded on this chip */
|
|
{
|
|
return 0;
|
|
}
|
|
if (*port == GPIOB && pin > 13) /* PB14/PB15 not bonded */
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
*mask = (uint16_t)(1u << pin);
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* @brief "pinset <pxN> [0|1]": set a GPIO output high/low, or read its level.
|
|
* e.g. "pinset pb0 1", "pinset pa0 0", "pinset pb0" (read level).
|
|
*/
|
|
static void CmdPinSet(int argc, char* argv[])
|
|
{
|
|
GPIO_Module* port = NULL;
|
|
uint16_t mask = 0;
|
|
GPIO_InitType s;
|
|
|
|
if (argc < 2)
|
|
{
|
|
cli_write("usage: pinset <pxN> [0|1] (e.g. pinset pb0 1)\r\n");
|
|
return;
|
|
}
|
|
|
|
if (!pinset_parse(argv[1], &port, &mask))
|
|
{
|
|
cli_printf("bad pin: %s (use pa0..pa6 / pb0..pb13)\r\n", argv[1]);
|
|
return;
|
|
}
|
|
|
|
if (port == GPIOA)
|
|
{
|
|
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA, ENABLE);
|
|
}
|
|
else
|
|
{
|
|
RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB, ENABLE);
|
|
}
|
|
|
|
if (argc >= 3)
|
|
{
|
|
/* reserved-pin guard: warn before reconfiguring SWD / console pins */
|
|
if ((port == GPIOA && (mask & (GPIO_PIN_4 | GPIO_PIN_5)) != 0u) ||
|
|
(port == GPIOB && (mask & (GPIO_PIN_6 | GPIO_PIN_7)) != 0u))
|
|
{
|
|
cli_write("warning: SWD/console pin - reconfigure may break the link\r\n");
|
|
}
|
|
|
|
GPIO_InitStruct(&s);
|
|
s.Pin = mask;
|
|
s.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
|
|
GPIO_InitPeripheral(port, &s);
|
|
|
|
if (argv[2][0] == '1' && argv[2][1] == '\0')
|
|
{
|
|
GPIO_SetBits(port, mask);
|
|
cli_printf("%s = 1\r\n", argv[1]);
|
|
}
|
|
else if (argv[2][0] == '0' && argv[2][1] == '\0')
|
|
{
|
|
GPIO_ResetBits(port, mask);
|
|
cli_printf("%s = 0\r\n", argv[1]);
|
|
}
|
|
else
|
|
{
|
|
cli_write("usage: pinset <pxN> [0|1] (0=low, 1=high)\r\n");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* read: the input data register reflects the actual pin level */
|
|
cli_printf("%s = %u\r\n", argv[1],
|
|
(unsigned)(GPIO_ReadInputDataBit(port, mask) ? 1u : 0u));
|
|
}
|
|
}
|
|
|
|
static const CliCmd_t s_cmds[] = {
|
|
{"help", "help [cmd]: list commands / show one command's help", CmdHelp},
|
|
{"version", "show bootloader version", CmdVersion},
|
|
{"devinfo", "show boot info + cur bank (OTA writes the OTHER bank)", CmdDevInfo},
|
|
{"bsdump", "bsdump: show bootsetting record + crc check", AppBootset_CmdBsdump},
|
|
{"bsset", "bsset <field> <val>: write bootsetting field (careful!)", AppBootset_CmdBsset},
|
|
{"appsw", "appsw [1|2]: switch boot bank (crc verified) + reboot", AppBootset_CmdAppswitch},
|
|
{"ota", "ota: UART enters OTA binary frame mode (see help ota)", CmdOta},
|
|
{"reset", "reset: system reset (reboot)", CmdReset},
|
|
{"pinset", "pinset <pxN> [0|1]: set/read GPIO level (pa0-6/pb0-13)", CmdPinSet},
|
|
{"uartinfo", "uartinfo: show USART1 pins/baud/format", CmdUartInfo},
|
|
};
|
|
|
|
#define CLI_CMD_COUNT (sizeof(s_cmds) / sizeof(s_cmds[0]))
|
|
|
|
static void CmdHelp(int argc, char* argv[])
|
|
{
|
|
uint32_t i;
|
|
|
|
if (argc >= 2)
|
|
{
|
|
for (i = 0; i < CLI_CMD_COUNT; i++)
|
|
{
|
|
if (strcmp(argv[1], s_cmds[i].name) == 0)
|
|
{
|
|
cli_printf(" %-8s %s\r\n", s_cmds[i].name, s_cmds[i].help);
|
|
return;
|
|
}
|
|
}
|
|
cli_printf("unknown command: %s (try 'help')\r\n", argv[1]);
|
|
return;
|
|
}
|
|
|
|
cli_write("mode: BOOT download\r\n");
|
|
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\r\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Line parsing / dispatch */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static int cli_exec_line(char* line)
|
|
{
|
|
char* argv[CLI_MAX_ARGS];
|
|
int argc = 0;
|
|
char* p = line;
|
|
uint32_t i;
|
|
|
|
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 0;
|
|
}
|
|
|
|
for (i = 0; i < CLI_CMD_COUNT; i++)
|
|
{
|
|
if (strcmp(argv[0], s_cmds[i].name) == 0)
|
|
{
|
|
s_cmds[i].handler(argc, argv);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
cli_printf("unknown command: %s (try 'help')\r\n", argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Download-mode superloop */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
#define BOOT_CLI_PROMPT "caiic->" /* wire-compatible with the app CLI */
|
|
|
|
/**
|
|
* @brief Redraw the input line: erase `shownLen` chars, print prompt + line.
|
|
* @return new shown length (in the boot CLI this always equals strlen(line)).
|
|
*/
|
|
static uint16_t boot_cli_redraw(const char* line, uint16_t shownLen)
|
|
{
|
|
uint16_t i;
|
|
uint16_t len = (uint16_t)strlen(line);
|
|
|
|
cli_write("\r" BOOT_CLI_PROMPT);
|
|
for (i = 0; i < shownLen; i++)
|
|
{
|
|
cli_write(" ");
|
|
}
|
|
cli_write("\r" BOOT_CLI_PROMPT);
|
|
if (len > 0u)
|
|
{
|
|
boot_usart_write((const uint8_t*)line, len);
|
|
}
|
|
return len;
|
|
}
|
|
|
|
/**
|
|
* @brief TAB completion on the first token of the line (command name only).
|
|
* Single match: complete it and append a space.
|
|
* Multiple matches: extend to the common prefix, or list candidates.
|
|
* Ported from the application CLI (mcm-ddc-ble/src/app_cli.c).
|
|
*/
|
|
static uint16_t boot_cli_tab_complete(char* line, uint16_t len)
|
|
{
|
|
char common[CLI_LINE_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++)
|
|
{
|
|
const char* name = s_cmds[i].name;
|
|
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_SIZE - 1u && len < commonLen)
|
|
{
|
|
boot_usart_write((const uint8_t*)&common[len], 1);
|
|
line[len] = common[len];
|
|
len++;
|
|
}
|
|
if (len < CLI_LINE_SIZE - 1u)
|
|
{
|
|
line[len] = ' ';
|
|
len++;
|
|
cli_write(" ");
|
|
}
|
|
return len;
|
|
}
|
|
|
|
if (commonLen > len)
|
|
{
|
|
/* extend to the common prefix */
|
|
while (len < CLI_LINE_SIZE - 1u && len < commonLen)
|
|
{
|
|
boot_usart_write((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++)
|
|
{
|
|
const char* name = s_cmds[i].name;
|
|
if (strncmp(name, line, len) == 0)
|
|
{
|
|
cli_write(name);
|
|
cli_write(" ");
|
|
}
|
|
}
|
|
cli_write("\r\n");
|
|
boot_cli_redraw(line, len);
|
|
}
|
|
return len;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Command history (ring buffer) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static char s_history[CLI_HISTORY_SIZE][CLI_LINE_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 > 0u)
|
|
{
|
|
uint8_t last = (uint8_t)((s_histHead + CLI_HISTORY_SIZE - 1u) % CLI_HISTORY_SIZE);
|
|
if (strcmp(s_history[last], line) == 0)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
strcpy(s_history[s_histHead], line);
|
|
s_histHead = (uint8_t)((s_histHead + 1u) % 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];
|
|
}
|
|
|
|
/**
|
|
* @brief Download-mode main loop. Never returns (leaves via reset only).
|
|
*/
|
|
void boot_cli_run(void)
|
|
{
|
|
static char s_line[CLI_LINE_SIZE];
|
|
uint16_t len = 0;
|
|
uint16_t shownLen = 0; /* chars currently displayed after the prompt */
|
|
uint8_t escState = 0; /* 0=normal, 1=got ESC, 2=got ESC '[' */
|
|
int histNav = -1; /* -1 = editing, 0.. = history entry (0=newest) */
|
|
uint32_t otaLastRx = 0;
|
|
|
|
cli_write("\r\nCLI ready (bootloader download mode). Type 'help' for commands.\r\n"
|
|
BOOT_CLI_PROMPT);
|
|
|
|
for (;;)
|
|
{
|
|
uint8_t ch;
|
|
|
|
/* deferred post-OTA_END reset (waits until the RSP drained) */
|
|
app_ota_reset_poll();
|
|
|
|
/* OTA binary-mode fallback exit after 3 s of silence */
|
|
if (s_otaMode && (boot_millis() - otaLastRx) > OTA_IDLE_EXIT_MS)
|
|
{
|
|
s_otaMode = 0;
|
|
app_ota_chan_idle();
|
|
cli_write("\r\n[ota] binary mode OFF (idle)\r\n" BOOT_CLI_PROMPT);
|
|
len = 0;
|
|
shownLen = 0;
|
|
}
|
|
|
|
if (boot_usart_read(&ch) == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (s_otaMode)
|
|
{
|
|
otaLastRx = boot_millis();
|
|
if (app_ota_chan_rx_uart(ch) != 0)
|
|
{
|
|
s_otaMode = 0;
|
|
cli_write("\r\n[ota] binary mode OFF (abort)\r\n" BOOT_CLI_PROMPT);
|
|
len = 0;
|
|
shownLen = 0;
|
|
}
|
|
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(s_line, cli_history_get(histNav));
|
|
shownLen = boot_cli_redraw(s_line, shownLen);
|
|
len = (uint16_t)strlen(s_line);
|
|
}
|
|
}
|
|
else if (ch == 'B') /* DOWN: newer entry / back to empty line */
|
|
{
|
|
if (histNav >= 0)
|
|
{
|
|
histNav--;
|
|
if (histNav >= 0)
|
|
{
|
|
strcpy(s_line, cli_history_get(histNav));
|
|
}
|
|
else
|
|
{
|
|
s_line[0] = '\0';
|
|
}
|
|
shownLen = boot_cli_redraw(s_line, shownLen);
|
|
len = (uint16_t)strlen(s_line);
|
|
}
|
|
}
|
|
/* other escape sequences are ignored */
|
|
continue;
|
|
}
|
|
|
|
switch (ch)
|
|
{
|
|
case CLI_KEY_ESC:
|
|
escState = 1;
|
|
break;
|
|
|
|
case '\r':
|
|
case '\n':
|
|
cli_write("\r\n");
|
|
s_line[len] = '\0';
|
|
cli_history_add(s_line); /* copy before dispatch tokenizes in place */
|
|
(void)cli_exec_line(s_line);
|
|
len = 0;
|
|
shownLen = 0;
|
|
histNav = -1;
|
|
cli_write(BOOT_CLI_PROMPT);
|
|
break;
|
|
|
|
case '\b':
|
|
case 0x7F:
|
|
if (len != 0u)
|
|
{
|
|
len--;
|
|
cli_write("\b \b");
|
|
shownLen = len;
|
|
}
|
|
break;
|
|
|
|
case CLI_KEY_TAB:
|
|
len = boot_cli_tab_complete(s_line, len);
|
|
shownLen = len;
|
|
break;
|
|
|
|
default:
|
|
if (ch >= 0x20u && ch < 0x7Fu && len < CLI_LINE_SIZE - 1u)
|
|
{
|
|
s_line[len++] = (char)ch;
|
|
boot_usart_write(&ch, 1); /* echo */
|
|
shownLen = len;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|