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

+28
View File
@@ -0,0 +1,28 @@
# Environment setup: https://mongoose.ws/docs/getting-started/build-environment/
PROG ?= example
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 -DMG_DASH_STREAM_BATCH=100
AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment
all: $(PROG)
./$(PROG)
# Rebuild the program
$(PROG): $(SOURCES) $(TOP)/mongoose.h
$(CC) $(SOURCES) $(CFLAGS) $(PFLAGS) $(AFLAGS) -o $@
# Create an embedded filesystem containing compressed dashboard.html
file_data.c: dashboard.html
node $(TOP)/resources/html2c.js dashboard.html -o file_data.c
@echo "On-flash size: `grep -oE '[0-9]+' file_data.c | wc -l`"
# For development: rebuild and restart if any of the sources change
watch:
node $(TOP)/resources/watch.js -w dashboard.c -w $(TOP)/mongoose.h -w dashboard.html -- make
clean:
rm -rf $(PROG) *.exe *.obj *.dSYM mongoose file_data.c
@@ -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>