From f30f57059d00f2a7d5d11ac8fcc8e9d8a1824d18 Mon Sep 17 00:00:00 2001
From: Alex Hirzel <alex@hirzel.us>
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 <FreeRTOSConfig.h>
+
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 <stdlib.h>
+#include <string.h> /* strlen */
+
+#include <espressif/esp_common.h>
+#include <espressif/user_interface.h>
+#include <esp/uart.h>
+#include <FreeRTOS.h>
+#include <task.h>
+
+#include <uart_repl/uart_repl.h>
+
+
+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 <uart_repl/uart_repl.h>
+
+#include <stdio.h> /* fflush, fputs, putchar, stdout, STDIN_FILENO */
+#include <string.h>/* memset */
+#include <unistd.h> /* read */
+#include <ctype.h> /* isalnum */
+
+#include <FreeRTOS.h>
+#include <task.h> /* 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("<Esc>", 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);
+	
+	// <Enter> 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--;
+		}
+
+	// <Esc>, 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 <Esc> 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 <Esc>) that
+		 * can be used to determine a given <Esc> is stand-alone after that
+		 * given time-out.
+		 */
+		STATE = UART_REPL_ANSI_JUST_ESCAPED;
+
+	} else {
+		// nonprintable or unhandled character; do nothing!
+		//printf("<NP 0x%02x>", 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("<Home>", stdout); break;
+					//case '2': fputs("<Ins>", stdout); break;
+					case '3': deleteKey(ST); break;
+					//case '4': fputs("<End>", stdout); break;
+					//case '5': fputs("<PgUp>", stdout); break;
+					//case '6': fputs("<PgDn>", 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 <stddef.h> /* 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_ */
+