esp-open-rtos/extras/uart_repl/uart_repl.c

395 lines
9.4 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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
* 0x400x5F (ASCII @AZ[\]^_). */
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 0x300x3F (ASCII 09:;<=>?), then by
* any number of "intermediate bytes" in the range 0x200x2F (ASCII
* space and !"#$%&'()*+,-./), then finally by a single "final byte" in
* the range 0x400x7E (ASCII @AZ[\]^_`az{|}~). */
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);
}