diff --git a/doc/Doxyfile b/doc/Doxyfile index 759a99d9..35844dae 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -666,6 +666,7 @@ INPUT = ../libraries/LCD \ ../libraries/Crypto \ ../libraries/RingOscillatorNoiseSource \ ../libraries/TransistorNoiseSource \ + ../libraries/Terminal \ . # This tag can be used to specify the character encoding of the source files diff --git a/gen/genkeymap.cpp b/gen/genkeymap.cpp new file mode 100644 index 00000000..49ddb1e1 --- /dev/null +++ b/gen/genkeymap.cpp @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2016 Southern Storm Software, Pty Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +// Generates the keymap state machine table for the Terminal class. + +#include +#include +#include +#include "../libraries/Terminal/USBKeysExtra.h" + +typedef struct { + const char *sequence; + int code; +} EscKey; +static EscKey const escKeys[] = { + // Based on the key sequence tables from: + // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys + // http://aperiodic.net/phil/archives/Geekery/term-function-keys.html + // Also some miscellaneous codes from other sources like the Linux console. + + // Cursor control keys. + {"[A", KEY_UP_ARROW}, + {"OA", KEY_UP_ARROW}, + {"A", KEY_UP_ARROW}, + {"[B", KEY_DOWN_ARROW}, + {"OB", KEY_DOWN_ARROW}, + {"B", KEY_DOWN_ARROW}, + {"[C", KEY_RIGHT_ARROW}, + {"OC", KEY_RIGHT_ARROW}, + {"C", KEY_RIGHT_ARROW}, + {"[D", KEY_LEFT_ARROW}, + {"OD", KEY_LEFT_ARROW}, + {"D", KEY_LEFT_ARROW}, + {"[H", KEY_HOME}, + {"OH", KEY_HOME}, + {"[1~", KEY_HOME}, + {"[F", KEY_END}, + {"OF", KEY_END}, + {"[4~", KEY_END}, + {"[2~", KEY_INSERT}, + {"[3~", KEY_DELETE}, + {"[5~", KEY_PAGE_UP}, + {"[6~", KEY_PAGE_DOWN}, + + // Numeric keypad. Mostly mapped back to ASCII. + {"O ", ' '}, + {"? ", ' '}, + {"OI", KEY_TAB}, + {"?I", KEY_TAB}, + {"OM", KEY_RETURN}, + {"?M", KEY_RETURN}, + {"Oj", '*'}, + {"?j", '*'}, + {"Ok", '+'}, + {"?k", '+'}, + {"Ol", ','}, + {"?l", ','}, + {"Om", '-'}, + {"?m", '-'}, + {"On", '.'}, + {"?n", '.'}, + {"Oo", '/'}, + {"?o", '/'}, + {"Op", '0'}, + {"?p", '0'}, + {"Oq", '1'}, + {"?q", '1'}, + {"Or", '2'}, + {"?r", '2'}, + {"Os", '3'}, + {"?s", '3'}, + {"Ot", '4'}, + {"?t", '4'}, + {"Ou", '5'}, + {"?u", '5'}, + {"Ov", '6'}, + {"?v", '6'}, + {"Ow", '7'}, + {"?w", '7'}, + {"Ox", '8'}, + {"?x", '8'}, + {"Oy", '9'}, + {"?y", '9'}, + {"OX", '='}, + {"?X", '='}, + + // Function keys. + {"[11~", KEY_F1}, + {"P", KEY_F1}, + {"OP", KEY_F1}, + {"[[A", KEY_F1}, + {"[12~", KEY_F2}, + {"Q", KEY_F2}, + {"OQ", KEY_F2}, + {"[[B", KEY_F2}, + {"[13~", KEY_F3}, + {"R", KEY_F3}, + {"OR", KEY_F3}, + {"[[C", KEY_F3}, + {"[14~", KEY_F4}, + {"S", KEY_F4}, + {"OS", KEY_F4}, + {"[[D", KEY_F4}, + {"[15~", KEY_F5}, + {"[[E", KEY_F5}, + {"[17~", KEY_F6}, + {"[18~", KEY_F7}, + {"[19~", KEY_F8}, + {"[20~", KEY_F9}, + {"[21~", KEY_F10}, + {"[23~", KEY_F11}, + {"[24~", KEY_F12}, + {"[25~", KEY_F13}, + {"[11;2~", KEY_F13}, + {"O2P", KEY_F13}, + {"[26~", KEY_F14}, + {"[12;2~", KEY_F14}, + {"O2Q", KEY_F14}, + {"[28~", KEY_F15}, + {"[13;2~", KEY_F15}, + {"O2R", KEY_F15}, + {"[29~", KEY_F16}, + {"[14;2~", KEY_F16}, + {"O2S", KEY_F16}, + {"[31~", KEY_F17}, + {"[15;2~", KEY_F17}, + {"[32~", KEY_F18}, + {"[17;2~", KEY_F18}, + {"[33~", KEY_F19}, + {"[18;2~", KEY_F19}, + {"[34~", KEY_F20}, + {"[19;2~", KEY_F20}, + {"[20;2~", KEY_F21}, + {"[23$", KEY_F21}, + {"[21;2~", KEY_F22}, + {"[24$", KEY_F22}, + {"[23;2~", KEY_F23}, + {"[11^", KEY_F23}, + {"[24;2~", KEY_F24}, + {"[12^", KEY_F24}, + + // Other keys. + {"[Z", KEY_BACK_TAB}, + {"OZ", KEY_BACK_TAB}, + {"[P", KEY_PAUSE}, + {"[G", KEY_NUMPAD_5}, +}; +#define numEscKeys (sizeof(escKeys) / sizeof(escKeys[0])) + +class Node +{ +public: + explicit Node(Node *parent = 0); + ~Node(); + + void add(const char *str, int code); + void dumpRules(std::vector *vec); + + int ch; + int code; + int offset; + Node *firstChild; + Node *lastChild; + Node *nextChild; +}; + +Node::Node(Node *parent) + : ch(0) + , code(-1) + , offset(0) + , firstChild(0) + , lastChild(0) + , nextChild(0) +{ + if (parent) { + if (parent->lastChild) + parent->lastChild->nextChild = this; + else + parent->firstChild = this; + parent->lastChild = this; + } +} + +Node::~Node() +{ + Node *child = firstChild; + Node *next; + while (child != 0) { + next = child->nextChild; + delete child; + child = next; + } +} + +void Node::add(const char *str, int code) +{ + int ch = str[0] & 0xFF; + Node *child = firstChild; + while (child != 0) { + if (child->ch == ch) + break; + child = child->nextChild; + } + if (!child) { + child = new Node(this); + child->ch = ch; + } + if (str[1] == '\0') { + // Leaf node at the end of a string. + child->code = code; + } else { + // Interior node with more children. + child->add(str + 1, code); + } +} + +void Node::dumpRules(std::vector *vec) +{ + Node *child; + + // First pass: Output the recognizers for this level. + offset = vec->size(); + child = firstChild; + while (child != 0) { + if (child->firstChild) { + // Interior nodes need 3 bytes for the character and offset. + vec->push_back((uint8_t)(child->ch | 0x80)); + vec->push_back(0); + vec->push_back(0); + } else { + // Leaf nodes need 2 bytes for the character and code. + vec->push_back((uint8_t)(child->ch)); + vec->push_back((uint8_t)(child->code)); + if (child->code > 255) + printf("Code 0x%X exceeds 255 - need to change table format\n", child->code); + } + child = child->nextChild; + } + vec->push_back(0); // Terminate this level. + + // Second pass: Output the recognizers for the child levels. + child = firstChild; + while (child != 0) { + if (child->firstChild) + child->dumpRules(vec); + child = child->nextChild; + } + + // Third pass: Back-patch the links to the child recognizers. + int posn = offset; + child = firstChild; + while (child != 0) { + if (child->firstChild) { + int value = child->offset; + (*vec)[posn + 1] = (uint8_t)value; + (*vec)[posn + 2] = (uint8_t)(value >> 8); + posn += 3; + } else { + posn += 2; + } + child = child->nextChild; + } +} + +int main(int argc, char *argv[]) +{ + Node *root = new Node(); + for (unsigned index = 0; index < numEscKeys; ++index) + root->add(escKeys[index].sequence, escKeys[index].code); + std::vector vec; + root->dumpRules(&vec); + printf("static uint8_t const keymap[%d] PROGMEM = {\n", (int)vec.size()); + for (unsigned index = 0; index < vec.size(); ++index) { + if ((index % 12) == 0) + printf(" "); + printf("0x%02X", (int)(vec[index])); + if ((index % 12) == 11) + printf(",\n"); + else + printf(", "); + } + printf("};\n"); + delete root; + return 0; +} diff --git a/gen/genwcwidth.c b/gen/genwcwidth.c new file mode 100644 index 00000000..6dee23ca --- /dev/null +++ b/gen/genwcwidth.c @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2016 Southern Storm Software, Pty Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +// Generates the Terminal::isWideCharacter() function from the data at: +// http://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt +// http://www.unicode.org/reports/tr11/ + +#include +#include +#include + +#define MAX_UNICODE 0x10FFFF +#define NUM_UNICODE (MAX_UNICODE + 1) + +static unsigned char *masks; + +static void mark_range(long code1, long code2) +{ + while (code1 <= code2) { + if (code1 > MAX_UNICODE) + break; + masks[code1 / 8] |= (1 << (code1 % 8)); + ++code1; + } +} + +static void unmark_range(long code1, long code2) +{ + while (code1 <= code2) { + if (code1 > MAX_UNICODE) + break; + masks[code1 / 8] &= ~(1 << (code1 % 8)); + ++code1; + } +} + +static void dump_ranges(void) +{ + long code; + int index, sum; + unsigned char *prevptr = 0; + unsigned char *ptr; + int dotdot = 0; + for (code = 0; code <= MAX_UNICODE; code += 0x100) { + ptr = masks + (code / 8); + sum = 0; + for (index = 0; index < 32; ++index) + sum += ptr[index]; + if (sum == 0 || sum == (0xFF * 32)) { + if (prevptr && !memcmp(ptr, prevptr, 32)) { + if (!dotdot) { + dotdot = 1; + printf("..\n"); + } + continue; + } + } + dotdot = 0; + printf("%06lX: ", code); + for (index = 0; index < 32; ++index) + printf("%02X", ptr[index]); + printf("\n"); + prevptr = ptr; + } + printf("\n"); +} + +static void print_lookup_table(const char *name, long first, long last) +{ + long index, size; + unsigned char *ptr = masks + first / 8; + size = (last - first + 1) / 8; + printf(" static unsigned char const %s[%ld] PROGMEM = {\n", name, size); + for (index = 0; index < size; ++index) { + if ((index % 8) == 0) + printf(" "); + printf("0x%02X", ptr[index]); + if (index < (size - 1)) { + if ((index % 8) == 7) + printf(",\n"); + else + printf(", "); + } else { + printf("\n"); + } + } + printf(" };\n"); +} + +static void recognizer(void) +{ + long code; + int first = 1; + + printf("bool Terminal::isWideCharacter(long code)\n{\n"); + printf(" // This function was automatically generated by genwcwidth.c\n"); + print_lookup_table("range3000", 0x3000, 0x30FF); + print_lookup_table("rangeFE00", 0xFE00, 0xFFFF); + printf(" unsigned c;\n"); + + // Bail out early for Latin character sets. + printf(" if (code < 0x2300) {\n"); + printf(" return false;\n"); + + // Densely populated ranges. + printf(" } else if (code >= 0x3000 && code <= 0x30FF) {\n"); + printf(" c = (unsigned)(code - 0x3000);\n"); + printf(" return (pgm_read_byte(range3000 + (c / 8)) & (1 << (c %% 8))) != 0;\n"); + printf(" } else if (code >= 0xFE00 && code <= 0xFFFF) {\n"); + printf(" c = (unsigned)(code - 0xFE00);\n"); + printf(" return (pgm_read_byte(rangeFE00 + (c / 8)) & (1 << (c %% 8))) != 0;\n"); + + // Deal with the main wide character ranges. + printf(" } else if (code >= 0x3400 && code <= 0x4DBF) {\n"); + printf(" return true;\n"); + printf(" } else if (code >= 0x4E00 && code <= 0x9FFF) {\n"); + printf(" return true;\n"); + printf(" } else if (code >= 0xF900 && code <= 0xFAFF) {\n"); + printf(" return true;\n"); + printf(" } else if (code >= 0x20000 && code <= 0x2FFFD) {\n"); + printf(" return true;\n"); + printf(" } else if (code >= 0x30000 && code <= 0x3FFFD) {\n"); + printf(" return true;\n"); + printf(" } else if ("); + + // Deal with the left-overs. + unmark_range(0x3000, 0x30FF); + unmark_range(0xFE00, 0xFFFF); + for (code = 0; code <= MAX_UNICODE; ++code) { + if (masks[code / 8] & (1 << (code % 8))) { + if (!first) + printf(" ||\n "); + else + first = 0; + printf("code == 0x%04lX", code); + } + } + printf(") {\n"); + printf(" return true;\n"); + printf(" }\n"); + + printf(" return false;\n"); + printf("}\n"); +} + +int main(int argc, char *argv[]) +{ + FILE *file; + char buffer[BUFSIZ]; + + // Allocate memory for the "is this a wide character?" mask array. + masks = calloc(NUM_UNICODE / 8, sizeof(unsigned char)); + if (!masks) { + fprintf(stderr, "out of memory\n"); + return 1; + } + + // Load the contents of "EastAsianWidth.txt". + if (argc < 2) { + fprintf(stderr, "Usage: %s EastAsianWidth.txt\n", argv[0]); + return 1; + } + if ((file = fopen(argv[1], "r")) == NULL) { + perror(argv[1]); + return 1; + } + while (fgets(buffer, sizeof(buffer), file)) { + if ((buffer[0] >= '0' && buffer[0] <= '9') || + (buffer[0] >= 'A' && buffer[0] <= 'F')) { + long code1 = 0; + long code2 = 0; + char *endptr = NULL; + code1 = strtol(buffer, &endptr, 16); + if (endptr[0] == '.' && endptr[1] == '.') { + endptr += 2; + code2 = strtol(buffer, &endptr, 16); + } else { + code2 = code1; + } + if (endptr[0] == ';') { + // Recognise 'W' and 'F' as wide characters. It is possible + // that 'A' (ambiguous) characters may also be wide but only + // in East Asian contexts, which we assume we're not for now. + if (endptr[1] == 'W' || endptr[1] == 'F') { + mark_range(code1, code2); + } + } + } + } + fclose(file); + + // Some special ranges that are implicitly all-wide even if the + // code points aren't currently allocated by the Unicode standard. + mark_range(0x3400, 0x4DBF); + mark_range(0x4E00, 0x9FFF); + mark_range(0xF900, 0xFAFF); + mark_range(0x20000, 0x2FFFD); + mark_range(0x30000, 0x3FFFD); + + // Dump the ranges. + dump_ranges(); + + // Unmark the special ranges to make it easier to find the left-overs. + unmark_range(0x3400, 0x4DBF); + unmark_range(0x4E00, 0x9FFF); + unmark_range(0xF900, 0xFAFF); + unmark_range(0x20000, 0x2FFFD); + unmark_range(0x30000, 0x3FFFD); + + // Create the recognition tree. + recognizer(); + + // Clean up and exit. + free(masks); + return 0; +} diff --git a/libraries/Terminal/Shell.cpp b/libraries/Terminal/Shell.cpp new file mode 100644 index 00000000..872c0ac4 --- /dev/null +++ b/libraries/Terminal/Shell.cpp @@ -0,0 +1,675 @@ +/* + * Copyright (C) 2016 Southern Storm Software, Pty Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "Shell.h" +#include +#include + +/** + * \class Shell Shell.h + * \brief Command-line shell access. + * + */ + +/** + * \def SHELL_MAX_CMD_LEN + * \brief Maximum command length for the shell, including the terminating NUL. + */ + +/** + * \typedef ShellCommandFunc + * \brief Type of functions that provide shell command handlers. + * + * \param shell Points to the shell instance that executed the command, + * which can be used to print command results or read more input. + * \param argc Number of arguments to the command, including the + * command's name. + * \param argv The arguments to the command. + * + * \sa ShellCommand() + * \relates Shell + */ + +/** + * \typedef ShellPasswordCheckFunc + * \brief Password checking function for login shells. + * + * \param userid Points to the user identifier that was supplied at login. + * \param password Points to the password that was supplied at login. + * + * \return Returns true if the user identifier and password combination + * is correct, false if incorrect. + * + * Timing can be very important: the check should take the same amount of + * time for valid and invalid user identifiers or passwords so that an + * attacker cannot gain knowledge about the valid users on the system + * based on failed login attempts. + * + * \relates Shell + */ + +/** + * \brief Constructs a new Shell instance. + * + * This constructor must be followed by a call to begin() to specify + * the underlying I/O stream. + */ +Shell::Shell() + : maxHistory(0) + , curLen(0) + , history(0) + , historyWrite(0) + , historyPosn(0) + , prom("> ") + , hideChars(false) +{ +} + +/** + * \brief Destroys this Shell object. + */ +Shell::~Shell() +{ + delete [] history; +} + +/** + * \brief Begin shell handling on an underlying character stream. + * + * \param stream The stream to apply the shell to. Usually this is a + * serial port or TCP network connection. + * \param maxHistory The number of commands to allocate in the history + * stack for scrolling back through using Up/Down arrow keys. + * \return Returns true if the shell was initialized, or false if there + * is insufficient memory for the history stack. + * + * This function will print the prompt() in preparation for entry of + * the first command. The default prompt is "> "; call setPrompt() + * before begin() to change this: + * + * \code + * Serial.begin(9600); + * shell.setPrompt("Command: "); + * shell.begin(Serial); + * \endcode + * + * \sa end(), setPrompt() + */ +bool Shell::begin(Stream &stream, size_t maxHistory) +{ + // Initialize the Terminal base class with the underlying stream. + Terminal::begin(stream); + + // Create the history buffer. + this->maxHistory = maxHistory; + delete [] history; + if (maxHistory) { + history = new char [sizeof(buffer) * maxHistory]; + if (!history) { + Terminal::end(); + return false; + } + memset(history, 0, sizeof(buffer) * maxHistory); + } else { + history = 0; + } + + // Clear other variables. + curLen = 0; + historyWrite = 0; + historyPosn = 0; + hideChars = false; + + // Print the initial prompt. + if (prom) + print(prom); + return true; +} + +/** + * \brief Ends shell processing on the underlying stream. + * + * This function is intended to be called when a TCP network connection + * is closed to clean up the shell state that was in use by the connection. + * + * \sa begin() + */ +void Shell::end() +{ + Terminal::end(); + delete [] history; + maxHistory = 0; + curLen = 0; + history = 0; + historyWrite = 0; + historyPosn = 0; + hideChars = false; +} + +/** @cond */ + +// Standard command names for use with executeBuiltin(). +static char const builtin_cmd_exit[] PROGMEM = "exit"; +static char const builtin_cmd_help[] PROGMEM = "help"; + +/** @endcond */ + +/** + * \brief Performs regular activities on the shell. + * + * This function must be called regularly from the application's main loop + * to process input for the shell. + */ +void Shell::loop() +{ + // Read the next key and bail out if none. We only process a single + // key each time we enter this function to prevent other tasks in the + // system from becoming starved of time resources if the bytes are + // arriving rapidly from the underyling stream. + int key = readKey(); + if (key == -1) + return; + + // Process the key. + switch (key) { + case KEY_BACKSPACE: + // Backspace over the last character. + clearCharacters(1); + break; + + case KEY_RETURN: + // CR, LF, or CRLF pressed, so execute the current command. + execute(); + break; + + case 0x15: + // CTRL-U - clear the entire command. + clearCharacters(curLen); + break; + + case 0x04: case 0x1A: + // CTRL-D or CTRL-Z - equivalent to the "exit" command. + executeBuiltin(builtin_cmd_exit); + break; + + case KEY_UP_ARROW: + // Go back one item in the command history. + if (history && historyPosn < maxHistory) { + ++historyPosn; + changeHistory(); + } + break; + + case KEY_DOWN_ARROW: + // Go forward one item in the command history. + if (history && historyPosn > 0) { + --historyPosn; + changeHistory(); + } + break; + + case KEY_F1: + // F1 is equivalent to the "help" command. + executeBuiltin(builtin_cmd_help); + break; + + case KEY_UNICODE: { + // Add the Unicode code point to the buffer if it will fit. + long code = unicodeKey(); + size_t size = Terminal::utf8Length(code); + if (size && (curLen + size) < (sizeof(buffer) - 1)) { + Terminal::utf8Format((uint8_t *)(buffer + curLen), code); + if (!hideChars) + write((uint8_t *)(buffer + curLen), size); + curLen += size; + } + } break; + + default: + if (key >= 0x20 && key <= 0x7E) { + // Printable ASCII character - echo and add it to the buffer. + if (curLen < (sizeof(buffer) - 1)) { + if (!hideChars) + write((uint8_t)key); + buffer[curLen++] = (char)key; + } + } + break; + } +} + +#if defined(__AVR__) + +// String compare of two strings in program memory. +static int progmem_strcmp(const char *str1, const char *str2) +{ + uint8_t ch1, ch2; + for (;;) { + ch1 = pgm_read_byte((const uint8_t *)str1); + ch2 = pgm_read_byte((const uint8_t *)str2); + if (!ch1) { + if (ch2) + return -1; + else + break; + } else if (!ch2) { + return 1; + } else if (ch1 != ch2) { + return ((int)ch1) - ((int)ch2); + } + ++str1; + ++str2; + } + return 0; +} + +#else + +#define progmem_strcmp(str1,str2) (strcmp((str1), (str2))) + +#endif + +// Reads the "name" field from a command information block in program memory. +static const char *readInfoName(const ShellCommandInfo *info) +{ +#if defined(__AVR__) + return (const char *)pgm_read_word + (((const uint8_t *)info) + offsetof(ShellCommandInfo, name)); +#else + return info->name; +#endif +} + +// Reads the "help" field from a command information block in program memory. +static const char *readInfoHelp(const ShellCommandInfo *info) +{ +#if defined(__AVR__) + return (const char *)pgm_read_word + (((const uint8_t *)info) + offsetof(ShellCommandInfo, help)); +#else + return info->help; +#endif +} + +// Reads the "func" field from a command information block in program memory. +static ShellCommandFunc readInfoFunc(const ShellCommandInfo *info) +{ +#if defined(__AVR__) + if (sizeof(ShellCommandFunc) == 2) { + return (ShellCommandFunc)pgm_read_word + (((const uint8_t *)info) + offsetof(ShellCommandInfo, func)); + } else { + return (ShellCommandFunc)pgm_read_dword + (((const uint8_t *)info) + offsetof(ShellCommandInfo, func)); + } +#else + return info->func; +#endif +} + +static ShellCommandRegister *firstCmd = 0; + +/** + * \brief Registers a command with the shell. + * + * \note This function is internal. The ShellCommand() macro should be + * used instead. + */ +void Shell::registerCommand(ShellCommandRegister *cmd) +{ + // Insert the command into the list in alphanumeric order. + // We cannot rely upon the construction order to sort the list for us. + ShellCommandRegister *prev = 0; + ShellCommandRegister *current = firstCmd; + while (current != 0) { + if (progmem_strcmp(readInfoName(cmd->info), readInfoName(current->info)) < 0) + break; + prev = current; + current = current->next; + } + if (prev) + prev->next = cmd; + else + firstCmd = cmd; + cmd->next = current; +} + +/** + * \fn const char *Shell::prompt() const + * \brief Gets the prompt string to display in the shell. + * + * \return The current prompt. The default is "> ". + * + * \sa setPrompt() + */ + +/** + * \fn void Shell::setPrompt(const char *prompt) + * \brief Sets the prompt string to display in the shell. + * + * \param prompt The new prompt string. The caller is responsible to ensure + * that the string persists after this call returns. The Shell class does + * not make a copy of the string. + * + * This function must be called before begin() or the first line will be + * prompted with the default of "> ". Afterwards, calling this function + * will change the prompt for the following line of input. + * + * \sa prompt() + */ + +/** + * \fn bool Shell::hideCharacters() const + * \brief Determine if character echo is hidden. + * + * \sa setHideCharacters() + */ + +/** + * \brief Enables or disables character hiding. + * + * \param hide Set to true to enable character hiding, or false to disable. + * + * When character hiding is enabled, all characters on a command-line are + * hidden and suppressed from being echoed. This is useful for the entry + * of passwords and other sensitive information. + * + * \sa hideCharacters() + */ +void Shell::setHideCharacters(bool hide) +{ + if (hideChars == hide) + return; + hideChars = hide; + if (hide) { + // Hide the current command if something has already been echoed. + size_t len = curLen; + clearCharacters(len); + curLen = len; + } else { + // Print the current in-progress command to un-hide it. + if (curLen) + write((const uint8_t *)buffer, curLen); + } +} + +/** + * \brief Displays help for all supported commands. + */ +void Shell::help() +{ + // Find the command with the maximum length. + ShellCommandRegister *current = firstCmd; + size_t maxLen = 0; + size_t len; + while (current != 0) { + len = strlen_P(readInfoName(current->info)); + if (len > maxLen) + maxLen = len; + current = current->next; + } + maxLen += 2; + + // Print the commands with the help strings aligned on the right. + current = firstCmd; + while (current != 0) { + writeProgMem(readInfoName(current->info)); + len = maxLen - strlen_P(readInfoName(current->info)); + while (len > 0) { + write(' '); + --len; + } + writeProgMem(readInfoHelp(current->info)); + println(); + current = current->next; + } +} + +/** + * \brief Executes the command in the buffer. + */ +void Shell::execute() +{ + size_t posn = 0; + + // Terminate the current line. + println(); + + // Make sure the command is properly NUL-terminated. + buffer[curLen] = '\0'; + + // If we have a history stack and the new command is different from + // the previous command, then copy the command into the stack. + if (history && curLen > 0) { + char *hist = history + sizeof(buffer) * historyWrite; + if (strcmp(hist, buffer) != 0) { + historyWrite = (historyWrite + 1) % maxHistory; + hist = history + sizeof(buffer) * historyWrite; + strcpy(hist, buffer); + } + } + + // Reset the history read position to the top of the stack. + historyPosn = 0; + + // Skip white space at the start of the line. + while (posn < curLen && buffer[posn] == ' ') + ++posn; + + // Break the command up into arguments and populate the argv array. + int argc = 0; + size_t outposn = 0; + char quote = 0; + while (posn < curLen) { + char ch = buffer[posn]; + if (ch == ' ') { + ++posn; + continue; + } + argv[argc++] = buffer + outposn; + do { + ch = buffer[posn]; + if (ch == '"' || ch == '\'') { + if (quote == ch) { + quote = 0; + ++posn; + continue; + } else if (!quote) { + quote = ch; + ++posn; + continue; + } + } else if (!quote && ch == ' ') { + break; + } + buffer[outposn++] = ch; + ++posn; + } while (posn < curLen); + buffer[outposn++] = '\0'; + if (posn < curLen) + ++posn; + } + + // Clear the line buffer. + curLen = 0; + + // Execute the command. + if (argc > 0) { + if (!execute(argc, argv)) { + // Could not find a matching command, try the builtin "help". + if (!strcmp_P(argv[0], builtin_cmd_help)) { + help(); + } else { + static char const unknown_cmd[] PROGMEM = "Unknown command: "; + writeProgMem(unknown_cmd); + print(argv[0]); + println(); + } + } + } + + // Prepare for the next command. + if (prom) + print(prom); +} + +/** + * \brief Executes a command that has been parsed into arguments. + * + * \param argc The number of elements in \a argv. + * \param argv The arguments. + * + * \return Returns true if the command was found; false if not found. + */ +bool Shell::execute(int argc, char **argv) +{ + ShellCommandRegister *current = firstCmd; + while (current != 0) { + if (!strcmp_P(argv[0], readInfoName(current->info))) { + ShellCommandFunc func = readInfoFunc(current->info); + (*func)(*this, argc, argv); + return true; + } + current = current->next; + } + return false; +} + +/** + * \brief Executes a builtin command like "exit" or "help". + * + * \param cmd The command to execute, which must point to program memory. + */ +void Shell::executeBuiltin(const char *cmd) +{ + clearCharacters(curLen); + curLen = strlen_P(cmd); + strncpy_P(buffer, cmd, curLen); + write((const uint8_t *)buffer, curLen); + execute(); +} + +/** + * \brief Clears characters from the input line by backspacing over them. + * + * \param len The number of characters to clear. + */ +void Shell::clearCharacters(size_t len) +{ + // If the characters are hidden, then there's nothing to backspace over. + if (hideChars) + return; + + // Backspace over all characters in the buffer. + while (len > 0 && curLen > 0) { + uint8_t ch = (uint8_t)(buffer[curLen - 1]); + if (ch < 0x80) { + backspace(); + } else { + // UTF-8 character sequence. Back up some more and + // determine the value of the Unicode code point. + long code = (ch & 0x3F); + uint8_t shift = 6; + while (curLen > 1) { + --curLen; + ch = (uint8_t)(buffer[curLen - 1]); + if ((ch & 0xC0) != 0x80) + break; + code |= ((long)(ch & 0x3F)) << shift; + shift += 6; + } + if ((ch & 0xE0) == 0xC0) + ch &= 0x1F; + else if ((ch & 0xF0) == 0xE0) + ch &= 0x0F; + else + ch &= 0x07; + code |= ((long)ch) << shift; + + // If the character is wide, we need to emit two backspaces. + if (isWideCharacter(code)) + backspace(); + backspace(); + } + --len; + --curLen; + } +} + +/** + * \brief Changes the current command to reflect a different position + * in the history stack. + */ +void Shell::changeHistory() +{ + // Replace the command with the historyPosn item from the stack. + // A historyPosn of 1 is the top of the history stack and a + // historyPosn of maxHistory is the bottom of the history stack. + // A historyPosn of 0 means that the down arrow has navigated + // off the history stack, so clear the command only. + if (historyPosn) { + size_t posn = (historyWrite + maxHistory - (historyPosn - 1)) % maxHistory; + char *hist = history + sizeof(buffer) * posn; + if (*hist != '\0') { + // Copy the line from the history into the command buffer. + clearCharacters(curLen); + curLen = strlen(hist); + memcpy(buffer, hist, curLen); + if (!hideChars) + write((uint8_t *)hist, curLen); + } else { + // We've gone too far - the history is still smaller + // than maxHistory in size. So reset the position and + // don't go any further. + --historyPosn; + } + } else { + // We've navigated off the history stack. + clearCharacters(curLen); + } +} + +/** + * \fn ShellCommand(name,help,function) + * \brief Registers a command with the shell. + * + * \param name The name of the command. + * \param help Help string to display that describes the command. + * \param function The function to call to handle the command. + * + * The \a name and \a help parameters must be constant strings that can + * be placed into program memory. + * + * \code + * void cmdMotor(Shell &shell, int argc, char *argv[]) + * { + * ... + * } + * + * ShellCommand(motor, "Turn the motor on or off", cmdMotor); + * \endcode + * + * If there are multiple Shell instances active in the system, then the + * command will be registered with all of them. + * + * \relates Shell + */ diff --git a/libraries/Terminal/Shell.h b/libraries/Terminal/Shell.h new file mode 100644 index 00000000..739e56c7 --- /dev/null +++ b/libraries/Terminal/Shell.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 Southern Storm Software, Pty Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef SHELL_h +#define SHELL_h + +#include "Terminal.h" + +class Shell; + +#if defined(__arm__) +#define SHELL_MAX_CMD_LEN 256 +#else +#define SHELL_MAX_CMD_LEN 64 +#endif + +typedef void (*ShellCommandFunc)(Shell &shell, int argc, char *argv[]); +typedef bool (*ShellPasswordCheckFunc)(const char *userid, const char *password); + +/** @cond */ + +typedef struct +{ + const char *name; + const char *help; + ShellCommandFunc func; + +} ShellCommandInfo; + +class ShellCommandRegister +{ +public: + inline ShellCommandRegister(const ShellCommandInfo *_info); + + const ShellCommandInfo *info; + ShellCommandRegister *next; +}; + +/** @endcond */ + +class Shell : public Terminal +{ +public: + Shell(); + virtual ~Shell(); + + bool begin(Stream &stream, size_t maxHistory = 0); + void end(); + + void loop(); + + static void registerCommand(ShellCommandRegister *cmd); + + const char *prompt() const { return prom; } + void setPrompt(const char *prompt) { prom = prompt; } + + bool hideCharacters() const { return hideChars; } + void setHideCharacters(bool hide); + + void help(); + +private: + char buffer[SHELL_MAX_CMD_LEN]; + char *argv[SHELL_MAX_CMD_LEN / 2]; + size_t maxHistory; + size_t curLen; + char *history; + size_t historyWrite; + size_t historyPosn; + const char *prom; + bool hideChars; + + // Disable copy constructor and operator=(). + Shell(const Shell &other) {} + Shell &operator=(const Shell &) { return *this; } + + void execute(); + bool execute(int argc, char **argv); + void executeBuiltin(const char *cmd); + void clearCharacters(size_t len); + void changeHistory(); +}; + +/** @cond */ + +inline ShellCommandRegister::ShellCommandRegister(const ShellCommandInfo *_info) + : info(_info) + , next(0) +{ + Shell::registerCommand(this); +} + +/** @endcond */ + +#define ShellCommand(name,help,function) \ + static char const shell_id_##name[] PROGMEM = #name; \ + static char const shell_help_##name[] PROGMEM = help; \ + static ShellCommandInfo const shell_info_##name PROGMEM = { \ + shell_id_##name, \ + shell_help_##name, \ + (function) \ + }; \ + static ShellCommandRegister shell_cmd_##name(&shell_info_##name) + +#endif diff --git a/libraries/Terminal/Terminal.cpp b/libraries/Terminal/Terminal.cpp new file mode 100644 index 00000000..cc815b5c --- /dev/null +++ b/libraries/Terminal/Terminal.cpp @@ -0,0 +1,1179 @@ +/* + * Copyright (C) 2016 Southern Storm Software, Pty Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "Terminal.h" + +/** + * \class Terminal Terminal.h + * \brief Extended stream interface for terminal operations. + * + * This class extends the standard Arduino Stream class with functions that + * are suitable for interfacing to VT100 terminal applications like PuTTY. + * + * The following example initializes a terminal running on the primary + * Arduino serial port: + * + * \code + * Terminal term; + * void setup() { + * Serial.begin(9600); + * term.begin(Serial); + * } + * \endcode + * + * The readKey() function reads input from the underlying stream, decodes + * any VT100 key escape sequences that it finds, and reports them to the + * application using USB, ASCII, or Unicode key codes. This is typically + * used in the application's main loop as follows: + * + * \code + * void loop() { + * int key = term.readKey(); + * switch (key) { + * case -1: break; // No key available. + * + * case KEY_LEFT_ARROW: + * // Left arrow key has been pressed. + * ... + * break; + * + * case KEY_RIGHT_ARROW: + * // Right arrow key has been pressed. + * ... + * break; + * + * case KEY_ESC: + * // Escape key has been pressed. + * ... + * break; + * + * default: + * if (key >= 0x20 && key <= 7E) { + * // Visible ASCII character has been typed. + * ... + * } + * break; + * } + * } + * \endcode + * + * This class understands extended characters in the UTF-8 encoding, + * allowing the full Unicode character set to be used in applications. + * Extended Unicode characters are reported by readKey() with the key code + * KEY_UNICODE, with the actual code point returned via unicodeKey(). + * + * On the output side, UTF-8 strings can be written to the terminal + * using write(), or writeUnicode() can be used to write a single + * Unicode character in UTF-8. + * + * \note This class does not have any special support for right-to-left + * scripts or composed characters. Unicode characters are read and written + * in the order in which they arrive. Applications may need to alter + * strings to display them correctly in such scripts. Patches are + * welcome to fix this. + * + * \sa Shell + */ + +/** @cond */ + +// States for the key recognition state machine. +#define STATE_INIT 0 // Initial state. +#define STATE_CR 1 // Last character was CR, eat following LF. +#define STATE_ESC 2 // Last character was ESC. +#define STATE_MATCH 3 // Matching an escape sequence. +#define STATE_UTF8 4 // Recognizing a UTF-8 sequence. + +// Number of milliseconds to wait after an ESC character before +// concluding that it is KEY_ESC rather than an escape sequence. +#define ESC_TIMEOUT_MS 40 + +// Number of milliseconds to wait for a new character within an +// escape sequence before concluding that the sequence was invalid +// or truncated, or not actually an escape sequence at all. +#define SEQ_TIMEOUT_MS 200 + +/** @endcond */ + +/** + * \brief Constructs a terminal object. + * + * This constructor must be followed by a call to begin() to specify + * the underlying stream to use for reading and writing. + * + * \sa begin() + */ +Terminal::Terminal() + : _stream(0) + , ucode(-1) + , ncols(80) + , nrows(24) + , timer(0) + , offset(0) + , state(STATE_INIT) + , utf8len(0) +{ +} + +/** + * \brief Destroys this terminal object. + */ +Terminal::~Terminal() +{ +} + +/** + * \brief Begins terminal operations on an underlying stream. + * + * \param stream The underlying stream, whether a serial port, TCP connection, + * or some other stream. + * + * \sa end() + */ +void Terminal::begin(Stream &stream) +{ + _stream = &stream; + ucode = -1; + state = STATE_INIT; +} + +/** + * \brief Ends terminal operations on an underlying stream. + * + * This function may be useful if you want to detach the terminal from + * the underlying stream so that it can be used for something else. + */ +void Terminal::end() +{ + _stream = 0; +} + +/** + * \brief Returns the number of bytes that are available for reading. + * + * \note It is possible for this function to return a positive value + * while readKey() does not produce a new key. This can happen with + * VT100 key escape sequences and UTF-8 characters that extend over + * multiple bytes. + * + * \sa readKey() + */ +int Terminal::available() +{ + return _stream ? _stream->available() : 0; +} + +/** + * \brief Peeks at the next byte from the underlying stream. + * + * \return The next byte or -1 if no bytes are available yet. + * + * \sa read() + */ +int Terminal::peek() +{ + return _stream ? _stream->peek() : -1; +} + +/** + * \brief Reads the next byte from the underlying stream. + * + * \return Returns 0x00 to 0xFF if a byte is ready, or -1 if none available. + * + * This function performs a low-level read on the underlying byte stream + * without applying any specific interpretation to the byte. In particular, + * escape sequences corresponding to arrow and function keys will not + * be recognized. + * + * Applications will usually want to call readKey() instead to handle + * escape sequences for arrow and function keys. This function is provided + * as a convenience to implement the parent Stream interface. + * + * \sa readKey() + */ +int Terminal::read() +{ + // Clear the key recognition state because we are bypassing readKey(). + state = STATE_INIT; + ucode = -1; + + // Read the next byte from the underlying stream. + return _stream ? _stream->read() : -1; +} + +/** + * \brief Flushes all data in the underlying stream. + */ +void Terminal::flush() +{ + if (_stream) + _stream->flush(); +} + +/** + * \brief Writes a single byte to the underlying stream. + * + * \param c The byte to write. + * \return The number of bytes written, zero on error. + */ +size_t Terminal::write(uint8_t c) +{ + return _stream ? _stream->write(c) : 0; +} + +/** + * \brief Writes a buffer of data to the underlying stream. + * + * \param buffer Points to the buffer to write. + * \param size The number of bytes in the \a buffer. + * + * \return The number of bytes written, which may be short on error. + */ +size_t Terminal::write(const uint8_t *buffer, size_t size) +{ + return _stream ? _stream->write(buffer, size) : 0; +} + +/** + * \brief Writes a static string that is stored in program memory. + * + * \param str Points to the NUL-terminated string in program memory. + * + * This is a convenience function for printing static strings that + * are stored in program memory. + * + * \sa write() + */ +void Terminal::writeProgMem(const char *str) +{ + uint8_t ch; + if (!_stream) + return; + while ((ch = pgm_read_byte((const uint8_t *)str)) != 0) { + _stream->write(ch); + ++str; + } +} + +/** + * \brief Determine if a character starts an escape sequence after ESC. + * + * \param ch The character to test. + * + * \return Returns true if \a ch starts an escape sequence; false otherwise. + */ +static bool escapeSequenceStart(int ch) +{ + if (ch == '[' || ch == '?') + return true; + else if (ch >= 'A' && ch <= 'Z') + return true; + else + return false; +} + +/** + * \brief Reads the next key that was typed on this terminal. + * + * \return Returns -1 if there is no key ready yet; 0x00 to 0x7F for + * an ASCII character; KEY_UNICODE for an extended Unicode code point, + * or a USB keyboard code for special arrow or function keys. + * + * For example, if the user types the Home key, then this function + * will return KEY_HOME. If the user types the capital letter A, + * then this function will return 0x41. + * + * If the user types an extended Unicode character (U+0080 and higher), + * then this function will return KEY_UNICODE. The application should + * call unicodeKey() to retrieve the actual code point. All Unicode + * characters are assumed to be in the UTF-8 encoding on the stream. + * + * Some ASCII control characters correspond to special keys and will + * be mapped appropriately: + * + * \li 0x08 (CTRL-H) and 0x7F (DEL) are mapped to KEY_BACKSPACE + * \li 0x0D (CTRL-M) and 0x0A (CTRL-J) are mapped to KEY_RETURN + * \li 0x09 (CTRL-I) is mapped to KEY_TAB + * \li 0x1B (CTRL-[) is mapped to KEY_ESCAPE + * + * In all of these cases, the original ASCII code will be reported + * by unicodeKey(). As a special case, if 0x0D is immediately followed + * by 0x0A (that is, CRLF) then KEY_RETURN will be reported only once + * with unicodeKey() set to 0x0D. This ensures that all line ending + * types are mapped to a single KEY_RETURN report. + * + * \sa unicodeKey(), read() + */ +int Terminal::readKey() +{ + int ch; + + // Bail out if there is no underlying stream. + if (!_stream) + return -1; + + // Read the next character and bail out if nothing yet. Some special + // peek-ahead handling is needed just after the ESC character. + if (state == STATE_ESC) { + ch = _stream->peek(); + if (ch < 0) { + // We just saw an ESC. If there has been a timeout + // then the key is KEY_ESC rather than the start of a + // VT100 escape sequence. + if ((millis() - timer) >= ESC_TIMEOUT_MS) { + state = STATE_INIT; + ucode = 0x1B; + return KEY_ESC; + } + ucode = -1; + return -1; + } else if (!escapeSequenceStart(ch)) { + // The next character is not legitimate as the start of + // an escape sequence, so the ESC must have been KEY_ESC. + state = STATE_INIT; + ucode = 0x1B; + return KEY_ESC; + } else { + // Part of an escape sequence. Read the character properly. + ch = _stream->read(); + } + } else { + // Read the next character without any peek-ahead. + ch = _stream->read(); + } + if (ch < 0) { + if (state == STATE_MATCH && (millis() - timer) >= SEQ_TIMEOUT_MS) { + // Timeout while waiting for the next character in an + // escape sequence. Abort and return to the initial state. + state = STATE_INIT; + } + ucode = -1; + return -1; + } + + // Determine what to do based on the key recognition state. + switch (state) { + case STATE_CR: + // We just saw a CR, so check for CRLF and eat the LF. + state = STATE_INIT; + if (ch == 0x0A) { + ucode = -1; + return -1; + } + // Fall through to the next case. + + case STATE_INIT: + if (ch >= 0x20 && ch <= 0x7E) { + // Printable ASCII character. + state = STATE_INIT; + ucode = ch; + return ch; + } else if (ch == 0x1B) { + // Start of an escape sequence, or the escape character itself. + state = STATE_ESC; + timer = millis(); + } else if (ch == 0x0D) { + // CR which may be followed by an LF. + state = STATE_CR; + ucode = ch; + return KEY_RETURN; + } else if (ch == 0x0A) { + // LF on its own without a preceding CR. + ucode = ch; + return KEY_RETURN; + } else if (ch == 0x08 || ch == 0x7F) { + // Backspace or DEL character. + state = STATE_INIT; + ucode = ch; + return KEY_BACKSPACE; + } else if (ch == 0x09) { + // TAB character. + state = STATE_INIT; + ucode = ch; + return KEY_TAB; + } else if (ch < 0x80) { + // Some other ASCII control character. + state = STATE_INIT; + ucode = ch; + return ch; + } else if (ch >= 0xC1 && ch <= 0xDF) { + // Two-byte UTF-8 sequence. + offset = ch & 0x1F; + utf8len = 2; + state = STATE_UTF8; + } else if (ch >= 0xE1 && ch <= 0xEF) { + // Three-byte UTF-8 sequence. + offset = ch & 0x0F; + utf8len = 3; + state = STATE_UTF8; + } else if (ch >= 0xF1 && ch <= 0xF7) { + // Four-byte UTF-8 sequence. + offset = ch & 0x07; + utf8len = 4; + state = STATE_UTF8; + } + break; + + case STATE_ESC: + // Next character just after the ESC. Start the escape + // sequence matching engine at offset zero in the keymap table. + state = STATE_MATCH; + offset = 0; + // Fall through to the next case. + + case STATE_MATCH: + // In the middle of matching an escape sequence. + if (ch == 0x1B) { + // ESC character seen in the middle of an escape sequence. + // The previous escape sequence is invalid so abort and restart. + state = STATE_ESC; + timer = millis(); + break; + } + ch = matchEscape(ch); + if (ch == -1) { + // Need more characters before knowing what this is. + timer = millis(); + } else if (ch == -2) { + // Invalid escape sequence so abort and restart. + state = STATE_INIT; + } else if (ch < 0x80) { + // Escape sequence corresponds to a normal ASCII character. + state = STATE_INIT; + ucode = ch; + return ch; + } else { + // Extended keycode for an arrow or function key. + state = STATE_INIT; + ucode = -1; + return ch; + } + break; + + case STATE_UTF8: + // Recognize a multi-byte UTF-8 character encoding. + if ((ch & 0xC0) == 0x80) { + if (utf8len <= 2) { + // Final character in the sequence. + ucode = (((long)offset) << 6) | (ch & 0x3F); + state = STATE_INIT; + if (ucode > 0x10FFFFL) + break; // The code point is out of range. + return KEY_UNICODE; + } else { + // More characters still yet to come. + --utf8len; + offset = (offset << 6) | (ch & 0x3F); + } + } else { + // This character is invalid as part of a UTF-8 sequence. + state = STATE_INIT; + } + break; + } + + // If we get here, then we're still waiting for a full sequence. + ucode = -1; + return -1; +} + +/** + * \fn long Terminal::unicodeKey() const + * \brief Gets the Unicode version of the last key returned by readKey(). + * + * If readKey() returned an ASCII character (0x00 to 0x7F) or KEY_UNICODE, + * then this function can be used to query the full Unicode code point for + * the key that was typed. If some other key is typed, or no key was + * typed, then this function will return -1. + * + * Unicode code points range between 0 and 0x10FFFF. + * + * \sa readKey(), writeUnicode() + */ + +/** + * \brief Writes a Unicode code point to the output in UTF-8 encoding. + * + * \param code The code point to be written between 0 and 0x10FFFF. + * \return The number of bytes that were written to the underlying stream + * to represent \a code. Returns zero if \a code is not a valid code point. + * + * This function is useful when a specific Unicode character is desired; + * for example the code point 0x2264 corresponds to the less than or + * equal to operator "≤". See the Unicode standard for more information. + * + * Unicode characters between 0x00 and 0x7F and strings that are already + * in the UTF-8 encoding can also be written using write(). + * + * \sa write() + */ +size_t Terminal::writeUnicode(long code) +{ + uint8_t utf8[4]; + size_t size = utf8Format(utf8, code); + if (size > 0) + write(utf8, size); + return size; +} + +/** + * \fn int Terminal::columns() const + * \brief Gets the number of columns in the window; defaults to 80. + * + * \sa rows(), setWindowSize(), cursorMove() + */ + +/** + * \fn int Terminal::rows() const + * \brief Gets the number of rows in the window; defaults to 24. + * + * \sa columns(), setWindowSize(), cursorMove() + */ + +/** + * \brief Sets the number of columns and rows in the window. + * + * \param columns The number of columns between 1 and 10000. + * \param rows The number of rows between 1 and 10000. + * + * This function should be used if the application has some information + * about the actual window size. For serial ports, this usually isn't + * available but telnet and ssh sessions can get the window size from + * the remote host and set it using this function. + * + * The window size defaults to 80x24 which is the standard default for + * terminal programs like PuTTY that emulate a VT100. + * + * \sa columns(), rows() + */ +void Terminal::setWindowSize(int columns, int rows) +{ + // Sanity-check the range first. + if (columns < 1) + columns = 1; + else if (columns > 10000) + columns = 10000; + if (rows < 1) + rows = 1; + else if (rows > 10000) + rows = 10000; + ncols = columns; + nrows = rows; +} + +/** + * \brief Move the cursor to the top-left position and clear the screen. + */ +void Terminal::clear() +{ + static char const escape[] PROGMEM = "\033[H\033[J"; + writeProgMem(escape); +} + +/** + * \brief Clears from the current cursor position to the end of the line. + */ +void Terminal::clearToEOL() +{ + static char const escape[] PROGMEM = "\033[K"; + writeProgMem(escape); +} + +/** + * \brief Moves the cursor to a specific location in the window. + * + * \param x The x position for the cursor between 0 and columns() - 1. + * \param y The y position for the cursor between 0 and rows() - 1. + * + * \sa cursorLeft(), columns(), rows(), setWindowSize() + */ +void Terminal::cursorMove(int x, int y) +{ + if (!_stream) + return; + if (x < 0) + x = 0; + else if (x >= ncols) + x = ncols - 1; + if (y < 0) + y = 0; + else if (y >= nrows) + y = nrows - 1; + _stream->write((uint8_t)0x1B); + _stream->write((uint8_t)'['); + _stream->print(y + 1); + _stream->write((uint8_t)';'); + _stream->print(x + 1); + _stream->write((uint8_t)'H'); +} + +/** + * \brief Moves the cursor left by one character. + * + * \sa cursorRight(), cursorUp(), cursorDown(), cursorMove() + */ +void Terminal::cursorLeft() +{ + static char const escape[] PROGMEM = "\033[D"; + writeProgMem(escape); +} + +/** + * \brief Moves the cursor right by one character. + * + * \sa cursorLeft(), cursorUp(), cursorDown(), cursorMove() + */ +void Terminal::cursorRight() +{ + static char const escape[] PROGMEM = "\033[C"; + writeProgMem(escape); +} + +/** + * \brief Moves the cursor up by one line. + * + * \sa cursorDown(), cursorLeft(), cursorRight(), cursorMove() + */ +void Terminal::cursorUp() +{ + static char const escape[] PROGMEM = "\033[A"; + writeProgMem(escape); +} + +/** + * \brief Moves the cursor down by one line. + * + * \sa cursorUp(), cursorLeft(), cursorRight(), cursorMove() + */ +void Terminal::cursorDown() +{ + static char const escape[] PROGMEM = "\033[B"; + writeProgMem(escape); +} + +/** + * \brief Backspaces over the last character. + * + * This function prints CTRL-H, a space, and another CTRL-H to backspace + * over and erase the last character on the current line. + * + * If the last character was a wide Unicode character, then two calls to + * this function are required to erase it. See isWideCharacter() for + * more information. + * + * \sa isWideCharacter() + */ +void Terminal::backspace() +{ + static char const escape[] PROGMEM = "\b \b"; + writeProgMem(escape); +} + +/** + * \brief Inserts a line at the cursor position. + * + * \sa insertChar(), deleteLine() + */ +void Terminal::insertLine() +{ + static char const escape[] PROGMEM = "\033[L"; + writeProgMem(escape); +} + +/** + * \brief Inserts a blank character at the cursor position. + * + * \sa insertLine(), deleteChar() + */ +void Terminal::insertChar() +{ + static char const escape[] PROGMEM = "\033[@"; + writeProgMem(escape); +} + +/** + * \brief Deletes a line at the cursor position. + * + * \sa deleteChar(), insertLine() + */ +void Terminal::deleteLine() +{ + static char const escape[] PROGMEM = "\033[M"; + writeProgMem(escape); +} + +/** + * \brief Deletes the character at the cursor position. + * + * \sa deleteLine(), insertChar() + */ +void Terminal::deleteChar() +{ + static char const escape[] PROGMEM = "\033[P"; + writeProgMem(escape); +} + +/** + * \brief Scrolls the contents of the window up one line. + * + * \sa scrollDown() + */ +void Terminal::scrollUp() +{ + static char const escape[] PROGMEM = "\033[S"; + writeProgMem(escape); +} + +/** + * \brief Scrolls the contents of the window down one line. + * + * \sa scrollUp() + */ +void Terminal::scrollDown() +{ + static char const escape[] PROGMEM = "\033[T"; + writeProgMem(escape); +} + +/** + * \brief Selects normal text with all attributes and colors off. + * + * \sa color(), bold(), underline(), blink(), reverse() + */ +void Terminal::normal() +{ + static char const escape[] PROGMEM = "\033[0m"; + writeProgMem(escape); +} + +/** + * \brief Enables bold text. + * + * \sa normal() + */ +void Terminal::bold() +{ + static char const escape[] PROGMEM = "\033[1m"; + writeProgMem(escape); +} + +/** + * \brief Enables underlined text. + */ +void Terminal::underline() +{ + static char const escape[] PROGMEM = "\033[4m"; + writeProgMem(escape); +} + +/** + * \brief Enables blinking text. + */ +void Terminal::blink() +{ + static char const escape[] PROGMEM = "\033[5m"; + writeProgMem(escape); +} + +/** + * \brief Reverse the foreground and background colors for inverted text. + */ +void Terminal::reverse() +{ + static char const escape[] PROGMEM = "\033[7m"; + writeProgMem(escape); +} + +/** + * \enum Terminal::Color + * \brief Terminal foreground or background colors. + */ + +/** + * \var Terminal::Black + * \brief Color is black. + */ + +/** + * \var Terminal::DarkRed + * \brief Color is dark red. + */ + +/** + * \var Terminal::DarkGreen + * \brief Color is dark green. + */ + +/** + * \var Terminal::DarkYellow + * \brief Color is dark yellow. + */ + +/** + * \var Terminal::DarkBlue + * \brief Color is dark blue. + */ + +/** + * \var Terminal::DarkMagenta + * \brief Color is dark magenta. + */ + +/** + * \var Terminal::DarkCyan + * \brief Color is dark cyan. + */ + +/** + * \var Terminal::LightGray + * \brief Color is light gray. + */ + +/** + * \var Terminal::DarkGray + * \brief Color is dark gray. + */ + +/** + * \var Terminal::Red + * \brief Color is light red. + */ + +/** + * \var Terminal::Green + * \brief Color is light green. + */ + +/** + * \var Terminal::Yellow + * \brief Color is light yellow. + */ + +/** + * \var Terminal::Blue + * \brief Color is light blue. + */ + +/** + * \var Terminal::Magenta + * \brief Color is light magenta. + */ + +/** + * \var Terminal::Cyan + * \brief Color is light cyan. + */ + +/** + * \var Terminal::White + * \brief Color is white. + */ + +/** + * \brief Selects a text foreground color with the default background color. + * + * \param fg The foreground color to select. + * + * All other text attributes (reverse, underline, etc) are disabled. + * + * The following example displays a warning string with the initial + * word in red and all following words in normal text: + * + * \code + * term.color(Terminal::Red); + * term.print("WARNING: "); + * term.normal(); + * term.println("All files on the SD card will be deleted!"); + * \endcode + * + * \sa normal() + */ +void Terminal::color(Color fg) +{ + uint8_t code = (fg & 0x07); + uint8_t bold = (fg & 0x08) ? 1 : 0; + if (!_stream) + return; + _stream->write((uint8_t)0x1B); + _stream->write((uint8_t)'['); + _stream->write((uint8_t)'0'); // reset all attributes first + _stream->write((uint8_t)';'); + _stream->write((uint8_t)'3'); + _stream->write((uint8_t)('0' + code)); + if (bold) { + _stream->write((uint8_t)';'); + _stream->write((uint8_t)'1'); + } + _stream->write((uint8_t)'m'); +} + +/** + * \brief Selects text foreground and background colors. + * + * \param fg The foreground color to select. + * \param bg The background color to select. + * + * All other text attributes (reverse, underline, etc) are disabled. + * + * \sa normal() + */ +void Terminal::color(Color fg, Color bg) +{ + uint8_t codefg = (fg & 0x07); + uint8_t boldfg = (fg & 0x08) ? 1 : 0; + uint8_t codebg = (bg & 0x07); + if (!_stream) + return; + _stream->write((uint8_t)0x1B); + _stream->write((uint8_t)'['); + _stream->write((uint8_t)'0'); // reset all attributes first + _stream->write((uint8_t)';'); + _stream->write((uint8_t)'3'); + _stream->write((uint8_t)('0' + codefg)); + if (boldfg) { + _stream->write((uint8_t)';'); + _stream->write((uint8_t)'1'); + } + _stream->write((uint8_t)';'); + _stream->write((uint8_t)'4'); + _stream->write((uint8_t)('0' + codebg)); + _stream->write((uint8_t)'m'); +} + +/** + * \brief Determine if a Unicode character is wide. + * + * \param code The code point for the Unicode character. + * \return Returns true if \a code is a wide character, false otherwise. + * + * Wide characters typically come from East Asian languages and occupy + * two spaces in a terminal. Two calls to backspace() are required to + * erase such characters. + * + * References: http://www.unicode.org/reports/tr11/, + * http://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt + * + * \sa backspace(), writeUnicode() + */ +bool Terminal::isWideCharacter(long code) +{ + // This function was automatically generated by genwcwidth.c + static unsigned char const range3000[32] PROGMEM = { + 0xF1, 0xFF, 0xF3, 0x3F, 0x01, 0x00, 0x01, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88 + }; + static unsigned char const rangeFE00[64] PROGMEM = { + 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xE1, 0xFF, + 0x9F, 0x01, 0x00, 0x7F, 0x0C, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, + 0x01, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00 + }; + unsigned c; + if (code < 0x2300) { + return false; + } else if (code >= 0x3000 && code <= 0x30FF) { + c = (unsigned)(code - 0x3000); + return (pgm_read_byte(range3000 + (c / 8)) & (1 << (c % 8))) != 0; + } else if (code >= 0xFE00 && code <= 0xFFFF) { + c = (unsigned)(code - 0xFE00); + return (pgm_read_byte(rangeFE00 + (c / 8)) & (1 << (c % 8))) != 0; + } else if (code >= 0x3400 && code <= 0x4DBF) { + return true; + } else if (code >= 0x4E00 && code <= 0x9FFF) { + return true; + } else if (code >= 0xF900 && code <= 0xFAFF) { + return true; + } else if (code >= 0x20000 && code <= 0x2FFFD) { + return true; + } else if (code >= 0x30000 && code <= 0x3FFFD) { + return true; + } else if (code == 0x2329 || + code == 0x232A || + code == 0x3250 || + code == 0xA015) { + return true; + } + return false; +} +/** + * \brief Determines the length of a Unicode code point in the UTF-8 encoding. + * + * \param code The code point to be written between 0 and 0x10FFFF. + * \return The number of bytes that makes up the UTF-8 encoding of \a code. + * Returns zero if \a code is not a valid code point. + * + * \sa utf8Format(), writeUnicode() + */ +size_t Terminal::utf8Length(long code) +{ + // Reference: https://tools.ietf.org/html/rfc3629 + if (code < 0) { + return 0; + } else if (code <= 0x7FL) { + return 1; + } else if (code <= 0x07FFL) { + return 2; + } else if (code >= 0xD800L && code <= 0xDFFF) { + // UTF-16 surrogate pairs are not valid in UTF-8. + return 0; + } else if (code <= 0xFFFFL) { + return 3; + } else if (code <= 0x10FFFFL) { + return 4; + } else { + return 0; + } +} + +/** + * \brief Formats a Unicode code point in a buffer in the UTF-8 encoding. + * + * \param buffer The buffer to write the UTF-8 encoding to. At most 4 + * bytes will be written to this buffer. + * \param code The code point to be written between 0 and 0x10FFFF. + * \return The number of bytes that were written to \a buffer to represent + * \a code. Returns zero if \a code is not a valid code point. + * + * \sa utf8Length(), writeUnicode() + */ +size_t Terminal::utf8Format(uint8_t *buffer, long code) +{ + // Reference: https://tools.ietf.org/html/rfc3629 + if (code < 0) { + return 0; + } else if (code <= 0x7FL) { + buffer[0] = (uint8_t)code; + return 1; + } else if (code <= 0x07FFL) { + buffer[0] = 0xC0 | (uint8_t)(code >> 6); + buffer[1] = 0x80 | (((uint8_t)code) & 0x3F); + return 2; + } else if (code >= 0xD800L && code <= 0xDFFF) { + // UTF-16 surrogate pairs are not valid in UTF-8. + return 0; + } else if (code <= 0xFFFFL) { + buffer[0] = 0xE0 | (uint8_t)(code >> 12); + buffer[1] = 0x80 | (((uint8_t)(code >> 6)) & 0x3F); + buffer[2] = 0x80 | (((uint8_t)code) & 0x3F); + return 3; + } else if (code <= 0x10FFFFL) { + buffer[0] = 0xF0 | (uint8_t)(code >> 18); + buffer[1] = 0x80 | (((uint8_t)(code >> 12)) & 0x3F); + buffer[2] = 0x80 | (((uint8_t)(code >> 6)) & 0x3F); + buffer[3] = 0x80 | (((uint8_t)code) & 0x3F); + return 4; + } else { + return 0; + } +} + +// Keymap rule table. Compact representation of a recognition tree. +// Each tree node is an array of entries of the following forms: +// 0 End of this tree level. +// ch code Leaf node: ASCII character (bit 7 clear) plus 8-bit keycode. +// ch offset Interior node: ASCII character with the high bit set +// plus a 16-bit offset to the first child node. +// This table was generated with the "genkeymap" tool. Do not edit this +// table but rather edit the tool and rebuild the table from it. +static uint8_t const keymap[459] PROGMEM = { + 0xDB, 0x1A, 0x00, 0xCF, 0x57, 0x01, 0x41, 0xDA, 0x42, 0xD9, 0x43, 0xD7, + 0x44, 0xD8, 0xBF, 0xA2, 0x01, 0x50, 0xC2, 0x51, 0xC3, 0x52, 0xC4, 0x53, + 0xC5, 0x00, 0x41, 0xDA, 0x42, 0xD9, 0x43, 0xD7, 0x44, 0xD8, 0x48, 0xD2, + 0xB1, 0x42, 0x00, 0x46, 0xD5, 0xB4, 0xC9, 0x00, 0xB2, 0xCC, 0x00, 0xB3, + 0x2B, 0x01, 0xB5, 0x46, 0x01, 0xB6, 0x49, 0x01, 0xDB, 0x4C, 0x01, 0x5A, + 0x0B, 0x50, 0xD0, 0x47, 0xE5, 0x00, 0x7E, 0xD2, 0xB1, 0x5D, 0x00, 0xB2, + 0x6C, 0x00, 0xB3, 0x7B, 0x00, 0xB4, 0x88, 0x00, 0xB5, 0x95, 0x00, 0xB7, + 0xA2, 0x00, 0xB8, 0xAF, 0x00, 0xB9, 0xBC, 0x00, 0x00, 0x7E, 0xC2, 0xBB, + 0x65, 0x00, 0x5E, 0xFA, 0x00, 0xB2, 0x69, 0x00, 0x00, 0x7E, 0xF0, 0x00, + 0x7E, 0xC3, 0xBB, 0x74, 0x00, 0x5E, 0xFB, 0x00, 0xB2, 0x78, 0x00, 0x00, + 0x7E, 0xF1, 0x00, 0x7E, 0xC4, 0xBB, 0x81, 0x00, 0x00, 0xB2, 0x85, 0x00, + 0x00, 0x7E, 0xF2, 0x00, 0x7E, 0xC5, 0xBB, 0x8E, 0x00, 0x00, 0xB2, 0x92, + 0x00, 0x00, 0x7E, 0xF3, 0x00, 0x7E, 0xC6, 0xBB, 0x9B, 0x00, 0x00, 0xB2, + 0x9F, 0x00, 0x00, 0x7E, 0xF4, 0x00, 0x7E, 0xC7, 0xBB, 0xA8, 0x00, 0x00, + 0xB2, 0xAC, 0x00, 0x00, 0x7E, 0xF5, 0x00, 0x7E, 0xC8, 0xBB, 0xB5, 0x00, + 0x00, 0xB2, 0xB9, 0x00, 0x00, 0x7E, 0xF6, 0x00, 0x7E, 0xC9, 0xBB, 0xC2, + 0x00, 0x00, 0xB2, 0xC6, 0x00, 0x00, 0x7E, 0xF7, 0x00, 0x7E, 0xD5, 0x00, + 0x7E, 0xD1, 0xB0, 0xE7, 0x00, 0xB1, 0xF4, 0x00, 0xB3, 0x01, 0x01, 0xB4, + 0x10, 0x01, 0xB5, 0x1F, 0x01, 0xB6, 0x22, 0x01, 0xB8, 0x25, 0x01, 0xB9, + 0x28, 0x01, 0x00, 0x7E, 0xCA, 0xBB, 0xED, 0x00, 0x00, 0xB2, 0xF1, 0x00, + 0x00, 0x7E, 0xF8, 0x00, 0x7E, 0xCB, 0xBB, 0xFA, 0x00, 0x00, 0xB2, 0xFE, + 0x00, 0x00, 0x7E, 0xF9, 0x00, 0x7E, 0xCC, 0x24, 0xF8, 0xBB, 0x09, 0x01, + 0x00, 0xB2, 0x0D, 0x01, 0x00, 0x7E, 0xFA, 0x00, 0x7E, 0xCD, 0x24, 0xF9, + 0xBB, 0x18, 0x01, 0x00, 0xB2, 0x1C, 0x01, 0x00, 0x7E, 0xFB, 0x00, 0x7E, + 0xF0, 0x00, 0x7E, 0xF1, 0x00, 0x7E, 0xF2, 0x00, 0x7E, 0xF3, 0x00, 0x7E, + 0xD4, 0xB1, 0x3A, 0x01, 0xB2, 0x3D, 0x01, 0xB3, 0x40, 0x01, 0xB4, 0x43, + 0x01, 0x00, 0x7E, 0xF4, 0x00, 0x7E, 0xF5, 0x00, 0x7E, 0xF6, 0x00, 0x7E, + 0xF7, 0x00, 0x7E, 0xD3, 0x00, 0x7E, 0xD6, 0x00, 0x41, 0xC2, 0x42, 0xC3, + 0x43, 0xC4, 0x44, 0xC5, 0x45, 0xC6, 0x00, 0x41, 0xDA, 0x42, 0xD9, 0x43, + 0xD7, 0x44, 0xD8, 0x48, 0xD2, 0x46, 0xD5, 0x20, 0x20, 0x49, 0xB3, 0x4D, + 0xB0, 0x6A, 0x2A, 0x6B, 0x2B, 0x6C, 0x2C, 0x6D, 0x2D, 0x6E, 0x2E, 0x6F, + 0x2F, 0x70, 0x30, 0x71, 0x31, 0x72, 0x32, 0x73, 0x33, 0x74, 0x34, 0x75, + 0x35, 0x76, 0x36, 0x77, 0x37, 0x78, 0x38, 0x79, 0x39, 0x58, 0x3D, 0x50, + 0xC2, 0x51, 0xC3, 0x52, 0xC4, 0x53, 0xC5, 0xB2, 0x99, 0x01, 0x5A, 0x0B, + 0x00, 0x50, 0xF0, 0x51, 0xF1, 0x52, 0xF2, 0x53, 0xF3, 0x00, 0x20, 0x20, + 0x49, 0xB3, 0x4D, 0xB0, 0x6A, 0x2A, 0x6B, 0x2B, 0x6C, 0x2C, 0x6D, 0x2D, + 0x6E, 0x2E, 0x6F, 0x2F, 0x70, 0x30, 0x71, 0x31, 0x72, 0x32, 0x73, 0x33, + 0x74, 0x34, 0x75, 0x35, 0x76, 0x36, 0x77, 0x37, 0x78, 0x38, 0x79, 0x39, + 0x58, 0x3D, 0x00 +}; + +/** + * \brief Matches the next character in an escape sequence. + * + * \param ch The next character to match. + * \return -1 if more characters are required, -2 if the escape sequence + * is invalid, or a positive key code if the match is complete. + */ +int Terminal::matchEscape(int ch) +{ + uint8_t kch; + for (;;) { + kch = pgm_read_byte(keymap + offset); + if (!kch) { + // No match at this level, so the escape sequence is invalid. + break; + } else if (kch & 0x80) { + // Interior node. + if ((kch & 0x7F) == ch) { + // Interior node matches. Go down one tree level. + offset = ((int)(pgm_read_byte(keymap + offset + 1))) | + (((int)(pgm_read_byte(keymap + offset + 2))) << 8); + return -1; + } + offset += 3; + } else { + // Leaf node. + if (kch == (uint8_t)ch) { + // We have found a match on a full escape sequence. + return pgm_read_byte(keymap + offset + 1); + } + offset += 2; + } + } + return -2; +} diff --git a/libraries/Terminal/Terminal.h b/libraries/Terminal/Terminal.h new file mode 100644 index 00000000..dbd009bb --- /dev/null +++ b/libraries/Terminal/Terminal.h @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2016 Southern Storm Software, Pty Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef TERMINAL_h +#define TERMINAL_h + +#include +#include +#include "USBKeysExtra.h" + +// Special key code that indicates that unicodeKey() contains the actual code. +#define KEY_UNICODE 0x1000 + +class Terminal : public Stream +{ +public: + Terminal(); + virtual ~Terminal(); + + void begin(Stream &stream); + void end(); + + virtual int available(); + virtual int peek(); + virtual int read(); + + virtual void flush(); + + virtual size_t write(uint8_t c); + virtual size_t write(const uint8_t *buffer, size_t size); + using Stream::write; + + void writeProgMem(const char *str); + + int readKey(); + + long unicodeKey() const { return ucode; } + + size_t writeUnicode(long code); + + int columns() const { return ncols; } + int rows() const { return nrows; } + + void setWindowSize(int columns, int rows); + + void clear(); + void clearToEOL(); + + void cursorMove(int x, int y); + void cursorLeft(); + void cursorRight(); + void cursorUp(); + void cursorDown(); + + void backspace(); + + void insertLine(); + void insertChar(); + void deleteLine(); + void deleteChar(); + + void scrollUp(); + void scrollDown(); + + void normal(); + void bold(); + void underline(); + void blink(); + void reverse(); + + enum Color + { + Black = 0x00, + DarkRed = 0x01, + DarkGreen = 0x02, + DarkYellow = 0x03, + DarkBlue = 0x04, + DarkMagenta = 0x05, + DarkCyan = 0x06, + LightGray = 0x07, + DarkGray = 0x08, + Red = 0x09, + Green = 0x0A, + Yellow = 0x0B, + Blue = 0x0C, + Magenta = 0x0D, + Cyan = 0x0E, + White = 0x0F + }; + + void color(Color fg); + void color(Color fg, Color bg); + + static bool isWideCharacter(long code); + + static size_t utf8Length(long code); + static size_t utf8Format(uint8_t *buffer, long code); + +private: + Stream *_stream; + long ucode; + int ncols, nrows; + unsigned long timer; + uint16_t offset; + uint8_t state; + uint8_t utf8len; + + int matchEscape(int ch); +}; + +#endif diff --git a/libraries/Terminal/USBKeysExtra.h b/libraries/Terminal/USBKeysExtra.h new file mode 100644 index 00000000..69d592ac --- /dev/null +++ b/libraries/Terminal/USBKeysExtra.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 Southern Storm Software, Pty Ltd. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef USBKEYSEXTRA_h +#define USBKEYSEXTRA_h + +// Extra key codes that are not included in the standard USBAPI.h header. +// Reference: http://www.usb.org/developers/hidpage/Hut1_12v2.pdf +// Note: USBAPI.h shifts the Hut codes by adding 136 (0x88) so that +// they don't intersect with ASCII. We do that here as well so +// that these codes can be used with Keyboard.press(). We use #ifndef +// here in case the core Arduino libraries add these in the future. + +#ifndef KEY_PRINT_SCREEN +#define KEY_PRINT_SCREEN (0x46 + 0x88) +#endif +#ifndef KEY_SCROLL_LOCK +#define KEY_SCROLL_LOCK (0x47 + 0x88) +#endif +#ifndef KEY_PAUSE +#define KEY_PAUSE (0x48 + 0x88) +#endif +#ifndef KEY_NUM_LOCK +#define KEY_NUM_LOCK (0x53 + 0x88) +#endif +#ifndef KEY_NUMPAD_5 +#define KEY_NUMPAD_5 (0x5D + 0x88) +#endif +#ifndef KEY_F13 +#define KEY_F13 (0x68 + 0x88) +#endif +#ifndef KEY_F14 +#define KEY_F14 (0x69 + 0x88) +#endif +#ifndef KEY_F15 +#define KEY_F15 (0x6A + 0x88) +#endif +#ifndef KEY_F16 +#define KEY_F16 (0x6B + 0x88) +#endif +#ifndef KEY_F17 +#define KEY_F17 (0x6C + 0x88) +#endif +#ifndef KEY_F18 +#define KEY_F18 (0x6D + 0x88) +#endif +#ifndef KEY_F19 +#define KEY_F19 (0x6E + 0x88) +#endif +#ifndef KEY_F20 +#define KEY_F20 (0x6F + 0x88) +#endif +#ifndef KEY_F21 +#define KEY_F21 (0x70 + 0x88) +#endif +#ifndef KEY_F22 +#define KEY_F22 (0x71 + 0x88) +#endif +#ifndef KEY_F23 +#define KEY_F23 (0x72 + 0x88) +#endif +#ifndef KEY_F24 +#define KEY_F24 (0x73 + 0x88) +#endif + +// USB does not have a code for "Back Tab" as it is usually Shift-TAB. +// For convenience, we map it to the ASCII vertical tab character (0x0B). +#define KEY_BACK_TAB 0x0B + +#ifndef KEY_RETURN + +// If the Arduino variant does not support USB, then USBAPI.h will not +// define the key codes that we need. So we define them here instead. + +#define KEY_RETURN (0x28 + 0x88) +#define KEY_ESC (0x29 + 0x88) +#define KEY_BACKSPACE (0x2A + 0x88) +#define KEY_TAB (0x2B + 0x88) +#define KEY_CAPS_LOCK (0x39 + 0x88) +#define KEY_F1 (0x3A + 0x88) +#define KEY_F2 (0x3B + 0x88) +#define KEY_F3 (0x3C + 0x88) +#define KEY_F4 (0x3D + 0x88) +#define KEY_F5 (0x3E + 0x88) +#define KEY_F6 (0x3F + 0x88) +#define KEY_F7 (0x40 + 0x88) +#define KEY_F8 (0x41 + 0x88) +#define KEY_F9 (0x42 + 0x88) +#define KEY_F10 (0x43 + 0x88) +#define KEY_F11 (0x44 + 0x88) +#define KEY_F12 (0x45 + 0x88) +#define KEY_INSERT (0x49 + 0x88) +#define KEY_HOME (0x4A + 0x88) +#define KEY_PAGE_UP (0x4B + 0x88) +#define KEY_DELETE (0x4C + 0x88) +#define KEY_END (0x4D + 0x88) +#define KEY_PAGE_DOWN (0x4E + 0x88) +#define KEY_RIGHT_ARROW (0x4F + 0x88) +#define KEY_LEFT_ARROW (0x50 + 0x88) +#define KEY_DOWN_ARROW (0x51 + 0x88) +#define KEY_UP_ARROW (0x52 + 0x88) + +#endif + +#endif diff --git a/libraries/Terminal/examples/Keys/Keys.ino b/libraries/Terminal/examples/Keys/Keys.ino new file mode 100644 index 00000000..02ef5a9e --- /dev/null +++ b/libraries/Terminal/examples/Keys/Keys.ino @@ -0,0 +1,121 @@ +/* +This example tests the Terminal class by displaying the VT100 keys that +are pressed. A real terminal program like PuTTY will be needed. + +This example is placed into the public domain. +*/ + +#include +#include + +Terminal term; + +#define K(name) {name, #name} +struct KeyInfo +{ + uint8_t code; + char name[19]; +}; +struct KeyInfo const keys[] PROGMEM = { + K(KEY_RETURN), + K(KEY_ESC), + K(KEY_BACKSPACE), + K(KEY_TAB), + K(KEY_BACK_TAB), + K(KEY_CAPS_LOCK), + K(KEY_F1), + K(KEY_F2), + K(KEY_F3), + K(KEY_F4), + K(KEY_F5), + K(KEY_F6), + K(KEY_F7), + K(KEY_F8), + K(KEY_F9), + K(KEY_F10), + K(KEY_F11), + K(KEY_F12), + K(KEY_F13), + K(KEY_F14), + K(KEY_F15), + K(KEY_F16), + K(KEY_F17), + K(KEY_F18), + K(KEY_F19), + K(KEY_F20), + K(KEY_F21), + K(KEY_F22), + K(KEY_F23), + K(KEY_F24), + K(KEY_INSERT), + K(KEY_HOME), + K(KEY_PAGE_UP), + K(KEY_DELETE), + K(KEY_END), + K(KEY_PAGE_DOWN), + K(KEY_RIGHT_ARROW), + K(KEY_LEFT_ARROW), + K(KEY_DOWN_ARROW), + K(KEY_UP_ARROW), + K(KEY_PRINT_SCREEN), + K(KEY_SCROLL_LOCK), + K(KEY_PAUSE), + K(KEY_NUM_LOCK), + K(KEY_NUMPAD_5), + {0, ""} +}; + +void setup() +{ + Serial.begin(9600); + term.begin(Serial); + + term.println("Press keys to see their codes ..."); + term.println(); +} + +void loop() +{ + int key = term.readKey(); + if (key >= 0x21 && key <= 0x7E) { + // Printable ASCII character. + term.print("ASCII: "); + term.write((uint8_t)key); + term.println(); + } else if (key == 0x20) { + // Space. + term.println("ASCII: SPACE"); + } else if (key == KEY_UNICODE) { + // Extended Unicode character. + term.print("Unicode: U+"); + term.print(term.unicodeKey(), 16); + term.println(); + } else if (key >= 0 && key <= 0xFF) { + // Special arrow or function key. + const uint8_t *table = (const uint8_t *)keys; + int code; + while ((code = pgm_read_byte(table)) != 0) { + if (code == key) { + term.print("Special: "); + term.writeProgMem((const char *)(table + 1)); + term.println(); + break; + } + table += sizeof(struct KeyInfo); + } + if (!code) { + // Non-printable ASCII or unknown key. + if (key < 0x20) + term.print("ASCII: 0x"); + else + term.print("Unknown: 0x"); + term.print(key, 16); + term.println(); + } + } else if (key >= 0) { + // Unknown keycode. Print in hex. + term.print("Unknown: 0x"); + term.print(key, 16); + term.println(); + } +} diff --git a/libraries/Terminal/examples/SerialShell/SerialShell.ino b/libraries/Terminal/examples/SerialShell/SerialShell.ino new file mode 100644 index 00000000..a243517f --- /dev/null +++ b/libraries/Terminal/examples/SerialShell/SerialShell.ino @@ -0,0 +1,35 @@ +/* +This example demonstrates how to create a simple shell on the serial port. + +This example is placed into the public domain. +*/ + +#include + +int ledPin = 13; + +Shell shell; + +void cmdLed(Shell &shell, int argc, char *argv[]) +{ + if (argc > 1 && !strcmp(argv[1], "on")) + digitalWrite(ledPin, HIGH); + else + digitalWrite(ledPin, LOW); +} + +ShellCommand(led, "Turns the status LED on or off", cmdLed); + +void setup() +{ + pinMode(ledPin, OUTPUT); + + Serial.begin(9600); + shell.setPrompt("Command: "); + shell.begin(Serial, 5); +} + +void loop() +{ + shell.loop(); +}