File syn_cli.c¶
Go to the documentation of this file
#if __has_include("syn_config.h")
#include "syn_config.h"
#endif
#if !defined(SYN_USE_CLI) || SYN_USE_CLI
#include "../port/syn_port_serial.h"
#include "../util/syn_assert.h"
#include "../util/syn_fmt.h"
#include "syn_cli.h"
#include <string.h>
#if !defined(SYN_CLI_USE_PRINTF) || SYN_CLI_USE_PRINTF
#include <stdarg.h>
#include <stdio.h>
#endif
/* ── Internal helpers ───────────────────────────────────────────────────── */
/* Forward declarations for built-in commands */
#if SYN_CLI_CMD_VERSION
static void cli_builtin_version(const SYN_CLI *cli);
#endif
#if SYN_CLI_CMD_UPTIME
static void cli_builtin_uptime(const SYN_CLI *cli);
#endif
#if SYN_CLI_CMD_ERRORS
static void cli_builtin_errors(const SYN_CLI *cli);
#endif
#if SYN_CLI_CMD_TASKS
static void cli_builtin_tasks(const SYN_CLI *cli);
#endif
/* Runtime-configured singletons (set via syn_cli_set_*) */
#if SYN_CLI_CMD_ERRORS
struct SYN_ErrLog;
static struct SYN_ErrLog *s_cli_errlog = NULL;
#endif
#if SYN_CLI_CMD_TASKS
struct SYN_Sched;
static struct SYN_Sched *s_cli_sched = NULL;
#endif
static void cli_putchar(const SYN_CLI *cli, char ch)
{
(void)cli;
syn_port_serial_write_byte((uint8_t)ch);
}
static void cli_puts(const SYN_CLI *cli, const char *str)
{
(void)cli;
if (str == NULL)
return;
size_t len = strlen(str);
if (len > 0) {
syn_port_serial_write((const uint8_t *)str, len);
}
}
static int cli_tokenize(char *line, char *argv[], int max_args)
{
int argc = 0;
char *p = line;
while (*p && argc < max_args) {
/* Skip whitespace */
while (*p == ' ' || *p == '\t')
p++;
if (*p == '\0')
break;
/* Handle quoted string */
if (*p == '"') {
p++; /* skip opening quote */
argv[argc++] = p;
while (*p && *p != '"')
p++;
if (*p == '"')
*p++ = '\0';
} else {
argv[argc++] = p;
while (*p && *p != ' ' && *p != '\t')
p++;
if (*p)
*p++ = '\0';
}
}
return argc;
}
static void cli_dispatch(SYN_CLI *cli, char *line)
{
char *argv[SYN_CLI_MAX_ARGS];
int argc = cli_tokenize(line, argv, SYN_CLI_MAX_ARGS);
if (argc == 0) {
return; /* empty line */
}
/* Built-in: help */
if (strcmp(argv[0], "help") == 0 || strcmp(argv[0], "?") == 0) {
cli_puts(cli, "Available commands:\r\n");
/* Built-in help entry */
cli_puts(cli, " help -- Show this help\r\n");
#if SYN_CLI_CMD_VERSION
cli_puts(cli, " version -- Show build info\r\n");
#endif
#if SYN_CLI_CMD_UPTIME
cli_puts(cli, " uptime -- Show system uptime\r\n");
#endif
#if SYN_CLI_CMD_ERRORS
if (s_cli_errlog != NULL)
cli_puts(cli, " errors -- Dump error log\r\n");
#endif
#if SYN_CLI_CMD_TASKS
if (s_cli_sched != NULL)
cli_puts(cli, " tasks -- Show scheduler tasks\r\n");
#endif
for (size_t i = 0; i < cli->command_count; i++) {
cli_puts(cli, " ");
cli_puts(cli, cli->commands[i].name);
if (cli->commands[i].help != NULL) {
/* Pad to align with built-in entries (15 chars for name) */
size_t name_len = strlen(cli->commands[i].name);
for (size_t p = name_len; p < 15; p++) {
cli_putchar(cli, ' ');
}
cli_puts(cli, "-- ");
cli_puts(cli, cli->commands[i].help);
}
cli_puts(cli, "\r\n");
}
return;
}
/* Built-in: version */
#if SYN_CLI_CMD_VERSION
if (strcmp(argv[0], "version") == 0) {
cli_builtin_version(cli);
return;
}
#endif
/* Built-in: uptime */
#if SYN_CLI_CMD_UPTIME
if (strcmp(argv[0], "uptime") == 0) {
cli_builtin_uptime(cli);
return;
}
#endif
/* Built-in: errors */
#if SYN_CLI_CMD_ERRORS
if (strcmp(argv[0], "errors") == 0) {
cli_builtin_errors(cli);
return;
}
#endif
/* Built-in: tasks */
#if SYN_CLI_CMD_TASKS
if (strcmp(argv[0], "tasks") == 0) {
cli_builtin_tasks(cli);
return;
}
#endif
/* Search command table */
for (size_t i = 0; i < cli->command_count; i++) {
if (strcmp(argv[0], cli->commands[i].name) == 0) {
if (cli->commands[i].handler != NULL) {
int ret = cli->commands[i].handler(argc, argv);
if (ret != 0) {
char buf[32];
const char *parts[] = {"Error: ", NULL, "\r\n"};
char num[12];
syn_fmt_int(num, sizeof(num), ret);
parts[1] = num;
syn_fmt_concat(buf, sizeof(buf), parts, 3);
cli_puts(cli, buf);
}
}
return;
}
}
/* Command not found */
cli_puts(cli, "Unknown command: ");
cli_puts(cli, argv[0]);
cli_puts(cli, "\r\nType 'help' for a list of commands.\r\n");
}
#if SYN_CLI_HISTORY_DEPTH > 0
static void cli_history_push(SYN_CLI *cli, const char *line)
{
if (line[0] == '\0')
return;
/* Don't store duplicates of the last entry */
if (cli->history_count > 0) {
size_t last = (cli->history_write + SYN_CLI_HISTORY_DEPTH - 1) % SYN_CLI_HISTORY_DEPTH;
if (strcmp(cli->history[last], line) == 0) {
cli->history_read = cli->history_write; /* reset cursor */
return;
}
}
strncpy(cli->history[cli->history_write], line, SYN_CLI_LINE_BUF_SIZE - 1);
cli->history[cli->history_write][SYN_CLI_LINE_BUF_SIZE - 1] = '\0';
cli->history_write = (cli->history_write + 1) % SYN_CLI_HISTORY_DEPTH;
if (cli->history_count < SYN_CLI_HISTORY_DEPTH) {
cli->history_count++;
}
cli->history_read = cli->history_write;
}
#endif
/* ── Public API ─────────────────────────────────────────────────────────── */
void syn_cli_init(SYN_CLI *cli, const SYN_CLI_Command *commands, size_t cmd_count,
const char *prompt)
{
SYN_ASSERT(cli != NULL);
memset(cli, 0, sizeof(*cli));
cli->commands = commands;
cli->command_count = cmd_count;
cli->prompt = (prompt != NULL) ? prompt : "> ";
cli->echo = true;
cli->line_pos = 0;
}
void syn_cli_set_echo(SYN_CLI *cli, bool echo)
{
SYN_ASSERT(cli != NULL);
cli->echo = echo;
}
void syn_cli_process_char(SYN_CLI *cli, char ch)
{
SYN_ASSERT(cli != NULL);
/* ANSI escape sequence parsing (e.g. Up Arrow = \x1B[A) */
if (cli->escape_state == 1) {
if (ch == '[')
cli->escape_state = 2;
else
cli->escape_state = 0;
return;
}
if (cli->escape_state == 2) {
cli->escape_state = 0;
if (ch == 'A')
ch = 0x10; /* Map Up arrow to Ctrl-P */
else
return; /* Ignore other arrows for now */
}
if (ch == 0x1B) {
cli->escape_state = 1;
return;
}
/* Newline — execute */
if (ch == '\r' || ch == '\n') {
if (cli->echo) {
cli_puts(cli, "\r\n");
}
cli->line_buf[cli->line_pos] = '\0';
#if SYN_CLI_HISTORY_DEPTH > 0
cli_history_push(cli, cli->line_buf);
#endif
cli->prompt_visible = false;
cli->line_pos = 0;
cli_dispatch(cli, cli->line_buf);
syn_cli_print_prompt(cli);
return;
}
/* Backspace / DEL */
if (ch == '\b' || ch == 0x7F) {
if (cli->line_pos > 0) {
cli->line_pos--;
if (cli->echo) {
cli_puts(cli, "\b \b"); /* erase character on terminal */
}
}
return;
}
/* Ctrl-C — cancel current line */
if (ch == 0x03) {
if (cli->echo) {
cli_puts(cli, "^C\r\n");
}
cli->line_pos = 0;
syn_cli_print_prompt(cli);
return;
}
/* Ctrl-U — clear line */
if (ch == 0x15) {
while (cli->line_pos > 0) {
cli->line_pos--;
if (cli->echo) {
cli_puts(cli, "\b \b");
}
}
return;
}
#if SYN_CLI_HISTORY_DEPTH > 0
/* Up arrow (simplified: Ctrl-P) — previous history */
if (ch == 0x10) { /* Ctrl-P */
if (cli->history_count > 0) {
/* Erase current line */
while (cli->line_pos > 0) {
cli->line_pos--;
if (cli->echo)
cli_puts(cli, "\b \b");
}
/* Navigate backwards */
cli->history_read =
(cli->history_read + SYN_CLI_HISTORY_DEPTH - 1) % SYN_CLI_HISTORY_DEPTH;
if (cli->history_read >= cli->history_count) {
cli->history_read = cli->history_count - 1;
}
strncpy(cli->line_buf, cli->history[cli->history_read], SYN_CLI_LINE_BUF_SIZE - 1);
cli->line_pos = strlen(cli->line_buf);
if (cli->echo)
cli_puts(cli, cli->line_buf);
}
return;
}
#endif
/* Ignore other control characters */
if (ch < 0x20 && ch != '\t') {
return;
}
/* Tab — treat as space */
if (ch == '\t') {
ch = ' ';
}
/* Normal character — append to buffer */
if (cli->line_pos < SYN_CLI_LINE_BUF_SIZE - 1) {
cli->line_buf[cli->line_pos++] = ch;
if (cli->echo) {
cli_putchar(cli, ch);
}
}
/* Silently drop if buffer is full */
}
void syn_cli_process_line(SYN_CLI *cli, const char *line)
{
SYN_ASSERT(cli != NULL);
SYN_ASSERT(line != NULL);
/* Copy into the line buffer so we can tokenize in-place */
strncpy(cli->line_buf, line, SYN_CLI_LINE_BUF_SIZE - 1);
cli->line_buf[SYN_CLI_LINE_BUF_SIZE - 1] = '\0';
cli->line_pos = 0;
cli_dispatch(cli, cli->line_buf);
}
void syn_cli_print_prompt(SYN_CLI *cli)
{
SYN_ASSERT(cli != NULL);
cli_puts(cli, cli->prompt);
if (cli->line_pos > 0) {
cli->line_buf[cli->line_pos] = '\0';
cli_puts(cli, cli->line_buf);
}
cli->prompt_visible = true;
}
void syn_cli_refresh_prompt(SYN_CLI *cli)
{
syn_cli_print_prompt(cli);
}
#if !defined(SYN_CLI_USE_PRINTF) || SYN_CLI_USE_PRINTF
void syn_cli_printf(SYN_CLI *cli, const char *fmt, ...)
{
SYN_ASSERT(cli != NULL);
bool was_prompt_visible = cli->prompt_visible;
if (was_prompt_visible) {
/* Clear prompt & active draft line once before outputting text */
cli_puts(cli, "\r\033[K");
cli->prompt_visible = false;
}
char buf[SYN_CLI_LINE_BUF_SIZE];
va_list args;
va_start(args, fmt);
int n = vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
if (n > 0) {
buf[sizeof(buf) - 1] = '\0';
cli_puts(cli, buf);
}
if (was_prompt_visible) {
/* Redraw prompt & draft text after outputting text */
syn_cli_print_prompt(cli);
}
}
#endif
/* ── Built-in command implementations ───────────────────────────────────── */
#include "../port/syn_port_system.h"
#include "../sched/syn_sched.h"
#include "../system/syn_errlog.h"
#include "../system/syn_version.h"
/* Singletons — defined near top of file, setters here */
#if SYN_CLI_CMD_ERRORS
void syn_cli_set_errlog(SYN_ErrLog *errlog)
{
s_cli_errlog = errlog;
}
#endif
#if SYN_CLI_CMD_TASKS
void syn_cli_set_scheduler(SYN_Sched *sched)
{
s_cli_sched = sched;
}
#endif
#if SYN_CLI_CMD_VERSION
static void cli_builtin_version(const SYN_CLI *cli)
{
char num[12];
const SYN_Version *v = syn_version();
cli_puts(cli, v->app_name);
cli_puts(cli, " v");
syn_fmt_uint(num, sizeof(num), v->year);
cli_puts(cli, num);
cli_puts(cli, ".");
syn_fmt_uint(num, sizeof(num), v->month);
cli_puts(cli, num);
cli_puts(cli, ".");
syn_fmt_uint(num, sizeof(num), v->release);
cli_puts(cli, num);
cli_puts(cli, " (");
cli_puts(cli, v->date);
cli_puts(cli, " ");
cli_puts(cli, v->time);
cli_puts(cli, ") git:");
cli_puts(cli, v->git_hash);
cli_puts(cli, "\r\n");
}
#endif
#if SYN_CLI_CMD_UPTIME
static void cli_builtin_uptime(const SYN_CLI *cli)
{
char num[12];
uint32_t ms = syn_port_get_tick_ms();
uint32_t secs = ms / 1000;
uint32_t mins = secs / 60;
uint32_t hours = mins / 60;
uint32_t days = hours / 24;
cli_puts(cli, "Uptime: ");
syn_fmt_uint(num, sizeof(num), days);
cli_puts(cli, num);
cli_puts(cli, "d ");
syn_fmt_uint(num, sizeof(num), hours % 24);
cli_puts(cli, num);
cli_puts(cli, "h ");
syn_fmt_uint(num, sizeof(num), mins % 60);
cli_puts(cli, num);
cli_puts(cli, "m ");
syn_fmt_uint(num, sizeof(num), secs % 60);
cli_puts(cli, num);
cli_puts(cli, "s (");
syn_fmt_uint(num, sizeof(num), ms);
cli_puts(cli, num);
cli_puts(cli, "ms)\r\n");
}
#endif
#if SYN_CLI_CMD_ERRORS
static void cli_builtin_errors(const SYN_CLI *cli)
{
if (s_cli_errlog == NULL) {
cli_puts(cli, "No error log configured. Call syn_cli_set_errlog().\r\n");
return;
}
char num[12];
size_t avail = syn_errlog_available(s_cli_errlog);
cli_puts(cli, "Error log: ");
syn_fmt_uint(num, sizeof(num), (uint32_t)syn_errlog_count(s_cli_errlog));
cli_puts(cli, num);
cli_puts(cli, " total, ");
syn_fmt_uint(num, sizeof(num), (uint32_t)avail);
cli_puts(cli, num);
cli_puts(cli, " stored\r\n");
if (avail == 0)
return;
static const char *const sev_names[] = {"INFO", "WARN", "ERR ", "FATL"};
SYN_ErrEntry e;
for (size_t i = 0; i < avail; i++) {
if (!syn_errlog_read(s_cli_errlog, i, &e))
continue;
cli_puts(cli, " [");
syn_fmt_uint(num, sizeof(num), e.timestamp);
cli_puts(cli, num);
cli_puts(cli, "] ");
const char *sn = (e.severity <= SYN_ERR_FATAL) ? sev_names[e.severity] : "????";
cli_puts(cli, sn);
cli_puts(cli, " code=0x");
syn_fmt_hex(num, sizeof(num), e.code, 4);
cli_puts(cli, num);
cli_puts(cli, " ctx=");
syn_fmt_uint(num, sizeof(num), e.context);
cli_puts(cli, num);
cli_puts(cli, " boot=");
syn_fmt_uint(num, sizeof(num), e.boot_count);
cli_puts(cli, num);
cli_puts(cli, "\r\n");
}
}
#endif
#if SYN_CLI_CMD_TASKS
static void cli_builtin_tasks(const SYN_CLI *cli)
{
if (s_cli_sched == NULL) {
cli_puts(cli, "No scheduler configured. Call syn_cli_set_scheduler().\r\n");
return;
}
char num[12];
cli_puts(cli, "Tasks:\r\n");
for (uint8_t i = 0; i < s_cli_sched->task_count; i++) {
const SYN_Task *t = &s_cli_sched->tasks[i];
cli_puts(cli, " ");
cli_puts(cli, t->name ? t->name : "(unnamed)");
cli_puts(cli, " prio=");
syn_fmt_uint(num, sizeof(num), t->priority);
cli_puts(cli, num);
static const char *const state_names[] = {" READY", " SUSPENDED", " DEAD",
" DEFERRED", " BLOCKED", " WAITING"};
const char *sn = (t->state <= 5) ? state_names[t->state] : " ???";
cli_puts(cli, sn);
cli_puts(cli, "\r\n");
}
}
#endif
#endif /* SYN_USE_CLI */