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