Merge pull request #3594 from cesanta/dash

Dash OTA dogfooding nits
This commit is contained in:
Sergey Lyubka
2026-06-09 12:20:50 +01:00
committed by GitHub
29 changed files with 2635 additions and 1478 deletions
@@ -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>