From c57e7a7b81d373c64b2a31708f68661e7f717b5b Mon Sep 17 00:00:00 2001 From: Lee Lup Yuen Date: Wed, 1 Feb 2023 14:16:15 +0800 Subject: [PATCH] examples/lvglterm: Add LVGL Terminal for NSH This PR adds an LVGL App that executes NSH Commands (entered with a Touchscreen Keyboard) and renders the NSH Output. The app follows the same design as the `lvgldemo` app and is explained here: ["NuttX RTOS for PinePhone: LVGL Terminal for NSH Shell"](https://lupyuen.github.io/articles/terminal) `examples/README.md`: Added doc for `lvglterm` app `examples/lvglterm/lvglterm.c`: LVGL Terminal App `examples/lvglterm/Makefile`, `Make.defs`: Makefile for LVGL Terminal `examples/lvglterm/Kconfig`: Added menuconfig option for "Application Configuration > Examples > LVGL Terminal" --- examples/README.md | 13 + examples/lvglterm/Kconfig | 23 ++ examples/lvglterm/Make.defs | 23 ++ examples/lvglterm/Makefile | 32 ++ examples/lvglterm/lvglterm.c | 588 +++++++++++++++++++++++++++++++++++ 5 files changed, 679 insertions(+) create mode 100644 examples/lvglterm/Kconfig create mode 100644 examples/lvglterm/Make.defs create mode 100644 examples/lvglterm/Makefile create mode 100644 examples/lvglterm/lvglterm.c diff --git a/examples/README.md b/examples/README.md index e7bb5569a..6256857b3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -669,6 +669,19 @@ A simple reader example for the `LSM303` acc-mag sensor. A simple reader example for the `LSM6DSL` acc-gyro sensor. +## `lvglterm` LVGL Terminal for NuttShell (NSH) + +LVGL application that executes NuttShell (NSH) commands entered with a +Touchscreen Keyboard and displays the NSH output. Prerequisite configuration +settings: + +- `CONFIG_NSH_ARCHINIT=n` – NSH architecture initialization must be disabled. +- `CONFIG_NSH_CONSOLE=y` – NSH must be configured to use a console. +- `CONFIG_LIBC_EXECFUNCS=y` – posix_spawn() must be enabled. +- `CONFIG_PIPES=y` – Pipes must be enabled. +- `CONFIG_GRAPHICS_LVGL=y` – LVGL graphics must be enabled. +- `CONFIG_LV_FONT_UNSCII_16=y` – LVGL font UNSCII 16 must be enabled. + ## `media` The media test simply writes values onto the media hidden behind a character diff --git a/examples/lvglterm/Kconfig b/examples/lvglterm/Kconfig new file mode 100644 index 000000000..dec9d9b24 --- /dev/null +++ b/examples/lvglterm/Kconfig @@ -0,0 +1,23 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +menuconfig EXAMPLES_LVGLTERM + tristate "LVGL Terminal" + default n + depends on GRAPHICS_LVGL + ---help--- + Enable LVGL Terminal + +if EXAMPLES_LVGLTERM + +config EXAMPLES_LVGLTERM_PRIORITY + int "lvglterm task priority" + default 100 + +config EXAMPLES_LVGLTERM_STACKSIZE + int "lvglterm stack size" + default 16384 + +endif # EXAMPLES_LVGLTERM diff --git a/examples/lvglterm/Make.defs b/examples/lvglterm/Make.defs new file mode 100644 index 000000000..17e96e8b7 --- /dev/null +++ b/examples/lvglterm/Make.defs @@ -0,0 +1,23 @@ +############################################################################ +# apps/examples/lvglterm/Make.defs +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +ifneq ($(CONFIG_EXAMPLES_LVGLTERM),) +CONFIGURED_APPS += $(APPDIR)/examples/lvglterm +endif diff --git a/examples/lvglterm/Makefile b/examples/lvglterm/Makefile new file mode 100644 index 000000000..6ff050286 --- /dev/null +++ b/examples/lvglterm/Makefile @@ -0,0 +1,32 @@ +############################################################################ +# apps/examples/lvglterm/Makefile +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +include $(APPDIR)/Make.defs + +PROGNAME = lvglterm +PRIORITY = $(CONFIG_EXAMPLES_LVGLTERM_PRIORITY) +STACKSIZE = $(CONFIG_EXAMPLES_LVGLTERM_STACKSIZE) +MODULE = $(CONFIG_EXAMPLES_LVGLTERM) + +# LVGL Terminal + +MAINSRC = lvglterm.c + +include $(APPDIR)/Application.mk diff --git a/examples/lvglterm/lvglterm.c b/examples/lvglterm/lvglterm.c new file mode 100644 index 000000000..33cfcb0ac --- /dev/null +++ b/examples/lvglterm/lvglterm.c @@ -0,0 +1,588 @@ +/**************************************************************************** + * apps/examples/lvglterm/lvglterm.c + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/* Reference: + * "NuttX RTOS for PinePhone: LVGL Terminal for NSH Shell" + * https://lupyuen.github.io/articles/terminal + */ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* NSH Task requires posix_spawn() */ + +#ifndef CONFIG_LIBC_EXECFUNCS +# error posix_spawn() should be enabled in the configuration +#endif + +/* NSH Redirection requires Pipes */ + +#ifndef CONFIG_DEV_PIPE_SIZE +# error FIFO and Named Pipe Drivers should be enabled in the configuration +#endif + +/* NSH Output requires a Monospaced Font */ + +#ifndef CONFIG_LV_FONT_UNSCII_16 +# error LVGL Font UNSCII 16 should be enabled in the configuration +#endif + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* Should we perform board-specific driver initialization? There are two + * ways that board initialization can occur: 1) automatically via + * board_late_initialize() during bootupif CONFIG_BOARD_LATE_INITIALIZE + * or 2). + * via a call to boardctl() if the interface is enabled + * (CONFIG_BOARDCTL=y). + * If this task is running as an NSH built-in application, then that + * initialization has probably already been performed otherwise we do it + * here. + */ + +#undef NEED_BOARDINIT + +#if defined(CONFIG_BOARDCTL) && !defined(CONFIG_NSH_ARCHINIT) +# define NEED_BOARDINIT 1 +#endif + +/* How often to poll for output from NSH Shell (milliseconds) */ + +#define TIMER_PERIOD_MS 100 + +/* Read and Write Pipes for NSH stdin, stdout and stderr */ + +#define READ_PIPE 0 +#define WRITE_PIPE 1 + +/* NSH Task to be started */ + +#define NSH_TASK "nsh" + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static int create_widgets(void); +static void timer_callback(lv_timer_t * timer); +static void input_callback(lv_event_t * e); +static bool has_input(int fd); +static void remove_escape_codes(char *buf, int len); + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +/* Pipes for NSH Shell: stdin, stdout, stderr */ + +static int g_nsh_stdin[2]; +static int g_nsh_stdout[2]; +static int g_nsh_stderr[2]; + +/* LVGL Column Container for NSH Widgets */ + +static lv_obj_t *g_col; + +/* LVGL Text Area Widgets for NSH Input and Output */ + +static lv_obj_t *g_input; +static lv_obj_t *g_output; + +/* LVGL Keyboard Widget for NSH Terminal */ + +static lv_obj_t *g_kb; + +/* LVGL Font Style for NSH Input and Output */ + +static lv_style_t g_terminal_style; + +/* LVGL Timer for polling NSH Output */ + +static lv_timer_t *g_timer; + +/* Arguments for NSH Task */ + +static char * const g_nsh_argv[] = +{ + NSH_TASK, NULL +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: create_terminal + * + * Description: + * Create the LVGL Terminal. Start the NSH Shell and redirect the NSH + * stdin, stdout and stderr to the LVGL Widgets. + * + * Input Parameters: + * None + * + * Returned Value: + * Zero (OK) on success; a negated errno value is returned on any failure. + * + ****************************************************************************/ + +static int create_terminal(void) +{ + int ret; + pid_t pid; + + /* Create the pipes for NSH Shell: stdin, stdout and stderr */ + + ret = pipe(g_nsh_stdin); + if (ret < 0) + { + _err("stdin pipe failed: %d\n", errno); + return ERROR; + } + + ret = pipe(g_nsh_stdout); + if (ret < 0) + { + _err("stdout pipe failed: %d\n", errno); + return ERROR; + } + + ret = pipe(g_nsh_stderr); + if (ret < 0) + { + _err("stderr pipe failed: %d\n", errno); + return ERROR; + } + + /* Close default stdin, stdout and stderr */ + + close(0); + close(1); + close(2); + + /* Assign the new pipes as stdin, stdout and stderr */ + + dup2(g_nsh_stdin[READ_PIPE], 0); + dup2(g_nsh_stdout[WRITE_PIPE], 1); + dup2(g_nsh_stderr[WRITE_PIPE], 2); + + /* Start the NSH Shell and inherit stdin, stdout and stderr */ + + ret = posix_spawn(&pid, /* Returned Task ID */ + NSH_TASK, /* NSH Path */ + NULL, /* Inherit stdin, stdout and stderr */ + NULL, /* Default spawn attributes */ + g_nsh_argv, /* Arguments */ + NULL); /* No environment */ + if (ret < 0) + { + int errcode = errno; + _err("posix_spawn failed: %d\n", errcode); + return -errcode; + } + + /* Create an LVGL Timer to poll for output from NSH Shell */ + + g_timer = lv_timer_create(timer_callback, /* Callback Function */ + TIMER_PERIOD_MS, /* Timer Period (millisec) */ + NULL); /* Callback Argument */ + DEBUGASSERT(g_timer != NULL); + + /* Create the LVGL Terminal Widgets */ + + ret = create_widgets(); + if (ret < 0) + { + return ret; + } + + return OK; +} + +/**************************************************************************** + * Name: create_widgets + * + * Description: + * Create the LVGL Widgets for LVGL Terminal. + * + * Input Parameters: + * None + * + * Returned Value: + * Zero (OK) on success; a negated errno value is returned on any failure. + * + ****************************************************************************/ + +static int create_widgets(void) +{ + /* Set the Font Style for NSH Input and Output to a Monospaced Font */ + + lv_style_init(&g_terminal_style); + lv_style_set_text_font(&g_terminal_style, &lv_font_unscii_16); + + /* Create an LVGL Container with Column Flex Direction */ + + g_col = lv_obj_create(lv_scr_act()); + DEBUGASSERT(g_col != NULL); + lv_obj_set_size(g_col, LV_PCT(100), LV_PCT(100)); + lv_obj_set_flex_flow(g_col, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(g_col, 0, 0); /* No padding */ + + /* Create an LVGL Text Area Widget for NSH Output */ + + g_output = lv_textarea_create(g_col); + DEBUGASSERT(g_output != NULL); + lv_obj_add_style(g_output, &g_terminal_style, 0); + lv_obj_set_width(g_output, LV_PCT(100)); + lv_obj_set_flex_grow(g_output, 1); /* Fill the column */ + + /* Create an LVGL Text Area Widget for NSH Input */ + + g_input = lv_textarea_create(g_col); + DEBUGASSERT(g_input != NULL); + lv_obj_add_style(g_input, &g_terminal_style, 0); + lv_obj_set_size(g_input, LV_PCT(100), LV_SIZE_CONTENT); + + /* Create an LVGL Keyboard Widget */ + + g_kb = lv_keyboard_create(g_col); + DEBUGASSERT(g_kb != NULL); + lv_obj_set_style_pad_all(g_kb, 0, 0); /* No padding */ + + /* Register the Callback Function for NSH Input */ + + lv_obj_add_event_cb(g_input, input_callback, LV_EVENT_ALL, NULL); + + /* Set the Keyboard to populate the NSH Input Text Area */ + + lv_keyboard_set_textarea(g_kb, g_input); + + return OK; +} + +/**************************************************************************** + * Name: timer_callback + * + * Description: + * Callback Function for LVGL Timer. Poll NSH stdout and stderr for output + * and display the output. + * + * Input Parameters: + * timer - LVGL Timer for the callback + * + * Returned Value: + * None + * + ****************************************************************************/ + +static void timer_callback(lv_timer_t *timer) +{ + int ret; + static char buf[64]; + + DEBUGASSERT(g_nsh_stdout[READ_PIPE] != 0); + DEBUGASSERT(g_nsh_stderr[READ_PIPE] != 0); + + /* Poll NSH stdout to check if there's output to be processed */ + + if (has_input(g_nsh_stdout[READ_PIPE])) + { + /* Read the output from NSH stdout */ + + ret = read(g_nsh_stdout[READ_PIPE], buf, sizeof(buf) - 1); + if (ret > 0) + { + /* Add to NSH Output Text Area */ + + buf[ret] = 0; + remove_escape_codes(buf, ret); + + DEBUGASSERT(g_output != NULL); + lv_textarea_add_text(g_output, buf); + } + } + + /* Poll NSH stderr to check if there's output to be processed */ + + if (has_input(g_nsh_stderr[READ_PIPE])) + { + /* Read the output from NSH stderr */ + + ret = read(g_nsh_stderr[READ_PIPE], buf, sizeof(buf) - 1); + if (ret > 0) + { + /* Add to NSH Output Text Area */ + + buf[ret] = 0; + remove_escape_codes(buf, ret); + + DEBUGASSERT(g_output != NULL); + lv_textarea_add_text(g_output, buf); + } + } +} + +/**************************************************************************** + * Name: input_callback + * + * Description: + * Callback Function for NSH Input Text Area. If Enter Key was pressed, + * send the NSH Input Command to NSH stdin. + * + * Input Parameters: + * e - LVGL Event for the callback + * + * Returned Value: + * None + * + ****************************************************************************/ + +static void input_callback(lv_event_t *e) +{ + int ret; + + /* Decode the LVGL Event */ + + const lv_event_code_t code = lv_event_get_code(e); + + /* If NSH Input Text Area has changed, get the Key Pressed */ + + if (code == LV_EVENT_VALUE_CHANGED) + { + /* Get the Button Index of the Keyboard Button Pressed */ + + const uint16_t id = lv_keyboard_get_selected_btn(g_kb); + + /* Get the Text of the Keyboard Button */ + + const char *key = lv_keyboard_get_btn_text(g_kb, id); + if (key == NULL) + { + return; + } + + /* If Key Pressed is Enter, send the Command to NSH stdin */ + + if (key[0] == 0xef && key[1] == 0xa2 && key[2] == 0xa2) + { + /* Read the NSH Input */ + + const char *cmd; + DEBUGASSERT(g_input != NULL); + cmd = lv_textarea_get_text(g_input); + if (cmd == NULL || cmd[0] == 0) + { + return; + } + + /* Send the Command to NSH stdin */ + + DEBUGASSERT(g_nsh_stdin[WRITE_PIPE] != 0); + ret = write(g_nsh_stdin[WRITE_PIPE], cmd, strlen(cmd)); + DEBUGASSERT(ret == strlen(cmd)); + + /* Erase the NSH Input */ + + lv_textarea_set_text(g_input, ""); + } + } +} + +/**************************************************************************** + * Name: has_input + * + * Description: + * Return true if the File Descriptor has data to be read. + * + * Input Parameters: + * fd - File Descriptor to be checked + * + * Returned Value: + * True if File Descriptor has data to be read; False otherwise + * + ****************************************************************************/ + +static bool has_input(int fd) +{ + int ret; + + /* Poll the File Descriptor for input */ + + struct pollfd fdp; + fdp.fd = fd; + fdp.events = POLLIN; + ret = poll(&fdp, /* File Descriptors */ + 1, /* Number of File Descriptors */ + 0); /* Poll Timeout (Milliseconds) */ + + if (ret > 0) + { + /* If poll is OK and there is input */ + + if ((fdp.revents & POLLIN) != 0) + { + /* Report that there is input */ + + return true; + } + + /* Else report no input */ + + return false; + } + else if (ret == 0) + { + /* If timeout, report no input */ + + return false; + } + else if (ret < 0) + { + /* Handle error */ + + _err("poll failed: %d, fd=%d\n", ret, fd); + return false; + } + + /* Never comes here */ + + DEBUGPANIC(); + return false; +} + +/**************************************************************************** + * Name: remove_escape_codes + * + * Description: + * Remove ANSI Escape Codes from the string. Assumes that buf[len] is 0. + * + * Input Parameters: + * buf - String to be processed + * len - Length of string + * + * Returned Value: + * None + * + ****************************************************************************/ + +static void remove_escape_codes(char *buf, int len) +{ + int i; + int j; + + for (i = 0; i < len; i++) + { + /* Escape Code looks like 0x1b 0x5b 0x4b */ + + if (buf[i] == 0x1b) + { + /* Remove 3 bytes */ + + for (j = i; j + 2 < len; j++) + { + buf[j] = buf[j + 3]; + } + } + } +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: main or lvglterm_main + * + * Description: + * Start an LVGL Terminal that runs interactive commands with NSH Shell. + * NSH Commands are entered through an LVGL Keyboard. NSH Output is + * rendered in an LVGL Widget. + * + * Input Parameters: + * Standard argc and argv + * + * Returned Value: + * Zero on success; a positive, non-zero value on failure. + * + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + int ret; + +#ifdef NEED_BOARDINIT + /* Perform board-specific driver initialization */ + + boardctl(BOARDIOC_INIT, 0); + +#ifdef CONFIG_BOARDCTL_FINALINIT + /* Perform architecture-specific final-initialization (if configured) */ + + boardctl(BOARDIOC_FINALINIT, 0); +#endif +#endif + + /* LVGL initialization */ + + lv_init(); + + /* LVGL port initialization */ + + lv_port_init(); + + /* Create the LVGL Widgets */ + + ret = create_terminal(); + if (ret < 0) + { + return EXIT_FAILURE; + } + + /* Handle LVGL tasks */ + + while (1) + { + uint32_t idle; + idle = lv_timer_handler(); + + /* Minimum sleep of 1ms */ + + idle = idle ? idle : 1; + usleep(idle * 1000); + } + + return EXIT_SUCCESS; +}