diff --git a/goTool/README.md b/goTool/README.md index 51fe065..6a1c555 100644 --- a/goTool/README.md +++ b/goTool/README.md @@ -31,7 +31,7 @@ go run . -port /dev/ttyUSB0 clients | `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) | | `ota` | 16–19 | 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` (0–100), `digit` (0–10 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): diff --git a/goTool/api_led_ring.go b/goTool/api_led_ring.go new file mode 100644 index 0000000..1e50fc1 --- /dev/null +++ b/goTool/api_led_ring.go @@ -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) +} diff --git a/goTool/api_serve.go b/goTool/api_serve.go index 9de85bf..18dce13 100644 --- a/goTool/api_serve.go +++ b/goTool/api_serve.go @@ -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: diff --git a/goTool/api_stream.go b/goTool/api_stream.go index 7e7b6f8..ccd8de1 100644 --- a/goTool/api_stream.go +++ b/goTool/api_stream.go @@ -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)", + Type: "stream_status", + 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() { diff --git a/goTool/autotest/runner.go b/goTool/autotest/runner.go index 955f7b0..4391730 100644 --- a/goTool/autotest/runner.go +++ b/goTool/autotest/runner.go @@ -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) } diff --git a/goTool/client_api.go b/goTool/client_api.go index 5bf7920..cd681cf 100644 --- a/goTool/client_api.go +++ b/goTool/client_api.go @@ -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) } diff --git a/goTool/cmd_led_ring.go b/goTool/cmd_led_ring.go index d131845..3404a54 100644 --- a/goTool/cmd_led_ring.go +++ b/goTool/cmd_led_ring.go @@ -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 0–100 (mode=progress)") digit := fs.Uint("digit", 0, "digit 0–10 (mode=digit)") r := fs.Uint("r", 0, "red 0–255") @@ -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 } diff --git a/goTool/led_ring_api.go b/goTool/led_ring_api.go new file mode 100644 index 0000000..ff9b852 --- /dev/null +++ b/goTool/led_ring_api.go @@ -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 +} diff --git a/goTool/pb/uart_messages.pb.go b/goTool/pb/uart_messages.pb.go index e03a68b..7ea84bc 100644 --- a/goTool/pb/uart_messages.pb.go +++ b/goTool/pb/uart_messages.pb.go @@ -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 (0–100 %), 2=digit (0–10), 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 (0–100 %), 2=digit (0–10), 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" + diff --git a/goTool/webui/index.html b/goTool/webui/index.html index 5b66838..31165c0 100644 --- a/goTool/webui/index.html +++ b/goTool/webui/index.html @@ -331,11 +331,17 @@ title="ESP-NOW Unicast-Test"> Test - + + + + + + + + +
Firmware OTA (A/B)
@@ -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 { diff --git a/main/README.md b/main/README.md index 2370200..955e54c 100644 --- a/main/README.md +++ b/main/README.md @@ -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 (0–10), `3` = blink, `4` = find-me, `5` = solid color (all LEDs) | | `progress` | 0–100 (% of ring lit, mode `1`) | -| `digit` | 0–10 (mode `2`, same segment maps as built-in digits) | +| `digit` | 0–10 (mode `2`, segment maps in `led_ring.c`) | | `r`, `g`, `b` | Color 0–255 | | `intensity` | Brightness 0–255 (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 diff --git a/main/cmd/cmd_led_ring.c b/main/cmd/cmd_led_ring.c index 5672ecd..c3874f5 100644 --- a/main/cmd/cmd_led_ring.c +++ b/main/cmd/cmd_led_ring.c @@ -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); - 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; + 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++; + } } - 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); - 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; + bool local_ok = true; + if (!req.slaves_only) { + local_ok = cmd_led_ring_apply(&req); } - 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); + 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; } - default: - ESP_LOGW(TAG, "unknown mode %lu", (unsigned long)mode); - reply(false, mode, 0, 0); + 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; } + + 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; + } + + 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) { diff --git a/main/cmd/cmd_led_ring.h b/main/cmd/cmd_led_ring.h index 89b2487..f35f686 100644 --- a/main/cmd/cmd_led_ring.h +++ b/main/cmd/cmd_led_ring.h @@ -1,6 +1,12 @@ #ifndef CMD_LED_RING_H #define CMD_LED_RING_H +#include +#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 diff --git a/main/esp_now_comm.c b/main/esp_now_comm.c index ec7a758..9f417b8 100644 --- a/main/esp_now_comm.c +++ b/main/esp_now_comm.c @@ -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; diff --git a/main/esp_now_comm.h b/main/esp_now_comm.h index b34252e..51a7906 100644 --- a/main/esp_now_comm.h +++ b/main/esp_now_comm.h @@ -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); diff --git a/main/led_ring.c b/main/led_ring.c index e0641b6..9ed05c5 100644 --- a/main/led_ring.c +++ b/main/led_ring.c @@ -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) { diff --git a/main/proto/esp_now_messages.pb.c b/main/proto/esp_now_messages.pb.c index aca1be1..955bd86 100644 --- a/main/proto/esp_now_messages.pb.c +++ b/main/proto/esp_now_messages.pb.c @@ -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) diff --git a/main/proto/esp_now_messages.pb.h b/main/proto/esp_now_messages.pb.h index be8e98e..8fe2f31 100644 --- a/main/proto/esp_now_messages.pb.h +++ b/main/proto/esp_now_messages.pb.h @@ -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 diff --git a/main/proto/esp_now_messages.proto b/main/proto/esp_now_messages.proto index def3642..ca6c6c4 100644 --- a/main/proto/esp_now_messages.proto +++ b/main/proto/esp_now_messages.proto @@ -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; } } diff --git a/main/proto/uart_messages.pb.h b/main/proto/uart_messages.pb.h index 950d8a6..0ad43df 100644 --- a/main/proto/uart_messages.pb.h +++ b/main/proto/uart_messages.pb.h @@ -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 (0–100 %), 2=digit (0–10), 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 (0–100 %), 2=digit (0–10), 3=blink, 4=find-me, 5=all LEDs solid color. */ typedef struct _alox_LedRingProgressRequest { uint32_t mode; /* * 0–100: 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 diff --git a/main/proto/uart_messages.proto b/main/proto/uart_messages.proto index 20b63e0..1d2b89f 100644 --- a/main/proto/uart_messages.proto +++ b/main/proto/uart_messages.proto @@ -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 (0–100 %), 2=digit (0–10), 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 (0–100 %), 2=digit (0–10), 3=blink, 4=find-me, 5=all LEDs solid color. message LedRingProgressRequest { uint32 mode = 1; /** 0–100: 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. */