This is an automated email from the ASF dual-hosted git repository.

acassis 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 1e19ceb34 examples/webpanel: Add a web panel application to configure 
NuttX
1e19ceb34 is described below

commit 1e19ceb3470057813c46bdf7c6a1c70ad434d8a1
Author: Tiago Medicci <[email protected]>
AuthorDate: Wed Jun 10 08:49:35 2026 -0300

    examples/webpanel: Add a web panel application to configure NuttX
    
    Initial development of the NuttX's Web Panel application. This is
    a self-hosted web page application that enables retrieving system
    info, provides a simple NSH terminal and enables uploading files.
    
    Signed-off-by: Tiago Medicci <[email protected]>
---
 .codespellrc                               |   3 +-
 examples/webpanel/.gitignore               |   2 +
 examples/webpanel/CMakeLists.txt           |  25 ++
 examples/webpanel/Kconfig                  | 109 +++++
 examples/webpanel/Make.defs                |  25 ++
 examples/webpanel/Makefile                 |  94 +++++
 examples/webpanel/cgi_files.c              | 314 ++++++++++++++
 examples/webpanel/cgi_renew.c              |  72 ++++
 examples/webpanel/cgi_sysinfo.c            | 414 +++++++++++++++++++
 examples/webpanel/cgi_upload.c             | 637 +++++++++++++++++++++++++++++
 examples/webpanel/content/www/index.html   | 454 ++++++++++++++++++++
 examples/webpanel/content/www/xterm.css    | 218 ++++++++++
 examples/webpanel/content/www/xterm.min.js |   8 +
 examples/webpanel/webpanel_main.c          | 207 ++++++++++
 examples/webpanel/ws_terminal.c            | 350 ++++++++++++++++
 examples/webpanel/ws_terminal.h            |  63 +++
 16 files changed, 2994 insertions(+), 1 deletion(-)

diff --git a/.codespellrc b/.codespellrc
index 8bcdf2bc7..651916c6a 100644
--- a/.codespellrc
+++ b/.codespellrc
@@ -8,6 +8,7 @@ exclude-file = .codespell-ignore-lines
 # Ignore complete files (e.g. legal text or other immutable material).
 skip =
   LICENSE,
+  examples/webpanel/content/www/xterm.min.js,
 
 # Ignore words list (FTP protocol commands and technical terms)
-ignore-words-list = ALLO, ARCHTYPE, parm
+ignore-words-list = ALLO, ARCHTYPE, parm, shiftIn
diff --git a/examples/webpanel/.gitignore b/examples/webpanel/.gitignore
new file mode 100644
index 000000000..583a0928d
--- /dev/null
+++ b/examples/webpanel/.gitignore
@@ -0,0 +1,2 @@
+# ROMFS build artifacts (generated by Makefile)
+/content
diff --git a/examples/webpanel/CMakeLists.txt b/examples/webpanel/CMakeLists.txt
new file mode 100644
index 000000000..ed47cabbd
--- /dev/null
+++ b/examples/webpanel/CMakeLists.txt
@@ -0,0 +1,25 @@
+# 
##############################################################################
+# apps/examples/webpanel/CMakeLists.txt
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# 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.
+#
+# 
##############################################################################
+
+if(CONFIG_EXAMPLES_WEBPANEL)
+  nuttx_add_application(NAME ${CONFIG_EXAMPLES_WEBPANEL_PROGNAME})
+endif()
diff --git a/examples/webpanel/Kconfig b/examples/webpanel/Kconfig
new file mode 100644
index 000000000..b1392c725
--- /dev/null
+++ b/examples/webpanel/Kconfig
@@ -0,0 +1,109 @@
+#
+# For a description of the syntax of this configuration file,
+# see the file kconfig-language.txt in the NuttX tools repository.
+#
+
+config EXAMPLES_WEBPANEL
+       tristate "Web Panel for device management"
+       default n
+       depends on NET_TCP
+       depends on NETUTILS_THTTPD
+       depends on NETUTILS_LIBWEBSOCKETS
+       depends on NETUTILS_LIBWEBSOCKETS_SERVER
+       depends on FS_BINFS
+       depends on FS_UNIONFS
+       depends on PSEUDOTERM
+       depends on PSEUDOTERM_SUSV1
+       ---help---
+               Enable the Web Panel application, a Tasmota-inspired web
+               interface for NuttX devices. Provides a landing page served
+               via THTTPD from ROMFS, CGI handlers for system info, file
+               management, and a WebSocket-based NSH terminal (powered
+               by libwebsockets).
+
+if EXAMPLES_WEBPANEL
+
+config EXAMPLES_WEBPANEL_PROGNAME
+       string "Program name"
+       default "webpanel"
+
+config EXAMPLES_WEBPANEL_PRIORITY
+       int "Web Panel task priority"
+       default 100
+
+config EXAMPLES_WEBPANEL_STACKSIZE
+       int "Web Panel stack size"
+       default 4096
+
+config EXAMPLES_WEBPANEL_NETIF
+       string "Network interface name"
+       default "eth0"
+       ---help---
+               Name of the network interface used by the web panel for
+               system info queries and DHCP renew (e.g. eth0, wlan0).
+
+config EXAMPLES_WEBPANEL_CGI_SYSINFO_PROGNAME
+       string "CGI sysinfo program name"
+       default "sysinfo"
+
+config EXAMPLES_WEBPANEL_CGI_SYSINFO_PRIORITY
+       int "CGI sysinfo task priority"
+       default 100
+
+config EXAMPLES_WEBPANEL_CGI_SYSINFO_STACKSIZE
+       int "CGI sysinfo stack size"
+       default 4096
+
+config EXAMPLES_WEBPANEL_CGI_FILES_PROGNAME
+       string "CGI files program name"
+       default "files"
+
+config EXAMPLES_WEBPANEL_CGI_FILES_PRIORITY
+       int "CGI files task priority"
+       default 100
+
+config EXAMPLES_WEBPANEL_CGI_FILES_STACKSIZE
+       int "CGI files stack size"
+       default 4096
+
+config EXAMPLES_WEBPANEL_CGI_UPLOAD_PROGNAME
+       string "CGI upload program name"
+       default "upload"
+
+config EXAMPLES_WEBPANEL_CGI_UPLOAD_PRIORITY
+       int "CGI upload task priority"
+       default 100
+
+config EXAMPLES_WEBPANEL_CGI_UPLOAD_STACKSIZE
+       int "CGI upload stack size"
+       default 8192
+
+config EXAMPLES_WEBPANEL_CGI_RENEW_PROGNAME
+       string "CGI DHCP renew program name"
+       default "dhcprenew"
+
+config EXAMPLES_WEBPANEL_CGI_RENEW_PRIORITY
+       int "CGI DHCP renew task priority"
+       default 100
+
+config EXAMPLES_WEBPANEL_CGI_RENEW_STACKSIZE
+       int "CGI DHCP renew stack size"
+       default 4096
+
+config EXAMPLES_WEBPANEL_WS_PORT
+       int "WebSocket terminal TCP port"
+       default 8080
+
+config EXAMPLES_WEBPANEL_WS_PRIORITY
+       int "WebSocket server task priority"
+       default 100
+
+config EXAMPLES_WEBPANEL_WS_STACKSIZE
+       int "WebSocket daemon stack size"
+       default 8192
+
+config EXAMPLES_WEBPANEL_WS_NSH_STACKSIZE
+       int "WebSocket NSH session stack size"
+       default 4096
+
+endif
diff --git a/examples/webpanel/Make.defs b/examples/webpanel/Make.defs
new file mode 100644
index 000000000..bf880eb82
--- /dev/null
+++ b/examples/webpanel/Make.defs
@@ -0,0 +1,25 @@
+############################################################################
+# apps/examples/webpanel/Make.defs
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# 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_WEBPANEL),)
+CONFIGURED_APPS += $(APPDIR)/examples/webpanel
+endif
diff --git a/examples/webpanel/Makefile b/examples/webpanel/Makefile
new file mode 100644
index 000000000..12d6be8e6
--- /dev/null
+++ b/examples/webpanel/Makefile
@@ -0,0 +1,94 @@
+############################################################################
+# apps/examples/webpanel/Makefile
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# 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
+
+# Web Panel application + CGI handlers
+# NuttX multi-program build: space-separated lists, matched by position.
+
+CSRCS   = romfs.c ws_terminal.c
+MAINSRC = webpanel_main.c cgi_sysinfo.c cgi_files.c cgi_upload.c cgi_renew.c
+
+PROGNAME  = $(CONFIG_EXAMPLES_WEBPANEL_PROGNAME) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_SYSINFO_PROGNAME) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_FILES_PROGNAME) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_UPLOAD_PROGNAME) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_RENEW_PROGNAME)
+PRIORITY  = $(CONFIG_EXAMPLES_WEBPANEL_PRIORITY) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_SYSINFO_PRIORITY) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_FILES_PRIORITY) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_UPLOAD_PRIORITY) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_RENEW_PRIORITY)
+STACKSIZE = $(CONFIG_EXAMPLES_WEBPANEL_STACKSIZE) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_SYSINFO_STACKSIZE) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_FILES_STACKSIZE) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_UPLOAD_STACKSIZE) \
+            $(CONFIG_EXAMPLES_WEBPANEL_CGI_RENEW_STACKSIZE)
+
+LWS_DIR = $(APPDIR)/netutils/libwebsockets
+CFLAGS += -I$(LWS_DIR) \
+          -I$(LWS_DIR)/libwebsockets/include
+
+MODULE    = $(CONFIG_EXAMPLES_WEBPANEL)
+
+VPATH   += content
+DEPPATH += --dep-path content
+
+# ROMFS image generation
+# Static web content goes directly into the ROMFS root (no www/ subdir)
+
+WEBPANEL_DIR  = $(APPDIR)/examples/webpanel
+CONTENT_DIR   = $(WEBPANEL_DIR)/content
+ROMFS_DIR     = $(CONTENT_DIR)/romfs
+ROMFS_IMG     = $(CONTENT_DIR)/romfs.img
+ROMFS_SRC     = $(CONTENT_DIR)/romfs.c
+
+WWW_FILES = $(wildcard $(CONTENT_DIR)/www/*)
+
+$(CONTENT_DIR)/.romfs_stamp: $(WWW_FILES)
+       $(Q) mkdir -p $(ROMFS_DIR)
+       $(Q) cp -a $(CONTENT_DIR)/www/* $(ROMFS_DIR)/
+       $(Q) touch $@
+
+$(ROMFS_IMG): $(CONTENT_DIR)/.romfs_stamp
+       $(Q) genromfs -f $@ -d $(ROMFS_DIR) -V "WebPanelROMFS"
+
+$(ROMFS_SRC): $(ROMFS_IMG)
+       $(Q) (cd $(CONTENT_DIR) && \
+             echo "#include <nuttx/compiler.h>" >$@ && \
+             xxd -i romfs.img | \
+             sed -e "s/romfs_img/webpanel_romfs_img/g" | \
+             sed -e "s/^unsigned char/const unsigned char aligned_data(4)/g" 
>>$@)
+
+content/romfs.c: $(ROMFS_SRC)
+
+context::
+
+clean::
+       $(call DELFILE, $(ROMFS_SRC))
+       $(call DELFILE, $(ROMFS_IMG))
+       $(call DELFILE, $(CONTENT_DIR)/.romfs_stamp)
+       $(Q) rm -rf $(ROMFS_DIR)
+
+distclean:: clean
+
+include $(APPDIR)/Application.mk
diff --git a/examples/webpanel/cgi_files.c b/examples/webpanel/cgi_files.c
new file mode 100644
index 000000000..fdc67b868
--- /dev/null
+++ b/examples/webpanel/cgi_files.c
@@ -0,0 +1,314 @@
+/****************************************************************************
+ * apps/examples/webpanel/cgi_files.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#define FILES_DIR "/mnt"
+
+/****************************************************************************
+ * Private Function Prototypes
+ ****************************************************************************/
+
+static void url_decode(char *dst, const char *src, size_t dstsize);
+static const char *get_query_param(const char *qs, const char *key,
+                                   char *val, size_t vallen);
+static void handle_list(void);
+static void handle_delete(const char *name);
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: url_decode
+ *
+ * Description:
+ *   Decode a URL-encoded string into a destination buffer.
+ *
+ * Input Parameters:
+ *   dst     - Destination buffer.
+ *   src     - Source URL-encoded string.
+ *   dstsize - Size of destination buffer.
+ *
+ * Returned Value:
+ *   None.
+ *
+ ****************************************************************************/
+
+static void url_decode(char *dst, const char *src, size_t dstsize)
+{
+  size_t i = 0;
+
+  while (*src && i < dstsize - 1)
+    {
+      if (*src == '%' && src[1] && src[2])
+        {
+          char hex[3];
+
+          hex[0] = src[1];
+          hex[1] = src[2];
+          hex[2] = '\0';
+          dst[i++] = (char)strtol(hex, NULL, 16);
+          src += 3;
+        }
+      else if (*src == '+')
+        {
+          dst[i++] = ' ';
+          src++;
+        }
+      else
+        {
+          dst[i++] = *src++;
+        }
+    }
+
+  dst[i] = '\0';
+}
+
+/****************************************************************************
+ * Name: get_query_param
+ *
+ * Description:
+ *   Find and decode a key=value parameter from the QUERY_STRING.
+ *
+ * Input Parameters:
+ *   qs     - Query string.
+ *   key    - Parameter name to search for.
+ *   val    - Output buffer for decoded value.
+ *   vallen - Size of output buffer.
+ *
+ * Returned Value:
+ *   Pointer to val on success; NULL if not found or invalid input.
+ *
+ ****************************************************************************/
+
+static const char *get_query_param(const char *qs, const char *key,
+                                   char *val, size_t vallen)
+{
+  const char *p;
+  size_t klen;
+
+  if (qs == NULL || key == NULL)
+    {
+      return NULL;
+    }
+
+  klen = strlen(key);
+  p = qs;
+
+  while (*p)
+    {
+      if (strncmp(p, key, klen) == 0 && p[klen] == '=')
+        {
+          const char *start = p + klen + 1;
+          const char *end = strchr(start, '&');
+          size_t len;
+
+          if (end == NULL)
+            {
+              end = start + strlen(start);
+            }
+
+          len = end - start;
+          if (len >= vallen)
+            {
+              len = vallen - 1;
+            }
+
+          url_decode(val, start, len + 1);
+          return val;
+        }
+
+      p = strchr(p, '&');
+      if (p == NULL)
+        {
+          break;
+        }
+
+      p++;
+    }
+
+  return NULL;
+}
+
+/****************************************************************************
+ * Name: handle_list
+ *
+ * Description:
+ *   Emit a JSON response listing files in FILES_DIR.
+ *
+ * Input Parameters:
+ *   None.
+ *
+ * Returned Value:
+ *   None.
+ *
+ ****************************************************************************/
+
+static void handle_list(void)
+{
+  DIR *dir;
+  struct dirent *ent;
+  struct stat st;
+  char path[128];
+  int first = 1;
+
+  puts("Content-type: application/json\r\n"
+       "\r\n");
+
+  printf("{\"files\":[");
+
+  dir = opendir(FILES_DIR);
+  if (dir != NULL)
+    {
+      while ((ent = readdir(dir)) != NULL)
+        {
+          if (ent->d_name[0] == '.')
+            {
+              continue;
+            }
+
+          snprintf(path, sizeof(path), "%s/%s", FILES_DIR, ent->d_name);
+
+          if (!first)
+            {
+              printf(",");
+            }
+
+          first = 0;
+
+          if (stat(path, &st) == 0)
+            {
+              printf("{\"name\":\"%s\",\"size\":%ld}",
+                     ent->d_name, (long)st.st_size);
+            }
+          else
+            {
+              printf("{\"name\":\"%s\",\"size\":0}", ent->d_name);
+            }
+        }
+
+      closedir(dir);
+    }
+
+  printf("]}\n");
+}
+
+/****************************************************************************
+ * Name: handle_delete
+ *
+ * Description:
+ *   Delete the requested file from FILES_DIR and emit a JSON response.
+ *
+ * Input Parameters:
+ *   name - File name to delete.
+ *
+ * Returned Value:
+ *   None.
+ *
+ ****************************************************************************/
+
+static void handle_delete(const char *name)
+{
+  char path[128];
+
+  if (name == NULL || name[0] == '\0' || strchr(name, '/') != NULL)
+    {
+      puts("Content-type: application/json\r\n"
+           "Status: 400\r\n"
+           "\r\n");
+      printf("{\"error\":\"Invalid filename\"}\n");
+      return;
+    }
+
+  snprintf(path, sizeof(path), "%s/%s", FILES_DIR, name);
+
+  if (unlink(path) == 0)
+    {
+      puts("Content-type: application/json\r\n"
+           "\r\n");
+      printf("{\"ok\":true}\n");
+    }
+  else
+    {
+      puts("Content-type: application/json\r\n"
+           "Status: 404\r\n"
+           "\r\n");
+      printf("{\"error\":\"File not found\"}\n");
+    }
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: files_main
+ *
+ * Description:
+ *   Entry point for file list/delete CGI handling.
+ *
+ * Input Parameters:
+ *   argc - Number of arguments.
+ *   argv - Argument vector.
+ *
+ * Returned Value:
+ *   Zero (OK).
+ *
+ ****************************************************************************/
+
+int files_main(int argc, FAR char *argv[])
+{
+  const char *qs;
+  char action[16];
+  char name[64];
+
+  qs = getenv("QUERY_STRING");
+
+  if (qs != NULL && get_query_param(qs, "action", action, sizeof(action)))
+    {
+      if (strcmp(action, "delete") == 0)
+        {
+          get_query_param(qs, "name", name, sizeof(name));
+          handle_delete(name);
+          return 0;
+        }
+    }
+
+  handle_list();
+  return 0;
+}
diff --git a/examples/webpanel/cgi_renew.c b/examples/webpanel/cgi_renew.c
new file mode 100644
index 000000000..098995094
--- /dev/null
+++ b/examples/webpanel/cgi_renew.c
@@ -0,0 +1,72 @@
+/****************************************************************************
+ * apps/examples/webpanel/cgi_renew.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <stdio.h>
+
+#include "netutils/netlib.h"
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: dhcprenew_main
+ *
+ * Description:
+ *   CGI entry point that renews the IPv4 address on the configured
+ *   interface and returns the result as JSON.
+ *
+ * Input Parameters:
+ *   argc - Number of arguments.
+ *   argv - Argument vector.
+ *
+ * Returned Value:
+ *   Zero on success; one on failure.
+ *
+ ****************************************************************************/
+
+int dhcprenew_main(int argc, FAR char *argv[])
+{
+  int ret;
+
+  ret = netlib_obtain_ipv4addr(CONFIG_EXAMPLES_WEBPANEL_NETIF);
+
+  puts("Content-type: application/json\r\n"
+       "\r\n");
+
+  if (ret >= 0)
+    {
+      puts("{\"status\":\"ok\"}\n");
+    }
+  else
+    {
+      printf("{\"status\":\"error\",\"code\":%d}\n", ret);
+    }
+
+  return ret < 0 ? 1 : 0;
+}
diff --git a/examples/webpanel/cgi_sysinfo.c b/examples/webpanel/cgi_sysinfo.c
new file mode 100644
index 000000000..619eb4cf1
--- /dev/null
+++ b/examples/webpanel/cgi_sysinfo.c
@@ -0,0 +1,414 @@
+/****************************************************************************
+ * apps/examples/webpanel/cgi_sysinfo.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <time.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/utsname.h>
+#include <net/if.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+
+/****************************************************************************
+ * Private Function Prototypes
+ ****************************************************************************/
+
+static void get_version(char *ver, size_t vlen,
+                        char *build, size_t blen,
+                        char *board, size_t boardlen);
+static void get_arch(char *arch, size_t archlen);
+static unsigned long get_uptime(char *buf, size_t len);
+static void get_net_info(char *ip, size_t iplen,
+                         char *mask, size_t masklen,
+                         char *gw, size_t gwlen,
+                         char *mac, size_t maclen);
+static int count_files(const char *path);
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: get_version
+ *
+ * Description:
+ *   Read version, build hash, and board information from /proc/version.
+ *
+ * Input Parameters:
+ *   ver      - Output buffer for version string.
+ *   vlen     - Size of ver buffer.
+ *   build    - Output buffer for build/hash string.
+ *   blen     - Size of build buffer.
+ *   board    - Output buffer for board string.
+ *   boardlen - Size of board buffer.
+ *
+ * Returned Value:
+ *   None.
+ *
+ ****************************************************************************/
+
+static void get_version(char *ver, size_t vlen,
+                        char *build, size_t blen,
+                        char *board, size_t boardlen)
+{
+  FILE *fp;
+  char line[256];
+  char *p;
+  char *sp;
+  char *last;
+  char *tok;
+  size_t n;
+
+  strncpy(ver,   "unknown", vlen);
+  strncpy(build, "unknown", blen);
+  strncpy(board, "unknown", boardlen);
+
+  fp = fopen("/proc/version", "r");
+  if (fp == NULL)
+    {
+      return;
+    }
+
+  if (fgets(line, sizeof(line), fp) != NULL)
+    {
+      /* Format: "NuttX version X.Y.Z HASH DATE TIME BOARD:CONFIG" */
+
+      p = strstr(line, "version ");
+      if (p != NULL)
+        {
+          p += 8;
+
+          /* version token */
+
+          sp = strchr(p, ' ');
+          if (sp != NULL)
+            {
+              n = sp - p;
+              if (n >= vlen)
+                {
+                  n = vlen - 1;
+                }
+
+              memcpy(ver, p, n);
+              ver[n] = '\0';
+
+              /* build (git hash) token */
+
+              p = sp + 1;
+              sp = strchr(p, ' ');
+              if (sp != NULL)
+                {
+                  n = sp - p;
+                  if (n >= blen)
+                    {
+                      n = blen - 1;
+                    }
+
+                  memcpy(build, p, n);
+                  build[n] = '\0';
+                }
+            }
+        }
+
+      /* Board name is the last whitespace-separated token; trim :config */
+
+      last = NULL;
+      tok  = strtok(line, " \t\r\n");
+      while (tok != NULL)
+        {
+          last = tok;
+          tok  = strtok(NULL, " \t\r\n");
+        }
+
+      if (last != NULL)
+        {
+          char *colon = strchr(last, ':');
+          if (colon != NULL)
+            {
+              *colon = '\0';
+            }
+
+          strncpy(board, last, boardlen - 1);
+          board[boardlen - 1] = '\0';
+        }
+    }
+
+  fclose(fp);
+}
+
+/****************************************************************************
+ * Name: get_arch
+ *
+ * Description:
+ *   Query architecture information via uname().
+ *
+ * Input Parameters:
+ *   arch    - Output buffer for architecture string.
+ *   archlen - Size of arch buffer.
+ *
+ * Returned Value:
+ *   None.
+ *
+ ****************************************************************************/
+
+static void get_arch(char *arch, size_t archlen)
+{
+  struct utsname uts;
+
+  strncpy(arch, "unknown", archlen);
+
+  if (uname(&uts) == 0)
+    {
+      strncpy(arch, uts.machine, archlen - 1);
+      arch[archlen - 1] = '\0';
+    }
+}
+
+/****************************************************************************
+ * Name: get_uptime
+ *
+ * Description:
+ *   Read system uptime based on CLOCK_MONOTONIC and format it.
+ *
+ * Input Parameters:
+ *   buf - Output buffer for formatted uptime.
+ *   len - Size of buf.
+ *
+ * Returned Value:
+ *   Uptime in seconds; zero on error.
+ *
+ ****************************************************************************/
+
+static unsigned long get_uptime(char *buf, size_t len)
+{
+  struct timespec ts;
+
+  if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)
+    {
+      unsigned long secs = ts.tv_sec;
+      unsigned int days = secs / 86400;
+      unsigned int hrs  = (secs % 86400) / 3600;
+      unsigned int mins = (secs % 3600) / 60;
+      unsigned int s    = secs % 60;
+
+      if (days > 0)
+        {
+          snprintf(buf, len, "%ud %02u:%02u:%02u", days, hrs, mins, s);
+        }
+      else
+        {
+          snprintf(buf, len, "%02u:%02u:%02u", hrs, mins, s);
+        }
+
+      return secs;
+    }
+
+  snprintf(buf, len, "unknown");
+  return 0;
+}
+
+/****************************************************************************
+ * Name: get_net_info
+ *
+ * Description:
+ *   Query IP, netmask, gateway, and MAC for the configured interface.
+ *
+ * Input Parameters:
+ *   ip      - Output buffer for IPv4 address.
+ *   iplen   - Size of ip buffer.
+ *   mask    - Output buffer for netmask.
+ *   masklen - Size of mask buffer.
+ *   gw      - Output buffer for gateway.
+ *   gwlen   - Size of gw buffer.
+ *   mac     - Output buffer for MAC address.
+ *   maclen  - Size of mac buffer.
+ *
+ * Returned Value:
+ *   None.
+ *
+ ****************************************************************************/
+
+static void get_net_info(char *ip, size_t iplen,
+                         char *mask, size_t masklen,
+                         char *gw, size_t gwlen,
+                         char *mac, size_t maclen)
+{
+  int sockfd;
+  struct ifreq ifr;
+
+  strncpy(ip, "N/A", iplen);
+  strncpy(mask, "N/A", masklen);
+  strncpy(gw, "N/A", gwlen);
+  strncpy(mac, "N/A", maclen);
+
+  sockfd = socket(AF_INET, SOCK_DGRAM, 0);
+  if (sockfd < 0)
+    {
+      return;
+    }
+
+  memset(&ifr, 0, sizeof(ifr));
+  strncpy(ifr.ifr_name, CONFIG_EXAMPLES_WEBPANEL_NETIF, IFNAMSIZ);
+
+  if (ioctl(sockfd, SIOCGIFADDR, &ifr) == 0)
+    {
+      struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_addr;
+      inet_ntop(AF_INET, &sa->sin_addr, ip, iplen);
+    }
+
+  if (ioctl(sockfd, SIOCGIFNETMASK, &ifr) == 0)
+    {
+      struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_netmask;
+      inet_ntop(AF_INET, &sa->sin_addr, mask, masklen);
+    }
+
+  if (ioctl(sockfd, SIOCGIFDSTADDR, &ifr) == 0)
+    {
+      struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_dstaddr;
+      inet_ntop(AF_INET, &sa->sin_addr, gw, gwlen);
+    }
+
+  if (ioctl(sockfd, SIOCGIFHWADDR, &ifr) == 0)
+    {
+      unsigned char *hw = (unsigned char *)ifr.ifr_hwaddr.sa_data;
+      snprintf(mac, maclen, "%02x:%02x:%02x:%02x:%02x:%02x",
+               hw[0], hw[1], hw[2], hw[3], hw[4], hw[5]);
+    }
+
+  close(sockfd);
+}
+
+/****************************************************************************
+ * Name: count_files
+ *
+ * Description:
+ *   Count visible files in a directory.
+ *
+ * Input Parameters:
+ *   path - Directory path.
+ *
+ * Returned Value:
+ *   Number of visible entries.
+ *
+ ****************************************************************************/
+
+static int count_files(const char *path)
+{
+  DIR *dir;
+  struct dirent *ent;
+  int count = 0;
+
+  dir = opendir(path);
+  if (dir == NULL)
+    {
+      return 0;
+    }
+
+  while ((ent = readdir(dir)) != NULL)
+    {
+      if (ent->d_name[0] != '.')
+        {
+          count++;
+        }
+    }
+
+  closedir(dir);
+  return count;
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: sysinfo_main
+ *
+ * Description:
+ *   CGI entry point that returns board and runtime information as JSON.
+ *
+ * Input Parameters:
+ *   argc - Number of arguments.
+ *   argv - Argument vector.
+ *
+ * Returned Value:
+ *   Zero (OK).
+ *
+ ****************************************************************************/
+
+int sysinfo_main(int argc, FAR char *argv[])
+{
+  char version[16];
+  char build[16];
+  char board[64];
+  char arch[32];
+  char uptime[32];
+  char ip[16];
+  char mask[16];
+  char gw[16];
+  char mac[24];
+  unsigned long uptime_sec;
+  int nfiles;
+
+  get_version(version, sizeof(version), build, sizeof(build),
+              board, sizeof(board));
+  get_arch(arch, sizeof(arch));
+  uptime_sec = get_uptime(uptime, sizeof(uptime));
+  get_net_info(ip, sizeof(ip), mask, sizeof(mask),
+               gw, sizeof(gw), mac, sizeof(mac));
+  nfiles = count_files("/mnt");
+
+  puts("Content-type: application/json\r\n"
+       "\r\n");
+
+  printf("{"
+         "\"chip\":\"" CONFIG_ARCH_CHIP "\","
+         "\"board\":\"%s\","
+         "\"os\":\"NuttX\","
+         "\"version\":\"%s\","
+         "\"build\":\"%s\","
+         "\"arch\":\"%s\","
+         "\"ifname\":\"" CONFIG_EXAMPLES_WEBPANEL_NETIF "\","
+         "\"ip\":\"%s\","
+         "\"mask\":\"%s\","
+         "\"gw\":\"%s\","
+         "\"mac\":\"%s\","
+         "\"uptime\":\"%s\","
+         "\"uptime_sec\":%lu,"
+         "\"files\":%d"
+         "}\n",
+         board, version, build, arch,
+         ip, mask, gw, mac,
+         uptime, uptime_sec, nfiles);
+
+  return 0;
+}
diff --git a/examples/webpanel/cgi_upload.c b/examples/webpanel/cgi_upload.c
new file mode 100644
index 000000000..4dfd137ad
--- /dev/null
+++ b/examples/webpanel/cgi_upload.c
@@ -0,0 +1,637 @@
+/****************************************************************************
+ * apps/examples/webpanel/cgi_upload.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#define UPLOAD_DIR   "/mnt"
+#define BUF_SIZE     512
+#define MAX_FILENAME 64
+#define MAX_BOUNDARY 80
+
+/****************************************************************************
+ * Private Function Prototypes
+ ****************************************************************************/
+
+static void cgi_error(int status, const char *msg);
+static void cgi_ok(const char *filename);
+static int extract_boundary(const char *content_type, char *boundary,
+                            size_t blen);
+static int extract_filename(const char *line, char *name, size_t nlen);
+static size_t read_stdin(char *buf, size_t n);
+static char *memmem_local(const char *haystack, size_t hlen,
+                          const char *needle, size_t nlen);
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: cgi_error
+ *
+ * Description:
+ *   Emit a JSON error response with HTTP status.
+ *
+ * Input Parameters:
+ *   status - HTTP status code.
+ *   msg    - Error message string.
+ *
+ * Returned Value:
+ *   None.
+ *
+ ****************************************************************************/
+
+static void cgi_error(int status, const char *msg)
+{
+  printf("Content-type: application/json\r\n"
+         "Status: %d\r\n"
+         "\r\n"
+         "{\"error\":\"%s\"}\n", status, msg);
+}
+
+/****************************************************************************
+ * Name: cgi_ok
+ *
+ * Description:
+ *   Emit a JSON success response.
+ *
+ * Input Parameters:
+ *   filename - Uploaded file name.
+ *
+ * Returned Value:
+ *   None.
+ *
+ ****************************************************************************/
+
+static void cgi_ok(const char *filename)
+{
+  printf("Content-type: application/json\r\n"
+         "\r\n"
+         "{\"ok\":true,\"name\":\"%s\"}\n", filename);
+}
+
+/****************************************************************************
+ * Name: extract_boundary
+ *
+ * Description:
+ *   Extract multipart boundary from CONTENT_TYPE.
+ *
+ * Input Parameters:
+ *   content_type - CONTENT_TYPE value.
+ *   boundary     - Output buffer for boundary token.
+ *   blen         - Size of boundary buffer.
+ *
+ * Returned Value:
+ *   Zero on success; negated value on failure.
+ *
+ ****************************************************************************/
+
+static int extract_boundary(const char *content_type, char *boundary,
+                            size_t blen)
+{
+  const char *p;
+  char *q;
+  size_t len;
+
+  p = strstr(content_type, "boundary=");
+  if (p == NULL)
+    {
+      return -1;
+    }
+
+  p += 9;
+
+  /* Skip optional quotes */
+
+  if (*p == '"')
+    {
+      p++;
+    }
+
+  strncpy(boundary, p, blen - 1);
+  boundary[blen - 1] = '\0';
+
+  /* Remove trailing quote if present */
+
+  q = strchr(boundary, '"');
+  if (q != NULL)
+    {
+      *q = '\0';
+    }
+
+  /* Remove trailing whitespace/CR/LF */
+
+  len = strlen(boundary);
+  while (len > 0 &&
+         (boundary[len - 1] == '\r' || boundary[len - 1] == '\n' ||
+          boundary[len - 1] == ' '))
+    {
+      boundary[--len] = '\0';
+    }
+
+  return len > 0 ? 0 : -1;
+}
+
+/****************************************************************************
+ * Name: extract_filename
+ *
+ * Description:
+ *   Extract a safe filename from a Content-Disposition header line.
+ *
+ * Input Parameters:
+ *   line - Header line.
+ *   name - Output buffer for filename.
+ *   nlen - Size of name buffer.
+ *
+ * Returned Value:
+ *   Zero on success; negated value on failure.
+ *
+ ****************************************************************************/
+
+static int extract_filename(const char *line, char *name, size_t nlen)
+{
+  const char *p;
+  const char *end;
+  size_t len;
+
+  p = strstr(line, "filename=\"");
+  if (p == NULL)
+    {
+      return -1;
+    }
+
+  p += 10;
+  end = strchr(p, '"');
+  if (end == NULL)
+    {
+      return -1;
+    }
+
+  len = end - p;
+  if (len == 0 || len >= nlen)
+    {
+      return -1;
+    }
+
+  /* Reject path separators in filename */
+
+  if (memchr(p, '/', len) != NULL || memchr(p, '\\', len) != NULL)
+    {
+      return -1;
+    }
+
+  memcpy(name, p, len);
+  name[len] = '\0';
+  return 0;
+}
+
+/****************************************************************************
+ * Name: read_stdin
+ *
+ * Description:
+ *   Read exactly n bytes from standard input unless EOF/error occurs.
+ *
+ * Input Parameters:
+ *   buf - Destination buffer.
+ *   n   - Requested number of bytes.
+ *
+ * Returned Value:
+ *   Number of bytes actually read.
+ *
+ ****************************************************************************/
+
+static size_t read_stdin(char *buf, size_t n)
+{
+  size_t total = 0;
+  while (total < n)
+    {
+      ssize_t r = read(STDIN_FILENO, buf + total, n - total);
+      if (r <= 0)
+        {
+          break;
+        }
+
+      total += r;
+    }
+
+  return total;
+}
+
+/****************************************************************************
+ * Name: memmem_local
+ *
+ * Description:
+ *   Find a byte sequence inside a bounded memory region.
+ *
+ * Input Parameters:
+ *   haystack - Input buffer.
+ *   hlen     - Size of haystack.
+ *   needle   - Pattern to search.
+ *   nlen     - Size of needle.
+ *
+ * Returned Value:
+ *   Pointer to first match; NULL if not found.
+ *
+ ****************************************************************************/
+
+static char *memmem_local(const char *haystack, size_t hlen,
+                          const char *needle, size_t nlen)
+{
+  const char *p;
+  if (nlen == 0)
+    {
+      return (char *)haystack;
+    }
+
+  if (hlen < nlen)
+    {
+      return NULL;
+    }
+
+  p = haystack;
+  while (p <= haystack + hlen - nlen)
+    {
+      if (memcmp(p, needle, nlen) == 0)
+        {
+          return (char *)p;
+        }
+
+      p++;
+    }
+
+  return NULL;
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: upload_main
+ *
+ * Description:
+ *   CGI entry point for multipart file upload.
+ *
+ * Input Parameters:
+ *   argc - Number of arguments.
+ *   argv - Argument vector.
+ *
+ * Returned Value:
+ *   Zero (OK).
+ *
+ ****************************************************************************/
+
+int upload_main(int argc, FAR char *argv[])
+{
+  const char *content_type;
+  const char *content_length_str;
+  char boundary_raw[MAX_BOUNDARY];
+  char boundary[MAX_BOUNDARY + 4];
+  size_t boundary_len;
+  char filename[MAX_FILENAME];
+  char filepath[MAX_FILENAME + 8];
+  char buf[BUF_SIZE];
+  size_t content_length;
+  size_t total_read;
+  int fd = -1;
+  int state;
+  char linebuf[256];
+  size_t linepos;
+  int headers_done;
+
+  content_type = getenv("CONTENT_TYPE");
+  content_length_str = getenv("CONTENT_LENGTH");
+
+  if (content_type == NULL || content_length_str == NULL)
+    {
+      cgi_error(400, "Missing Content-Type or Content-Length");
+      return 0;
+    }
+
+  content_length = strtoul(content_length_str, NULL, 10);
+  if (content_length == 0 || content_length > 1024 * 1024)
+    {
+      cgi_error(400, "Invalid content length (max 1MB)");
+      return 0;
+    }
+
+  if (extract_boundary(content_type, boundary_raw, sizeof(boundary_raw)) < 0)
+    {
+      cgi_error(400, "No boundary in Content-Type");
+      return 0;
+    }
+
+  /* Multipart boundaries in the body are prefixed with "--" */
+
+  snprintf(boundary, sizeof(boundary), "--%s", boundary_raw);
+  boundary_len = strlen(boundary);
+
+  /* State machine for multipart parsing:
+   * 0 = looking for first boundary
+   * 1 = reading headers after boundary
+   * 2 = reading file data
+   * 3 = done
+   */
+
+  state = 0;
+  total_read = 0;
+  filename[0] = '\0';
+  linepos = 0;
+  headers_done = 0;
+
+  /* Read the entire POST body in chunks and process inline.
+   * We accumulate a line buffer for header parsing, and stream
+   * file data directly to disk.
+   */
+
+  while (total_read < content_length && state != 3)
+    {
+      size_t toread = content_length - total_read;
+      size_t nread;
+
+      if (toread > sizeof(buf))
+        {
+          toread = sizeof(buf);
+        }
+
+      nread = read_stdin(buf, toread);
+      if (nread == 0)
+        {
+          break;
+        }
+
+      total_read += nread;
+
+      size_t i = 0;
+      while (i < nread && state != 3)
+        {
+          switch (state)
+            {
+              case 0:
+
+                /* Accumulate until we find the first boundary line */
+
+                while (i < nread)
+                  {
+                    if (buf[i] == '\n')
+                      {
+                        linebuf[linepos] = '\0';
+
+                        /* Strip trailing \r */
+
+                        if (linepos > 0 && linebuf[linepos - 1] == '\r')
+                          {
+                            linebuf[--linepos] = '\0';
+                          }
+
+                        if (strncmp(linebuf, boundary, boundary_len) == 0)
+                          {
+                            state = 1;
+                            headers_done = 0;
+                            linepos = 0;
+                            i++;
+                            break;
+                          }
+
+                        linepos = 0;
+                        i++;
+                      }
+                    else
+                      {
+                        if (linepos < sizeof(linebuf) - 1)
+                          {
+                            linebuf[linepos++] = buf[i];
+                          }
+
+                        i++;
+                      }
+                  }
+
+                break;
+
+              case 1:
+                /* Parse headers after boundary, looking for
+                 * Content-Disposition with filename.
+                 * Headers end with an empty line.
+                 */
+
+                while (i < nread && !headers_done)
+                  {
+                    if (buf[i] == '\n')
+                      {
+                        linebuf[linepos] = '\0';
+
+                        if (linepos > 0 && linebuf[linepos - 1] == '\r')
+                          {
+                            linebuf[--linepos] = '\0';
+                          }
+
+                        if (linepos == 0)
+                          {
+                            /* Empty line = end of headers */
+
+                            headers_done = 1;
+
+                            if (filename[0] == '\0')
+                              {
+                                cgi_error(400, "No filename in upload");
+                                if (fd >= 0)
+                                  {
+                                    close(fd);
+                                  }
+
+                                return 0;
+                              }
+
+                            snprintf(filepath, sizeof(filepath), "%s/%s",
+                                     UPLOAD_DIR, filename);
+
+                            fd = open(filepath,
+                                      O_WRONLY | O_CREAT | O_TRUNC,
+                                      0666);
+                            if (fd < 0)
+                              {
+                                cgi_error(500, "Cannot create file");
+                                return 0;
+                              }
+
+                            state = 2;
+                            i++;
+                            break;
+                          }
+
+                        if (strstr(linebuf, "Content-Disposition") != NULL)
+                          {
+                            extract_filename(linebuf, filename,
+                                             sizeof(filename));
+                          }
+
+                        linepos = 0;
+                        i++;
+                      }
+                    else
+                      {
+                        if (linepos < sizeof(linebuf) - 1)
+                          {
+                            linebuf[linepos++] = buf[i];
+                          }
+
+                        i++;
+                      }
+                  }
+
+                break;
+
+              case 2:
+              {
+                /* Write file data. We need to detect the closing boundary
+                 * which appears as "\r\n--BOUNDARY" in the stream.
+                   * Buffer the last boundary_len+4 bytes to check for the
+                   * boundary.
+                 */
+
+                size_t remaining = nread - i;
+                char *bnd;
+
+                bnd = memmem_local(buf + i, remaining,
+                                   boundary, boundary_len);
+                if (bnd != NULL)
+                  {
+                    /* Found boundary. Write everything before it,
+                     * minus the preceding \r\n.
+                     */
+
+                    size_t datalen = bnd - (buf + i);
+                    if (datalen >= 2)
+                      {
+                        datalen -= 2;
+                      }
+
+                    if (datalen > 0)
+                      {
+                        write(fd, buf + i, datalen);
+                      }
+
+                    close(fd);
+                    fd = -1;
+                    state = 3;
+                    break;
+                  }
+                else
+                  {
+                    /* No boundary in this chunk. Write data but hold back
+                     * enough bytes to avoid splitting a boundary across
+                     * chunks.
+                     */
+
+                    size_t safe;
+
+                    if (remaining > boundary_len + 4)
+                      {
+                        safe = remaining - boundary_len - 4;
+                      }
+                    else
+                      {
+                        safe = 0;
+                      }
+
+                    if (safe > 0)
+                      {
+                        write(fd, buf + i, safe);
+                        i += safe;
+                      }
+                    else
+                      {
+                        /* Not enough data to be safe. This means we're near
+                         * the end of a chunk and need more data. Just write
+                         * what we have and hope the boundary comes in the
+                         * next read. For simplicity, write it all - worst
+                         * case we include a few extra bytes at end of file
+                         * which will be fixed when the boundary is found.
+                         */
+
+                        write(fd, buf + i, remaining);
+                        i = nread;
+                      }
+                  }
+
+                break;
+              }
+            }
+        }
+    }
+
+  /* Drain any remaining data from stdin */
+
+  while (total_read < content_length)
+    {
+      size_t toread = content_length - total_read;
+      size_t nread;
+
+      if (toread > sizeof(buf))
+        {
+          toread = sizeof(buf);
+        }
+
+      nread = read_stdin(buf, toread);
+      if (nread == 0)
+        {
+          break;
+        }
+
+      total_read += nread;
+    }
+
+  if (fd >= 0)
+    {
+      close(fd);
+    }
+
+  if (state == 3 && filename[0] != '\0')
+    {
+      cgi_ok(filename);
+    }
+  else if (filename[0] != '\0')
+    {
+      cgi_ok(filename);
+    }
+  else
+    {
+      cgi_error(400, "Upload incomplete or no file received");
+    }
+
+  return 0;
+}
diff --git a/examples/webpanel/content/www/index.html 
b/examples/webpanel/content/www/index.html
new file mode 100644
index 000000000..7d4928e31
--- /dev/null
+++ b/examples/webpanel/content/www/index.html
@@ -0,0 +1,454 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>NuttX Web Panel</title>
+<link rel="stylesheet" href="/xterm.css">
+<style>
+*{box-sizing:border-box;margin:0;padding:0}
+body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
+  background:#1a1a2e;color:#e0e0e0;min-height:100vh}
+a{color:#5dade2;text-decoration:none}
+a:hover{text-decoration:underline}
+
+.hdr{background:#16213e;padding:12px 20px;border-bottom:2px solid #0f3460;
+  display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap}
+.hdr h1{font-size:18px;color:#e94560;display:flex;align-items:center;gap:8px}
+.hdr .chip{font-size:11px;background:#0f3460;color:#7ec8e3;padding:2px 8px;
+  border-radius:10px}
+.hdr .uptime{font-size:11px;color:#8899aa}
+
+nav{background:#0f1b33;display:flex;gap:0;overflow-x:auto}
+nav a{padding:10px 18px;font-size:13px;color:#8899aa;border-bottom:2px solid 
transparent;
+  white-space:nowrap;transition:all .2s}
+nav a:hover,nav 
a.act{color:#e94560;border-bottom-color:#e94560;text-decoration:none;
+  background:rgba(233,69,96,.06)}
+
+.page{display:none;padding:16px 20px;max-width:1200px;margin:0 auto}
+.page.vis{display:block}
+
+.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
+.card{background:#16213e;border:1px solid 
#0f3460;border-radius:8px;padding:16px;
+  transition:border-color .2s}
+.card:hover{border-color:#e94560}
+.card h3{font-size:14px;color:#e94560;margin-bottom:10px;display:flex;
+  align-items:center;gap:6px}
+.card .ico{font-size:18px}
+.row{display:flex;justify-content:space-between;padding:4px 0;font-size:12px;
+  border-bottom:1px solid rgba(255,255,255,.04)}
+.row .lbl{color:#8899aa}
+.row .val{color:#c0d0e0;font-family:monospace}
+
+.badge{display:inline-block;padding:1px 7px;border-radius:8px;font-size:10px;
+  font-weight:600}
+.badge-ok{background:#1e4d2b;color:#4ade80}
+.badge-warn{background:#4d3e1e;color:#fbbf24}
+
+.term-wrap{background:#0d1117;border:1px solid #0f3460;border-radius:8px;
+  padding:8px;height:360px;overflow:hidden;position:relative}
+.term-placeholder{display:flex;align-items:center;justify-content:center;
+  height:100%;color:#556;font-size:13px;flex-direction:column;gap:8px}
+
+.fm-toolbar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center}
+.fm-path{background:#0d1117;border:1px solid 
#0f3460;border-radius:4px;padding:6px 10px;
+  color:#c0d0e0;font-family:monospace;font-size:12px;flex:1;min-width:120px}
+.btn{background:#e94560;color:#fff;border:none;padding:7px 
16px;border-radius:4px;
+  cursor:pointer;font-size:12px;font-weight:600;transition:background .2s}
+.btn:hover{background:#c73852}
+.btn-sec{background:#0f3460;color:#7ec8e3}
+.btn-sec:hover{background:#1a4a80}
+
+.fm-list{background:#0d1117;border:1px solid 
#0f3460;border-radius:8px;overflow:hidden}
+.fm-item{display:flex;align-items:center;padding:8px 12px;font-size:12px;
+  border-bottom:1px solid rgba(255,255,255,.04);gap:10px}
+.fm-item:hover{background:rgba(233,69,96,.04)}
+.fm-item .name{flex:1;font-family:monospace;color:#c0d0e0}
+.fm-item .sz{color:#667;min-width:60px;text-align:right}
+.fm-item .acts{display:flex;gap:6px}
+.fm-item .acts button{background:none;border:1px solid #0f3460;color:#8899aa;
+  padding:2px 8px;border-radius:3px;cursor:pointer;font-size:11px}
+.fm-item .acts button:hover{border-color:#e94560;color:#e94560}
+.fm-empty{padding:24px;text-align:center;color:#556;font-size:13px}
+
+.upload-zone{border:2px dashed 
#0f3460;border-radius:8px;padding:24px;text-align:center;
+  color:#556;font-size:13px;cursor:pointer;transition:border-color 
.2s;margin-top:12px}
+.upload-zone:hover,.upload-zone.drag{border-color:#e94560;color:#8899aa}
+.upload-zone input{display:none}
+
+.net-if{margin-bottom:12px}
+.net-if h4{font-size:12px;color:#7ec8e3;margin-bottom:6px}
+
+.toast{position:fixed;bottom:20px;right:20px;background:#16213e;border:1px 
solid #0f3460;
+  border-radius:8px;padding:10px 16px;font-size:12px;color:#c0d0e0;
+  transform:translateY(80px);opacity:0;transition:all .3s;z-index:999}
+.toast.show{transform:translateY(0);opacity:1}
+.toast.err{border-color:#e94560}
+</style>
+</head>
+<body>
+
+<div class="hdr">
+  <h1>NuttX Web Panel <span class="chip" id="chip">--</span></h1>
+  <span class="uptime" id="uptime">Uptime: --</span>
+</div>
+
+<nav id="nav">
+  <a href="#home" class="act" data-p="home">Home</a>
+  <a href="#terminal" data-p="terminal">Terminal</a>
+  <a href="#files" data-p="files">Files</a>
+  <a href="#network" data-p="network">Network</a>
+</nav>
+
+<!-- HOME -->
+<div class="page vis" id="p-home">
+  <div class="grid">
+    <div class="card">
+      <h3><span class="ico">&#9881;</span> System</h3>
+      <div class="row"><span class="lbl">Board</span><span class="val" 
id="si-board">--</span></div>
+      <div class="row"><span class="lbl">OS</span><span class="val" 
id="si-os">NuttX</span></div>
+      <div class="row"><span class="lbl">Version</span><span class="val" 
id="si-ver">--</span></div>
+      <div class="row"><span class="lbl">Build</span><span class="val" 
id="si-build">--</span></div>
+      <div class="row"><span class="lbl">Arch</span><span class="val" 
id="si-arch">--</span></div>
+    </div>
+    <div class="card">
+      <h3><span class="ico">&#128268;</span> Network</h3>
+      <div class="row"><span class="lbl">Interface</span><span class="val" 
id="si-ifname">--</span></div>
+      <div class="row"><span class="lbl">IP</span><span class="val" 
id="si-ip">--</span></div>
+      <div class="row"><span class="lbl">Mask</span><span class="val" 
id="si-mask">--</span></div>
+      <div class="row"><span class="lbl">Gateway</span><span class="val" 
id="si-gw">--</span></div>
+      <div class="row"><span class="lbl">MAC</span><span class="val" 
id="si-mac">--</span></div>
+    </div>
+    <div class="card">
+      <h3><span class="ico">&#128190;</span> Storage</h3>
+      <div class="row"><span class="lbl">SmartFS</span><span 
class="val">/mnt</span></div>
+      <div class="row"><span class="lbl">Web Root</span><span 
class="val">/data/www (ROMFS)</span></div>
+      <div class="row"><span class="lbl">Files</span><span class="val" 
id="si-files">--</span></div>
+    </div>
+    <div class="card">
+      <h3><span class="ico">&#9889;</span> Quick Actions</h3>
+      <div style="display:flex;flex-direction:column;gap:8px;margin-top:4px">
+        <button class="btn" onclick="showPage('terminal')">Open 
Terminal</button>
+        <button class="btn btn-sec" onclick="showPage('files')">Manage 
Files</button>
+        <button class="btn btn-sec" onclick="fetchSysInfo()">Refresh 
Info</button>
+      </div>
+    </div>
+  </div>
+</div>
+
+<!-- TERMINAL -->
+<div class="page" id="p-terminal">
+  <div class="card" style="padding:12px">
+    <h3><span class="ico">&#9000;</span> NSH Terminal</h3>
+    <div class="fm-toolbar" style="margin-bottom:8px">
+      <button class="btn btn-sec" id="term-connect" 
onclick="connectTerminal()">Connect</button>
+      <button class="btn btn-sec" id="term-disc" 
onclick="disconnectTerminal()" style="display:none">Disconnect</button>
+      <span id="term-status" 
style="font-size:11px;color:#8899aa;margin-left:8px">Disconnected</span>
+    </div>
+    <div class="term-wrap" id="term-container"></div>
+  </div>
+</div>
+
+<!-- FILES -->
+<div class="page" id="p-files">
+  <div class="card" style="padding:12px">
+    <h3><span class="ico">&#128193;</span> File Manager &mdash; /mnt</h3>
+    <div class="fm-toolbar">
+      <span class="fm-path">/mnt</span>
+      <button class="btn btn-sec" onclick="fetchFiles()">Refresh</button>
+    </div>
+    <div class="fm-list" id="fm-list">
+      <div class="fm-empty">Loading file list&hellip;</div>
+    </div>
+    <div class="upload-zone" id="upload-zone" 
onclick="document.getElementById('file-in').click()">
+      <input type="file" id="file-in" onchange="uploadFile(this.files[0])">
+      Drop file here or click to upload to /mnt
+    </div>
+  </div>
+</div>
+
+<!-- NETWORK -->
+<div class="page" id="p-network">
+  <div class="grid">
+    <div class="card">
+      <h3><span class="ico">&#128268;</span> <span 
id="net-ifname">--</span></h3>
+      <div class="net-if">
+        <div class="row"><span class="lbl">Status</span><span 
class="val"><span class="badge badge-ok">RUNNING</span></span></div>
+        <div class="row"><span class="lbl">IP Address</span><span class="val" 
id="net-ip">--</span></div>
+        <div class="row"><span class="lbl">Netmask</span><span class="val" 
id="net-mask">--</span></div>
+        <div class="row"><span class="lbl">Gateway</span><span class="val" 
id="net-gw">--</span></div>
+        <div class="row"><span class="lbl">MAC</span><span class="val" 
id="net-mac">--</span></div>
+        <div class="row"><span class="lbl">MTU</span><span class="val" 
id="net-mtu">1500</span></div>
+      </div>
+    </div>
+    <div class="card">
+      <h3><span class="ico">&#128220;</span> DHCP</h3>
+      <div class="row"><span class="lbl">Mode</span><span class="val">Client 
(auto)</span></div>
+      <div class="row"><span class="lbl">Last Renew</span><span class="val" 
id="net-lease">--</span></div>
+      <div style="margin-top:10px;display:flex;align-items:center;gap:10px">
+        <button class="btn btn-sec" id="renew-btn" onclick="renewDhcp()">Renew 
Lease</button>
+        <span id="renew-status" style="font-size:11px;color:#8899aa"></span>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="toast" id="toast"></div>
+
+<script src="/xterm.min.js"></script>
+<script>
+var term=null, termWs=null, termReady=false, termQueue=[], termDataSub=null;
+var uptimeSec=0, uptimeRef=0;
+function $(id){return document.getElementById(id)}
+function showPage(p){
+  
document.querySelectorAll('.page').forEach(function(e){e.classList.remove('vis')});
+  document.querySelectorAll('nav 
a').forEach(function(e){e.classList.remove('act')});
+  $('p-'+p).classList.add('vis');
+  document.querySelector('nav a[data-p="'+p+'"]').classList.add('act');
+  if(p==='terminal')initTerminal();
+}
+document.querySelectorAll('nav a').forEach(function(a){
+  a.addEventListener('click',function(e){
+    e.preventDefault();showPage(this.dataset.p);
+  });
+});
+
+function toast(msg,err){
+  var t=$('toast');t.textContent=msg;
+  t.className='toast'+(err?' err':'')+' show';
+  setTimeout(function(){t.classList.remove('show')},3000);
+}
+
+function fmtUptime(s){
+  var d=Math.floor(s/86400);
+  var h=Math.floor((s%86400)/3600);
+  var m=Math.floor((s%3600)/60);
+  var sec=s%60;
+  var hh=('0'+h).slice(-2),mm=('0'+m).slice(-2),ss=('0'+sec).slice(-2);
+  return d>0?(d+'d '+hh+':'+mm+':'+ss):(hh+':'+mm+':'+ss);
+}
+
+setInterval(function(){
+  if(!uptimeRef)return;
+  var s=uptimeSec+Math.floor((Date.now()-uptimeRef)/1000);
+  $('uptime').textContent='Uptime: '+fmtUptime(s);
+},1000);
+
+function fetchSysInfo(){
+  var x=new XMLHttpRequest();
+  x.open('GET','/cgi-bin/sysinfo');
+  x.onload=function(){
+    if(x.status===200){
+      try{
+        var d=JSON.parse(x.responseText);
+        if(d.version)$('si-ver').textContent=d.version;
+        if(d.build)$('si-build').textContent=d.build;
+        if(d.chip)$('chip').textContent=d.chip;
+        if(d.board)$('si-board').textContent=d.board;
+        if(d.ifname){
+          $('si-ifname').textContent=d.ifname;
+          $('net-ifname').textContent=d.ifname;
+        }
+        if(d.arch)$('si-arch').textContent=d.arch;
+        if(d.ip){$('si-ip').textContent=d.ip;$('net-ip').textContent=d.ip;}
+        
if(d.mask){$('si-mask').textContent=d.mask;$('net-mask').textContent=d.mask;}
+        if(d.gw){$('si-gw').textContent=d.gw;$('net-gw').textContent=d.gw;}
+        
if(d.mac){$('si-mac').textContent=d.mac;$('net-mac').textContent=d.mac;}
+        if(d.uptime_sec!==undefined){
+          uptimeSec=d.uptime_sec;
+          uptimeRef=Date.now();
+          $('uptime').textContent='Uptime: '+fmtUptime(d.uptime_sec);
+        } else if(d.uptime){
+          $('uptime').textContent='Uptime: '+d.uptime;
+        }
+        if(d.files!==undefined)$('si-files').textContent=d.files;
+        toast('System info updated');
+      }catch(e){toast('Parse error','err');}
+    }
+  };
+  x.onerror=function(){};
+  x.send();
+}
+
+function fetchFiles(){
+  var x=new XMLHttpRequest();
+  x.open('GET','/cgi-bin/files');
+  x.onload=function(){
+    if(x.status===200){
+      try{
+        var d=JSON.parse(x.responseText);
+        renderFiles(d.files||[]);
+      }catch(e){renderFiles([]);}
+    }
+  };
+  x.onerror=function(){
+    $('fm-list').innerHTML='<div class="fm-empty">Could not load files (CGI 
not ready)</div>';
+  };
+  x.send();
+}
+
+function renderFiles(files){
+  var el=$('fm-list');
+  if(!files.length){el.innerHTML='<div class="fm-empty">No files in 
/mnt</div>';return;}
+  var h='';
+  for(var i=0;i<files.length;i++){
+    var f=files[i];
+    h+='<div class="fm-item">'
+      +'<span class="name">'+esc(f.name)+'</span>'
+      +'<span class="sz">'+(f.size||'--')+'</span>'
+      +'<span class="acts">';
+    if(f.name.match(/\.py$/i))
+      h+='<button onclick="runScript(\''+esc(f.name)+'\')">Run</button>';
+    h+='<button onclick="deleteFile(\''+esc(f.name)+'\')">Del</button>'
+      +'</span></div>';
+  }
+  el.innerHTML=h;
+}
+
+function esc(s){return 
s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');}
+
+function uploadFile(file){
+  if(!file)return;
+  toast('Uploading '+file.name+'...');
+  var form=new FormData();
+  form.append('file',file);
+  var x=new XMLHttpRequest();
+  x.open('POST','/cgi-bin/upload');
+  x.onload=function(){
+    if(x.status===200){toast('Uploaded '+file.name);fetchFiles();}
+    else toast('Upload failed: '+x.status,true);
+  };
+  x.onerror=function(){toast('Upload failed (CGI not ready)',true);};
+  x.send(form);
+}
+
+function deleteFile(name){
+  if(!confirm('Delete '+name+'?'))return;
+  var x=new XMLHttpRequest();
+  x.open('POST','/cgi-bin/files?action=delete&name='+encodeURIComponent(name));
+  x.onload=function(){
+    if(x.status===200){toast('Deleted '+name);fetchFiles();}
+    else toast('Delete failed',true);
+  };
+  x.send();
+}
+
+function initTerminal(){
+  if(term)return;
+  term=new 
Terminal({cursorBlink:true,fontSize:13,theme:{background:'#0d1117',foreground:'#c0d0e0'}});
+  term.open($('term-container'));
+  term.writeln('NuttX Web Terminal - click Connect to start NSH session');
+  termDataSub=term.onData(function(data){
+    if(termWs&&termWs.readyState===WebSocket.OPEN)termWs.send(data);
+    else termQueue.push(data);
+  });
+}
+
+function setTermStatus(s,ok){
+  $('term-status').textContent=s;
+  $('term-status').style.color=ok?'#4ade80':'#8899aa';
+}
+
+function connectTerminal(){
+  initTerminal();
+  if(termWs&&termWs.readyState===WebSocket.OPEN)return;
+  var host=window.location.hostname||'127.0.0.1';
+  var url='ws://'+host+':8080';
+  setTermStatus('Connecting...',false);
+  termWs=new WebSocket(url);
+  termWs.onopen=function(){
+    termReady=true;
+    setTermStatus('Connected',true);
+    $('term-connect').style.display='none';
+    $('term-disc').style.display='';
+    while(termQueue.length)termWs.send(termQueue.shift());
+  };
+  termWs.onmessage=function(ev){term.write(ev.data);};
+  termWs.onclose=function(){
+    termReady=false;
+    setTermStatus('Disconnected',false);
+    $('term-connect').style.display='';
+    $('term-disc').style.display='none';
+    termWs=null;
+  };
+  termWs.onerror=function(){
+    toast('WebSocket connection failed',true);
+    setTermStatus('Error',false);
+  };
+}
+
+function disconnectTerminal(){
+  if(termWs)termWs.close();
+}
+
+function sendTerminalCmd(cmd){
+  showPage('terminal');
+  if(!term)initTerminal();
+  if(!termWs||termWs.readyState!==WebSocket.OPEN){
+    connectTerminal();
+    setTimeout(function(){sendTerminalCmd(cmd);},800);
+    return;
+  }
+  termWs.send(cmd);
+}
+
+function runScript(name){
+  var cmd='python /mnt/'+name;
+  sendTerminalCmd(cmd+'\r');
+  toast('Running: '+cmd);
+}
+
+function renewDhcp(){
+  var btn=$('renew-btn'), st=$('renew-status');
+  btn.disabled=true;
+  st.textContent='Renewing...';
+  st.style.color='#fbbf24';
+  var x=new XMLHttpRequest();
+  x.open('GET','/cgi-bin/dhcprenew');
+  x.timeout=30000;
+  x.onload=function(){
+    btn.disabled=false;
+    try{
+      var d=JSON.parse(x.responseText);
+      if(d.status==='ok'){
+        var now=new Date();
+        var 
ts=('0'+now.getHours()).slice(-2)+':'+('0'+now.getMinutes()).slice(-2)+':'+('0'+now.getSeconds()).slice(-2);
+        $('net-lease').textContent='Renewed at '+ts;
+        st.textContent='OK';
+        st.style.color='#4ade80';
+        toast('DHCP lease renewed');
+        setTimeout(fetchSysInfo,1000);
+      } else {
+        st.textContent='Failed (code '+d.code+')';
+        st.style.color='#e94560';
+        toast('DHCP renew failed',true);
+      }
+    }catch(e){
+      st.textContent='Error';
+      st.style.color='#e94560';
+      toast('DHCP renew error',true);
+    }
+    setTimeout(function(){st.textContent='';},5000);
+  };
+  x.onerror=x.ontimeout=function(){
+    btn.disabled=false;
+    st.textContent='Timeout';
+    st.style.color='#e94560';
+    toast('DHCP renew timed out',true);
+  };
+  x.send();
+}
+
+var uz=$('upload-zone');
+uz.addEventListener('dragover',function(e){e.preventDefault();uz.classList.add('drag');});
+uz.addEventListener('dragleave',function(){uz.classList.remove('drag');});
+uz.addEventListener('drop',function(e){
+  e.preventDefault();uz.classList.remove('drag');
+  if(e.dataTransfer.files.length)uploadFile(e.dataTransfer.files[0]);
+});
+
+fetchSysInfo();
+fetchFiles();
+</script>
+</body>
+</html>
diff --git a/examples/webpanel/content/www/xterm.css 
b/examples/webpanel/content/www/xterm.css
new file mode 100644
index 000000000..e97b64390
--- /dev/null
+++ b/examples/webpanel/content/www/xterm.css
@@ -0,0 +1,218 @@
+/**
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+ * https://github.com/chjj/term.js
+ * @license MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to 
deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * Originally forked from (with the author's permission):
+ *   Fabrice Bellard's javascript vt100 for jslinux:
+ *   http://bellard.org/jslinux/
+ *   Copyright (c) 2011 Fabrice Bellard
+ *   The original design remains. The terminal itself
+ *   has been extended to include xterm CSI codes, among
+ *   other features.
+ */
+
+/**
+ *  Default styles for xterm.js
+ */
+
+.xterm {
+    cursor: text;
+    position: relative;
+    user-select: none;
+    -ms-user-select: none;
+    -webkit-user-select: none;
+}
+
+.xterm.focus,
+.xterm:focus {
+    outline: none;
+}
+
+.xterm .xterm-helpers {
+    position: absolute;
+    top: 0;
+    /**
+     * The z-index of the helpers must be higher than the canvases in order for
+     * IMEs to appear on top.
+     */
+    z-index: 5;
+}
+
+.xterm .xterm-helper-textarea {
+    padding: 0;
+    border: 0;
+    margin: 0;
+    /* Move textarea out of the screen to the far left, so that the cursor is 
not visible */
+    position: absolute;
+    opacity: 0;
+    left: -9999em;
+    top: 0;
+    width: 0;
+    height: 0;
+    z-index: -5;
+    /** Prevent wrapping so the IME appears against the textarea at the 
correct position */
+    white-space: nowrap;
+    overflow: hidden;
+    resize: none;
+}
+
+.xterm .composition-view {
+    /* TODO: Composition position got messed up somewhere */
+    background: #000;
+    color: #FFF;
+    display: none;
+    position: absolute;
+    white-space: nowrap;
+    z-index: 1;
+}
+
+.xterm .composition-view.active {
+    display: block;
+}
+
+.xterm .xterm-viewport {
+    /* On OS X this is required in order for the scroll bar to appear fully 
opaque */
+    background-color: #000;
+    overflow-y: scroll;
+    cursor: default;
+    position: absolute;
+    right: 0;
+    left: 0;
+    top: 0;
+    bottom: 0;
+}
+
+.xterm .xterm-screen {
+    position: relative;
+}
+
+.xterm .xterm-screen canvas {
+    position: absolute;
+    left: 0;
+    top: 0;
+}
+
+.xterm .xterm-scroll-area {
+    visibility: hidden;
+}
+
+.xterm-char-measure-element {
+    display: inline-block;
+    visibility: hidden;
+    position: absolute;
+    top: 0;
+    left: -9999em;
+    line-height: normal;
+}
+
+.xterm.enable-mouse-events {
+    /* When mouse events are enabled (eg. tmux), revert to the standard 
pointer cursor */
+    cursor: default;
+}
+
+.xterm.xterm-cursor-pointer,
+.xterm .xterm-cursor-pointer {
+    cursor: pointer;
+}
+
+.xterm.column-select.focus {
+    /* Column selection mode */
+    cursor: crosshair;
+}
+
+.xterm .xterm-accessibility:not(.debug),
+.xterm .xterm-message {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    right: 0;
+    z-index: 10;
+    color: transparent;
+    pointer-events: none;
+}
+
+.xterm .xterm-accessibility-tree:not(.debug) *::selection {
+  color: transparent;
+}
+
+.xterm .xterm-accessibility-tree {
+  user-select: text;
+  white-space: pre;
+}
+
+.xterm .live-region {
+    position: absolute;
+    left: -9999px;
+    width: 1px;
+    height: 1px;
+    overflow: hidden;
+}
+
+.xterm-dim {
+    /* Dim should not apply to background, so the opacity of the foreground 
color is applied
+     * explicitly in the generated class and reset to 1 here */
+    opacity: 1 !important;
+}
+
+.xterm-underline-1 { text-decoration: underline; }
+.xterm-underline-2 { text-decoration: double underline; }
+.xterm-underline-3 { text-decoration: wavy underline; }
+.xterm-underline-4 { text-decoration: dotted underline; }
+.xterm-underline-5 { text-decoration: dashed underline; }
+
+.xterm-overline {
+    text-decoration: overline;
+}
+
+.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
+.xterm-overline.xterm-underline-2 { text-decoration: overline double 
underline; }
+.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
+.xterm-overline.xterm-underline-4 { text-decoration: overline dotted 
underline; }
+.xterm-overline.xterm-underline-5 { text-decoration: overline dashed 
underline; }
+
+.xterm-strikethrough {
+    text-decoration: line-through;
+}
+
+.xterm-screen .xterm-decoration-container .xterm-decoration {
+       z-index: 6;
+       position: absolute;
+}
+
+.xterm-screen .xterm-decoration-container 
.xterm-decoration.xterm-decoration-top-layer {
+       z-index: 7;
+}
+
+.xterm-decoration-overview-ruler {
+    z-index: 8;
+    position: absolute;
+    top: 0;
+    right: 0;
+    pointer-events: none;
+}
+
+.xterm-decoration-top {
+    z-index: 2;
+    position: relative;
+}
diff --git a/examples/webpanel/content/www/xterm.min.js 
b/examples/webpanel/content/www/xterm.min.js
new file mode 100644
index 000000000..0a51bfb69
--- /dev/null
+++ b/examples/webpanel/content/www/xterm.min.js
@@ -0,0 +1,8 @@
+/**
+ * Skipped minification because the original files appears to be already 
minified.
+ * Original file: /npm/@xterm/[email protected]/lib/xterm.js
+ *
+ * Do NOT use SRI with dynamically generated files! More information: 
https://www.jsdelivr.com/using-sri-with-dynamic-files
+ */
+!function(e,t){if("object"==typeof exports&&"object"==typeof 
module)module.exports=t();else if("function"==typeof 
define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof 
exports?exports:e)[s]=i[s]}}(globalThis,(()=>(()=>{"use strict";var 
e={4567:function(e,t,i){var s=this&&this.__decorate||function(e,t,i,s){var 
r,n=arguments.length,o=n<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof
 Reflect&&"function"==typeof Reflect.decorate)o=Reflect.d [...]
",e.GS="",e.RS="",e.US="",e.SP=" 
",e.DEL=""}(i||(t.C0=i={})),function(e){e.PAD="€",e.HOP="",e.BPH="‚",e.NBH="ƒ",e.IND="„",e.NEL="
…
",e.SSA="†",e.ESA="‡",e.HTS="ˆ",e.HTJ="‰",e.VTS="Š",e.PLD="‹",e.PLU="Œ",e.RI="",e.SS2="Ž",e.SS3="",e.DCS="",e.PU1="‘",e.PU2="’",e.STS="“",e.CCH="”",e.MW="•",e.SPA="–",e.EPA="—",e.SOS="˜",e.SGCI="™",e.SCI="š",e.CSI="›",e.ST="œ",e.OSC="",e.PM="ž",e.APC="Ÿ"}(s||(t.C1=s={})),function(e){e.ST=`${i.ESC}\\`}(r||(t.C1_ESCAPED=r={}))},7399:(e,t,i)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.evaluateKeyboardEvent=void
 0;const s=i(2584),r={48:["0",")"],49:["1","!"],50:["2","@"],51:["3", [...]
+//# sourceMappingURL=xterm.js.map
\ No newline at end of file
diff --git a/examples/webpanel/webpanel_main.c 
b/examples/webpanel/webpanel_main.c
new file mode 100644
index 000000000..7831808f3
--- /dev/null
+++ b/examples/webpanel/webpanel_main.c
@@ -0,0 +1,207 @@
+/****************************************************************************
+ * apps/examples/webpanel/webpanel_main.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <sys/mount.h>
+#include <sys/boardctl.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include <nuttx/drivers/ramdisk.h>
+
+#include "netutils/thttpd.h"
+#include "fsutils/mksmartfs.h"
+#include "ws_terminal.h"
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#define SECTORSIZE   64
+#define NSECTORS(b)  (((b) + SECTORSIZE - 1) / SECTORSIZE)
+#define WEBPANEL_RAMDISK_MINOR 2
+#define ROMFSDEV     "/dev/ram2"
+
+#define ROMFS_MOUNTPT  "/data/tmp_romfs"
+#define BINFS_MOUNTPT  "/data/tmp_binfs"
+#define BINFS_PREFIX   "cgi-bin"
+#define UNIONFS_MOUNTPT CONFIG_THTTPD_PATH
+
+#define SMARTFS_DEV    "/dev/smart0"
+#define SMARTFS_MNT    "/mnt"
+
+/****************************************************************************
+ * External References
+ ****************************************************************************/
+
+extern const unsigned char webpanel_romfs_img[];
+extern const unsigned int  webpanel_romfs_img_len;
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: main
+ *
+ * Description:
+ *   Initialize WebPanel filesystem mounts, start the websocket daemon,
+ *   then launch the THTTPD server.
+ *
+ * Input Parameters:
+ *   argc - Number of arguments.
+ *   argv - Argument vector.
+ *
+ * Returned Value:
+ *   EXIT_SUCCESS on success; EXIT_FAILURE on setup error.
+ *
+ ****************************************************************************/
+
+int main(int argc, FAR char *argv[])
+{
+  struct boardioc_romdisk_s desc;
+  char *thttpd_argv = "thttpd";
+  struct statfs check_fs;
+  struct statfs sfs;
+  int ret;
+
+  /* Prevent duplicate startup (e.g. rcS runs again in child NSH) */
+
+  if (statfs(UNIONFS_MOUNTPT, &check_fs) == 0)
+    {
+      return 0;
+    }
+
+  /* Register the ROMFS image as a ramdisk */
+
+  printf("WebPanel: Registering ROMFS ramdisk\n");
+
+  desc.minor    = WEBPANEL_RAMDISK_MINOR;
+  desc.nsectors = NSECTORS(webpanel_romfs_img_len);
+  desc.sectsize = SECTORSIZE;
+  desc.image    = (FAR uint8_t *)webpanel_romfs_img;
+
+  ret = boardctl(BOARDIOC_ROMDISK, (uintptr_t)&desc);
+  if (ret < 0 && errno != EEXIST)
+    {
+      printf("WebPanel: ERROR romdisk_register failed: %d\n", ret);
+      return EXIT_FAILURE;
+    }
+
+  /* Create mount point directories */
+
+  mkdir("/data", 0777);
+  mkdir(ROMFS_MOUNTPT, 0777);
+  mkdir(BINFS_MOUNTPT, 0777);
+
+  /* Mount ROMFS at temp location */
+
+  printf("WebPanel: Mounting ROMFS at %s\n", ROMFS_MOUNTPT);
+
+  ret = mount(ROMFSDEV, ROMFS_MOUNTPT, "romfs", MS_RDONLY, NULL);
+  if (ret < 0 && errno != EBUSY)
+    {
+      printf("WebPanel: ERROR mount ROMFS failed: %d\n", errno);
+      return EXIT_FAILURE;
+    }
+
+  /* Mount BINFS at temp location (exposes built-in apps as CGI) */
+
+  printf("WebPanel: Mounting BINFS at %s\n", BINFS_MOUNTPT);
+
+  ret = mount(NULL, BINFS_MOUNTPT, "binfs", MS_RDONLY, NULL);
+  if (ret < 0 && errno != EBUSY)
+    {
+      printf("WebPanel: ERROR mount BINFS failed: %d\n", errno);
+      return EXIT_FAILURE;
+    }
+
+  /* Create UNIONFS: ROMFS content + BINFS under cgi-bin/ prefix */
+
+  printf("WebPanel: Creating UNIONFS at %s\n", UNIONFS_MOUNTPT);
+
+  ret = mount(NULL, UNIONFS_MOUNTPT, "unionfs", 0,
+              "fspath1=" ROMFS_MOUNTPT ",prefix1="
+              ",fspath2=" BINFS_MOUNTPT ",prefix2=" BINFS_PREFIX);
+  if (ret < 0 && errno != EBUSY)
+    {
+      printf("WebPanel: ERROR mount UNIONFS failed: %d\n", errno);
+      return EXIT_FAILURE;
+    }
+
+  /* Ensure SmartFS is formatted and mounted at /mnt for user files */
+
+  if (statfs(SMARTFS_MNT, &sfs) != 0)
+    {
+      printf("WebPanel: SmartFS not mounted, formatting %s\n",
+             SMARTFS_DEV);
+      ret = mksmartfs(SMARTFS_DEV, 0);
+      if (ret < 0)
+        {
+          printf("WebPanel: WARNING mksmartfs failed: %d\n", errno);
+        }
+      else
+        {
+          mkdir(SMARTFS_MNT, 0777);
+          ret = mount(SMARTFS_DEV, SMARTFS_MNT, "smartfs", 0, NULL);
+          if (ret < 0)
+            {
+              printf("WebPanel: WARNING mount SmartFS failed: %d\n",
+                     errno);
+            }
+          else
+            {
+              printf("WebPanel: SmartFS mounted at %s\n", SMARTFS_MNT);
+            }
+        }
+    }
+  else
+    {
+      printf("WebPanel: SmartFS already mounted at %s\n", SMARTFS_MNT);
+    }
+
+  /* Start WebSocket terminal server */
+
+  ret = ws_terminal_start();
+  if (ret < 0)
+    {
+      printf("WebPanel: WARNING WebSocket server failed to start\n");
+    }
+
+  /* Start THTTPD */
+
+  printf("WebPanel: Starting THTTPD (serving from %s)\n", UNIONFS_MOUNTPT);
+  fflush(stdout);
+
+  thttpd_main(1, &thttpd_argv);
+
+  printf("WebPanel: THTTPD terminated\n");
+  return 0;
+}
diff --git a/examples/webpanel/ws_terminal.c b/examples/webpanel/ws_terminal.c
new file mode 100644
index 000000000..7db8d7392
--- /dev/null
+++ b/examples/webpanel/ws_terminal.c
@@ -0,0 +1,350 @@
+/****************************************************************************
+ * apps/examples/webpanel/ws_terminal.c
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#include <nuttx/config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sched.h>
+#include <spawn.h>
+
+#include <libwebsockets.h>
+
+#include "ws_terminal.h"
+
+/****************************************************************************
+ * Pre-processor Definitions
+ ****************************************************************************/
+
+#ifndef CONFIG_EXAMPLES_WEBPANEL_WS_PORT
+#  define CONFIG_EXAMPLES_WEBPANEL_WS_PORT 8080
+#endif
+
+#ifndef CONFIG_EXAMPLES_WEBPANEL_WS_PRIORITY
+#  define CONFIG_EXAMPLES_WEBPANEL_WS_PRIORITY 100
+#endif
+
+#ifndef CONFIG_EXAMPLES_WEBPANEL_WS_STACKSIZE
+#  define CONFIG_EXAMPLES_WEBPANEL_WS_STACKSIZE 8192
+#endif
+
+#ifndef CONFIG_EXAMPLES_WEBPANEL_WS_NSH_STACKSIZE
+#  define CONFIG_EXAMPLES_WEBPANEL_WS_NSH_STACKSIZE 4096
+#endif
+
+#define WS_IOBUF_SIZE  512
+
+/****************************************************************************
+ * Private Types
+ ****************************************************************************/
+
+struct ws_session
+{
+  int masterfd;
+  pid_t nshpid;
+  unsigned char txbuf[LWS_PRE + WS_IOBUF_SIZE];
+  size_t txlen;
+};
+
+/****************************************************************************
+ * Private Function Prototypes
+ ****************************************************************************/
+
+static int ws_spawn_nsh(int masterfd, pid_t *pid);
+static int ws_callback(struct lws *wsi,
+                       enum lws_callback_reasons reason,
+                       void *user, void *in, size_t len);
+static int ws_daemon(int argc, FAR char *argv[]);
+
+/****************************************************************************
+ * Private Data
+ ****************************************************************************/
+
+static const struct lws_protocols g_protocols[] =
+{
+  {
+    "",
+    ws_callback,
+    sizeof(struct ws_session),
+    WS_IOBUF_SIZE,
+    0, NULL, 0
+  },
+  LWS_PROTOCOL_LIST_TERM
+};
+
+/****************************************************************************
+ * Private Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: ws_spawn_nsh
+ *
+ * Description:
+ *   Spawn an NSH instance attached to a PTY slave.
+ *
+ * Input Parameters:
+ *   masterfd - PTY master descriptor.
+ *   pid      - Location to receive spawned task PID.
+ *
+ * Returned Value:
+ *   Zero on success; negated errno on failure.
+ *
+ ****************************************************************************/
+
+static int ws_spawn_nsh(int masterfd, pid_t *pid)
+{
+  char slavepath[32];
+  FAR char *nshargv[] =
+  {
+    "nsh", NULL
+  };
+
+  posix_spawn_file_actions_t actions;
+  posix_spawnattr_t attr;
+  struct sched_param param;
+  int ret;
+
+  ret = grantpt(masterfd);
+  if (ret < 0)
+    {
+      return ret;
+    }
+
+  ret = unlockpt(masterfd);
+  if (ret < 0)
+    {
+      return ret;
+    }
+
+  ret = ptsname_r(masterfd, slavepath, sizeof(slavepath));
+  if (ret < 0)
+    {
+      return ret;
+    }
+
+  posix_spawn_file_actions_init(&actions);
+  posix_spawn_file_actions_addopen(&actions, 0, slavepath,
+                                   O_RDWR, 0);
+  posix_spawn_file_actions_adddup2(&actions, 0, 1);
+  posix_spawn_file_actions_adddup2(&actions, 0, 2);
+
+  posix_spawnattr_init(&attr);
+  param.sched_priority = CONFIG_EXAMPLES_WEBPANEL_WS_PRIORITY;
+  posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSCHEDPARAM);
+  posix_spawnattr_setschedparam(&attr, &param);
+  posix_spawnattr_setstacksize(&attr,
+                               CONFIG_EXAMPLES_WEBPANEL_WS_NSH_STACKSIZE);
+
+  ret = posix_spawn(pid, "nsh", &actions, &attr, nshargv, NULL);
+
+  posix_spawn_file_actions_destroy(&actions);
+  posix_spawnattr_destroy(&attr);
+
+  return ret == 0 ? 0 : -ret;
+}
+
+/****************************************************************************
+ * Name: ws_callback
+ *
+ * Description:
+ *   libwebsockets protocol callback for the NSH terminal relay.
+ *
+ ****************************************************************************/
+
+static int ws_callback(struct lws *wsi,
+                       enum lws_callback_reasons reason,
+                       void *user, void *in, size_t len)
+{
+  struct ws_session *sess = (struct ws_session *)user;
+  ssize_t n;
+
+  switch (reason)
+    {
+    case LWS_CALLBACK_ESTABLISHED:
+      sess->masterfd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
+      if (sess->masterfd < 0)
+        {
+          printf("ws_terminal: failed to open PTY\n");
+          return -1;
+        }
+
+      sess->nshpid = -1;
+      if (ws_spawn_nsh(sess->masterfd, &sess->nshpid) < 0)
+        {
+          printf("ws_terminal: failed to spawn NSH\n");
+          close(sess->masterfd);
+          sess->masterfd = -1;
+          return -1;
+        }
+
+      /* Make PTY master non-blocking for polling */
+
+      fcntl(sess->masterfd, F_SETFL,
+            fcntl(sess->masterfd, F_GETFL) | O_NONBLOCK);
+
+      sess->txlen = 0;
+      printf("ws_terminal: client connected\n");
+
+      lws_set_timer_usecs(wsi, 50 * LWS_USEC_PER_SEC / 1000);
+      break;
+
+    case LWS_CALLBACK_RECEIVE:
+      if (sess->masterfd >= 0 && len > 0)
+        {
+          write(sess->masterfd, in, len);
+        }
+
+      break;
+
+    case LWS_CALLBACK_SERVER_WRITEABLE:
+      if (sess->masterfd < 0)
+        {
+          break;
+        }
+
+      n = read(sess->masterfd,
+               &sess->txbuf[LWS_PRE],
+               WS_IOBUF_SIZE);
+      if (n > 0)
+        {
+          lws_write(wsi, &sess->txbuf[LWS_PRE], n, LWS_WRITE_TEXT);
+          lws_set_timer_usecs(wsi, 10 * LWS_USEC_PER_SEC / 1000);
+        }
+      else
+        {
+          lws_set_timer_usecs(wsi, 50 * LWS_USEC_PER_SEC / 1000);
+        }
+
+      break;
+
+    case LWS_CALLBACK_TIMER:
+      lws_callback_on_writable(wsi);
+      break;
+
+    case LWS_CALLBACK_CLOSED:
+      printf("ws_terminal: client disconnected\n");
+      if (sess->nshpid > 0)
+        {
+          task_delete(sess->nshpid);
+          sess->nshpid = -1;
+        }
+
+      if (sess->masterfd >= 0)
+        {
+          close(sess->masterfd);
+          sess->masterfd = -1;
+        }
+
+      break;
+
+    default:
+      break;
+    }
+
+  return 0;
+}
+
+/****************************************************************************
+ * Name: ws_daemon
+ *
+ * Description:
+ *   WebSocket terminal daemon using libwebsockets.
+ *
+ ****************************************************************************/
+
+static int ws_daemon(int argc, FAR char *argv[])
+{
+  struct lws_context_creation_info info;
+  struct lws_context *context;
+  int n;
+
+  lws_set_log_level(LLL_ERR | LLL_WARN, NULL);
+
+  memset(&info, 0, sizeof(info));
+  info.port = CONFIG_EXAMPLES_WEBPANEL_WS_PORT;
+  info.protocols = g_protocols;
+  info.vhost_name = "localhost";
+  info.options = 0;
+
+  context = lws_create_context(&info);
+  if (context == NULL)
+    {
+      printf("ws_terminal: ERROR creating lws context\n");
+      return EXIT_FAILURE;
+    }
+
+  printf("WebPanel: WebSocket terminal listening on port %d\n",
+         CONFIG_EXAMPLES_WEBPANEL_WS_PORT);
+
+  n = 0;
+  while (n >= 0)
+    {
+      n = lws_service(context, 100);
+    }
+
+  lws_context_destroy(context);
+  return EXIT_FAILURE;
+}
+
+/****************************************************************************
+ * Public Functions
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: ws_terminal_start
+ *
+ * Description:
+ *   Start the websocket terminal daemon task.
+ *
+ * Input Parameters:
+ *   None.
+ *
+ * Returned Value:
+ *   Task PID on success; negated errno value on failure.
+ *
+ ****************************************************************************/
+
+int ws_terminal_start(void)
+{
+  int pid;
+
+  pid = task_create("ws_daemon",
+                    CONFIG_EXAMPLES_WEBPANEL_WS_PRIORITY,
+                    CONFIG_EXAMPLES_WEBPANEL_WS_STACKSIZE,
+                    ws_daemon, NULL);
+  if (pid < 0)
+    {
+      printf("WebPanel: ERROR failed to start WebSocket server: %d\n",
+             errno);
+      return -errno;
+    }
+
+  return pid;
+}
diff --git a/examples/webpanel/ws_terminal.h b/examples/webpanel/ws_terminal.h
new file mode 100644
index 000000000..91cc637d9
--- /dev/null
+++ b/examples/webpanel/ws_terminal.h
@@ -0,0 +1,63 @@
+/****************************************************************************
+ * apps/examples/webpanel/ws_terminal.h
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+#ifndef __APPS_EXAMPLES_WEBPANEL_WS_TERMINAL_H
+#define __APPS_EXAMPLES_WEBPANEL_WS_TERMINAL_H
+
+/****************************************************************************
+ * Included Files
+ ****************************************************************************/
+
+#ifdef __cplusplus
+#define EXTERN extern "C"
+extern "C"
+{
+#else
+#define EXTERN extern
+#endif
+
+/****************************************************************************
+ * Public Function Prototypes
+ ****************************************************************************/
+
+/****************************************************************************
+ * Name: ws_terminal_start
+ *
+ * Description:
+ *   Start the websocket terminal daemon task.
+ *
+ * Input Parameters:
+ *   None.
+ *
+ * Returned Value:
+ *   Task PID on success; negated errno value on failure.
+ *
+ ****************************************************************************/
+
+int ws_terminal_start(void);
+
+#undef EXTERN
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __APPS_EXAMPLES_WEBPANEL_WS_TERMINAL_H */


Reply via email to