Add CACHE_STATUS UART poll and dashboard live stream.
Combine cached accel and tap in one low-overhead master command for ~16 ms host polling. The dashboard uses a single live-stream toggle plus per-slave accel-stream controls; fix live_stream state so polling is not cleared every slow client refresh. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+58
-55
@@ -290,10 +290,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small px-3 pt-2 mb-0">
|
||||
Accel-Stream pro Slave per „Stream an“ aktivieren (~16 ms ESP-NOW). Tap-Notify (S/D/T)
|
||||
konfiguriert den Slave; „Empfang an“ startet das Abfragen von Tap-Events (~16 ms).
|
||||
<strong>Live-Stream</strong> startet die schnelle <code>CACHE_STATUS</code>-Abfrage (~16 ms).
|
||||
Pro Slave <strong>Accel</strong> aktiviert den ESP-NOW-Accel-Stream; Tap-Notify (S/D/T) steuert
|
||||
die Tap-Arten auf dem Slave.
|
||||
</p>
|
||||
<div class="px-3 pb-2 d-flex flex-wrap gap-2 align-items-center">
|
||||
<button type="button"
|
||||
class="btn btn-sm"
|
||||
:class="state.live_stream ? 'btn-warning' : 'btn-success'"
|
||||
@click="setLiveStream(!state.live_stream)"
|
||||
:disabled="busy || !state.uart_connected || !(state.clients || []).length"
|
||||
x-text="state.live_stream ? 'Live-Stream aus' : 'Live-Stream an'"></button>
|
||||
<span class="text-muted small">Tap alle Slaves:</span>
|
||||
<label class="tap-toggle"><input type="checkbox" x-model="allTapSingle" :disabled="busy"> S</label>
|
||||
<label class="tap-toggle"><input type="checkbox" x-model="allTapDouble" :disabled="busy"> D</label>
|
||||
@@ -316,7 +323,7 @@
|
||||
<th>Deadzone</th>
|
||||
<th>Accel (LSB)</th>
|
||||
<th>Akku</th>
|
||||
<th>Stream</th>
|
||||
<th>Accel</th>
|
||||
<th>Tap-Notify</th>
|
||||
<th>Tap</th>
|
||||
<th>Aktion</th>
|
||||
@@ -350,7 +357,8 @@
|
||||
: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>
|
||||
x-text="c.accel_stream ? 'Aus' : 'An'"
|
||||
title="ESP-NOW Accel-Stream auf Slave"></button>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1 align-items-center">
|
||||
@@ -375,16 +383,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1 align-items-center">
|
||||
<button type="button"
|
||||
class="btn btn-sm"
|
||||
:class="c.tap_receive ? 'btn-warning' : 'btn-outline-success'"
|
||||
@click="setTapReceive(c.id, !c.tap_receive)"
|
||||
:disabled="busy || !state.uart_connected || !c.available || !tapNotifyAny(c)"
|
||||
x-text="c.tap_receive ? 'Aus' : 'An'"
|
||||
title="Tap-Events vom Master abfragen"></button>
|
||||
<span :class="tapCellClass(c)" x-text="formatLastTap(c)" :title="tapTitle(c)"></span>
|
||||
</div>
|
||||
<span :class="tapCellClass(c)" x-text="formatLastTap(c)" :title="tapTitle(c)"></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1 align-items-center">
|
||||
@@ -779,12 +778,14 @@
|
||||
return t;
|
||||
},
|
||||
formatAccel(c) {
|
||||
if (!this.state?.live_stream) return '—';
|
||||
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 (!this.state?.live_stream) return 'Live-Stream aus — oben einschalten';
|
||||
if (!c?.accel_stream) return 'Accel-Stream für diesen Slave nicht aktiv';
|
||||
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 : ''}`;
|
||||
@@ -820,7 +821,7 @@
|
||||
return d;
|
||||
},
|
||||
formatLastTap(c) {
|
||||
if (!c?.tap_receive) return '—';
|
||||
if (!this.state?.live_stream) return '—';
|
||||
if (!this.tapNotifyAny(c)) return '—';
|
||||
const labels = { single: 'Single', double: 'Double', triple: 'Triple' };
|
||||
const d = this.activeTapDisplay(c);
|
||||
@@ -829,7 +830,7 @@
|
||||
return labels[c.last_tap] || c.last_tap;
|
||||
},
|
||||
tapTitle(c) {
|
||||
if (!c?.tap_receive) return 'Tap-Empfang aus — „An“ klicken';
|
||||
if (!this.state?.live_stream) return 'Live-Stream aus';
|
||||
if (!this.tapNotifyAny(c)) return 'Tap-Notify nicht konfiguriert (S/D/T)';
|
||||
const d = this.activeTapDisplay(c);
|
||||
if (!d && !c?.last_tap) return 'Warte auf Tap-Event…';
|
||||
@@ -1080,11 +1081,47 @@
|
||||
async setMasterDeadzone() {
|
||||
await this.setDeadzone(0, this.masterDz);
|
||||
},
|
||||
patchLiveStream(enabled) {
|
||||
let clients = this.state.clients || [];
|
||||
if (!enabled) {
|
||||
clients = clients.map((c) => ({
|
||||
...c,
|
||||
accel_valid: false,
|
||||
accel_x: 0,
|
||||
accel_y: 0,
|
||||
accel_z: 0,
|
||||
accel_age_ms: 0,
|
||||
last_tap: '',
|
||||
last_tap_at: 0
|
||||
}));
|
||||
this.tapDisplay = {};
|
||||
}
|
||||
this.state = { ...this.state, live_stream: enabled, clients };
|
||||
},
|
||||
async setLiveStream(enable) {
|
||||
this.busy = true;
|
||||
try {
|
||||
const r = await fetch('/api/live-stream', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enable })
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) {
|
||||
this.flash(data.error || 'Live-Stream fehlgeschlagen', false);
|
||||
return;
|
||||
}
|
||||
this.patchLiveStream(!!data.enabled);
|
||||
this.flash(`Live-Stream ${data.enabled ? 'an' : 'aus'}`, true);
|
||||
} catch (e) {
|
||||
this.flash(String(e), false);
|
||||
} finally {
|
||||
this.busy = false;
|
||||
}
|
||||
},
|
||||
patchClientAccelStream(clientId, enabled) {
|
||||
const clients = (this.state.clients || []).map((c) => {
|
||||
if (c.id !== clientId) {
|
||||
return c;
|
||||
}
|
||||
if (c.id !== clientId) return c;
|
||||
const next = { ...c, accel_stream: enabled };
|
||||
if (!enabled) {
|
||||
next.accel_valid = false;
|
||||
@@ -1103,7 +1140,7 @@
|
||||
const r = await fetch(`/api/clients/${clientId}/accel-stream`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enable: enable })
|
||||
body: JSON.stringify({ enable })
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) {
|
||||
@@ -1182,40 +1219,6 @@
|
||||
this.busy = false;
|
||||
}
|
||||
},
|
||||
patchClientTapReceive(clientId, enabled) {
|
||||
const clients = (this.state.clients || []).map((c) => {
|
||||
if (c.id !== clientId) return c;
|
||||
const next = { ...c, tap_receive: enabled };
|
||||
if (!enabled) {
|
||||
next.last_tap = '';
|
||||
next.last_tap_at = 0;
|
||||
delete this.tapDisplay[clientId];
|
||||
}
|
||||
return next;
|
||||
});
|
||||
this.state = { ...this.state, clients };
|
||||
},
|
||||
async setTapReceive(clientId, enable) {
|
||||
this.busy = true;
|
||||
try {
|
||||
const r = await fetch(`/api/clients/${clientId}/tap-receive`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enable })
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok || !data.success) {
|
||||
this.flash(data.error || `Tap-Empfang Slave ${clientId} fehlgeschlagen`, false);
|
||||
return;
|
||||
}
|
||||
this.patchClientTapReceive(clientId, !!data.enabled);
|
||||
this.flash(`Slave ${clientId}: Tap-Empfang ${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);
|
||||
|
||||
Reference in New Issue
Block a user