Refactor field / fieldsets

This commit is contained in:
cpq committed 2026-04-21 14:24:25 +01:00
1 parent a160aa6e5f
commit 06b864c328
27 files changed
+428 -1018

No files matched your search

+128 -69
View File
@@ -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
View File
@@ -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);
}
@@ -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;
}
+13
View File
@@ -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;
}
+131
View File
@@ -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;
}
@@ -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">
@@ -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));
});