| //! @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; |
| } |