blob: 8c9e2abc7041ffd12f2c86d18470f14e7b26feb7 [file] [log] [blame]
//! @file
//!
//! Copyright (c) Memfault, Inc.
//! See LICENSE for details
//!
//! @brief
//! Minimal shell/console implementation for platforms that do not include one.
//! NOTE: For simplicity, ANSI escape sequences are not dealt with!
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include "memfault/config.h"
#include "memfault/core/compiler.h"
#include "memfault/demo/shell.h"
#include "memfault/demo/shell_commands.h"
#define MEMFAULT_SHELL_MAX_ARGS (16)
#define MEMFAULT_SHELL_PROMPT "mflt> "
#if defined(MEMFAULT_DEMO_SHELL_COMMAND_EXTENSIONS)
// When the extension list is enabled, iterate over both the core commands and
// the extension commands. This construct, despite being pretty intricate,
// saves about ~28 bytes of code space over running the iteration twice in a
// row, and keeps the iterator in one macro, instead of two.
#define MEMFAULT_SHELL_FOR_EACH_COMMAND(command) \
const sMemfaultShellCommand *command = g_memfault_shell_commands; \
for (size_t i = 0; i < g_memfault_num_shell_commands + s_mflt_shell.num_extension_commands; \
++i, command = (i < g_memfault_num_shell_commands) ? \
&g_memfault_shell_commands[i] : \
(s_mflt_shell.extension_commands ? \
&s_mflt_shell.extension_commands[i - g_memfault_num_shell_commands] : \
NULL))
#else
#define MEMFAULT_SHELL_FOR_EACH_COMMAND(command) \
for (const sMemfaultShellCommand *command = g_memfault_shell_commands; \
command < &g_memfault_shell_commands[g_memfault_num_shell_commands]; ++command)
#endif
static struct MemfaultShellContext {
int (*send_char)(char c);
size_t rx_size;
// the char we will ignore when received end-of-line sequences
char eol_ignore_char;
char rx_buffer[MEMFAULT_DEMO_SHELL_RX_BUFFER_SIZE];
#if defined(MEMFAULT_DEMO_SHELL_COMMAND_EXTENSIONS)
const sMemfaultShellCommand *extension_commands;
size_t num_extension_commands;
#endif
} s_mflt_shell;
static bool prv_booted(void) {
return s_mflt_shell.send_char != NULL;
}
static void prv_send_char(char c) {
if (!prv_booted()) {
return;
}
s_mflt_shell.send_char(c);
}
static void prv_echo(char c) {
if (c == '\n') {
prv_send_char('\r');
prv_send_char('\n');
} else if ('\b' == c) {
prv_send_char('\b');
prv_send_char(' ');
prv_send_char('\b');
} else {
prv_send_char(c);
}
}
static char prv_last_char(void) {
return s_mflt_shell.rx_buffer[s_mflt_shell.rx_size - 1];
}
static bool prv_is_rx_buffer_full(void) {
return s_mflt_shell.rx_size >= MEMFAULT_DEMO_SHELL_RX_BUFFER_SIZE;
}
static void prv_reset_rx_buffer(void) {
memset(s_mflt_shell.rx_buffer, 0, sizeof(s_mflt_shell.rx_buffer));
s_mflt_shell.rx_size = 0;
}
static void prv_echo_str(const char *str) {
for (const char *c = str; *c != '\0'; ++c) {
prv_echo(*c);
}
}
static void prv_send_prompt(void) {
prv_echo_str(MEMFAULT_SHELL_PROMPT);
}
static const sMemfaultShellCommand *prv_find_command(const char *name) {
MEMFAULT_SHELL_FOR_EACH_COMMAND (command) {
if (strcmp(command->command, name) == 0) {
return command;
}
}
return NULL;
}
static void prv_process(void) {
if (prv_last_char() != '\n' && !prv_is_rx_buffer_full()) {
return;
}
char *argv[MEMFAULT_SHELL_MAX_ARGS] = { 0 };
int argc = 0;
char *next_arg = NULL;
for (size_t i = 0; i < s_mflt_shell.rx_size && argc < MEMFAULT_SHELL_MAX_ARGS; ++i) {
char *const c = &s_mflt_shell.rx_buffer[i];
if (*c == ' ' || *c == '\n' || i == s_mflt_shell.rx_size - 1) {
*c = '\0';
if (next_arg) {
argv[argc++] = next_arg;
next_arg = NULL;
}
} else if (!next_arg) {
next_arg = c;
}
}
if (s_mflt_shell.rx_size == MEMFAULT_DEMO_SHELL_RX_BUFFER_SIZE) {
prv_echo('\n');
}
if (argc >= 1) {
const sMemfaultShellCommand *command = prv_find_command(argv[0]);
if (!command) {
prv_echo_str("Unknown command: ");
prv_echo_str(argv[0]);
prv_echo('\n');
prv_echo_str("Type 'help' to list all commands\n");
} else {
command->handler(argc, argv);
}
}
prv_reset_rx_buffer();
prv_send_prompt();
}
void memfault_demo_shell_boot(const sMemfaultShellImpl *impl) {
s_mflt_shell.eol_ignore_char = 0;
s_mflt_shell.send_char = impl->send_char;
prv_reset_rx_buffer();
prv_echo_str("\n" MEMFAULT_SHELL_PROMPT);
}
#if defined(MEMFAULT_DEMO_SHELL_COMMAND_EXTENSIONS)
void memfault_shell_command_set_extensions(const sMemfaultShellCommand *const commands,
size_t num_commands) {
s_mflt_shell.extension_commands = commands;
s_mflt_shell.num_extension_commands = num_commands;
}
#endif
//! Logic to deal with CR, LF, CRLF, or LFCR end-of-line (EOL) sequences
//! @return true if the character should be ignored, false otherwise
static bool prv_should_ignore_eol_char(char c) {
if (s_mflt_shell.eol_ignore_char != 0) {
return (c == s_mflt_shell.eol_ignore_char);
}
//
// Check to see if we have encountered our first newline character since the shell was booted
// (either a CR ('\r') or LF ('\n')). Once found, we will use this character as our EOL delimiter
// and ignore the opposite character if we see it in the future.
//
if (c == '\r') {
s_mflt_shell.eol_ignore_char = '\n';
} else if (c == '\n') {
s_mflt_shell.eol_ignore_char = '\r';
}
return false;
}
void memfault_demo_shell_receive_char(char c) {
if (prv_should_ignore_eol_char(c) || prv_is_rx_buffer_full() || !prv_booted()) {
return;
}
const bool is_backspace = (c == '\b');
if (is_backspace && s_mflt_shell.rx_size == 0) {
return; // nothing left to delete so don't echo the backspace
}
// CR are our EOL delimiter. Remap as a LF here since that's what internal handling logic expects
if (c == '\r') {
c = '\n';
}
prv_echo(c);
if (is_backspace) {
s_mflt_shell.rx_buffer[--s_mflt_shell.rx_size] = '\0';
return;
}
s_mflt_shell.rx_buffer[s_mflt_shell.rx_size++] = c;
prv_process();
}
int memfault_shell_help_handler(MEMFAULT_UNUSED int argc, MEMFAULT_UNUSED char *argv[]) {
MEMFAULT_SHELL_FOR_EACH_COMMAND (command) {
prv_echo_str(command->command);
prv_echo_str(": ");
prv_echo_str(command->help);
prv_echo('\n');
}
return 0;
}