From d217852e1bf51beef5ccecd3eb8e11e74ce73683 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 12 Nov 2021 09:23:00 -0500 Subject: [PATCH 1/3] Added command history feature This commit adds a command history feature. It works similarly to other command shells, where the up and down arrow keys cycle through recently used commands. The user can make changes to a previous command's text before running it again. If the user has already entered part of a command and then scrolls through the history, the partial command is saved at the beginning of the history list.we This feature requires a significant amount of memory to be used for storing the command history list. As a rule of thumb, command history should only be enabled on systems with at least 8k of RAM (Arduino MEGA and better). It can be enabled on smaller systems, but only when (CONFIG_SHELL_MAX_INPUT * (CONFIG_SHELL_COMMAND_HISTORY + 1)) + 4 bytes of RAM are available. For a history list that is 10 commands long and the default input buffer length 0f 70, this would be 774 bytes. This much memory usage only makes sense on some systems, so this feature is disabled by default. To enable it, set the CONFIG_SHELL_COMMAND_HISTORY macro in Shell.h to the desired command history list length. The max history length is 255 entries. To implement this feature, the following changes were made: --A number of global variables were added (4 bytes), and count was moved from a static in shell_task to be a global as well --shellbuf is expanded into a two dimensional array, the first dimension is the list of historic commands, and the second dimension is the index within that command buffer. The command history list is implemented as a ring buffer. --Added shell_clear_command(), a public function to clear the current command buffer and displayed text in the shell --Basic processing of VT100 escape sequences, including Command Sequence Indroducer sequences, for receiving arrow keys. This may also lead to implementing the left and right arrow keys as a future project, allowing the user to move a cursor left and right when typing a command. The biggest hurdle to this right now is that editing the middle or beginning of a long command will lead to lots of inserting in the middle of a command buffer, which will be slow. --In addition to using count to track the length of an entered command, command text (including arguments) are now terminated by a NULL char --When command history is enabled, command text (including arguments) are now copied to a new buffer for parsing. This prevents the NULL string terminators between each argument from being added to the command history entry. --- Shell.c | 254 +++++++++++++++++++++++++++++++++++++++++++++++--------- Shell.h | 18 +++- 2 files changed, 233 insertions(+), 39 deletions(-) diff --git a/Shell.c b/Shell.c index 5286bd4..efb58af 100644 --- a/Shell.c +++ b/Shell.c @@ -33,9 +33,49 @@ struct shell_command_entry list[CONFIG_SHELL_MAX_COMMANDS]; char * argv_list[CONFIG_SHELL_MAX_COMMAND_ARGS]; /** - * This is the main buffer to store received characters from the user´s terminal + * This is the index of the current command buffer in the command history ring + * buffer */ -char shellbuf[CONFIG_SHELL_MAX_INPUT]; +uint8_t currentBuf; + +/** + * This is the index of the command last read from the command history ring + * buffer + */ +uint8_t historyBuf; + +/** + * This is the index of the oldest entry in the command history ring buffer + */ +uint8_t oldestBuf; + +/** + * This is a flag to indicate if the command history ring buffer has wrapped or + * not + */ +bool bufferWrapped; + +/** + * Number of characters written to buffer + */ +uint16_t count; + +#if CONFIG_SHELL_COMMAND_HISTORY != 1 +/** + * This is a buffer for storing a command that's been entered before scrolling + * through the command history (eg, if referencing a previous command but not + * executing it), and also for parsing command strings into arguments for + * processing + */ +char scratchpad[CONFIG_SHELL_MAX_INPUT]; +#endif + +/** + * This is the main buffer to store the command history, as well as received + * characters from the user´s terminal (in the element pointed to by + * currentBuf). This is implemented as a ring buffer + */ +char shellbuf[CONFIG_SHELL_COMMAND_HISTORY][CONFIG_SHELL_MAX_INPUT]; #ifdef ARDUINO /** @@ -85,6 +125,11 @@ static void shell_process_escape(int argc, char ** argv); */ static void shell_prompt(); +/** + * Clears the current command text + */ +static void shell_clear_command(); + /*-------------------------------------------------------------* * Public API Implementation * *-------------------------------------------------------------*/ @@ -95,6 +140,16 @@ bool shell_init(shell_reader_t reader, shell_writer_t writer, char * msg) shell_unregister_all(); + // Initialize command history buffer + for(uint8_t i = CONFIG_SHELL_COMMAND_HISTORY; i > 0; i--) { // For each element in the command history buffer + shellbuf[i-1][0] = '\0'; // Set first char of this entry to NULL, an empty string + } + currentBuf = 0; // Point to the first element in the ring buffer + historyBuf = currentBuf; // Initialize this to current buffer, since we haven't read from history yet + oldestBuf = 0; // Point to the first element in the ring buffer, since there's no input yet, this is the oldest + bufferWrapped = false; // The buffer should now be empty, so it hasn't wrapped yet + count = 0; // Initialize character count to 0 + shell_reader = reader; shell_writer = writer; initialized = true; @@ -283,8 +338,8 @@ void shell_print_error(int error, const char * field) void shell_task() { - // Number of characters written to buffer (this should be static var) - static uint16_t count = 0; + static bool escapeMode = false; + static bool csiMode = false; uint8_t i = 0; bool cc = 0; int retval = 0; @@ -307,46 +362,131 @@ void shell_task() // Process each one of the received characters if (shell_reader(&rxchar)) { + if (escapeMode) { + if (csiMode) { // Control Sequence Introducer mode + if ((rxchar >= 0x30) && (rxchar <= 0x3f)) { // Parameter bytes, ignore for now + + } else if ((rxchar >= 0x20) && (rxchar <= 0x2F)) { // Intermediate byte, ignore for now + + } else if ((rxchar >= 0x40) && (rxchar <= 0x7E)) { // Final byte, this is the actual command + switch (rxchar) { + case SHELL_VT100_ARROWUP: // Arrow up pressed + if (historyBuf != oldestBuf) { +#if CONFIG_SHELL_COMMAND_HISTORY != 1 + if (historyBuf == currentBuf) { // If we're leaving the newest command buffer + if (count > 0) { // If a partial command has been entered + strcpy(scratchpad,shellbuf[currentBuf]); // Save the currently entered command for later editing + } else { + scratchpad[0] = '\0'; // Make sure scratchpad is empty + } + } + shell_clear_command(); // Clear the current command buffer and displayed text + // Move historyBuf to point to the next oldest command + if (historyBuf == 0) { // Make sure to wrap ring buffer correctly + historyBuf = CONFIG_SHELL_COMMAND_HISTORY - 1; + } else { + historyBuf--; + } + shell_print(shellbuf[historyBuf]); // Print the historic command to the shell + strcpy(shellbuf[currentBuf],shellbuf[historyBuf]); // Copy the historic command into the current command buffer + count = strlen(shellbuf[currentBuf]); // Update the char counter +#endif + } else { + shell_putc(SHELL_ASCII_BEL); // Print a Bell to indicate we're at the end of the history + } + break; + case SHELL_VT100_ARROWDOWN: // Arrow down pressed + if (historyBuf != currentBuf) { +#if CONFIG_SHELL_COMMAND_HISTORY != 1 + shell_clear_command(); // Clear the current command buffer and displayed text + // Move historyBuf to point to the next newest command + if (historyBuf == CONFIG_SHELL_COMMAND_HISTORY -1) { // Make sure to wrap ring buffer correctly + historyBuf = 0; + } else { + historyBuf++; + } + if (historyBuf == currentBuf) { // We've reached the newest command buffer (not a history entry), so restore the saved command text from the scratchpad + if(strlen(scratchpad) > 0) { // If there's a partial command saved to the scratchpad + shell_print(scratchpad); // Print scratchpad text to the shell + strcpy(shellbuf[currentBuf],scratchpad); // Copy the scratchpad text into the current command buffer + } + } else { // We're still moving between history entries + shell_print(shellbuf[historyBuf]); // Print the historic command to the shell + strcpy(shellbuf[currentBuf],shellbuf[historyBuf]); // Copy the historic command into the current command buffer + } + count = strlen(shellbuf[currentBuf]); // Update the char counter +#endif + } else { + shell_putc(SHELL_ASCII_BEL); // Print a Bell to indicate we're at the end of the history + } + break; + default: + // Process other escape sequences: maybe later + break; + } + csiMode = false; // Exit Control Sequence Introducer mode + escapeMode = false; // Exit escape character mode + } + } else { // Handle other escape modes later + switch (rxchar) { + case SHELL_VT100_CSI: // Escape sequence is a Control Sequence Introduction + csiMode = true; // Enter Control Sequence Introducer mode on next char received + break; + default: // Process other escape modes later + escapeMode = false; + break; + } + } + } else { // Not in escape sequence mode + switch (rxchar) { + case SHELL_ASCII_ESC: // For VT100 escape sequences + escapeMode = true; + break; - switch (rxchar) { - case SHELL_ASCII_ESC: // For VT100 escape sequences - // Process escape sequences: maybe later - break; - - case SHELL_ASCII_DEL: - shell_putc(SHELL_ASCII_BEL); - break; + case SHELL_ASCII_DEL: + shell_putc(SHELL_ASCII_BEL); + break; - case SHELL_ASCII_HT: - shell_putc(SHELL_ASCII_BEL); - break; + case SHELL_ASCII_HT: + shell_putc(SHELL_ASCII_BEL); + break; - case SHELL_ASCII_CR: // Enter key pressed - shellbuf[count] = '\0'; - shell_println(""); - cc = true; - break; + case SHELL_ASCII_CR: // Enter key pressed + shellbuf[currentBuf][count] = '\0'; + shell_println(""); + cc = true; + break; - case SHELL_ASCII_BS: // Backspace pressed - if (count > 0) { - count--; - shell_putc(SHELL_ASCII_BS); - shell_putc(SHELL_ASCII_SP); - shell_putc(SHELL_ASCII_BS); - } else - shell_putc(SHELL_ASCII_BEL); - break; - default: - // Process printable characters, but ignore other ASCII chars - if (count < (CONFIG_SHELL_MAX_INPUT - 1) && rxchar >= 0x20 && rxchar < 0x7F) { - shellbuf[count] = rxchar; - shell_putc(rxchar); - count++; + case SHELL_ASCII_BS: // Backspace pressed + if (count > 0) { + shellbuf[currentBuf][count] = '\0'; // Set char to NULL so we don't have to worry about this text next time through the ring buffer + count--; + shell_putc(SHELL_ASCII_BS); + shell_putc(SHELL_ASCII_SP); + shell_putc(SHELL_ASCII_BS); + } else + shell_putc(SHELL_ASCII_BEL); + break; + default: + // Process printable characters, but ignore other ASCII chars + if (count < (CONFIG_SHELL_MAX_INPUT - 1) && rxchar >= 0x20 && rxchar < 0x7F) { + shellbuf[currentBuf][count] = rxchar; + if (count < CONFIG_SHELL_MAX_INPUT - 2) { // If we aren't at the end of the input buffer + shellbuf[currentBuf][count + 1] = '\0'; // Set next char to NULL to denote the end of the string in case there was old historic command text left here + } + shell_putc(rxchar); + count++; + } } } // Check if a full command is available on the buffer to process if (cc) { - argc = shell_parse(shellbuf, argv_list, CONFIG_SHELL_MAX_COMMAND_ARGS); +#if CONFIG_SHELL_COMMAND_HISTORY == 1 + argc = shell_parse(shellbuf[currentBuf], argv_list, CONFIG_SHELL_MAX_COMMAND_ARGS); +#else + strcpy(scratchpad,shellbuf[currentBuf]); // Copy current command buffer to scratchpad so we don't fill command history with NULLs between each argument + argc = shell_parse(scratchpad, argv_list, CONFIG_SHELL_MAX_COMMAND_ARGS); +#endif // Process escape sequences before giving args to command implementation shell_process_escape(argc, argv_list); // sequential search on command table @@ -373,8 +513,35 @@ void shell_task() shell_println((const char *) "Command NOT found."); // Print not found!! #endif } + + if (count != 0) { // If we didn't get an empty command + if ( + (currentBuf == oldestBuf) || // If the history buffer is empty, ie we're on the first command, or... + (strcmp(shellbuf[currentBuf],shellbuf[currentBuf == 0 ? CONFIG_SHELL_COMMAND_HISTORY - 1 : currentBuf - 1]) != 0) // This command is not the same as the previous command + ) { + // Increment command history ring buffer indices + if (currentBuf == CONFIG_SHELL_COMMAND_HISTORY - 1) { + currentBuf = 0; + bufferWrapped = true; + } else { + currentBuf++; + } + if (bufferWrapped) { // If the history buffer is full, and it's time to start moving the oldest buffer entry index + if (oldestBuf == CONFIG_SHELL_COMMAND_HISTORY - 1) { + oldestBuf = 0; + } else { + oldestBuf++; + } + } + } + } + + // Clear flags and counters + historyBuf = currentBuf; // Update historyBuf so it's ready to move to the most recent history entry count = 0; cc = false; + + // Print a new prompt shell_println(""); shell_prompt(); } @@ -418,7 +585,7 @@ static int shell_parse(char * buf, char ** argv, unsigned short maxargs) { int i = 0; int argc = 0; - int length = strlen(buf) + 1; //String lenght to parse = strlen + 1 + int length = strlen(buf) + 1; //String length to parse = strlen + 1 char toggle = 0; bool escape = false; @@ -511,6 +678,19 @@ static void shell_prompt() #endif } +static void shell_clear_command() +{ + while (count > 0) { // While there are chars left in the current command buffer + // Clear out the displayed text (just like a backspace) + shell_putc(SHELL_ASCII_BS); + shell_putc(SHELL_ASCII_SP); + shell_putc(SHELL_ASCII_BS); + + shellbuf[currentBuf][count - 1] = '\0'; // Set the char to NULL + count--; // Decrement char counter + } +} + /*-------------------------------------------------------------* * Shell formatted print support * *-------------------------------------------------------------*/ diff --git a/Shell.h b/Shell.h index 1f1980f..f4b34bd 100644 --- a/Shell.h +++ b/Shell.h @@ -71,6 +71,19 @@ #define CONFIG_SHELL_FMT_BUFFER 70 #endif +/** + * This macro sets the size of the command history list (or disables the feature + * when set to 1). This feature is disabled by default, and should only be + * enabled on systems with enough RAM to support it. As a rule of thumb, + * command history should only be enabled on systems with at least 8k of RAM + * (Arduino MEGA and better). It can be enabled on smaller systems, but only + * when (CONFIG_SHELL_MAX_INPUT * (CONFIG_SHELL_COMMAND_HISTORY + 1)) + 4 bytes + * of RAM are available. For a history list that is 10 commands long and the + * default input buffer length 0f 70, this would be 774 bytes. The max history + * length is 255 entries. + */ +#define CONFIG_SHELL_COMMAND_HISTORY 1 + /** * End of user configurable parameters, do not touch anything below this line */ @@ -88,6 +101,7 @@ #define SHELL_ASCII_DEL 0x7F #define SHELL_ASCII_US 0x1F #define SHELL_ASCII_SP 0x20 +#define SHELL_VT100_CSI 0x5B #define SHELL_VT100_ARROWUP 'A' #define SHELL_VT100_ARROWDOWN 'B' #define SHELL_VT100_ARROWRIGHT 'C' @@ -269,7 +283,7 @@ extern "C" { * @param fmt The string to send to the terminal, the string can include format * specifiers in a similar fashion to printf standard function. * - * @param ... Aditional arguments that are inserted on the string as text + * @param ... Additional arguments that are inserted on the string as text */ void shell_printf(const char * fmt, ...); @@ -342,7 +356,7 @@ extern "C" { * @param fmt The string to send to the terminal, the string can include format * specifiers in a similar fashion to printf standard function. * - * @param ... Aditional arguments that are inserted on the string as text + * @param ... Additional arguments that are inserted on the string as text */ void shell_printf_pm(const char * fmt, ...); #endif From ec93d258add6e2f72d9b085e74bec146dbe49056 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 12 Nov 2021 13:00:04 -0500 Subject: [PATCH 2/3] Update Telnet example to send telnet options and fix typo in Shell.h Adds Dont Linemode and Will Echo Telnet options to the Telnet example. These are needed for cleanly sending arrow keys. NOTE: Putty doesn't respect Dont Linemode, and requires that Terminal->Local Line Editing be forced off. Also fixed a typo in Shell.h --- Shell.h | 2 +- examples/Shell_Telnet/Shell_Telnet.ino | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Shell.h b/Shell.h index f4b34bd..526a5c0 100644 --- a/Shell.h +++ b/Shell.h @@ -79,7 +79,7 @@ * (Arduino MEGA and better). It can be enabled on smaller systems, but only * when (CONFIG_SHELL_MAX_INPUT * (CONFIG_SHELL_COMMAND_HISTORY + 1)) + 4 bytes * of RAM are available. For a history list that is 10 commands long and the - * default input buffer length 0f 70, this would be 774 bytes. The max history + * default input buffer length of 70, this would be 774 bytes. The max history * length is 255 entries. */ #define CONFIG_SHELL_COMMAND_HISTORY 1 diff --git a/examples/Shell_Telnet/Shell_Telnet.ino b/examples/Shell_Telnet/Shell_Telnet.ino index 66f684e..0325db0 100644 --- a/examples/Shell_Telnet/Shell_Telnet.ino +++ b/examples/Shell_Telnet/Shell_Telnet.ino @@ -71,6 +71,11 @@ void loop() { // Check if a client is willing to connect and get client object client = server.available(); + char telnetCommands[] = { + 0xFF, 0xFE, 0x22, // Dont Linemode + 0xFF, 0xFB, 0x01, // Will Echo + }; + client.write(telnetCommands,sizeof(telnetCommands)); // This should always be called to process user input shell_task(); } @@ -140,5 +145,3 @@ void shell_writer(char data) client.write(data); } } - - From a88129b36b702fc3f31b3fca4494a6db81a20c20 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 15 Nov 2021 16:59:53 -0500 Subject: [PATCH 3/3] set Telnet modes in correct way --- examples/Shell_Telnet/Shell_Telnet.ino | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/Shell_Telnet/Shell_Telnet.ino b/examples/Shell_Telnet/Shell_Telnet.ino index 0325db0..7e63736 100644 --- a/examples/Shell_Telnet/Shell_Telnet.ino +++ b/examples/Shell_Telnet/Shell_Telnet.ino @@ -35,6 +35,15 @@ byte gateway[] = { byte subnet[] = { 255, 255, 255, 0 }; +//Telnet Macros +#define TELNET_IAC 0xFF +#define TELNET_DONT 0xFE +#define TELNET_DO 0xFD +#define TELNET_WONT 0xFC +#define TELNET_WILL 0xFB +#define TELNET_OPT_ECHO 0x01 +#define TELNET_SUPPRESS_GA 0x03 + // Create the server instance on port 23 (default port for telnet protocol) EthernetServer server = EthernetServer(23); // The client that is willing to connect to server @@ -72,9 +81,10 @@ void loop() // Check if a client is willing to connect and get client object client = server.available(); char telnetCommands[] = { - 0xFF, 0xFE, 0x22, // Dont Linemode - 0xFF, 0xFB, 0x01, // Will Echo - }; + TELNET_IAC, TELNET_WILL, TELNET_OPT_ECHO, + TELNET_IAC, TELNET_DONT, TELNET_OPT_ECHO, + TELNET_IAC, TELNET_WILL, TELNET_SUPPRESS_GA + }; client.write(telnetCommands,sizeof(telnetCommands)); // This should always be called to process user input shell_task();