mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-15 10:29:47 +00:00
Dash OTA dogfooding nits
This commit is contained in:
1 parent
73037880e6
commit
27459efc61
29 files changed
+2635
-1478
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user