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:
+140
-4
@@ -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> (0–100 %),
|
||||
<code>digit</code> (0–10), <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 0–10</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 {
|
||||
|
||||
Reference in New Issue
Block a user