Add LED ring control per client and broadcast over REST and WebSocket.

Solid color mode fills all ring LEDs; master routes UART commands to slaves
via ESPNOW_LED_RING. goTool exposes POST /api/led-ring, WebSocket set_led_ring,
and a dashboard LED panel with master/slave/all targets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 19:24:55 +02:00
co-authored by Cursor
parent 47c75110c9
commit eb67a46158
21 changed files with 759 additions and 133 deletions
+140 -4
View File
@@ -331,11 +331,17 @@
title="ESP-NOW Unicast-Test">
Test
</button>
<button type="button" class="btn btn-outline-warning btn-sm"
@click="findMe(c.id)"
<button type="button" class="btn btn-outline-info btn-sm"
@click="ledRing({ clientId: c.id })"
:disabled="busy || !state.uart_connected || !c.available"
title="LED-Ring Find me (ESP-NOW)">
Find me
title="LED-Ring (aktueller Modus)">
LED
</button>
<button type="button" class="btn btn-outline-warning btn-sm"
@click="ledRing({ clientId: c.id, mode: 'find-me' })"
:disabled="busy || !state.uart_connected || !c.available"
title="Find me">
Find
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
@click="restart(c.id)"
@@ -354,6 +360,82 @@
</div>
</section>
<section class="col-12">
<div class="card">
<div class="card-header">LED-Ring</div>
<div class="card-body">
<p class="text-muted small mb-3">
Modi: <code>clear</code>, <code>color</code> (ganzer Ring), <code>progress</code> (0100&nbsp;%),
<code>digit</code> (010), <code>blink</code>, <code>find-me</code>.
Ziel: Master (<code>client_id=0</code>), ein Slave oder alle Slaves (Broadcast).
</p>
<div class="row g-3 align-items-end">
<div class="col-md-2">
<label class="form-label small text-muted">Modus</label>
<select class="form-select form-select-sm" x-model="led.mode" :disabled="busy">
<option value="color">Farbe (alle LEDs)</option>
<option value="clear">Aus (clear)</option>
<option value="progress">Progress</option>
<option value="digit">Ziffer/Symbol</option>
<option value="blink">Blink</option>
<option value="find-me">Find me</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label small text-muted">RGB / Intensität</label>
<div class="d-flex flex-wrap gap-2">
<input type="number" class="form-control form-control-sm" style="width:4rem" min="0" max="255"
placeholder="R" x-model.number="led.r" :disabled="busy">
<input type="number" class="form-control form-control-sm" style="width:4rem" min="0" max="255"
placeholder="G" x-model.number="led.g" :disabled="busy">
<input type="number" class="form-control form-control-sm" style="width:4rem" min="0" max="255"
placeholder="B" x-model.number="led.b" :disabled="busy">
<input type="number" class="form-control form-control-sm" style="width:5rem" min="0" max="255"
title="0 = Geräte-Default (~5 %)"
placeholder="Int." x-model.number="led.intensity" :disabled="busy">
</div>
</div>
<div class="col-md-2" x-show="led.mode === 'progress'">
<label class="form-label small text-muted">Progress %</label>
<input type="number" class="form-control form-control-sm" min="0" max="100"
x-model.number="led.progress" :disabled="busy">
</div>
<div class="col-md-2" x-show="led.mode === 'digit'">
<label class="form-label small text-muted">Ziffer 010</label>
<input type="number" class="form-control form-control-sm" min="0" max="10"
x-model.number="led.digit" :disabled="busy">
</div>
<div class="col-md-2" x-show="led.mode === 'blink'">
<label class="form-label small text-muted">Blink ms × Anzahl</label>
<div class="d-flex gap-1">
<input type="number" class="form-control form-control-sm" min="1"
x-model.number="led.blinkMs" :disabled="busy">
<input type="number" class="form-control form-control-sm" min="1"
x-model.number="led.blinkCount" :disabled="busy">
</div>
</div>
<div class="col-md-4 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-primary btn-sm"
@click="ledRing({ clientId: 0 })"
:disabled="busy || !state.uart_connected">
Master
</button>
<button type="button" class="btn btn-outline-primary btn-sm"
@click="ledRing({ allClients: true, slavesOnly: true })"
:disabled="busy || !state.uart_connected">
Alle Slaves
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
@click="ledRing({ allClients: true })"
:disabled="busy || !state.uart_connected">
Alle + Master
</button>
</div>
</div>
</div>
</div>
</section>
<section class="col-12">
<div class="card">
<div class="card-header">Firmware OTA (A/B)</div>
@@ -480,6 +562,17 @@
busy: false,
configMsg: '',
configMsgOk: false,
led: {
mode: 'color',
r: 0,
g: 120,
b: 255,
intensity: 0,
progress: 50,
digit: 0,
blinkMs: 350,
blinkCount: 1
},
connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = proto + '//' + location.host + '/ws';
@@ -853,6 +946,49 @@
this.busy = false;
}
},
async ledRing(opts = {}) {
const clientId = opts.clientId ?? 0;
const mode = opts.mode ?? this.led.mode;
this.busy = true;
try {
const r = await fetch('/api/led-ring', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode,
client_id: clientId,
all_clients: !!opts.allClients,
slaves_only: !!opts.slavesOnly,
r: this.led.r,
g: this.led.g,
b: this.led.b,
intensity: this.led.intensity,
progress: this.led.progress,
digit: this.led.digit,
blink_ms: this.led.blinkMs,
blink_count: this.led.blinkCount
})
});
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || 'LED-Ring fehlgeschlagen', false);
return;
}
let label = 'Master';
if (opts.allClients) {
label = opts.slavesOnly
? `Alle Slaves (${data.slaves_updated})`
: `Alle + Master (${data.slaves_updated} Slaves)`;
} else if (clientId > 0) {
label = `Slave ${clientId}`;
}
this.flash(`LED ${mode}${label}`, true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
async findMe(clientId = 0) {
this.busy = true;
try {