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:
simon 2026-05-29 19:24:55 +02:00
parent 47c75110c9
commit eb67a46158
21 changed files with 759 additions and 133 deletions

View File

@ -31,7 +31,7 @@ go run . -port /dev/ttyUSB0 clients
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
| `ota` | 1619 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW |
| `ota-progress` | 21 | Query per-slave ESP-NOW OTA progress on the master (`-client N`, default all) |
| `led-ring` | 8 | LED ring: `-mode clear\|progress\|digit\|blink\|find-me`, … |
| `led-ring` | 8 | LED ring: `-mode clear\|color\|progress\|digit\|blink\|find-me`, `-client`, `-all` |
| `find-me` | 22 | Locate pod (`-client 0` master, `>0` slave via ESP-NOW) |
| `restart` | 23 | Reboot master or slave (`-client 0` / `>0`) |
@ -87,7 +87,7 @@ Polling runs only when at least one connection has `receive_accel: true` **and**
**Hello** (on connect; accel is off until `set_stream`):
```json
{"type":"hello","serial_port":"/dev/ttyUSB0","interval_ms":16,"commands":["set_stream","get_stream","set_accel_stream","get_accel_stream"]}
{"type":"hello","serial_port":"/dev/ttyUSB0","interval_ms":16,"commands":["set_stream","get_stream","set_accel_stream","get_accel_stream","set_led_ring"]}
```
**Receive accel on this connection** (optional `interval_ms`, default from `-accel-interval`):
@ -116,6 +116,15 @@ Reply:
{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true}
```
**LED ring** (same JSON fields as `POST /api/led-ring`):
```json
{"type":"set_led_ring","mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":200}
{"type":"set_led_ring","mode":"digit","all_clients":true,"slaves_only":true,"digit":3,"g":255}
```
Reply: `{"type":"led_ring_status","success":true,"slaves_updated":2,...}`
**Accel** (only to connections with `receive_accel: true`, and only while slaves stream):
```json
@ -158,7 +167,17 @@ The dashboard can configure nodes using the same UART commands as the CLI:
| Alle Slaves | per-slave ESP-NOW (Master bleibt unverändert; CLI `-all` setzt auch den Master) |
| Unicast test | `unicast-test -client ID` |
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `GET/PUT /api/clients/{id}/accel-stream`, `POST /api/accel-stream` (legacy / `all_clients`), `POST /api/unicast-test`, `POST /api/find-me`, `POST /api/restart`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `GET/PUT /api/clients/{id}/accel-stream`, `POST /api/accel-stream` (legacy / `all_clients`), `POST /api/led-ring`, `POST /api/unicast-test`, `POST /api/find-me`, `POST /api/restart`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
**LED ring** (`POST /api/led-ring` and WebSocket `set_led_ring` on `:8081`):
```json
{"mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":128}
{"mode":"digit","client_id":0,"digit":3,"r":0,"g":255,"b":0}
{"mode":"find-me","all_clients":true,"slaves_only":true}
```
Modes: `clear`, `color` (full ring), `progress` (0100), `digit` (010 symbols), `blink`, `find-me`. Use `client_id` (0 = master), or `all_clients` (+ optional `slaves_only`) for broadcast.
**Accel stream per slave** (must be enabled before values appear; goTool polls only while at least one slave has stream on):

36
goTool/api_led_ring.go Normal file
View File

@ -0,0 +1,36 @@
package main
import (
"encoding/json"
"net/http"
)
func mountLedRingAPI(mux *http.ServeMux, link *managedSerial) {
mux.HandleFunc("/api/led-ring", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
serveLedRingPost(w, r, link)
})
}
func serveLedRingPost(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body ledRingAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, ledRingAPIResponse{Error: "invalid JSON"})
return
}
if body.Mode == "" {
writeJSON(w, http.StatusBadRequest, ledRingAPIResponse{Error: "mode required"})
return
}
out := applyLedRing(link, body)
status := http.StatusOK
if out.Error != "" {
status = http.StatusServiceUnavailable
} else if !out.Success {
status = http.StatusServiceUnavailable
}
writeJSON(w, status, out)
}

View File

@ -69,6 +69,7 @@ type otaAPIResponse struct {
func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCtl *accelStreamCtl) {
mountAccelStreamAPI(mux, link, hub, streamCtl)
mountLedRingAPI(mux, link)
mux.HandleFunc("/api/deadzone", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:

View File

@ -132,7 +132,9 @@ func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubs
Type: "hello",
Serial: portName,
IntervalMs: int(h.defaultInterval / time.Millisecond),
Commands: []string{"set_stream", "get_stream", "set_accel_stream", "get_accel_stream"},
Commands: []string{
"set_stream", "get_stream", "set_accel_stream", "get_accel_stream", "set_led_ring",
},
}
if data, err := json.Marshal(hello); err == nil {
_ = conn.WriteMessage(websocket.TextMessage, data)
@ -317,6 +319,15 @@ func writeStreamStatus(conn *websocket.Conn, msg StreamStatusMessage) {
_ = conn.WriteMessage(websocket.TextMessage, data)
}
func writeLedRingStatus(conn *websocket.Conn, out ledRingAPIResponse) {
out.Type = "led_ring_status"
data, err := json.Marshal(out)
if err != nil {
return
}
_ = conn.WriteMessage(websocket.TextMessage, data)
}
func writeAccelStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) {
msg := AccelStreamStatusMessage{
Type: "accel_stream_status",
@ -393,10 +404,22 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
Success: resp.GetSuccess(),
})
case "set_led_ring":
var body ledRingAPIRequest
if err := json.Unmarshal(data, &body); err != nil {
writeLedRingStatus(conn, ledRingAPIResponse{Error: "invalid JSON"})
return
}
if body.Mode == "" {
writeLedRingStatus(conn, ledRingAPIResponse{Error: "mode required"})
return
}
writeLedRingStatus(conn, applyLedRing(link, body))
default:
writeStreamStatus(conn, StreamStatusMessage{
Type: "stream_status",
Error: "unknown type (set_stream, get_stream, set_accel_stream, get_accel_stream)",
Error: "unknown type (set_stream, get_stream, set_accel_stream, get_accel_stream, set_led_ring)",
})
}
}
@ -434,7 +457,7 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
DefaultIntervalMs: defMs,
MinIntervalMs: int(minAPIStreamInterval / time.Millisecond),
MaxIntervalMs: int(maxAPIStreamInterval / time.Millisecond),
Description: "WebSocket: per-connection accel receive + interval; slave stream via set_accel_stream",
Description: "WebSocket: accel stream + set_led_ring (modes: clear, color, progress, digit, blink, find-me)",
})
})
@ -454,6 +477,7 @@ func runAPIServer(portName string, link *managedSerial, addr string, defaultInte
mux := http.NewServeMux()
mountExternalAPI(mux, portName, defaultInterval, hub, link, dash, ctl)
mountLedRingAPI(mux, link)
srv := &http.Server{Addr: addr, Handler: mux}
go func() {

View File

@ -347,6 +347,8 @@ func ledRingModeValue(mode string) (uint32, error) {
return 3, nil
case "find_me", "findme":
return 4, nil
case "color", "solid", "fill":
return 5, nil
default:
return 0, fmt.Errorf("unknown led_ring mode %q", mode)
}

View File

@ -377,6 +377,16 @@ func (s *serialPort) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgres
return s.ledRingProgress(req)
}
func (m *managedSerial) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
var resp *pb.LedRingProgressResponse
err := m.withPort(func(sp *serialPort) error {
var e error
resp, e = sp.LedRing(req)
return e
})
return resp, err
}
func (s *serialPort) FindMe(clientID uint32) (*pb.EspNowFindMeResponse, error) {
return s.espnowFindMe(clientID)
}

View File

@ -7,17 +7,12 @@ import (
"powerpod/gotool/pb"
)
const (
ledRingModeClear = 0
ledRingModeProgress = 1
ledRingModeDigit = 2
ledRingModeBlink = 3
ledRingModeFindMe = 4
)
func runLedRing(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("led-ring", flag.ExitOnError)
mode := fs.String("mode", "progress", "clear, progress, digit, blink, or find-me")
mode := fs.String("mode", "progress", "clear, color, progress, digit, blink, or find-me")
clientID := fs.Uint("client", 0, "0=master ring, >0=slave via ESP-NOW")
allClients := fs.Bool("all", false, "broadcast to all slaves")
slavesOnly := fs.Bool("slaves-only", false, "with -all: do not change master ring")
progress := fs.Uint("progress", 0, "fill level 0100 (mode=progress)")
digit := fs.Uint("digit", 0, "digit 010 (mode=digit)")
r := fs.Uint("r", 0, "red 0255")
@ -30,20 +25,9 @@ func runLedRing(sp *serialPort, args []string) error {
return err
}
var modeVal uint32
switch *mode {
case "clear":
modeVal = ledRingModeClear
case "progress":
modeVal = ledRingModeProgress
case "digit":
modeVal = ledRingModeDigit
case "blink":
modeVal = ledRingModeBlink
case "find-me", "find_me", "findme":
modeVal = ledRingModeFindMe
default:
return fmt.Errorf("unknown -mode %q (clear, progress, digit, blink, find-me)", *mode)
modeVal, err := ledRingModeFromString(*mode)
if err != nil {
return err
}
resp, err := sp.ledRingProgress(&pb.LedRingProgressRequest{
@ -56,11 +40,15 @@ func runLedRing(sp *serialPort, args []string) error {
Intensity: uint32(*intensity),
BlinkMs: uint32(*blinkMs),
BlinkCount: uint32(*blinkCount),
ClientId: uint32(*clientID),
AllClients: *allClients,
SlavesOnly: *slavesOnly,
})
if err != nil {
return err
}
fmt.Printf("success=%v mode=%d progress=%d digit=%d\n",
resp.GetSuccess(), resp.GetMode(), resp.GetProgress(), resp.GetDigit())
fmt.Printf("success=%v mode=%d progress=%d digit=%d client_id=%d slaves_updated=%d\n",
resp.GetSuccess(), resp.GetMode(), resp.GetProgress(), resp.GetDigit(),
resp.GetClientId(), resp.GetSlavesUpdated())
return nil
}

106
goTool/led_ring_api.go Normal file
View File

@ -0,0 +1,106 @@
package main
import (
"fmt"
"strings"
"powerpod/gotool/pb"
)
const (
ledRingModeClear = 0
ledRingModeProgress = 1
ledRingModeDigit = 2
ledRingModeBlink = 3
ledRingModeFindMe = 4
ledRingModeColor = 5
)
type ledRingAPIRequest struct {
Mode string `json:"mode"`
ClientID uint32 `json:"client_id"`
AllClients bool `json:"all_clients"`
SlavesOnly bool `json:"slaves_only"`
Progress uint32 `json:"progress"`
Digit uint32 `json:"digit"`
R uint32 `json:"r"`
G uint32 `json:"g"`
B uint32 `json:"b"`
Intensity uint32 `json:"intensity"`
BlinkMs uint32 `json:"blink_ms"`
BlinkCount uint32 `json:"blink_count"`
}
type ledRingAPIResponse struct {
Type string `json:"type,omitempty"` // led_ring_status (WebSocket)
Success bool `json:"success"`
Mode uint32 `json:"mode,omitempty"`
Progress uint32 `json:"progress,omitempty"`
Digit uint32 `json:"digit,omitempty"`
ClientID uint32 `json:"client_id,omitempty"`
SlavesUpdated uint32 `json:"slaves_updated,omitempty"`
Error string `json:"error,omitempty"`
}
func ledRingModeFromString(s string) (uint32, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "clear", "":
return ledRingModeClear, nil
case "color", "solid", "fill":
return ledRingModeColor, nil
case "progress":
return ledRingModeProgress, nil
case "digit":
return ledRingModeDigit, nil
case "blink":
return ledRingModeBlink, nil
case "find-me", "find_me", "findme":
return ledRingModeFindMe, nil
default:
return 0, fmt.Errorf("unknown mode %q (clear, color, progress, digit, blink, find-me)", s)
}
}
func ledRingPBFromAPI(in ledRingAPIRequest) (*pb.LedRingProgressRequest, error) {
mode, err := ledRingModeFromString(in.Mode)
if err != nil {
return nil, err
}
return &pb.LedRingProgressRequest{
Mode: mode,
Progress: in.Progress,
Digit: in.Digit,
R: in.R,
G: in.G,
B: in.B,
Intensity: in.Intensity,
BlinkMs: in.BlinkMs,
BlinkCount: in.BlinkCount,
ClientId: in.ClientID,
AllClients: in.AllClients,
SlavesOnly: in.SlavesOnly,
}, nil
}
func applyLedRing(link *managedSerial, in ledRingAPIRequest) ledRingAPIResponse {
req, err := ledRingPBFromAPI(in)
if err != nil {
return ledRingAPIResponse{Error: err.Error()}
}
resp, err := link.LedRing(req)
if err != nil {
return ledRingAPIResponse{Error: err.Error()}
}
out := ledRingAPIResponse{
Success: resp.GetSuccess(),
Mode: resp.GetMode(),
Progress: resp.GetProgress(),
Digit: resp.GetDigit(),
ClientID: resp.GetClientId(),
SlavesUpdated: resp.GetSlavesUpdated(),
}
if !out.Success && out.Error == "" {
out.Error = "led ring command rejected"
}
return out
}

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"`
@ -1548,6 +1548,12 @@ type LedRingProgressRequest struct {
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"`
// * 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" +

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 {

View File

@ -378,14 +378,19 @@ Control the 95-LED ring from the host. The firmware **does not** animate digits
| Field | Meaning |
|-------|---------|
| `mode` | `0` = clear, `1` = progress bar, `2` = digit, `3` = blink full ring, `4` = find-me (R/G/B ×3 @ full brightness) |
| `mode` | `0` = clear, `1` = progress, `2` = digit (010), `3` = blink, `4` = find-me, `5` = solid color (all LEDs) |
| `progress` | 0100 (% of ring lit, mode `1`) |
| `digit` | 010 (mode `2`, same segment maps as built-in digits) |
| `digit` | 010 (mode `2`, segment maps in `led_ring.c`) |
| `r`, `g`, `b` | Color 0255 |
| `intensity` | Brightness 0255 (scaled into RGB; `0` → firmware default ~5 %) |
| `blink_ms`, `blink_count` | Pulse length and count (mode `3`; defaults 350 ms, 1) |
| `client_id` | `0` = master ring only; `>0` = ESP-NOW unicast to one slave |
| `all_clients` | Broadcast to all registered slaves |
| `slaves_only` | With `all_clients`: do not change master ring |
**Response:** `led_ring_progress_response` (`success`, `mode`, `progress`, `digit`).
**Response:** `led_ring_progress_response` (`success`, `mode`, `progress`, `digit`, `client_id`, `slaves_updated`).
Slaves receive the same command via ESP-NOW `ESPNOW_LED_RING` and run it locally.
```bash
go run . -port /dev/ttyUSB0 led-ring -mode progress -progress 75 -g 80 -b 255
@ -395,6 +400,8 @@ go run . -port /dev/ttyUSB0 led-ring -mode blink -g 255 -blink-count 2
go run . -port /dev/ttyUSB0 find-me
go run . -port /dev/ttyUSB0 find-me -client 16
go run . -port /dev/ttyUSB0 led-ring -mode find-me
go run . -port /dev/ttyUSB0 led-ring -mode color -r 255 -g 0 -b 0 -client 16
go run . -port /dev/ttyUSB0 led-ring -mode digit -digit 5 -all
```
### CLIENT_INFO command

View File

@ -1,5 +1,7 @@
#include "cmd_led_ring.h"
#include "client_registry.h"
#include "esp_log.h"
#include "esp_now_comm.h"
#include "led_ring.h"
#include "uart_cmd.h"
@ -10,6 +12,7 @@ static const char *TAG = "[LED_RING_CMD]";
#define LED_RING_MODE_DIGIT 2
#define LED_RING_MODE_BLINK 3
#define LED_RING_MODE_FIND_ME 4
#define LED_RING_MODE_COLOR 5
static uint8_t clamp_u8(uint32_t v) {
if (v > 255) {
@ -32,7 +35,82 @@ static uint8_t resolve_intensity(uint32_t intensity) {
return clamp_u8(intensity);
}
static void reply(bool success, uint32_t mode, uint32_t progress, uint32_t digit) {
bool cmd_led_ring_apply(const alox_LedRingProgressRequest *req) {
if (req == NULL) {
return false;
}
uint32_t mode = req->mode;
uint8_t r = clamp_u8(req->r);
uint8_t g = clamp_u8(req->g);
uint8_t b = clamp_u8(req->b);
uint8_t intensity = resolve_intensity(req->intensity);
led_command_t cmd = {0};
switch (mode) {
case LED_RING_MODE_CLEAR:
cmd.mode = LED_CMD_CLEAR;
led_ring_send_command(&cmd);
return true;
case LED_RING_MODE_COLOR:
cmd.mode = LED_CMD_SET_COLOR;
cmd.r = r;
cmd.g = g;
cmd.b = b;
cmd.intensity = intensity;
led_ring_send_command(&cmd);
return true;
case LED_RING_MODE_PROGRESS: {
cmd.mode = LED_CMD_PROGRESS;
cmd.progress = clamp_progress(req->progress);
cmd.r = r;
cmd.g = g;
cmd.b = b;
cmd.intensity = intensity;
led_ring_send_command(&cmd);
return true;
}
case LED_RING_MODE_DIGIT:
if (req->digit > 10) {
return false;
}
cmd.mode = LED_CMD_SET_DIGIT;
cmd.value = (uint8_t)req->digit;
cmd.r = r;
cmd.g = g;
cmd.b = b;
cmd.intensity = intensity;
led_ring_send_command(&cmd);
return true;
case LED_RING_MODE_FIND_ME:
led_ring_find_me();
return true;
case LED_RING_MODE_BLINK:
cmd.mode = LED_CMD_BLINK;
cmd.r = r;
cmd.g = g;
cmd.b = b;
cmd.intensity = intensity;
cmd.blink_ms = (uint16_t)(req->blink_ms > 0 ? req->blink_ms : 350);
cmd.blink_count = req->blink_count > 0 ? (uint8_t)req->blink_count : 1;
if (cmd.blink_count == 0) {
cmd.blink_count = 1;
}
led_ring_send_command(&cmd);
return true;
default:
return false;
}
}
static void reply(bool success, uint32_t mode, uint32_t progress, uint32_t digit,
uint32_t client_id, uint32_t slaves_updated) {
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_LED_RING,
alox_UartMessage_led_ring_progress_response_tag);
@ -40,16 +118,26 @@ static void reply(bool success, uint32_t mode, uint32_t progress, uint32_t digit
response.payload.led_ring_progress_response.mode = mode;
response.payload.led_ring_progress_response.progress = progress;
response.payload.led_ring_progress_response.digit = digit;
response.payload.led_ring_progress_response.client_id = client_id;
response.payload.led_ring_progress_response.slaves_updated = slaves_updated;
uart_cmd_send(&response, TAG);
}
static esp_err_t push_led_ring_to_slave(const client_info_t *client,
const alox_LedRingProgressRequest *req) {
if (client == NULL || req == NULL) {
return ESP_ERR_INVALID_ARG;
}
return esp_now_comm_send_led_ring(client->mac, client->id, req);
}
static void handle_led_ring(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
alox_LedRingProgressRequest req = alox_LedRingProgressRequest_init_zero;
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed");
reply(false, 0, 0, 0);
reply(false, 0, 0, 0, 0, 0);
return;
}
@ -61,84 +149,53 @@ static void handle_led_ring(const uint8_t *data, size_t len) {
}
uint32_t mode = req.mode;
uint8_t r = clamp_u8(req.r);
uint8_t g = clamp_u8(req.g);
uint8_t b = clamp_u8(req.b);
uint8_t intensity = resolve_intensity(req.intensity);
led_command_t cmd = {0};
switch (mode) {
case LED_RING_MODE_CLEAR:
cmd.mode = LED_CMD_CLEAR;
led_ring_send_command(&cmd);
ESP_LOGI(TAG, "clear");
reply(true, mode, 0, 0);
return;
case LED_RING_MODE_PROGRESS: {
uint8_t progress = clamp_progress(req.progress);
cmd.mode = LED_CMD_PROGRESS;
cmd.progress = progress;
cmd.r = r;
cmd.g = g;
cmd.b = b;
cmd.intensity = intensity;
led_ring_send_command(&cmd);
ESP_LOGI(TAG, "progress %u%% rgb=%u,%u,%u", (unsigned)progress,
(unsigned)r, (unsigned)g, (unsigned)b);
reply(true, mode, progress, 0);
if (req.all_clients) {
size_t n = client_registry_count();
uint32_t sent = 0;
for (size_t i = 0; i < n; i++) {
const client_info_t *client = client_registry_at(i);
if (client == NULL) {
continue;
}
if (push_led_ring_to_slave(client, &req) == ESP_OK) {
sent++;
}
}
bool local_ok = true;
if (!req.slaves_only) {
local_ok = cmd_led_ring_apply(&req);
}
ESP_LOGI(TAG, "LED ring mode %lu → %u/%u slaves%s", (unsigned long)mode,
(unsigned)sent, (unsigned)n, req.slaves_only ? "" : " + master");
reply(local_ok || sent > 0, mode, req.progress, req.digit, 0, sent);
return;
}
case LED_RING_MODE_DIGIT: {
if (req.digit > 10) {
ESP_LOGW(TAG, "digit %lu out of range", (unsigned long)req.digit);
reply(false, mode, 0, req.digit);
return;
}
cmd.mode = LED_CMD_SET_DIGIT;
cmd.value = (uint8_t)req.digit;
cmd.r = r;
cmd.g = g;
cmd.b = b;
cmd.intensity = intensity;
led_ring_send_command(&cmd);
ESP_LOGI(TAG, "digit %u rgb=%u,%u,%u", (unsigned)cmd.value, (unsigned)r,
(unsigned)g, (unsigned)b);
reply(true, mode, 0, req.digit);
if (req.client_id == 0) {
bool ok = cmd_led_ring_apply(&req);
ESP_LOGI(TAG, "LED ring mode %lu on master", (unsigned long)mode);
reply(ok, mode, req.progress, req.digit, 0, 0);
return;
}
case LED_RING_MODE_FIND_ME:
led_ring_find_me();
ESP_LOGI(TAG, "find-me");
reply(true, mode, 0, 0);
return;
case LED_RING_MODE_BLINK: {
cmd.mode = LED_CMD_BLINK;
cmd.r = r;
cmd.g = g;
cmd.b = b;
cmd.intensity = intensity;
cmd.blink_ms = (uint16_t)(req.blink_ms > 0 ? req.blink_ms : 350);
cmd.blink_count = req.blink_count > 0 ? (uint8_t)req.blink_count : 1;
if (cmd.blink_count == 0) {
cmd.blink_count = 1;
}
led_ring_send_command(&cmd);
ESP_LOGI(TAG, "blink x%u %u ms rgb=%u,%u,%u", (unsigned)cmd.blink_count,
(unsigned)cmd.blink_ms, (unsigned)r, (unsigned)g, (unsigned)b);
reply(true, mode, 0, 0);
const client_info_t *client = client_registry_find_by_id(req.client_id);
if (client == NULL) {
ESP_LOGW(TAG, "client id %lu not in registry", (unsigned long)req.client_id);
reply(false, mode, req.progress, req.digit, req.client_id, 0);
return;
}
default:
ESP_LOGW(TAG, "unknown mode %lu", (unsigned long)mode);
reply(false, mode, 0, 0);
return;
esp_err_t err = push_led_ring_to_slave(client, &req);
if (err == ESP_OK) {
ESP_LOGI(TAG, "LED ring mode %lu → slave %lu", (unsigned long)mode,
(unsigned long)req.client_id);
} else {
ESP_LOGW(TAG, "LED ring to slave %lu failed: %s",
(unsigned long)req.client_id, esp_err_to_name(err));
}
reply(err == ESP_OK, mode, req.progress, req.digit, req.client_id,
err == ESP_OK ? 1 : 0);
}
void cmd_led_ring_register(void) {

View File

@ -1,6 +1,12 @@
#ifndef CMD_LED_RING_H
#define CMD_LED_RING_H
#include <stdbool.h>
#include "uart_messages.pb.h"
/** Apply LED ring command locally (master or slave). */
bool cmd_led_ring_apply(const alox_LedRingProgressRequest *req);
void cmd_led_ring_register(void);
#endif

View File

@ -1,6 +1,7 @@
#include "bosch456.h"
#include "client_registry.h"
#include "esp_now_comm.h"
#include "cmd_led_ring.h"
#include "led_ring.h"
#include "ota_espnow.h"
#include "pod_reboot.h"
@ -209,6 +210,30 @@ static esp_err_t send_find_me(const uint8_t *dest_mac, uint32_t client_id) {
return send_message(dest_mac, &msg);
}
static esp_err_t send_led_ring(const uint8_t *dest_mac, uint32_t client_id,
const alox_LedRingProgressRequest *req) {
if (req == NULL) {
return ESP_ERR_INVALID_ARG;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_LED_RING;
msg.which_payload = alox_EspNowMessage_led_ring_tag;
msg.payload.led_ring.client_id = client_id;
msg.payload.led_ring.mode = req->mode;
msg.payload.led_ring.progress = req->progress;
msg.payload.led_ring.digit = req->digit;
msg.payload.led_ring.r = req->r;
msg.payload.led_ring.g = req->g;
msg.payload.led_ring.b = req->b;
msg.payload.led_ring.intensity = req->intensity;
msg.payload.led_ring.blink_ms = req->blink_ms;
msg.payload.led_ring.blink_count = req->blink_count;
return send_message(dest_mac, &msg);
}
static esp_err_t send_restart(const uint8_t *dest_mac, uint32_t client_id) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
@ -347,6 +372,26 @@ esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN],
return err;
}
esp_err_t esp_now_comm_send_led_ring(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id,
const alox_LedRingProgressRequest *req) {
if (mac == NULL || !s_config.master || req == NULL) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err = send_led_ring(mac, client_id, req);
if (err == ESP_OK) {
ESP_LOGI(TAG, "unicast LED_RING mode %lu to %s client_id=%lu",
(unsigned long)req->mode, mac_str, (unsigned long)client_id);
} else {
ESP_LOGW(TAG, "unicast LED_RING to %s failed: %s", mac_str,
esp_err_to_name(err));
}
return err;
}
esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t seq) {
if (mac == NULL || !s_config.master) {
@ -454,6 +499,36 @@ static void handle_slave_restart(const uint8_t *master_mac,
pod_schedule_restart();
}
static void handle_slave_led_ring(const uint8_t *master_mac,
const alox_EspNowLedRing *msg) {
uint32_t my_id = s_own_mac[5];
if (msg->client_id != 0 && msg->client_id != my_id) {
return;
}
if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) {
return;
}
alox_LedRingProgressRequest req = alox_LedRingProgressRequest_init_zero;
req.mode = msg->mode;
req.progress = msg->progress;
req.digit = msg->digit;
req.r = msg->r;
req.g = msg->g;
req.b = msg->b;
req.intensity = msg->intensity;
req.blink_ms = msg->blink_ms;
req.blink_count = msg->blink_count;
char mac_str[18];
mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "LED_RING mode %lu from master %s (id=%lu)",
(unsigned long)req.mode, mac_str, (unsigned long)my_id);
cmd_led_ring_apply(&req);
}
static void handle_slave_find_me(const uint8_t *master_mac,
const alox_EspNowFindMe *req) {
uint32_t my_id = s_own_mac[5];
@ -704,6 +779,12 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
}
handle_slave_accel_stream(info->src_addr, &msg.payload.accel_stream);
break;
case alox_EspNowMessage_led_ring_tag:
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) {
break;
}
handle_slave_led_ring(info->src_addr, &msg.payload.led_ring);
break;
case alox_EspNowMessage_find_me_tag:
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) {
break;

View File

@ -4,6 +4,7 @@
#include "app_config.h"
#include "client_registry.h"
#include "esp_err.h"
#include "uart_messages.pb.h"
esp_err_t esp_now_comm_init(const app_config_t *config);
@ -23,6 +24,11 @@ esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id);
/** Master: LED ring command on one slave. */
esp_err_t esp_now_comm_send_led_ring(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id,
const alox_LedRingProgressRequest *req);
/** Master: request reboot on one slave. */
esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id);

View File

@ -116,6 +116,8 @@ void vTaskLedRing(void *pvParameters) {
for (int i = 0; i < digit.count; i++) {
led_strip_set_pixel(led_ring, RING_LEDS - digit.leds[i], r, g, b);
}
} else if (cmd.mode == LED_CMD_SET_COLOR) {
ring_fill_color(r, g, b);
} else if (cmd.mode == LED_CMD_PROGRESS) {
uint32_t lit = ((uint32_t)cmd.progress * RING_LEDS + 50) / 100;
if (lit > RING_LEDS) {

View File

@ -30,6 +30,9 @@ PB_BIND(alox_EspNowAccelStream, alox_EspNowAccelStream, AUTO)
PB_BIND(alox_EspNowAccelSample, alox_EspNowAccelSample, AUTO)
PB_BIND(alox_EspNowLedRing, alox_EspNowLedRing, AUTO)
PB_BIND(alox_EspNowOtaStart, alox_EspNowOtaStart, AUTO)

View File

@ -24,7 +24,8 @@ typedef enum _alox_EspNowMessageType {
alox_EspNowMessageType_ESPNOW_FIND_ME = 10,
alox_EspNowMessageType_ESPNOW_RESTART = 11,
alox_EspNowMessageType_ESPNOW_ACCEL_SAMPLE = 12,
alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM = 13
alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM = 13,
alox_EspNowMessageType_ESPNOW_LED_RING = 14
} alox_EspNowMessageType;
/* Struct definitions */
@ -75,6 +76,20 @@ typedef struct _alox_EspNowAccelSample {
int32_t z;
} alox_EspNowAccelSample;
/* * Master → slave: LED ring command (same modes as UART LedRingProgressRequest). */
typedef struct _alox_EspNowLedRing {
uint32_t client_id;
uint32_t mode;
uint32_t progress;
uint32_t digit;
uint32_t r;
uint32_t g;
uint32_t b;
uint32_t intensity;
uint32_t blink_ms;
uint32_t blink_count;
} alox_EspNowLedRing;
/* Master → slave: begin OTA (erase inactive slot; slave replies ESPNOW_OTA_STATUS). */
typedef struct _alox_EspNowOtaStart {
uint32_t total_size;
@ -116,6 +131,7 @@ typedef struct _alox_EspNowMessage {
alox_EspNowRestart restart;
alox_EspNowAccelSample accel_sample;
alox_EspNowAccelStream accel_stream;
alox_EspNowLedRing led_ring;
} payload;
} alox_EspNowMessage;
@ -126,8 +142,9 @@ extern "C" {
/* Helper constants for enums */
#define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM+1))
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_LED_RING
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_LED_RING+1))
@ -153,6 +170,7 @@ extern "C" {
#define alox_EspNowAccelDeadzone_init_default {0, 0}
#define alox_EspNowAccelStream_init_default {0, 0}
#define alox_EspNowAccelSample_init_default {0, 0, 0, 0}
#define alox_EspNowLedRing_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_EspNowOtaStart_init_default {0}
#define alox_EspNowOtaPayload_init_default {0, {0, {0}}}
#define alox_EspNowOtaEnd_init_default {0}
@ -166,6 +184,7 @@ extern "C" {
#define alox_EspNowAccelDeadzone_init_zero {0, 0}
#define alox_EspNowAccelStream_init_zero {0, 0}
#define alox_EspNowAccelSample_init_zero {0, 0, 0, 0}
#define alox_EspNowLedRing_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_EspNowOtaStart_init_zero {0}
#define alox_EspNowOtaPayload_init_zero {0, {0, {0}}}
#define alox_EspNowOtaEnd_init_zero {0}
@ -191,6 +210,16 @@ extern "C" {
#define alox_EspNowAccelSample_x_tag 2
#define alox_EspNowAccelSample_y_tag 3
#define alox_EspNowAccelSample_z_tag 4
#define alox_EspNowLedRing_client_id_tag 1
#define alox_EspNowLedRing_mode_tag 2
#define alox_EspNowLedRing_progress_tag 3
#define alox_EspNowLedRing_digit_tag 4
#define alox_EspNowLedRing_r_tag 5
#define alox_EspNowLedRing_g_tag 6
#define alox_EspNowLedRing_b_tag 7
#define alox_EspNowLedRing_intensity_tag 8
#define alox_EspNowLedRing_blink_ms_tag 9
#define alox_EspNowLedRing_blink_count_tag 10
#define alox_EspNowOtaStart_total_size_tag 1
#define alox_EspNowOtaPayload_seq_tag 1
#define alox_EspNowOtaPayload_data_tag 2
@ -211,6 +240,7 @@ extern "C" {
#define alox_EspNowMessage_restart_tag 12
#define alox_EspNowMessage_accel_sample_tag 13
#define alox_EspNowMessage_accel_stream_tag 14
#define alox_EspNowMessage_led_ring_tag 15
/* Struct field encoding specification for nanopb */
#define alox_EspNowUnicastTest_FIELDLIST(X, a) \
@ -263,6 +293,20 @@ X(a, STATIC, SINGULAR, SINT32, z, 4)
#define alox_EspNowAccelSample_CALLBACK NULL
#define alox_EspNowAccelSample_DEFAULT NULL
#define alox_EspNowLedRing_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, UINT32, mode, 2) \
X(a, STATIC, SINGULAR, UINT32, progress, 3) \
X(a, STATIC, SINGULAR, UINT32, digit, 4) \
X(a, STATIC, SINGULAR, UINT32, r, 5) \
X(a, STATIC, SINGULAR, UINT32, g, 6) \
X(a, STATIC, SINGULAR, UINT32, b, 7) \
X(a, STATIC, SINGULAR, UINT32, intensity, 8) \
X(a, STATIC, SINGULAR, UINT32, blink_ms, 9) \
X(a, STATIC, SINGULAR, UINT32, blink_count, 10)
#define alox_EspNowLedRing_CALLBACK NULL
#define alox_EspNowLedRing_DEFAULT NULL
#define alox_EspNowOtaStart_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, total_size, 1)
#define alox_EspNowOtaStart_CALLBACK NULL
@ -300,7 +344,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,ota_status,payload.ota_status), 10)
X(a, STATIC, ONEOF, MESSAGE, (payload,find_me,payload.find_me), 11) \
X(a, STATIC, ONEOF, MESSAGE, (payload,restart,payload.restart), 12) \
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_sample,payload.accel_sample), 13) \
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream,payload.accel_stream), 14)
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream,payload.accel_stream), 14) \
X(a, STATIC, ONEOF, MESSAGE, (payload,led_ring,payload.led_ring), 15)
#define alox_EspNowMessage_CALLBACK NULL
#define alox_EspNowMessage_DEFAULT NULL
#define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover
@ -316,6 +361,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream,payload.accel_stream),
#define alox_EspNowMessage_payload_restart_MSGTYPE alox_EspNowRestart
#define alox_EspNowMessage_payload_accel_sample_MSGTYPE alox_EspNowAccelSample
#define alox_EspNowMessage_payload_accel_stream_MSGTYPE alox_EspNowAccelStream
#define alox_EspNowMessage_payload_led_ring_MSGTYPE alox_EspNowLedRing
extern const pb_msgdesc_t alox_EspNowUnicastTest_msg;
extern const pb_msgdesc_t alox_EspNowFindMe_msg;
@ -325,6 +371,7 @@ extern const pb_msgdesc_t alox_EspNowSlavePresence_msg;
extern const pb_msgdesc_t alox_EspNowAccelDeadzone_msg;
extern const pb_msgdesc_t alox_EspNowAccelStream_msg;
extern const pb_msgdesc_t alox_EspNowAccelSample_msg;
extern const pb_msgdesc_t alox_EspNowLedRing_msg;
extern const pb_msgdesc_t alox_EspNowOtaStart_msg;
extern const pb_msgdesc_t alox_EspNowOtaPayload_msg;
extern const pb_msgdesc_t alox_EspNowOtaEnd_msg;
@ -340,6 +387,7 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
#define alox_EspNowAccelDeadzone_fields &alox_EspNowAccelDeadzone_msg
#define alox_EspNowAccelStream_fields &alox_EspNowAccelStream_msg
#define alox_EspNowAccelSample_fields &alox_EspNowAccelSample_msg
#define alox_EspNowLedRing_fields &alox_EspNowLedRing_msg
#define alox_EspNowOtaStart_fields &alox_EspNowOtaStart_msg
#define alox_EspNowOtaPayload_fields &alox_EspNowOtaPayload_msg
#define alox_EspNowOtaEnd_fields &alox_EspNowOtaEnd_msg
@ -355,6 +403,7 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
#define alox_EspNowAccelStream_size 8
#define alox_EspNowDiscover_size 6
#define alox_EspNowFindMe_size 6
#define alox_EspNowLedRing_size 60
#define alox_EspNowOtaEnd_size 0
#define alox_EspNowOtaPayload_size 209
#define alox_EspNowOtaStart_size 6

View File

@ -19,6 +19,7 @@ enum EspNowMessageType {
ESPNOW_RESTART = 11;
ESPNOW_ACCEL_SAMPLE = 12;
ESPNOW_SET_ACCEL_STREAM = 13;
ESPNOW_LED_RING = 14;
}
message EspNowUnicastTest {
@ -68,6 +69,20 @@ message EspNowAccelSample {
sint32 z = 4;
}
/** Master → slave: LED ring command (same modes as UART LedRingProgressRequest). */
message EspNowLedRing {
uint32 client_id = 1;
uint32 mode = 2;
uint32 progress = 3;
uint32 digit = 4;
uint32 r = 5;
uint32 g = 6;
uint32 b = 7;
uint32 intensity = 8;
uint32 blink_ms = 9;
uint32 blink_count = 10;
}
// Master slave: begin OTA (erase inactive slot; slave replies ESPNOW_OTA_STATUS).
message EspNowOtaStart {
uint32 total_size = 1;
@ -105,5 +120,6 @@ message EspNowMessage {
EspNowRestart restart = 12;
EspNowAccelSample accel_sample = 13;
EspNowAccelStream accel_stream = 14;
EspNowLedRing led_ring = 15;
}
}

View File

@ -139,8 +139,8 @@ typedef struct _alox_EspNowUnicastTestResponse {
uint32_t seq;
} alox_EspNowUnicastTestResponse;
/* 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. */
typedef struct _alox_LedRingProgressRequest {
uint32_t mode;
/* * 0100: fraction of ring LEDs to light (mode=progress) */
@ -156,6 +156,12 @@ typedef struct _alox_LedRingProgressRequest {
uint32_t blink_ms;
/* * Number of pulses (mode=blink, default 1) */
uint32_t blink_count;
/* * 0 = master ring only; >0 = one slave; ignored when all_clients */
uint32_t client_id;
/* * Broadcast to all registered slaves (and optionally master unless slaves_only) */
bool all_clients;
/* * With all_clients: do not change master ring */
bool slaves_only;
} alox_LedRingProgressRequest;
typedef struct _alox_LedRingProgressResponse {
@ -163,6 +169,8 @@ typedef struct _alox_LedRingProgressResponse {
uint32_t mode;
uint32_t progress;
uint32_t digit;
uint32_t client_id;
uint32_t slaves_updated;
} alox_LedRingProgressResponse;
/* * Host → master: find-me on local ring (client_id=0) or ESP-NOW unicast to one slave. */
@ -326,8 +334,8 @@ extern "C" {
#define alox_AccelSnapshotResponse_init_default {0, {alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default, alox_AccelSample_init_default}}
#define alox_EspNowUnicastTestRequest_init_default {0, 0}
#define alox_EspNowUnicastTestResponse_init_default {0, 0}
#define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_LedRingProgressResponse_init_default {0, 0, 0, 0}
#define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_LedRingProgressResponse_init_default {0, 0, 0, 0, 0, 0}
#define alox_EspNowFindMeRequest_init_default {0}
#define alox_EspNowFindMeResponse_init_default {0, 0}
#define alox_RestartRequest_init_default {0}
@ -356,8 +364,8 @@ extern "C" {
#define alox_AccelSnapshotResponse_init_zero {0, {alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero, alox_AccelSample_init_zero}}
#define alox_EspNowUnicastTestRequest_init_zero {0, 0}
#define alox_EspNowUnicastTestResponse_init_zero {0, 0}
#define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_LedRingProgressResponse_init_zero {0, 0, 0, 0}
#define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_LedRingProgressResponse_init_zero {0, 0, 0, 0, 0, 0}
#define alox_EspNowFindMeRequest_init_zero {0}
#define alox_EspNowFindMeResponse_init_zero {0, 0}
#define alox_RestartRequest_init_zero {0}
@ -426,10 +434,15 @@ extern "C" {
#define alox_LedRingProgressRequest_intensity_tag 7
#define alox_LedRingProgressRequest_blink_ms_tag 8
#define alox_LedRingProgressRequest_blink_count_tag 9
#define alox_LedRingProgressRequest_client_id_tag 10
#define alox_LedRingProgressRequest_all_clients_tag 11
#define alox_LedRingProgressRequest_slaves_only_tag 12
#define alox_LedRingProgressResponse_success_tag 1
#define alox_LedRingProgressResponse_mode_tag 2
#define alox_LedRingProgressResponse_progress_tag 3
#define alox_LedRingProgressResponse_digit_tag 4
#define alox_LedRingProgressResponse_client_id_tag 5
#define alox_LedRingProgressResponse_slaves_updated_tag 6
#define alox_EspNowFindMeRequest_client_id_tag 1
#define alox_EspNowFindMeResponse_success_tag 1
#define alox_EspNowFindMeResponse_client_id_tag 2
@ -660,7 +673,10 @@ X(a, STATIC, SINGULAR, UINT32, g, 5) \
X(a, STATIC, SINGULAR, UINT32, b, 6) \
X(a, STATIC, SINGULAR, UINT32, intensity, 7) \
X(a, STATIC, SINGULAR, UINT32, blink_ms, 8) \
X(a, STATIC, SINGULAR, UINT32, blink_count, 9)
X(a, STATIC, SINGULAR, UINT32, blink_count, 9) \
X(a, STATIC, SINGULAR, UINT32, client_id, 10) \
X(a, STATIC, SINGULAR, BOOL, all_clients, 11) \
X(a, STATIC, SINGULAR, BOOL, slaves_only, 12)
#define alox_LedRingProgressRequest_CALLBACK NULL
#define alox_LedRingProgressRequest_DEFAULT NULL
@ -668,7 +684,9 @@ X(a, STATIC, SINGULAR, UINT32, blink_count, 9)
X(a, STATIC, SINGULAR, BOOL, success, 1) \
X(a, STATIC, SINGULAR, UINT32, mode, 2) \
X(a, STATIC, SINGULAR, UINT32, progress, 3) \
X(a, STATIC, SINGULAR, UINT32, digit, 4)
X(a, STATIC, SINGULAR, UINT32, digit, 4) \
X(a, STATIC, SINGULAR, UINT32, client_id, 5) \
X(a, STATIC, SINGULAR, UINT32, slaves_updated, 6)
#define alox_LedRingProgressResponse_CALLBACK NULL
#define alox_LedRingProgressResponse_DEFAULT NULL
@ -826,8 +844,8 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_EspNowFindMeResponse_size 8
#define alox_EspNowUnicastTestRequest_size 12
#define alox_EspNowUnicastTestResponse_size 8
#define alox_LedRingProgressRequest_size 54
#define alox_LedRingProgressResponse_size 20
#define alox_LedRingProgressRequest_size 64
#define alox_LedRingProgressResponse_size 32
#define alox_OtaEndPayload_size 0
#define alox_OtaPayload_size 209
#define alox_OtaSlaveProgressEntry_size 30

View File

@ -160,8 +160,8 @@ message EspNowUnicastTestResponse {
uint32 seq = 2;
}
// 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.
message LedRingProgressRequest {
uint32 mode = 1;
/** 0100: fraction of ring LEDs to light (mode=progress) */
@ -177,6 +177,12 @@ message LedRingProgressRequest {
uint32 blink_ms = 8;
/** Number of pulses (mode=blink, default 1) */
uint32 blink_count = 9;
/** 0 = master ring only; >0 = one slave; ignored when all_clients */
uint32 client_id = 10;
/** Broadcast to all registered slaves (and optionally master unless slaves_only) */
bool all_clients = 11;
/** With all_clients: do not change master ring */
bool slaves_only = 12;
}
message LedRingProgressResponse {
@ -184,6 +190,8 @@ message LedRingProgressResponse {
uint32 mode = 2;
uint32 progress = 3;
uint32 digit = 4;
uint32 client_id = 5;
uint32 slaves_updated = 6;
}
/** Host → master: find-me on local ring (client_id=0) or ESP-NOW unicast to one slave. */