diff --git a/examples/lvglterm/Kconfig b/examples/lvglterm/Kconfig index dba596b44..2f4c598f2 100644 --- a/examples/lvglterm/Kconfig +++ b/examples/lvglterm/Kconfig @@ -7,9 +7,15 @@ menuconfig EXAMPLES_LVGLTERM tristate "LVGL Terminal" default n depends on GRAPHICS_LVGL + select PSEUDOTERM ---help--- Enable LVGL Terminal + The shell runs on a pseudo-terminal so that it, and the programs + it starts, see a console on their standard streams: isatty() is + what tells an interactive program to prompt, echo and flush its + output line by line. + if EXAMPLES_LVGLTERM choice diff --git a/examples/lvglterm/lvglterm.c b/examples/lvglterm/lvglterm.c index ad555cc55..b59271db5 100644 --- a/examples/lvglterm/lvglterm.c +++ b/examples/lvglterm/lvglterm.c @@ -24,11 +24,10 @@ * "NuttX RTOS for PinePhone: LVGL Terminal for NSH Shell" * https://lupyuen.github.io/articles/terminal * - * Code shared by both input variants: it starts the NSH shell with its - * standard streams redirected through pipes, renders the shell output in an - * LVGL text area, and delegates the input source to the selected variant - * (on-screen keyboard in lvglterm_touch.c, physical keyboard in - * lvglterm_kbd.c). + * Code shared by both input variants: it starts the NSH shell on a + * pseudo-terminal, renders the shell output in an LVGL text area, and + * delegates the input source to the selected variant (on-screen keyboard in + * lvglterm_touch.c, physical keyboard in lvglterm_kbd.c). */ /**************************************************************************** @@ -37,14 +36,17 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include +#include #include #include @@ -56,10 +58,15 @@ # error posix_spawn() should be enabled in the configuration #endif -/* NSH Redirection requires Pipes */ +/* The shell runs on a pseudo-terminal rather than on plain pipes because + * that is what makes its standard streams look like a console. Interactive + * programs decide from isatty() whether to echo, to prompt and to + * line-buffer their output and the terminal driver is what echoes back what + * the user types. + */ -#ifndef CONFIG_DEV_PIPE_SIZE -# error FIFO and Named Pipe Drivers should be enabled in the configuration +#ifndef CONFIG_PSEUDOTERM +# error Pseudo-Terminal (PTY) support should be enabled in the configuration #endif /* NSH Output requires a Monospaced Font. The size is selectable so that @@ -86,6 +93,16 @@ #define TIMER_PERIOD_MS 20 +/* How many reads the output poll may perform on a single timer tick */ + +#define MAX_READS_PER_TICK 16 + +/* How many keystrokes may wait for the shell to consume them. A shell line + * is shorter than this, and the shell truncates the over-long ones anyway. + */ + +#define INPUT_QUEUE_SIZE 256 + /* Trim the output text area once it grows past this many characters */ #define TERM_MAXCHARS 4096 @@ -108,6 +125,8 @@ * Private Function Prototypes ****************************************************************************/ +static bool has_output(void); +static void flush_input(void); static int create_widgets(void); static void timer_callback(lv_timer_t *timer); @@ -115,10 +134,6 @@ static void timer_callback(lv_timer_t *timer); * Public Data ****************************************************************************/ -/* Pipe to NSH stdin, written by the selected input variant */ - -int g_nsh_stdin[2]; - /* LVGL Column Container and NSH Output Text Area (shared with the variant) */ lv_obj_t *g_col; @@ -132,10 +147,16 @@ lv_style_t g_terminal_style; * Private Data ****************************************************************************/ -/* Pipes for NSH stdout and stderr */ +/* PTY master: the terminal's end of the shell's console. Keystrokes are + * written to it and the shell output is read from it. + */ -static int g_nsh_stdout[2]; -static int g_nsh_stderr[2]; +static int g_nsh_fd = -1; + +/* Keystrokes waiting for room in the terminal (see lvglterm_send_input) */ + +static char g_input_queue[INPUT_QUEUE_SIZE]; +static int g_input_queued; /* LVGL Timer for polling NSH Output */ @@ -148,25 +169,103 @@ static char * const g_nsh_argv[] = NSH_TASK, NULL }; +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: has_output + * + * Description: + * Return true if the shell has produced output that can be read without + * blocking. + * + ****************************************************************************/ + +static bool has_output(void) +{ + struct pollfd fdp; + + fdp.fd = g_nsh_fd; + fdp.events = POLLIN; + return poll(&fdp, 1, 0) > 0 && (fdp.revents & POLLIN) != 0; +} + +/**************************************************************************** + * Name: flush_input + * + * Description: + * Hand the queued keystrokes to the terminal, but no more of them than it + * can take right now: a write that has to wait for room blocks the LVGL + * thread, and that deadlocks the terminal, since the shell stops reading + * as soon as the echo it produces has filled the output buffer that only + * this thread drains. Runs in the LVGL thread. + * + ****************************************************************************/ + +static void flush_input(void) +{ + int nwritten; + int space; + + if (g_input_queued == 0) + { + return; + } + + if (ioctl(g_nsh_fd, FIONSPACE, &space) < 0 || space <= 0) + { + return; + } + + if (space > g_input_queued) + { + space = g_input_queued; + } + + nwritten = write(g_nsh_fd, g_input_queue, space); + if (nwritten <= 0) + { + return; + } + + g_input_queued -= nwritten; + memmove(g_input_queue, g_input_queue + nwritten, g_input_queued); +} + /**************************************************************************** * Public Functions ****************************************************************************/ /**************************************************************************** - * Name: lvglterm_has_input + * Name: lvglterm_send_input * * Description: - * Return true if the file descriptor has data to be read. + * Send keystrokes to the shell. Whatever the terminal cannot take yet is + * queued and handed over by the terminal's periodic timer. Must run in + * the LVGL thread. * ****************************************************************************/ -bool lvglterm_has_input(int fd) +void lvglterm_send_input(FAR const char *buf, int len) { - struct pollfd fdp; + int room = INPUT_QUEUE_SIZE - g_input_queued; - fdp.fd = fd; - fdp.events = POLLIN; - return poll(&fdp, 1, 0) > 0 && (fdp.revents & POLLIN) != 0; + if (len > room) + { + /* The queue drains no faster than the shell reads. Dropping what does + * not fit keeps the terminal responsive, and a line that long would be + * truncated by the shell in any case. + */ + + gwarn("WARNING: dropping %d input bytes\n", len - room); + len = room; + } + + memcpy(g_input_queue + g_input_queued, buf, len); + g_input_queued += len; + + flush_input(); } /**************************************************************************** @@ -297,9 +396,8 @@ static int create_widgets(void) * Name: create_terminal * * Description: - * Start the NSH shell with its streams redirected to pipes, create the - * shared widgets and the output-polling timer, and set up the input - * variant. + * Start the NSH shell on a pseudo-terminal, create the shared widgets and + * the output-polling timer, and set up the input variant. * ****************************************************************************/ @@ -307,25 +405,31 @@ static int create_terminal(int argc, FAR char *argv[]) { int ret; pid_t pid; + int slave; - /* Create the pipes for NSH Shell: stdin, stdout and stderr */ + /* Create the pseudo-terminal. The slave keeps the driver defaults, ECHO + * and the \n -> \r\n output translation among them, which is what a + * console provides and what the shell's line editor expects. + */ - if (pipe(g_nsh_stdin) < 0 || pipe(g_nsh_stdout) < 0 || - pipe(g_nsh_stderr) < 0) + if (openpty(&g_nsh_fd, &slave, NULL, NULL, NULL) < 0) { - fprintf(stderr, "pipe failed: %d\n", errno); + fprintf(stderr, "openpty failed: %d\n", errno); return ERROR; } - /* Close default stdin, stdout and stderr and assign the new pipes */ + /* Close default stdin, stdout and stderr and assign the terminal slave, + * which is the shell's console once it is spawned below. + */ close(0); close(1); close(2); - dup2(g_nsh_stdin[READ_PIPE], 0); - dup2(g_nsh_stdout[WRITE_PIPE], 1); - dup2(g_nsh_stderr[WRITE_PIPE], 2); + dup2(slave, 0); + dup2(slave, 1); + dup2(slave, 2); + close(slave); /* Start the NSH Shell and inherit stdin, stdout and stderr */ @@ -358,7 +462,7 @@ static int create_terminal(int argc, FAR char *argv[]) * Name: timer_callback * * Description: - * Poll NSH stdout and stderr for output and render it, then let the input + * Poll the terminal for shell output and render it, then let the input * variant perform its periodic work. Runs in the LVGL thread. * ****************************************************************************/ @@ -366,30 +470,37 @@ static int create_terminal(int argc, FAR char *argv[]) static void timer_callback(lv_timer_t *timer) { static char buf[64]; + int reads; int ret; - /* Drain the input variant first (local echo, scroll) so that what the user - * typed is rendered before the resulting shell output. + /* Collect the keystrokes and hand them over first, so that what the user + * typed is rendered (the terminal echoes it back to us) before the + * resulting shell output. */ lvglterm_input_poll(); + flush_input(); - if (lvglterm_has_input(g_nsh_stdout[READ_PIPE])) - { - ret = read(g_nsh_stdout[READ_PIPE], buf, sizeof(buf)); - if (ret > 0) - { - lvglterm_add_output(buf, ret); - } - } + /* Drain what the shell has produced. A writer blocks once it has filled + * the terminal buffer, so read as long as there is something to read, but + * keep a cap: a program producing output faster than the screen can take + * it must not keep the LVGL thread out of lv_timer_handler(). + */ - if (lvglterm_has_input(g_nsh_stderr[READ_PIPE])) + for (reads = 0; reads < MAX_READS_PER_TICK; reads++) { - ret = read(g_nsh_stderr[READ_PIPE], buf, sizeof(buf)); - if (ret > 0) + if (!has_output()) { - lvglterm_add_output(buf, ret); + break; } + + ret = read(g_nsh_fd, buf, sizeof(buf)); + if (ret <= 0) + { + break; + } + + lvglterm_add_output(buf, ret); } } diff --git a/examples/lvglterm/lvglterm.h b/examples/lvglterm/lvglterm.h index 627919cdd..684e73327 100644 --- a/examples/lvglterm/lvglterm.h +++ b/examples/lvglterm/lvglterm.h @@ -31,25 +31,15 @@ #include #include -/**************************************************************************** - * Pre-processor Definitions - ****************************************************************************/ - -/* Read and Write ends of the NSH pipes */ - -#define READ_PIPE 0 -#define WRITE_PIPE 1 - /**************************************************************************** * Public Data ****************************************************************************/ -/* Shared state owned by the core (lvglterm.c). The input variant writes the - * shell input to g_nsh_stdin, adds its widgets under g_col (styled with - * g_terminal_style) and may render into g_output. +/* Shared state owned by the core (lvglterm.c). The input variant adds its + * widgets under g_col (styled with g_terminal_style) and may render into + * g_output. */ -extern int g_nsh_stdin[2]; /* Input variant -> NSH stdin */ extern lv_obj_t *g_col; /* Column container (widget parent) */ extern lv_obj_t *g_output; /* NSH output text area */ extern lv_style_t g_terminal_style; /* Monospaced font style */ @@ -58,15 +48,18 @@ extern lv_style_t g_terminal_style; /* Monospaced font style */ * Public Function Prototypes ****************************************************************************/ -/* Shared helpers provided by the core */ +/* Shared helpers provided by the core. Note that the terminal echoes the + * keystrokes back as the shell reads them, so an input variant must not echo + * what it sends a second time. + */ -bool lvglterm_has_input(int fd); +void lvglterm_send_input(FAR const char *buf, int len); void lvglterm_add_output(FAR const char *buf, int len); /* Provided by the selected input variant (lvglterm_touch.c or * lvglterm_kbd.c). lvglterm_input_create() sets up the input source under - * g_col and wires it to g_nsh_stdin. lvglterm_input_poll() runs on every - * LVGL timer tick (LVGL thread) for periodic work; it may be a no-op. + * g_col. lvglterm_input_poll() runs on every LVGL timer tick (LVGL thread) + * for periodic work; it may be a no-op. */ void lvglterm_input_create(int argc, FAR char *argv[]); diff --git a/examples/lvglterm/lvglterm_kbd.c b/examples/lvglterm/lvglterm_kbd.c index daea4c50b..9ebc65c62 100644 --- a/examples/lvglterm/lvglterm_kbd.c +++ b/examples/lvglterm/lvglterm_kbd.c @@ -80,11 +80,10 @@ static FAR const char *g_kbddev; /* Keyboard device path (/dev/kbdN) */ * Name: feed_char * * Description: - * Forward one character to NSH stdin and, for printable characters and - * newline, echo it on screen. Control keys such as backspace are echoed - * by the shell's own line editor (via its stdout) to avoid handling them - * twice. Carriage return is normalised to newline. Runs in the LVGL - * thread, so it may touch the LVGL widgets directly. + * Forward one character to the shell. Nothing is echoed here: the + * terminal echoes the printable characters as the shell reads them and the + * shell's own line editor deals with the control keys, so echoing again + * would show everything twice. Carriage return is normalised to newline. * ****************************************************************************/ @@ -95,12 +94,7 @@ static void feed_char(char ch) ch = '\n'; } - write(g_nsh_stdin[WRITE_PIPE], &ch, 1); - - if (ch == '\n' || ((uint8_t)ch >= 0x20 && (uint8_t)ch < 0x7f)) - { - lvglterm_add_output(&ch, 1); - } + lvglterm_send_input(&ch, 1); } /**************************************************************************** @@ -184,8 +178,8 @@ void lvglterm_input_create(int argc, FAR char *argv[]) * * Description: * Drain any pending key presses from the keyboard device and forward them - * to NSH (echoing what is typed on screen). Runs in the LVGL thread from - * the terminal's periodic timer, so the read must not block. + * to the shell. Runs in the LVGL thread from the terminal's periodic + * timer, so the read must not block. * ****************************************************************************/ diff --git a/examples/lvglterm/lvglterm_touch.c b/examples/lvglterm/lvglterm_touch.c index 4285c97e6..8610d4010 100644 --- a/examples/lvglterm/lvglterm_touch.c +++ b/examples/lvglterm/lvglterm_touch.c @@ -67,7 +67,6 @@ static lv_obj_t *g_kb; static void input_callback(lv_event_t *e) { - int ret; const lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_VALUE_CHANGED) @@ -94,21 +93,21 @@ static void input_callback(lv_event_t *e) return; } - /* Echo the command on the output so the prompt line reads - * "nsh> ", then send it to the shell. + /* Send the command to the shell. The terminal echoes it back as + * the shell reads it, which is what puts it on the prompt line as + * "nsh> ", so nothing is echoed here. */ len = strlen(cmd); - lvglterm_add_output(cmd, len); + lvglterm_send_input(cmd, len); + + /* The shell acts on the line only once it is terminated */ + if (cmd[len - 1] != '\n') { - lvglterm_add_output("\n", 1); + lvglterm_send_input("\n", 1); } - DEBUGASSERT(g_nsh_stdin[WRITE_PIPE] != 0); - ret = write(g_nsh_stdin[WRITE_PIPE], cmd, len); - DEBUGASSERT(ret == len); - lv_textarea_set_text(g_input, ""); } }