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 <filipe.cavalcanti@espressif.com>
This commit is contained in:
Filipe Cavalcanti 2026-08-20 15:26:24 -03:00 committed by Xiang Xiao
parent d570175cc9
commit 2b92bedaee
5 changed files with 190 additions and 87 deletions

View file

@ -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

View file

@ -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_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);
}
}

View file

@ -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[]);

View file

@ -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.
*
****************************************************************************/

View file

@ -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, "");
}
}