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">⚙</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">🔌</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">💾</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">⚡</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">⌨</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">📁</span> File Manager — /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…</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">🔌</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">📜</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,'&').replace(/</g,'<').replace(/"/g,'"');}
+
+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, ¶m);
+ 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 */