diff --git a/mongoose.c b/mongoose.c index 5ce93212..485238e0 100644 --- a/mongoose.c +++ b/mongoose.c @@ -3342,38 +3342,42 @@ bool mg_json_get_bool(struct mg_str json, const char *path, bool *v) { return found; } -bool mg_json_unescape(struct mg_str s, char *to, size_t n) { - size_t i, j; - for (i = 0, j = 0; i < s.len && j < n; i++, j++) { - if (s.buf[i] == '\\' && i + 5 < s.len && s.buf[i + 1] == 'u') { - // \uXXXX escape. We process simple one-byte chars \u00xx within ASCII - // range. More complex chars would require dragging in a UTF8 library, - // which is too much for us - if (mg_str_to_num(mg_str_n(s.buf + i + 2, 4), 16, &to[j], - sizeof(uint8_t)) == false) - return false; - i += 5; - } else if (s.buf[i] == '\\' && i + 1 < s.len) { - char c = json_esc(s.buf[i + 1], 0); - if (c == 0) return false; - to[j] = c; - i++; - } else { - to[j] = s.buf[i]; +size_t mg_json_unescape(struct mg_str json, const char *path, char *to, + size_t n) { + struct mg_str s = mg_json_get_tok(json, path); + size_t res = 0, i, j; + if (s.len > 1 && s.buf[0] == '"') { // Is is a string? + s.len -= 2, s.buf++; // Trim surrounding double-quotes + for (i = 0, j = 0; i < s.len && j < n; i++, j++) { + if (s.buf[i] == '\\' && i + 5 < s.len && s.buf[i + 1] == 'u') { + // \uXXXX escape. We process simple one-byte chars \u00xx within ASCII + // range. More complex chars would require dragging in a UTF8 library, + // which is too much for us + if (mg_str_to_num(mg_str_n(s.buf + i + 2, 4), 16, &to[j], + sizeof(uint8_t)) == false) + break; + i += 5; + } else if (s.buf[i] == '\\' && i + 1 < s.len) { + char c = json_esc(s.buf[i + 1], 0); + if (c == 0) return false; + to[j] = c; + i++; + } else { + to[j] = s.buf[i]; + } } + if (j < n) res = j; + if (n > 0) to[j < n ? j : n - 1] = '\0'; } - if (j >= n) return false; - if (n > 0) to[j] = '\0'; - return true; + return res; } char *mg_json_get_str(struct mg_str json, const char *path) { char *result = NULL; int len = 0, off = mg_json_get(json, path, &len); - if (off >= 0 && len > 1 && json.buf[off] == '"') { + if (off >= 0 && len > 2 && json.buf[off] == '"') { if ((result = (char *) mg_calloc(1, (size_t) len)) != NULL && - !mg_json_unescape(mg_str_n(json.buf + off + 1, (size_t) (len - 2)), - result, (size_t) len)) { + mg_json_unescape(json, path, result, (size_t) len) == 0) { mg_free(result); result = NULL; } diff --git a/mongoose.h b/mongoose.h index 9f8baf33..2994c2ce 100644 --- a/mongoose.h +++ b/mongoose.h @@ -3099,8 +3099,8 @@ long mg_json_get_long(struct mg_str json, const char *path, long dflt); char *mg_json_get_str(struct mg_str json, const char *path); char *mg_json_get_hex(struct mg_str json, const char *path, int *len); char *mg_json_get_b64(struct mg_str json, const char *path, int *len); +size_t mg_json_unescape(struct mg_str json, const char *path, char *, size_t); -bool mg_json_unescape(struct mg_str str, char *buf, size_t len); size_t mg_json_next(struct mg_str obj, size_t ofs, struct mg_str *key, struct mg_str *val); diff --git a/resources/dashboard.js b/resources/dashboard.js index fc9a74a1..f3c39c61 100644 --- a/resources/dashboard.js +++ b/resources/dashboard.js @@ -76,6 +76,22 @@ const parent = keys.reduce((o, k) => o?.[k], obj); return parent && delete parent[last]; }; + const eq = (a, b) => + a === b || + (a && b && typeof a === 'object' && typeof b === 'object' && + Object.keys(a).length === Object.keys(b).length && + Object.keys(a).every(k => eq(a[k], b[k]))); + const isEmpty = obj => obj && typeof obj === 'object' && Object.keys(obj).length === 0; + function prune(o1, o2, p = '') { + for (const k in p ? get(o2, p) : o2) { + const path = p ? p + '.' + k : k; + if (!has(o1, path)) continue; + const x1 = get(o1, path), x2 = get(o2, path); + if (eq(x1, x2)) del(o2, path); + if (x2 && typeof (x2) === 'object') prune(o1, o2, path); + if (isEmpty(get(o2, path))) del(o2, path); + } + } function safeEval(expr, context) { const c = Object.freeze(context); @@ -234,12 +250,7 @@ if (isMock) { return delay(750, true).then(val => console.log('response', name)); } else { - return rpc.call(name, args) - .then(r => { - //if (storedConfig?.debug) console.log('CALL', name, args, '->', r); - return r; - }) - .catch(err => console.log('CALL FAILED', name, args, err)); + return rpc.call(name, args).catch(err => console.log('CALL FAILED', name, args, err)); } }; @@ -256,10 +267,8 @@ }; function apply_and_rescan(target, patch) { - apply(target, patch) - for (const key of Object.keys(settings.data)) { - if (settings.data[key] == settings.edits[key]) delete settings.edits[key]; - } + apply(target, patch); + prune(settings.data, settings.edits); rescan(); }; diff --git a/src/json.c b/src/json.c index 5a9f523a..017ee127 100644 --- a/src/json.c +++ b/src/json.c @@ -305,38 +305,42 @@ bool mg_json_get_bool(struct mg_str json, const char *path, bool *v) { return found; } -bool mg_json_unescape(struct mg_str s, char *to, size_t n) { - size_t i, j; - for (i = 0, j = 0; i < s.len && j < n; i++, j++) { - if (s.buf[i] == '\\' && i + 5 < s.len && s.buf[i + 1] == 'u') { - // \uXXXX escape. We process simple one-byte chars \u00xx within ASCII - // range. More complex chars would require dragging in a UTF8 library, - // which is too much for us - if (mg_str_to_num(mg_str_n(s.buf + i + 2, 4), 16, &to[j], - sizeof(uint8_t)) == false) - return false; - i += 5; - } else if (s.buf[i] == '\\' && i + 1 < s.len) { - char c = json_esc(s.buf[i + 1], 0); - if (c == 0) return false; - to[j] = c; - i++; - } else { - to[j] = s.buf[i]; +size_t mg_json_unescape(struct mg_str json, const char *path, char *to, + size_t n) { + struct mg_str s = mg_json_get_tok(json, path); + size_t res = 0, i, j; + if (s.len > 1 && s.buf[0] == '"') { // Is is a string? + s.len -= 2, s.buf++; // Trim surrounding double-quotes + for (i = 0, j = 0; i < s.len && j < n; i++, j++) { + if (s.buf[i] == '\\' && i + 5 < s.len && s.buf[i + 1] == 'u') { + // \uXXXX escape. We process simple one-byte chars \u00xx within ASCII + // range. More complex chars would require dragging in a UTF8 library, + // which is too much for us + if (mg_str_to_num(mg_str_n(s.buf + i + 2, 4), 16, &to[j], + sizeof(uint8_t)) == false) + break; + i += 5; + } else if (s.buf[i] == '\\' && i + 1 < s.len) { + char c = json_esc(s.buf[i + 1], 0); + if (c == 0) return false; + to[j] = c; + i++; + } else { + to[j] = s.buf[i]; + } } + if (j < n) res = j; + if (n > 0) to[j < n ? j : n - 1] = '\0'; } - if (j >= n) return false; - if (n > 0) to[j] = '\0'; - return true; + return res; } char *mg_json_get_str(struct mg_str json, const char *path) { char *result = NULL; int len = 0, off = mg_json_get(json, path, &len); - if (off >= 0 && len > 1 && json.buf[off] == '"') { + if (off >= 0 && len > 2 && json.buf[off] == '"') { if ((result = (char *) mg_calloc(1, (size_t) len)) != NULL && - !mg_json_unescape(mg_str_n(json.buf + off + 1, (size_t) (len - 2)), - result, (size_t) len)) { + mg_json_unescape(json, path, result, (size_t) len) == 0) { mg_free(result); result = NULL; } diff --git a/src/json.h b/src/json.h index 5cd82f88..68822a1d 100644 --- a/src/json.h +++ b/src/json.h @@ -18,7 +18,7 @@ long mg_json_get_long(struct mg_str json, const char *path, long dflt); char *mg_json_get_str(struct mg_str json, const char *path); char *mg_json_get_hex(struct mg_str json, const char *path, int *len); char *mg_json_get_b64(struct mg_str json, const char *path, int *len); +size_t mg_json_unescape(struct mg_str json, const char *path, char *, size_t); -bool mg_json_unescape(struct mg_str str, char *buf, size_t len); size_t mg_json_next(struct mg_str obj, size_t ofs, struct mg_str *key, struct mg_str *val); diff --git a/test/unit_test.c b/test/unit_test.c index bdbeea28..2983bd1e 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -3517,14 +3517,21 @@ static void test_json(void) { { char to[4], expect[4] = {0, 0, 0, 0}; memset(to, 0, sizeof(to)); - ASSERT(mg_json_unescape(mg_str("\\u0000"), to, 4) && + ASSERT(mg_json_unescape(mg_str("\"\\u0000\""), "$", to, 4) && memcmp(to, expect, 4) == 0); to[0] = 0; expect[0] = (char) 0xff; - ASSERT(mg_json_unescape(mg_str("\\u00ff"), to, 4) && + ASSERT(mg_json_unescape(mg_str("\"\\u00ff\""), "$", to, 4) && memcmp(to, expect, 4) == 0); - ASSERT(!mg_json_unescape(mg_str("\\u0100"), to, 4)); - ASSERT(!mg_json_unescape(mg_str("\\u1000"), to, 4)); + ASSERT(!mg_json_unescape(mg_str("\"\\u0100\""), "$", to, 4)); + ASSERT(!mg_json_unescape(mg_str("\"\\u1000\""), "$", to, 4)); + json = mg_str("{\"a\":\"\"}"); + mg_snprintf(to, sizeof(to), "hi"); + ASSERT(mg_json_unescape(json, "$.a", to, sizeof(to)) == 0); + ASSERT(to[0] == '\0'); + json = mg_str("{\"a\":\"b\"}"); + ASSERT(mg_json_unescape(json, "$.a", to, sizeof(to)) == 1); + ASSERT(strcmp(to, "b") == 0); } { @@ -4548,7 +4555,7 @@ int main(void) { s_error = false; test_mqtt(); // sorry, MQTT_LOCALHOST is also skipped DASHBOARD("mqtt"); -#else +#else (void) test_mqtt; #endif diff --git a/tutorials/device-dashboard/full-dashboard/server.c b/tutorials/device-dashboard/full-dashboard/server.c index fb7ca96c..eafa9d1b 100644 --- a/tutorials/device-dashboard/full-dashboard/server.c +++ b/tutorials/device-dashboard/full-dashboard/server.c @@ -59,7 +59,6 @@ static void get_metrics(union mg_val *val) { val->fn = (void (*)(void)) print_metrics; } -static bool s_motor = false; static double s_volume = 17.2; static char s_name[20] = "Dublin"; // This function prints a JSON object with read-only metrics @@ -68,11 +67,14 @@ size_t print_settings(void (*fn)(char, void *), void *arg, va_list *ap) { (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:%s", ",", MG_ESC("motor"), s_motor ? "true" : "false"); 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; } @@ -100,7 +102,7 @@ static void get_controls(union mg_val *val) { // 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, NULL}, + {"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}, diff --git a/tutorials/device-dashboard/full-dashboard/web_root/index.html b/tutorials/device-dashboard/full-dashboard/web_root/index.html index 8ab591d3..81ac6a18 100644 --- a/tutorials/device-dashboard/full-dashboard/web_root/index.html +++ b/tutorials/device-dashboard/full-dashboard/web_root/index.html @@ -205,7 +205,7 @@
- +
diff --git a/tutorials/device-dashboard/full-dashboard/web_root/main.js b/tutorials/device-dashboard/full-dashboard/web_root/main.js index 47901587..d9a9473e 100644 --- a/tutorials/device-dashboard/full-dashboard/web_root/main.js +++ b/tutorials/device-dashboard/full-dashboard/web_root/main.js @@ -2,6 +2,8 @@ document.addEventListener("DOMContentLoaded", () => { const tabs = Array.from(document.querySelectorAll(".header-left .nav-link[href^='#']")); const panels = Array.from(document.querySelectorAll(".tab-panel")); + let chart = null; + const activateTab = targetId => { const targetPanel = document.getElementById(targetId); if (!targetPanel) return; @@ -18,6 +20,19 @@ document.addEventListener("DOMContentLoaded", () => { panel.classList.toggle("tab-panel-active", isActive); panel.hidden = !isActive; }); + + requestAnimationFrame(() => { + const rect = el.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) rect.height = rect.width = 10; + + if (!chart) { + options.width = rect.width; + options.height = rect.height; + chart = new uPlot(options, [], el); + } else { + chart.setSize({ width: rect.width, height: rect.height }); + } + }); }; tabs.forEach(tab => { @@ -34,11 +49,10 @@ document.addEventListener("DOMContentLoaded", () => { const el = document.getElementById("chart"); - const rect = el.getBoundingClientRect(); const options = { - width: rect.width, - height: rect.height, + width: 100, + height: 100, tzDate: (ts) => uPlot.tzDate(new Date(ts * 1e3), "Europe/London"), plugins: [], legend: { show: false }, @@ -53,17 +67,13 @@ document.addEventListener("DOMContentLoaded", () => { ], }; - // Create chart instance - const chart = new uPlot(options, [], el); - function updateGraphPoints(args) { - if (!args.points) return; + 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]), ]; - //console.log(123, args.points); chart.setData(data); }; diff --git a/tutorials/device-dashboard/full-dashboard/web_root/style.css b/tutorials/device-dashboard/full-dashboard/web_root/style.css index eedf8a9a..70f022e4 100644 --- a/tutorials/device-dashboard/full-dashboard/web_root/style.css +++ b/tutorials/device-dashboard/full-dashboard/web_root/style.css @@ -114,6 +114,7 @@ body { transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); gap: var(--spacing-2); text-decoration: none; + white-space: nowrap; } .nav-link-active {