mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-01 05:52:32 +07:00
+1
-1
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user