This is an automated email from the ASF dual-hosted git repository.
xiaoxiang781216 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx-apps.git
The following commit(s) were added to refs/heads/master by this push:
new 2b92bedae examples/lvglterm: replace PIPES with PSEUDOTERM
2b92bedae is described below
commit 2b92bedaee0989995255ea4e51a7fc04963a0ae9
Author: Filipe Cavalcanti <[email protected]>
AuthorDate: Thu Aug 20 15:26:24 2026 -0300
examples/lvglterm: replace PIPES with PSEUDOTERM
The lvglterm terminal now runs the shell on a pseudo-terminal instead of
three
plain pipes.
Signed-off-by: Filipe Cavalcanti <[email protected]>
---
examples/lvglterm/Kconfig | 6 ++
examples/lvglterm/lvglterm.c | 199 +++++++++++++++++++++++++++++--------
examples/lvglterm/lvglterm.h | 27 ++---
examples/lvglterm/lvglterm_kbd.c | 20 ++--
examples/lvglterm/lvglterm_touch.c | 17 ++--
5 files changed, 186 insertions(+), 83 deletions(-)
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 <nuttx/config.h>
#include <sys/boardctl.h>
+#include <sys/ioctl.h>
#include <unistd.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
+#include <errno.h>
#include <nuttx/debug.h>
#include <poll.h>
+#include <pty.h>
#include <spawn.h>
#include <lvgl/lvgl.h>
@@ -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_fd = -1;
-static int g_nsh_stdout[2];
-static int g_nsh_stderr[2];
+/* 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 */
@@ -149,26 +170,104 @@ static char * const g_nsh_argv[] =
};
/****************************************************************************
- * Public Functions
+ * Private Functions
****************************************************************************/
/****************************************************************************
- * Name: lvglterm_has_input
+ * Name: has_output
*
* Description:
- * Return true if the file descriptor has data to be read.
+ * Return true if the shell has produced output that can be read without
+ * blocking.
*
****************************************************************************/
-bool lvglterm_has_input(int fd)
+static bool has_output(void)
{
struct pollfd fdp;
- fdp.fd = fd;
+ 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_send_input
+ *
+ * Description:
+ * 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.
+ *
+ ****************************************************************************/
+
+void lvglterm_send_input(FAR const char *buf, int len)
+{
+ int room = INPUT_QUEUE_SIZE - g_input_queued;
+
+ 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();
+}
+
/****************************************************************************
* Name: lvglterm_add_output
*
@@ -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();
+
+ /* 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_stdout[READ_PIPE]))
+ for (reads = 0; reads < MAX_READS_PER_TICK; reads++)
{
- ret = read(g_nsh_stdout[READ_PIPE], buf, sizeof(buf));
- if (ret > 0)
+ if (!has_output())
{
- lvglterm_add_output(buf, ret);
+ break;
}
- }
- if (lvglterm_has_input(g_nsh_stderr[READ_PIPE]))
- {
- ret = read(g_nsh_stderr[READ_PIPE], buf, sizeof(buf));
- if (ret > 0)
+ ret = read(g_nsh_fd, buf, sizeof(buf));
+ if (ret <= 0)
{
- lvglterm_add_output(buf, ret);
+ 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 <stdbool.h>
#include <lvgl/lvgl.h>
-/****************************************************************************
- * 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> <command>", 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> <command>", 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, "");
}
}