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
+57 -6
View File
@@ -1530,8 +1530,8 @@ func (x *EspNowUnicastTestResponse) GetSeq() uint32 {
return 0
}
// Host → device: LED ring display (progress bar, digit, clear, blink, or find-me).
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink full ring, 4=find-me (R/G/B ×3 @ full brightness).
// Host → master: LED ring on master (client_id=0) and/or slaves via ESP-NOW.
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color.
type LedRingProgressRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Mode uint32 `protobuf:"varint,1,opt,name=mode,proto3" json:"mode,omitempty"`
@@ -1547,7 +1547,13 @@ type LedRingProgressRequest struct {
// * Pulse length in ms (mode=blink, default 350)
BlinkMs uint32 `protobuf:"varint,8,opt,name=blink_ms,json=blinkMs,proto3" json:"blink_ms,omitempty"`
// * Number of pulses (mode=blink, default 1)
BlinkCount uint32 `protobuf:"varint,9,opt,name=blink_count,json=blinkCount,proto3" json:"blink_count,omitempty"`
BlinkCount uint32 `protobuf:"varint,9,opt,name=blink_count,json=blinkCount,proto3" json:"blink_count,omitempty"`
// * 0 = master ring only; >0 = one slave; ignored when all_clients
ClientId uint32 `protobuf:"varint,10,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
// * Broadcast to all registered slaves (and optionally master unless slaves_only)
AllClients bool `protobuf:"varint,11,opt,name=all_clients,json=allClients,proto3" json:"all_clients,omitempty"`
// * With all_clients: do not change master ring
SlavesOnly bool `protobuf:"varint,12,opt,name=slaves_only,json=slavesOnly,proto3" json:"slaves_only,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1645,12 +1651,35 @@ func (x *LedRingProgressRequest) GetBlinkCount() uint32 {
return 0
}
func (x *LedRingProgressRequest) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
func (x *LedRingProgressRequest) GetAllClients() bool {
if x != nil {
return x.AllClients
}
return false
}
func (x *LedRingProgressRequest) GetSlavesOnly() bool {
if x != nil {
return x.SlavesOnly
}
return false
}
type LedRingProgressResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
Mode uint32 `protobuf:"varint,2,opt,name=mode,proto3" json:"mode,omitempty"`
Progress uint32 `protobuf:"varint,3,opt,name=progress,proto3" json:"progress,omitempty"`
Digit uint32 `protobuf:"varint,4,opt,name=digit,proto3" json:"digit,omitempty"`
ClientId uint32 `protobuf:"varint,5,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
SlavesUpdated uint32 `protobuf:"varint,6,opt,name=slaves_updated,json=slavesUpdated,proto3" json:"slaves_updated,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1713,6 +1742,20 @@ func (x *LedRingProgressResponse) GetDigit() uint32 {
return 0
}
func (x *LedRingProgressResponse) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
func (x *LedRingProgressResponse) GetSlavesUpdated() uint32 {
if x != nil {
return x.SlavesUpdated
}
return 0
}
// * Host → master: find-me on local ring (client_id=0) or ESP-NOW unicast to one slave.
type EspNowFindMeRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -2411,7 +2454,7 @@ const file_uart_messages_proto_rawDesc = "" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" +
"\x19EspNowUnicastTestResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x10\n" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"\xe2\x01\n" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"\xc1\x02\n" +
"\x16LedRingProgressRequest\x12\x12\n" +
"\x04mode\x18\x01 \x01(\rR\x04mode\x12\x1a\n" +
"\bprogress\x18\x02 \x01(\rR\bprogress\x12\x14\n" +
@@ -2422,12 +2465,20 @@ const file_uart_messages_proto_rawDesc = "" +
"\tintensity\x18\a \x01(\rR\tintensity\x12\x19\n" +
"\bblink_ms\x18\b \x01(\rR\ablinkMs\x12\x1f\n" +
"\vblink_count\x18\t \x01(\rR\n" +
"blinkCount\"y\n" +
"blinkCount\x12\x1b\n" +
"\tclient_id\x18\n" +
" \x01(\rR\bclientId\x12\x1f\n" +
"\vall_clients\x18\v \x01(\bR\n" +
"allClients\x12\x1f\n" +
"\vslaves_only\x18\f \x01(\bR\n" +
"slavesOnly\"\xbd\x01\n" +
"\x17LedRingProgressResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x12\n" +
"\x04mode\x18\x02 \x01(\rR\x04mode\x12\x1a\n" +
"\bprogress\x18\x03 \x01(\rR\bprogress\x12\x14\n" +
"\x05digit\x18\x04 \x01(\rR\x05digit\"2\n" +
"\x05digit\x18\x04 \x01(\rR\x05digit\x12\x1b\n" +
"\tclient_id\x18\x05 \x01(\rR\bclientId\x12%\n" +
"\x0eslaves_updated\x18\x06 \x01(\rR\rslavesUpdated\"2\n" +
"\x13EspNowFindMeRequest\x12\x1b\n" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\"M\n" +
"\x14EspNowFindMeResponse\x12\x18\n" +