mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-19 11:09:42 +00:00
Refactor field / fieldsets
This commit is contained in:
1 parent
a160aa6e5f
commit
06b864c328
27 files changed
+428
-1018
No files matched your search
+128
-69
@@ -3150,25 +3150,35 @@ void mg_rpc_list(struct mg_rpc_req *r);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
enum mg_val_type { MG_VAL_INT, MG_VAL_BOOL, MG_VAL_DBL, MG_VAL_STR, MG_VAL_FN };
|
||||
|
||||
union mg_val {
|
||||
int i;
|
||||
bool b;
|
||||
double d;
|
||||
struct mg_str s;
|
||||
char *s;
|
||||
void (*fn)(void);
|
||||
};
|
||||
|
||||
struct mg_field {
|
||||
const char *name;
|
||||
enum mg_val_type type;
|
||||
void (*get)(union mg_val *);
|
||||
void (*set)(const union mg_val *);
|
||||
void *value;
|
||||
size_t value_size;
|
||||
};
|
||||
|
||||
struct mg_field_set {
|
||||
const char *name;
|
||||
struct mg_field *fields;
|
||||
void (*sync)(bool is_reading);
|
||||
int read_level;
|
||||
int write_level;
|
||||
};
|
||||
|
||||
struct mg_dash {
|
||||
struct mg_field *fields;
|
||||
struct mg_field_set *sets;
|
||||
};
|
||||
|
||||
static inline struct mg_str trimq(struct mg_str s) { // Trim double quotes
|
||||
@@ -3214,10 +3224,21 @@ static inline void mg_dash_error(struct mg_connection *c, struct mg_str req,
|
||||
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[i].name != NULL; 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;
|
||||
@@ -3225,93 +3246,119 @@ static inline struct mg_field *mg_dash_find_field(struct mg_field *fields,
|
||||
|
||||
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 *);
|
||||
union mg_val val = {0};
|
||||
// union mg_val val = {0};
|
||||
size_t n = 0;
|
||||
if (f->get) f->get(&val);
|
||||
// if (f->get) f->get(&val);
|
||||
n += mg_xprintf(fn, arg, "%m:", MG_ESC(f->name));
|
||||
if (f->type == MG_VAL_BOOL) {
|
||||
n += mg_xprintf(fn, arg, "%s", val.b ? "true" : "false");
|
||||
n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false");
|
||||
} else if (f->type == MG_VAL_INT) {
|
||||
n += mg_xprintf(fn, arg, "%d", val.i);
|
||||
n += mg_xprintf(fn, arg, "%d", *(int *) f->value);
|
||||
} else if (f->type == MG_VAL_DBL) {
|
||||
n += mg_xprintf(fn, arg, "%g", val.d);
|
||||
n += mg_xprintf(fn, arg, "%g", *(double *) f->value);
|
||||
} else if (f->type == MG_VAL_STR) {
|
||||
n += mg_xprintf(fn, arg, "%m", mg_print_esc, val.s.len, val.s.buf);
|
||||
} else if (f->type == MG_VAL_FN) {
|
||||
n += mg_xprintf(fn, arg, val.fn ? "%M" : "null", val.fn);
|
||||
// struct mg_str *s = * (struct mg_str *) f->value;
|
||||
n += mg_xprintf(fn, arg, "%m", MG_ESC(f->value));
|
||||
// } else if (f->type == MG_VAL_FN) {
|
||||
// n += mg_xprintf(fn, arg, val.fn ? "%M" : "null", val.fn);
|
||||
} else {
|
||||
n += mg_xprintf(fn, arg, "null");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static inline bool mg_parse_field(struct mg_str json, struct mg_field *f,
|
||||
union mg_val *val) {
|
||||
bool ok = false;
|
||||
if (f->type == MG_VAL_BOOL) {
|
||||
ok = mg_json_get_bool(json, "$", &val->b);
|
||||
} else if (f->type == MG_VAL_INT) {
|
||||
double d;
|
||||
ok = mg_json_get_num(json, "$", &d);
|
||||
if (ok) val->i = (int) d;
|
||||
} else if (f->type == MG_VAL_DBL) {
|
||||
ok = mg_json_get_num(json, "$", &val->d);
|
||||
} else if (f->type == MG_VAL_STR) {
|
||||
val->s = json;
|
||||
} else if (f->type == MG_VAL_FN) {
|
||||
val->s = json;
|
||||
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]);
|
||||
}
|
||||
return ok;
|
||||
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].sync) dash->sets[i].sync(false);
|
||||
if (i > 0) n += mg_xprintf(fn, arg, ",");
|
||||
n += mg_xprintf(fn, arg, "%m:", MG_ESC(dash->sets[i].name));
|
||||
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, &dash->sets[i]);
|
||||
}
|
||||
n += mg_xprintf(fn, arg, "}");
|
||||
} else if (set != NULL) {
|
||||
if (set->sync) set->sync(false);
|
||||
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_field *fields = va_arg(*ap, struct mg_field *);
|
||||
// mg_dash_get_fn get = va_arg(*ap, mg_dash_get_fn);
|
||||
struct mg_dash *dash = va_arg(*ap, struct mg_dash *);
|
||||
struct mg_str name = trimq(mg_json_get_tok(*req, "$.params"));
|
||||
struct mg_field *f = mg_dash_find_field(fields, name);
|
||||
size_t i, n = 0;
|
||||
if (name.len == 0) {
|
||||
const char *comma = "";
|
||||
n += mg_xprintf(fn, arg, "{");
|
||||
for (i = 0; fields[i].name != NULL; i++) {
|
||||
n += mg_xprintf(fn, arg, "%s", comma);
|
||||
n += mg_xprintf(fn, arg, "%M", mg_print_field, &fields[i]);
|
||||
comma = ",";
|
||||
}
|
||||
n += mg_xprintf(fn, arg, "}");
|
||||
} else if (f != NULL) {
|
||||
n += mg_xprintf(fn, arg, "{%M}", mg_print_field, f);
|
||||
} else {
|
||||
n += mg_xprintf(fn, arg, "null");
|
||||
}
|
||||
return n;
|
||||
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_field *f) {
|
||||
if (f != NULL) {
|
||||
mg_dash_broadcast(mgr, "{%m:%m,%m:{%M}}", MG_ESC("method"),
|
||||
MG_ESC("change"), MG_ESC("params"), mg_print_field, f);
|
||||
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) {
|
||||
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 void mg_dash_apply(struct mg_connection *c, struct mg_str json,
|
||||
struct mg_field *fields) {
|
||||
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);
|
||||
}
|
||||
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;
|
||||
struct mg_field *f = NULL;
|
||||
int count = 0;
|
||||
// struct mg_field *f = NULL;
|
||||
while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) {
|
||||
key = trimq(key);
|
||||
if ((f = mg_dash_find_field(fields, key)) != NULL && f->set != NULL) {
|
||||
union mg_val v = {0};
|
||||
mg_parse_field(val, f, &v);
|
||||
f->set(&v);
|
||||
mg_dash_send_change(c->mgr, f);
|
||||
break;
|
||||
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->sync != NULL) {
|
||||
set->sync(true);
|
||||
mg_dash_send_change(c->mgr, dash, set->name);
|
||||
}
|
||||
} else {
|
||||
MG_ERROR(("UNKNOWN FIELD: [%.*s]", key.len, key.buf));
|
||||
MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf));
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static inline void mg_dash_process_msg(struct mg_connection *c,
|
||||
@@ -3320,22 +3367,35 @@ static inline void mg_dash_process_msg(struct mg_connection *c,
|
||||
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->fields);
|
||||
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"));
|
||||
mg_dash_apply(c, params, dash->fields);
|
||||
mg_dash_success(c, req, "%s", "true");
|
||||
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
|
||||
|
||||
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("/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 {
|
||||
struct mg_http_serve_opts opts;
|
||||
memset(&opts, 0, sizeof(opts));
|
||||
@@ -3345,10 +3405,9 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev,
|
||||
}
|
||||
} else if (ev == MG_EV_WS_MSG) {
|
||||
// Add this to automatically handle "get" and "set" JSON-RPC calls
|
||||
struct mg_dash *dash = (struct mg_dash *) c->fn_data;
|
||||
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
|
||||
if (dash == NULL || dash->fields == NULL) {
|
||||
mg_dash_error(c, wm->data, "%s", "no fields defined");
|
||||
if (dash == NULL) {
|
||||
mg_dash_error(c, wm->data, "%s", "no dash defined");
|
||||
} else {
|
||||
mg_dash_process_msg(c, wm, dash);
|
||||
}
|
||||
|
||||
+128
-69
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "fmt.h"
|
||||
#include "json.h"
|
||||
#include "log.h"
|
||||
#include "printf.h"
|
||||
#include "str.h"
|
||||
#include "ws.h"
|
||||
@@ -11,19 +13,27 @@ union mg_val {
|
||||
int i;
|
||||
bool b;
|
||||
double d;
|
||||
struct mg_str s;
|
||||
char *s;
|
||||
void (*fn)(void);
|
||||
};
|
||||
|
||||
struct mg_field {
|
||||
const char *name;
|
||||
enum mg_val_type type;
|
||||
void (*get)(union mg_val *);
|
||||
void (*set)(const union mg_val *);
|
||||
void *value;
|
||||
size_t value_size;
|
||||
};
|
||||
|
||||
struct mg_field_set {
|
||||
const char *name;
|
||||
struct mg_field *fields;
|
||||
void (*sync)(bool is_reading);
|
||||
int read_level;
|
||||
int write_level;
|
||||
};
|
||||
|
||||
struct mg_dash {
|
||||
struct mg_field *fields;
|
||||
struct mg_field_set *sets;
|
||||
};
|
||||
|
||||
static inline struct mg_str trimq(struct mg_str s) { // Trim double quotes
|
||||
@@ -69,10 +79,21 @@ static inline void mg_dash_error(struct mg_connection *c, struct mg_str req,
|
||||
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[i].name != NULL; 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;
|
||||
@@ -80,93 +101,119 @@ static inline struct mg_field *mg_dash_find_field(struct mg_field *fields,
|
||||
|
||||
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 *);
|
||||
union mg_val val = {0};
|
||||
// union mg_val val = {0};
|
||||
size_t n = 0;
|
||||
if (f->get) f->get(&val);
|
||||
// if (f->get) f->get(&val);
|
||||
n += mg_xprintf(fn, arg, "%m:", MG_ESC(f->name));
|
||||
if (f->type == MG_VAL_BOOL) {
|
||||
n += mg_xprintf(fn, arg, "%s", val.b ? "true" : "false");
|
||||
n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false");
|
||||
} else if (f->type == MG_VAL_INT) {
|
||||
n += mg_xprintf(fn, arg, "%d", val.i);
|
||||
n += mg_xprintf(fn, arg, "%d", *(int *) f->value);
|
||||
} else if (f->type == MG_VAL_DBL) {
|
||||
n += mg_xprintf(fn, arg, "%g", val.d);
|
||||
n += mg_xprintf(fn, arg, "%g", *(double *) f->value);
|
||||
} else if (f->type == MG_VAL_STR) {
|
||||
n += mg_xprintf(fn, arg, "%m", mg_print_esc, val.s.len, val.s.buf);
|
||||
} else if (f->type == MG_VAL_FN) {
|
||||
n += mg_xprintf(fn, arg, val.fn ? "%M" : "null", val.fn);
|
||||
// struct mg_str *s = * (struct mg_str *) f->value;
|
||||
n += mg_xprintf(fn, arg, "%m", MG_ESC(f->value));
|
||||
// } else if (f->type == MG_VAL_FN) {
|
||||
// n += mg_xprintf(fn, arg, val.fn ? "%M" : "null", val.fn);
|
||||
} else {
|
||||
n += mg_xprintf(fn, arg, "null");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static inline bool mg_parse_field(struct mg_str json, struct mg_field *f,
|
||||
union mg_val *val) {
|
||||
bool ok = false;
|
||||
if (f->type == MG_VAL_BOOL) {
|
||||
ok = mg_json_get_bool(json, "$", &val->b);
|
||||
} else if (f->type == MG_VAL_INT) {
|
||||
double d;
|
||||
ok = mg_json_get_num(json, "$", &d);
|
||||
if (ok) val->i = (int) d;
|
||||
} else if (f->type == MG_VAL_DBL) {
|
||||
ok = mg_json_get_num(json, "$", &val->d);
|
||||
} else if (f->type == MG_VAL_STR) {
|
||||
val->s = json;
|
||||
} else if (f->type == MG_VAL_FN) {
|
||||
val->s = json;
|
||||
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]);
|
||||
}
|
||||
return ok;
|
||||
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].sync) dash->sets[i].sync(false);
|
||||
if (i > 0) n += mg_xprintf(fn, arg, ",");
|
||||
n += mg_xprintf(fn, arg, "%m:", MG_ESC(dash->sets[i].name));
|
||||
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, &dash->sets[i]);
|
||||
}
|
||||
n += mg_xprintf(fn, arg, "}");
|
||||
} else if (set != NULL) {
|
||||
if (set->sync) set->sync(false);
|
||||
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_field *fields = va_arg(*ap, struct mg_field *);
|
||||
// mg_dash_get_fn get = va_arg(*ap, mg_dash_get_fn);
|
||||
struct mg_dash *dash = va_arg(*ap, struct mg_dash *);
|
||||
struct mg_str name = trimq(mg_json_get_tok(*req, "$.params"));
|
||||
struct mg_field *f = mg_dash_find_field(fields, name);
|
||||
size_t i, n = 0;
|
||||
if (name.len == 0) {
|
||||
const char *comma = "";
|
||||
n += mg_xprintf(fn, arg, "{");
|
||||
for (i = 0; fields[i].name != NULL; i++) {
|
||||
n += mg_xprintf(fn, arg, "%s", comma);
|
||||
n += mg_xprintf(fn, arg, "%M", mg_print_field, &fields[i]);
|
||||
comma = ",";
|
||||
}
|
||||
n += mg_xprintf(fn, arg, "}");
|
||||
} else if (f != NULL) {
|
||||
n += mg_xprintf(fn, arg, "{%M}", mg_print_field, f);
|
||||
} else {
|
||||
n += mg_xprintf(fn, arg, "null");
|
||||
}
|
||||
return n;
|
||||
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_field *f) {
|
||||
if (f != NULL) {
|
||||
mg_dash_broadcast(mgr, "{%m:%m,%m:{%M}}", MG_ESC("method"),
|
||||
MG_ESC("change"), MG_ESC("params"), mg_print_field, f);
|
||||
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) {
|
||||
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 void mg_dash_apply(struct mg_connection *c, struct mg_str json,
|
||||
struct mg_field *fields) {
|
||||
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);
|
||||
}
|
||||
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;
|
||||
struct mg_field *f = NULL;
|
||||
int count = 0;
|
||||
// struct mg_field *f = NULL;
|
||||
while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) {
|
||||
key = trimq(key);
|
||||
if ((f = mg_dash_find_field(fields, key)) != NULL && f->set != NULL) {
|
||||
union mg_val v = {0};
|
||||
mg_parse_field(val, f, &v);
|
||||
f->set(&v);
|
||||
mg_dash_send_change(c->mgr, f);
|
||||
break;
|
||||
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->sync != NULL) {
|
||||
set->sync(true);
|
||||
mg_dash_send_change(c->mgr, dash, set->name);
|
||||
}
|
||||
} else {
|
||||
MG_ERROR(("UNKNOWN FIELD: [%.*s]", key.len, key.buf));
|
||||
MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf));
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static inline void mg_dash_process_msg(struct mg_connection *c,
|
||||
@@ -175,22 +222,35 @@ static inline void mg_dash_process_msg(struct mg_connection *c,
|
||||
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->fields);
|
||||
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"));
|
||||
mg_dash_apply(c, params, dash->fields);
|
||||
mg_dash_success(c, req, "%s", "true");
|
||||
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
|
||||
|
||||
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("/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 {
|
||||
struct mg_http_serve_opts opts;
|
||||
memset(&opts, 0, sizeof(opts));
|
||||
@@ -200,10 +260,9 @@ static inline void mg_dash_ev_handler(struct mg_connection *c, int ev,
|
||||
}
|
||||
} else if (ev == MG_EV_WS_MSG) {
|
||||
// Add this to automatically handle "get" and "set" JSON-RPC calls
|
||||
struct mg_dash *dash = (struct mg_dash *) c->fn_data;
|
||||
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
|
||||
if (dash == NULL || dash->fields == NULL) {
|
||||
mg_dash_error(c, wm->data, "%s", "no fields defined");
|
||||
if (dash == NULL) {
|
||||
mg_dash_error(c, wm->data, "%s", "no dash defined");
|
||||
} else {
|
||||
mg_dash_process_msg(c, wm, dash);
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,17 +1,17 @@
|
||||
# Environment setup: https://mongoose.ws/docs/getting-started/build-environment/
|
||||
|
||||
PROG ?= example
|
||||
MGDIR ?= ../../..
|
||||
SOURCES = server.c dashboard.html.c $(MGDIR)/mongoose.c
|
||||
MONGOOSE_DIR ?= ../..
|
||||
SOURCES = main.c mongoose_app.c dashboard.html.c $(MONGOOSE_DIR)/mongoose.c
|
||||
CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -g3 -O0
|
||||
PFLAGS = -I$(MGDIR) -DMG_DATA_SIZE=64 -DMG_TLS=MG_TLS_BUILTIN
|
||||
PFLAGS = -I$(MONGOOSE_DIR) -DMG_DATA_SIZE=64 -DMG_TLS=MG_TLS_BUILTIN
|
||||
AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment
|
||||
|
||||
all: $(PROG)
|
||||
./$(PROG)
|
||||
|
||||
# Rebuild the program
|
||||
$(PROG): $(SOURCES)
|
||||
$(PROG): $(SOURCES) $(MONGOOSE_DIR)/mongoose.h
|
||||
$(CC) $(SOURCES) $(CFLAGS) $(PFLAGS) $(AFLAGS) -o $@
|
||||
|
||||
# Generate a single, self-contained dashboard.html from the UI in web_root/
|
||||
@@ -20,12 +20,12 @@ dashboard.html: $(wildcard web_root/*)
|
||||
|
||||
# Create an embedded filesystem containing compressed dashboard.html
|
||||
dashboard.html.c: dashboard.html
|
||||
node $(MGDIR)/resources/pack.js dashboard.html::gzip > $@
|
||||
node $(MONGOOSE_DIR)/resources/pack.js dashboard.html::gzip > $@
|
||||
@echo "On-flash size: `grep -oE '[0-9]+' $@ | wc -l`"
|
||||
|
||||
# For development: rebuild and restart if any of the sources change
|
||||
watch: $(SOURCES)
|
||||
watchexec -r -w server.c -w web_root -e c,h,html,css,js -- make
|
||||
watch:
|
||||
watchexec -r -w main.c -w mongoose_app.c -w $(MONGOOSE_DIR)/mongoose.h -w web_root -e c,h,html,css,js -- make
|
||||
|
||||
clean:
|
||||
rm -rf $(PROG) *.exe *.obj *.dSYM mongoose dashboard.*
|
||||
@@ -1,145 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Cesanta Software Limited
|
||||
// SPDX-License-Identifier: GPL-2.0-only or commercial
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
#define HTTP_ADDR "http://0.0.0.0:8000"
|
||||
#else
|
||||
#define HTTP_ADDR "http://0.0.0.0:80"
|
||||
#endif
|
||||
|
||||
#define NUM_POINTS 7 // How many data points to send
|
||||
|
||||
struct point {
|
||||
uint16_t x; // X coordinate of the data point, usually this is time
|
||||
uint16_t y1; // We can plot multiple values per graph,
|
||||
uint16_t y2; // let's plot 2 values: y1 and y2.
|
||||
};
|
||||
|
||||
// This function prints a JSON array of plot points
|
||||
size_t print_points(void (*fn)(char, void *), void *arg, va_list *ap) {
|
||||
size_t i, n = 0;
|
||||
const char *comma = "";
|
||||
(void) ap;
|
||||
n += mg_xprintf(fn, arg, "[");
|
||||
for (i = 0; i < NUM_POINTS; i++) {
|
||||
struct point p;
|
||||
p.x = (uint16_t) i;
|
||||
p.y1 = 80 -
|
||||
(uint16_t) ((90 * (i > (NUM_POINTS - 1) / 4
|
||||
? i - (NUM_POINTS - 1) / 4
|
||||
: (NUM_POINTS - 1) / 4 - i)) /
|
||||
(NUM_POINTS - 1)) +
|
||||
(rand() % 9);
|
||||
p.y2 = 20 + (uint16_t) ((69 * i) / NUM_POINTS) + (rand() % 5);
|
||||
n += mg_xprintf(fn, arg, "%s[%hu,%hu,%hu]", comma, p.x, p.y1, p.y2);
|
||||
comma = ",";
|
||||
}
|
||||
n += mg_xprintf(fn, arg, "]");
|
||||
return n;
|
||||
}
|
||||
static void get_points(union mg_val *val) {
|
||||
val->fn = (void (*)(void)) print_points;
|
||||
}
|
||||
|
||||
static int s_ram = 32, s_cpu = 37, s_humidity = 59;
|
||||
static double s_temp = 24.8;
|
||||
// This function prints a JSON object with read-only metrics
|
||||
size_t print_metrics(void (*fn)(char, void *), void *arg, va_list *ap) {
|
||||
size_t n = 0;
|
||||
(void) ap;
|
||||
n += mg_xprintf(fn, arg, "{");
|
||||
n += mg_xprintf(fn, arg, "%s%m:%.3g", " ", MG_ESC("temperature"), s_temp);
|
||||
n += mg_xprintf(fn, arg, "%s%m:%d", ",", MG_ESC("humidity"), s_humidity);
|
||||
n += mg_xprintf(fn, arg, "%s%m:%d", ",", MG_ESC("cpu"), s_cpu);
|
||||
n += mg_xprintf(fn, arg, "%s%m:%d", ",", MG_ESC("ram"), s_ram);
|
||||
n += mg_xprintf(fn, arg, "}");
|
||||
return n;
|
||||
}
|
||||
static void get_metrics(union mg_val *val) {
|
||||
val->fn = (void (*)(void)) print_metrics;
|
||||
}
|
||||
|
||||
static double s_volume = 17.2;
|
||||
static char s_name[20] = "Dublin";
|
||||
// This function prints a JSON object with read-only metrics
|
||||
size_t print_settings(void (*fn)(char, void *), void *arg, va_list *ap) {
|
||||
size_t n = 0;
|
||||
(void) ap;
|
||||
n += mg_xprintf(fn, arg, "{");
|
||||
n += mg_xprintf(fn, arg, "%s%m:%g", " ", MG_ESC("volume"), s_volume);
|
||||
n += mg_xprintf(fn, arg, "%s%m:%m", ",", MG_ESC("name"), MG_ESC(s_name));
|
||||
n += mg_xprintf(fn, arg, "}");
|
||||
return n;
|
||||
}
|
||||
static void set_settings(const union mg_val *val) {
|
||||
mg_json_unescape(val->s, "$.name", s_name, sizeof(s_name));
|
||||
mg_json_get_num(val->s, "$.volume", &s_volume);
|
||||
}
|
||||
static void get_settings(union mg_val *val) {
|
||||
val->fn = (void (*)(void)) print_settings;
|
||||
}
|
||||
|
||||
static bool s_fan, s_relay1, s_relay2;
|
||||
size_t print_controls(void (*fn)(char, void *), void *arg, va_list *ap) {
|
||||
size_t n = 0;
|
||||
(void) ap;
|
||||
n += mg_xprintf(fn, arg, "{");
|
||||
n += mg_xprintf(fn, arg, "%s%m:%s", " ", MG_ESC("fan"),
|
||||
s_fan ? "true" : "false");
|
||||
n += mg_xprintf(fn, arg, "%s%m:%s", ",", MG_ESC("relay1"),
|
||||
s_relay1 ? "true" : "false");
|
||||
n += mg_xprintf(fn, arg, "%s%m:%s", ",", MG_ESC("relay2"),
|
||||
s_relay2 ? "true" : "false");
|
||||
n += mg_xprintf(fn, arg, "}");
|
||||
return n;
|
||||
}
|
||||
static void set_controls(const union mg_val *val) {
|
||||
mg_json_get_bool(val->s, "$.fan", &s_fan);
|
||||
mg_json_get_bool(val->s, "$.relay1", &s_relay1);
|
||||
mg_json_get_bool(val->s, "$.relay2", &s_relay2);
|
||||
}
|
||||
static void get_controls(union mg_val *val) {
|
||||
val->fn = (void (*)(void)) print_controls;
|
||||
}
|
||||
|
||||
// Modify this. This represents device's state to the dashboard
|
||||
static struct mg_field fields[] = {
|
||||
{"controls", MG_VAL_FN, get_controls, set_controls},
|
||||
{"settings", MG_VAL_FN, get_settings, set_settings},
|
||||
{"metrics", MG_VAL_FN, get_metrics, NULL},
|
||||
{"points", MG_VAL_FN, get_points, NULL},
|
||||
{NULL, 0, 0, 0},
|
||||
};
|
||||
|
||||
struct mg_mgr mgr;
|
||||
struct mg_dash dash = {fields};
|
||||
|
||||
void mongoose_init(void) {
|
||||
mg_mgr_init(&mgr);
|
||||
mg_http_listen(&mgr, HTTP_ADDR, mg_dash_ev_handler, &dash);
|
||||
}
|
||||
|
||||
void mongoose_poll(void) {
|
||||
mg_mgr_poll(&mgr, 1);
|
||||
|
||||
// Simulate metrics change periodically
|
||||
static uint64_t timer = 0;
|
||||
if (mg_timer_expired(&timer, 30000, mg_now())) {
|
||||
// s_temp = 14.8 + ((double)rand() / RAND_MAX) * 20.0;
|
||||
// s_humidity = 37 + (rand() % 21);
|
||||
// s_cpu = 3 + (rand() % 21);
|
||||
// s_ram = 25 + (rand() % 16);
|
||||
mg_dash_send_change(&mgr, mg_dash_find_field(fields, mg_str("metrics")));
|
||||
mg_dash_send_change(&mgr, mg_dash_find_field(fields, mg_str("points")));
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
mongoose_init();
|
||||
for (;;) {
|
||||
mongoose_poll();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
/*! https://github.com/leeoniya/uPlot (v1.6.32) */
|
||||
var uPlot=function(){"use strict";const l="u-off",e="u-label",t="width",n="height",i="top",o="bottom",s="left",r="right",u="#000",a=u+"0",f="mousemove",c="mousedown",h="mouseup",d="mouseenter",p="mouseleave",m="dblclick",g="change",x="dppxchange",w="--",_="undefined"!=typeof window,b=_?document:null,v=_?window:null,k=_?navigator:null;let y,M;function S(l,e){if(null!=e){let t=l.classList;!t.contains(e)&&t.add(e)}}function T(l,e){let t=l.classList;t.contains(e)&&t.remove(e)}function E(l,e,t){l.style[e]=t+"px"}function z(l,e,t,n){let i=b.createElement(l);return null!=e&&S(i,e),null!=t&&t.insertBefore(i,n),i}function D(l,e){return z("div",l,e)}const P=new WeakMap;function A(e,t,n,i,o){let s="translate("+t+"px,"+n+"px)";s!=P.get(e)&&(e.style.transform=s,P.set(e,s),0>t||0>n||t>i||n>o?S(e,l):T(e,l))}const W=new WeakMap;function Y(l,e,t){let n=e+t;n!=W.get(l)&&(W.set(l,n),l.style.background=e,l.style.borderColor=t)}const C=new WeakMap;function H(l,e,t,n){let i=e+""+t;i!=C.get(l)&&(C.set(l,i),l.style.height=t+"px",l.style.width=e+"px",l.style.marginLeft=n?-e/2+"px":0,l.style.marginTop=n?-t/2+"px":0)}const F={passive:!0},R={...F,capture:!0};function G(l,e,t,n){e.addEventListener(l,t,n?R:F)}function I(l,e,t){e.removeEventListener(l,t,F)}function L(l,e,t,n){let i;t=t||0;let o=2147483647>=(n=n||e.length-1);for(;n-t>1;)i=o?t+n>>1:sl((t+n)/2),l>e[i]?t=i:n=i;return l-e[t]>e[n]-l?n:t}function O(l){return(e,t,n)=>{let i=-1,o=-1;for(let o=t;n>=o;o++)if(l(e[o])){i=o;break}for(let i=n;i>=t;i--)if(l(e[i])){o=i;break}return[i,o]}}_&&function l(){let e=devicePixelRatio;y!=e&&(y=e,M&&I(g,M,l),M=matchMedia(`(min-resolution: ${y-.001}dppx) and (max-resolution: ${y+.001}dppx)`),G(g,M,l),v.dispatchEvent(new CustomEvent(x)))}();const N=l=>null!=l,j=l=>null!=l&&l>0,U=O(N),V=O(j);function B(l,e,t,n){let i=hl(l),o=hl(e);l==e&&(-1==i?(l*=t,e/=t):(l/=t,e*=t));let s=10==t?dl:pl,r=1==o?ul:sl,u=(1==i?sl:ul)(s(ol(l))),a=r(s(ol(e))),f=cl(t,u),c=cl(t,a);return 10==t&&(0>u&&(f=Al(f,-u)),0>a&&(c=Al(c,-a))),n||2==t?(l=f*i,e=c*o):(l=Pl(l,f),e=Dl(e,c)),[l,e]}function $(l,e,t,n){let i=B(l,e,t,n);return 0==l&&(i[0]=0),0==e&&(i[1]=0),i}const J=.1,q={mode:3,pad:J},K={pad:0,soft:null,mode:0},X={min:K,max:K};function Z(l,e,t,n){return Ol(t)?ll(l,e,t):(K.pad=t,K.soft=n?0:null,K.mode=n?3:0,ll(l,e,X))}function Q(l,e){return null==l?e:l}function ll(l,e,t){let n=t.min,i=t.max,o=Q(n.pad,0),s=Q(i.pad,0),r=Q(n.hard,-gl),u=Q(i.hard,gl),a=Q(n.soft,gl),f=Q(i.soft,-gl),c=Q(n.mode,0),h=Q(i.mode,0),d=e-l,p=dl(d),m=fl(ol(l),ol(e)),g=dl(m),x=ol(g-p);(1e-24>d||x>10)&&(d=0,0!=l&&0!=e||(d=1e-24,2==c&&a!=gl&&(o=0),2==h&&f!=-gl&&(s=0)));let w=d||m||1e3,_=dl(w),b=cl(10,sl(_)),v=Al(Pl(l-w*(0==d?0==l?.1:1:o),b/10),24),k=a>l||1!=c&&(3!=c||v>a)&&(2!=c||a>v)?gl:a,y=fl(r,k>v&&l>=k?k:al(k,v)),M=Al(Dl(e+w*(0==d?0==e?.1:1:s),b/10),24),S=e>f||1!=h&&(3!=h||f>M)&&(2!=h||M>f)?-gl:f,T=al(u,M>S&&S>=e?S:fl(S,M));return y==T&&0==y&&(T=100),[y,T]}const el=new Intl.NumberFormat(_?k.language:"en-US"),tl=l=>el.format(l),nl=Math,il=nl.PI,ol=nl.abs,sl=nl.floor,rl=nl.round,ul=nl.ceil,al=nl.min,fl=nl.max,cl=nl.pow,hl=nl.sign,dl=nl.log10,pl=nl.log2,ml=(l,e=1)=>nl.asinh(l/e),gl=1/0;function xl(l){return 1+(0|dl((l^l>>31)-(l>>31)))}function wl(l,e,t){return al(fl(l,e),t)}function _l(l){return"function"==typeof l}function bl(l){return _l(l)?l:()=>l}const vl=l=>l,kl=(l,e)=>e,yl=()=>null,Ml=()=>!0,Sl=(l,e)=>l==e,Tl=/\.\d*?(?=9{6,}|0{6,})/gm,El=l=>{if(Il(l)||Wl.has(l))return l;const e=""+l,t=e.match(Tl);if(null==t)return l;let n=t[0].length-1;if(-1!=e.indexOf("e-")){let[l,t]=e.split("e");return+`${El(l)}e${t}`}return Al(l,n)};function zl(l,e){return El(Al(El(l/e))*e)}function Dl(l,e){return El(ul(El(l/e))*e)}function Pl(l,e){return El(sl(El(l/e))*e)}function Al(l,e=0){if(Il(l))return l;let t=10**e;return rl(l*t*(1+Number.EPSILON))/t}const Wl=new Map;function Yl(l){return((""+l).split(".")[1]||"").length}function Cl(l,e,t,n){let i=[],o=n.map(Yl);for(let s=e;t>s;s++){let e=ol(s),t=Al(cl(l,s),e);for(let r=0;n.length>r;r++){let u=10==l?+`${n[r]}e${s}`:n[r]*t,a=(0>s?e:0)+(o[r]>s?o[r]:0),f=10==l?u:Al(u,a);i.push(f),Wl.set(f,a)}}return i}const Hl={},Fl=[],Rl=[null,null],Gl=Array.isArray,Il=Number.isInteger;function Ll(l){return"string"==typeof l}function Ol(l){let e=!1;if(null!=l){let t=l.constructor;e=null==t||t==Object}return e}function Nl(l){return null!=l&&"object"==typeof l}const jl=Object.getPrototypeOf(Uint8Array),Ul="__proto__";function Vl(l,e=Ol){let t;if(Gl(l)){let n=l.find((l=>null!=l));if(Gl(n)||e(n)){t=Array(l.length);for(let n=0;l.length>n;n++)t[n]=Vl(l[n],e)}else t=l.slice()}else if(l instanceof jl)t=l.slice();else if(e(l)){t={};for(let n in l)n!=Ul&&(t[n]=Vl(l[n],e))}else t=l;return t}function Bl(l){let e=arguments;for(let t=1;e.length>t;t++){let n=e[t];for(let e in n)e!=Ul&&(Ol(l[e])?Bl(l[e],Vl(n[e])):l[e]=Vl(n[e]))}return l}function $l(l,e,t){for(let n,i=0,o=-1;e.length>i;i++){let s=e[i];if(s>o){for(n=s-1;n>=0&&null==l[n];)l[n--]=null;for(n=s+1;t>n&&null==l[n];)l[o=n++]=null}}}const Jl="undefined"==Line truncated
|
||||
@@ -1 +0,0 @@
|
||||
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
|
||||
@@ -1,31 +0,0 @@
|
||||
# Environment setup: https://mongoose.ws/docs/getting-started/build-environment/
|
||||
|
||||
PROG ?= example
|
||||
MGDIR ?= ../../..
|
||||
SOURCES = server.c dashboard.html.c $(MGDIR)/mongoose.c
|
||||
CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -g3 -O0
|
||||
PFLAGS = -I$(MGDIR) -DMG_DATA_SIZE=64 -DMG_TLS=MG_TLS_BUILTIN
|
||||
AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment
|
||||
|
||||
all: $(PROG)
|
||||
./$(PROG)
|
||||
|
||||
# Rebuild the program
|
||||
$(PROG): $(SOURCES)
|
||||
$(CC) $(SOURCES) $(CFLAGS) $(PFLAGS) $(AFLAGS) -o $@
|
||||
|
||||
# Generate a single, self-contained dashboard.html from the UI in web_root/
|
||||
dashboard.html: $(wildcard web_root/*)
|
||||
monolith web_root/index.html > $@
|
||||
|
||||
# Create an embedded filesystem containing compressed dashboard.html
|
||||
dashboard.html.c: dashboard.html
|
||||
node $(MGDIR)/resources/pack.js dashboard.html::gzip > $@
|
||||
@echo "On-flash size: `grep -oE '[0-9]+' $@ | wc -l`"
|
||||
|
||||
# For development: rebuild and restart if any of the sources change
|
||||
watch: $(SOURCES)
|
||||
watchexec -r -w server.c -w web_root -e c,h,html,css,js -- make
|
||||
|
||||
clean:
|
||||
rm -rf $(PROG) *.exe *.obj *.dSYM mongoose dashboard.*
|
||||
@@ -1,51 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Cesanta Software Limited
|
||||
// SPDX-License-Identifier: GPL-2.0-only or commercial
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
#define HTTP_ADDR "http://0.0.0.0:8000"
|
||||
#else
|
||||
#define HTTP_ADDR "http://0.0.0.0:80"
|
||||
#endif
|
||||
|
||||
static const char *s_version = "1.2.3";
|
||||
static bool s_led1 = true; // Mock LED
|
||||
|
||||
static void led1_get(union mg_val *val) {
|
||||
val->b = s_led1;
|
||||
}
|
||||
static void led1_set(const union mg_val *val) {
|
||||
s_led1 = val->b;
|
||||
}
|
||||
|
||||
static void firmware_version_get(union mg_val *val) {
|
||||
val->s = mg_str(s_version);
|
||||
}
|
||||
|
||||
// Modify this. This represents device's state to the dashboard
|
||||
static struct mg_field fields[] = {
|
||||
{"led1", MG_VAL_BOOL, led1_get, led1_set},
|
||||
{"firmware_version", MG_VAL_STR, firmware_version_get, NULL},
|
||||
{NULL, 0, 0, 0},
|
||||
};
|
||||
|
||||
struct mg_mgr mgr;
|
||||
struct mg_dash dash = {fields};
|
||||
|
||||
void mongoose_init(void) {
|
||||
mg_mgr_init(&mgr);
|
||||
mg_http_listen(&mgr, HTTP_ADDR, mg_dash_ev_handler, &dash);
|
||||
}
|
||||
|
||||
void mongoose_poll(void) {
|
||||
mg_mgr_poll(&mgr, 1);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
mongoose_init();
|
||||
for (;;) {
|
||||
mongoose_poll();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Mongoose device dashboard -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/cesanta/mongoose@master/resources/dashboard.js"></script>
|
||||
<script>
|
||||
Dashboard.init({
|
||||
debug: true,
|
||||
data: {
|
||||
led1: true,
|
||||
firmware_version: '1.0.0',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div style="margin: 2rem auto; width: 480px; padding: 1rem 2rem; background: #0001; border-radius:.3rem;">
|
||||
<h1 style="font-size: 1.25rem;">
|
||||
My Device, version <span data-set-html="Dashboard.data.firmware_version"></span>
|
||||
</h1>
|
||||
<div style="margin: 1rem 0;">
|
||||
<span>LED status:</span>
|
||||
<input type="checkbox" class="toggle" data-bind="led1" data-autosave="1" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,48 +0,0 @@
|
||||
:root {
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box; margin: 0; padding: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: relative;
|
||||
width: 2.75rem;
|
||||
height: 1.6rem;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
border: 1px solid transparent;
|
||||
background: #d1d5db;
|
||||
border-radius: 0.75rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
.toggle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
top: 1px;
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.toggle:checked {
|
||||
background: #d1d5db;
|
||||
}
|
||||
.toggle:checked::after {
|
||||
transform: translateX(calc(100% - 3px));
|
||||
}
|
||||
.toggle:disabled,
|
||||
.toggle.disabled {
|
||||
background: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.toggle.edited {
|
||||
border-color: #ee0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2026 Cesanta Software Limited
|
||||
// SPDX-License-Identifier: GPL-2.0-only or commercial
|
||||
|
||||
extern void mongoose_init(void);
|
||||
extern void mongoose_poll(void);
|
||||
|
||||
int main(void) {
|
||||
mongoose_init();
|
||||
for (;;) {
|
||||
mongoose_poll();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-FileCopyrightText: 2026 Cesanta Software Limited
|
||||
// SPDX-License-Identifier: GPL-2.0-only or commercial
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
#define HTTP_ADDR "http://0.0.0.0:8000"
|
||||
#else
|
||||
#define HTTP_ADDR "http://0.0.0.0:80"
|
||||
#endif
|
||||
|
||||
// Control panel
|
||||
static bool s_fan, s_relay1, s_relay2;
|
||||
|
||||
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)},
|
||||
{NULL, 0, NULL, 0},
|
||||
};
|
||||
|
||||
static void sync_control(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);
|
||||
} else {
|
||||
// s_fan = gpio_read(FAN_PIN);
|
||||
// s_relay1 = gpio_read(RELAY_1_PIN);
|
||||
// s_relay2 = gpio_read(RELAY_2_PIN);
|
||||
}
|
||||
}
|
||||
|
||||
// Read-only device Metrics
|
||||
static int s_ram = 32, s_cpu = 37, s_humidity = 59;
|
||||
static double s_temperature = 24.8;
|
||||
|
||||
static struct mg_field fields_metrics[] = {
|
||||
{"ram", MG_VAL_INT, &s_ram, sizeof(s_ram)},
|
||||
{"cpu", MG_VAL_INT, &s_cpu, sizeof(s_cpu)},
|
||||
{"humidity", MG_VAL_INT, &s_humidity, sizeof(s_humidity)},
|
||||
{"temperature", MG_VAL_DBL, &s_temperature, sizeof(s_temperature)},
|
||||
{NULL, 0, NULL, 0},
|
||||
};
|
||||
|
||||
static void sync_metrics(bool is_write) {
|
||||
if (is_write) return;
|
||||
// Read values from hardware into the state variables
|
||||
// s_ram = calculate_current_free_ram();
|
||||
}
|
||||
|
||||
// Read-write device settings
|
||||
static double s_volume = 17.2;
|
||||
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)},
|
||||
{NULL, 0, NULL, 0},
|
||||
};
|
||||
|
||||
static void sync_settings(bool is_write) {
|
||||
if (is_write) {
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
#define NUM_POINTS 7 // How many data points to send
|
||||
|
||||
struct point {
|
||||
uint16_t x; // X coordinate of the data point, usually this is time
|
||||
uint16_t y1; // We can plot multiple values per graph,
|
||||
uint16_t y2; // let's plot 2 values: y1 and y2.
|
||||
};
|
||||
|
||||
// Serialise chart data into a string: [[x,y1,y2],[x,y1,y2],...]
|
||||
// One uint16_t takes 5 character max, so one entry is 19 bytes max.
|
||||
// We could be more efficient by storing hex or base64-encoded data, but
|
||||
// let's keep it simple. However if we do, update data parsing logic in
|
||||
// the web_root/main.js
|
||||
#define POINT_SIZE 19
|
||||
static char s_chart1_data[NUM_POINTS * POINT_SIZE + 2 + 1];
|
||||
|
||||
static struct mg_field fields_chart1[] = {
|
||||
{"data", MG_VAL_STR, s_chart1_data, sizeof(s_chart1_data)},
|
||||
{NULL, 0, NULL, 0},
|
||||
};
|
||||
|
||||
// Randomly generate graph points and serialise them into a string
|
||||
void sync_chart1(bool is_write) {
|
||||
size_t i, len = 0;
|
||||
if (is_write) return;
|
||||
len += mg_snprintf(s_chart1_data + len, sizeof(s_chart1_data) - len, "[");
|
||||
for (i = 0; i < NUM_POINTS; i++) {
|
||||
struct point p;
|
||||
p.x = (uint16_t) i;
|
||||
p.y1 = 85 - (uint16_t) ((49 * i) / NUM_POINTS) + (rand() % 9);
|
||||
p.y2 = 20 + (uint16_t) ((69 * i) / NUM_POINTS) + (rand() % 5);
|
||||
len += mg_snprintf(s_chart1_data + len, sizeof(s_chart1_data) - len,
|
||||
"%s[%hu,%hu,%hu]", i > 0 ? "," : "", p.x, p.y1, p.y2);
|
||||
}
|
||||
len += mg_snprintf(s_chart1_data + len, sizeof(s_chart1_data) - len, "]");
|
||||
}
|
||||
|
||||
static struct mg_field_set field_sets[] = {
|
||||
{"control", fields_control, sync_control, 0, 0},
|
||||
{"metrics", fields_metrics, sync_metrics, 0, 0},
|
||||
{"settings", fields_settings, sync_settings, 0, 0},
|
||||
{"chart1", fields_chart1, sync_chart1, 0, 0},
|
||||
{0, 0, 0, 0, 0},
|
||||
};
|
||||
static struct mg_mgr s_mgr;
|
||||
static struct mg_dash s_dashboard = {field_sets};
|
||||
|
||||
void mongoose_init(void) {
|
||||
mg_mgr_init(&s_mgr);
|
||||
mg_http_listen(&s_mgr, HTTP_ADDR, mg_dash_ev_handler, &s_dashboard);
|
||||
}
|
||||
|
||||
void mongoose_poll(void) {
|
||||
mg_mgr_poll(&s_mgr, 1);
|
||||
|
||||
{
|
||||
// Send metrics change periodically
|
||||
static uint64_t timer = 0;
|
||||
if (mg_timer_expired(&timer, 30000, mg_now())) {
|
||||
sync_metrics(false);
|
||||
mg_dash_send_change(&s_mgr, &s_dashboard, "metrics");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
# Environment setup: https://mongoose.ws/docs/getting-started/build-environment/
|
||||
|
||||
PROG ?= example
|
||||
MGDIR ?= ../../..
|
||||
SOURCES = server.c dashboard.html.c $(MGDIR)/mongoose.c
|
||||
CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -g3 -O0
|
||||
PFLAGS = -I$(MGDIR) -DMG_DATA_SIZE=64 -DMG_TLS=MG_TLS_BUILTIN
|
||||
AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment
|
||||
|
||||
all: $(PROG)
|
||||
./$(PROG)
|
||||
|
||||
# Rebuild the program
|
||||
$(PROG): $(SOURCES)
|
||||
$(CC) $(SOURCES) $(CFLAGS) $(PFLAGS) $(AFLAGS) -o $@
|
||||
|
||||
# Generate a single, self-contained dashboard.html from the UI in web_root/
|
||||
dashboard.html: $(wildcard web_root/*)
|
||||
monolith web_root/index.html > $@
|
||||
|
||||
# Create an embedded filesystem containing compressed dashboard.html
|
||||
dashboard.html.c: dashboard.html
|
||||
node $(MGDIR)/resources/pack.js dashboard.html::gzip > $@
|
||||
@echo "On-flash size: `grep -oE '[0-9]+' $@ | wc -l`"
|
||||
|
||||
# For development: rebuild and restart if any of the sources change
|
||||
watch: $(SOURCES)
|
||||
watchexec -r -w server.c -w web_root -e c,h,html,css,js -- make
|
||||
|
||||
clean:
|
||||
rm -rf $(PROG) *.exe *.obj *.dSYM mongoose dashboard.*
|
||||
@@ -1,77 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Cesanta Software Limited
|
||||
// SPDX-License-Identifier: GPL-2.0-only or commercial
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
#define HTTP_ADDR "http://0.0.0.0:8000"
|
||||
#else
|
||||
#define HTTP_ADDR "http://0.0.0.0:80"
|
||||
#endif
|
||||
|
||||
#define NUM_POINTS 512 // How many data points to send
|
||||
#define INTERVAL_MS 200 // How often, in milliseconds
|
||||
|
||||
struct point {
|
||||
uint16_t x; // X coordinate of the data point, usually this is time
|
||||
uint16_t y1; // We can plot multiple values per graph,
|
||||
uint16_t y2; // let's plot 2 values: y1 and y2.
|
||||
};
|
||||
|
||||
// This function prints a JSON array of plot points
|
||||
size_t print_points(void (*fn)(char, void *), void *arg, va_list *ap) {
|
||||
size_t i, n = 0;
|
||||
const char *comma = "";
|
||||
(void) ap;
|
||||
n += mg_xprintf(fn, arg, "[");
|
||||
for (i = 0; i < NUM_POINTS; i++) {
|
||||
struct point p;
|
||||
p.x = (uint16_t) i;
|
||||
p.y1 = 50 -
|
||||
(uint16_t) ((40 * (i > (NUM_POINTS - 1) / 4
|
||||
? i - (NUM_POINTS - 1) / 4
|
||||
: (NUM_POINTS - 1) / 4 - i)) /
|
||||
(NUM_POINTS - 1)) +
|
||||
(rand() % 9);
|
||||
p.y2 = 20 + (uint16_t) ((29 * i) / NUM_POINTS) + (rand() % 5);
|
||||
n += mg_xprintf(fn, arg, "%s[%hu,%hu,%hu]", comma, p.x, p.y1, p.y2);
|
||||
comma = ",";
|
||||
}
|
||||
n += mg_xprintf(fn, arg, "]");
|
||||
return n;
|
||||
}
|
||||
|
||||
static void get_points(union mg_val *val) {
|
||||
val->fn = (void (*)(void)) print_points;
|
||||
}
|
||||
|
||||
// Modify this. This represents device's state to the dashboard
|
||||
static struct mg_field fields[] = {
|
||||
{"points", MG_VAL_FN, get_points, NULL},
|
||||
{NULL, 0, 0, 0},
|
||||
};
|
||||
|
||||
struct mg_mgr mgr;
|
||||
struct mg_dash dash = {fields};
|
||||
|
||||
void mongoose_init(void) {
|
||||
mg_mgr_init(&mgr);
|
||||
mg_http_listen(&mgr, HTTP_ADDR, mg_dash_ev_handler, &dash);
|
||||
}
|
||||
|
||||
void mongoose_poll(void) {
|
||||
mg_mgr_poll(&mgr, 1);
|
||||
// Send WS change notifications periodically
|
||||
static uint64_t timer = 0;
|
||||
if (mg_timer_expired(&timer, INTERVAL_MS, mg_now())) {
|
||||
mg_dash_send_change(&mgr, mg_dash_find_field(fields, mg_str("points")));
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
mongoose_init();
|
||||
for (;;) {
|
||||
mongoose_poll();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Mongoose device dashboard -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/cesanta/mongoose@master/resources/dashboard.js"></script>
|
||||
<script>
|
||||
Dashboard.init({
|
||||
debug: false,
|
||||
data: {
|
||||
points: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<link rel="stylesheet" href="uplot.min.css" />
|
||||
|
||||
<script src="uPlot.iife.min.js"></script>
|
||||
<script src="main.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="main">
|
||||
<h1>
|
||||
Graph example
|
||||
</h1>
|
||||
|
||||
<div class="panel" style="margin: 1rem 0;">
|
||||
<div class="panel-title">Graph</div>
|
||||
<div class="panel-row">
|
||||
|
||||
<div id="graph" style="height: 10rem; width: 100%;">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size:80%;color: #000000a0">
|
||||
<p>In server.c, we generate "change" notifications periodically,
|
||||
reporting 512 data points:
|
||||
<code>{"method":"change","params":{"points":[[0,83],[1,99],...</code>
|
||||
</p>
|
||||
<p>In main.js, we create a uPlot chart instance bound to the div
|
||||
inside the panel</p>
|
||||
<p>In main.js, we call <code>Dashboard.on('change', function() {...})</code> to intercept all change notifications, get the points, and then calling chart.setData() to plot received values.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,36 +0,0 @@
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
|
||||
const el = document.getElementById("graph");
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
const options = {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
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') },
|
||||
],
|
||||
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)'},
|
||||
],
|
||||
};
|
||||
|
||||
// Create chart instance
|
||||
const chart = new uPlot(options, [], el);
|
||||
|
||||
// Intercept change notifications
|
||||
Dashboard.on('change', function(args) {
|
||||
const data = [
|
||||
args.points.map(x => x[0]),
|
||||
args.points.map(x => x[1]),
|
||||
args.points.map(x => x[2]),
|
||||
];
|
||||
//console.log(123, args.points);
|
||||
chart.setData(data);
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
:root {
|
||||
--btn-color: white;
|
||||
--btn-disabled-color: #d1d5db;
|
||||
--bg-color: '#d8dcdf';
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box; margin: 0; padding: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
body { background: #0004;}
|
||||
h1 {font-size: 1.25rem;}
|
||||
|
||||
.main {
|
||||
margin: 2rem auto; width: 640px; padding: 1em 2em;
|
||||
border-radius:0.3rem; background: #fff5;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex; flex-direction: column; gap: 0.5rem;
|
||||
background: #fffe;
|
||||
border: 1px solid #0002;
|
||||
border-radius: 0.3rem;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.panel-title {
|
||||
margin: -0.5rem -0.75rem 0 -0.75rem;
|
||||
padding: 0.5rem 0.75rem 0.3rem 0.75rem;
|
||||
border-bottom: 1px solid #0003;
|
||||
border-radius: 0.25rem 0.25rem 0 0;
|
||||
background: #0001;
|
||||
font-weight: 600;
|
||||
color: #000c;
|
||||
}
|
||||
|
||||
.input, .dropdown {
|
||||
display:inline-flex;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
border: 1px solid #ccc;
|
||||
border-width: 1px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin:0;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0.25rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: #2563eb;
|
||||
color: var(--btn-color);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.button.loading {
|
||||
color: transparent;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.button:disabled,.button:hover:disabled,.button.disabled,.button:hover.disabled {
|
||||
background: var(--btn-disabled-color) !important;
|
||||
border-color: var(--btn-disabled-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.panel-row {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input:disabled,.select:disabled,.input.disabled,.select.disabled,.slider.disabled {
|
||||
background: var(--bg-color) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input.edited, .select.edited, .slider.edited>* {
|
||||
border-color: #50500040;
|
||||
background: #ffff0020;
|
||||
}
|
||||
|
||||
.input.error, .select.error, .slider.error>* {
|
||||
border-color: #50000040;
|
||||
background: #ff000020;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
/*! https://github.com/leeoniya/uPlot (v1.6.32) */
|
||||
var uPlot=function(){"use strict";const l="u-off",e="u-label",t="width",n="height",i="top",o="bottom",s="left",r="right",u="#000",a=u+"0",f="mousemove",c="mousedown",h="mouseup",d="mouseenter",p="mouseleave",m="dblclick",g="change",x="dppxchange",w="--",_="undefined"!=typeof window,b=_?document:null,v=_?window:null,k=_?navigator:null;let y,M;function S(l,e){if(null!=e){let t=l.classList;!t.contains(e)&&t.add(e)}}function T(l,e){let t=l.classList;t.contains(e)&&t.remove(e)}function E(l,e,t){l.style[e]=t+"px"}function z(l,e,t,n){let i=b.createElement(l);return null!=e&&S(i,e),null!=t&&t.insertBefore(i,n),i}function D(l,e){return z("div",l,e)}const P=new WeakMap;function A(e,t,n,i,o){let s="translate("+t+"px,"+n+"px)";s!=P.get(e)&&(e.style.transform=s,P.set(e,s),0>t||0>n||t>i||n>o?S(e,l):T(e,l))}const W=new WeakMap;function Y(l,e,t){let n=e+t;n!=W.get(l)&&(W.set(l,n),l.style.background=e,l.style.borderColor=t)}const C=new WeakMap;function H(l,e,t,n){let i=e+""+t;i!=C.get(l)&&(C.set(l,i),l.style.height=t+"px",l.style.width=e+"px",l.style.marginLeft=n?-e/2+"px":0,l.style.marginTop=n?-t/2+"px":0)}const F={passive:!0},R={...F,capture:!0};function G(l,e,t,n){e.addEventListener(l,t,n?R:F)}function I(l,e,t){e.removeEventListener(l,t,F)}function L(l,e,t,n){let i;t=t||0;let o=2147483647>=(n=n||e.length-1);for(;n-t>1;)i=o?t+n>>1:sl((t+n)/2),l>e[i]?t=i:n=i;return l-e[t]>e[n]-l?n:t}function O(l){return(e,t,n)=>{let i=-1,o=-1;for(let o=t;n>=o;o++)if(l(e[o])){i=o;break}for(let i=n;i>=t;i--)if(l(e[i])){o=i;break}return[i,o]}}_&&function l(){let e=devicePixelRatio;y!=e&&(y=e,M&&I(g,M,l),M=matchMedia(`(min-resolution: ${y-.001}dppx) and (max-resolution: ${y+.001}dppx)`),G(g,M,l),v.dispatchEvent(new CustomEvent(x)))}();const N=l=>null!=l,j=l=>null!=l&&l>0,U=O(N),V=O(j);function B(l,e,t,n){let i=hl(l),o=hl(e);l==e&&(-1==i?(l*=t,e/=t):(l/=t,e*=t));let s=10==t?dl:pl,r=1==o?ul:sl,u=(1==i?sl:ul)(s(ol(l))),a=r(s(ol(e))),f=cl(t,u),c=cl(t,a);return 10==t&&(0>u&&(f=Al(f,-u)),0>a&&(c=Al(c,-a))),n||2==t?(l=f*i,e=c*o):(l=Pl(l,f),e=Dl(e,c)),[l,e]}function $(l,e,t,n){let i=B(l,e,t,n);return 0==l&&(i[0]=0),0==e&&(i[1]=0),i}const J=.1,q={mode:3,pad:J},K={pad:0,soft:null,mode:0},X={min:K,max:K};function Z(l,e,t,n){return Ol(t)?ll(l,e,t):(K.pad=t,K.soft=n?0:null,K.mode=n?3:0,ll(l,e,X))}function Q(l,e){return null==l?e:l}function ll(l,e,t){let n=t.min,i=t.max,o=Q(n.pad,0),s=Q(i.pad,0),r=Q(n.hard,-gl),u=Q(i.hard,gl),a=Q(n.soft,gl),f=Q(i.soft,-gl),c=Q(n.mode,0),h=Q(i.mode,0),d=e-l,p=dl(d),m=fl(ol(l),ol(e)),g=dl(m),x=ol(g-p);(1e-24>d||x>10)&&(d=0,0!=l&&0!=e||(d=1e-24,2==c&&a!=gl&&(o=0),2==h&&f!=-gl&&(s=0)));let w=d||m||1e3,_=dl(w),b=cl(10,sl(_)),v=Al(Pl(l-w*(0==d?0==l?.1:1:o),b/10),24),k=a>l||1!=c&&(3!=c||v>a)&&(2!=c||a>v)?gl:a,y=fl(r,k>v&&l>=k?k:al(k,v)),M=Al(Dl(e+w*(0==d?0==e?.1:1:s),b/10),24),S=e>f||1!=h&&(3!=h||f>M)&&(2!=h||M>f)?-gl:f,T=al(u,M>S&&S>=e?S:fl(S,M));return y==T&&0==y&&(T=100),[y,T]}const el=new Intl.NumberFormat(_?k.language:"en-US"),tl=l=>el.format(l),nl=Math,il=nl.PI,ol=nl.abs,sl=nl.floor,rl=nl.round,ul=nl.ceil,al=nl.min,fl=nl.max,cl=nl.pow,hl=nl.sign,dl=nl.log10,pl=nl.log2,ml=(l,e=1)=>nl.asinh(l/e),gl=1/0;function xl(l){return 1+(0|dl((l^l>>31)-(l>>31)))}function wl(l,e,t){return al(fl(l,e),t)}function _l(l){return"function"==typeof l}function bl(l){return _l(l)?l:()=>l}const vl=l=>l,kl=(l,e)=>e,yl=()=>null,Ml=()=>!0,Sl=(l,e)=>l==e,Tl=/\.\d*?(?=9{6,}|0{6,})/gm,El=l=>{if(Il(l)||Wl.has(l))return l;const e=""+l,t=e.match(Tl);if(null==t)return l;let n=t[0].length-1;if(-1!=e.indexOf("e-")){let[l,t]=e.split("e");return+`${El(l)}e${t}`}return Al(l,n)};function zl(l,e){return El(Al(El(l/e))*e)}function Dl(l,e){return El(ul(El(l/e))*e)}function Pl(l,e){return El(sl(El(l/e))*e)}function Al(l,e=0){if(Il(l))return l;let t=10**e;return rl(l*t*(1+Number.EPSILON))/t}const Wl=new Map;function Yl(l){return((""+l).split(".")[1]||"").length}function Cl(l,e,t,n){let i=[],o=n.map(Yl);for(let s=e;t>s;s++){let e=ol(s),t=Al(cl(l,s),e);for(let r=0;n.length>r;r++){let u=10==l?+`${n[r]}e${s}`:n[r]*t,a=(0>s?e:0)+(o[r]>s?o[r]:0),f=10==l?u:Al(u,a);i.push(f),Wl.set(f,a)}}return i}const Hl={},Fl=[],Rl=[null,null],Gl=Array.isArray,Il=Number.isInteger;function Ll(l){return"string"==typeof l}function Ol(l){let e=!1;if(null!=l){let t=l.constructor;e=null==t||t==Object}return e}function Nl(l){return null!=l&&"object"==typeof l}const jl=Object.getPrototypeOf(Uint8Array),Ul="__proto__";function Vl(l,e=Ol){let t;if(Gl(l)){let n=l.find((l=>null!=l));if(Gl(n)||e(n)){t=Array(l.length);for(let n=0;l.length>n;n++)t[n]=Vl(l[n],e)}else t=l.slice()}else if(l instanceof jl)t=l.slice();else if(e(l)){t={};for(let n in l)n!=Ul&&(t[n]=Vl(l[n],e))}else t=l;return t}function Bl(l){let e=arguments;for(let t=1;e.length>t;t++){let n=e[t];for(let e in n)e!=Ul&&(Ol(l[e])?Bl(l[e],Vl(n[e])):l[e]=Vl(n[e]))}return l}function $l(l,e,t){for(let n,i=0,o=-1;e.length>i;i++){let s=e[i];if(s>o){for(n=s-1;n>=0&&null==l[n];)l[n--]=null;for(n=s+1;t>n&&null==l[n];)l[o=n++]=null}}}const Jl="undefined"==Line truncated
|
||||
@@ -1 +0,0 @@
|
||||
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
|
||||
@@ -1,31 +0,0 @@
|
||||
# Environment setup: https://mongoose.ws/docs/getting-started/build-environment/
|
||||
|
||||
PROG ?= example
|
||||
MGDIR ?= ../../..
|
||||
SOURCES = server.c dashboard.html.c $(MGDIR)/mongoose.c
|
||||
CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -g3 -O0
|
||||
PFLAGS = -I$(MGDIR) -DMG_DATA_SIZE=64 -DMG_TLS=MG_TLS_BUILTIN
|
||||
AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment
|
||||
|
||||
all: $(PROG)
|
||||
./$(PROG)
|
||||
|
||||
# Rebuild the program
|
||||
$(PROG): $(SOURCES)
|
||||
$(CC) $(SOURCES) $(CFLAGS) $(PFLAGS) $(AFLAGS) -o $@
|
||||
|
||||
# Generate a single, self-contained dashboard.html from the UI in web_root/
|
||||
dashboard.html: $(wildcard web_root/*)
|
||||
monolith web_root/index.html > $@
|
||||
|
||||
# Create an embedded filesystem containing compressed dashboard.html
|
||||
dashboard.html.c: dashboard.html
|
||||
node $(MGDIR)/resources/pack.js dashboard.html::gzip > $@
|
||||
@echo "On-flash size: `grep -oE '[0-9]+' $@ | wc -l`"
|
||||
|
||||
# For development: rebuild and restart if any of the sources change
|
||||
watch: $(SOURCES)
|
||||
watchexec -r -w server.c -w web_root -e c,h,html,css,js -- make
|
||||
|
||||
clean:
|
||||
rm -rf $(PROG) *.exe *.obj *.dSYM mongoose dashboard.*
|
||||
@@ -1,54 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Cesanta Software Limited
|
||||
// SPDX-License-Identifier: GPL-2.0-only or commercial
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
#define HTTP_ADDR "http://0.0.0.0:8000"
|
||||
#else
|
||||
#define HTTP_ADDR "http://0.0.0.0:80"
|
||||
#endif
|
||||
|
||||
static int s_threshold = 17;
|
||||
static char s_location[20] = "Galway";
|
||||
|
||||
static void location_get(union mg_val *val) {
|
||||
val->s = mg_str(s_location);
|
||||
}
|
||||
static void location_set(const union mg_val *val) {
|
||||
mg_json_unescape(val->s, "$", s_location, sizeof(s_location));
|
||||
}
|
||||
|
||||
static void threshold_get(union mg_val *val) {
|
||||
val->i = s_threshold;
|
||||
}
|
||||
static void threshold_set(const union mg_val *val) {
|
||||
s_threshold = val->i;
|
||||
}
|
||||
|
||||
// Modify this. This represents device's state to the dashboard
|
||||
static struct mg_field fields[] = {
|
||||
{"location", MG_VAL_STR, location_get, location_set},
|
||||
{"threshold", MG_VAL_INT, threshold_get, threshold_set},
|
||||
{0, 0, 0, 0},
|
||||
};
|
||||
|
||||
struct mg_mgr mgr;
|
||||
struct mg_dash dash = {fields};
|
||||
|
||||
void mongoose_init(void) {
|
||||
mg_mgr_init(&mgr);
|
||||
mg_http_listen(&mgr, HTTP_ADDR, mg_dash_ev_handler, &dash);
|
||||
}
|
||||
|
||||
void mongoose_poll(void) {
|
||||
mg_mgr_poll(&mgr, 1);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
mongoose_init();
|
||||
for (;;) {
|
||||
mongoose_poll();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Mongoose device dashboard -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/cesanta/mongoose@master/resources/dashboard.js"></script>
|
||||
<script>
|
||||
Dashboard.init({
|
||||
debug: true,
|
||||
data: {
|
||||
location: 'Dublin',
|
||||
threshold: 42,
|
||||
firmware_version: '1.0.0',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="main">
|
||||
<h1>
|
||||
Settings panel example
|
||||
</h1>
|
||||
|
||||
<div class="panel" style="margin: 1rem 0;">
|
||||
<div class="panel-title">Settings panel</div>
|
||||
<div class="panel-row">
|
||||
<span>Location:</span>
|
||||
<input class="input" data-bind='location' data-autosave="1" />
|
||||
</div>
|
||||
<div class="panel-row">
|
||||
<span>Noise threshold:</span>
|
||||
<input class="input" data-bind="threshold" />
|
||||
</div>
|
||||
<div class="panel-row" style="justify-content: end;">
|
||||
<button class="button" data-cancel="loc.*|threshold">cancel</button>
|
||||
<button class="button" data-save="loc.*|threshold">save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size:80%;color: #000000a0">
|
||||
When a field is edited, a local copy is created. For as long as that
|
||||
copy is different from the device's value, the field is added an
|
||||
"edited" CSS class. Bad values (e.g. non-numeric value for the field
|
||||
that is bound to a numeric data) give a "error" CSS class.
|
||||
|
||||
<p style="margin-top: .5rem;">The Location field has autosave flag set, so it is saved automatically
|
||||
as the user type text<//>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,108 +0,0 @@
|
||||
:root {
|
||||
--btn-color: white;
|
||||
--btn-disabled-color: #d1d5db;
|
||||
--bg-color: '#d8dcdf';
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box; margin: 0; padding: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
body { background: #0004;}
|
||||
h1 {font-size: 1.25rem;}
|
||||
|
||||
.main {
|
||||
margin: 2rem auto; width: 380px; padding: 1em 2em;
|
||||
border-radius:0.3rem; background: #fff5;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex; flex-direction: column; gap: 0.5rem;
|
||||
background: #fffe;
|
||||
border: 1px solid #0002;
|
||||
border-radius: 0.3rem;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.panel-title {
|
||||
margin: -0.5rem -0.75rem 0 -0.75rem;
|
||||
padding: 0.5rem 0.75rem 0.3rem 0.75rem;
|
||||
border-bottom: 1px solid #0003;
|
||||
border-radius: 0.25rem 0.25rem 0 0;
|
||||
background: #0001;
|
||||
font-weight: 600;
|
||||
color: #000c;
|
||||
}
|
||||
|
||||
.input, .dropdown {
|
||||
display:inline-flex;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
border: 1px solid #ccc;
|
||||
border-width: 1px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin:0;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0.25rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: #2563eb;
|
||||
color: var(--btn-color);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.button.loading {
|
||||
color: transparent;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.button:disabled,.button:hover:disabled,.button.disabled,.button:hover.disabled {
|
||||
background: var(--btn-disabled-color) !important;
|
||||
border-color: var(--btn-disabled-color);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.panel-row {
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input:disabled,.select:disabled,.input.disabled,.select.disabled,.slider.disabled {
|
||||
background: var(--bg-color) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input.edited, .select.edited, .slider.edited>* {
|
||||
border-color: #50500040;
|
||||
background: #ffff0020;
|
||||
}
|
||||
|
||||
.input.error, .select.error, .slider.error>* {
|
||||
border-color: #50000040;
|
||||
background: #ff000020;
|
||||
}
|
||||
+11
-6
@@ -1,10 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="uPlot.min.css">
|
||||
<title>My Dashboard</title>
|
||||
|
||||
<!-- Mongoose device dashboard -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/cesanta/mongoose@master/resources/dashboard.js"></script>
|
||||
<script>
|
||||
@@ -31,9 +29,16 @@
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="main.js" defer></script>
|
||||
<script src="uPlot.iife.min.js" defer></script>
|
||||
<title>My Dashboard</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<!-- uPlot graph library -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uplot@1.6.32/dist/uPlot.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/uplot@1.6.32/dist/uPlot.iife.min.js" defer></script>
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
File renamed without changes.
+10
-9
@@ -71,18 +71,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
],
|
||||
};
|
||||
|
||||
function updateGraphPoints(args) {
|
||||
if (!args.points || !chart) return;
|
||||
const data = [
|
||||
args.points.map(x => x[0]),
|
||||
args.points.map(x => x[1]),
|
||||
args.points.map(x => x[2]),
|
||||
function updateGraphPoints(data) {
|
||||
if (!data) return;
|
||||
const parsed = JSON.parse(data);
|
||||
const uplot_data = [
|
||||
parsed.map(x => x[0]),
|
||||
parsed.map(x => x[1]),
|
||||
parsed.map(x => x[2]),
|
||||
];
|
||||
chart.setData(data);
|
||||
chart.setData(uplot_data);
|
||||
};
|
||||
|
||||
// Intercept change notifications
|
||||
Dashboard.on('change', args => updateGraphPoints(args));
|
||||
Dashboard.call('get', 'points').then(resp => updateGraphPoints(resp));
|
||||
Dashboard.on('change', args => updateGraphPoints(args?.chart1?.data));
|
||||
Dashboard.call('get', 'chart1').then(resp => updateGraphPoints(resp?.data));
|
||||
|
||||
});
|
||||
File renamed without changes.
Reference in new issue
Block a user