From f30f57059d00f2a7d5d11ac8fcc8e9d8a1824d18 Mon Sep 17 00:00:00 2001 From: Alex Hirzel Date: Sun, 1 Apr 2018 20:00:51 -0400 Subject: [PATCH] add a helper library, uart_repl, which handles basic ANSI over UART --- examples/uart_repl_test/FreeRTOSConfig.h | 4 + examples/uart_repl_test/Makefile | 9 + examples/uart_repl_test/uart_repl_test.c | 31 ++ extras/uart_repl/component.mk | 9 + extras/uart_repl/uart_repl.c | 395 +++++++++++++++++++++++ extras/uart_repl/uart_repl.h | 45 +++ 6 files changed, 493 insertions(+) create mode 100644 examples/uart_repl_test/FreeRTOSConfig.h create mode 100644 examples/uart_repl_test/Makefile create mode 100644 examples/uart_repl_test/uart_repl_test.c create mode 100644 extras/uart_repl/component.mk create mode 100644 extras/uart_repl/uart_repl.c create mode 100644 extras/uart_repl/uart_repl.h diff --git a/examples/uart_repl_test/FreeRTOSConfig.h b/examples/uart_repl_test/FreeRTOSConfig.h new file mode 100644 index 0000000..8e68f77 --- /dev/null +++ b/examples/uart_repl_test/FreeRTOSConfig.h @@ -0,0 +1,4 @@ +#define configUSE_COUNTING_SEMAPHORES (1) + +#include_next + diff --git a/examples/uart_repl_test/Makefile b/examples/uart_repl_test/Makefile new file mode 100644 index 0000000..f159fbf --- /dev/null +++ b/examples/uart_repl_test/Makefile @@ -0,0 +1,9 @@ +PROGRAM=uart_repl_test.c +EXTRA_COMPONENTS=extras/stdin_uart_interrupt extras/uart_repl +ESPBAUD=921600 + +include ../../common.mk + +serial: + screen $(ESPPORT) 115200 + diff --git a/examples/uart_repl_test/uart_repl_test.c b/examples/uart_repl_test/uart_repl_test.c new file mode 100644 index 0000000..bcfe087 --- /dev/null +++ b/examples/uart_repl_test/uart_repl_test.c @@ -0,0 +1,31 @@ +#include +#include /* strlen */ + +#include +#include +#include +#include +#include + +#include + + +void handle_command(char const d[]) { + if (!strcmp(d, "ts") || !strcmp(d, "time")) { + printf("the tick count since boot is: %u\n", xTaskGetTickCount()); + } else if (!strcmp(d, "help")) { + printf("commands include ts, time\n"); + } else { + printf("command not recognized, try help\n"); + } +} + + +void user_init(void) { + uart_set_baud(0, 115200); + + printf("\n\nWelcome to the uart REPL demo. try \"help\"\n"); + uart_repl_init(&handle_command); +} + + diff --git a/extras/uart_repl/component.mk b/extras/uart_repl/component.mk new file mode 100644 index 0000000..0a9db16 --- /dev/null +++ b/extras/uart_repl/component.mk @@ -0,0 +1,9 @@ +# Component makefile for extras/uart_repl + +# expected anyone using RTC driver includes it as 'uart_repl/uart_repl.h' +INC_DIRS += $(uart_repl_ROOT).. + +# args for passing into compile rule generation +uart_repl_SRC_DIR = $(uart_repl_ROOT) + +$(eval $(call component_compile_rules,uart_repl)) diff --git a/extras/uart_repl/uart_repl.c b/extras/uart_repl/uart_repl.c new file mode 100644 index 0000000..7e9a585 --- /dev/null +++ b/extras/uart_repl/uart_repl.c @@ -0,0 +1,395 @@ +/* Read-Evaluate-Print Loop over UART + * + * This is a library that allows you to quickly prototype REPL-type loops. + * Currently, basic ANSI escape sequences are supported so that GNU screen(1) + * can be used with the delete and arrow keys. The framework is very expandable + * to other ANSI escape sequences. + * + * Dependencies: it is recommended that you also use extras/stdin_uart_interrupt + * to make this more responsive. + */ + +#include + +#include /* fflush, fputs, putchar, stdout, STDIN_FILENO */ +#include /* memset */ +#include /* read */ +#include /* isalnum */ + +#include +#include /* vTaskDelete, xTaskCreate */ + + +/* This is a helper macro which creates a highly-localized and optimizable + * function. It greatly aids in code readability, and the compiler should be + * able to eliminate most of the stack overhead from these function calls. + * + * Note: Uses a GCC-specific version of VA_ARGS that allows us to have no + * arguments specified (the default case). */ +#define UTIL(F, ...) static void F(struct serial_terminal_status *ST, \ + ##__VA_ARGS__) + +// convenience macros +#define POS (ST->lineCursorPosition) +#define LEN (ST->lineLength) +#define CH (ST->lastReadChar) +#define LINE (ST->line) +#define STATE (ST->state) +#define CSI (ST->csi_seq) + + +UTIL(bell) { + // ring bell + putchar('\a'); +} + +UTIL(arrowLeft) { + if (POS) { + POS--; + putchar('\b'); // move cursor backward + } else { + bell(ST); + } +} + +UTIL(arrowRight) { + if (POS < LEN) { + putchar(LINE[POS++]); + } else { + bell(ST); + } +} + +UTIL(arrowUp) { + // TODO - in the future perhaps we can support line history + bell(ST); +} +UTIL(arrowDown) { + // TODO - in the future perhaps we can support line history + bell(ST); +} + +UTIL(backSpace) { + if (POS) { + int j; + + // copy the rest of the string (if any) one character backwards, and + // also update the screen as we go + putchar('\b'); + for (j = POS; j < LEN; j++) { + LINE[j-1] = LINE[j]; + putchar(LINE[j-1]); + } + + // erase the ending character, also account for the loss + putchar(' '); + putchar('\b'); + LINE[--LEN] = '\0'; + POS--; + + // we just moved the cursor right a few spaces, so reset it now + for (j = LEN - POS; j > 0; putchar('\b'), j--); + + } else { + bell(ST); + } +} + +UTIL(deleteKey) { + if (POS < LEN) { + arrowRight(ST); + backSpace(ST); + } else { + bell(ST); + } +} + + +UTIL(prompt) { + while (POS) { + LINE[--POS] = '\0'; + } + LEN = 0; + fputs("> ", stdout); +} + + +UTIL(pushPrintable) { + if (POS == LEN) { + // XXX TODO could overflow + LINE[POS+1] = '\0'; + } + LINE[POS] = CH; + LEN++; + POS++; + putchar(CH); +} + +UTIL(realEscapeKey) { + //fputs("", stdout); +} + +UTIL(nonAnsiChar) { + + // normal printable, echoing character (but line might be full) + if (CH >= 0x20 && CH < 0x7F) { + if (POS + 1 < sizeof(LINE)) { + // if line length is respected... + pushPrintable(ST); + } else { + bell(ST); + } + + // Backspace key or CTRL+H + } else if (0x7F == CH || 0x08 == CH) { + backSpace(ST); + + // key + } else if ('\n' == CH || '\r' == CH) { + putchar('\n'); + ST->lineCb((char const *) LINE); + prompt(ST); + + // CTRL+C, abort current command + } else if (0x03 == CH) { + bell(ST); + putchar('\n'); + prompt(ST); + + // CTRL+L, redraw on new line + } else if (0x0C == CH) { + putchar('\n'); + prompt(ST); + fputs(LINE, stdout); + + // CTRL+U, clear line in place + } else if (0x15 == CH) { + while (POS) { + fputs("\b \b", stdout); + LINE[--POS] = '\0'; + LEN--; + } + + // , starting an escape sequence maybe + } else if (0x1b == CH) { + /* + * There is going to be an issue here, and POS cannot fix it right now. + * There is a non-determinism when using only the character queue. It + * turns out that the ANSI escape sequence parser will need to introduce + * some kind of waiting concept to determine whether a given is + * due to an escape sequence or just a stand-alone escape. + * + * TODO: For now, we just assume Esc is always part of an ANSI escape + * sequence. In the future. the character-wise state machine should + * incorporate one external event (a delay after pressing ) that + * can be used to determine a given is stand-alone after that + * given time-out. + */ + STATE = UART_REPL_ANSI_JUST_ESCAPED; + + } else { + // nonprintable or unhandled character; do nothing! + //printf("", CH); + } +} + + +UTIL(AnsiCSIBackout) { + + if (UART_REPL_ANSI_NONE == STATE) { + // nothing to do + return; + } + + /* If this gets called, we are part of the way thru parsing an ANSI + * sequence, and we need to back out of it from whereever we're at in the + * parsing process. Use the existing state information to functionally + * ignore this ANSI escape sequence by using nonAnsiChar(ST) to re-handle the + * keys in the proper order. if this routine is called after the CSI + * structure has been completely populated, it is assumed the CH character + * will still represent the final byte by the time we get here. so if + * AnsiCSIBackout is called from e.g. arrowRight(), it is assumed that CH is + * still equal to final_byte. + */ + + char tempChar; + int j; + + switch (STATE) { + case UART_REPL_ANSI_JUST_ESCAPED: + // handled below, only back out one character + realEscapeKey(ST); + nonAnsiChar(ST); + STATE = UART_REPL_ANSI_NONE; + break; + case UART_REPL_ANSI_READ_CSI_PARAMETER_BYTES: + case UART_REPL_ANSI_READ_CSI_INTERMEDIATE_BYTES: + case UART_REPL_ANSI_READ_CSI_FINAL_BYTE: + realEscapeKey(ST); + tempChar = CH; // backup the current key + CH = '['; + nonAnsiChar(ST); + for (j = 0; j < CSI.parameter_n_bytes; j++) { + CH = CSI.parameter_bytes[j]; + nonAnsiChar(ST); + CSI.parameter_bytes[j] = '\0'; + CSI.parameter_n_bytes--; + } + for (j = 0; j < CSI.intermediate_n_bytes; j++) { + CH = CSI.intermediate_bytes[j]; + nonAnsiChar(ST); + CSI.intermediate_bytes[j] = '\0'; + CSI.intermediate_n_bytes--; + } + CH = tempChar; // restore + nonAnsiChar(ST); + STATE = UART_REPL_ANSI_NONE; + break; + case UART_REPL_ANSI_NONE: + default: + break; + } +} + + +/* this takes an input of the csi_seq structure, and does whatever it wants with + * it */ +UTIL(AnsiCSI) { + switch (CSI.final_byte) { + // handle arrow keys (note: shifted versions are not captured) + case 'A': arrowUp(ST); break; + case 'B': arrowDown(ST); break; + case 'C': arrowRight(ST); break; + case 'D': arrowLeft(ST); break; + case '~': + if (1 == CSI.parameter_n_bytes) { + switch (CSI.parameter_bytes[0]) { + //case '1': fputs("", stdout); break; + //case '2': fputs("", stdout); break; + case '3': deleteKey(ST); break; + //case '4': fputs("", stdout); break; + //case '5': fputs("", stdout); break; + //case '6': fputs("", stdout); break; + default: AnsiCSIBackout(ST); break; + } + } else { + AnsiCSIBackout(ST); + } + break; + default: + AnsiCSIBackout(ST); + break; + } +} + + + +UTIL(readCH) { + if (!read(STDIN_FILENO, (void*)&CH, 1)) { + fputs("never see this print as read(...) is blocking\n", stdout); + } +} + + + + +UTIL(MainStateMachine) { + prompt(ST); + +top: + fflush(stdout); + switch (STATE) { + case UART_REPL_ANSI_NONE: + readCH(ST); + nonAnsiChar(ST); + break; + + case UART_REPL_ANSI_JUST_ESCAPED: + readCH(ST); + + CSI.parameter_n_bytes = 0; + CSI.intermediate_n_bytes = 0; + + /* Wikipedia: Sequences have different lengths. All sequences start + * with ESC (27 / hex 0x1B), followed by a second byte in the range + * 0x40–0x5F (ASCII @A–Z[\]^_). */ + if (CH < 0x40 || CH > 0x5F) { + AnsiCSIBackout(ST); + } else if ('[' == CH) { + STATE = UART_REPL_ANSI_READ_CSI_PARAMETER_BYTES; + readCH(ST); + } else { + AnsiCSIBackout(ST); + } + break; + + /* Wikipedia: The ESC [ is followed by any number (including none) of + * "parameter bytes" in the range 0x30–0x3F (ASCII 0–9:;<=>?), then by + * any number of "intermediate bytes" in the range 0x20–0x2F (ASCII + * space and !"#$%&'()*+,-./), then finally by a single "final byte" in + * the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). */ + case UART_REPL_ANSI_READ_CSI_PARAMETER_BYTES: + if (CH >= 0x30 && CH <= 0x3F) { + // valid parameter byte + CSI.parameter_bytes[CSI.parameter_n_bytes++] = CH; + readCH(ST); // for the next thing + } else if (CH >= 0x20 && CH <= 0x2F) { + // valid intermediate byte + STATE = UART_REPL_ANSI_READ_CSI_INTERMEDIATE_BYTES; + } else if (CH >= 0x40 && CH <= 0x7E) { + // valid final byte + STATE = UART_REPL_ANSI_READ_CSI_FINAL_BYTE; + } else { + AnsiCSIBackout(ST); + } + break; + + case UART_REPL_ANSI_READ_CSI_INTERMEDIATE_BYTES: + if (CH >= 0x20 && CH <= 0x2F) { + // valid intermediate byte + CSI.intermediate_bytes[CSI.intermediate_n_bytes++] = CH; + readCH(ST); // for the next thing + } else if (CH >= 0x40 && CH <= 0x7E) { + // valid final byte + STATE = UART_REPL_ANSI_READ_CSI_FINAL_BYTE; + } else { + AnsiCSIBackout(ST); + } + break; + + case UART_REPL_ANSI_READ_CSI_FINAL_BYTE: + if (CH >= 0x40 && CH <= 0x7E) { + // valid final byte + CSI.final_byte = CH; + AnsiCSI(ST); + STATE = UART_REPL_ANSI_NONE; + } else { + AnsiCSIBackout(ST); + } + break; + } + + goto top; +} + +#undef UTIL +#undef POS +#undef LINE +#undef LEN +#undef CH +#undef STATE + + +void uart_repl_task(void *pvParameters) { + struct serial_terminal_status cc; + memset(&cc, 0, sizeof(cc)); + cc.lineCb = pvParameters; + MainStateMachine(&cc); + vTaskDelete(NULL); // just in case we get here +} + + +void uart_repl_init(uart_repl_handler line_cb) { + xTaskCreate(uart_repl_task, "uart_repl", 256, (void *)line_cb, 10, NULL); +} + diff --git a/extras/uart_repl/uart_repl.h b/extras/uart_repl/uart_repl.h new file mode 100644 index 0000000..3ad7b6f --- /dev/null +++ b/extras/uart_repl/uart_repl.h @@ -0,0 +1,45 @@ +#ifndef _SWC_UART_REPL_ +#define _SWC_UART_REPL_ +#include /* size_t */ + + +#if 0 +/* in the future, maybe add support for special keys */ +enum uart_repl_special_key { + UART_REPL_NONE, + UART_REPL_UP, + UART_REPL_DOWN, + UART_REPL_RIGHT, + UART_REPL_LEFT, +}; +#endif + +typedef void (*uart_repl_handler)(char const *); + +struct serial_terminal_status { + char line[80]; + unsigned int lineCursorPosition; // this is the index of the next character to be written + unsigned int lineLength; // length of string total so far + char lastReadChar; + uart_repl_handler lineCb; + enum uart_repl_ansi_parse_state { + UART_REPL_ANSI_NONE = 0, + UART_REPL_ANSI_JUST_ESCAPED, + UART_REPL_ANSI_READ_CSI_PARAMETER_BYTES, + UART_REPL_ANSI_READ_CSI_INTERMEDIATE_BYTES, + UART_REPL_ANSI_READ_CSI_FINAL_BYTE, + } state; + struct { + unsigned int parameter_n_bytes; + unsigned int intermediate_n_bytes; + char parameter_bytes[10]; + char intermediate_bytes[10]; + char final_byte; + } csi_seq; +}; + +void uart_repl_task(void *); +void uart_repl_init(uart_repl_handler); + +#endif /* ndef _SWC_UART_REPL_ */ +