From 1045635d81aed56bafe1e1504dfb83f0e7068fff Mon Sep 17 00:00:00 2001 From: cpq Date: Tue, 28 Apr 2026 06:24:51 +0100 Subject: [PATCH] Device dashboard refactoring --- mongoose.h | 138 +++- resources/dashboard.js | 2 +- resources/inline.js | 2 +- src/dash.h | 138 +++- tutorials/device-dashboard/Makefile | 34 - tutorials/device-dashboard/full/Makefile | 31 + tutorials/device-dashboard/full/dashboard.c | 144 ++++ .../index.html => full/dashboard.html} | 200 +++++- tutorials/device-dashboard/{ => full}/main.c | 0 .../device-dashboard/mongoose_dashboard.c | 245 ------- tutorials/device-dashboard/web_root/logo.svg | 23 - tutorials/device-dashboard/web_root/main.js | 95 --- tutorials/device-dashboard/web_root/style.css | 653 ------------------ 13 files changed, 624 insertions(+), 1081 deletions(-) delete mode 100644 tutorials/device-dashboard/Makefile create mode 100644 tutorials/device-dashboard/full/Makefile create mode 100644 tutorials/device-dashboard/full/dashboard.c rename tutorials/device-dashboard/{web_root/index.html => full/dashboard.html} (51%) rename tutorials/device-dashboard/{ => full}/main.c (100%) delete mode 100644 tutorials/device-dashboard/mongoose_dashboard.c delete mode 100644 tutorials/device-dashboard/web_root/logo.svg delete mode 100644 tutorials/device-dashboard/web_root/main.js delete mode 100644 tutorials/device-dashboard/web_root/style.css diff --git a/mongoose.h b/mongoose.h index 31107929..a6d59642 100644 --- a/mongoose.h +++ b/mongoose.h @@ -3170,7 +3170,8 @@ struct mg_field { struct mg_field_set { const char *name; struct mg_field *fields; - void (*sync)(bool is_reading); + void (*reader)(void); + void (*writer)(void); int read_level; int write_level; }; @@ -3179,20 +3180,29 @@ struct mg_dash_custom_handler { struct mg_dash_custom_handler *next; struct mg_str uri_pattern; mg_event_handler_t handler; + void *handler_data; +}; + +struct mg_dash_file { + struct mg_dash_file *next; + char *name; + size_t size; }; struct mg_dash { struct mg_field_set *sets; struct mg_dash_custom_handler *custom_handlers; + struct mg_dash_file *files; }; -#define MG_DASH_REGISTER_CUSTOM_HANDLER(dash_, uri_, fn_) \ - do { \ - static struct mg_dash_custom_handler ch_; \ - ch_.next = (dash_)->custom_handlers; \ - ch_.uri_pattern = mg_str(uri_); \ - ch_.handler = (fn_); \ - (dash_)->custom_handlers = &ch_; \ +#define MG_DASH_REGISTER_CUSTOM_HANDLER(dash_, uri_, fn_, data_) \ + do { \ + static struct mg_dash_custom_handler ch_; \ + ch_.next = (dash_)->custom_handlers; \ + ch_.uri_pattern = mg_str(uri_); \ + ch_.handler = (fn_); \ + ch_.handler_data = (data_); \ + (dash_)->custom_handlers = &ch_; \ } while (0) static inline struct mg_str trimq(struct mg_str s) { // Trim double quotes @@ -3298,14 +3308,14 @@ static inline size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { if (name->len == 0) { n += mg_xprintf(fn, arg, "{"); for (i = 0; dash->sets != NULL && dash->sets[i].name != NULL; i++) { - if (dash->sets[i].sync) dash->sets[i].sync(false); + if (dash->sets[i].reader) dash->sets[i].reader(); if (i > 0) n += mg_xprintf(fn, arg, ","); n += mg_xprintf(fn, arg, "%m:", MG_ESC(dash->sets[i].name)); n += mg_xprintf(fn, arg, "%M", mg_print_field_set, &dash->sets[i]); } n += mg_xprintf(fn, arg, "}"); } else if (set != NULL) { - if (set->sync) set->sync(false); + if (set->reader) set->reader(); n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); } else { n += mg_xprintf(fn, arg, "null"); @@ -3324,7 +3334,7 @@ static inline void mg_dash_send_change(struct mg_mgr *mgr, struct mg_dash *dash, const char *name) { struct mg_field_set *set = mg_dash_find_field_set(dash, mg_str(name)); if (set != NULL) { - if (set->sync) set->sync(false); + if (set->reader) set->reader(); mg_dash_broadcast(mgr, "{%m:%m,%m:{%m:%M}}", MG_ESC("method"), MG_ESC("change"), MG_ESC("params"), MG_ESC(set->name), mg_print_field_set, set); @@ -3365,8 +3375,8 @@ static inline int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, for (i = 0; set->fields[i].name != NULL; i++) { if (mg_dash_parse_field(val, &set->fields[i])) count++; } - if (count > 0 && set->sync != NULL) { - set->sync(true); + if (count > 0 && set->writer != NULL) { + set->writer(); mg_dash_send_change(c->mgr, dash, set->name); } } else { @@ -3395,6 +3405,96 @@ static inline void mg_dash_process_msg(struct mg_connection *c, #define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n" #define MG_JSON_HEADERS "Content-Type: application/json\r\n" MG_NO_CACHE_HEADERS +struct mg_upload_state { + char marker; // Tells that we're a file upload connection + size_t expected; // POST data length, bytes + size_t received; // Already received bytes + void *ctx; // OTA context +}; + +static inline void mg_dash_file_add(struct mg_dash *dash, struct mg_str name, + size_t size) { + struct mg_dash_file *f = mg_calloc(1, sizeof(*f)); + f->name = mg_strdup(name).buf; + f->size = size; + f->next = dash->files; + dash->files = f; +} + +static inline void mg_dash_file_del(struct mg_dash *dash, struct mg_str name) { + struct mg_dash_file **head, *f; + for (head = &dash->files, f = *head; f; head = &(*head)->next, f = *head) { + if (mg_strcmp(mg_str(f->name), name) == 0) { + MG_INFO(("Deleting %s", f->name)); + *head = f->next; + mg_free(f->name); + mg_free(f); + return; + } + } +} + +static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + struct mg_upload_state *us = (struct mg_upload_state *) c->data; + if ((ev == MG_EV_HTTP_HDRS || ev == MG_EV_HTTP_MSG) && us->marker == 0) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + if (hm->uri.len > 7) hm->uri.len -= 7, hm->uri.buf += 7; // Strip /files/ + if (sizeof(*us) > sizeof(c->data)) { + mg_error(c, + "FAILURE: sizeof(c->data) == %lu, need %lu." + " Set #define MG_DATA_SIZE XXX", + sizeof(c->data), sizeof(*us)); + } else if (mg_strcmp(hm->query, mg_str("action=delete")) == 0) { + // Query string ?action=delete - file deletion + mg_dash_file_del(c->fn_data, hm->uri); + mg_dash_send_change(c->mgr, c->fn_data, "files"); + mg_http_reply(c, 200, NULL, "true"); + } else if (mg_path_is_sane(hm->uri) == false) { + // Bad file name, return error. Protect from traversal, etc + mg_http_reply(c, 400, NULL, "Bad name"); + } else if (mg_match(hm->method, mg_str("POST"), NULL) || + mg_match(hm->method, mg_str("PUT"), NULL)) { + // File upload + char path[128]; + mg_snprintf(path, sizeof(path), "/tmp/%.*s", hm->uri.len, hm->uri.buf); + us->ctx = fopen(path, "wb+"); + if (us->ctx == NULL) { + mg_http_reply(c, 500, NULL, "File upload error %d\n", errno); + } else { + mg_dash_file_add(c->fn_data, hm->uri, hm->body.len); + MG_DEBUG(("Starting upload, [%.*s] %lu", hm->uri.len, hm->uri.buf, + hm->body.len)); + us->marker = 'U'; + us->expected = hm->body.len; + us->received = 0; + mg_iobuf_del(&c->recv, 0, hm->head.len); + c->pfn = NULL; + c->fn = file_ev_handler; + mg_call(c, MG_EV_READ, &c->recv.len); + } + } else { + // Serve file + char path[128]; + mg_snprintf(path, sizeof(path), "/tmp/%.*s", hm->uri.len, hm->uri.buf); + mg_http_serve_file(c, ev_data, path, NULL); + } + } else if (ev == MG_EV_READ && us->marker == 'U') { + // Write uploaded data + MG_DEBUG(("Uploading.. recv.len=%u", c->recv.len)); + us->received += c->recv.len; + fwrite(c->recv.buf, 1, c->recv.len, us->ctx); + c->recv.len = 0; + if (us->received >= us->expected) { + fclose(us->ctx); + MG_DEBUG(("Uploaded %lu", us->expected)); + mg_http_reply(c, 200, NULL, "%lu uploaded\n", us->expected); + mg_dash_send_change(c->mgr, c->fn_data, "files"); + memset(us, 0, sizeof(*us)); + c->is_draining = 1; + } + } +} + static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { struct mg_dash *dash = (struct mg_dash *) c->fn_data; @@ -3404,6 +3504,8 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, memset(parts, 0, sizeof(parts)); if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { mg_ws_upgrade(c, hm, NULL); + } else if (mg_match(hm->uri, mg_str("/files/#"), NULL)) { + file_ev_handler(c, ev, ev_data); } else if (mg_match(hm->uri, mg_str("/api/get/#"), parts) || mg_match(hm->uri, mg_str("/api/get"), NULL)) { mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_name, dash, @@ -3438,6 +3540,16 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, } } +#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32 +#define MG_HTTP_ADDR "http://0.0.0.0:8000" +#define MG_HTTPS_ADDR "https://0.0.0.0:8443" +#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502" +#else +#define MG_HTTP_ADDR "http://0.0.0.0:80" +#define MG_HTTPS_ADDR "http://0.0.0.0:443" +#define MG_MODBUS_ADDR "tcp://0.0.0.0:502" +#endif + // Helper forward declarations for Mongoose CMSIS pack modules extern struct mg_mgr g_mgr; extern void mg_dash_init(struct mg_mgr *); diff --git a/resources/dashboard.js b/resources/dashboard.js index a2d684dd..83db856c 100644 --- a/resources/dashboard.js +++ b/resources/dashboard.js @@ -247,7 +247,7 @@ } } for (const attr of el.attributes) { - const cv = decodeURIComponent(attr.value || ''); + const cv = attr.value || ''; if (attr.originalValue || cv.includes('${')) { if (!attr.originalValue) attr.originalValue = cv; const v = substituteExpressions(attr.originalValue, context); diff --git a/resources/inline.js b/resources/inline.js index 84ed3096..2bc4521d 100644 --- a/resources/inline.js +++ b/resources/inline.js @@ -147,7 +147,7 @@ async function processHtml(content) { processHtml(html).then(result => { if (outputFile) { fs.writeFileSync(outputFile, result); - console.log(`Written to ${outputFile}`); + //console.log(`Written to ${outputFile}`); } else { console.log(result); } diff --git a/src/dash.h b/src/dash.h index b799dc76..ce8b5a9b 100644 --- a/src/dash.h +++ b/src/dash.h @@ -25,7 +25,8 @@ struct mg_field { struct mg_field_set { const char *name; struct mg_field *fields; - void (*sync)(bool is_reading); + void (*reader)(void); + void (*writer)(void); int read_level; int write_level; }; @@ -34,20 +35,29 @@ struct mg_dash_custom_handler { struct mg_dash_custom_handler *next; struct mg_str uri_pattern; mg_event_handler_t handler; + void *handler_data; +}; + +struct mg_dash_file { + struct mg_dash_file *next; + char *name; + size_t size; }; struct mg_dash { struct mg_field_set *sets; struct mg_dash_custom_handler *custom_handlers; + struct mg_dash_file *files; }; -#define MG_DASH_REGISTER_CUSTOM_HANDLER(dash_, uri_, fn_) \ - do { \ - static struct mg_dash_custom_handler ch_; \ - ch_.next = (dash_)->custom_handlers; \ - ch_.uri_pattern = mg_str(uri_); \ - ch_.handler = (fn_); \ - (dash_)->custom_handlers = &ch_; \ +#define MG_DASH_REGISTER_CUSTOM_HANDLER(dash_, uri_, fn_, data_) \ + do { \ + static struct mg_dash_custom_handler ch_; \ + ch_.next = (dash_)->custom_handlers; \ + ch_.uri_pattern = mg_str(uri_); \ + ch_.handler = (fn_); \ + ch_.handler_data = (data_); \ + (dash_)->custom_handlers = &ch_; \ } while (0) static inline struct mg_str trimq(struct mg_str s) { // Trim double quotes @@ -153,14 +163,14 @@ static inline size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { if (name->len == 0) { n += mg_xprintf(fn, arg, "{"); for (i = 0; dash->sets != NULL && dash->sets[i].name != NULL; i++) { - if (dash->sets[i].sync) dash->sets[i].sync(false); + if (dash->sets[i].reader) dash->sets[i].reader(); if (i > 0) n += mg_xprintf(fn, arg, ","); n += mg_xprintf(fn, arg, "%m:", MG_ESC(dash->sets[i].name)); n += mg_xprintf(fn, arg, "%M", mg_print_field_set, &dash->sets[i]); } n += mg_xprintf(fn, arg, "}"); } else if (set != NULL) { - if (set->sync) set->sync(false); + if (set->reader) set->reader(); n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); } else { n += mg_xprintf(fn, arg, "null"); @@ -179,7 +189,7 @@ static inline void mg_dash_send_change(struct mg_mgr *mgr, struct mg_dash *dash, const char *name) { struct mg_field_set *set = mg_dash_find_field_set(dash, mg_str(name)); if (set != NULL) { - if (set->sync) set->sync(false); + if (set->reader) set->reader(); mg_dash_broadcast(mgr, "{%m:%m,%m:{%m:%M}}", MG_ESC("method"), MG_ESC("change"), MG_ESC("params"), MG_ESC(set->name), mg_print_field_set, set); @@ -220,8 +230,8 @@ static inline int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, for (i = 0; set->fields[i].name != NULL; i++) { if (mg_dash_parse_field(val, &set->fields[i])) count++; } - if (count > 0 && set->sync != NULL) { - set->sync(true); + if (count > 0 && set->writer != NULL) { + set->writer(); mg_dash_send_change(c->mgr, dash, set->name); } } else { @@ -250,6 +260,96 @@ static inline void mg_dash_process_msg(struct mg_connection *c, #define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n" #define MG_JSON_HEADERS "Content-Type: application/json\r\n" MG_NO_CACHE_HEADERS +struct mg_upload_state { + char marker; // Tells that we're a file upload connection + size_t expected; // POST data length, bytes + size_t received; // Already received bytes + void *ctx; // OTA context +}; + +static inline void mg_dash_file_add(struct mg_dash *dash, struct mg_str name, + size_t size) { + struct mg_dash_file *f = mg_calloc(1, sizeof(*f)); + f->name = mg_strdup(name).buf; + f->size = size; + f->next = dash->files; + dash->files = f; +} + +static inline void mg_dash_file_del(struct mg_dash *dash, struct mg_str name) { + struct mg_dash_file **head, *f; + for (head = &dash->files, f = *head; f; head = &(*head)->next, f = *head) { + if (mg_strcmp(mg_str(f->name), name) == 0) { + MG_INFO(("Deleting %s", f->name)); + *head = f->next; + mg_free(f->name); + mg_free(f); + return; + } + } +} + +static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + struct mg_upload_state *us = (struct mg_upload_state *) c->data; + if ((ev == MG_EV_HTTP_HDRS || ev == MG_EV_HTTP_MSG) && us->marker == 0) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + if (hm->uri.len > 7) hm->uri.len -= 7, hm->uri.buf += 7; // Strip /files/ + if (sizeof(*us) > sizeof(c->data)) { + mg_error(c, + "FAILURE: sizeof(c->data) == %lu, need %lu." + " Set #define MG_DATA_SIZE XXX", + sizeof(c->data), sizeof(*us)); + } else if (mg_strcmp(hm->query, mg_str("action=delete")) == 0) { + // Query string ?action=delete - file deletion + mg_dash_file_del(c->fn_data, hm->uri); + mg_dash_send_change(c->mgr, c->fn_data, "files"); + mg_http_reply(c, 200, NULL, "true"); + } else if (mg_path_is_sane(hm->uri) == false) { + // Bad file name, return error. Protect from traversal, etc + mg_http_reply(c, 400, NULL, "Bad name"); + } else if (mg_match(hm->method, mg_str("POST"), NULL) || + mg_match(hm->method, mg_str("PUT"), NULL)) { + // File upload + char path[128]; + mg_snprintf(path, sizeof(path), "/tmp/%.*s", hm->uri.len, hm->uri.buf); + us->ctx = fopen(path, "wb+"); + if (us->ctx == NULL) { + mg_http_reply(c, 500, NULL, "File upload error %d\n", errno); + } else { + mg_dash_file_add(c->fn_data, hm->uri, hm->body.len); + MG_DEBUG(("Starting upload, [%.*s] %lu", hm->uri.len, hm->uri.buf, + hm->body.len)); + us->marker = 'U'; + us->expected = hm->body.len; + us->received = 0; + mg_iobuf_del(&c->recv, 0, hm->head.len); + c->pfn = NULL; + c->fn = file_ev_handler; + mg_call(c, MG_EV_READ, &c->recv.len); + } + } else { + // Serve file + char path[128]; + mg_snprintf(path, sizeof(path), "/tmp/%.*s", hm->uri.len, hm->uri.buf); + mg_http_serve_file(c, ev_data, path, NULL); + } + } else if (ev == MG_EV_READ && us->marker == 'U') { + // Write uploaded data + MG_DEBUG(("Uploading.. recv.len=%u", c->recv.len)); + us->received += c->recv.len; + fwrite(c->recv.buf, 1, c->recv.len, us->ctx); + c->recv.len = 0; + if (us->received >= us->expected) { + fclose(us->ctx); + MG_DEBUG(("Uploaded %lu", us->expected)); + mg_http_reply(c, 200, NULL, "%lu uploaded\n", us->expected); + mg_dash_send_change(c->mgr, c->fn_data, "files"); + memset(us, 0, sizeof(*us)); + c->is_draining = 1; + } + } +} + static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { struct mg_dash *dash = (struct mg_dash *) c->fn_data; @@ -259,6 +359,8 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, memset(parts, 0, sizeof(parts)); if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { mg_ws_upgrade(c, hm, NULL); + } else if (mg_match(hm->uri, mg_str("/files/#"), NULL)) { + file_ev_handler(c, ev, ev_data); } else if (mg_match(hm->uri, mg_str("/api/get/#"), parts) || mg_match(hm->uri, mg_str("/api/get"), NULL)) { mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_name, dash, @@ -293,6 +395,16 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, } } +#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32 +#define MG_HTTP_ADDR "http://0.0.0.0:8000" +#define MG_HTTPS_ADDR "https://0.0.0.0:8443" +#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502" +#else +#define MG_HTTP_ADDR "http://0.0.0.0:80" +#define MG_HTTPS_ADDR "http://0.0.0.0:443" +#define MG_MODBUS_ADDR "tcp://0.0.0.0:502" +#endif + // Helper forward declarations for Mongoose CMSIS pack modules extern struct mg_mgr g_mgr; extern void mg_dash_init(struct mg_mgr *); diff --git a/tutorials/device-dashboard/Makefile b/tutorials/device-dashboard/Makefile deleted file mode 100644 index 87634286..00000000 --- a/tutorials/device-dashboard/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -# Environment setup: https://mongoose.ws/docs/getting-started/build-environment/ - -PROG ?= example -MONGOOSE_DIR ?= ../.. -SOURCES = main.c mongoose_dashboard.c dashboard.html.c $(MONGOOSE_DIR)/mongoose.c -CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -g3 -O0 -PFLAGS = -I$(MONGOOSE_DIR) -DMG_DATA_SIZE=64 -DMG_TLS=MG_TLS_BUILTIN -DMG_ENABLE_LINES=1 -AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment - -all: $(PROG) - ./$(PROG) - -# Rebuild the program -$(PROG): $(SOURCES) $(MONGOOSE_DIR)/mongoose.h - $(CC) $(SOURCES) $(CFLAGS) $(PFLAGS) $(AFLAGS) -o $@ - -# Generate a single, self-contained dashboard.html from the UI in web_root/ -dashboard.html: $(wildcard web_root/*) - node $(MONGOOSE_DIR)/resources/inline.js web_root/index.html -o $@ - -# Create an embedded filesystem containing compressed dashboard.html -dashboard.html.c: dashboard.html - node $(MONGOOSE_DIR)/resources/pack.js dashboard.html::gzip > $@ - @echo "On-flash size: `grep -oE '[0-9]+' $@ | wc -l`" - -purge: - curl -s https://purge.jsdelivr.net/gh/cesanta/mongoose@master/resources/dashboard.js - -# For development: rebuild and restart if any of the sources change -watch: - node $(MONGOOSE_DIR)/resources/watch.js -w main.c -w mongoose_dashboard.c -w $(MONGOOSE_DIR)/mongoose.h -w web_root -- make - -clean: - rm -rf $(PROG) *.exe *.obj *.dSYM mongoose dashboard.* diff --git a/tutorials/device-dashboard/full/Makefile b/tutorials/device-dashboard/full/Makefile new file mode 100644 index 00000000..b50492ed --- /dev/null +++ b/tutorials/device-dashboard/full/Makefile @@ -0,0 +1,31 @@ +# Environment setup: https://mongoose.ws/docs/getting-started/build-environment/ + +PROG ?= example +TOP ?= ../../.. +SOURCES = main.c dashboard.c file_data.c $(TOP)/mongoose.c +CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -O0 +PFLAGS = -I$(TOP) -DMG_DATA_SIZE=64 -DMG_TLS=MG_TLS_BUILTIN -DMG_ENABLE_LINES=1 +AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment + +all: $(PROG) + ./$(PROG) + +# Rebuild the program +$(PROG): $(SOURCES) $(TOP)/mongoose.h + $(CC) $(SOURCES) $(CFLAGS) $(PFLAGS) $(AFLAGS) -o $@ + +# Create an embedded filesystem containing compressed dashboard.html +file_data.c: dashboard.html + node $(TOP)/resources/inline.js dashboard.html -o out.html + node $(TOP)/resources/pack.js out.html:dashboard.html:gzip > file_data.c + @echo "On-flash size: `grep -oE '[0-9]+' file_data.c | wc -l`" + +purge: + curl -s https://purge.jsdelivr.net/gh/cesanta/mongoose@master/resources/dashboard.js + +# For development: rebuild and restart if any of the sources change +watch: + node $(TOP)/resources/watch.js -w main.c -w dashboard.c -w $(TOP)/mongoose.h -w dashboard.html -- make + +clean: + rm -rf $(PROG) *.exe *.obj *.dSYM mongoose out.* file_data.c diff --git a/tutorials/device-dashboard/full/dashboard.c b/tutorials/device-dashboard/full/dashboard.c new file mode 100644 index 00000000..ccf3b9f1 --- /dev/null +++ b/tutorials/device-dashboard/full/dashboard.c @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2026 Cesanta Software Limited +// SPDX-License-Identifier: GPL-2.0-only or commercial + +#include "mongoose.h" + +// Control panel +// s_led1, s_led2, s_led3 are used to communicate LED status +static bool s_led1, s_led2, s_led3; + +static struct mg_field fields_leds[] = { + {"led1", MG_VAL_BOOL, &s_led1, sizeof(s_led1)}, + {"led2", MG_VAL_BOOL, &s_led2, sizeof(s_led2)}, + {"led3", MG_VAL_BOOL, &s_led3, sizeof(s_led3)}, + {NULL, 0, NULL, 0}, +}; + +static void write_leds(void) { + // gpio_write(LED1_PIN, s_led1); + // gpio_write(LED2_PIN, s_led2); + // gpio_write(LED3_PIN, s_led3); +} + +static void read_leds(void) { + // s_led1 = gpio_read(LED1_PIN); + // s_led2 = gpio_read(LED2_PIN); + // s_led3 = gpio_read(LED3_PIN); +} + +// Read-only device Metrics +static int s_ram = 32, s_cpu = 7; +static double s_temperature = 24.8; + +static struct mg_field fields_metrics[] = { + {"ram", MG_VAL_INT, &s_ram, sizeof(s_ram)}, + {"cpu", MG_VAL_INT, &s_cpu, sizeof(s_cpu)}, + {"temperature", MG_VAL_DBL, &s_temperature, sizeof(s_temperature)}, + {NULL, 0, NULL, 0}, +}; + +static void read_metrics(void) { + s_ram = 25 + (rand() % 16); // Sumulate metric change + s_cpu = 7 + (rand() % 21); + s_temperature = 14.8 + ((double) rand() / RAND_MAX) * 20.0; +} + +// Read-write device settings +static double s_volume = 17.2; +static int s_log_level = MG_LL_DEBUG; +static char s_name[20] = "Dublin"; + +static struct mg_field fields_settings[] = { + {"volume", MG_VAL_DBL, &s_volume, sizeof(s_volume)}, + {"name", MG_VAL_STR, &s_name, sizeof(s_name)}, + {"log_level", MG_VAL_INT, &s_log_level, sizeof(s_log_level)}, + {NULL, 0, NULL, 0}, +}; + +static void write_settings(void) { + mg_log_level = s_log_level; +} + +static void read_settings(void) { + s_log_level = mg_log_level; +} + +#define NUM_POINTS 7 // How many data points to send + +struct point { + uint16_t x; // X coordinate of the data point, usually this is time + uint16_t y1; // We can plot multiple values per graph, + uint16_t y2; // let's plot 2 values: y1 and y2. +}; + +// Serialise chart data into a string: [[x,y1,y2],[x,y1,y2],...] +// One uint16_t takes 5 character max, so one entry is 19 bytes max. +// We could be more efficient by storing hex or base64-encoded data, but +// let's keep it simple. However if we do, update data parsing logic in +// the web_root/main.js +#define POINT_SIZE 19 +static char s_chart1_data[NUM_POINTS * POINT_SIZE + 2 + 1]; + +static struct mg_field fields_chart1[] = { + {"data", MG_VAL_RAW, s_chart1_data, sizeof(s_chart1_data)}, + {NULL, 0, NULL, 0}, +}; + +// Randomly generate graph points and serialise them into a string +void read_chart1(void) { + size_t i, len = 0; + len += mg_snprintf(s_chart1_data + len, sizeof(s_chart1_data) - len, "["); + for (i = 0; i < NUM_POINTS; i++) { + struct point p; + p.x = (uint16_t) i; + p.y1 = 85 - (uint16_t) ((49 * i) / NUM_POINTS) + (rand() % 9); + p.y2 = 20 + (uint16_t) ((69 * i) / NUM_POINTS) + (rand() % 5); + len += mg_snprintf(s_chart1_data + len, sizeof(s_chart1_data) - len, + "%s[%hu,%hu,%hu]", i > 0 ? "," : "", p.x, p.y1, p.y2); + } + len += mg_snprintf(s_chart1_data + len, sizeof(s_chart1_data) - len, "]"); +} + +// Files array: [{"name": "foo.txt", "size": 1234}] +static char s_files[1024]; +static struct mg_field fields_files[] = { + {"data", MG_VAL_RAW, s_files, sizeof(s_files)}, + {NULL, 0, NULL, 0}, +}; +static struct mg_dash s_dashboard; +static void read_files(void) { + size_t len = 0; + struct mg_dash_file *f; + len += mg_snprintf(s_files + len, sizeof(s_files) - len, "["); + for (f = s_dashboard.files; f; f = f->next) { + len += mg_snprintf(s_files + len, sizeof(s_files) - len, "%s{%m:%m,%m:%u}", + len > 1 ? "," : "", MG_ESC("name"), MG_ESC(f->name), + MG_ESC("size"), f->size); + } + len += mg_snprintf(s_files + len, sizeof(s_files) - len, "]"); +} + +// Each field set exports REST HTTP API /api/NAME +static struct mg_field_set field_sets[] = { + {"leds", fields_leds, read_leds, write_leds, 0, 0}, + {"metrics", fields_metrics, read_metrics, NULL, 0, 0}, + {"settings", fields_settings, read_settings, write_settings, 0, 0}, + {"chart1", fields_chart1, read_chart1, NULL, 0, 0}, + {"files", fields_files, read_files, NULL, 0, 0}, + {0, 0, NULL, NULL, 0, 0}, +}; +static struct mg_dash s_dashboard = {field_sets, NULL, NULL}; + +void mg_dash_init(struct mg_mgr *mgr) { + mg_dash_file_add(&s_dashboard, mg_str("device-config.json"), 1234); + mg_dash_file_add(&s_dashboard, mg_str("device-log-2026-04-25.txt"), 1327854); + mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &s_dashboard); +} + +void mg_dash_poll(struct mg_mgr *mgr) { + // Send metrics change periodically + static uint64_t timer = 0; + if (mg_timer_expired(&timer, 30000, mg_now())) { + mg_dash_send_change(mgr, &s_dashboard, "metrics"); + } +} diff --git a/tutorials/device-dashboard/web_root/index.html b/tutorials/device-dashboard/full/dashboard.html similarity index 51% rename from tutorials/device-dashboard/web_root/index.html rename to tutorials/device-dashboard/full/dashboard.html index 544daada..29e42e33 100644 --- a/tutorials/device-dashboard/web_root/index.html +++ b/tutorials/device-dashboard/full/dashboard.html @@ -18,8 +18,184 @@ }); - - + + @@ -33,7 +209,25 @@