From 59f9b36dddc348166593c24402c02f863810e34d Mon Sep 17 00:00:00 2001 From: cpq Date: Fri, 1 May 2026 10:01:18 +0100 Subject: [PATCH] Dash updates --- mongoose.c | 378 ++++++++++++++++++ mongoose.h | 357 +---------------- resources/dashboard.js | 40 +- resources/watch.js | 22 +- src/dash.c | 374 +++++++++++++++++ src/dash.h | 357 +---------------- tutorials/device-dashboard/full/dashboard.c | 182 ++++++--- .../device-dashboard/full/dashboard.html | 228 +++++++---- 8 files changed, 1114 insertions(+), 824 deletions(-) create mode 100644 src/dash.c diff --git a/mongoose.c b/mongoose.c index 481b5526..0d4f7fc5 100644 --- a/mongoose.c +++ b/mongoose.c @@ -116,6 +116,384 @@ fail: return 0; } +#ifdef MG_ENABLE_LINES +#line 1 "src/dash.c" +#endif + + +#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 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; +} + +static void mg_dash_broadcast(struct mg_mgr *mgr, const char *fmt, ...) { + struct mg_connection *c; + va_list ap; + for (c = mgr->conns; c != NULL; c = c->next) { + if (!c->is_websocket) continue; + if (c->send.len > MG_DASH_MAX_SEND_BUF_SIZE) { + MG_ERROR(("%lu buffered data %lu > MG_DASH_MAX_SEND_BUF_SIZE, throttled", + c->id, c->send.len)); + continue; + } + va_start(ap, fmt); + mg_ws_vprintf(c, WEBSOCKET_OP_TEXT, fmt, &ap); + va_end(ap); + } +} + +static size_t mg_print_fmt(mg_pfn_t out, void *param, va_list *ap) { + const char *fmt = va_arg(*ap, const char *); + ap = va_arg(*ap, va_list *); + return mg_vxprintf(out, param, fmt, ap); +} + +static void mg_dash_success(struct mg_connection *c, struct mg_str req, + const char *fmt, ...) { + struct mg_str id = mg_json_get_tok(req, "$.id"); + va_list ap; + va_start(ap, fmt); + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:%M}", MG_ESC("id"), id.len, + id.buf, MG_ESC("result"), mg_print_fmt, fmt, &ap); + va_end(ap); +} + +static void mg_dash_error(struct mg_connection *c, struct mg_str req, + const char *fmt, ...) { + struct mg_str id = mg_json_get_tok(req, "$.id"); + va_list ap; + va_start(ap, fmt); + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:{%m:%d,%m:%m}}", MG_ESC("id"), + id.len, id.buf, MG_ESC("error"), MG_ESC("code"), -1, + MG_ESC("message"), mg_print_fmt, fmt, &ap); + va_end(ap); +} + +static struct mg_field_set *mg_dash_find_field_set(struct mg_dash *dash, + struct mg_str name) { + struct mg_field_set *fs; + for (fs = dash->sets; fs != NULL; fs = fs->next) { + if (mg_strcmp(name, mg_str(fs->name)) == 0) return fs; + } + return NULL; +} + +// static struct mg_field *mg_dash_find_field(struct mg_field *fields, +// struct mg_str name) { +// size_t i; +// for (i = 0; fields != NULL && fields[i].name != NULL; i++) { +// if (mg_strcmp(name, mg_str(fields[i].name)) == 0) return &fields[i]; +// } +// return NULL; +// } + +static size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_field *f = va_arg(*ap, struct mg_field *); + size_t n = 0; + n += mg_xprintf(fn, arg, "%m:", MG_ESC(f->name)); + if (f->type == MG_VAL_BOOL) { + n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false"); + } else if (f->type == MG_VAL_INT) { + n += mg_xprintf(fn, arg, "%d", *(int *) f->value); + } else if (f->type == MG_VAL_DBL) { + n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value); + } else if (f->type == MG_VAL_STR) { + n += mg_xprintf(fn, arg, "%m", MG_ESC(f->value)); + } else if (f->type == MG_VAL_RAW) { + n += mg_xprintf(fn, arg, "%s", f->value); + } else { + n += mg_xprintf(fn, arg, "null"); + } + return n; +} + +static size_t mg_print_field_set(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_field_set *set = va_arg(*ap, struct mg_field_set *); + size_t i, n = 0; + n += mg_xprintf(fn, arg, "{"); + for (i = 0; set != NULL && set->fields[i].name != NULL; i++) { + if (i > 0) n += mg_xprintf(fn, arg, ","); + n += mg_xprintf(fn, arg, "%M", mg_print_field, &set->fields[i]); + } + n += mg_xprintf(fn, arg, "}"); + return n; +} + +static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_dash *dash = va_arg(*ap, struct mg_dash *); + struct mg_str *name = va_arg(*ap, struct mg_str *); + struct mg_field_set *set = mg_dash_find_field_set(dash, *name); + size_t n = 0; + if (name->len == 0) { + struct mg_field_set *fs; + const char *comma = ""; + n += mg_xprintf(fn, arg, "{"); + for (fs = dash->sets; fs != NULL; fs = fs->next) { + if (fs->reader) fs->reader(); + n += mg_xprintf(fn, arg, comma); + n += mg_xprintf(fn, arg, "%m:", MG_ESC(fs->name)); + n += mg_xprintf(fn, arg, "%M", mg_print_field_set, fs); + comma = ","; + } + n += mg_xprintf(fn, arg, "}"); + } else if (set != NULL) { + if (set->reader) set->reader(); + n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); + } else { + n += mg_xprintf(fn, arg, "null"); + } + return n; +} + +static size_t mg_dash_print(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_str *req = va_arg(*ap, struct mg_str *); + struct mg_dash *dash = va_arg(*ap, struct mg_dash *); + struct mg_str name = trimq(mg_json_get_tok(*req, "$.params")); + return mg_xprintf(fn, arg, "%M", mg_dash_print_name, dash, &name); +} + +void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *set) { + 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); +} + +static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { + char json_path[128]; + bool ok = false; + mg_snprintf(json_path, sizeof(json_path), "$.%s", f->name); + if (f->type == MG_VAL_BOOL) { + ok = mg_json_get_bool(json, json_path, (bool *) f->value); + } else if (f->type == MG_VAL_INT) { + double d; + ok = mg_json_get_num(json, json_path, &d); + if (ok) *(int *) f->value = (int) d; + } else if (f->type == MG_VAL_DBL) { + ok = mg_json_get_num(json, json_path, (double *) f->value); + } else if (f->type == MG_VAL_STR) { + ok = mg_json_unescape(json, json_path, (char *) f->value, f->value_size); + } else if (f->type == MG_VAL_RAW) { + ok = mg_snprintf((char *) f->value, f->value_size, "%.*s", json.len, + json.buf) == json.len; + } + return ok; +} + +static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, + struct mg_str json) { + struct mg_str key, val; + size_t ofs = 0; + int count = 0; + // struct mg_field *f = NULL; + while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) { + struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key)); + if (set != NULL) { + size_t i; + for (i = 0; set->fields[i].name != NULL; i++) { + if (mg_dash_parse_field(val, &set->fields[i])) count++; + } + if (count > 0) { + if (set->writer) set->writer(); + mg_dash_send_change(c->mgr, set); + } + } else { + MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); + } + } + return count; +} + +static void mg_dash_process_msg(struct mg_connection *c, + struct mg_ws_message *wm, + struct mg_dash *dash) { + struct mg_str req = wm->data; + struct mg_str method = trimq(mg_json_get_tok(req, "$.method")); + if (mg_match(method, mg_str("get"), NULL)) { + mg_dash_success(c, req, "%M", mg_dash_print, &req, dash); + } else if (mg_match(method, mg_str("set"), NULL)) { + struct mg_str params = trimq(mg_json_get_tok(req, "$.params")); + int count = mg_dash_apply(c, dash, params); + mg_dash_success(c, req, "%d", count); + } else { + mg_dash_error(c, req, "%s", "unknown method"); + } +} + +void mg_dash_file_add(struct mg_str name, size_t size) { + struct mg_dash_file *f = (struct mg_dash_file *) mg_calloc(1, sizeof(*f)); + f->name = mg_strdup(name).buf; + f->size = size; + f->next = mg_dash_files; + mg_dash_files = f; +} + +void mg_dash_file_del(struct mg_str name) { + struct mg_dash_file **head, *f; + for (head = &mg_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; + } + } +} + +// Files array: [{"name": "foo.txt", "size": 1234}] +struct mg_dash_file *mg_dash_files; +static char s_files[1024]; + +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 = mg_dash_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, "]"); +} + +static struct mg_field fields_files[] = { + {"data", MG_VAL_RAW, s_files, sizeof(s_files)}, + {NULL, MG_VAL_INT, NULL, 0}, +}; + +static struct mg_field_set set_files = { + "files", fields_files, read_files, NULL, 0, 0, NULL, +}; + +static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + // struct mg_dash *dash = (struct mg_dash *) c->fn_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(hm->uri); + mg_dash_send_change(c->mgr, &set_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(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, hm, 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, (FILE *) us->ctx); + c->recv.len = 0; + if (us->received >= us->expected) { + fclose((FILE *) 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, &set_files); + memset(us, 0, sizeof(*us)); + c->is_draining = 1; + } + } +} + +void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + struct mg_dash *dash = (struct mg_dash *) c->fn_data; + if (ev == MG_EV_OPEN) { + if (mg_dash_find_field_set(dash, mg_str("files")) == NULL) { + MG_DASH_ADD_FIELD_SET(dash, &set_files); + } + } else if (ev == MG_EV_HTTP_MSG) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + struct mg_str parts[5]; + 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, + &parts[0]); + } 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 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_OPEN) { + // WS connection established, send change notifications for all data + struct mg_field_set *fs; + for (fs = dash->sets; fs != NULL; fs = fs->next) { + mg_dash_send_change(c->mgr, fs); + } + // Send change notification with no params to indicate we're done for now + mg_dash_broadcast(c->mgr, "{%m:%m}", MG_ESC("method"), MG_ESC("change")); + } else if (ev == MG_EV_WS_MSG) { + // Add this to automatically handle "get" and "set" JSON-RPC calls + struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; + if (dash == NULL) { + mg_dash_error(c, wm->data, "%s", "no dash defined"); + } else { + mg_dash_process_msg(c, wm, dash); + } + } +} + #ifdef MG_ENABLE_LINES #line 1 "src/dns.c" #endif diff --git a/mongoose.h b/mongoose.h index 2bb2e67f..febf140b 100644 --- a/mongoose.h +++ b/mongoose.h @@ -3174,6 +3174,7 @@ struct mg_field_set { void (*writer)(void); int read_level; int write_level; + struct mg_field_set *next; }; struct mg_dash_custom_handler { @@ -3192,9 +3193,15 @@ struct mg_dash_file { struct mg_dash { struct mg_field_set *sets; struct mg_dash_custom_handler *custom_handlers; - struct mg_dash_file *files; + //struct mg_dash_file *files; }; +#define MG_DASH_ADD_FIELD_SET(dash_, set_) \ + do { \ + (set_)->next = (dash_)->sets; \ + (dash_)->sets = (set_); \ + } while (0) + #define MG_DASH_REGISTER_CUSTOM_HANDLER(dash_, uri_, fn_, data_) \ do { \ static struct mg_dash_custom_handler ch_; \ @@ -3205,342 +3212,6 @@ struct mg_dash { (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; -} - -static inline void mg_dash_broadcast(struct mg_mgr *mgr, const char *fmt, ...) { - struct mg_connection *c; - va_list ap; - for (c = mgr->conns; c != NULL; c = c->next) { - if (!c->is_websocket) continue; - va_start(ap, fmt); - mg_ws_vprintf(c, WEBSOCKET_OP_TEXT, fmt, &ap); - va_end(ap); - } -} - -static inline size_t mg_print_fmt(mg_pfn_t out, void *param, va_list *ap) { - const char *fmt = va_arg(*ap, const char *); - ap = va_arg(*ap, va_list *); - return mg_vxprintf(out, param, fmt, ap); -} - -static inline void mg_dash_success(struct mg_connection *c, struct mg_str req, - const char *fmt, ...) { - struct mg_str id = mg_json_get_tok(req, "$.id"); - va_list ap; - va_start(ap, fmt); - mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:%M}", MG_ESC("id"), id.len, - id.buf, MG_ESC("result"), mg_print_fmt, fmt, &ap); - va_end(ap); -} - -static inline void mg_dash_error(struct mg_connection *c, struct mg_str req, - const char *fmt, ...) { - struct mg_str id = mg_json_get_tok(req, "$.id"); - va_list ap; - va_start(ap, fmt); - mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:{%m:%d,%m:%m}}", MG_ESC("id"), - id.len, id.buf, MG_ESC("error"), MG_ESC("code"), -1, - MG_ESC("message"), mg_print_fmt, fmt, &ap); - va_end(ap); -} - -static inline struct mg_field_set *mg_dash_find_field_set(struct mg_dash *dash, - struct mg_str name) { - size_t i; - for (i = 0; dash->sets != NULL && dash->sets[i].name != NULL; i++) { - if (mg_strcmp(name, mg_str(dash->sets[i].name)) == 0) { - return &dash->sets[i]; - } - } - return NULL; -} - -static inline struct mg_field *mg_dash_find_field(struct mg_field *fields, - struct mg_str name) { - size_t i; - for (i = 0; fields != NULL && fields[i].name != NULL; i++) { - if (mg_strcmp(name, mg_str(fields[i].name)) == 0) return &fields[i]; - } - return NULL; -} - -static inline size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_field *f = va_arg(*ap, struct mg_field *); - size_t n = 0; - n += mg_xprintf(fn, arg, "%m:", MG_ESC(f->name)); - if (f->type == MG_VAL_BOOL) { - n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false"); - } else if (f->type == MG_VAL_INT) { - n += mg_xprintf(fn, arg, "%d", *(int *) f->value); - } else if (f->type == MG_VAL_DBL) { - n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value); - } else if (f->type == MG_VAL_STR) { - n += mg_xprintf(fn, arg, "%m", MG_ESC(f->value)); - } else if (f->type == MG_VAL_RAW) { - n += mg_xprintf(fn, arg, "%s", f->value); - } else { - n += mg_xprintf(fn, arg, "null"); - } - return n; -} - -static inline size_t mg_print_field_set(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_field_set *set = va_arg(*ap, struct mg_field_set *); - size_t i, n = 0; - n += mg_xprintf(fn, arg, "{"); - for (i = 0; set != NULL && set->fields[i].name != NULL; i++) { - if (i > 0) n += mg_xprintf(fn, arg, ","); - n += mg_xprintf(fn, arg, "%M", mg_print_field, &set->fields[i]); - } - n += mg_xprintf(fn, arg, "}"); - return n; -} - -static inline size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_dash *dash = va_arg(*ap, struct mg_dash *); - struct mg_str *name = va_arg(*ap, struct mg_str *); - struct mg_field_set *set = mg_dash_find_field_set(dash, *name); - size_t i, n = 0; - 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].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->reader) set->reader(); - n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); - } else { - n += mg_xprintf(fn, arg, "null"); - } - return n; -} - -static inline size_t mg_dash_print(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_str *req = va_arg(*ap, struct mg_str *); - struct mg_dash *dash = va_arg(*ap, struct mg_dash *); - struct mg_str name = trimq(mg_json_get_tok(*req, "$.params")); - return mg_xprintf(fn, arg, "%M", mg_dash_print_name, dash, &name); -} - -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->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); - } -} - -static inline int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { - char json_path[128]; - bool ok = false; - mg_snprintf(json_path, sizeof(json_path), "$.%s", f->name); - if (f->type == MG_VAL_BOOL) { - ok = mg_json_get_bool(json, json_path, (bool *) f->value); - } else if (f->type == MG_VAL_INT) { - double d; - ok = mg_json_get_num(json, json_path, &d); - if (ok) *(int *) f->value = (int) d; - } else if (f->type == MG_VAL_DBL) { - ok = mg_json_get_num(json, json_path, (double *) f->value); - } else if (f->type == MG_VAL_STR) { - ok = mg_json_unescape(json, json_path, (char *) f->value, f->value_size); - } else if (f->type == MG_VAL_RAW) { - ok = mg_snprintf((char *) f->value, f->value_size, "%.*s", json.len, - json.buf) == json.len; - } - return ok; -} - -static inline int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, - struct mg_str json) { - struct mg_str key, val; - size_t ofs = 0; - int count = 0; - // struct mg_field *f = NULL; - while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) { - struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key)); - if (set != NULL) { - size_t i; - for (i = 0; set->fields[i].name != NULL; i++) { - if (mg_dash_parse_field(val, &set->fields[i])) count++; - } - if (count > 0 && set->writer != NULL) { - set->writer(); - mg_dash_send_change(c->mgr, dash, set->name); - } - } else { - MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); - } - } - return count; -} - -static inline void mg_dash_process_msg(struct mg_connection *c, - struct mg_ws_message *wm, - struct mg_dash *dash) { - struct mg_str req = wm->data; - struct mg_str method = trimq(mg_json_get_tok(req, "$.method")); - if (mg_match(method, mg_str("get"), NULL)) { - mg_dash_success(c, req, "%M", mg_dash_print, &req, dash); - } else if (mg_match(method, mg_str("set"), NULL)) { - struct mg_str params = trimq(mg_json_get_tok(req, "$.params")); - int count = mg_dash_apply(c, dash, params); - mg_dash_success(c, req, "%d", count); - } else { - mg_dash_error(c, req, "%s", "unknown method"); - } -} - -#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 = (struct mg_dash_file *) 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_dash *dash = (struct mg_dash *) c->fn_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(dash, hm->uri); - mg_dash_send_change(c->mgr, dash, "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(dash, 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, hm, 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, (FILE *) us->ctx); - c->recv.len = 0; - if (us->received >= us->expected) { - fclose((FILE *) 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, dash, "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; - if (ev == MG_EV_HTTP_MSG) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - struct mg_str parts[5]; - 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, - &parts[0]); - } 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 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 - struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; - if (dash == NULL) { - mg_dash_error(c, wm->data, "%s", "no dash defined"); - } else { - mg_dash_process_msg(c, wm, dash); - } - } -} - #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" @@ -3551,6 +3222,18 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, #define MG_MODBUS_ADDR "tcp://0.0.0.0:502" #endif +#ifndef MG_DASH_MAX_SEND_BUF_SIZE +#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5) +#endif + +// File manager +extern struct mg_dash_file *mg_dash_files; +void mg_dash_file_add(struct mg_str name, size_t size); +void mg_dash_file_del(struct mg_str name); + +void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data); +void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *); + // 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 256707d8..52117fd6 100644 --- a/resources/dashboard.js +++ b/resources/dashboard.js @@ -7,7 +7,7 @@ function Rpc(url, onev) { let ws, id = 1, oncall, q = []; const pending = new Map(); - const realsend = s => { ws.send(s); on('send', s); }; + const realsend = s => { ws.send(s); onev && onev('send', s); }; const fail = e => { for (const p of pending.values()) p[1](e || 'disconnected'); pending.clear(); }; const send = x => ws && ws.readyState === 1 ? realsend(JSON.stringify(x)) @@ -61,7 +61,7 @@ const isMock = !!window.frameElement || (window !== window.top); const settings = {data: {}, edits: {}, debug: true}; const userhandlers = {}; // User event handlers - const status = { online: true, username: '', userlevel: 0, lastseen: '' }; + const status = { online: true, username: '', userlevel: 0, lastseen: '', initialized: false }; const rpc = isMock ? null : Rpc('api/websocket', function (evname, args) { if (evname == 'open') status.online = true, rescan(); if (evname == 'close') status.online = false, rescan(); @@ -152,8 +152,15 @@ el.classList.toggle('error', false); } if (!el.bound) el.addEventListener("input", ev => edit(el, key, el.type == 'checkbox' ? ev.target.checked : ev.target.value)), el.bound = true; + } else if (el.tagName == 'BUTTON') { + if (!el.bound) { + el.addEventListener("pointerdown", ev => Dashboard.call('set', set({}, key, true))); + el.addEventListener("pointerup", ev => Dashboard.call('set', set({}, key, false))); + el.bound = true; + el.dataset.autosave = 1; + } } else { - el.innerHTML = v; + el.innerHTML = v; } }; @@ -317,19 +324,40 @@ userhandlers[name] = fn; }; + const setbody = visibility => document.body.style.visibility = visibility; + function init(conf) { apply_and_rescan(settings, conf); if (rpc) { rpc.handle((method, args) => { - if (method == 'change') apply_and_rescan(settings.data, args); + if (method == 'change') { + // "initialized" means that the device has sent us all initial + // data change notifications, and our settings.data contain + // actual refreshed device data - not the UI mock + if (!args) { + setbody('visible'); + status.initialized = true; + } + if (args) apply_and_rescan(settings.data, args); + } if (method != 'change') console.log('UNKNOWN REQUEST', method, args); if (userhandlers[method]) userhandlers[method](args); }); - // After init, fetch all device data - rpc.call('get').then(r => apply_and_rescan(settings.data, r)); + //rpc.call('get').then(r => apply_and_rescan(settings.data, r)); + } else { + setTimeout(function() { + status.initialized = true; + setbody('visible'); + }, 500); } }; + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => setbody('hidden'), { once: true }); + } else { + setbody('hidden'); + } + global.Dashboard = { init, call, on, status }; })(window); diff --git a/resources/watch.js b/resources/watch.js index a7544b37..81fdfd0f 100644 --- a/resources/watch.js +++ b/resources/watch.js @@ -28,10 +28,17 @@ if (watchTargets.length === 0 || command.length === 0) { let child = null; let restartTimeout = null; +let ignoreEventsUntil = 0; +let lastStartAt = 0; +const MIN_RESTART_MS = 250; function start() { + // Prevent restart loops caused by the command touching watched paths. + ignoreEventsUntil = Date.now() + 200; + lastStartAt = Date.now(); + if (child) { - child.kill(); + child.kill('SIGTERM'); } console.log(`Starting: ${command.join(' ')}`); child = spawn(command[0], command.slice(1), { stdio: 'inherit', shell: true }); @@ -45,20 +52,25 @@ function start() { function watch(target) { const stats = fs.statSync(target); if (stats.isFile()) { - fs.watchFile(target, (curr, prev) => { - if (curr.mtime !== prev.mtime) { + // fs.watchFile is polling-based; use a small interval to react quickly. + fs.watchFile(target, { interval: 100 }, (curr, prev) => { + if (Date.now() < ignoreEventsUntil) return; + if (curr.mtimeMs !== prev.mtimeMs) { console.log(`File changed: ${target}`); clearTimeout(restartTimeout); - restartTimeout = setTimeout(start, 100); + const delay = Math.max(25, MIN_RESTART_MS - (Date.now() - lastStartAt)); + restartTimeout = setTimeout(start, delay); } }); } else { const dir = target; fs.watch(dir, { recursive: true }, (eventType, filename) => { + if (Date.now() < ignoreEventsUntil) return; if (filename) { console.log(`File changed: ${filename}`); clearTimeout(restartTimeout); - restartTimeout = setTimeout(start, 100); + const delay = Math.max(25, MIN_RESTART_MS - (Date.now() - lastStartAt)); + restartTimeout = setTimeout(start, delay); } }); } diff --git a/src/dash.c b/src/dash.c new file mode 100644 index 00000000..23008c02 --- /dev/null +++ b/src/dash.c @@ -0,0 +1,374 @@ +#include "dash.h" + +#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 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; +} + +static void mg_dash_broadcast(struct mg_mgr *mgr, const char *fmt, ...) { + struct mg_connection *c; + va_list ap; + for (c = mgr->conns; c != NULL; c = c->next) { + if (!c->is_websocket) continue; + if (c->send.len > MG_DASH_MAX_SEND_BUF_SIZE) { + MG_ERROR(("%lu buffered data %lu > MG_DASH_MAX_SEND_BUF_SIZE, throttled", + c->id, c->send.len)); + continue; + } + va_start(ap, fmt); + mg_ws_vprintf(c, WEBSOCKET_OP_TEXT, fmt, &ap); + va_end(ap); + } +} + +static size_t mg_print_fmt(mg_pfn_t out, void *param, va_list *ap) { + const char *fmt = va_arg(*ap, const char *); + ap = va_arg(*ap, va_list *); + return mg_vxprintf(out, param, fmt, ap); +} + +static void mg_dash_success(struct mg_connection *c, struct mg_str req, + const char *fmt, ...) { + struct mg_str id = mg_json_get_tok(req, "$.id"); + va_list ap; + va_start(ap, fmt); + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:%M}", MG_ESC("id"), id.len, + id.buf, MG_ESC("result"), mg_print_fmt, fmt, &ap); + va_end(ap); +} + +static void mg_dash_error(struct mg_connection *c, struct mg_str req, + const char *fmt, ...) { + struct mg_str id = mg_json_get_tok(req, "$.id"); + va_list ap; + va_start(ap, fmt); + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:{%m:%d,%m:%m}}", MG_ESC("id"), + id.len, id.buf, MG_ESC("error"), MG_ESC("code"), -1, + MG_ESC("message"), mg_print_fmt, fmt, &ap); + va_end(ap); +} + +static struct mg_field_set *mg_dash_find_field_set(struct mg_dash *dash, + struct mg_str name) { + struct mg_field_set *fs; + for (fs = dash->sets; fs != NULL; fs = fs->next) { + if (mg_strcmp(name, mg_str(fs->name)) == 0) return fs; + } + return NULL; +} + +// static struct mg_field *mg_dash_find_field(struct mg_field *fields, +// struct mg_str name) { +// size_t i; +// for (i = 0; fields != NULL && fields[i].name != NULL; i++) { +// if (mg_strcmp(name, mg_str(fields[i].name)) == 0) return &fields[i]; +// } +// return NULL; +// } + +static size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_field *f = va_arg(*ap, struct mg_field *); + size_t n = 0; + n += mg_xprintf(fn, arg, "%m:", MG_ESC(f->name)); + if (f->type == MG_VAL_BOOL) { + n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false"); + } else if (f->type == MG_VAL_INT) { + n += mg_xprintf(fn, arg, "%d", *(int *) f->value); + } else if (f->type == MG_VAL_DBL) { + n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value); + } else if (f->type == MG_VAL_STR) { + n += mg_xprintf(fn, arg, "%m", MG_ESC(f->value)); + } else if (f->type == MG_VAL_RAW) { + n += mg_xprintf(fn, arg, "%s", f->value); + } else { + n += mg_xprintf(fn, arg, "null"); + } + return n; +} + +static size_t mg_print_field_set(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_field_set *set = va_arg(*ap, struct mg_field_set *); + size_t i, n = 0; + n += mg_xprintf(fn, arg, "{"); + for (i = 0; set != NULL && set->fields[i].name != NULL; i++) { + if (i > 0) n += mg_xprintf(fn, arg, ","); + n += mg_xprintf(fn, arg, "%M", mg_print_field, &set->fields[i]); + } + n += mg_xprintf(fn, arg, "}"); + return n; +} + +static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_dash *dash = va_arg(*ap, struct mg_dash *); + struct mg_str *name = va_arg(*ap, struct mg_str *); + struct mg_field_set *set = mg_dash_find_field_set(dash, *name); + size_t n = 0; + if (name->len == 0) { + struct mg_field_set *fs; + const char *comma = ""; + n += mg_xprintf(fn, arg, "{"); + for (fs = dash->sets; fs != NULL; fs = fs->next) { + if (fs->reader) fs->reader(); + n += mg_xprintf(fn, arg, comma); + n += mg_xprintf(fn, arg, "%m:", MG_ESC(fs->name)); + n += mg_xprintf(fn, arg, "%M", mg_print_field_set, fs); + comma = ","; + } + n += mg_xprintf(fn, arg, "}"); + } else if (set != NULL) { + if (set->reader) set->reader(); + n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); + } else { + n += mg_xprintf(fn, arg, "null"); + } + return n; +} + +static size_t mg_dash_print(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_str *req = va_arg(*ap, struct mg_str *); + struct mg_dash *dash = va_arg(*ap, struct mg_dash *); + struct mg_str name = trimq(mg_json_get_tok(*req, "$.params")); + return mg_xprintf(fn, arg, "%M", mg_dash_print_name, dash, &name); +} + +void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *set) { + 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); +} + +static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { + char json_path[128]; + bool ok = false; + mg_snprintf(json_path, sizeof(json_path), "$.%s", f->name); + if (f->type == MG_VAL_BOOL) { + ok = mg_json_get_bool(json, json_path, (bool *) f->value); + } else if (f->type == MG_VAL_INT) { + double d; + ok = mg_json_get_num(json, json_path, &d); + if (ok) *(int *) f->value = (int) d; + } else if (f->type == MG_VAL_DBL) { + ok = mg_json_get_num(json, json_path, (double *) f->value); + } else if (f->type == MG_VAL_STR) { + ok = mg_json_unescape(json, json_path, (char *) f->value, f->value_size); + } else if (f->type == MG_VAL_RAW) { + ok = mg_snprintf((char *) f->value, f->value_size, "%.*s", json.len, + json.buf) == json.len; + } + return ok; +} + +static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, + struct mg_str json) { + struct mg_str key, val; + size_t ofs = 0; + int count = 0; + // struct mg_field *f = NULL; + while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) { + struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key)); + if (set != NULL) { + size_t i; + for (i = 0; set->fields[i].name != NULL; i++) { + if (mg_dash_parse_field(val, &set->fields[i])) count++; + } + if (count > 0) { + if (set->writer) set->writer(); + mg_dash_send_change(c->mgr, set); + } + } else { + MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); + } + } + return count; +} + +static void mg_dash_process_msg(struct mg_connection *c, + struct mg_ws_message *wm, + struct mg_dash *dash) { + struct mg_str req = wm->data; + struct mg_str method = trimq(mg_json_get_tok(req, "$.method")); + if (mg_match(method, mg_str("get"), NULL)) { + mg_dash_success(c, req, "%M", mg_dash_print, &req, dash); + } else if (mg_match(method, mg_str("set"), NULL)) { + struct mg_str params = trimq(mg_json_get_tok(req, "$.params")); + int count = mg_dash_apply(c, dash, params); + mg_dash_success(c, req, "%d", count); + } else { + mg_dash_error(c, req, "%s", "unknown method"); + } +} + +void mg_dash_file_add(struct mg_str name, size_t size) { + struct mg_dash_file *f = (struct mg_dash_file *) mg_calloc(1, sizeof(*f)); + f->name = mg_strdup(name).buf; + f->size = size; + f->next = mg_dash_files; + mg_dash_files = f; +} + +void mg_dash_file_del(struct mg_str name) { + struct mg_dash_file **head, *f; + for (head = &mg_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; + } + } +} + +// Files array: [{"name": "foo.txt", "size": 1234}] +struct mg_dash_file *mg_dash_files; +static char s_files[1024]; + +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 = mg_dash_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, "]"); +} + +static struct mg_field fields_files[] = { + {"data", MG_VAL_RAW, s_files, sizeof(s_files)}, + {NULL, MG_VAL_INT, NULL, 0}, +}; + +static struct mg_field_set set_files = { + "files", fields_files, read_files, NULL, 0, 0, NULL, +}; + +static void file_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + // struct mg_dash *dash = (struct mg_dash *) c->fn_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(hm->uri); + mg_dash_send_change(c->mgr, &set_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(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, hm, 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, (FILE *) us->ctx); + c->recv.len = 0; + if (us->received >= us->expected) { + fclose((FILE *) 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, &set_files); + memset(us, 0, sizeof(*us)); + c->is_draining = 1; + } + } +} + +void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + struct mg_dash *dash = (struct mg_dash *) c->fn_data; + if (ev == MG_EV_OPEN) { + if (mg_dash_find_field_set(dash, mg_str("files")) == NULL) { + MG_DASH_ADD_FIELD_SET(dash, &set_files); + } + } else if (ev == MG_EV_HTTP_MSG) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + struct mg_str parts[5]; + 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, + &parts[0]); + } 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 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_OPEN) { + // WS connection established, send change notifications for all data + struct mg_field_set *fs; + for (fs = dash->sets; fs != NULL; fs = fs->next) { + mg_dash_send_change(c->mgr, fs); + } + // Send change notification with no params to indicate we're done for now + mg_dash_broadcast(c->mgr, "{%m:%m}", MG_ESC("method"), MG_ESC("change")); + } else if (ev == MG_EV_WS_MSG) { + // Add this to automatically handle "get" and "set" JSON-RPC calls + struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; + if (dash == NULL) { + mg_dash_error(c, wm->data, "%s", "no dash defined"); + } else { + mg_dash_process_msg(c, wm, dash); + } + } +} diff --git a/src/dash.h b/src/dash.h index 5223f806..ef2b4444 100644 --- a/src/dash.h +++ b/src/dash.h @@ -29,6 +29,7 @@ struct mg_field_set { void (*writer)(void); int read_level; int write_level; + struct mg_field_set *next; }; struct mg_dash_custom_handler { @@ -47,9 +48,15 @@ struct mg_dash_file { struct mg_dash { struct mg_field_set *sets; struct mg_dash_custom_handler *custom_handlers; - struct mg_dash_file *files; + //struct mg_dash_file *files; }; +#define MG_DASH_ADD_FIELD_SET(dash_, set_) \ + do { \ + (set_)->next = (dash_)->sets; \ + (dash_)->sets = (set_); \ + } while (0) + #define MG_DASH_REGISTER_CUSTOM_HANDLER(dash_, uri_, fn_, data_) \ do { \ static struct mg_dash_custom_handler ch_; \ @@ -60,342 +67,6 @@ struct mg_dash { (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; -} - -static inline void mg_dash_broadcast(struct mg_mgr *mgr, const char *fmt, ...) { - struct mg_connection *c; - va_list ap; - for (c = mgr->conns; c != NULL; c = c->next) { - if (!c->is_websocket) continue; - va_start(ap, fmt); - mg_ws_vprintf(c, WEBSOCKET_OP_TEXT, fmt, &ap); - va_end(ap); - } -} - -static inline size_t mg_print_fmt(mg_pfn_t out, void *param, va_list *ap) { - const char *fmt = va_arg(*ap, const char *); - ap = va_arg(*ap, va_list *); - return mg_vxprintf(out, param, fmt, ap); -} - -static inline void mg_dash_success(struct mg_connection *c, struct mg_str req, - const char *fmt, ...) { - struct mg_str id = mg_json_get_tok(req, "$.id"); - va_list ap; - va_start(ap, fmt); - mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:%M}", MG_ESC("id"), id.len, - id.buf, MG_ESC("result"), mg_print_fmt, fmt, &ap); - va_end(ap); -} - -static inline void mg_dash_error(struct mg_connection *c, struct mg_str req, - const char *fmt, ...) { - struct mg_str id = mg_json_get_tok(req, "$.id"); - va_list ap; - va_start(ap, fmt); - mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:{%m:%d,%m:%m}}", MG_ESC("id"), - id.len, id.buf, MG_ESC("error"), MG_ESC("code"), -1, - MG_ESC("message"), mg_print_fmt, fmt, &ap); - va_end(ap); -} - -static inline struct mg_field_set *mg_dash_find_field_set(struct mg_dash *dash, - struct mg_str name) { - size_t i; - for (i = 0; dash->sets != NULL && dash->sets[i].name != NULL; i++) { - if (mg_strcmp(name, mg_str(dash->sets[i].name)) == 0) { - return &dash->sets[i]; - } - } - return NULL; -} - -static inline struct mg_field *mg_dash_find_field(struct mg_field *fields, - struct mg_str name) { - size_t i; - for (i = 0; fields != NULL && fields[i].name != NULL; i++) { - if (mg_strcmp(name, mg_str(fields[i].name)) == 0) return &fields[i]; - } - return NULL; -} - -static inline size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_field *f = va_arg(*ap, struct mg_field *); - size_t n = 0; - n += mg_xprintf(fn, arg, "%m:", MG_ESC(f->name)); - if (f->type == MG_VAL_BOOL) { - n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false"); - } else if (f->type == MG_VAL_INT) { - n += mg_xprintf(fn, arg, "%d", *(int *) f->value); - } else if (f->type == MG_VAL_DBL) { - n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value); - } else if (f->type == MG_VAL_STR) { - n += mg_xprintf(fn, arg, "%m", MG_ESC(f->value)); - } else if (f->type == MG_VAL_RAW) { - n += mg_xprintf(fn, arg, "%s", f->value); - } else { - n += mg_xprintf(fn, arg, "null"); - } - return n; -} - -static inline size_t mg_print_field_set(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_field_set *set = va_arg(*ap, struct mg_field_set *); - size_t i, n = 0; - n += mg_xprintf(fn, arg, "{"); - for (i = 0; set != NULL && set->fields[i].name != NULL; i++) { - if (i > 0) n += mg_xprintf(fn, arg, ","); - n += mg_xprintf(fn, arg, "%M", mg_print_field, &set->fields[i]); - } - n += mg_xprintf(fn, arg, "}"); - return n; -} - -static inline size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_dash *dash = va_arg(*ap, struct mg_dash *); - struct mg_str *name = va_arg(*ap, struct mg_str *); - struct mg_field_set *set = mg_dash_find_field_set(dash, *name); - size_t i, n = 0; - 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].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->reader) set->reader(); - n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); - } else { - n += mg_xprintf(fn, arg, "null"); - } - return n; -} - -static inline size_t mg_dash_print(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_str *req = va_arg(*ap, struct mg_str *); - struct mg_dash *dash = va_arg(*ap, struct mg_dash *); - struct mg_str name = trimq(mg_json_get_tok(*req, "$.params")); - return mg_xprintf(fn, arg, "%M", mg_dash_print_name, dash, &name); -} - -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->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); - } -} - -static inline int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { - char json_path[128]; - bool ok = false; - mg_snprintf(json_path, sizeof(json_path), "$.%s", f->name); - if (f->type == MG_VAL_BOOL) { - ok = mg_json_get_bool(json, json_path, (bool *) f->value); - } else if (f->type == MG_VAL_INT) { - double d; - ok = mg_json_get_num(json, json_path, &d); - if (ok) *(int *) f->value = (int) d; - } else if (f->type == MG_VAL_DBL) { - ok = mg_json_get_num(json, json_path, (double *) f->value); - } else if (f->type == MG_VAL_STR) { - ok = mg_json_unescape(json, json_path, (char *) f->value, f->value_size); - } else if (f->type == MG_VAL_RAW) { - ok = mg_snprintf((char *) f->value, f->value_size, "%.*s", json.len, - json.buf) == json.len; - } - return ok; -} - -static inline int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, - struct mg_str json) { - struct mg_str key, val; - size_t ofs = 0; - int count = 0; - // struct mg_field *f = NULL; - while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) { - struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key)); - if (set != NULL) { - size_t i; - for (i = 0; set->fields[i].name != NULL; i++) { - if (mg_dash_parse_field(val, &set->fields[i])) count++; - } - if (count > 0 && set->writer != NULL) { - set->writer(); - mg_dash_send_change(c->mgr, dash, set->name); - } - } else { - MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); - } - } - return count; -} - -static inline void mg_dash_process_msg(struct mg_connection *c, - struct mg_ws_message *wm, - struct mg_dash *dash) { - struct mg_str req = wm->data; - struct mg_str method = trimq(mg_json_get_tok(req, "$.method")); - if (mg_match(method, mg_str("get"), NULL)) { - mg_dash_success(c, req, "%M", mg_dash_print, &req, dash); - } else if (mg_match(method, mg_str("set"), NULL)) { - struct mg_str params = trimq(mg_json_get_tok(req, "$.params")); - int count = mg_dash_apply(c, dash, params); - mg_dash_success(c, req, "%d", count); - } else { - mg_dash_error(c, req, "%s", "unknown method"); - } -} - -#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 = (struct mg_dash_file *) 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_dash *dash = (struct mg_dash *) c->fn_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(dash, hm->uri); - mg_dash_send_change(c->mgr, dash, "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(dash, 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, hm, 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, (FILE *) us->ctx); - c->recv.len = 0; - if (us->received >= us->expected) { - fclose((FILE *) 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, dash, "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; - if (ev == MG_EV_HTTP_MSG) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - struct mg_str parts[5]; - 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, - &parts[0]); - } 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 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 - struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; - if (dash == NULL) { - mg_dash_error(c, wm->data, "%s", "no dash defined"); - } else { - mg_dash_process_msg(c, wm, dash); - } - } -} - #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" @@ -406,6 +77,18 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev, #define MG_MODBUS_ADDR "tcp://0.0.0.0:502" #endif +#ifndef MG_DASH_MAX_SEND_BUF_SIZE +#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5) +#endif + +// File manager +extern struct mg_dash_file *mg_dash_files; +void mg_dash_file_add(struct mg_str name, size_t size); +void mg_dash_file_del(struct mg_str name); + +void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data); +void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *); + // 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/full/dashboard.c b/tutorials/device-dashboard/full/dashboard.c index 9d02a6d6..d85c7507 100644 --- a/tutorials/device-dashboard/full/dashboard.c +++ b/tutorials/device-dashboard/full/dashboard.c @@ -5,33 +5,57 @@ static struct mg_dash s_dashboard; +// Action buttons +static bool s_action1, s_action2; +static uint64_t s_action2_timeout; +static void write_actions(void) { + if (s_action2 == true) s_action2_timeout = mg_now() + 750; +} +static void read_actions(void) { + if (s_action2_timeout > mg_now()) s_action2 = true; +} +static struct mg_field fields_actions[] = { + {"action1", MG_VAL_BOOL, &s_action1, sizeof(s_action1)}, + {"action2", MG_VAL_BOOL, &s_action2, sizeof(s_action2)}, + {NULL, MG_VAL_INT, NULL, 0}, +}; +static struct mg_field_set set_actions = { + "actions", fields_actions, read_actions, write_actions, 0, 0, NULL, +}; + // Control panel // s_led1, s_led2, s_led3 are used to communicate LED status static bool s_led1, s_led2, s_led3; - +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); +} 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, MG_VAL_INT, 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); -} +static struct mg_field_set set_leds = { + "leds", fields_leds, read_leds, write_leds, 0, 0, NULL, +}; // Read-only device Metrics static int s_ram = 32, s_cpu = 7; static double s_temperature = 24.8; +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; +} + static struct mg_field fields_metrics[] = { {"ram", MG_VAL_INT, &s_ram, sizeof(s_ram)}, {"cpu", MG_VAL_INT, &s_cpu, sizeof(s_cpu)}, @@ -39,24 +63,15 @@ static struct mg_field fields_metrics[] = { {NULL, MG_VAL_INT, 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; -} +static struct mg_field_set set_metrics = { + "metrics", fields_metrics, read_metrics, NULL, 0, 0, NULL, +}; // 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, MG_VAL_INT, NULL, 0}, -}; - static void write_settings(void) { mg_log_level = s_log_level; } @@ -65,7 +80,18 @@ static void read_settings(void) { s_log_level = mg_log_level; } -#define NUM_POINTS 7 // How many data points to send +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, MG_VAL_INT, NULL, 0}, +}; + +static struct mg_field_set set_settings = { + "settings", fields_settings, read_settings, write_settings, 0, 0, NULL, +}; + +#define NUM_POINTS_GRAPH1 7 // How many graph1 data points to send struct point { uint16_t x; // X coordinate of the data point, usually this is time @@ -79,67 +105,91 @@ struct point { // 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, MG_VAL_INT, NULL, 0}, -}; +static char s_graph1_data[NUM_POINTS_GRAPH1 * POINT_SIZE + 2 + 1]; // Randomly generate graph points and serialise them into a string -void read_chart1(void) { +void read_graph1(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++) { + len += mg_snprintf(s_graph1_data + len, sizeof(s_graph1_data) - len, "["); + for (i = 0; i < NUM_POINTS_GRAPH1; 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, + p.y1 = 85 - (uint16_t) ((49 * i) / NUM_POINTS_GRAPH1) + (rand() % 9); + p.y2 = 20 + (uint16_t) ((69 * i) / NUM_POINTS_GRAPH1) + (rand() % 5); + len += mg_snprintf(s_graph1_data + len, sizeof(s_graph1_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, "]"); + len += mg_snprintf(s_graph1_data + len, sizeof(s_graph1_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)}, +static struct mg_field fields_graph1[] = { + {"data", MG_VAL_RAW, s_graph1_data, sizeof(s_graph1_data)}, {NULL, MG_VAL_INT, NULL, 0}, }; -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); + +static struct mg_field_set set_graph1 = { + "graph1", fields_graph1, read_graph1, NULL, 0, 0, NULL, +}; + +#define NUM_POINTS_GRAPH2 100 // How many graph2 data points to send +static char s_graph2_data[NUM_POINTS_GRAPH2 * 4 + 2 + 1]; +static bool s_graph2_report = true; + +// Adjust t += 7, i * 9, and i * 3 +void read_graph2(void) { + static uint16_t t; + size_t i, len = 0; + len = mg_snprintf(s_graph2_data, sizeof(s_graph2_data), "["); + t += 23; + for (i = 0; i < NUM_POINTS_GRAPH2; i++) { + uint16_t x = (uint16_t) (i * 7 + t); + uint16_t y = (uint16_t) (i * 3 - t / 2); + uint8_t a = (x ^ (x >> 3)) & 255; + uint8_t b = (y ^ (y >> 4)) & 255; + int v = + 70 + ((a < 128 ? a : 255 - a) >> 1) + ((b < 128 ? b : 255 - b) >> 2); + len += mg_snprintf(s_graph2_data + len, sizeof(s_graph2_data) - len, + "%s%hhu", i > 0 ? "," : "", (uint8_t) v); } - len += mg_snprintf(s_files + len, sizeof(s_files) - len, "]"); + len += mg_snprintf(s_graph2_data + len, sizeof(s_graph2_data) - 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_field fields_graph2[] = { + {"data", MG_VAL_RAW, s_graph2_data, sizeof(s_graph2_data)}, + {"report", MG_VAL_BOOL, &s_graph2_report, sizeof(s_graph2_report)}, + {NULL, MG_VAL_INT, NULL, 0}, +}; + +static struct mg_field_set set_graph2 = { + "graph2", fields_graph2, read_graph2, NULL, 0, 0, NULL, }; void mg_dash_init(struct mg_mgr *mgr) { - s_dashboard.sets = field_sets; - 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_DASH_ADD_FIELD_SET(&s_dashboard, &set_leds); + MG_DASH_ADD_FIELD_SET(&s_dashboard, &set_metrics); + MG_DASH_ADD_FIELD_SET(&s_dashboard, &set_settings); + MG_DASH_ADD_FIELD_SET(&s_dashboard, &set_graph1); + MG_DASH_ADD_FIELD_SET(&s_dashboard, &set_graph2); + MG_DASH_ADD_FIELD_SET(&s_dashboard, &set_actions); + + // Add two fake files - for demonstration + mg_dash_file_add(mg_str("device-config.json"), 1234); + mg_dash_file_add(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"); + static uint64_t timer1 = 0, timer2 = 0; + if (mg_timer_expired(&timer1, 30000, mg_now())) { + mg_dash_send_change(mgr, &set_metrics); + } + if (mg_timer_expired(&timer2, 120, mg_now()) && s_graph2_report) { + mg_dash_send_change(mgr, &set_graph2); + } + if (s_action2 == true && s_action2_timeout < mg_now()) { + s_action2 = false; + mg_dash_send_change(mgr, &set_actions); } } diff --git a/tutorials/device-dashboard/full/dashboard.html b/tutorials/device-dashboard/full/dashboard.html index 3b051c6f..79f6076f 100644 --- a/tutorials/device-dashboard/full/dashboard.html +++ b/tutorials/device-dashboard/full/dashboard.html @@ -9,7 +9,9 @@ Dashboard.init({ debug: true, data: { - chart1: {data: []}, + graph1: {data: []}, + graph2: {data: [], report: true}, + actions: { action1: false, action2: false }, leds: { led1: true, led2: false, led3: false }, settings: { volume: 12, log_level: 3, name: 'Galway' }, metrics: { temperature: 1.2, cpu: 4, ram: 5 }, @@ -23,8 +25,6 @@ const tabs = Array.from(document.querySelectorAll(".header-left .nav-link[href^='#']")); const panels = Array.from(document.querySelectorAll(".tab-panel")); - let chart = null; - const activateTab = targetId => { const targetPanel = document.getElementById(targetId); if (!targetPanel) return; @@ -42,17 +42,10 @@ panel.hidden = !isActive; }); - requestAnimationFrame(() => { - const rect = el.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0) rect.height = rect.width = 10; - if (!chart) { - options.width = rect.width; - options.height = rect.height; - chart = new uPlot(options, [], el); - } else { - chart.setSize({ width: rect.width, height: rect.height }); - } + requestAnimationFrame(() => { + mkGraph(graphs.graph1); + mkGraph(graphs.graph2); }); }; @@ -68,45 +61,76 @@ const initialId = window.location.hash.slice(1) || "overview"; activateTab(initialId); + // GRAPHS + // {stroke: '#14B8A6FF', fill: '#14B8A61A'}, // teal + // {stroke: '#F59E0BFF', fill: '#F59E0B1A'}, // amber + // {stroke: '#22C55EFF', fill: '#22C55E1A'}, // green + // {stroke: '#EF4444FF', fill: '#EF44441A'}, // red + // {stroke: '#8B5CF6FF', fill: '#8B5CF61A'}, // purple + // {stroke: '#058DC7FF', fill: '#058DC71A'}, // azure + // {stroke: '#ED7E17FF', fill: '#ED7E171A'}, // tangerine - const el = document.getElementById("chart"); - - const options = { - width: 100, - height: 100, - tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), "Europe/London"), - plugins: [], - legend: { show: false }, - axes: [ - { values: "{MMM} {DD}", size: 25 }, - { size: 50, xvalues: (u, vals, space) => vals.map(v => (+v / 1000).toFixed(1) + 'k') }, - ], - scales: { - x: { time: true }, - y: { range: [0, 100] }, + const graphs = { + graph1: { + el: document.getElementById("graph1"), + plot: null, + opts: { + width: 10, height: 10, + tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), "Europe/London"), + legend: { show: false }, + axes: [ { size: 25, gap: 2, }, { size: 40, gap: 2, }, ], + scales: { x: { time: false }, y: { range: [0, 100] }, }, + series: [ + {}, + {stroke: '#22C55EFF', fill: '#22C55E1A'}, // green + {stroke: '#F59E0BFF', fill: '#F59E0B1A'}, // amber + ], + }, + }, + graph2: { + el: document.getElementById("graph2"), + plot: null, + opts: { + width: 10, height: 10, + legend: { show: false }, + axes: [ { size: 25, gap: 2, }, { size: 40, gap: 2, }, ], + scales: { x: { time: false }, y: { range: [50, 200] }, }, + series: [ + {}, + {stroke: '#058DC7FF', fill: '#058DC71A'}, // azure + ], + } }, - series: [ - {}, - {stroke: 'rgba(5, 141, 199, 1)', fill: 'rgba(5, 141, 199, 0.1)'}, - {stroke: 'rgba(237, 126, 23, 1)', fill: 'rgba(237, 126, 23, 0.1)'}, - ], }; - function updateGraphPoints(data) { - if (!data) return; - // const parsed = JSON.parse(data); - const parsed = data || []; - const uplot_data = [ - parsed.map(x => x[0]), - parsed.map(x => x[1]), - parsed.map(x => x[2]), - ]; - chart.setData(uplot_data); + function mkGraph(g) { + if (!g.plot) g.plot = new uPlot(g.opts, [], g.el); + const rect = g.el.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + g.plot.setSize({ width: rect.width, height: rect.height }); + } }; - // Intercept change notifications - Dashboard.on('change', args => updateGraphPoints(args?.chart1?.data)); - Dashboard.call('get', 'chart1').then(resp => updateGraphPoints(resp?.data)); + function updateGraph1(data) { + data && graphs.graph1.plot.setData([ + data.map(x => x[0]), + data.map(x => x[1]), + data.map(x => x[2]), + ]); + }; + + function updateGraph2(data) { + data && graphs.graph2.plot.setData([ + data.map((_, i) => i), + data, + ]); + }; + + // Intercept graph change notifications, re-plot data + Dashboard.on('change', args => { + args?.graph1?.data && updateGraph1(args?.graph1?.data); + args?.graph2?.data && updateGraph2(args?.graph2?.data); + }); }); @@ -284,7 +309,7 @@ a, a:visited, a:link { text-decoration: none; }
-
+
Temperature
@@ -326,34 +351,47 @@ a, a:visited, a:link { text-decoration: none; } ok
-
-
- This dashboard source code is here. - To tailor it to your firmware, follow - this guide -
-
-
+
- Sensor History -
+ Static Graph +
Temperature
Humidity
-
+
- +
+
+ Real-Time Graph +
+ autoupdate: + +
Motor Voltage
+
+
+
+
+
+
+ +
+ +
+ +
-
LED Control
+
+ LED Control +
LED1
@@ -367,12 +405,56 @@ a, a:visited, a:link { text-decoration: none; }
LED3
+
+ + To attach LED controls to your device, + modify read_leds() and write_leds() functions + +
+
-
- To attach these LED controls to your hardware, - modify mongoose_dashboard.c :: sync_leds() function. + +
+
+ Start/Stop Action Button + actions.action1=${actions.action1} +
+
+
+
Press to start/stop
+
+
+ + This button is bound to the data-bind="actions.action1" + boolean field. When it is pressed, the field on a device + side is set to true. When unpressed, it is set to false. + Add device-specific code in read_actions() and write_actions() + to act on the button state. + +
+
+ +
+
+ Start Action Button + actions.action2=${actions.action2} +
+
+
+
Press to start
+ +
+
+ + This button is different - the C handler code for it + reacts on start, and ignores stops. Use it to implement actions + that start some process - like formatting SD card, which + finishes at some point in the future. + When the process is finished, flip the boolean field + back to false, and send change notification. +