From 8534053e5795a6114ec854c47e8229f47889655b Mon Sep 17 00:00:00 2001 From: cpq Date: Sat, 25 Apr 2026 09:37:09 +0100 Subject: [PATCH] Event log --- mongoose.c | 36 ++++++------ mongoose.h | 27 ++++++++- src/dash.h | 27 ++++++++- src/http.c | 36 ++++++------ tutorials/device-dashboard/Makefile | 2 +- .../device-dashboard/mongoose_dashboard.c | 51 ++++++++++++----- .../device-dashboard/web_root/index.html | 57 ++++++------------- tutorials/device-dashboard/web_root/main.js | 6 +- tutorials/device-dashboard/web_root/style.css | 36 +++++++++++- 9 files changed, 182 insertions(+), 96 deletions(-) diff --git a/mongoose.c b/mongoose.c index 485238e0..481b5526 100644 --- a/mongoose.c +++ b/mongoose.c @@ -1831,9 +1831,6 @@ struct mg_fs mg_fs_posix = {p_stat, p_list, p_open, p_close, p_read, - - - static int mg_ncasecmp(const char *s1, const char *s2, size_t len) { int diff = 0; if (len > 0) do { @@ -2078,11 +2075,14 @@ static bool mg_http_parse_headers(const char *s, const char *end, v.len--; // Trim spaces } // detect duplicated headers -> discard - if (((mg_strcasecmp(k, mg_str("Content-Length")) == 0) && (++cl_count > 1)) || - ((mg_strcasecmp(k, mg_str("Transfer-Encoding")) == 0) && (++te_count > 1)) || - ((mg_strcasecmp(k, mg_str("Authorization")) == 0) && (++auth_count > 1)) || - ((mg_strcasecmp(k, mg_str("Cookie")) == 0) && (++cookie_count > 1)) || - ((mg_strcasecmp(k, mg_str("Connection")) == 0) && (++conn_count > 1))) + if (((mg_strcasecmp(k, mg_str("Content-Length")) == 0) && + (++cl_count > 1)) || + ((mg_strcasecmp(k, mg_str("Transfer-Encoding")) == 0) && + (++te_count > 1)) || + ((mg_strcasecmp(k, mg_str("Authorization")) == 0) && + (++auth_count > 1)) || + ((mg_strcasecmp(k, mg_str("Cookie")) == 0) && (++cookie_count > 1)) || + ((mg_strcasecmp(k, mg_str("Connection")) == 0) && (++conn_count > 1))) return false; // MG_INFO(("--HH [%.*s] [%.*s]", (int) k.len, k.buf, (int) v.len, v.buf)); h[i].name = k, h[i].value = v; // Success. Assign values @@ -2119,7 +2119,7 @@ int mg_http_parse(const char *s, size_t len, struct mg_http_message *hm) { version_prefix_valid = hm->proto.len > 5 && (mg_ncasecmp(hm->proto.buf, "HTTP/", 5) == 0); if (!is_response && !version_prefix_valid) - return -1; // no version detected in request + return -1; // no version detected in request if (!is_response && hm->proto.len > 0 && (!version_prefix_valid || hm->proto.len != 8 || (hm->proto.buf[5] < '0' || hm->proto.buf[5] > '9') || @@ -2402,12 +2402,14 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm, const char *path, const struct mg_http_serve_opts *opts) { char etag[64], tmp[MG_PATH_MAX]; - struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; + struct mg_fs *fs = opts && opts->fs ? opts->fs : &mg_fs_posix; struct mg_fd *fd = NULL; size_t size = 0; time_t mtime = 0; + const char *mime_types = opts && opts->mime_types ? opts->mime_types : NULL; + const char *hdrs = opts && opts->extra_headers ? opts->extra_headers : ""; struct mg_str *inm = NULL; - struct mg_str mime = guess_content_type(mg_str(path), opts->mime_types); + struct mg_str mime = guess_content_type(mg_str(path), mime_types); bool gzip = false; if (path != NULL) { @@ -2425,21 +2427,21 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm, } // Failed to open, and page404 is configured? Open it, then - if (fd == NULL && opts->page404 != NULL) { + if (fd == NULL && opts && opts->page404) { fd = mg_fs_open(fs, opts->page404, MG_FS_READ); path = opts->page404; - mime = guess_content_type(mg_str(path), opts->mime_types); + mime = guess_content_type(mg_str(path), mime_types); } if (fd == NULL || fs->st(path, &size, &mtime) == 0) { - mg_http_reply(c, 404, opts->extra_headers, "Not found\n"); + mg_http_reply(c, 404, hdrs, "Not found\n"); mg_fs_close(fd); // NOTE: mg_http_etag() call should go first! } else if (mg_http_etag(etag, sizeof(etag), size, mtime) != NULL && (inm = mg_http_get_header(hm, "If-None-Match")) != NULL && mg_strcasecmp(*inm, mg_str(etag)) == 0) { mg_fs_close(fd); - mg_http_reply(c, 304, opts->extra_headers, ""); + mg_http_reply(c, 304, hdrs, ""); } else { int n, status = 200; char range[100]; @@ -2473,11 +2475,11 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm, "%s%s%s\r\n", status, mg_http_status_code_str(status), (int) mime.len, mime.buf, etag, (uint64_t) cl, gzip ? "Content-Encoding: gzip\r\n" : "", - range, opts->extra_headers ? opts->extra_headers : ""); + range, hdrs); if (mg_strcasecmp(hm->method, mg_str("HEAD")) == 0 || c->is_closing) { c->is_resp = 0; mg_fs_close(fd); - } else { // start serving static content only if not closing, see #3354 + } else { // start serving static content only if not closing, see #3354 // Track to-be-sent content length at the end of c->data, aligned size_t *clp = (size_t *) &c->data[(sizeof(c->data) - sizeof(size_t)) / sizeof(size_t) * sizeof(size_t)]; diff --git a/mongoose.h b/mongoose.h index 27a3e29f..081446fe 100644 --- a/mongoose.h +++ b/mongoose.h @@ -3177,10 +3177,26 @@ struct mg_field_set { int write_level; }; +struct mg_dash_custom_handler { + struct mg_dash_custom_handler *next; + struct mg_str uri_pattern; + mg_event_handler_t handler; +}; + struct mg_dash { struct mg_field_set *sets; + struct mg_dash_custom_handler *custom_handlers; }; +#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_; \ + } while (0) + static inline struct mg_str trimq(struct mg_str s) { // Trim double quotes if (s.len > 1 && s.buf[0] == '"') s.len -= 2, s.buf++; return s; @@ -3394,12 +3410,21 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, } else if (mg_match(hm->uri, mg_str("/api/set"), NULL)) { int count = mg_dash_apply(c, dash, hm->body); mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count); - } else { + } else if (mg_match(hm->uri, mg_str("/"), NULL)) { struct mg_http_serve_opts opts; memset(&opts, 0, sizeof(opts)); opts.fs = &mg_fs_packed; mg_mem_files = mg_packed_files; mg_http_serve_file(c, hm, "/dashboard.html", &opts); + } else { + struct mg_dash_custom_handler *ch = dash->custom_handlers; + for (ch = dash->custom_handlers; ch != NULL; ch = ch->next) { + if (mg_match(hm->uri, ch->uri_pattern, NULL)) { + ch->handler(c, ev, ev_data); + break; + } + } + if (ch == NULL) mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found"); } } else if (ev == MG_EV_WS_MSG) { // Add this to automatically handle "get" and "set" JSON-RPC calls diff --git a/src/dash.h b/src/dash.h index b8fc0d4d..357c368c 100644 --- a/src/dash.h +++ b/src/dash.h @@ -32,10 +32,26 @@ struct mg_field_set { int write_level; }; +struct mg_dash_custom_handler { + struct mg_dash_custom_handler *next; + struct mg_str uri_pattern; + mg_event_handler_t handler; +}; + struct mg_dash { struct mg_field_set *sets; + struct mg_dash_custom_handler *custom_handlers; }; +#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_; \ + } while (0) + static inline struct mg_str trimq(struct mg_str s) { // Trim double quotes if (s.len > 1 && s.buf[0] == '"') s.len -= 2, s.buf++; return s; @@ -249,12 +265,21 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, } else if (mg_match(hm->uri, mg_str("/api/set"), NULL)) { int count = mg_dash_apply(c, dash, hm->body); mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count); - } else { + } else if (mg_match(hm->uri, mg_str("/"), NULL)) { struct mg_http_serve_opts opts; memset(&opts, 0, sizeof(opts)); opts.fs = &mg_fs_packed; mg_mem_files = mg_packed_files; mg_http_serve_file(c, hm, "/dashboard.html", &opts); + } else { + struct mg_dash_custom_handler *ch = dash->custom_handlers; + for (ch = dash->custom_handlers; ch != NULL; ch = ch->next) { + if (mg_match(hm->uri, ch->uri_pattern, NULL)) { + ch->handler(c, ev, ev_data); + break; + } + } + if (ch == NULL) mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found"); } } else if (ev == MG_EV_WS_MSG) { // Add this to automatically handle "get" and "set" JSON-RPC calls diff --git a/src/http.c b/src/http.c index 0a4d2c8c..e2f6095b 100644 --- a/src/http.c +++ b/src/http.c @@ -1,15 +1,12 @@ #include "http.h" -#include "arch.h" #include "base64.h" #include "fmt.h" -#include "json.h" #include "log.h" #include "net.h" #include "printf.h" #include "ssi.h" #include "util.h" #include "version.h" -#include "ws.h" static int mg_ncasecmp(const char *s1, const char *s2, size_t len) { int diff = 0; @@ -255,11 +252,14 @@ static bool mg_http_parse_headers(const char *s, const char *end, v.len--; // Trim spaces } // detect duplicated headers -> discard - if (((mg_strcasecmp(k, mg_str("Content-Length")) == 0) && (++cl_count > 1)) || - ((mg_strcasecmp(k, mg_str("Transfer-Encoding")) == 0) && (++te_count > 1)) || - ((mg_strcasecmp(k, mg_str("Authorization")) == 0) && (++auth_count > 1)) || - ((mg_strcasecmp(k, mg_str("Cookie")) == 0) && (++cookie_count > 1)) || - ((mg_strcasecmp(k, mg_str("Connection")) == 0) && (++conn_count > 1))) + if (((mg_strcasecmp(k, mg_str("Content-Length")) == 0) && + (++cl_count > 1)) || + ((mg_strcasecmp(k, mg_str("Transfer-Encoding")) == 0) && + (++te_count > 1)) || + ((mg_strcasecmp(k, mg_str("Authorization")) == 0) && + (++auth_count > 1)) || + ((mg_strcasecmp(k, mg_str("Cookie")) == 0) && (++cookie_count > 1)) || + ((mg_strcasecmp(k, mg_str("Connection")) == 0) && (++conn_count > 1))) return false; // MG_INFO(("--HH [%.*s] [%.*s]", (int) k.len, k.buf, (int) v.len, v.buf)); h[i].name = k, h[i].value = v; // Success. Assign values @@ -296,7 +296,7 @@ int mg_http_parse(const char *s, size_t len, struct mg_http_message *hm) { version_prefix_valid = hm->proto.len > 5 && (mg_ncasecmp(hm->proto.buf, "HTTP/", 5) == 0); if (!is_response && !version_prefix_valid) - return -1; // no version detected in request + return -1; // no version detected in request if (!is_response && hm->proto.len > 0 && (!version_prefix_valid || hm->proto.len != 8 || (hm->proto.buf[5] < '0' || hm->proto.buf[5] > '9') || @@ -579,12 +579,14 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm, const char *path, const struct mg_http_serve_opts *opts) { char etag[64], tmp[MG_PATH_MAX]; - struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; + struct mg_fs *fs = opts && opts->fs ? opts->fs : &mg_fs_posix; struct mg_fd *fd = NULL; size_t size = 0; time_t mtime = 0; + const char *mime_types = opts && opts->mime_types ? opts->mime_types : NULL; + const char *hdrs = opts && opts->extra_headers ? opts->extra_headers : ""; struct mg_str *inm = NULL; - struct mg_str mime = guess_content_type(mg_str(path), opts->mime_types); + struct mg_str mime = guess_content_type(mg_str(path), mime_types); bool gzip = false; if (path != NULL) { @@ -602,21 +604,21 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm, } // Failed to open, and page404 is configured? Open it, then - if (fd == NULL && opts->page404 != NULL) { + if (fd == NULL && opts && opts->page404) { fd = mg_fs_open(fs, opts->page404, MG_FS_READ); path = opts->page404; - mime = guess_content_type(mg_str(path), opts->mime_types); + mime = guess_content_type(mg_str(path), mime_types); } if (fd == NULL || fs->st(path, &size, &mtime) == 0) { - mg_http_reply(c, 404, opts->extra_headers, "Not found\n"); + mg_http_reply(c, 404, hdrs, "Not found\n"); mg_fs_close(fd); // NOTE: mg_http_etag() call should go first! } else if (mg_http_etag(etag, sizeof(etag), size, mtime) != NULL && (inm = mg_http_get_header(hm, "If-None-Match")) != NULL && mg_strcasecmp(*inm, mg_str(etag)) == 0) { mg_fs_close(fd); - mg_http_reply(c, 304, opts->extra_headers, ""); + mg_http_reply(c, 304, hdrs, ""); } else { int n, status = 200; char range[100]; @@ -650,11 +652,11 @@ void mg_http_serve_file(struct mg_connection *c, struct mg_http_message *hm, "%s%s%s\r\n", status, mg_http_status_code_str(status), (int) mime.len, mime.buf, etag, (uint64_t) cl, gzip ? "Content-Encoding: gzip\r\n" : "", - range, opts->extra_headers ? opts->extra_headers : ""); + range, hdrs); if (mg_strcasecmp(hm->method, mg_str("HEAD")) == 0 || c->is_closing) { c->is_resp = 0; mg_fs_close(fd); - } else { // start serving static content only if not closing, see #3354 + } else { // start serving static content only if not closing, see #3354 // Track to-be-sent content length at the end of c->data, aligned size_t *clp = (size_t *) &c->data[(sizeof(c->data) - sizeof(size_t)) / sizeof(size_t) * sizeof(size_t)]; diff --git a/tutorials/device-dashboard/Makefile b/tutorials/device-dashboard/Makefile index ff3bc858..ce28b833 100644 --- a/tutorials/device-dashboard/Makefile +++ b/tutorials/device-dashboard/Makefile @@ -4,7 +4,7 @@ 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 +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) diff --git a/tutorials/device-dashboard/mongoose_dashboard.c b/tutorials/device-dashboard/mongoose_dashboard.c index 5fc8ab7b..4c09243b 100644 --- a/tutorials/device-dashboard/mongoose_dashboard.c +++ b/tutorials/device-dashboard/mongoose_dashboard.c @@ -10,24 +10,24 @@ #endif // Control panel -static bool s_fan, s_relay1, s_relay2; +static bool s_led1, s_led2, s_led3; -static struct mg_field fields_control[] = { - {"fan", MG_VAL_BOOL, &s_fan, sizeof(s_fan)}, - {"relay1", MG_VAL_BOOL, &s_relay1, sizeof(s_relay1)}, - {"relay2", MG_VAL_BOOL, &s_relay2, sizeof(s_relay2)}, +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 sync_control(bool is_write) { +static void sync_leds(bool is_write) { if (is_write) { - // gpio_write(FAN_PIN, s_fan); - // gpio_write(RELAY_1_PIN, s_relay1); - // gpio_write(RELAY_2_PIN, s_relay2); + // gpio_write(LED1_PIN, s_led1); + // gpio_write(LED2_PIN, s_led2); + // gpio_write(LED3_PIN, s_led3); } else { - // s_fan = gpio_read(FAN_PIN); - // s_relay1 = gpio_read(RELAY_1_PIN); - // s_relay2 = gpio_read(RELAY_2_PIN); + // s_led1 = gpio_read(LED1_PIN); + // s_led2 = gpio_read(LED2_PIN); + // s_led3 = gpio_read(LED3_PIN); } } @@ -107,16 +107,39 @@ void sync_chart1(bool is_write) { len += mg_snprintf(s_chart1_data + len, sizeof(s_chart1_data) - len, "]"); } +static int s_num_log_files = 0; +static int s_log_file_index = 0; +static char s_log_file_name[100]; +static struct mg_field fields_log_files[] = { + {"size", MG_VAL_INT, &s_num_log_files, sizeof(s_num_log_files)}, + {"index", MG_VAL_INT, &s_log_file_index, sizeof(s_log_file_index)}, + {"name", MG_VAL_STR, &s_log_file_name, sizeof(s_log_file_name)}, + {NULL, 0, NULL, 0}, +}; +static void sync_log_files(bool is_write) { + if (is_write) return; + mg_log_level = s_log_level; + s_log_level = mg_log_level; +} + static struct mg_field_set field_sets[] = { - {"controls", fields_control, sync_control, 0, 0}, + {"leds", fields_leds, sync_leds, 0, 0}, {"metrics", fields_metrics, sync_metrics, 0, 0}, {"settings", fields_settings, sync_settings, 0, 0}, {"chart1", fields_chart1, sync_chart1, 0, 0}, + {"log_files", fields_log_files, sync_log_files, 0, 0}, {0, 0, 0, 0, 0}, }; -static struct mg_dash s_dashboard = {field_sets}; +static struct mg_dash s_dashboard = {field_sets, NULL}; + +static void log_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_HTTP_MSG) { + mg_http_serve_file(c, ev_data, "Makefile", NULL); + } +} void mg_dash_init(struct mg_mgr *mgr) { + MG_DASH_REGISTER_CUSTOM_HANDLER(&s_dashboard, "/logs/#", log_ev_handler); mg_http_listen(mgr, HTTP_ADDR, mg_dash_ev_handler, &s_dashboard); } diff --git a/tutorials/device-dashboard/web_root/index.html b/tutorials/device-dashboard/web_root/index.html index f23a1740..578bc356 100644 --- a/tutorials/device-dashboard/web_root/index.html +++ b/tutorials/device-dashboard/web_root/index.html @@ -10,22 +10,9 @@ debug: true, data: { chart1: {data: ''}, - controls: { - fan: true, - relay1: false, - relay2: false, - }, - settings: { - volume: 12, - log_level: 3, - name: 'Galway', - }, - metrics: { - temperature: 1.2, - humidity: 3, - cpu: 4, - ram: 5, - }, + leds: { led1: true, led2: false, led3: false }, + settings: { volume: 12, log_level: 3, name: 'Galway' }, + metrics: { temperature: 1.2, humidity: 3, cpu: 4, ram: 5 }, }, }); @@ -75,7 +62,7 @@ - Events + Event Log @@ -175,30 +162,19 @@
-
Controls
+
LED Control
-
-
-
-
Cooling Fan
-
PWM controlled output
-
- -
-
-
-
Relay Channel 1
-
Auxiliary load output
-
- -
-
-
-
Relay Channel 2
-
Safety-controlled line
-
- -
+
+
LED1
+ +
+
+
LED2
+ +
+
+
LED3
+
@@ -236,6 +212,7 @@ diff --git a/tutorials/device-dashboard/web_root/main.js b/tutorials/device-dashboard/web_root/main.js index 380e8a7d..a128ccbb 100644 --- a/tutorials/device-dashboard/web_root/main.js +++ b/tutorials/device-dashboard/web_root/main.js @@ -86,6 +86,8 @@ document.addEventListener("DOMContentLoaded", () => { Dashboard.on('change', args => updateGraphPoints(args?.chart1?.data)); Dashboard.call('get', 'chart1').then(resp => updateGraphPoints(resp?.data)); - // Set iframe source in JS, to prevent Monolith killing it - document.getElementById("logs").src = "/logs/"; + // Show logs + fetch('logs/') + .then(resp => resp.text()) + .then(text => document.getElementById("logs").textContent = text); }); diff --git a/tutorials/device-dashboard/web_root/style.css b/tutorials/device-dashboard/web_root/style.css index d1b88248..4de432dd 100644 --- a/tutorials/device-dashboard/web_root/style.css +++ b/tutorials/device-dashboard/web_root/style.css @@ -265,18 +265,19 @@ body { .field { display: flex; flex-direction: row; + justify-content: space-between; align-items: center; gap: 0.75rem; } .field-label { - font-size: 0.72rem; + font-size: 0.75rem; font-weight: 700; - letter-spacing: 0.05em; text-transform: uppercase; color: #94a3b8; - width: 4.5rem; flex-shrink: 0; + white-space: nowrap; + min-width: 6rem; } .field-input { @@ -419,6 +420,35 @@ body { flex-shrink: 0; } +.file-content { + width: 100%; + min-height: calc(100vh - 7rem); + margin-top: 0.5rem; + padding: 1rem 1.15rem; + overflow: auto; + color: #dbeafe; + background: + linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.98)), + #0f172a; + border: 1px solid rgba(51, 65, 85, 0.9); + border-radius: 1rem; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 2px 6px rgba(15, 23, 42, 0.1), + 0 12px 32px rgba(15, 23, 42, 0.14); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 0.82rem; + line-height: 1.55; + white-space: pre-wrap; + word-break: break-word; + tab-size: 2; +} + +.file-content:empty::before { + content: "Waiting for events..."; + color: #64748b; +} + /* Chart */ .chart-panel {