Stream slave accel via ESP-NOW with master snapshot cache.

Slaves push BMA456 samples at 16ms when enabled; the master caches per
client and exposes ACCEL_SNAPSHOT and ACCEL_STREAM over UART. goTool adds
dashboard stream controls, HTTP accel-stream routes, and an external
WebSocket API with per-connection receive/interval and slave stream commands.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 19:11:36 +02:00
co-authored by Cursor
parent ba20544762
commit 47c75110c9
35 changed files with 2409 additions and 300 deletions
+76 -3
View File
@@ -55,11 +55,12 @@
.badge-offline { background: #5c6570; color: #f0f3f5; }
.badge.bg-secondary { background: #4a5560 !important; color: #f0f3f5; }
.mac {
.mac, .accel {
font-family: ui-monospace, monospace;
font-size: 0.85rem;
color: var(--pp-accent);
}
.accel-stale { color: var(--pp-text-muted); }
.pp-table {
--bs-table-color: var(--pp-text);
@@ -265,7 +266,9 @@
<span class="badge bg-secondary" x-text="(state.clients || []).length + ' registered'"></span>
</div>
</div>
<p class="text-muted small px-3 pt-2 mb-0">Slaves per ESP-NOW — Master-Deadzone bleibt separat.</p>
<p class="text-muted small px-3 pt-2 mb-0">
Accel-Stream pro Slave per „Stream an“ aktivieren (~16&nbsp;ms ESP-NOW). Ohne Aktivierung keine Werte.
</p>
<div class="card-body p-0 pt-2">
<div class="table-responsive">
<table class="table pp-table table-hover">
@@ -276,12 +279,14 @@
<th>Ver</th>
<th>Status</th>
<th>Deadzone</th>
<th>Accel (LSB)</th>
<th>Stream</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<template x-if="!(state.clients || []).length">
<tr><td colspan="6" class="text-muted text-center py-4">No clients</td></tr>
<tr><td colspan="8" class="text-muted text-center py-4">No clients</td></tr>
</template>
<template x-for="c in (state.clients || [])" :key="c.id + c.mac">
<tr>
@@ -294,6 +299,20 @@
x-text="c.available ? 'available' : 'inactive'"></span>
</td>
<td x-text="c.deadzone != null ? c.deadzone : '—'"></td>
<td>
<span class="accel"
:class="accelCellClass(c)"
x-text="formatAccel(c)"
:title="accelTitle(c)"></span>
</td>
<td>
<button type="button"
class="btn btn-sm"
:class="c.accel_stream ? 'btn-warning' : 'btn-outline-success'"
@click="setAccelStream(c.id, !c.accel_stream)"
:disabled="busy || !state.uart_connected || !c.available"
x-text="c.accel_stream ? 'Aus' : 'An'"></button>
</td>
<td>
<div class="d-flex flex-wrap gap-1 align-items-center">
<input type="number" class="form-control form-control-sm dz-input"
@@ -496,6 +515,22 @@
if (!hex || hex.length !== 12) return hex || '';
return hex.match(/.{2}/g).join(':');
},
formatAccel(c) {
if (!c?.accel_stream) return '—';
if (!c?.accel_valid) return '…';
return `${c.accel_x} / ${c.accel_y} / ${c.accel_z}`;
},
accelTitle(c) {
if (!c?.accel_stream) return 'Accel-Stream nicht aktiviert';
if (!c?.accel_valid) return 'Warte auf erste ESP-NOW Samples…';
const age = c.accel_age_ms != null ? `${c.accel_age_ms} ms alt` : '';
return `x=${c.accel_x} y=${c.accel_y} z=${c.accel_z} (raw LSB, ±2g)${age ? ' · ' + age : ''}`;
},
accelCellClass(c) {
if (!c?.accel_valid) return 'accel-stale';
if (c.accel_age_ms != null && c.accel_age_ms > 200) return 'accel-stale';
return '';
},
formatSize(n) {
if (n == null) return '';
if (n < 1024) return n + ' B';
@@ -732,6 +767,44 @@
async setMasterDeadzone() {
await this.setDeadzone(0, this.masterDz);
},
patchClientAccelStream(clientId, enabled) {
const clients = (this.state.clients || []).map((c) => {
if (c.id !== clientId) {
return c;
}
const next = { ...c, accel_stream: enabled };
if (!enabled) {
next.accel_valid = false;
next.accel_x = 0;
next.accel_y = 0;
next.accel_z = 0;
next.accel_age_ms = 0;
}
return next;
});
this.state = { ...this.state, clients };
},
async setAccelStream(clientId, enable) {
this.busy = true;
try {
const r = await fetch(`/api/clients/${clientId}/accel-stream`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enable: enable })
});
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || `Accel-Stream Slave ${clientId} fehlgeschlagen`, false);
return;
}
this.patchClientAccelStream(clientId, !!data.enabled);
this.flash(`Slave ${clientId}: Accel-Stream ${data.enabled ? 'an' : 'aus'}`, true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
async setDeadzoneAll(deadzone) {
if (deadzone == null || deadzone < 0) {
this.flash('Ungültiger Deadzone-Wert', false);