Dash OTA dogfooding nits

This commit is contained in:
cpq committed 2026-06-09 11:56:40 +01:00
1 parent 73037880e6
commit 27459efc61
29 files changed
+2635 -1478

No files matched your search

+35 -30
View File
@@ -4,6 +4,14 @@ Mongoose is a two-file C/C++ networking library for embedded and desktop systems
It is developed by Cesanta (https://cesanta.com) and available at https://mongoose.ws.
License: GPLv2 or commercial.
## General rules
- Always re-read files before patching, so you do not overwrite existing changes
- Never guess. If you don't know, say you don't know and stop
- Read mongoose.h for API definition and docstrings
- Do not use a separate HTTP library, MQTT library, or WebSocket library,
or Modbus-TCP library alongside Mongoose. Mongoose provides all of these.
## Integration
Copy `mongoose.h` and `mongoose.c` into a `mongoose/` subdirectory of the
@@ -38,16 +46,12 @@ your_project/
└── mongoose_config.h # required for embedded: set MG_ARCH and options
```
Minimal `mongoose_config.h` for STM32:
Minimal `mongoose_config.h` should set `MG_ARCH`. For exmaple, for STM32:
```c
#define MG_ARCH MG_ARCH_CUBE
// add driver, TLS, and other options here
```
Do not use a separate HTTP library, MQTT library, or WebSocket library alongside
Mongoose. Mongoose provides all of these.
## Core API
```c
@@ -80,7 +84,7 @@ void handler(struct mg_connection *c, int ev, void *ev_data) {
## TCP/IP stack — set exactly one
Configure in the build system (`-D` flag) or in `mongoose_config.h`:
Configure in `mongoose_config.h`:
| Define | Use when |
|--------|----------|
@@ -216,7 +220,11 @@ for (;;) {
- Do **not** add vanilla JS event listeners, `fetch()` calls, or custom
reactive logic to `dashboard.html`.
- Do **not** modify `dashboard.html` unless the user explicitly asks.
- Bind controls to device state using `data-bind` attributes:
- Bind controls to device state using `data-bind` attributes
- The `__status` object in evaluations is read-only, do not alter it
- If you need to pass data between the UI and backend.c, add extra
fieldsets/fields - see next section about it
- If you want to display device data in HTML, use `${}` evaluations
```html
<!-- checkbox bound to a device field, auto-saves on change -->
@@ -322,35 +330,20 @@ static struct mg_field fields_leds[] = {
};
```
In order to simulate array of elements, use an integer field as an index.
Reader first writes an index of the element to read, and then it reads
an element.
Array field sets are recognised by the "index" pointer in the
field set descriptor:
// Non-NULL: array set. *index is set before fn(READ) call;
// fn sets *index = -1 to signal end of iteration.
// Size query: framework sets *index = -1 before fn(READ); fn sets *index = total size
```c
struct event {
static struct event {
int index;
char message[100];
};
static struct event s_event;
static void read_event(void) {
mg_snprintf(s_event.message, sizeof(s_event.message), "my ev %d",
s_event.index);
}
static void write_event(void) {
// Do nothing. Dashboard sets the s_event.index
}
static struct mg_field fields_event[] = {
{"index", MG_VAL_INT, &s_event.index, sizeof(s_event.index)},
{"message", MG_VAL_STR, &s_event.message, sizeof(s_event.message)},
{NULL, MG_VAL_INT, NULL, 0},
};
} s_event;
static struct mg_field_set field_set_event = {
"event", fields_event, read_event, write_event, 0, 0, NULL,
"event", fields_event, event_fn, &s_event.index, 0, 0, 0
};
```
@@ -398,6 +391,18 @@ MG_ERROR(("errno=%d", err));
Set `MG_ENABLE_LOG=0` to disable all logging (production / size-sensitive builds).
## Firmware OTA
Mongoose implements OTA for a variety of targets, see MG_OTA_ defines. To
enable, set the respective define in `mongoose_config.h`, for example:
```c
#define MG_OTA MG_OTA_STM32H5
```
Once this is done, you do OTA via HTTP upload, or HTTP periodic pull, or over
MQTT. Read https://mongoose.ws/docs/guides/firmware-ota-updates/ for details.
## Key rules for AI code generation
- **Two files only**: always use `mongoose.h` + `mongoose.c`. Never add a
+644 -355
View File
File diff suppressed because it is too large. Load diff
+151 -49
View File
@@ -1897,6 +1897,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm,
void (*fn)(struct mg_connection *, const char *));
void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm,
void (*fn)(struct mg_connection *, const char *));
const char *mg_upload_path(struct mg_connection *c);
void mg_http_bauth(struct mg_connection *, const char *user, const char *pass);
struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v);
size_t mg_http_next_multipart(struct mg_str, size_t, struct mg_http_part *);
@@ -3277,50 +3278,95 @@ void mg_rpc_list(struct mg_rpc_req *r);
#ifndef MG_DASH_MAX_USER_NAME
#define MG_DASH_MAX_USER_NAME 32
#endif
#ifndef MG_HTTP_ADDR
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
#define MG_HTTP_ADDR "http://0.0.0.0:8000"
#define MG_HTTPS_ADDR "https://0.0.0.0:8443"
#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502"
#else
#define MG_HTTP_ADDR "http://0.0.0.0:80"
#define MG_HTTPS_ADDR "http://0.0.0.0:443"
#define MG_MODBUS_ADDR "tcp://0.0.0.0:502"
#endif
#endif
#ifndef MG_DASH_MAX_SEND_BUF_SIZE
#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5)
#endif
// C type of a value bound to a dashboard field, see struct mg_field
enum mg_val_type {
MG_VAL_INT,
MG_VAL_BOOL,
MG_VAL_DBL,
MG_VAL_STR,
MG_VAL_RAW,
MG_VAL_INT, // int
MG_VAL_UINT64, // uint64_t
MG_VAL_BOOL, // bool
MG_VAL_DBL, // double
MG_VAL_STR, // Zero-terminated string
MG_VAL_RAW, // Pre-formatted JSON value, copied verbatim
};
// Dashboard field: binds a JSON key to a C variable for read and/or write
struct mg_field {
const char *name;
enum mg_val_type type;
void *value;
size_t value_size;
const char *name; // JSON key
enum mg_val_type type; // C type of the value, see enum mg_val_type
void *value; // Pointer to the bound variable
size_t value_size; // sizeof(*value) for type checking; 0 makes it read-only
};
// Operation passed to a struct mg_field_set read/write/delete/add callback.
// MG_DASH_ADD: fields hold the values of a new element to append; *index is
// not set. Return true to accept and persist it, false to reject (e.g. a cap
// was reached)
enum mg_dash_op { MG_DASH_READ, MG_DASH_WRITE, MG_DASH_DELETE, MG_DASH_ADD };
struct mg_dash;
// Authenticated (or guest) dashboard user
struct mg_dash_user {
struct mg_dash_user *next; // Next user in the list
char name[MG_DASH_MAX_USER_NAME]; // Username
char token[21]; // Session token, sent to the client as a cookie
int level; // Access level, see struct mg_dash::authenticate
uint64_t expire; // Session expiration time, see mg_millis()
struct mg_dash *dash; // Dashboard this user belongs to
};
// Named group of related dashboard fields, e.g. "settings" or "metrics"
struct mg_field_set {
const char *name;
struct mg_field *fields;
void (*reader)(void);
void (*writer)(void);
int read_level;
int write_level;
struct mg_field_set *next;
const char *name; // Set name, used as a JSON key
struct mg_field *fields; // Zero-terminated array of fields
bool (*fn)(enum mg_dash_op op,
struct mg_dash_user *u); // Read/write/delete/add callback
int *index; // Non-NULL: array set. *index is set before fn(READ) call;
// fn sets *index = -1 to signal end of iteration.
// Size query: framework sets *index = -1 before fn(READ); fn
// sets *index = total size
bool (*get_dir)(const struct mg_dash_user *u, char *buf,
size_t len); // Non-NULL: file array
struct mg_field_set *next; // Next set in the list
};
// Custom URI handler, registered with MG_DASH_REGISTER_CUSTOM_HANDLER()
struct mg_dash_custom_handler {
struct mg_dash_custom_handler *next;
struct mg_str uri_pattern;
mg_event_handler_t handler;
void *handler_data;
};
struct mg_dash_file {
struct mg_dash_file *next;
char *name;
size_t size;
struct mg_dash_custom_handler *next; // Next handler in the list
struct mg_str uri_pattern; // URI to match, see mg_match()
mg_event_handler_t handler; // Event handler function
void *handler_data; // Opaque data for the handler
};
// Dashboard instance: fields, users and handlers served over HTTP and WS
struct mg_dash {
struct mg_field_set *sets;
struct mg_dash_custom_handler *custom_handlers;
struct mg_field_set *sets; // Field sets, see MG_DASH_ADD_FIELD_SET()
struct mg_dash_custom_handler *custom_handlers; // Custom URI handlers
// Validates pass, fills in user, returns access level (<= 0: failure).
// NULL: no login required, all clients get guest access
int (*authenticate)(char *user, size_t userlen, const char *pass);
int session_auto_expiration_seconds;
//struct mg_dash_file *files;
int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default
struct mg_dash_user *guest; // Used when authenticate == NULL
struct mg_fs *upload_fs; // FS for uploads/listing, default &mg_fs_posix
};
#define MG_DASH_ADD_FIELD_SET(dash_, set_) \
@@ -3339,28 +3385,50 @@ struct mg_dash {
(dash_)->custom_handlers = &ch_; \
} while (0)
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
#define MG_HTTP_ADDR "http://0.0.0.0:8000"
#define MG_HTTPS_ADDR "https://0.0.0.0:8443"
#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502"
#else
#define MG_HTTP_ADDR "http://0.0.0.0:80"
#define MG_HTTPS_ADDR "http://0.0.0.0:443"
#define MG_MODBUS_ADDR "tcp://0.0.0.0:502"
#endif
#ifndef MG_DASH_MAX_SEND_BUF_SIZE
#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5)
#endif
// File manager
extern struct mg_dash_file *mg_dash_files;
void mg_dash_file_add(struct mg_str name, size_t size);
void mg_dash_file_del(struct mg_str name);
// Dashboard event handler. Pass to mg_http_listen() as the fn argument
void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data);
// Notify WebSocket clients that a field set changed: broadcasts the new
// array size if *set->index < 0, or a single element update otherwise
void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *);
// Default reader for file-backed array sets (set->get_dir != NULL)
// Usage example - this is a complete implemenation of the file manager:
// ```c
// static struct file {
// int index;
// char name[64];
// size_t size;
// uint64_t checksum;
// } s_file;
// static struct mg_field_set set_files;
//
// static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) {
// (void) u;
// mkdir("/tmp/dashboard", 0755);
// mg_snprintf(buf, len, "%s", "/tmp/dashboard");
// return true;
// }
//
// static struct mg_field fields_files[] = {
// {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)},
// {"size", MG_VAL_UINT64, &s_file.size, 0},
// {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)},
// {NULL, MG_VAL_INT, NULL, 0},
// };
//
// static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) {
// if (op == MG_DASH_WRITE) return s_uploads_enabled && u->level >= 7;
// if (op != MG_DASH_READ) return false;
// if (!mg_dash_dir_read(&set_files, u)) return false;
// s_file.checksum = 0; // Mock to show how to set custom fields
// return true;
// }
//
// static struct mg_field_set set_files = {"files", fields_files, files_fn, &s_file.index, get_dir, NULL};
// ```
bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u);
// Helper forward declarations for Mongoose CMSIS pack modules
extern struct mg_mgr g_mgr;
extern void mg_dash_init(struct mg_mgr *);
@@ -3470,6 +3538,40 @@ enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 };
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
const char *metadata_url, void (*fn)(const char *status));
#ifndef MG_OTA_URL
#define MG_OTA_URL NULL
#endif
#ifndef MG_OTA_STATUS_FN
#define MG_OTA_STATUS_FN NULL
#endif
#ifndef MG_OTA_FIRMWARE_VERSION
#define MG_OTA_FIRMWARE_VERSION "1.0.0"
#endif
#ifndef MG_OTA_MAX_VERSION_LEN
#define MG_OTA_MAX_VERSION_LEN 64
#endif
// Scannable "MG_VERSION:<MG_OTA_FIRMWARE_VERSION>" tag embedded in every
// firmware binary, for server-side extraction. mg_mgr_init() stashes its
// address in mgr->userdata, which is what keeps it from being stripped by
// -Wl,--gc-sections on builds that never poll for OTAs
extern const char mg_fw_version[];
#ifndef MG_OTA_PULL_INTERVAL_SECONDS
#define MG_OTA_PULL_INTERVAL_SECONDS 60
#endif
#ifndef MG_ENABLE_CUSTOM_DEVICE_ID
#define MG_ENABLE_CUSTOM_DEVICE_ID 0
#endif
void mg_ota_device_id(char *buf, size_t len);
void mg_ota_poll(struct mg_mgr *);
+201 -82
View File
@@ -3,19 +3,13 @@
(function (global) {
"use strict";
const Rpc = (function() {
function Rpc(url, onev) {
let ws, id = 1, oncall, q = [], reconnect = true;
const pending = new Map();
const realsend = s => { ws.send(s); onev && onev('send', s); };
const fail = e => { for (const p of pending.values()) p[1](e || 'disconnected'); pending.clear(); };
const send = x => ws && ws.readyState === 1
? realsend(JSON.stringify(x))
: q.push(x);
const Watch = (function() {
function Watch(url, onev, onmsg) {
let ws, reconnect = true;
const connect = () => {
ws = new WebSocket(url);
ws.onopen = () => { onev && onev('open'); while (q.length) realsend(JSON.stringify(q.shift())); };
ws.onclose = () => { onev && onev('close'); fail(); reconnect && setTimeout(connect, 1000); };
ws.onopen = () => onev && onev('open');
ws.onclose = () => { onev && onev('close'); reconnect && setTimeout(connect, 1000); };
ws.onmessage = e => {
onev && onev('message', e.data);
let m;
@@ -25,69 +19,92 @@
console.log('INVALID JSON:', e.data);
return;
}
if (m.id && pending.has(m.id)) {
const [ok, bad] = pending.get(m.id);
pending.delete(m.id);
m.error ? bad(m.error) : ok(m.result);
} else if (oncall && m.method) {
Promise.resolve(oncall(m.method, m.params))
.then(r => m.id && send({ id: m.id, result: r }))
.catch(err => m.id && send({
id: m.id,
error: { code: -32000, message: String(err?.message || err) }
}));
}
onmsg && onmsg(m);
};
};
connect();
return {
close: () => { reconnect = false; ws?.close(); },
call: (method, params) => new Promise((ok, bad) => {
const i = id++;
pending.set(i, [ok, bad]);
send({ id: i, method, params });
}),
notify: (method, params) => send({ method, params }),
handle: fn => (oncall = fn, this),
get ws() { return ws; }
};
}
return Rpc;
return Watch;
})();
function rpc_connect() {
function fetch_and_ready() {
if (isMock) {
setTimeout(() => userhandlers['ready']?.(), 500);
return;
return delay(500).then(rescan).then(mark_ready);
}
if (rpc) return rpc;
//if (settings.auth && !status.authed) return null;
rpc = Rpc('api/websocket', function (evname, args) {
if (evname == 'open') status.online = true, rescan();
if (evname == 'close') status.online = false, rescan();
if (settings.debug) console.log('WS', evname, args);
});
rpc.handle((method, args) => {
if (method == 'change' && args) apply_and_rescan(settings.data, args);
if (method == 'logout') rpc_disconnect();
if (userhandlers[method]) userhandlers[method](args);
});
return rpc;
return fetch('api/get')
.then(r => r.ok ? r.json() : Promise.reject(r.status))
.then(data => {
for (const k in data) {
if (typeof data[k] === 'number') {
const prev = settings.data[k];
const arr = prev && typeof prev === 'object' ? prev : {};
data[k] = {
...arr,
size: data[k],
start: typeof arr.start === 'number' ? arr.start : 0,
data: Array.isArray(arr.data) ? arr.data : [],
};
}
}
return apply_and_rescan(settings.data, data);
})
.then(expand_arrays)
.then(() => {
ws_connect();
return mark_ready();
})
.catch(err => console.log('GET failed', err));
}
function rpc_disconnect() {
if (!rpc) return;
rpc.close();
rpc = null;
function ws_connect() {
if (isMock || ws) return;
ws = Watch('api/websocket', function (evname, args) {
if (evname == 'open') { status.online = true; rescan(); }
if (evname == 'close') { status.online = false; rescan(); }
if (settings.debug) console.log('WS', evname, args);
}, function (m) {
if (m.method == 'change' && m.params) {
const rest = {};
const arrays = [];
for (const k in m.params) {
const ma = k.match(/^(.+)\/(\d+)$/);
if (ma) {
const arr = settings.data[ma[1]];
if (arr?.data) { const i = +ma[2] - (arr.start || 0); if (i >= 0 && i < arr.data.length) arr.data[i] = Object.assign(arr.data[i] || {}, m.params[k]); }
} else if (typeof m.params[k] === 'number' && settings.data[k]?.data !== undefined) {
const arr = settings.data[k];
arr.size = m.params[k];
if (is_expandable(arr)) arrays.push(expand_array(k, arr.start, arr.end));
} else {
rest[k] = m.params[k];
}
}
apply(settings.data, rest);
prune(settings.data, settings.edits);
Promise.all(arrays)
.then(rescan)
.then(() => userhandlers['change']?.(m.params));
}
if (m.method == 'logout') ws_disconnect();
});
}
function ws_disconnect() {
if (!ws) return;
ws.close();
ws = null;
status.online = false;
}
function login(params) {
if (isMock) { rpc_connect(); return Promise.resolve({}); }
if (isMock) return fetch_and_ready().then(() => ({}));
const opts = {};
if (params) {
const username = (params?.username ?? '').trim();
@@ -100,19 +117,18 @@
.then(resp => {
status.username = resp?.user || '';
status.userlevel = resp?.level || 0;
rpc_connect();
return resp;
return fetch_and_ready().then(() => resp);
});
};
function logout() {
if (isMock) { rpc_disconnect(); return Promise.resolve({}); }
if (isMock) { ws_disconnect(); return Promise.resolve({}); }
return fetch('api/logout')
.catch(() => true)
.then(() => {
status.username = '';
status.userlevel = 0;
rpc_disconnect();
ws_disconnect();
return true;
});
}
@@ -122,7 +138,27 @@
const settings = {data: {}, edits: {}, debug: true};
const userhandlers = {}; // User event handlers
const status = { online: false, username: '', userlevel: 0, ready: false };
let rpc = null;
let ws = null;
let ready_started = false;
function install_ready_gate() {
document.documentElement.classList.remove('dashboard-ready');
if (document.getElementById('dashboard-ready-gate')) return;
const style = document.createElement('style');
style.id = 'dashboard-ready-gate';
style.textContent = `html:not(.dashboard-ready) *,html:not(.dashboard-ready) *::before,html:not(.dashboard-ready) *::after{transition:none!important;animation:none!important}`;
document.head.appendChild(style);
}
function mark_ready() {
if (ready_started) return Promise.resolve();
ready_started = true;
return next_frame().then(() => {
status.ready = true;
document.documentElement.classList.add('dashboard-ready');
userhandlers['ready']?.();
});
}
const fromPath = (path, value) => path.split('.').reverse().reduce((acc, k) => ({ [k]: acc }), value);
const get = (obj, path) => path.split('.').reduce((o, k) => o?.[k], obj);
@@ -222,7 +258,11 @@
function save(key) {
const changes = {};
set(changes, key, get(settings.edits, key));
rpc.call('set', changes).then(r => apply_and_rescan(settings.data, r));
fetch('api/set', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(changes),
}).catch(err => console.log('SET failed', key, err));
};
// Cancel the edits for keys that match pattern
@@ -231,6 +271,14 @@
rescan();
};
// Set a checkbox's `checked` *property* from a boolean expression. The
// `checked` content attribute is presence-based - templating its value
// would not work, so this binds straight to the IDL property instead
function handle_checked(el, key, context) {
if (!has(context, key)) { console.error('EVAL', key, context); return; }
el.checked = !!get(context, key);
};
function handle_save(el, key) {
el.disabled = !edited(key);
if (!el.bound) el.addEventListener("click", ev => save(key)), el.bound = true;
@@ -244,18 +292,30 @@
function handle_repeat(el, key, context) {
if (!has(context, key)) { console.error('EVAL', key, context); return; }
if (!el.orig) el.orig = el.children[0].cloneNode(true);
const v = get(context, key), v2 = JSON.stringify(v);
if (el.v2 === v2) return;
const frag = document.createDocumentFragment();
const v = get(context, key);
const it = el.dataset.iterator ?? '__obj';
v.forEach(function (item, index) {
const child = el.orig.cloneNode(true);
const ctx = { ...context, [it]: item };
process(child, ctx, true);
frag.appendChild(child);
});
el.replaceChildren(frag);
el.v2 = v2;
const json = v.map(item => JSON.stringify(item));
const prev = el.itemsJson || [];
if (json.length === prev.length && el.children.length === json.length) {
// Same length: re-template each changed row's existing DOM node in
// place instead of replacing it. Untouched rows are left completely
// alone, and changed rows keep their live elements too - so bound
// properties (e.g. a checkbox's `checked`) change on an
// already-painted node, which is what lets CSS transitions play, and
// in-progress edits in unrelated fields of that row survive
json.forEach((j, i) => {
if (j !== prev[i]) process(el.children[i], { ...context, [it]: v[i] }, true);
});
} else {
const frag = document.createDocumentFragment();
v.forEach(item => {
const node = el.orig.cloneNode(true);
process(node, { ...context, [it]: item }, true);
frag.appendChild(node);
});
el.replaceChildren(frag);
}
el.itemsJson = json;
};
function upload(el, is_ota) {
@@ -267,7 +327,7 @@
const reader = new FileReader();
reader.readAsArrayBuffer(f);
reader.onload = function () {
const url = is_ota ? 'api/ota' : `fs/${encodeURIComponent(f.name)}`;
const url = is_ota ? 'api/ota' : `fs/${el.dataset.upload}/${encodeURIComponent(f.name)}`;
let begin = Date.now(), body = reader.result, ok = false;
// console.log(2, url, body);
fetch(url, { method: 'POST', body })
@@ -295,7 +355,8 @@
}
function process(root, context, x) {
const all = root.querySelectorAll("*:not([data-repeat] *)");
const selector = x ? "*" : "*:not([data-repeat] *)";
const all = root.querySelectorAll(selector);
const els = root.nodeType === Node.DOCUMENT_NODE ? all : [root, ...all];
els.forEach(function (el) {
if (el.tagName === "SCRIPT" || el.tagName === "STYLE") return;
@@ -309,14 +370,16 @@
}
for (const attr of el.attributes) {
const cv = attr.value || '';
if (attr.originalValue || cv.includes('${')) {
if (!attr.originalValue) attr.originalValue = cv;
const v = substituteExpressions(attr.originalValue, context);
if (!el.originalAttrs) el.originalAttrs = {};
if (el.originalAttrs[attr.name] || cv.includes('${')) {
if (!el.originalAttrs[attr.name]) el.originalAttrs[attr.name] = cv;
const v = substituteExpressions(el.originalAttrs[attr.name], context);
if (v !== attr.value) attr.value = v;
}
}
const handlers = {
bind: handle_bind,
checked: handle_checked,
save: handle_save,
cancel: handle_cancel,
repeat: handle_repeat,
@@ -341,21 +404,75 @@
function rescan() {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", realrescan, { once: true });
return new Promise(resolve => {
document.addEventListener("DOMContentLoaded", () => {
realrescan();
resolve();
}, { once: true });
});
} else {
realrescan();
return Promise.resolve();
}
};
const delay = (ms, value) => new Promise(r => setTimeout(() => r(value), ms));
const next_frame = () => new Promise(r => requestAnimationFrame(() => r()));
const is_expandable = arr => arr && Array.isArray(arr.data) &&
typeof arr.start === 'number' && typeof arr.end === 'number';
function expand_array(name, start, end) {
const arr = settings.data[name];
const page = 100, all = [];
const stop = Math.min(end, typeof arr?.size === 'number' ? arr.size - 1 : end);
const done = () => {
set(settings.data, `${name}.data`, all);
set(settings.data, `${name}.start`, start);
return rescan().then(() => all);
};
const next = from => {
if (from > stop) return Promise.resolve(done());
const to = Math.min(from + page - 1, stop);
return fetch(`api/get/${name}/${from}/${to}`)
.then(r => r.ok ? r.json() : Promise.reject(r.statusText))
.then(data => {
if (!data || !data.length) return done();
all.push(...data);
return next(from + data.length);
});
};
return next(start);
}
function expand_arrays() {
const all = [];
for (const name in settings.data) {
const arr = settings.data[name];
if (is_expandable(arr)) all.push(expand_array(name, arr.start, arr.end));
}
return Promise.all(all);
}
function call(name, args) {
if (isMock) {
return delay(750, true).then(val => console.log('response', name));
} else {
return rpc.call(name, args)
.catch(err => console.log('CALL FAILED', name, args, err));
return delay(750, true).then(() => console.log('response', name));
}
if (name === 'set') {
return fetch('api/set', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(args),
}).catch(err => console.log('CALL FAILED', name, args, err));
}
if (args && typeof args === 'object') {
const {name: aname, start: s = 0, size: total = 100, end} = args;
const stop = typeof end === 'number' ? end : s + total - 1;
return expand_array(aname, s, stop).catch(err => console.log('CALL FAILED', name, args, err));
}
const path = typeof args === 'string' && args ? '/' + args : '';
return fetch('api/get' + path)
.then(r => r.ok ? r.json() : Promise.reject(r.statusText))
.catch(err => console.log('CALL FAILED', name, args, err));
};
function apply(target, patch) {
@@ -373,16 +490,18 @@
function apply_and_rescan(target, patch) {
apply(target, patch);
prune(settings.data, settings.edits);
rescan();
return rescan();
};
function on(name, fn) {
userhandlers[name] = fn;
if (name === 'ready' && status.ready) setTimeout(fn, 0);
};
function init(conf) {
install_ready_gate();
apply_and_rescan(settings, conf);
if (!settings.auth) rpc_connect();
if (!settings.auth) fetch_and_ready();
};
global.Dashboard = { init, call, on, status, login, logout };
+1 -1
View File
@@ -62,7 +62,7 @@ $(foreach T,$(RP_TARGETS), $(foreach P,$(PROJECTS), $(eval $(call RP_DASHBOARD_R
%.zip: %
zip -qr $@ $<
upload: $(ZIPS)
deploy: $(ZIPS)
scp $(ZIPS) h1:/data/downloads/
clean:
+52 -35
View File
@@ -5,7 +5,7 @@
// inlining referenced CSS, JS, images, and icons.
//
// Usage:
// node html2c.js [-o OUTPUT.c] HTML_FILE
// node html2c.js [-o OUTPUT.c] HTML_FILE [HTML_FILE ...]
const fs = require('fs');
const path = require('path');
@@ -15,24 +15,25 @@ const zlib = require('zlib');
const args = process.argv.slice(2);
let outputFile = null;
let inputFile = null;
const inputFiles = [];
const useMap = new Map();
for (let i = 0; i < args.length; i++) {
if (args[i] === '-o' && i + 1 < args.length) {
outputFile = args[++i];
} else if (!args[i].startsWith('-') && inputFile === null) {
inputFile = args[i];
} else if (args[i] === '--use' && i + 1 < args.length) {
const eq = args[++i].indexOf('=');
if (eq > 0) useMap.set(args[i].slice(0, eq), args[i].slice(eq + 1));
} else if (!args[i].startsWith('-')) {
inputFiles.push(args[i]);
}
}
if (!inputFile) {
console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE');
if (inputFiles.length === 0) {
console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE [HTML_FILE ...]');
process.exit(1);
}
const baseDir = path.dirname(path.resolve(inputFile));
const html = fs.readFileSync(inputFile, 'utf8');
function fetchUrl(url) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
@@ -47,12 +48,15 @@ function fetchUrl(url) {
});
}
async function readAsset(name, encoding) {
async function readAsset(name, encoding, baseDir) {
const override = useMap.get(name);
if (override != null) name = override;
if (name.startsWith('http://') || name.startsWith('https://')) {
const data = await fetchUrl(name);
return encoding ? data.toString(encoding) : data;
}
return fs.readFileSync(path.join(baseDir, name), encoding || null);
const fullPath = path.isAbsolute(name) ? name : path.join(baseDir, name);
return fs.readFileSync(fullPath, encoding || null);
}
function attr(match, name) {
@@ -66,21 +70,21 @@ function replaceAttr(match, name, value) {
return match.replace(re, `${name}="${value}"`);
}
async function inlineCss(match) {
async function inlineCss(match, baseDir) {
const href = attr(match, 'href');
if (!href) return match;
const css = await readAsset(href, 'utf8');
const css = await readAsset(href, 'utf8', baseDir);
return `<style>${css}</style>`;
}
async function inlineJs(match) {
async function inlineJs(match, baseDir) {
const src = attr(match, 'src');
if (!src) return match;
const js = await readAsset(src, 'utf8');
const js = await readAsset(src, 'utf8', baseDir);
return `<script>${js}</script>`;
}
async function inlineImg(match) {
async function inlineImg(match, baseDir) {
const src = attr(match, 'src');
if (!src) return match;
const ext = path.extname(src).toLowerCase();
@@ -91,7 +95,7 @@ async function inlineImg(match) {
else if (ext === '.webp') mime = 'image/webp';
else if (ext === '.ico') mime = 'image/x-icon';
const data = await readAsset(src);
const data = await readAsset(src, null, baseDir);
if (ext === '.svg') {
const svg = Buffer.isBuffer(data) ? data.toString('utf8') : data;
return replaceAttr(
@@ -107,10 +111,10 @@ async function inlineImg(match) {
);
}
async function inlineSvg(match) {
async function inlineSvg(match, baseDir) {
const href = attr(match, 'href');
if (!href || !href.toLowerCase().endsWith('.svg')) return match;
const data = await readAsset(href);
const data = await readAsset(href, null, baseDir);
return replaceAttr(
match,
'href',
@@ -118,7 +122,7 @@ async function inlineSvg(match) {
);
}
async function processHtml(content) {
async function processHtml(content, baseDir) {
let result = content;
const cssRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/g;
const jsRegex = /<script[^>]+src=["'][^"']+["'][^>]*><\/script>/g;
@@ -126,16 +130,16 @@ async function processHtml(content) {
const svgRegex = /<link[^>]+rel=["']icon["'][^>]*>/g;
for (const match of content.match(cssRegex) || []) {
if (!match.includes('data:')) result = result.replace(match, await inlineCss(match));
if (!match.includes('data:')) result = result.replace(match, await inlineCss(match, baseDir));
}
for (const match of content.match(jsRegex) || []) {
if (!match.includes('data:')) result = result.replace(match, await inlineJs(match));
if (!match.includes('data:')) result = result.replace(match, await inlineJs(match, baseDir));
}
for (const match of content.match(imgRegex) || []) {
if (!match.includes('data:')) result = result.replace(match, await inlineImg(match));
if (!match.includes('data:')) result = result.replace(match, await inlineImg(match, baseDir));
}
for (const match of content.match(svgRegex) || []) {
if (!match.includes('data:')) result = result.replace(match, await inlineSvg(match));
if (!match.includes('data:')) result = result.replace(match, await inlineSvg(match, baseDir));
}
return result;
@@ -145,30 +149,43 @@ function cArray(data) {
return Array.from(Buffer.from(data)).concat(0).join(',');
}
function generateC(data) {
const destination = `${path.basename(inputFile)}.gz`;
const stat = fs.statSync(inputFile);
const mtime = parseInt(stat.mtimeMs / 1000);
const zipped = zlib.gzipSync(data);
const bytes = cArray(zipped);
function generateC(files) {
const vars = files.map(({inputFile, data}, i) => {
const zipped = zlib.gzipSync(data);
return {inputFile, zipped};
});
const decls = vars.map(({zipped}, i) =>
`static const unsigned char v${i}[] = {${cArray(zipped)}};`
).join('\n');
const entries = vars.map(({inputFile, zipped}, i) => {
const destination = `${path.basename(inputFile)}.gz`;
const mtime = parseInt(fs.statSync(inputFile).mtimeMs / 1000);
return ` {"/${destination}", v${i}, sizeof(v${i}) - 1, ${mtime}}, // size: ${zipped.length}`;
}).join('\n');
return `// DO NOT EDIT. This file is generated using this command:
// ${process.argv.join(' ')}
#include "mongoose.h"
static const unsigned char v0[] = {${bytes}};
${decls}
const struct mg_mem_file mg_packed_files[] = {
{"/${destination}", v0, sizeof(v0) - 1, ${mtime}}, // size: ${zipped.length}
${entries}
{NULL, NULL, 0, 0}
};
`;
}
processHtml(html)
.then(result => {
const c = generateC(result);
Promise.all(inputFiles.map(inputFile => {
const baseDir = path.dirname(path.resolve(inputFile));
const html = fs.readFileSync(inputFile, 'utf8');
return processHtml(html, baseDir).then(data => ({inputFile, data}));
}))
.then(files => {
const c = generateC(files);
if (outputFile) {
fs.writeFileSync(outputFile, c);
} else {
-157
View File
@@ -1,157 +0,0 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const args = process.argv.slice(2);
let outputFile = null;
let inputFile = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === '-o' && i + 1 < args.length) {
outputFile = args[++i];
} else if (!args[i].startsWith('-')) {
inputFile = args[i];
}
}
if (!inputFile) {
console.error('Usage: node inline.js [-o OUTPUT_FILE] HTML_FILE');
process.exit(1);
}
const baseDir = path.dirname(path.resolve(inputFile));
const html = fs.readFileSync(inputFile, 'utf8');
function fetchUrl(url) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
client.get(url, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
res.on('error', reject);
}).on('error', reject);
});
}
async function inlineCss(match) {
const href = match.match(/href=["']([^"']+)["']/)[1];
let css = '';
if (href.startsWith('http')) {
css = await fetchUrl(href);
} else {
const cssPath = path.join(baseDir, href);
css = fs.readFileSync(cssPath, 'utf8');
}
return `<style>${css}</style>`;
}
async function inlineJs(match) {
const match2 = match.match(/src=["']([^"']+)["']/);
if (!match2) return match;
const src = match2[1];
let js = '';
if (src.startsWith('http')) {
js = await fetchUrl(src);
} else {
const jsPath = path.join(baseDir, src);
js = fs.readFileSync(jsPath, 'utf8');
}
return `<script>${js}</script>`;
}
async function inlineImg(match) {
const match2 = match.match(/src=["']([^"']+)["']/);
if (!match2) return match;
const src = match2[1];
const ext = path.extname(src).toLowerCase();
let mime = 'image/png';
if (ext === '.svg') mime = 'image/svg+xml';
else if (ext === '.jpg' || ext === '.jpeg') mime = 'image/jpeg';
else if (ext === '.gif') mime = 'image/gif';
else if (ext === '.webp') mime = 'image/webp';
else if (ext === '.ico') mime = 'image/x-icon';
let data;
if (src.startsWith('http')) {
data = await fetchUrl(src);
} else {
const imgPath = path.join(baseDir, src);
data = fs.readFileSync(imgPath);
}
const base64 = Buffer.from(data).toString('base64');
if (ext === '.svg') {
const svgData = typeof data === 'string' ? data : data.toString('utf8');
return match.replace(/src="[^"]+"/, `src="data:${mime};utf8,${encodeURIComponent(svgData)}"`);
}
return match.replace(/src="[^"]+"/, `src="data:${mime};base64,${base64}"`);
}
async function inlineSvg(match) {
const match2 = match.match(/href=["']([^"']+)["']/);
if (!match2) return match;
const href = match2[1];
if (!href.toLowerCase().endsWith('.svg')) return match;
let data;
if (href.startsWith('http')) {
data = await fetchUrl(href);
} else {
const svgPath = path.join(baseDir, href);
data = fs.readFileSync(svgPath, 'utf8');
}
const base64 = Buffer.from(data).toString('base64');
return match.replace(/href="[^"]+"/, `href="data:image/svg+xml;base64,${base64}"`);
}
async function processHtml(content) {
let result = content;
const cssRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/g;
const jsRegex = /<script[^>]+src=["'][^"']+["'][^>]*><\/script>/g;
const imgRegex = /<img[^>]+src=["'][^"']+["'][^>]*>/g;
const svgRegex = /<link[^>]+rel=["']icon["'][^>]*>/g;
const cssMatches = content.match(cssRegex) || [];
for (const match of cssMatches) {
if (!match.includes('data:')) {
result = result.replace(match, await inlineCss(match));
}
}
const jsMatches = content.match(jsRegex) || [];
for (const match of jsMatches) {
if (!match.includes('data:')) {
result = result.replace(match, await inlineJs(match));
}
}
const imgMatches = content.match(imgRegex) || [];
for (const match of imgMatches) {
if (!match.includes('data:')) {
result = result.replace(match, await inlineImg(match));
}
}
const svgMatches = content.match(svgRegex) || [];
for (const match of svgMatches) {
if (!match.includes('data:')) {
result = result.replace(match, await inlineSvg(match));
}
}
return result;
}
processHtml(html).then(result => {
if (outputFile) {
fs.writeFileSync(outputFile, result);
//console.log(`Written to ${outputFile}`);
} else {
console.log(result);
}
}).catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
-43
View File
@@ -1,43 +0,0 @@
// Copyright (c) 2024 Cesanta Software Limited
//
// Utility that generates packed filesystem C file compatible with the
// Mongoose Network Library, https://github.com/cesanta/mongoose
//
// Usage:
// node pack.js FILE[:DESTINATION[:gzip]] ...
const fs = require('fs');
const zlib = require('zlib');
const argv = process.argv.slice(2);
// Convert each command-line arguments into [ DATA_ARRAY, STRUCT_INITIALIZER ]
const entries = argv.map(function(filename, i) {
const parts = filename.split(':');
const stat = fs.statSync(parts[0]);
const data = fs.readFileSync(parts[0], null);
let bytes = Array.from(data);
let destination = (parts[1] || parts[0]).replace(/^\.+[\/\\]*/, '');
if (parts[2] == 'gzip') {
bytes = Array.from(zlib.gzipSync(data));
destination += '.gz';
}
// concat(0) appends trailing 0, in order to make any file an asciz string
return [
`static const unsigned char v${i}[] = {${bytes.concat(0).join(',')}};`,
` {"/${destination}", v${i}, sizeof(v${i}) - 1, ${parseInt(stat.mtimeMs / 1000)}}`,
];
});
process.stdout.write(`// DO NOT EDIT. This file is generated using this command:
// ${process.argv.join(' ')}
#include "mongoose.h"
${entries.map(x => x[0]).join('\n\n')}
const struct mg_mem_file mg_packed_files[] = {
${entries.map(x => x[1]).join(',\n')},
{NULL, NULL, 0, 0}
};
`);
+2 -2
View File
@@ -70,8 +70,8 @@ function sign(firmware, privateKeyFile = 'private.pem') {
? firmware.slice(0, dot) + '.signed' + firmware.slice(dot)
: firmware + '.signed';
fs.writeFileSync(out, Buffer.concat([fw, raw]));
console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature)`);
fs.writeFileSync(out, Buffer.concat([fw, raw, Buffer.from('MGSG')]));
console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature + 4 bytes magic)`);
}
const [,, cmd, arg1, arg2] = process.argv;
+487 -245
View File
@@ -5,67 +5,21 @@
#define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n"
#define MG_JSON_HEADERS "Content-Type: application/json\r\n" MG_NO_CACHE_HEADERS
struct mg_dash_user {
struct mg_dash_user *next;
char name[32]; // User name
char token[21]; // Login token
int level; // Access level
uint64_t expire; // Expiration timestamp
#define CONN_HANDLED 'Z'
struct mg_dash_cdata {
char marker;
struct mg_dash_user *u;
struct mg_dash *dash;
};
#define CONN_HANDLED 'Z'
static struct mg_dash_user s_guest;
static struct mg_str trimq(struct mg_str s) { // Trim double quotes
if (s.len > 1 && s.buf[0] == '"') s.len -= 2, s.buf++;
return s;
}
static void mg_dash_broadcast(struct mg_mgr *mgr, int level, const char *fmt,
...) {
struct mg_connection *c;
va_list ap;
for (c = mgr->conns; c != NULL; c = c->next) {
int user_level = *(int *) c->data;
if (!c->is_websocket) continue;
if (level > 0 && user_level < level) continue;
if (c->send.len > MG_DASH_MAX_SEND_BUF_SIZE) {
mg_error(c, "%lu buffered data %lu > MG_DASH_MAX_SEND_BUF_SIZE", c->id,
c->send.len);
} else {
va_start(ap, fmt);
mg_ws_vprintf(c, WEBSOCKET_OP_TEXT, fmt, &ap);
va_end(ap);
}
}
}
static size_t mg_print_fmt(mg_pfn_t out, void *param, va_list *ap) {
const char *fmt = va_arg(*ap, const char *);
ap = va_arg(*ap, va_list *);
return mg_vxprintf(out, param, fmt, ap);
}
static void mg_dash_success(struct mg_connection *c, struct mg_str req,
const char *fmt, ...) {
struct mg_str id = mg_json_get_tok(req, "$.id");
va_list ap;
va_start(ap, fmt);
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:%M}", MG_ESC("id"), id.len,
id.buf, MG_ESC("result"), mg_print_fmt, fmt, &ap);
va_end(ap);
}
static void mg_dash_error(struct mg_connection *c, struct mg_str req,
const char *fmt, ...) {
struct mg_str id = mg_json_get_tok(req, "$.id");
va_list ap;
va_start(ap, fmt);
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:{%m:%d,%m:%m}}", MG_ESC("id"),
id.len, id.buf, MG_ESC("error"), MG_ESC("code"), -1,
MG_ESC("message"), mg_print_fmt, fmt, &ap);
va_end(ap);
}
static struct mg_field_set *mg_dash_find_field_set(struct mg_dash *dash,
struct mg_str name) {
struct mg_field_set *fs;
@@ -92,6 +46,8 @@ static size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) {
n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false");
} else if (f->type == MG_VAL_INT) {
n += mg_xprintf(fn, arg, "%d", *(int *) f->value);
} else if (f->type == MG_VAL_UINT64) {
n += mg_xprintf(fn, arg, "%llu", (uint64_t) *(uint64_t *) f->value);
} else if (f->type == MG_VAL_DBL) {
n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value);
} else if (f->type == MG_VAL_STR) {
@@ -116,10 +72,56 @@ static size_t mg_print_field_set(mg_pfn_t fn, void *arg, va_list *ap) {
return n;
}
static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) {
static int mg_dash_array_size(struct mg_field_set *set,
struct mg_dash_user *u) {
int saved = *set->index, sz = -1;
*set->index = -1;
if (set->fn) {
if (set->fn(MG_DASH_READ, u)) sz = *set->index;
} else if (set->get_dir) {
mg_dash_dir_read(set, u);
sz = *set->index;
}
*set->index = saved;
return sz;
}
static size_t mg_dash_print_array(mg_pfn_t fn, void *arg, va_list *ap) {
struct mg_field_set *set = va_arg(*ap, struct mg_field_set *);
int from = va_arg(*ap, int);
int to = va_arg(*ap, int);
struct mg_dash_user *u = va_arg(*ap, struct mg_dash_user *);
bool started = false;
int saved = *set->index;
size_t n = 0;
*set->index = from;
n += mg_xprintf(fn, arg, "[");
for (;;) {
bool done = to >= 0 && *set->index > to;
if (!done) {
if (set->fn)
set->fn(MG_DASH_READ, u);
else if (set->get_dir)
mg_dash_dir_read(set, u);
done = *set->index < 0;
}
if (done) break;
n += mg_xprintf(fn, arg, "%s%M", started ? "," : "", mg_print_field_set,
set);
started = true;
(*set->index)++;
}
n += mg_xprintf(fn, arg, "]");
*set->index = saved;
return n;
}
static size_t mg_dash_print_endpoint(mg_pfn_t fn, void *arg, va_list *ap) {
struct mg_dash *dash = va_arg(*ap, struct mg_dash *);
struct mg_dash_user *u = va_arg(*ap, struct mg_dash_user *);
struct mg_str *name = va_arg(*ap, struct mg_str *);
int level = va_arg(*ap, int);
struct mg_str *from_str = va_arg(*ap, struct mg_str *);
struct mg_str *to_str = va_arg(*ap, struct mg_str *);
struct mg_field_set *set = mg_dash_find_field_set(dash, *name);
size_t n = 0;
if (name->len == 0) {
@@ -127,37 +129,74 @@ static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) {
const char *comma = "";
n += mg_xprintf(fn, arg, "{");
for (fs = dash->sets; fs != NULL; fs = fs->next) {
if (fs->read_level > 0 && level < fs->read_level) continue;
if (fs->reader) fs->reader();
n += mg_xprintf(fn, arg, comma);
n += mg_xprintf(fn, arg, "%m:", MG_ESC(fs->name));
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, fs);
if (fs->index != NULL) {
int sz = mg_dash_array_size(fs, u);
if (sz < 0) continue;
n += mg_xprintf(fn, arg, comma);
n += mg_xprintf(fn, arg, "%m:%d", MG_ESC(fs->name), sz);
} else {
if (fs->fn && !fs->fn(MG_DASH_READ, u)) continue;
n += mg_xprintf(fn, arg, comma);
n += mg_xprintf(fn, arg, "%m:%M", MG_ESC(fs->name), mg_print_field_set,
fs);
}
comma = ",";
}
n += mg_xprintf(fn, arg, "}");
} else if (set != NULL &&
(set->read_level <= 0 || level >= set->read_level)) {
if (set->reader) set->reader();
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set);
} else if (set != NULL && (set->fn == NULL || set->fn(MG_DASH_READ, u))) {
if (set->index != NULL && from_str != NULL && from_str->len > 0) {
int from = 0, to = 0;
mg_str_to_num(*from_str, 10, &from, sizeof(from));
to = from;
if (to_str != NULL && to_str->len > 0) {
mg_str_to_num(*to_str, 10, &to, sizeof(to));
}
n += mg_xprintf(fn, arg, "%M", mg_dash_print_array, set, from, to, u);
} else if (set->index != NULL) {
n += mg_xprintf(fn, arg, "%d", mg_dash_array_size(set, u));
} else {
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set);
}
} else {
n += mg_xprintf(fn, arg, "null");
}
return n;
}
static size_t mg_dash_print(mg_pfn_t fn, void *arg, va_list *ap) {
struct mg_str *req = va_arg(*ap, struct mg_str *);
struct mg_dash *dash = va_arg(*ap, struct mg_dash *);
int level = va_arg(*ap, int);
struct mg_str name = trimq(mg_json_get_tok(*req, "$.params"));
return mg_xprintf(fn, arg, "%M", mg_dash_print_name, dash, &name, level);
}
void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *set) {
if (set->reader) set->reader();
mg_dash_broadcast(mgr, set->read_level, "{%m:%m,%m:{%m:%M}}",
MG_ESC("method"), MG_ESC("change"), MG_ESC("params"),
MG_ESC(set->name), mg_print_field_set, set);
struct mg_connection *c;
for (c = mgr->conns; c != NULL; c = c->next) {
struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data;
struct mg_dash_user *u = d->u;
if (!c->is_websocket) continue;
if (u == NULL) continue;
if (set->index != NULL && *set->index < 0) {
int sz = mg_dash_array_size(set, u);
if (sz < 0) continue;
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%d}}", MG_ESC("method"),
MG_ESC("change"), MG_ESC("params"), MG_ESC(set->name), sz);
} else {
int saved_idx = set->index != NULL ? *set->index : 0;
bool ok = set->fn ? set->fn(MG_DASH_READ, u)
: (set->get_dir ? mg_dash_dir_read(set, u) : true);
if (!ok) {
if (set->index != NULL) *set->index = saved_idx;
continue;
}
if (set->index != NULL) {
char key[64];
mg_snprintf(key, sizeof(key), "%s/%d", set->name, *set->index);
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%M}}",
MG_ESC("method"), MG_ESC("change"), MG_ESC("params"),
MG_ESC(key), mg_print_field_set, set);
*set->index = saved_idx;
} else {
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%M}}",
MG_ESC("method"), MG_ESC("change"), MG_ESC("params"),
MG_ESC(set->name), mg_print_field_set, set);
}
}
}
}
static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) {
@@ -174,6 +213,13 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) {
*(int *) f->value = (int) d;
ok = true;
}
} else if (f->type == MG_VAL_UINT64) {
double d;
if (f->value_size == sizeof(uint64_t) &&
mg_json_get_num(json, json_path, &d) && d == (double) (int64_t) d) {
*(uint64_t *) f->value = (uint64_t) d;
ok = true;
}
} else if (f->type == MG_VAL_DBL) {
ok = f->value_size == sizeof(double) &&
mg_json_get_num(json, json_path, (double *) f->value);
@@ -187,97 +233,99 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) {
}
static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash,
struct mg_str json, int level) {
struct mg_str json, struct mg_dash_user *u) {
struct mg_str key, val;
size_t ofs = 0;
int total_count = 0;
while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) {
struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key));
int count = 0;
if (set != NULL && (set->write_level <= 0 || level >= set->write_level)) {
if (set == NULL) {
MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf));
continue;
}
if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) continue; // auth check
{
size_t i;
for (i = 0; set->fields[i].name != NULL; i++) {
if (mg_dash_parse_field(val, &set->fields[i])) count++;
}
if (count) {
if (set->writer) set->writer();
mg_dash_send_change(c->mgr, set);
total_count += count;
}
} else if (set == NULL) {
MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf));
}
if (count) {
if (set->fn) set->fn(MG_DASH_WRITE, u); // apply side effects
mg_dash_send_change(c->mgr, set);
total_count += count;
}
}
return total_count;
}
static void mg_dash_process_msg(struct mg_connection *c,
struct mg_ws_message *wm,
struct mg_dash *dash) {
struct mg_str req = wm->data;
struct mg_str method = trimq(mg_json_get_tok(req, "$.method"));
int level = *(int *) c->data;
if (mg_match(method, mg_str("get"), NULL)) {
mg_dash_success(c, req, "%M", mg_dash_print, &req, dash, level);
} else if (mg_match(method, mg_str("set"), NULL)) {
struct mg_str params = trimq(mg_json_get_tok(req, "$.params"));
int count = mg_dash_apply(c, dash, params, level);
mg_dash_success(c, req, "%d", count);
} else {
mg_dash_error(c, req, "%s", "unknown method");
bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u) {
char dir[256], fname[128] = "";
struct mg_fs *fs = u->dash->upload_fs ? u->dash->upload_fs : &mg_fs_posix;
struct mg_field *name_field = NULL, *size_field = NULL;
size_t i;
if (!set->get_dir(u, dir, sizeof(dir))) return false;
for (i = 0; set->fields[i].name != NULL; i++) {
if (name_field == NULL && set->fields[i].type == MG_VAL_STR &&
strcmp(set->fields[i].name, "name") == 0)
name_field = &set->fields[i];
if (size_field == NULL && strcmp(set->fields[i].name, "size") == 0)
size_field = &set->fields[i];
}
if (name_field == NULL) return false;
if (*set->index == -1) { // Size query: count all files
int count = 0;
while (mg_fs_ls(fs, dir, fname, sizeof(fname))) count++;
*set->index = count;
return true;
}
{ // Regular read: scan to *set->index
int target = *set->index, cur = 0;
while (mg_fs_ls(fs, dir, fname, sizeof(fname))) {
if (cur++ == target) {
mg_snprintf((char *) name_field->value, name_field->value_size, "%s",
fname);
if (size_field != NULL) {
char path[512];
size_t sz = 0;
mg_snprintf(path, sizeof(path), "%s/%s", dir, fname);
fs->st(path, &sz, NULL);
if (size_field->type == MG_VAL_UINT64)
*(uint64_t *) size_field->value = (uint64_t) sz;
else if (size_field->type == MG_VAL_INT)
*(int *) size_field->value = (int) sz;
}
return true;
}
}
*set->index = -1; // No more entries
return true;
}
}
void mg_dash_file_add(struct mg_str name, size_t size) {
struct mg_dash_file *f = (struct mg_dash_file *) mg_calloc(1, sizeof(*f));
f->name = mg_strdup(name).buf;
f->size = size;
f->next = mg_dash_files;
mg_dash_files = f;
}
void mg_dash_file_del(struct mg_str name) {
struct mg_dash_file **head, *f;
for (head = &mg_dash_files, f = *head; f; head = &(*head)->next, f = *head) {
if (mg_strcmp(mg_str(f->name), name) == 0) {
MG_INFO(("Deleting %s", f->name));
*head = f->next;
mg_free(f->name);
mg_free(f);
return;
static bool mg_dash_set_file_name(struct mg_field_set *set,
struct mg_str name) {
size_t i;
for (i = 0; set->fields[i].name != NULL; i++) {
struct mg_field *f = &set->fields[i];
if (f->type == MG_VAL_STR && strcmp(f->name, "name") == 0) {
mg_snprintf((char *) f->value, f->value_size, "%.*s", (int) name.len,
name.buf);
return true;
}
}
return false;
}
// Files array: [{"name": "foo.txt", "size": 1234}]
struct mg_dash_file *mg_dash_files;
static char s_files[1024];
static void read_files(void) {
size_t len = 0;
struct mg_dash_file *f;
len += mg_snprintf(s_files + len, sizeof(s_files) - len, "[");
for (f = mg_dash_files; f; f = f->next) {
len += mg_snprintf(s_files + len, sizeof(s_files) - len, "%s{%m:%m,%m:%u}",
len > 1 ? "," : "", MG_ESC("name"), MG_ESC(f->name),
MG_ESC("size"), f->size);
}
len += mg_snprintf(s_files + len, sizeof(s_files) - len, "]");
}
static struct mg_field fields_files[] = {
{"data", MG_VAL_RAW, s_files, sizeof(s_files)},
{NULL, MG_VAL_INT, NULL, 0},
};
static struct mg_field_set set_files = {
"files", fields_files, read_files, NULL, 0, 0, NULL,
};
static inline void mg_log_http_req(struct mg_connection *c,
struct mg_http_message *hm) {
int len = 0;
size_t n, spaces = 0;
size_t n, spaces = 0, body_n = hm->body.len;
struct mg_http_message tmp;
memset(&tmp, 0, sizeof(tmp));
len = mg_http_parse((char *) c->send.buf, c->send.len, &tmp);
@@ -286,18 +334,24 @@ static inline void mg_log_http_req(struct mg_connection *c,
(c->send.buf[c->send.len - spaces - 1] == '\r' ||
c->send.buf[c->send.len - spaces - 1] == '\n'))
spaces++;
MG_DEBUG(("%lu %.*s %.*s %.*s: %lu %.*s -> %lu %.*s", c->id, hm->method.len,
hm->method.buf, hm->uri.len, hm->uri.buf, c->send.len > 15 ? 3 : 0,
&c->send.buf[9], hm->body.len, hm->body.len, hm->body.buf,
c->send.len - n, c->send.len - n - spaces, c->send.buf + n));
}
static struct mg_str mg_dash_file_name(struct mg_http_message *hm) {
struct mg_str name = mg_str_n(hm->uri.buf + 4, hm->uri.len - 4); // - /fs/
// Decode file name in-place - directly into the request buffer
int len = mg_url_decode(name.buf, name.len, name.buf, name.len + 1, 0);
if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len;
return name;
// hm->body.len comes from Content-Length and can be larger than the bytes
// actually buffered so far (e.g. mid-stream uploads); cap the preview to
// what's actually present in c->recv, or we'd read past its end
{
char *recv_end = (char *) c->recv.buf + c->recv.len;
if (hm->body.buf >= (char *) c->recv.buf && hm->body.buf <= recv_end) {
size_t avail = (size_t) (recv_end - hm->body.buf);
if (body_n > avail) body_n = avail;
} else {
body_n = 0;
}
}
MG_DEBUG(("%lu %.*s %.*s%s%.*s %.*s: %lu %.*s -> %lu %.*s", c->id,
hm->method.len, hm->method.buf, hm->uri.len, hm->uri.buf,
hm->query.len > 0 ? "?" : "", hm->query.len, hm->query.buf,
c->send.len > 15 ? 3 : 0, &c->send.buf[9], hm->body.len, body_n,
hm->body.buf, c->send.len - n, c->send.len - n - spaces,
c->send.buf + n));
}
static void mg_dash_ota_cb(struct mg_connection *c, const char *errmsg) {
@@ -309,13 +363,20 @@ static void mg_dash_upload_cb(struct mg_connection *c, const char *errmsg) {
if (errmsg) {
mg_http_reply(c, 500, NULL, "%s\n", errmsg);
} else {
char path[128];
size_t size = 0;
mg_snprintf(path, sizeof(path), "/tmp/%s", c->data);
mg_fs_posix.st(path, &size, NULL);
mg_dash_file_add(mg_str(c->data), size);
// mg_http_start_upload() repurposes c->data for its own bookkeeping,
// so the field set can't be cached there. Re-derive the dashboard from
// c->fn_data instead, and notify every file-backed array: the upload
// could belong to any of them, and re-querying get_dir() per recipient
// is what mg_dash_send_change() does anyway (directories can be
// user-specific)
struct mg_dash *dash = (struct mg_dash *) c->fn_data;
struct mg_field_set *fs;
mg_http_reply(c, 200, NULL, "ok\n");
mg_dash_send_change(c->mgr, &set_files);
for (fs = dash->sets; fs != NULL; fs = fs->next) {
if (fs->get_dir == NULL) continue;
*fs->index = -1; // Signal mg_dash_send_change() to broadcast new size
mg_dash_send_change(c->mgr, fs);
}
}
c->is_draining = 1;
}
@@ -326,16 +387,69 @@ static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) {
return mg_millis() + t * 1000;
}
static struct mg_dash_user *mg_dash_add_user(struct mg_dash_user **users,
struct mg_dash *dash,
const char *name,
const char *token, int level) {
struct mg_dash_user *u = (struct mg_dash_user *) mg_calloc(1, sizeof(*u));
if (u != NULL) {
mg_snprintf(u->name, sizeof(u->name), "%s", name);
if (token == NULL) {
mg_random_str(u->token, sizeof(u->token) - 1);
} else {
mg_snprintf(u->token, sizeof(u->token), "%s", token);
}
u->level = level;
u->expire = mg_dash_make_expiration_time(dash);
u->dash = dash;
u->next = *users;
*users = u;
}
return u;
}
static struct mg_dash_user *mg_dash_find_user(struct mg_dash_user *users,
const char *name) {
struct mg_dash_user *u;
for (u = users; u != NULL; u = u->next) {
if (strcmp(u->name, name) == 0) return u;
}
return NULL;
}
static struct mg_dash_user *mg_dash_find_token(struct mg_dash_user *users,
const char *token) {
struct mg_dash_user *u;
for (u = users; u != NULL; u = u->next) {
if (strcmp(u->token, token) == 0) return u;
}
return NULL;
}
static void mg_dash_refresh_user(struct mg_dash *dash,
struct mg_dash_user *user) {
user->expire = mg_dash_make_expiration_time(dash);
}
// Parse HTTP requests, return authenticated user or NULL
static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm,
static struct mg_dash_user *mg_dash_authenticate(struct mg_connection *c,
struct mg_http_message *hm,
struct mg_dash *dash) {
static struct mg_dash_user *s_users; // List of authenticated users
char user[100], pass[100];
static struct mg_dash_user admin = {NULL, "admin", "admin", 9, (uint64_t) -1};
struct mg_dash_user *u, *tmp, *result = NULL;
struct mg_dash_user *u, *tmp;
struct mg_str *ah;
int level = 0, num_users = 0;
if (dash->authenticate == NULL) return &admin;
if (dash->authenticate == NULL) {
mg_snprintf(s_guest.name, sizeof(s_guest.name), "%s", "guest");
s_guest.level = 9;
s_guest.dash = dash;
dash->guest = &s_guest;
return dash->guest;
}
mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass));
ah = mg_http_get_header(hm, "Authorization");
// MG_DEBUG(("user [%s], pass: [%s], h: %.*s", user, pass, hm->head.len,
// hm->head.buf));
@@ -343,47 +457,48 @@ static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm,
for (u = s_users; u != NULL; u = tmp) {
tmp = u->next;
if (u->expire < mg_millis()) {
struct mg_connection *conn;
MG_DEBUG(("Deleting expired auth %s/%d %llu %u", u->name, u->level,
u->expire, mg_millis() - u->expire));
for (conn = c->mgr->conns; conn != NULL; conn = conn->next) {
struct mg_dash_cdata *d = (struct mg_dash_cdata *) conn->data;
if (conn->is_websocket && d->u == u) {
d->u = NULL;
conn->is_closing = 1;
}
}
LIST_DELETE(struct mg_dash_user, &s_users, u);
mg_free(u);
}
}
if (pass[0] != '\0') {
struct mg_str *ah = mg_http_get_header(hm, "Authorization");
if (ah != NULL) {
// Auth header and password are set, auth by user/password via glue API
int num_users = 0, level = dash->authenticate(user, sizeof(user), pass);
MG_DEBUG(("user %s, level: %d", user, level));
if (level > 0) { // Proceed only if the firmware authenticated us
for (u = s_users; u != NULL && result == NULL;
u = u->next, num_users++) {
if (strcmp(user, u->name) == 0) {
u->expire = mg_dash_make_expiration_time(dash);
result = u;
}
}
// Not yet authenticated, add to the list
if (result == NULL && num_users < 10) {
result = (struct mg_dash_user *) mg_calloc(1, sizeof(*result));
mg_snprintf(result->name, sizeof(result->name), "%s", user);
mg_random_str(result->token, sizeof(result->token) - 1);
result->level = level, result->next = s_users, s_users = result;
result->expire = mg_dash_make_expiration_time(dash);
}
}
} else if (ah == NULL) {
for (u = s_users; u != NULL && result == NULL; u = u->next) {
if (strcmp(u->token, pass) == 0) {
u->expire = mg_dash_make_expiration_time(dash);
result = u;
}
}
if (pass[0] == '\0') return NULL;
for (u = s_users; u != NULL; u = u->next) num_users++;
if (ah == NULL) {
u = mg_dash_find_token(s_users, pass);
if (u != NULL) {
mg_dash_refresh_user(dash, u);
return u;
}
}
level = dash->authenticate(user, sizeof(user), pass);
MG_DEBUG(("user %s, level: %d", user, level));
if (level <= 0) return NULL;
u = mg_dash_find_user(s_users, user);
if (u != NULL) {
if (ah == NULL) mg_snprintf(u->token, sizeof(u->token), "%s", pass);
mg_dash_refresh_user(dash, u);
return u;
}
if (num_users < 10)
return mg_dash_add_user(&s_users, dash, user, ah == NULL ? pass : NULL,
level);
// MG_DEBUG(("[%s/%s] -> %s", user, pass, result ? "OK" : "FAIL"));
return result;
return NULL;
}
static void mg_handle_login(struct mg_connection *c, struct mg_dash_user *u) {
@@ -408,48 +523,157 @@ static void mg_handle_logout(struct mg_connection *c) {
mg_http_reply(c, 401, cookie, "Unauthorized\n");
}
static void mg_dash_handle_del(struct mg_connection *c, struct mg_dash *dash,
struct mg_dash_user *u, struct mg_str *parts) {
struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
if (set == NULL || set->index == NULL) {
mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n");
} else {
int from = 0, to = 0, count = 0;
if (parts[1].len) mg_str_to_num(parts[1], 10, &from, sizeof(from));
to = parts[2].len ? 0 : from;
if (parts[2].len) mg_str_to_num(parts[2], 10, &to, sizeof(to));
for (*set->index = from; *set->index <= to; (*set->index)++) {
if (set->fn && set->fn(MG_DASH_DELETE, u))
count++;
else
break;
}
if (count) {
*set->index = -1;
mg_dash_send_change(c->mgr, set);
mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count);
} else {
mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n");
}
}
}
// Handle "POST /api/get/<set>/<index>": modify one array element. Loads the
// element at <index> first - that doubles as a read-access check and as a
// pre-fill, so that JSON keys absent from the body keep their old values -
// then overlays the values from the body and asks fn to persist them
static void mg_dash_handle_mod(struct mg_connection *c, struct mg_dash *dash,
struct mg_dash_user *u, struct mg_str *parts,
struct mg_str body) {
struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
int index = 0, count = 0;
size_t i;
if (set == NULL || set->index == NULL || set->fn == NULL) {
mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n");
return;
}
mg_str_to_num(parts[1], 10, &index, sizeof(index));
*set->index = index;
if (!set->fn(MG_DASH_READ, u) || *set->index != index) {
mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n");
return;
}
for (i = 0; set->fields[i].name != NULL; i++) {
if (mg_dash_parse_field(body, &set->fields[i])) count++;
}
if (count && set->fn(MG_DASH_WRITE, u)) {
mg_dash_send_change(c->mgr, set);
mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count);
} else {
mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n");
}
}
// Handle "POST /api/add/<set>": append a new array element. Parses the body
// straight into the bound fields, then asks fn to accept and persist them as
// a new element - fn returns false to reject, e.g. when a cap is reached
static void mg_dash_handle_add(struct mg_connection *c, struct mg_dash *dash,
struct mg_dash_user *u, struct mg_str *parts,
struct mg_str body) {
struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
int count = 0;
size_t i;
if (set == NULL || set->index == NULL || set->fn == NULL) {
mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n");
return;
}
for (i = 0; set->fields[i].name != NULL; i++) {
if (mg_dash_parse_field(body, &set->fields[i])) count++;
}
if (count && set->fn(MG_DASH_ADD, u)) {
*set->index = -1; // Signal mg_dash_send_change() to send new size
mg_dash_send_change(c->mgr, set);
mg_http_reply(c, 200, MG_JSON_HEADERS, "true\n");
} else {
mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n");
}
}
void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
struct mg_dash *dash = (struct mg_dash *) c->fn_data;
struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data;
if (ev == MG_EV_OPEN) {
if (mg_dash_find_field_set(dash, mg_str("files")) == NULL) {
MG_DASH_ADD_FIELD_SET(dash, &set_files);
}
d->dash = dash;
// c->is_hexdumping = 1;
} else if (ev == MG_EV_HTTP_HDRS && c->data[0] == 0) {
} else if (ev == MG_EV_HTTP_HDRS && d->marker == 0) {
// Received headers - check authentication and possibly start uploads/ota
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
struct mg_dash_user *u = mg_dash_authenticate(hm, dash);
struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash);
struct mg_str parts[3];
memset(parts, 0, sizeof(parts));
if (mg_match(hm->uri, mg_str("/api/hi"), NULL) ||
mg_match(hm->uri, mg_str("/api/logout"), NULL)) {
// Do nothing, handle them MG_EV_HTTP_MSG. We bypass auth for those
} else if (u == NULL && mg_match(hm->uri, mg_str("/api/#"), NULL)) {
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
c->data[0] = CONN_HANDLED;
d->marker = CONN_HANDLED;
} else if (mg_match(hm->uri, mg_str("/api/login"), NULL) && u != NULL) {
mg_handle_login(c, u);
c->data[0] = CONN_HANDLED;
d->marker = CONN_HANDLED;
} else if (mg_match(hm->uri, mg_str("/api/ota"), NULL)) {
mg_http_start_ota(c, hm, mg_dash_ota_cb);
} else if (mg_match(hm->uri, mg_str("/fs/#"), NULL) &&
} else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts) &&
(mg_strcasecmp(hm->method, mg_str("POST")) == 0 ||
mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) {
struct mg_str name = mg_dash_file_name(hm);
mg_snprintf(c->data, sizeof(c->data), "%.*s", (int) name.len, name.buf);
mg_http_start_upload(c, hm, name, mg_str("/tmp"), &mg_fs_posix,
mg_dash_upload_cb);
struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
struct mg_str name = parts[1];
int len =
mg_url_decode(name.buf, name.len, (char *) name.buf, name.len + 1, 0);
if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len;
if (set == NULL || set->get_dir == NULL) {
mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found\n");
d->marker = CONN_HANDLED;
} else if (u == NULL) {
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
d->marker = CONN_HANDLED;
} else if (!mg_path_is_sane(name)) {
mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n");
d->marker = CONN_HANDLED;
} else {
mg_dash_set_file_name(set, name);
if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) {
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
d->marker = CONN_HANDLED;
} else {
char dir[256];
struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix;
if (!set->get_dir(u, dir, sizeof(dir))) {
mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n");
d->marker = CONN_HANDLED;
} else {
mg_http_start_upload(c, hm, name, mg_str(dir), fs,
mg_dash_upload_cb);
}
}
}
}
if (c->data[0] != '\0') mg_log_http_req(c, hm);
} else if (ev == MG_EV_HTTP_MSG && c->data[0] != '\0') {
if (d->marker != '\0') mg_log_http_req(c, hm);
} else if (ev == MG_EV_HTTP_MSG && d->marker != '\0') {
// The response has been send in EV_HDRS path, so we're not reponding
// anything but clearing the c->data[0] flag for the next request.
c->data[0] = 0;
// anything but clearing the marker for the next request.
d->marker = 0;
c->is_resp = 0;
} else if (ev == MG_EV_HTTP_MSG && c->data[0] == '\0') {
} else if (ev == MG_EV_HTTP_MSG && d->marker == '\0') {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
struct mg_dash_user *u = mg_dash_authenticate(hm, dash);
int level = u == NULL ? 0 : u->level;
struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash);
struct mg_str parts[5];
memset(parts, 0, sizeof(parts));
@@ -460,27 +684,60 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m}", MG_ESC("method"),
MG_ESC("logout"));
} else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) {
*(int *) c->data = level;
d->u = u;
mg_ws_upgrade(c, hm, NULL);
} else if (mg_match(hm->uri, mg_str("/fs/#"), NULL)) {
struct mg_str name = mg_dash_file_name(hm);
char path[128];
mg_snprintf(path, sizeof(path), "/tmp/%.*s", name.len, name.buf);
if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) {
// Delete file
mg_dash_file_del(name);
mg_dash_send_change(c->mgr, &set_files);
mg_http_reply(c, 200, NULL, "true");
} else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts)) {
struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
if (set == NULL || set->get_dir == NULL) {
mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found");
} else if (u == NULL) {
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
} else {
// Serve file
mg_http_serve_file(c, hm, path, NULL);
char dir[256], path[512];
struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix;
struct mg_str name = parts[1];
int len = mg_url_decode(name.buf, name.len, (char *) name.buf,
name.len + 1, 0);
if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len;
if (!mg_path_is_sane(name)) {
mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n");
return;
}
if (!set->get_dir(u, dir, sizeof(dir))) {
mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n");
return;
}
mg_snprintf(path, sizeof(path), "%s/%.*s", dir, name.len, name.buf);
if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) {
mg_dash_set_file_name(set, name);
if (set->fn != NULL && !set->fn(MG_DASH_DELETE, u)) {
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
} else {
fs->rm(path);
*set->index = -1; // Signal mg_dash_send_change() to send new size
mg_dash_send_change(c->mgr, set);
mg_http_reply(c, 200, NULL, "true");
}
} else {
mg_http_serve_file(c, hm, path, NULL);
}
}
} else if (mg_match(hm->uri, mg_str("/api/get/#"), parts) ||
} else if (mg_match(hm->uri, mg_str("/api/del/*/*/*"), parts) ||
mg_match(hm->uri, mg_str("/api/del/*/*"), parts)) {
mg_dash_handle_del(c, dash, u, parts);
} else if (mg_match(hm->uri, mg_str("/api/add/*"), parts)) {
mg_dash_handle_add(c, dash, u, parts, hm->body);
} else if (mg_match(hm->uri, mg_str("/api/get/*/*"), parts) &&
mg_strcasecmp(hm->method, mg_str("POST")) == 0) {
mg_dash_handle_mod(c, dash, u, parts, hm->body);
} else if (mg_match(hm->uri, mg_str("/api/get/*/*/*"), parts) ||
mg_match(hm->uri, mg_str("/api/get/*/*"), parts) ||
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], level);
mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_endpoint,
dash, u, &parts[0], &parts[1], &parts[2]);
} else if (mg_match(hm->uri, mg_str("/api/set"), NULL)) {
int count = mg_dash_apply(c, dash, hm->body, level);
int count = mg_dash_apply(c, dash, hm->body, u);
mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count);
} else if (mg_match(hm->uri, mg_str("/"), NULL)) {
struct mg_http_serve_opts opts;
@@ -498,20 +755,5 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
if (ch == NULL) mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found");
mg_log_http_req(c, hm);
}
} else if (ev == MG_EV_WS_OPEN) {
// WS connection established, send change notifications for all data
struct mg_field_set *fs;
for (fs = dash->sets; fs != NULL; fs = fs->next) {
mg_dash_send_change(c->mgr, fs);
}
mg_dash_broadcast(c->mgr, 0, "{%m:%m}", MG_ESC("method"), MG_ESC("ready"));
} else if (ev == MG_EV_WS_MSG) {
// Add this to automatically handle "get" and "set" JSON-RPC calls
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
if (dash == NULL) {
mg_dash_error(c, wm->data, "%s", "no dash defined");
} else {
mg_dash_process_msg(c, wm, dash);
}
}
}
+116 -49
View File
@@ -7,50 +7,95 @@
#include "str.h"
#include "ws.h"
#ifndef MG_DASH_MAX_USER_NAME
#define MG_DASH_MAX_USER_NAME 32
#endif
#ifndef MG_HTTP_ADDR
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
#define MG_HTTP_ADDR "http://0.0.0.0:8000"
#define MG_HTTPS_ADDR "https://0.0.0.0:8443"
#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502"
#else
#define MG_HTTP_ADDR "http://0.0.0.0:80"
#define MG_HTTPS_ADDR "http://0.0.0.0:443"
#define MG_MODBUS_ADDR "tcp://0.0.0.0:502"
#endif
#endif
#ifndef MG_DASH_MAX_SEND_BUF_SIZE
#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5)
#endif
// C type of a value bound to a dashboard field, see struct mg_field
enum mg_val_type {
MG_VAL_INT,
MG_VAL_BOOL,
MG_VAL_DBL,
MG_VAL_STR,
MG_VAL_RAW,
MG_VAL_INT, // int
MG_VAL_UINT64, // uint64_t
MG_VAL_BOOL, // bool
MG_VAL_DBL, // double
MG_VAL_STR, // Zero-terminated string
MG_VAL_RAW, // Pre-formatted JSON value, copied verbatim
};
// Dashboard field: binds a JSON key to a C variable for read and/or write
struct mg_field {
const char *name;
enum mg_val_type type;
void *value;
size_t value_size;
const char *name; // JSON key
enum mg_val_type type; // C type of the value, see enum mg_val_type
void *value; // Pointer to the bound variable
size_t value_size; // sizeof(*value) for type checking; 0 makes it read-only
};
// Operation passed to a struct mg_field_set read/write/delete/add callback.
// MG_DASH_ADD: fields hold the values of a new element to append; *index is
// not set. Return true to accept and persist it, false to reject (e.g. a cap
// was reached)
enum mg_dash_op { MG_DASH_READ, MG_DASH_WRITE, MG_DASH_DELETE, MG_DASH_ADD };
struct mg_dash;
// Authenticated (or guest) dashboard user
struct mg_dash_user {
struct mg_dash_user *next; // Next user in the list
char name[MG_DASH_MAX_USER_NAME]; // Username
char token[21]; // Session token, sent to the client as a cookie
int level; // Access level, see struct mg_dash::authenticate
uint64_t expire; // Session expiration time, see mg_millis()
struct mg_dash *dash; // Dashboard this user belongs to
};
// Named group of related dashboard fields, e.g. "settings" or "metrics"
struct mg_field_set {
const char *name;
struct mg_field *fields;
void (*reader)(void);
void (*writer)(void);
int read_level;
int write_level;
struct mg_field_set *next;
const char *name; // Set name, used as a JSON key
struct mg_field *fields; // Zero-terminated array of fields
bool (*fn)(enum mg_dash_op op,
struct mg_dash_user *u); // Read/write/delete/add callback
int *index; // Non-NULL: array set. *index is set before fn(READ) call;
// fn sets *index = -1 to signal end of iteration.
// Size query: framework sets *index = -1 before fn(READ); fn
// sets *index = total size
bool (*get_dir)(const struct mg_dash_user *u, char *buf,
size_t len); // Non-NULL: file array
struct mg_field_set *next; // Next set in the list
};
// Custom URI handler, registered with MG_DASH_REGISTER_CUSTOM_HANDLER()
struct mg_dash_custom_handler {
struct mg_dash_custom_handler *next;
struct mg_str uri_pattern;
mg_event_handler_t handler;
void *handler_data;
};
struct mg_dash_file {
struct mg_dash_file *next;
char *name;
size_t size;
struct mg_dash_custom_handler *next; // Next handler in the list
struct mg_str uri_pattern; // URI to match, see mg_match()
mg_event_handler_t handler; // Event handler function
void *handler_data; // Opaque data for the handler
};
// Dashboard instance: fields, users and handlers served over HTTP and WS
struct mg_dash {
struct mg_field_set *sets;
struct mg_dash_custom_handler *custom_handlers;
struct mg_field_set *sets; // Field sets, see MG_DASH_ADD_FIELD_SET()
struct mg_dash_custom_handler *custom_handlers; // Custom URI handlers
// Validates pass, fills in user, returns access level (<= 0: failure).
// NULL: no login required, all clients get guest access
int (*authenticate)(char *user, size_t userlen, const char *pass);
int session_auto_expiration_seconds;
//struct mg_dash_file *files;
int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default
struct mg_dash_user *guest; // Used when authenticate == NULL
struct mg_fs *upload_fs; // FS for uploads/listing, default &mg_fs_posix
};
#define MG_DASH_ADD_FIELD_SET(dash_, set_) \
@@ -69,28 +114,50 @@ struct mg_dash {
(dash_)->custom_handlers = &ch_; \
} while (0)
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
#define MG_HTTP_ADDR "http://0.0.0.0:8000"
#define MG_HTTPS_ADDR "https://0.0.0.0:8443"
#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502"
#else
#define MG_HTTP_ADDR "http://0.0.0.0:80"
#define MG_HTTPS_ADDR "http://0.0.0.0:443"
#define MG_MODBUS_ADDR "tcp://0.0.0.0:502"
#endif
#ifndef MG_DASH_MAX_SEND_BUF_SIZE
#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5)
#endif
// File manager
extern struct mg_dash_file *mg_dash_files;
void mg_dash_file_add(struct mg_str name, size_t size);
void mg_dash_file_del(struct mg_str name);
// Dashboard event handler. Pass to mg_http_listen() as the fn argument
void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data);
// Notify WebSocket clients that a field set changed: broadcasts the new
// array size if *set->index < 0, or a single element update otherwise
void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *);
// Default reader for file-backed array sets (set->get_dir != NULL)
// Usage example - this is a complete implemenation of the file manager:
// ```c
// static struct file {
// int index;
// char name[64];
// size_t size;
// uint64_t checksum;
// } s_file;
// static struct mg_field_set set_files;
//
// static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) {
// (void) u;
// mkdir("/tmp/dashboard", 0755);
// mg_snprintf(buf, len, "%s", "/tmp/dashboard");
// return true;
// }
//
// static struct mg_field fields_files[] = {
// {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)},
// {"size", MG_VAL_UINT64, &s_file.size, 0},
// {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)},
// {NULL, MG_VAL_INT, NULL, 0},
// };
//
// static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) {
// if (op == MG_DASH_WRITE) return s_uploads_enabled && u->level >= 7;
// if (op != MG_DASH_READ) return false;
// if (!mg_dash_dir_read(&set_files, u)) return false;
// s_file.checksum = 0; // Mock to show how to set custom fields
// return true;
// }
//
// static struct mg_field_set set_files = {"files", fields_files, files_fn, &s_file.index, get_dir, NULL};
// ```
bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u);
// Helper forward declarations for Mongoose CMSIS pack modules
extern struct mg_mgr g_mgr;
extern void mg_dash_init(struct mg_mgr *);
+16 -9
View File
@@ -64,15 +64,22 @@ bool mg_ota_flash_end(struct mg_flash *flash) {
MG_DEBUG(("CRC: %x/%x, size: %lu/%lu, status: %s", s_crc32, crc32, s_size,
size, ok ? "ok" : "fail"));
#ifdef MG_OTA_PUBLIC_KEY
if (ok && s_size > 64) {
static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY;
uint8_t hash[32];
size_t fw_size = s_size - 64;
mg_sha256(hash, (uint8_t *) base, fw_size);
ok = mg_uecc_verify(s_pubkey, hash, sizeof(hash),
(uint8_t *) base + fw_size,
mg_uecc_secp256r1()) == 1;
MG_INFO(("Signature: %s", ok ? "ok" : "fail"));
if (ok) {
bool signed_fw = s_size > 68 &&
memcmp((uint8_t *) base + s_size - 4, "MGSG", 4) == 0;
if (signed_fw) {
static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY;
uint8_t hash[32];
size_t fw_size = s_size - 68; // strip 64-byte sig + 4-byte magic
mg_sha256(hash, (uint8_t *) base, fw_size);
ok = mg_uecc_verify(s_pubkey, hash, sizeof(hash),
(uint8_t *) base + fw_size,
mg_uecc_secp256r1()) == 1;
MG_INFO(("Signature: %s", ok ? "ok" : "fail"));
} else {
ok = false;
MG_ERROR(("Unsigned firmware rejected"));
}
}
#endif
s_size = 0;
+8 -3
View File
@@ -1024,19 +1024,19 @@ static void mg_upload_handler(struct mg_connection *c, int ev, void *ev_data) {
if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; }
else mg_ota_end();
p->fn(c, "write error");
p->fn = NULL;
mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL;
} else if (p->received >= p->expected) {
const char *errmsg = NULL;
if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; }
else if (!mg_ota_end()) errmsg = "OTA finalize failed";
p->fn(c, errmsg);
p->fn = NULL;
mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL;
}
} else if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) {
if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; }
else mg_ota_end();
p->fn(c, ev == MG_EV_ERROR ? (const char *) ev_data : "connection closed");
p->fn = NULL;
mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL;
}
(void) ev_data;
}
@@ -1046,6 +1046,10 @@ static void mg_upload_default_cb(struct mg_connection *c, const char *status) {
mg_http_reply(c, status ? 500 : 200, "", "%s\n", status ? status : "ok");
}
const char *mg_upload_path(struct mg_connection *c) {
return (const char *) c->pfn_data;
}
void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm,
struct mg_str name, struct mg_str dir,
struct mg_fs *fs,
@@ -1065,6 +1069,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm,
p->fd = fd;
p->fn = fn;
c->fn = mg_upload_handler;
c->pfn_data = strdup(path);
c->pfn = NULL;
mg_iobuf_del(&c->recv, 0, hm->head.len);
mg_call(c, MG_EV_READ, &c->recv.len);
+1
View File
@@ -64,6 +64,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm,
void (*fn)(struct mg_connection *, const char *));
void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm,
void (*fn)(struct mg_connection *, const char *));
const char *mg_upload_path(struct mg_connection *c);
void mg_http_bauth(struct mg_connection *, const char *user, const char *pass);
struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v);
size_t mg_http_next_multipart(struct mg_str, size_t, struct mg_http_part *);
+6
View File
@@ -2,6 +2,7 @@
#include "dns.h"
#include "fmt.h"
#include "log.h"
#include "ota.h"
#include "printf.h"
#include "profile.h"
#include "timer.h"
@@ -278,6 +279,11 @@ void mg_mgr_free(struct mg_mgr *mgr) {
void mg_mgr_init(struct mg_mgr *mgr) {
memset(mgr, 0, sizeof(*mgr));
// Anchor mg_fw_version: a real store into a struct field the optimiser
// can't elide forces the linker to keep the section alive even when no
// OTA polling code runs it. Callers that want mgr->userdata for their own
// purposes simply overwrite it after this call returns
mgr->userdata = (void *) mg_fw_version;
#if MG_ENABLE_EPOLL
if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0)
MG_ERROR(("epoll_create1 errno %d", errno));
+1
View File
@@ -2180,6 +2180,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
struct mg_connection *c, *tmp;
uint64_t now = mg_millis();
mg_timer_poll(&mgr->timers, now);
mg_ota_poll(mgr);
if (mgr->ifp == NULL || mgr->ifp->driver == NULL) return;
mg_tcpip_poll(mgr->ifp, now);
for (c = mgr->conns; c != NULL; c = tmp) {
+126 -100
View File
@@ -2,146 +2,172 @@
#include "http.h"
#include "json.h"
#include "log.h"
#include "timer.h"
#include "printf.h"
#include "url.h"
#include "util.h"
enum { MG_OTA_STATUS_WAITING, MG_OTA_STATUS_SUCCESS, MG_OTA_STATUS_FAIL };
static int s_version_status;
static int s_ota_status;
static uint64_t s_start_time;
#ifndef MG_OTA_MAX_URL_LEN
#define MG_OTA_MAX_URL_LEN 256
#endif
static struct mg_ota_metadata {
char *version;
char *url;
// Scannable version tag embedded in every firmware binary, for server-side
// version extraction. Non-static so mg_mgr_init() can stash its address in
// mgr->userdata: that store is what actually keeps -Wl,--gc-sections from
// stripping it on builds that never poll for OTAs (an attribute alone does
// not - the linker drops unreferenced sections regardless of "used"/"retain")
const char mg_fw_version[] = "MG_VERSION:" MG_OTA_FIRMWARE_VERSION;
static struct mg_ota_state {
char my_version[MG_OTA_MAX_VERSION_LEN];
char json_url[MG_OTA_MAX_URL_LEN];
char version[MG_OTA_MAX_VERSION_LEN];
char url[MG_OTA_MAX_URL_LEN];
size_t size;
uint8_t sha256[32];
} s_ota_metadata;
void (*fn)(const char *status);
} *s_ota;
static void free_ota_metadata(void) {
if (s_ota_metadata.version) mg_free(s_ota_metadata.version);
if (s_ota_metadata.url) mg_free(s_ota_metadata.url);
memset(&s_ota_metadata, 0, sizeof(s_ota_metadata));
}
static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data);
static int fetch_ota_metadata(struct mg_http_message *response) {
double result;
if (mg_http_status(response) != 200) goto fetch_failed;
if (mg_json_get(response->body, "$", NULL) != 0) goto fetch_failed;
free_ota_metadata();
s_ota_metadata.version = mg_json_get_str(response->body, "$.version");
if (s_ota_metadata.version == NULL) goto fetch_failed;
s_ota_metadata.url = mg_json_get_str(response->body, "$.url");
if (s_ota_metadata.url == NULL) goto fetch_failed;
if (!mg_json_get_num(response->body, "$.size", &result) || result <= 0)
goto fetch_failed;
s_ota_metadata.size = (size_t) result;
// TODO (robertc2000): parse and validate sha256
MG_DEBUG(("Firmware version: %s, url: %s, size: %ld", s_ota_metadata.version,
s_ota_metadata.url, s_ota_metadata.size));
return MG_OTA_STATUS_SUCCESS;
fetch_failed:
free_ota_metadata();
return MG_OTA_STATUS_FAIL;
#if MG_ENABLE_CUSTOM_DEVICE_ID
#else
void mg_ota_device_id(char *buf, size_t len) {
#if MG_ARCH == MG_ARCH_CUBE && defined(UID_BASE)
mg_snprintf(buf, len, "%M", mg_print_hex, 12, (uint8_t *) UID_BASE);
#else
mg_snprintf(buf, len, "%d", 0);
#endif
}
#endif
static void s_version_fn(struct mg_connection *c, int ev, void *ev_data) {
struct mg_str host = mg_url_host((char *) c->fn_data);
struct mg_http_message *hm;
static int fetch_status;
uint64_t expiration = *(uint64_t *) c->data;
if (ev == MG_EV_POLL) {
if (s_start_time + 5 * 1000 < mg_millis()) {
mg_error(c, "Connection timeout");
}
if (mg_millis() > expiration) mg_error(c, "Metadata timeout");
} else if (ev == MG_EV_CONNECT) {
char id[33];
struct mg_str host = mg_url_host(s_ota->json_url);
const char *uri = mg_url_uri(s_ota->json_url);
const char *sep = strchr(uri, '?') == NULL ? "?" : "&";
mg_ota_device_id(id, sizeof(id));
id[sizeof(id) - 1] = '\0';
mg_printf(c,
"GET %s HTTP/1.1\r\n"
"GET %s%sarch=%d&version=%s&id=%s&interval=%d HTTP/1.1\r\n"
"Host: %.*s\r\n"
"Connection: close\r\n\r\n",
mg_url_uri((char *) c->fn_data), (int) host.len, host.buf);
fetch_status = MG_OTA_STATUS_WAITING;
uri, sep, MG_ARCH, s_ota->my_version, id,
MG_OTA_PULL_INTERVAL_SECONDS, host.len, host.buf);
} else if (ev == MG_EV_HTTP_MSG) {
hm = (struct mg_http_message *) ev_data;
fetch_status = fetch_ota_metadata(hm);
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
double result;
MG_DEBUG(("Got metadata:\n%.*s", hm->body.len, hm->body.buf));
if (mg_http_status(hm) != 200 || mg_json_get(hm->body, "$", NULL) != 0 ||
!mg_json_unescape(hm->body, "$.version", s_ota->version,
sizeof(s_ota->version)) ||
!mg_json_unescape(hm->body, "$.url", s_ota->url, sizeof(s_ota->url)) ||
!mg_json_get_num(hm->body, "$.size", &result) || result <= 0) {
char buf[100];
mg_snprintf(buf, sizeof(buf), "Bad metadata: %.*s", hm->body.len,
hm->body.buf);
s_ota->fn(buf);
mg_free(s_ota);
s_ota = NULL;
} else if (strcmp(s_ota->version, s_ota->my_version) == 0) {
s_ota->fn("Same version");
mg_free(s_ota);
s_ota = NULL;
} else {
struct mg_connection *fc;
s_ota->size = (size_t) result;
// TODO (robertc2000): parse and validate sha256
MG_DEBUG(("Firmware version: %s, url: %s, size: %ld", s_ota->version,
s_ota->url, s_ota->size));
fc = mg_http_connect(c->mgr, s_ota->url, s_firmware_fn, NULL);
if (fc == NULL) {
s_ota->fn("Failed to connect");
mg_free(s_ota);
s_ota = NULL;
} else {
*(uint64_t *) fc->data = mg_millis() + 300 * 1000; // Set expiration
}
}
c->is_closing = 1;
} else if (ev == MG_EV_ERROR) {
MG_ERROR(("%lu Connection error", c->id));
s_version_status = MG_OTA_STATUS_FAIL;
} else if (ev == MG_EV_CLOSE) {
MG_DEBUG(("%lu Connection closed %lu", c->id, mg_millis() - s_start_time));
s_version_status = fetch_status;
s_ota->fn((char *) ev_data);
mg_free(s_ota);
s_ota = NULL;
}
}
static void s_ota_done(struct mg_connection *c, const char *errmsg) {
(void) c;
s_ota_status = errmsg ? MG_OTA_STATUS_FAIL : MG_OTA_STATUS_SUCCESS;
static void status_fn(const char *errmsg) {
if (errmsg) MG_ERROR(("OTA failed: %s", errmsg));
}
static void status_fn_2(struct mg_connection *c, const char *errmsg) {
if (s_ota) s_ota->fn(errmsg);
mg_free(s_ota);
s_ota = NULL;
mg_http_reply(c, errmsg ? 500 : 200, "", "%s\n", errmsg ? errmsg : "ok");
}
static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) {
struct mg_str host = mg_url_host(s_ota_metadata.url);
uint64_t expiration = *(uint64_t *) c->data;
if (ev == MG_EV_POLL) {
if (s_start_time + 120 * 1000 < mg_millis()) mg_error(c, "Timeout");
if (mg_millis() > expiration) mg_error(c, "OTA timeout");
} else if (ev == MG_EV_CONNECT) {
struct mg_str host = mg_url_host(s_ota->url);
mg_printf(c,
"GET %s HTTP/1.1\r\n"
"Host: %.*s\r\n"
"Connection: close\r\n\r\n",
mg_url_uri(s_ota_metadata.url), (int) host.len, host.buf);
mg_url_uri(s_ota->url), (int) host.len, host.buf);
} else if (ev == MG_EV_HTTP_HDRS) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
int status = mg_http_status(hm);
if (status != 200 || hm->body.len != s_ota_metadata.size) {
if (status != 200 || hm->body.len != s_ota->size) {
mg_error(c, "Bad HTTP response: status %d, size %lu vs %lu", status,
(unsigned long) hm->body.len,
(unsigned long) s_ota_metadata.size);
s_ota_status = MG_OTA_STATUS_FAIL;
return;
(unsigned long) hm->body.len, (unsigned long) s_ota->size);
} else {
MG_DEBUG(("Beginning OTA (%lu bytes)", (unsigned long) s_ota->size));
mg_http_start_ota(c, hm, status_fn_2);
}
MG_DEBUG(
("Beginning OTA (%lu bytes)", (unsigned long) s_ota_metadata.size));
mg_http_start_ota(c, hm, s_ota_done);
} else if (ev == MG_EV_ERROR) {
MG_ERROR(("%lu Connection error", c->id));
s_ota_status = MG_OTA_STATUS_FAIL;
s_ota->fn((char *) ev_data);
mg_free(s_ota);
s_ota = NULL;
}
(void) ev_data;
}
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
const char *metadata_url,
void (*fn)(const char *status)) {
s_version_status = MG_OTA_STATUS_WAITING;
s_ota_status = MG_OTA_STATUS_WAITING;
s_start_time = mg_millis();
MG_DEBUG(("Connecting to %s to retrieve metadata", metadata_url));
if (!mg_http_connect(mgr, metadata_url, s_version_fn,
(void *) metadata_url)) {
if (fn) fn("Failed to connect");
return;
}
while (s_version_status == MG_OTA_STATUS_WAITING) mg_mgr_poll(mgr, 10);
if (s_version_status == MG_OTA_STATUS_SUCCESS) {
if (strcmp(s_ota_metadata.version, current_version) == 0) {
if (fn) fn("Same version");
free_ota_metadata();
return;
}
void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version,
const char *json_url, void (*fn)(const char *status)) {
if (fn == NULL) fn = status_fn;
if (s_ota != NULL) {
fn("OTA already in progress");
} else if ((s_ota = (struct mg_ota_state *) mg_calloc(1, sizeof(*s_ota))) ==
NULL) {
fn("Out of memory");
} else {
if (fn) fn("Version retrieving error");
free_ota_metadata();
return;
struct mg_connection *c;
mg_snprintf(s_ota->my_version, sizeof(s_ota->my_version), "%s", my_version);
mg_snprintf(s_ota->json_url, sizeof(s_ota->json_url), "%s", json_url);
MG_DEBUG(("Connecting to %s", json_url));
c = mg_http_connect(mgr, s_ota->json_url, s_version_fn, NULL);
if (c == NULL) {
mg_free(s_ota);
s_ota = NULL;
fn("Failed to connect");
} else {
s_ota->fn = fn;
*(uint64_t *) c->data = mg_millis() + 5 * 1000; // Set expiration
}
}
}
void mg_ota_poll(struct mg_mgr *mgr) {
static uint64_t t = 1;
if (MG_OTA_URL != NULL &&
mg_timer_expired(&t, MG_OTA_PULL_INTERVAL_SECONDS * 1000, mg_millis())) {
mg_ota_url_check(mgr, MG_OTA_FIRMWARE_VERSION, MG_OTA_URL,
MG_OTA_STATUS_FN);
}
if (fn) fn("Pulling firmware");
s_start_time = mg_millis();
MG_DEBUG(("Connecting to %s to download firmware", s_ota_metadata.url));
if (!mg_http_connect(mgr, s_ota_metadata.url, s_firmware_fn, NULL)) {
if (fn) fn("Failed to connect");
free_ota_metadata();
return;
}
while (s_ota_status == MG_OTA_STATUS_WAITING) mg_mgr_poll(mgr, 10);
if (s_ota_status == MG_OTA_STATUS_FAIL && fn) fn("OTA fail");
free_ota_metadata();
}
+34
View File
@@ -100,3 +100,37 @@ enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 };
// Pull based OTA over HTTP
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
const char *metadata_url, void (*fn)(const char *status));
#ifndef MG_OTA_URL
#define MG_OTA_URL NULL
#endif
#ifndef MG_OTA_STATUS_FN
#define MG_OTA_STATUS_FN NULL
#endif
#ifndef MG_OTA_FIRMWARE_VERSION
#define MG_OTA_FIRMWARE_VERSION "1.0.0"
#endif
#ifndef MG_OTA_MAX_VERSION_LEN
#define MG_OTA_MAX_VERSION_LEN 64
#endif
// Scannable "MG_VERSION:<MG_OTA_FIRMWARE_VERSION>" tag embedded in every
// firmware binary, for server-side extraction. mg_mgr_init() stashes its
// address in mgr->userdata, which is what keeps it from being stripped by
// -Wl,--gc-sections on builds that never poll for OTAs
extern const char mg_fw_version[];
#ifndef MG_OTA_PULL_INTERVAL_SECONDS
#define MG_OTA_PULL_INTERVAL_SECONDS 60
#endif
#ifndef MG_ENABLE_CUSTOM_DEVICE_ID
#define MG_ENABLE_CUSTOM_DEVICE_ID 0
#endif
void mg_ota_device_id(char *buf, size_t len);
void mg_ota_poll(struct mg_mgr *);
+2
View File
@@ -8,6 +8,7 @@
#include "tls.h"
#include "url.h"
#include "util.h"
#include "ota.h"
#if MG_ENABLE_SOCKET
@@ -757,6 +758,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
mg_iotest(mgr, ms);
now = mg_millis();
mg_timer_poll(&mgr->timers, now);
mg_ota_poll(mgr);
for (c = mgr->conns; c != NULL; c = tmp) {
bool is_resp = c->is_resp;
+385 -59
View File
@@ -4202,32 +4202,65 @@ static int dash_authenticate(char *user, size_t userlen, const char *pass) {
}
struct dash_ws_test {
const char *req;
int opened;
int done;
int result;
int saw_set3;
};
struct dash_arr_ws_test {
int opened;
int done;
int saw_arr_index; // index seen in "arr/INDEX" change key, -1 if not seen
int saw_arr_val; // "val" seen in the change params, -1 if not seen
int saw_arr_size; // size seen in a plain "arr":N change, -1 if not seen
};
static void dash_arr_ws_cb(struct mg_connection *c, int ev, void *ev_data) {
struct dash_arr_ws_test *d = (struct dash_arr_ws_test *) c->fn_data;
if (ev == MG_EV_WS_OPEN) {
d->opened = 1;
} else if (ev == MG_EV_WS_MSG) {
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
struct mg_str method = mg_json_get_tok(wm->data, "$.method");
if (mg_strcmp(method, mg_str("\"change\"")) == 0) {
// Check for "arr/INDEX" (element change) or "arr":N (size change)
const char *p = wm->data.buf;
const char *end = p + wm->data.len;
while (p < end - 6) {
if (strncmp(p, "\"arr/", 5) == 0) {
const char *v;
p += 5;
d->saw_arr_index = (int) strtol(p, NULL, 10);
v = strstr(p, "\"val\":");
if (v != NULL) d->saw_arr_val = (int) strtol(v + 6, NULL, 10);
d->done = 1;
c->is_closing = 1;
break;
}
if (strncmp(p, "\"arr\":", 6) == 0) {
d->saw_arr_size = (int) strtol(p + 6, NULL, 10);
d->done = 1;
c->is_closing = 1;
break;
}
p++;
}
}
} else if (ev == MG_EV_CLOSE && d->done == 0) {
d->done = -1;
}
}
static void dash_ws_cb(struct mg_connection *c, int ev, void *ev_data) {
struct dash_ws_test *d = (struct dash_ws_test *) c->fn_data;
if (ev == MG_EV_WS_OPEN) {
if (d->req != NULL) {
mg_ws_send(c, d->req, strlen(d->req), WEBSOCKET_OP_TEXT);
}
d->opened = 1;
} else if (ev == MG_EV_WS_MSG) {
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
struct mg_str method = mg_json_get_tok(wm->data, "$.method");
struct mg_str set3 = mg_json_get_tok(wm->data, "$.params.set3");
double result = 0;
if (mg_strcmp(method, mg_str("\"change\"")) == 0 && set3.buf != NULL) {
d->saw_set3 = 1;
}
if (mg_json_get_num(wm->data, "$.result", &result)) {
d->result = (int) result;
d->done = 1;
c->is_closing = 1;
} else if (d->req == NULL &&
mg_strcmp(method, mg_str("\"ready\"")) == 0) {
d->done = 1;
c->is_closing = 1;
}
@@ -4238,8 +4271,89 @@ static void dash_ws_cb(struct mg_connection *c, int ev, void *ev_data) {
char three[10] = "hi";
int one = 1;
static void rfields1(void) {
mg_snprintf(three, sizeof(three), "t: %d", one); // Simulate array
#define TEST_ARR_SIZE 17
#define TEST_ARR_CAP (TEST_ARR_SIZE + 2) // Cap for MG_DASH_ADD tests
#define TEST_ARR_RO_INDEX 3 // This element is read-only: WRITE is always denied
static int s_arr_index;
static int s_arr_val;
static int s_arr_store[TEST_ARR_CAP]; // Backing store, init to squares
static int s_arr_count = TEST_ARR_SIZE; // Logical size, grows via MG_DASH_ADD
static bool set1_fn(enum mg_dash_op op, struct mg_dash_user *u) {
if (u->level < 3) return false;
if (op == MG_DASH_READ) {
mg_snprintf(three, sizeof(three), "t: %d", one);
return true;
}
return u->level >= 7;
}
static bool set2_fn(enum mg_dash_op op, struct mg_dash_user *u) {
(void) u; return op == MG_DASH_READ;
}
static bool set3_fn(enum mg_dash_op op, struct mg_dash_user *u) {
if (u->level < 7) return false;
return op == MG_DASH_READ || op == MG_DASH_WRITE;
}
static bool test_upload_dir(const struct mg_dash_user *u, char *buf, size_t len) {
(void) u;
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
mkdir("/tmp/mongoose_dash_test", 0755);
#endif
mg_snprintf(buf, len, "%s", "/tmp/mongoose_dash_test");
return true;
}
struct test_file_entry {
char name[64];
size_t size;
};
static struct test_file_entry s_test_file;
static int s_test_file_index;
static struct mg_field test_files_fields[] = {
{"name", MG_VAL_STR, s_test_file.name, sizeof(s_test_file.name)},
{"size", MG_VAL_UINT64, &s_test_file.size, 0},
{NULL, MG_VAL_INT, NULL, 0},
};
static struct mg_field_set test_files_set = {
"files", test_files_fields, NULL, &s_test_file_index, test_upload_dir, NULL};
static struct test_file_entry s_delete_file;
static char s_deleted_file[sizeof(s_delete_file.name)];
static char s_uploaded_file[sizeof(s_delete_file.name)];
static struct mg_field_set *s_delete_file_set;
static bool delete_file_fn(enum mg_dash_op op, struct mg_dash_user *u) {
if (op == MG_DASH_READ) return mg_dash_dir_read(s_delete_file_set, u);
if (op == MG_DASH_WRITE) {
mg_snprintf(s_uploaded_file, sizeof(s_uploaded_file), "%s",
s_delete_file.name);
}
if (op == MG_DASH_DELETE) {
mg_snprintf(s_deleted_file, sizeof(s_deleted_file), "%s",
s_delete_file.name);
}
return true;
}
static bool arr_fn(enum mg_dash_op op, struct mg_dash_user *u) {
(void) u;
if (op == MG_DASH_READ) {
if (s_arr_index < 0) { s_arr_index = s_arr_count; return true; }
if (s_arr_index >= s_arr_count) { s_arr_index = -1; return true; }
s_arr_val = s_arr_store[s_arr_index];
return true;
}
if (op == MG_DASH_WRITE) {
if (s_arr_index < 0 || s_arr_index >= s_arr_count) return false;
if (s_arr_index == TEST_ARR_RO_INDEX) return false; // Deny: read-only
s_arr_store[s_arr_index] = s_arr_val;
return true;
}
if (op == MG_DASH_ADD) {
if (s_arr_count >= TEST_ARR_CAP) return false; // Deny: cap reached
s_arr_store[s_arr_count++] = s_arr_val;
return true;
}
if (op == MG_DASH_DELETE) return s_arr_index < s_arr_count;
return false;
}
static void test_dash(void) {
@@ -4248,7 +4362,7 @@ static void test_dash(void) {
const char *ws_url = "ws://localhost:26352/api/websocket";
struct mg_mgr mgr;
struct mg_dash dash;
struct dash_ws_test ws_user, ws_admin, ws_user_notify;
struct dash_ws_test ws_admin, ws_user_notify;
bool two = false;
int admin = 7;
int i;
@@ -4265,13 +4379,20 @@ static void test_dash(void) {
{"admin", MG_VAL_INT, &admin, sizeof(admin)},
{NULL, MG_VAL_INT, NULL, 0},
};
struct mg_field_set set1 = {"set1", fields1, rfields1, NULL, 3, 7, NULL};
struct mg_field_set set2 = {"set2", fields2, NULL, NULL, 0, 0, NULL};
struct mg_field_set set3 = {"set3", fields3, NULL, NULL, 7, 7, NULL};
struct mg_field_set set1 = {"set1", fields1, set1_fn, NULL, NULL, NULL};
struct mg_field_set set2 = {"set2", fields2, set2_fn, NULL, NULL, NULL};
struct mg_field_set set3 = {"set3", fields3, set3_fn, NULL, NULL, NULL};
struct mg_field arr_fields[] = {
{"val", MG_VAL_INT, &s_arr_val, sizeof(s_arr_val)},
{NULL, MG_VAL_INT, NULL, 0},
};
struct mg_field_set arr_set = {"arr", arr_fields, arr_fn, &s_arr_index, NULL,
NULL};
const char *get_all_expected =
"{\"files\":{\"data\":[]},\"set3\":{\"admin\":7},"
"{\"arr\":17,\"set3\":{\"admin\":7},"
"\"set2\":{\"two\":false},"
"\"set1\":{\"one\":1,\"three\":\"t: 1\"}}\n";
"\"set1\":{\"one\":1,\"three\":\"t: 1\"},"
"\"files\":0}\n";
const char *set1_req = "POST /api/set HTTP/1.0\nContent-Length: 18\n\n"
"{\"set1\":{\"one\":2}}";
const char *set1_set3_req = "POST /api/set HTTP/1.0\n"
@@ -4290,8 +4411,14 @@ static void test_dash(void) {
"Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 20\n\n"
"{\"set3\":{\"admin\":8}}";
const char *set1_ws_set4_req =
"{\"id\":1,\"method\":\"set\",\"params\":{\"set1\":{\"one\":4}}}";
const char *set3_admin_req9 = "POST /api/set HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 20\n\n"
"{\"set3\":{\"admin\":9}}";
const char *set1_admin_set4_req = "POST /api/set HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 18\n\n"
"{\"set1\":{\"one\":4}}";
const char *set1_expected = "{\"one\":2,\"three\":\"t: 2\"}\n";
const char *set1_expected3 = "{\"one\":3,\"three\":\"t: 3\"}\n";
const char *set1_expected4 = "{\"one\":4,\"three\":\"t: 4\"}\n";
@@ -4302,14 +4429,47 @@ static void test_dash(void) {
const char *set2_req = "POST /api/set HTTP/1.0\nContent-Length: 21\n\n"
"{\"set2\":{\"two\":true}}";
const char *set2_expected = "{\"two\":false}\n";
const char *arr7_expected = "[{\"val\":49}]\n";
const char *arr0_2_expected = "[{\"val\":0},{\"val\":1},{\"val\":4}]\n";
const char *arr5_mod_req = "POST /api/get/arr/5 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 10\n\n"
"{\"val\":99}";
const char *arr100_mod_req = "POST /api/get/arr/100 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 10\n\n"
"{\"val\":99}";
const char *arr3_mod_req = "POST /api/get/arr/3 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 11\n\n"
"{\"val\":777}";
const char *arr_add555_req = "POST /api/add/arr HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 11\n\n"
"{\"val\":555}";
const char *arr_add556_req = "POST /api/add/arr HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 11\n\n"
"{\"val\":556}";
const char *arr_add557_req = "POST /api/add/arr HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 11\n\n"
"{\"val\":557}";
const char *arr5_mod_expected = "[{\"val\":99}]\n";
const char *arr3_expected = "[{\"val\":9}]\n";
const char *arr17_expected = "[{\"val\":555}]\n";
const char *files_probe = "\"files\"";
const char *test_bin_probe = "\"test.bin\"";
char marker = 0;
memset(&dash, 0, sizeof(dash));
dash.session_auto_expiration_seconds = 1;
remove("/tmp/mongoose_dash_test/test.bin");
MG_DASH_ADD_FIELD_SET(&dash, &test_files_set);
MG_DASH_ADD_FIELD_SET(&dash, &set1);
ASSERT(dash.sets == &set1);
ASSERT(set1.next == NULL);
ASSERT(set1.next == &test_files_set);
MG_DASH_ADD_FIELD_SET(&dash, &set2);
ASSERT(dash.sets == &set2);
@@ -4326,26 +4486,9 @@ static void test_dash(void) {
ASSERT(dash.custom_handlers->handler == dash_custom);
ASSERT(dash.custom_handlers->handler_data == &marker);
while (mg_dash_files != NULL) mg_dash_file_del(mg_str(mg_dash_files->name));
mg_dash_file_add(mg_str("one.txt"), 123);
ASSERT(mg_dash_files != NULL);
ASSERT(strcmp(mg_dash_files->name, "one.txt") == 0);
ASSERT(mg_dash_files->size == 123);
mg_dash_file_add(mg_str("two.bin"), 456);
ASSERT(mg_dash_files != NULL);
ASSERT(strcmp(mg_dash_files->name, "two.bin") == 0);
ASSERT(mg_dash_files->size == 456);
ASSERT(mg_dash_files->next != NULL);
ASSERT(strcmp(mg_dash_files->next->name, "one.txt") == 0);
mg_dash_file_del(mg_str("one.txt"));
ASSERT(mg_dash_files != NULL);
ASSERT(strcmp(mg_dash_files->name, "two.bin") == 0);
ASSERT(mg_dash_files->next == NULL);
mg_dash_file_del(mg_str("missing.txt"));
ASSERT(mg_dash_files != NULL);
mg_dash_file_del(mg_str("two.bin"));
ASSERT(mg_dash_files == NULL);
for (i = 0; i < TEST_ARR_SIZE; i++) s_arr_store[i] = i * i;
MG_DASH_ADD_FIELD_SET(&dash, &arr_set);
mg_mgr_init(&mgr);
mg_mem_files = mg_packed_files;
ASSERT(mg_http_listen(&mgr, url, mg_dash_ev_handler, &dash) != NULL);
@@ -4422,41 +4565,38 @@ static void test_dash(void) {
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, set1_expected3) == 0);
ws_user_notify.req = NULL;
ws_user_notify.opened = 0;
ws_user_notify.done = 0;
ws_user_notify.result = -1;
ws_user_notify.saw_set3 = 0;
mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_user_notify, "%s",
"Authorization: Basic dXNlcjp1c2Vy\r\n");
for (i = 0; i < 200 && ws_user_notify.opened == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_user_notify.opened == 1);
ASSERT(fetch(&mgr, buf, url, set3_admin_req9) == 200);
ASSERT(cmpbody(buf, "1\n") == 0);
for (i = 0; i < 200 && ws_user_notify.done == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_user_notify.done == 1);
ASSERT(ws_user_notify.done == 0);
ASSERT(ws_user_notify.saw_set3 == 0);
ws_user.req = set1_ws_set4_req;
ws_user.done = 0;
ws_user.result = -1;
ws_user.saw_set3 = 0;
mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_user, "%s",
"Authorization: Basic dXNlcjp1c2Vy\r\n");
for (i = 0; i < 200 && ws_user.done == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_user.done == 1);
ASSERT(ws_user.result == 0);
ASSERT(ws_user.saw_set3 == 0);
ASSERT(fetch(&mgr, buf, url,
"GET /api/get/set1 HTTP/1.0\n"
"Authorization: Basic dXNlcjp1c2Vy\n\n") == 200);
ASSERT(cmpbody(buf, set1_expected3) == 0);
ws_admin.req = set1_ws_set4_req;
ws_admin.opened = 0;
ws_admin.done = 0;
ws_admin.result = -1;
ws_admin.saw_set3 = 0;
mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_admin, "%s",
"Authorization: Basic YWRtaW46YWRtaW4=\r\n");
for (i = 0; i < 200 && ws_admin.opened == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_admin.opened == 1);
ASSERT(fetch(&mgr, buf, url, set3_admin_req) == 200);
ASSERT(cmpbody(buf, "1\n") == 0);
for (i = 0; i < 200 && ws_admin.done == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_admin.done == 1);
ASSERT(ws_admin.result == 1);
ASSERT(ws_admin.saw_set3 == 1);
ASSERT(fetch(&mgr, buf, url, set1_admin_set4_req) == 200);
ASSERT(cmpbody(buf, "1\n") == 0);
ASSERT(fetch(&mgr, buf, url,
"GET /api/get/set1 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
@@ -4466,7 +4606,193 @@ static void test_dash(void) {
ASSERT(fetch(&mgr, buf, url, "GET /api/hi HTTP/1.0\n\n") == 200);
ASSERT(cmpbody(buf, "hi\n") == 0);
// Array tests with auth (17 squares)
ASSERT(fetch(&mgr, buf, url,
"GET /api/get/arr HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, "17\n") == 0);
ASSERT(fetch(&mgr, buf, url,
"GET /api/get/arr/7 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, arr7_expected) == 0);
ASSERT(fetch(&mgr, buf, url,
"GET /api/get/arr/0/2 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, arr0_2_expected) == 0);
// Array deletion: single element
ASSERT(fetch(&mgr, buf, url,
"GET /api/del/arr/3 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, "1\n") == 0);
// Array deletion: range
ASSERT(fetch(&mgr, buf, url,
"GET /api/del/arr/1/3 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, "3\n") == 0);
// Array deletion: out-of-range returns 403
ASSERT(fetch(&mgr, buf, url,
"GET /api/del/arr/100 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 403);
// Non-array set returns 404
ASSERT(fetch(&mgr, buf, url,
"GET /api/del/set1/0 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 404);
// Test that array change notification uses "arr/INDEX" key format
{
struct dash_arr_ws_test ws_arr;
memset(&ws_arr, 0, sizeof(ws_arr));
ws_arr.saw_arr_index = -1;
ws_arr.saw_arr_val = -1;
ws_arr.saw_arr_size = -1;
mg_ws_connect(&mgr, ws_url, dash_arr_ws_cb, &ws_arr, "%s",
"Authorization: Basic YWRtaW46YWRtaW4=\r\n");
for (i = 0; i < 200 && ws_arr.opened == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_arr.opened == 1);
s_arr_index = 5;
mg_dash_send_change(&mgr, &arr_set);
for (i = 0; i < 200 && ws_arr.done == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_arr.done == 1);
ASSERT(ws_arr.saw_arr_index == 5);
}
// Array modification: POST /api/get/<set>/<index> updates one element
// and broadcasts a "<set>/<index>" WS change notification with new value
{
struct dash_arr_ws_test ws_mod;
memset(&ws_mod, 0, sizeof(ws_mod));
ws_mod.saw_arr_index = -1;
ws_mod.saw_arr_val = -1;
ws_mod.saw_arr_size = -1;
mg_ws_connect(&mgr, ws_url, dash_arr_ws_cb, &ws_mod, "%s",
"Authorization: Basic YWRtaW46YWRtaW4=\r\n");
for (i = 0; i < 200 && ws_mod.opened == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_mod.opened == 1);
ASSERT(fetch(&mgr, buf, url, arr5_mod_req) == 200);
ASSERT(cmpbody(buf, "1\n") == 0);
for (i = 0; i < 200 && ws_mod.done == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_mod.done == 1);
ASSERT(ws_mod.saw_arr_index == 5);
ASSERT(ws_mod.saw_arr_val == 99);
// The new value got persisted - a fresh read returns it
ASSERT(fetch(&mgr, buf, url,
"GET /api/get/arr/5 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, arr5_mod_expected) == 0);
// Out-of-range index: 404
ASSERT(fetch(&mgr, buf, url, arr100_mod_req) == 404);
// Read-only element: fn denies MG_DASH_WRITE -> 403, value unchanged
ASSERT(fetch(&mgr, buf, url, arr3_mod_req) == 403);
ASSERT(fetch(&mgr, buf, url,
"GET /api/get/arr/3 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, arr3_expected) == 0);
}
// Array addition: POST /api/add/<set> appends an element and broadcasts
// a plain "<set>":N WS change notification with the new size
{
struct dash_arr_ws_test ws_add;
memset(&ws_add, 0, sizeof(ws_add));
ws_add.saw_arr_index = -1;
ws_add.saw_arr_val = -1;
ws_add.saw_arr_size = -1;
mg_ws_connect(&mgr, ws_url, dash_arr_ws_cb, &ws_add, "%s",
"Authorization: Basic YWRtaW46YWRtaW4=\r\n");
for (i = 0; i < 200 && ws_add.opened == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_add.opened == 1);
ASSERT(fetch(&mgr, buf, url, arr_add555_req) == 200);
ASSERT(cmpbody(buf, "true\n") == 0);
for (i = 0; i < 200 && ws_add.done == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_add.done == 1);
ASSERT(ws_add.saw_arr_size == TEST_ARR_SIZE + 1);
// The new element landed at the end of the array
ASSERT(fetch(&mgr, buf, url,
"GET /api/get/arr/17 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, arr17_expected) == 0);
// One more addition reaches the cap, then fn denies further ones
ASSERT(fetch(&mgr, buf, url, arr_add556_req) == 200);
ASSERT(fetch(&mgr, buf, url, arr_add557_req) == 403);
}
mg_mgr_free(&mgr);
// Test no files fieldset: /fs/ returns 404
{
const char *url2 = "http://localhost:26353";
struct mg_mgr mgr2;
struct mg_dash dash2;
memset(&dash2, 0, sizeof(dash2));
MG_DASH_ADD_FIELD_SET(&dash2, &set1);
MG_DASH_ADD_FIELD_SET(&dash2, &set2);
MG_DASH_ADD_FIELD_SET(&dash2, &set3);
mg_mgr_init(&mgr2);
mg_mem_files = mg_packed_files;
ASSERT(mg_http_listen(&mgr2, url2, mg_dash_ev_handler, &dash2) != NULL);
ASSERT(fetch(&mgr2, buf, url2, "GET /api/get HTTP/1.0\n\n") == 200);
ASSERT(strstr(buf, files_probe) == NULL); // no files fieldset
ASSERT(fetch(&mgr2, buf, url2, "GET /fs/files/test.txt HTTP/1.0\n\n") == 404);
ASSERT(fetch(&mgr2, buf, url2,
"POST /fs/files/test.txt HTTP/1.0\nContent-Length: 5\n\nhello") ==
404);
mg_mgr_free(&mgr2);
}
// Test files fieldset with get_dir: /fs/ endpoints active
{
const char *url3 = "http://localhost:26354";
struct mg_mgr mgr3;
struct mg_dash dash3;
static int idx3;
static struct mg_field ff3[] = {
{"name", MG_VAL_STR, s_delete_file.name, sizeof(s_delete_file.name)},
{"size", MG_VAL_UINT64, &s_delete_file.size, 0},
{NULL, MG_VAL_INT, NULL, 0},
};
static struct mg_field_set fset3 = {"files", ff3, delete_file_fn, &idx3,
test_upload_dir, NULL};
memset(&dash3, 0, sizeof(dash3));
s_deleted_file[0] = '\0';
s_uploaded_file[0] = '\0';
s_delete_file_set = &fset3;
MG_DASH_ADD_FIELD_SET(&dash3, &fset3);
mg_mgr_init(&mgr3);
mg_mem_files = mg_packed_files;
ASSERT(mg_http_listen(&mgr3, url3, mg_dash_ev_handler, &dash3) != NULL);
ASSERT(fetch(&mgr3, buf, url3, "GET /api/get HTTP/1.0\n\n") == 200);
ASSERT(strstr(buf, files_probe) != NULL); // files fieldset present
ASSERT(fetch(&mgr3, buf, url3,
"DELETE /fs/files/nofile.txt HTTP/1.0\n\n") == 200);
ASSERT(strcmp(s_deleted_file, "nofile.txt") == 0);
ASSERT(fetch(&mgr3, buf, url3,
"DELETE /fs/files/..%2Fevil.txt HTTP/1.0\n\n") == 400);
ASSERT(strcmp(s_deleted_file, "nofile.txt") == 0);
ASSERT(fetch(&mgr3, buf, url3,
"PUT /fs/files/test.bin HTTP/1.0\nContent-Length: 4\n\ndata") ==
200);
ASSERT(strcmp(s_uploaded_file, "test.bin") == 0);
ASSERT(fetch(&mgr3, buf, url3,
"PUT /fs/files/..%2Fevil.bin HTTP/1.0\nContent-Length: "
"4\n\ndata") == 400);
ASSERT(strcmp(s_uploaded_file, "test.bin") == 0);
ASSERT(fetch(&mgr3, buf, url3, "GET /api/get/files/0 HTTP/1.0\n\n") == 200);
ASSERT(strstr(buf, test_bin_probe) != NULL);
mg_mgr_free(&mgr3);
}
}
#define DASHBOARD(x) \
@@ -5,7 +5,7 @@ TOP ?= ../../..
SOURCES = dashboard.c file_data.c $(TOP)/mongoose.c
CFLAGS_EXTRA ?= -DMG_TLS=MG_TLS_BUILTIN
CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -O0 $(CFLAGS_EXTRA)
PFLAGS = -I$(TOP) -DMG_DATA_SIZE=64 -DMG_ENABLE_LINES=1 -DMAIN
PFLAGS = -I$(TOP) -DMG_DATA_SIZE=64 -DMG_ENABLE_LINES=1 -DMAIN -DMG_DASH_STREAM_BATCH=100
AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment
all: $(PROG)
@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2026 Cesanta Software Limited
// SPDX-License-Identifier: GPL-2.0-only or commercial
#include "mongoose.h"
// Array elements are stored in a dynamically reallocated array
struct entry {
char text[20]; // Modifiable via GUI
bool checked; // Modifiable via GUI
};
static struct entry *s_arr; // Backing storage, grown/shrunk via realloc()
static size_t s_count; // Number of elements in s_arr
static struct entry s_entry; // Staging area: holds values for read/write/add
static int s_index; // Current element index, see struct mg_field_set
static struct mg_field fields_arr[] = {
{"index", MG_VAL_INT, &s_index, sizeof(s_index)},
{"text", MG_VAL_STR, &s_entry.text, sizeof(s_entry.text)},
{"checked", MG_VAL_BOOL, &s_entry.checked, sizeof(s_entry.checked)},
{NULL, MG_VAL_INT, NULL, 0},
};
// Grow s_arr by one slot and append the staged entry. False on OOM
static bool entry_append(void) {
struct entry *arr =
(struct entry *) realloc(s_arr, (s_count + 1) * sizeof(*arr));
if (arr == NULL) return false;
s_arr = arr;
s_arr[s_count++] = s_entry;
return true;
}
// Remove the entry at the given index, shifting the tail down and shrinking
// s_arr by one slot. False if the index is out of range
static bool entry_remove(int index) {
if (index < 0 || (size_t) index >= s_count) return false;
memmove(&s_arr[index], &s_arr[index + 1],
(s_count - (size_t) index - 1) * sizeof(*s_arr));
s_count--;
if (s_count == 0) {
free(s_arr);
s_arr = NULL;
} else {
struct entry *arr = (struct entry *) realloc(s_arr, s_count * sizeof(*arr));
if (arr != NULL) s_arr = arr;
}
return true;
}
// Read, write, delete or append an array element. The framework sets s_index
// before the call: a negative value asks for the total count, an
// out-of-range value means "end of iteration" - both are answered by
// setting s_index accordingly. MG_DASH_ADD builds a new entry from the
// staged fields and appends it regardless of s_index
static bool arr_fn(enum mg_dash_op op, struct mg_dash_user *u) {
(void) u;
if (op == MG_DASH_READ) {
if (s_index < 0) {
s_index = (int) s_count;
} else if ((size_t) s_index >= s_count) {
s_index = -1;
} else {
s_entry = s_arr[s_index];
}
return true;
}
if (op == MG_DASH_WRITE && s_index >= 0 && (size_t) s_index < s_count) {
s_arr[s_index] = s_entry;
return true;
}
if (op == MG_DASH_DELETE) return entry_remove(s_index);
if (op == MG_DASH_ADD) return entry_append();
return false;
}
static struct mg_field_set field_set_arr = {
"event", fields_arr, arr_fn, &s_index, NULL, NULL,
};
void mg_dash_init(struct mg_mgr *mgr) {
static struct mg_dash dash; // Important: keep it static!
static const char *demo[] = {"Wake up", "Make coffee", "Write code",
"Run tests", "Ship it", "Relax", "Sleep"};
size_t i;
for (i = 0; i < sizeof(demo) / sizeof(demo[0]); i++) {
mg_snprintf(s_entry.text, sizeof(s_entry.text), "%s", demo[i]);
s_entry.checked = (i == 0);
entry_append();
}
MG_DASH_ADD_FIELD_SET(&dash, &field_set_arr);
mg_mem_files = mg_packed_files;
mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &dash);
}
void mg_dash_poll(struct mg_mgr *mgr) {
(void) mgr;
}
// On desktop, build with -DMAIN flag to include main().
// On embedded system, run this code in your main() function
#ifdef MAIN
int main(void) {
struct mg_mgr mgr;
mg_mgr_init(&mgr);
mg_dash_init(&mgr);
for (;;) {
mg_mgr_poll(&mgr, 1);
mg_dash_poll(&mgr);
}
return 0;
}
#endif
@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Array Dashboard</title>
<script src="https://mongoose.ws/resources/dashboard.js"></script>
<script>
let pageStart = 0;
function getPageSize() { return parseInt(document.getElementById('page-size').value) || 5; }
function loadPage(start) {
pageStart = Math.max(0, start);
Dashboard.call('get', { name: 'event', start: pageStart, size: getPageSize() });
}
// PATCH one element, then reload the current page to show the result
function modifyEntry(index, patch) {
return fetch(`api/get/event/${index}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
}).then(() => loadPage(pageStart));
}
// Delete one element, then reload the current page
function removeEntry(index) {
return fetch(`api/del/event/${index}`, { method: 'DELETE' })
.then(() => loadPage(pageStart));
}
// Append a new element from the "add" form, then reload the current page
function addEntry() {
const text = document.getElementById('new-text').value;
const checked = document.getElementById('new-checked').checked;
return fetch('api/add/event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, checked }),
}).then(() => {
document.getElementById('new-text').value = '';
document.getElementById('new-checked').checked = false;
loadPage(pageStart);
});
}
Dashboard.init({
debug: true,
data: { event: { start: 0, end: 100, data: [] } },
});
Dashboard.on('ready', () => {
document.body.style.visibility = 'visible';
loadPage(0);
});
</script>
<style>
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; background:#f8fafc; font:500 14px/1.35 Arial,sans-serif; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; height: 2rem; padding: 0.3rem 1rem; font-size: 0.85rem; font-family: inherit; font-weight: 600; border-radius: 0.5rem; border: 1px solid transparent; cursor: pointer; transition: all 0.15s ease; white-space: nowrap; }
.btn-secondary { color: #64748b; background: #ffffff; border-color: #e2e8f0; }
.btn-secondary:hover { background: #f1f5f9; border-color: #cbd5e1; color: #475569; }
.btn-primary { color: #ffffff; background: linear-gradient(160deg, #38bdf8, #0ea5e9); border-color: #0284c7; }
.btn-primary:hover { background: linear-gradient(160deg, #0ea5e9, #0284c7); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
.btn-icon { width: 2rem; padding: 0.3rem; }
.toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 0.75rem; margin: 1.25rem 0; }
input[type=text] { width: 4rem; padding: 0.3rem 0.5rem; font-size: 0.85rem; font-family: inherit; border: 1px solid #e2e8f0; border-radius: 0.5rem; outline: none; }
input[type=text]:focus { border-color: #7dd3fc; }
.toggle { position: relative; width: 2.75rem; height: 1.6rem; margin: 0; vertical-align: middle; border: 1px solid transparent; background: #d1d5db; border-radius: 0.8rem; outline: none; cursor: pointer; -webkit-appearance: none; appearance: none; transition: background 0.2s ease; }
.toggle::after { content: ""; position: absolute; left: 2px; top: 50%; width: 1.3rem; height: 1.3rem; background: #fff; border-radius: 50%; box-shadow: 0 1px 2px rgba(0,0,0,0.25); transform: translateY(-50%) translateX(0); transition: transform 0.2s ease; }
.toggle:checked { background: #10b981; }
.toggle:checked::after { transform: translateY(-50%) translateX(calc(100% - 4px)); }
.toggle:focus-visible { box-shadow: 0 0 0 3px rgba(16,185,129,0.25); }
.table-wrap { max-width: 44rem; overflow: auto; border: 1px solid #e2e8f0; border-radius: 0.5rem; background: #fff; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 0.65rem 0.85rem; border-bottom: 1px solid #e2e8f0; text-align: left; }
th { font-size: 0.75rem; text-transform: uppercase; color: #64748b; background: #f1f5f9; }
td:first-child { width: 7rem; color: #334155; font-variant-numeric: tabular-nums; }
tr:last-child td { border-bottom: 0; }
</style>
</head>
<body style="visibility: hidden;">
<main style="margin: 2rem; color: #64748b;">
<h1>Array read/write/add/delete</h1>
<div class="toolbar">
<button class="btn btn-secondary" onclick="loadPage(pageStart - getPageSize())">Prev</button>
<button class="btn btn-secondary" onclick="loadPage(pageStart + getPageSize())">Next</button>
<button class="btn btn-primary" onclick="loadPage(pageStart)">Reload</button>
<label>Page size: <input id="page-size" type="text" value="5" /></label>
<span>${(event.start ?? 0) + 1}–${(event.start ?? 0) + event.data.length} of ${event.size ?? 0}</span>
</div>
<div class="table-wrap" id="entries">
<table>
<thead><tr><th>Index</th><th>Text</th><th>Done</th><th></th></tr></thead>
<tbody data-repeat="event.data" data-iterator="ev">
<tr>
<td>${ev.index}</td>
<td><input type="text" style="width: 100%;" value="${ev.text}" onchange="modifyEntry(${ev.index}, {text: this.value})" /></td>
<td><input type="checkbox" class="toggle" data-checked="ev.checked" onchange="modifyEntry(${ev.index}, {checked: this.checked})" /></td>
<td>
<button class="btn btn-secondary btn-icon" onclick="removeEntry(${ev.index})" title="Delete" aria-label="Delete">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 6h18" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<line x1="10" y1="11" x2="10" y2="17" />
<line x1="14" y1="11" x2="14" y2="17" />
</svg>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="toolbar">
<input id="new-text" type="text" style="width: 12rem;" placeholder="New entry text" />
<label><input id="new-checked" type="checkbox" class="toggle" /> Done</label>
<button class="btn btn-primary" onclick="addEntry()">Add</button>
</div>
</main>
</body>
</html>
@@ -1,59 +0,0 @@
// SPDX-FileCopyrightText: 2026 Cesanta Software Limited
// SPDX-License-Identifier: GPL-2.0-only or commercial
#include "mongoose.h"
struct event {
int index;
char message[100];
};
static struct event s_event;
static void read_event(void) {
mg_snprintf(s_event.message, sizeof(s_event.message), "my ev %d",
s_event.index);
}
static void write_event(void) {
// Do nothing. Dashboard sets the s_event.index
}
static struct mg_field fields_event[] = {
{"index", MG_VAL_INT, &s_event.index, sizeof(s_event.index)},
{"message", MG_VAL_STR, &s_event.message, sizeof(s_event.message)},
{NULL, MG_VAL_INT, NULL, 0},
};
static struct mg_field_set field_set_event = {
"event", fields_event, read_event, write_event, 0, 0, NULL,
};
void mg_dash_init(struct mg_mgr *mgr) {
static struct mg_dash dash; // Important: keep it static!
MG_DASH_ADD_FIELD_SET(&dash, &field_set_event);
mg_mem_files = mg_packed_files;
mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &dash);
}
void mg_dash_poll(struct mg_mgr *mgr) {
(void) mgr;
}
// On desktop, build with -DMAIN flag to include main().
// On embedded system, run this code in your main() function
#ifdef MAIN
int main(void) {
struct mg_mgr mgr;
mg_mgr_init(&mgr);
mg_dash_init(&mgr);
for (;;) {
mg_mgr_poll(&mgr, 1);
mg_dash_poll(&mgr);
}
return 0;
}
#endif
@@ -1,135 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Array Dashboard</title>
<script src="https://mongoose.ws/resources/dashboard.js"></script>
<script>
Dashboard.init({
debug: true,
data: { event: { index: 0, message: 'hi' } },
});
let pageStart = 0;
let loading = false;
let eventReadQueue = Promise.resolve();
async function readEvent(index) {
eventReadQueue = eventReadQueue.then(async () => {
await Dashboard.call('set', { event: { index } });
const ev = await Dashboard.call('get', 'event');
return ev && typeof ev == 'object' ? ev : { index, message: `my ev ${index}` };
});
return eventReadQueue;
}
function setLoading(value) {
loading = value;
document.querySelectorAll('[data-page-button]').forEach(btn => btn.disabled = value);
}
async function loadRows(start) {
if (loading) return;
const count = Number(document.querySelector('#page-size').value) || 10;
const tbody = document.querySelector('#events tbody');
const status = document.querySelector('#table-status');
pageStart = Math.max(0, start);
tbody.replaceChildren();
status.textContent = `Loading ${pageStart}..${pageStart + count - 1}`;
setLoading(true);
try {
const rows = await Promise.all(Array.from({ length: count }, (_, i) =>
readEvent(pageStart + i)));
const frag = document.createDocumentFragment();
rows.forEach(ev => {
const tr = document.createElement('tr');
const tdIndex = document.createElement('td');
const tdMessage = document.createElement('td');
tdIndex.textContent = ev.index;
tdMessage.textContent = ev.message;
tr.append(tdIndex, tdMessage);
frag.appendChild(tr);
});
tbody.replaceChildren(frag);
status.textContent = `Showing ${pageStart}..${pageStart + count - 1}`;
} catch (err) {
status.textContent = `Error: ${err && err.message ? err.message : err}`;
} finally {
setLoading(false);
}
}
// Show dashboard only when all device data is reported
Dashboard.on('ready', () => {
document.body.style.visibility = 'visible';
loadRows(0);
});
</script>
<style>
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; background:#f8fafc; font:500 14px/1.35 Arial,sans-serif; }
code {white-space: nowrap; background: #ddd; border-radius: 4px; padding: 1px 4px;}
.field { display: flex; flex-direction: row; justify-content: space-between; align-items: center; gap: 0.75rem; }
.field-label { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: #94a3b8; flex-shrink: 0; white-space: nowrap; min-width: 3rem; }
.field-input { flex: 1; min-width: 0; padding: 0.45rem 0.75rem; font-size: 0.88rem; font-family: inherit; color: #334155; border: 1px solid #e2e8f0; border-radius: 0.55rem; outline: none; transition: border-color 0.15s ease; }
.field-input.edited { border-color: #00500040 !important; background: #00ff0020; }
.field-input.error { border-color: #50000040; background: #ff000020; }
.field-input::placeholder { color: #cbd5e1; }
.field-input:hover { border-color: #cbd5e1; }
.field-input:focus { border-color: #7dd3fc; }
.field-input:disabled, .field-input.disabled { color: #cbd5e1; border-color: #e2e8f0; cursor: not-allowed; pointer-events: none; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; height: 2rem; padding: 0.3rem 1rem; font-size: 0.85rem; font-family: inherit; font-weight: 600; border-radius: 0.5rem; border: 1px solid transparent; cursor: pointer; transition: all 0.15s ease; white-space: nowrap; }
.btn-secondary { color: #64748b; background: #ffffff; border-color: #e2e8f0; box-shadow: 0 1px 2px rgba(148, 163, 184, 0.1); }
.btn-secondary:hover { background: #f1f5f9; border-color: #cbd5e1; color: #475569; }
.btn-primary { color: #ffffff; background: linear-gradient(160deg, #38bdf8, #0ea5e9); border-color: #0284c7; box-shadow: 0 1px 3px rgba(14, 165, 233, 0.3), 0 4px 10px rgba(14, 165, 233, 0.15); }
.btn-primary:hover { background: linear-gradient(160deg, #0ea5e9, #0284c7); box-shadow: 0 1px 3px rgba(14, 165, 233, 0.4), 0 6px 14px rgba(14, 165, 233, 0.2); }
.btn-primary:active, .btn-secondary:active { transform: translateY(0); }
.btn:disabled, .btn.disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; box-shadow: none; transform: none; }
.toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 0.75rem; margin: 1.25rem 0; }
.table-wrap { max-width: 44rem; overflow: auto; border: 1px solid #e2e8f0; border-radius: 0.5rem; background: #fff; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 0.65rem 0.85rem; border-bottom: 1px solid #e2e8f0; text-align: left; }
th { font-size: 0.75rem; text-transform: uppercase; color: #64748b; background: #f1f5f9; }
td:first-child { width: 7rem; color: #334155; font-variant-numeric: tabular-nums; }
tr:last-child td { border-bottom: 0; }
#table-status { min-width: 12rem; color: #64748b; }
</style>
</head>
<body style="visibility: hidden;">
<main id="main" style="margin: 2rem; color: #64748b;">
<h1>Array read/write</h1>
<div class="toolbar">
<button class="btn btn-secondary" data-page-button onclick="loadRows(pageStart - Number(document.querySelector('#page-size').value || 10))">Prev</button>
<button class="btn btn-secondary" data-page-button onclick="loadRows(pageStart + Number(document.querySelector('#page-size').value || 10))">Next</button>
<div class="field" style="width: 9rem;">
Rows
<input id="page-size" class="field-input" type="text" value="10" />
</div>
<button class="btn btn-primary" data-page-button onclick="loadRows(pageStart)">Reload</button>
<span id="table-status"></span>
</div>
<div class="table-wrap">
<table id="events">
<thead>
<tr>
<th>Index</th>
<th>Message</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</main>
</body>
</html>
+96 -43
View File
@@ -9,33 +9,32 @@ static int authenticate(char *user, size_t userlen, const char *pass);
// Action buttons
static bool s_action1, s_action2;
static uint64_t s_action2_timeout;
static void write_actions(void) {
if (s_action2 == true) s_action2_timeout = mg_now() + 750;
}
static void read_actions(void) {
if (s_action2_timeout > mg_now()) s_action2 = true;
static bool actions_fn(enum mg_dash_op op, struct mg_dash_user *u) {
(void) u;
if (op == MG_DASH_READ) {
if (s_action2_timeout > mg_now()) s_action2 = true;
return true;
}
if (op == MG_DASH_WRITE) {
if (s_action2 == true) s_action2_timeout = mg_now() + 750;
return true;
}
return false;
}
static struct mg_field fields_actions[] = {
{"action1", MG_VAL_BOOL, &s_action1, sizeof(s_action1)},
{"action2", MG_VAL_BOOL, &s_action2, sizeof(s_action2)},
{NULL, MG_VAL_INT, NULL, 0},
};
static struct mg_field_set set_actions = {
"actions", fields_actions, read_actions, write_actions, 0, 0, NULL,
};
static struct mg_field_set set_actions = {"actions", fields_actions, actions_fn,
NULL, NULL, NULL};
// Control panel
// s_led1, s_led2, s_led3 are used to communicate LED status
static bool s_led1, s_led2, s_led3;
static void write_leds(void) {
// gpio_write(LED1_PIN, s_led1);
// gpio_write(LED2_PIN, s_led2);
// gpio_write(LED3_PIN, s_led3);
}
static void read_leds(void) {
// s_led1 = gpio_read(LED1_PIN);
// s_led2 = gpio_read(LED2_PIN);
// s_led3 = gpio_read(LED3_PIN);
static bool leds_fn(enum mg_dash_op op, struct mg_dash_user *u) {
(void) u;
return op == MG_DASH_READ || op == MG_DASH_WRITE;
}
static struct mg_field fields_leds[] = {
{"led1", MG_VAL_BOOL, &s_led1, sizeof(s_led1)},
@@ -43,18 +42,20 @@ static struct mg_field fields_leds[] = {
{"led3", MG_VAL_BOOL, &s_led3, sizeof(s_led3)},
{NULL, MG_VAL_INT, NULL, 0},
};
static struct mg_field_set set_leds = {
"leds", fields_leds, read_leds, write_leds, 0, 0, NULL,
};
static struct mg_field_set set_leds = {"leds", fields_leds, leds_fn,
NULL, NULL, NULL};
// Read-only device Metrics
static int s_ram = 32, s_cpu = 7;
static double s_temperature = 24.8;
static void read_metrics(void) {
s_ram = 25 + (rand() % 16); // Sumulate metric change
static bool metrics_fn(enum mg_dash_op op, struct mg_dash_user *u) {
(void) u;
if (op != MG_DASH_READ) return false;
s_ram = 25 + (rand() % 16);
s_cpu = 7 + (rand() % 21);
s_temperature = 14.8 + ((double) rand() / RAND_MAX) * 20.0;
return true;
}
static struct mg_field fields_metrics[] = {
@@ -64,9 +65,8 @@ static struct mg_field fields_metrics[] = {
{NULL, MG_VAL_INT, NULL, 0},
};
static struct mg_field_set set_metrics = {
"metrics", fields_metrics, read_metrics, NULL, 0, 0, NULL,
};
static struct mg_field_set set_metrics = {"metrics", fields_metrics, metrics_fn,
NULL, NULL, NULL};
// Read-write device settings
static bool s_enable_login = false;
@@ -78,13 +78,18 @@ static char s_ota_version[] = "1.1.2";
static char s_ota_status[40] = "No scans yet";
static char s_ota_url[100] = "https://my-product.com/ota.json";
static void write_settings(void) {
mg_log_level = s_log_level;
s_dash.authenticate = s_enable_login ? authenticate : NULL;
}
static void read_settings(void) {
s_log_level = mg_log_level;
static bool settings_fn(enum mg_dash_op op, struct mg_dash_user *u) {
if (u->level < 3) return false;
if (op == MG_DASH_READ) {
s_log_level = mg_log_level;
return true;
}
if (op == MG_DASH_WRITE && u->level >= 7) {
mg_log_level = s_log_level;
s_dash.authenticate = s_enable_login ? authenticate : NULL;
return true;
}
return false;
}
static struct mg_field fields_settings[] = {
@@ -100,8 +105,7 @@ static struct mg_field fields_settings[] = {
};
static struct mg_field_set set_settings = {
"settings", fields_settings, read_settings, write_settings, 3, 7, NULL,
};
"settings", fields_settings, settings_fn, NULL, NULL, NULL};
#define NUM_POINTS_GRAPH1 7 // How many graph1 data points to send
@@ -139,9 +143,14 @@ static struct mg_field fields_graph1[] = {
{NULL, MG_VAL_INT, NULL, 0},
};
static struct mg_field_set set_graph1 = {
"graph1", fields_graph1, read_graph1, NULL, 0, 0, NULL,
};
static bool graph1_fn(enum mg_dash_op op, struct mg_dash_user *u) {
(void) u;
if (op != MG_DASH_READ) return false;
read_graph1();
return true;
}
static struct mg_field_set set_graph1 = {"graph1", fields_graph1, graph1_fn,
NULL, NULL, NULL};
#define NUM_POINTS_GRAPH2 100 // How many graph2 data points to send
static char s_graph2_data[NUM_POINTS_GRAPH2 * 4 + 2 + 1];
@@ -172,9 +181,13 @@ static struct mg_field fields_graph2[] = {
{NULL, MG_VAL_INT, NULL, 0},
};
static struct mg_field_set set_graph2 = {
"graph2", fields_graph2, read_graph2, NULL, 0, 0, NULL,
};
static bool graph2_fn(enum mg_dash_op op, struct mg_dash_user *u) {
(void) u;
if (op == MG_DASH_READ) read_graph2();
return op == MG_DASH_READ || op == MG_DASH_WRITE;
}
static struct mg_field_set set_graph2 = {"graph2", fields_graph2, graph2_fn,
NULL, NULL, NULL};
static int authenticate(char *user, size_t userlen, const char *pass) {
int level = 0; // Authentication failure
@@ -188,7 +201,42 @@ static int authenticate(char *user, size_t userlen, const char *pass) {
return level;
}
static struct file {
int index;
char name[64];
size_t size;
uint64_t checksum;
} s_file;
static struct mg_field_set set_files;
static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) {
(void) u;
mg_snprintf(buf, len, "%s", "/tmp/dashboard");
return true;
}
static struct mg_field fields_files[] = {
{"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)},
{"size", MG_VAL_UINT64, &s_file.size, 0},
{"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)},
{NULL, MG_VAL_INT, NULL, 0},
};
// Custom reader: let the framework fill "name" and "size", then add our
// own field. A real implementation would hash the file instead of
// hardcoding zero
static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) {
if (op != MG_DASH_READ) return false;
if (!mg_dash_dir_read(&set_files, u)) return false;
s_file.checksum = 0;
return true;
}
static struct mg_field_set set_files = {"files", fields_files, files_fn,
&s_file.index, get_dir, NULL};
void mg_dash_init(struct mg_mgr *mgr) {
MG_DASH_ADD_FIELD_SET(&s_dash, &set_files);
MG_DASH_ADD_FIELD_SET(&s_dash, &set_leds);
MG_DASH_ADD_FIELD_SET(&s_dash, &set_metrics);
MG_DASH_ADD_FIELD_SET(&s_dash, &set_settings);
@@ -196,9 +244,14 @@ void mg_dash_init(struct mg_mgr *mgr) {
MG_DASH_ADD_FIELD_SET(&s_dash, &set_graph2);
MG_DASH_ADD_FIELD_SET(&s_dash, &set_actions);
// Add two fake files - for demonstration
mg_dash_file_add(mg_str("device-config.json"), 1234);
mg_dash_file_add(mg_str("device-log-2026-04-25.txt"), 1327854);
#if MG_ARCH == MG_ARCH_UNIX
// Create demo files in the upload directory
mkdir("/tmp/dashboard", 0755);
mg_file_printf(&mg_fs_posix, "/tmp/dashboard/device-config.json",
"{\"model\":\"example\",\"fw\":\"1.0\"}");
mg_file_printf(&mg_fs_posix, "/tmp/dashboard/device-log-2026-04-25.txt",
"2026-04-25 12:00:00 Device started\n");
#endif
mg_mem_files = mg_packed_files;
mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &s_dash);
@@ -16,7 +16,7 @@
leds: { led1: true, led2: false, led3: false },
settings: { volume: 12, log_level: 3, name: 'Galway', enable_login: true, ota_version: '1.2.3', ota_status: 'No checks yet', ota_url: 'http://my-product.com/ota.json', ota_interval: 30 },
metrics: { temperature: 1.2, cpu: 4, ram: 5, version: '1.2.3' },
files: {data: [{name: 'device-log-2026-04-25.txt', size: 8625563}]},
files: {start: 0, end: 100, data: [{name: 'device-log-2026-04-25.txt', size: 8625563}]},
},
});
</script>
@@ -170,10 +170,12 @@
]);
};
// Intercept graph change notifications, re-plot data
// Intercept change notifications: re-plot graphs, reload the file
// list whenever its size changes (file uploaded or deleted)
Dashboard.on('change', args => {
args?.graph1?.data && updateGraph1(args?.graph1?.data);
args?.graph2?.data && updateGraph2(args?.graph2?.data);
typeof args?.files === 'number' && Dashboard.call('get', { name: 'files', start: 0, end: 100 });
});
});
</script>
@@ -689,7 +691,7 @@ a, a:visited, a:link { text-decoration: none; }
<div class="panel-title">
<span>
<span>File Manager</span>
<span class="metric-status">${files.data.length} Files</span>
<span class="metric-status">${files.size ?? files.data.length} Files</span>
</span>
<div class="header-actions">
<label class="btn btn-primary">
@@ -699,7 +701,7 @@ a, a:visited, a:link { text-decoration: none; }
<path d="M5 21h14" />
</svg>
Upload
<input type="file" hidden data-upload="1" />
<input type="file" hidden data-upload="files" />
</label>
</div>
</div>
@@ -717,14 +719,14 @@ a, a:visited, a:link { text-decoration: none; }
</div>
<div class="file-size">${f.size}</div>
<div class="file-actions">
<a href="fs/${encodeURIComponent(f.name)}" target="_blank" class="btn btn-secondary">
<a href="fs/files/${encodeURIComponent(f.name)}" target="_blank" class="btn btn-secondary">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3v12" />
<path d="m7 10 5 5 5-5" />
<path d="M5 21h14" />
</svg>
</a>
<button onclick="fetch('fs/${encodeURIComponent(f.name)}', {method: 'DELETE'}); return false;" class="btn btn-secondary">
<button onclick="fetch('fs/files/${encodeURIComponent(f.name)}', {method: 'DELETE'}); return false;" class="btn btn-secondary">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18" />
<path d="M8 6V4h8v2" />
+16 -14
View File
@@ -6,23 +6,25 @@
static bool s_led1;
static char s_version[10];
static void read_status(void) {
MG_INFO(("READING STATUS"));
mg_snprintf(s_version, sizeof(s_version), "1.2.3");
// These conditionals make this file work on both microcontroller and desktop
// Useful for developing the UI on desktop, and copying to an MCU project.
static bool status_fn(enum mg_dash_op op, struct mg_dash_user *u) {
(void) u;
if (op == MG_DASH_READ) {
MG_INFO(("READING STATUS"));
mg_snprintf(s_version, sizeof(s_version), "1.2.3");
// These conditionals make this file work on both microcontroller and desktop
#if MG_ARCH == MG_ARCH_CUBE
s_led1 = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); // Read LED state into field
s_led1 = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
#endif
}
static void write_status(void) {
MG_INFO(("WRITING STATUS"));
return true;
}
if (op == MG_DASH_WRITE) {
MG_INFO(("WRITING STATUS"));
#if MG_ARCH == MG_ARCH_CUBE
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, s_led1); // Write LED state from field
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, s_led1);
#endif
return true;
}
return false;
}
static struct mg_field fields_status[] = {
@@ -32,7 +34,7 @@ static struct mg_field fields_status[] = {
};
static struct mg_field_set field_set_status = {
"status", fields_status, read_status, write_status, 0, 0, NULL,
"status", fields_status, status_fn, NULL, NULL, NULL,
};
void mg_dash_init(struct mg_mgr *mgr) {
@@ -44,7 +44,7 @@
<div>
This toggle button is a stylised checkbox.
<code>data-bind="status.led1"</code> binds it to the device variable.
<code>data-autosave="1"</code> makes it send <code>{"led1": VALUE}</code> to the device every time the user toggles the
<code>data-autosave="1"</code> makes it send <code>{"status":{"led1": VALUE}}</code> to the device every time the user toggles the
button. On the device side, <code>read_status()</code> or <code>write_status()</code> are called every time
the LED state is read or written.
</div>