${} evaluations

This commit is contained in:
cpq
2026-04-20 06:39:50 +01:00
parent f74f450dd1
commit 85d0810cbc
7 changed files with 464 additions and 169 deletions
@@ -9,7 +9,7 @@
#define HTTP_ADDR "http://0.0.0.0:80"
#endif
#define NUM_POINTS 258 // How many data points to send
#define NUM_POINTS 7 // How many data points to send
struct point {
uint16_t x; // X coordinate of the data point, usually this is time
@@ -39,24 +39,69 @@ size_t print_points(void (*fn)(char, void *), void *arg, va_list *ap) {
n += mg_xprintf(fn, arg, "]");
return n;
}
static void get_points(union mg_val *val) {
val->fn = (void (*)(void)) print_points;
}
static bool s_led;
static void get_led(union mg_val *val) {
val->b = s_led;
// This function prints a JSON object with read-only metrics
size_t print_metrics(void (*fn)(char, void *), void *arg, va_list *ap) {
size_t n = 0;
(void) ap;
n += mg_xprintf(fn, arg, "{");
n += mg_xprintf(fn, arg, "%s%m:%g", " ", MG_ESC("temperature"), 24.8);
n += mg_xprintf(fn, arg, "%s%m:%d", ",", MG_ESC("humidity"), 59);
n += mg_xprintf(fn, arg, "%s%m:%d", ",", MG_ESC("cpu"), 37);
n += mg_xprintf(fn, arg, "%s%m:%d", ",", MG_ESC("ram"), 32);
n += mg_xprintf(fn, arg, "}");
return n;
}
static void get_metrics(union mg_val *val) {
val->fn = (void (*)(void)) print_metrics;
}
static void set_led(const union mg_val *val) {
s_led = val->b;
static bool s_motor = false;
static double s_volume = 17.2;
static char s_name[20] = "Dublin";
// This function prints a JSON object with read-only metrics
size_t print_settings(void (*fn)(char, void *), void *arg, va_list *ap) {
size_t n = 0;
(void) ap;
n += mg_xprintf(fn, arg, "{");
n += mg_xprintf(fn, arg, "%s%m:%g", " ", MG_ESC("volume"), s_volume);
n += mg_xprintf(fn, arg, "%s%m:%s", ",", MG_ESC("motor"), s_motor ? "true" : "false");
n += mg_xprintf(fn, arg, "%s%m:%m", ",", MG_ESC("name"), MG_ESC(s_name));
n += mg_xprintf(fn, arg, "}");
return n;
}
static void get_settings(union mg_val *val) {
val->fn = (void (*)(void)) print_settings;
}
static bool s_fan, s_relay1, s_relay2;
size_t print_controls(void (*fn)(char, void *), void *arg, va_list *ap) {
size_t n = 0;
(void) ap;
n += mg_xprintf(fn, arg, "{");
n += mg_xprintf(fn, arg, "%s%m:%s", " ", MG_ESC("fan"), s_fan ? "true" : "false");
n += mg_xprintf(fn, arg, "%s%m:%s", ",", MG_ESC("relay1"), s_relay1 ? "true" : "false");
n += mg_xprintf(fn, arg, "%s%m:%s", ",", MG_ESC("relay2"), s_relay2 ? "true" : "false");
n += mg_xprintf(fn, arg, "}");
return n;
}
static void set_controls(const union mg_val *val) {
mg_json_get_bool(val->s, "$.fan", &s_fan);
mg_json_get_bool(val->s, "$.relay1", &s_relay1);
mg_json_get_bool(val->s, "$.relay2", &s_relay2);
}
static void get_controls(union mg_val *val) {
val->fn = (void (*)(void)) print_controls;
}
// Modify this. This represents device's state to the dashboard
static struct mg_field fields[] = {
{"led", MG_VAL_BOOL, get_led, set_led},
{"controls", MG_VAL_FN, get_controls, set_controls},
{"settings", MG_VAL_FN, get_settings, NULL},
{"metrics", MG_VAL_FN, get_metrics, NULL},
{"points", MG_VAL_FN, get_points, NULL},
{NULL, 0, 0, 0},
};
@@ -11,12 +11,23 @@
Dashboard.init({
debug: true,
data: {
temperature: 25,
humidity: 60,
voltage: 12.5,
current: 4.2,
power: 52.5,
speed: 100,
points: [],
controls: {
fan: true,
relay1: false,
relay2: false,
},
settings: {
volume: 12,
motor: false,
name: 'Galway',
},
metrics: {
temperature: 1.2,
humidity: 3,
cpu: 4,
ram: 5,
},
},
});
</script>
@@ -90,35 +101,54 @@
<div class="overview-grid" aria-label="Device metrics overview">
<article class="metric-tile">
<div class="metric-label">Temperature</div>
<div class="metric-value">
<span class="metric-number">24.8</span>
<span class="metric-unit">°C</span>
<div class="metric-value-row">
<div class="metric-value">
<span class="metric-number" data-bind="metrics.temperature">24.8</span>
<span class="metric-unit">°C</span>
</div>
<span class="metric-status metric-status-good">ok</span>
</div>
<div class="metric-status metric-status-good">Stable — within target range</div>
</article>
<article class="metric-tile">
<div class="metric-label">Humidity</div>
<div class="metric-value">
<span class="metric-number">51</span>
<span class="metric-unit">%</span>
<div class="metric-value-row">
<div class="metric-value">
<span class="metric-number" data-bind="metrics.humidity">51</span>
<span class="metric-unit">%</span>
</div>
<span class="metric-status metric-status-good">ok</span>
</div>
<div class="metric-status metric-status-good">+2% in the last hour</div>
</article>
<article class="metric-tile">
<article class="metric-tile metric-tile-warning">
<div class="metric-label">CPU Load</div>
<div class="metric-value">
<span class="metric-number">37</span>
<span class="metric-unit">%</span>
<div class="metric-value-row">
<div class="metric-value">
<span class="metric-number" data-bind="metrics.cpu">37</span>
<span class="metric-unit">%</span>
</div>
<span class="metric-status metric-status-warn">moderate</span>
</div>
</article>
<article class="metric-tile metric-tile-alert">
<div class="metric-label">Free Memory</div>
<div class="metric-value-row">
<div class="metric-value">
<span class="metric-number">${metrics.ram}</span>
<span class="metric-unit">KB</span>
</div>
<span class="metric-status metric-status-alert" style="${metrics.ram <= 30 ? '': 'display:none;'}">low</span>
<span class="metric-status metric-status-warn" style="${metrics.ram <= 50 && metrics.ram > 30 ? '': 'display:none;'}">low</span>
</div>
<div class="metric-status metric-status-warn">Moderate load</div>
</article>
<article class="metric-tile">
<div class="metric-label">Free Memory</div>
<div class="metric-value">
<span class="metric-number">182</span>
<span class="metric-unit">KB</span>
<div class="metric-label">Volume</div>
<div class="metric-value-row">
<div class="metric-value">
<span class="metric-number" data-bind="settings.volume">18.7</span>
<span class="metric-unit">dB</span>
</div>
<span class="metric-status metric-status-good">ok</span>
</div>
<div class="metric-status metric-status-good">Healthy headroom</div>
</article>
</div>
@@ -147,37 +177,48 @@
<div class="control-name">Cooling Fan</div>
<div class="control-desc">PWM controlled output</div>
</div>
<label class="toggle">
<input type="checkbox" checked>
<span class="toggle-track"></span>
</label>
<input type="checkbox" data-bind="controls.fan" data-autosave="1" class="toggle" />
</div>
<div class="control-row">
<div class="control-info">
<div class="control-name">Relay Channel 1</div>
<div class="control-desc">Auxiliary load output</div>
</div>
<label class="toggle">
<input type="checkbox" checked>
<span class="toggle-track"></span>
</label>
<input type="checkbox" data-bind="controls.relay1" data-autosave="1" class="toggle" />
</div>
<div class="control-row">
<div class="control-info">
<div class="control-name">Relay Channel 2</div>
<div class="control-desc">Safety-controlled line</div>
</div>
<label class="toggle">
<input type="checkbox">
<span class="toggle-track"></span>
</label>
<input type="checkbox" data-bind="controls.relay2" data-autosave="1" class="toggle" />
</div>
</div>
</div>
</div>
</section>
<section id="settings" class="tab-panel" role="tabpanel" aria-labelledby="tab-settings" hidden></section>
<section id="settings" class="tab-panel" role="tabpanel" aria-labelledby="tab-settings" hidden>
<div class="settings-layout">
<div class="settings-panel">
<div class="settings-panel-title">Device settings</div>
<div class="settings-fields">
<div class="field">
<label class="field-label" for="setting-name">Name</label>
<input class="field-input" id="setting-name" type="text" data-bind="settings.name" />
</div>
<div class="field">
<label class="field-label" for="setting-volume">Volume</label>
<input class="field-input" id="setting-volume" type="text" data-bind="settings.volume" />
</div>
</div>
<div class="settings-panel-footer">
<button class="btn btn-secondary" type="button" data-cancel="settings">Cancel</button>
<button class="btn btn-primary" type="button" data-save="settings">Save</button>
</div>
</div>
</div>
</section>
<section id="firmware" class="tab-panel" role="tabpanel" aria-labelledby="tab-firmware" hidden></section>
<section id="events" class="tab-panel" role="tabpanel" aria-labelledby="tab-events" hidden></section>
</main>
@@ -57,6 +57,7 @@ document.addEventListener("DOMContentLoaded", () => {
const chart = new uPlot(options, [], el);
function updateGraphPoints(args) {
if (!args.points) return;
const data = [
args.points.map(x => x[0]),
args.points.map(x => x[1]),
@@ -139,10 +139,6 @@ body {
.tab-panel {
min-height: calc(100vh - 6rem);
/* border-radius: .5rem; */
/* background-color: #eee; */
/* border: 1px solid rgba(216, 224, 234, 0.78); */
/* padding: var(--spacing-4); */
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.72),
0 18px 40px rgba(148, 163, 184, 0.1);
@@ -150,7 +146,7 @@ body {
.overview-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(5, 1fr);
gap: 1rem;
}
@@ -185,6 +181,13 @@ body {
text-transform: uppercase;
}
.metric-value-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.metric-value {
display: flex;
align-items: baseline;
@@ -207,28 +210,208 @@ body {
}
.metric-status {
font-size: 0.78rem;
font-weight: 600;
margin-top: 0.1rem;
display: none;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
padding: 0.18rem 0.52rem;
border-radius: 0.4rem;
border: 1px solid transparent;
white-space: nowrap;
}
.metric-status-good {
color: #16a34a;
color: #166534;
background: #dcfce7;
border-color: #86efac;
}
.metric-status-warn {
color: #d97706;
color: #92400e;
background: #fef3c7;
border-color: #fcd34d;
}
.metric-status-alert {
color: #991b1b;
background: #fee2e2;
border-color: #fca5a5;
}
.metric-status-info {
color: #0369a1;
color: #075985;
background: #e0f2fe;
border-color: #7dd3fc;
}
.tab-panel[hidden] {
display: none;
}
/* ── Settings panel ─────────────────────────────── */
.settings-layout {
padding-top: 1rem;
max-width: 22rem;
}
.settings-panel {
background: #ffffff;
border: 1px solid rgba(203, 213, 225, 0.75);
border-radius: 1rem;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 1),
0 2px 6px rgba(148, 163, 184, 0.1),
0 8px 24px rgba(148, 163, 184, 0.08);
overflow: hidden;
}
.settings-panel-title {
font-size: 0.95rem;
font-weight: 700;
color: #334155;
padding: 1rem 1.25rem 0.9rem;
border-bottom: 1px solid #e2e8f0;
}
.settings-fields {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem 1.25rem;
}
.field {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.75rem;
}
.field-label {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #94a3b8;
width: 4.5rem;
flex-shrink: 0;
}
.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, .select.edited, .slider.edited>* {
border-color: #50500040;
background: #ffff0020;
}
.field-input.error, .select.error, .slider.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;
}
.select:disabled,
.select.disabled {
color: #cbd5e1;
border-color: #e2e8f0;
cursor: not-allowed;
pointer-events: none;
}
.settings-panel-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
}
.btn {
display: inline-flex;
align-items: center;
padding: 0.42rem 1rem;
font-size: 0.82rem;
font-family: inherit;
font-weight: 600;
border-radius: 0.55rem;
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);
/*transform: translateY(-1px);*/
}
.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;
}
/* ── Detail row (chart + controls) ─────────────────────────────── */
.detail-row {
@@ -360,57 +543,45 @@ body {
color: #7d94ac;
}
/* Toggle switch */
.toggle {
position: relative;
display: inline-block;
width: 3rem;
height: 1.65rem;
width: 2.75rem;
height: 1.6rem;
margin: 0;
vertical-align: top;
border: 1px solid transparent;
background: #d1d5db;
border-radius: 0.75rem;
outline: none;
cursor: pointer;
flex-shrink: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.toggle input {
.toggle::after {
content: "";
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.toggle-track {
position: absolute;
inset: 0;
border-radius: 999px;
background: #cbd5e1;
transition: background 0.22s ease;
}
.toggle-track::before {
content: '';
position: absolute;
width: 1.25rem;
height: 1.25rem;
left: 1px;
top: 1px;
width: 1.35rem;
height: 1.35rem;
background: #fff;
border-radius: 50%;
background: #ffffff;
top: 50%;
left: 0.2rem;
transform: translateY(-50%);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
transition: left 0.22s ease;
transition: all 0.3s;
}
.toggle input:checked + .toggle-track {
background: linear-gradient(135deg, #10b981, #06b6d4);
.toggle:checked {
background: #d1d5db;
}
.toggle input:checked + .toggle-track::before {
left: calc(100% - 1.45rem);
.toggle:checked::after {
transform: translateX(calc(100% - 3px));
}
.toggle input:focus-visible + .toggle-track {
outline: 2px solid #0ea5e9;
outline-offset: 2px;
.toggle:disabled,
.toggle.disabled {
background: #d1d5db;
cursor: not-allowed;
}
.toggle.edited {
border-color: #ee0;
}
@media (max-width: 900px) {