Add find-me LED locate on master and slaves via ESP-NOW.

UART FIND_ME (client_id 0 = local ring, >0 = unicast), ESPNOW_FIND_ME payload, CLI/dashboard buttons per slave.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-19 22:04:07 +02:00
co-authored by Cursor
parent 8931912583
commit efd6260201
25 changed files with 659 additions and 84 deletions
+3 -2
View File
@@ -29,7 +29,8 @@ 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`, `-progress`, `-digit`, RGB, `-intensity` (0 = ~5 %), `-blink-ms`, `-blink-count` |
| `led-ring` | 8 | LED ring: `-mode clear\|progress\|digit\|blink\|find-me`, |
| `find-me` | 22 | Locate pod (`-client 0` master, `>0` slave via ESP-NOW) |
`clients` requires slaves to have responded to master discover broadcasts first.
@@ -70,7 +71,7 @@ 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`, `POST /api/unicast-test`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test`, `POST /api/find-me`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
| UI / API | Behaviour |
|----------|-----------|
+33
View File
@@ -40,6 +40,16 @@ type unicastAPIResponse struct {
Error string `json:"error,omitempty"`
}
type findMeAPIRequest struct {
ClientID uint32 `json:"client_id"`
}
type findMeAPIResponse struct {
Success bool `json:"success"`
ClientID uint32 `json:"client_id,omitempty"`
Error string `json:"error,omitempty"`
}
type otaAPIResponse struct {
Success bool `json:"success"`
BytesWritten uint32 `json:"bytes_written,omitempty"`
@@ -65,6 +75,13 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub) {
}
serveUnicastTest(w, r, link)
})
mux.HandleFunc("/api/find-me", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
serveFindMe(w, r, link)
})
mux.HandleFunc("/api/ota", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -216,6 +233,22 @@ func applyDeadzoneToSlaves(link *managedSerial, deadzone uint32) (uint32, error)
return updated, nil
}
func serveFindMe(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body findMeAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, findMeAPIResponse{Error: "invalid JSON"})
return
}
if err := link.FindMe(body.ClientID); err != nil {
writeJSON(w, http.StatusServiceUnavailable, findMeAPIResponse{
ClientID: body.ClientID,
Error: err.Error(),
})
return
}
writeJSON(w, http.StatusOK, findMeAPIResponse{Success: true, ClientID: body.ClientID})
}
func serveUnicastTest(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body unicastAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+6
View File
@@ -172,6 +172,12 @@ func (s *serialPort) espnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastT
return r, nil
}
func (m *managedSerial) FindMe(clientID uint32) error {
return m.withPort(func(sp *serialPort) error {
return runFindMeClient(sp, clientID)
})
}
func (s *serialPort) ledRingProgress(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_LED_RING,
+61
View File
@@ -0,0 +1,61 @@
package main
import (
"flag"
"fmt"
"google.golang.org/protobuf/proto"
"powerpod/gotool/pb"
)
func runFindMe(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("find-me", flag.ExitOnError)
clientID := fs.Uint("client", 0, "0=master LED ring, >0=ESP-NOW unicast to slave id")
if err := fs.Parse(args); err != nil {
return err
}
return runFindMeClient(sp, uint32(*clientID))
}
func runFindMeClient(sp *serialPort, clientID uint32) error {
resp, err := sp.espnowFindMe(clientID)
if err != nil {
return err
}
if !resp.GetSuccess() {
return fmt.Errorf("find-me rejected (client_id=%d)", resp.GetClientId())
}
if clientID == 0 {
fmt.Println("find-me started on master")
} else {
fmt.Printf("find-me sent to slave %d\n", clientID)
}
return nil
}
func (s *serialPort) espnowFindMe(clientID uint32) (*pb.EspNowFindMeResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_FIND_ME,
Payload: &pb.UartMessage_EspnowFindMeRequest{
EspnowFindMeRequest: &pb.EspNowFindMeRequest{ClientId: clientID},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_FIND_ME)}, body...)
respPayload, err := s.exchangePayload(payload, "FIND_ME")
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetEspnowFindMeResponse()
if r == nil {
return nil, fmt.Errorf("missing espnow_find_me_response")
}
return r, nil
}
+5 -2
View File
@@ -12,11 +12,12 @@ const (
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, or blink")
mode := fs.String("mode", "progress", "clear, progress, digit, blink, or find-me")
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")
@@ -39,8 +40,10 @@ func runLedRing(sp *serialPort, args []string) error {
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)", *mode)
return fmt.Errorf("unknown -mode %q (clear, progress, digit, blink, find-me)", *mode)
}
resp, err := sp.ledRingProgress(&pb.LedRingProgressRequest{
+5 -2
View File
@@ -21,7 +21,8 @@ func usage() {
fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n")
fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions)\n")
fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n")
fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0100%%, rgb, intensity)\n\n")
fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0100%%, rgb, intensity)\n")
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n\n")
flag.PrintDefaults()
}
@@ -48,7 +49,7 @@ func main() {
os.Exit(2)
}
runErr = runServe(*portName, *baud, flag.Args()[1:])
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "unicast-test", "unicast_test", "led-ring", "led_ring", "ota", "ota-progress", "ota_progress":
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "unicast-test", "unicast_test", "led-ring", "led_ring", "find-me", "find_me", "ota", "ota-progress", "ota_progress":
if *portName == "" {
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
usage()
@@ -70,6 +71,8 @@ func main() {
runErr = runUnicastTest(sp, flag.Args()[1:])
case "led-ring", "led_ring":
runErr = runLedRing(sp, flag.Args()[1:])
case "find-me", "find_me":
runErr = runFindMe(sp, flag.Args()[1:])
case "ota":
runErr = runOTA(sp, flag.Args()[1:])
case "ota-progress", "ota_progress":
+197 -52
View File
@@ -39,6 +39,7 @@ const (
MessageType_OTA_STATUS MessageType = 19
MessageType_OTA_START_ESPNOW MessageType = 20
MessageType_OTA_SLAVE_PROGRESS MessageType = 21
MessageType_FIND_ME MessageType = 22
)
// Enum value maps for MessageType.
@@ -59,6 +60,7 @@ var (
19: "OTA_STATUS",
20: "OTA_START_ESPNOW",
21: "OTA_SLAVE_PROGRESS",
22: "FIND_ME",
}
MessageType_value = map[string]int32{
"UNKNOWN": 0,
@@ -76,6 +78,7 @@ var (
"OTA_STATUS": 19,
"OTA_START_ESPNOW": 20,
"OTA_SLAVE_PROGRESS": 21,
"FIND_ME": 22,
}
)
@@ -128,6 +131,8 @@ type UartMessage struct {
// *UartMessage_OtaSlaveProgressResponse
// *UartMessage_LedRingProgressRequest
// *UartMessage_LedRingProgressResponse
// *UartMessage_EspnowFindMeRequest
// *UartMessage_EspnowFindMeResponse
Payload isUartMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -330,6 +335,24 @@ func (x *UartMessage) GetLedRingProgressResponse() *LedRingProgressResponse {
return nil
}
func (x *UartMessage) GetEspnowFindMeRequest() *EspNowFindMeRequest {
if x != nil {
if x, ok := x.Payload.(*UartMessage_EspnowFindMeRequest); ok {
return x.EspnowFindMeRequest
}
}
return nil
}
func (x *UartMessage) GetEspnowFindMeResponse() *EspNowFindMeResponse {
if x != nil {
if x, ok := x.Payload.(*UartMessage_EspnowFindMeResponse); ok {
return x.EspnowFindMeResponse
}
}
return nil
}
type isUartMessage_Payload interface {
isUartMessage_Payload()
}
@@ -402,6 +425,14 @@ type UartMessage_LedRingProgressResponse struct {
LedRingProgressResponse *LedRingProgressResponse `protobuf:"bytes,18,opt,name=led_ring_progress_response,json=ledRingProgressResponse,proto3,oneof"`
}
type UartMessage_EspnowFindMeRequest struct {
EspnowFindMeRequest *EspNowFindMeRequest `protobuf:"bytes,19,opt,name=espnow_find_me_request,json=espnowFindMeRequest,proto3,oneof"`
}
type UartMessage_EspnowFindMeResponse struct {
EspnowFindMeResponse *EspNowFindMeResponse `protobuf:"bytes,20,opt,name=espnow_find_me_response,json=espnowFindMeResponse,proto3,oneof"`
}
func (*UartMessage_AckPayload) isUartMessage_Payload() {}
func (*UartMessage_EchoPayload) isUartMessage_Payload() {}
@@ -436,6 +467,10 @@ func (*UartMessage_LedRingProgressRequest) isUartMessage_Payload() {}
func (*UartMessage_LedRingProgressResponse) isUartMessage_Payload() {}
func (*UartMessage_EspnowFindMeRequest) isUartMessage_Payload() {}
func (*UartMessage_EspnowFindMeResponse) isUartMessage_Payload() {}
type Ack struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@@ -1068,8 +1103,8 @@ func (x *EspNowUnicastTestResponse) GetSeq() uint32 {
return 0
}
// Host → device: LED ring display (progress bar, digit, clear, or blink).
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink full ring.
// 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).
type LedRingProgressRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Mode uint32 `protobuf:"varint,1,opt,name=mode,proto3" json:"mode,omitempty"`
@@ -1251,6 +1286,103 @@ func (x *LedRingProgressResponse) GetDigit() uint32 {
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"`
ClientId uint32 `protobuf:"varint,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EspNowFindMeRequest) Reset() {
*x = EspNowFindMeRequest{}
mi := &file_uart_messages_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EspNowFindMeRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EspNowFindMeRequest) ProtoMessage() {}
func (x *EspNowFindMeRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EspNowFindMeRequest.ProtoReflect.Descriptor instead.
func (*EspNowFindMeRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{14}
}
func (x *EspNowFindMeRequest) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
type EspNowFindMeResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
ClientId uint32 `protobuf:"varint,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EspNowFindMeResponse) Reset() {
*x = EspNowFindMeResponse{}
mi := &file_uart_messages_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EspNowFindMeResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EspNowFindMeResponse) ProtoMessage() {}
func (x *EspNowFindMeResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EspNowFindMeResponse.ProtoReflect.Descriptor instead.
func (*EspNowFindMeResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{15}
}
func (x *EspNowFindMeResponse) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
func (x *EspNowFindMeResponse) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
type OtaStartPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -1261,7 +1393,7 @@ type OtaStartPayload struct {
func (x *OtaStartPayload) Reset() {
*x = OtaStartPayload{}
mi := &file_uart_messages_proto_msgTypes[14]
mi := &file_uart_messages_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1273,7 +1405,7 @@ func (x *OtaStartPayload) String() string {
func (*OtaStartPayload) ProtoMessage() {}
func (x *OtaStartPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[14]
mi := &file_uart_messages_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1286,7 +1418,7 @@ func (x *OtaStartPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaStartPayload.ProtoReflect.Descriptor instead.
func (*OtaStartPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{14}
return file_uart_messages_proto_rawDescGZIP(), []int{16}
}
func (x *OtaStartPayload) GetTotalSize() uint32 {
@@ -1307,7 +1439,7 @@ type OtaPayload struct {
func (x *OtaPayload) Reset() {
*x = OtaPayload{}
mi := &file_uart_messages_proto_msgTypes[15]
mi := &file_uart_messages_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1319,7 +1451,7 @@ func (x *OtaPayload) String() string {
func (*OtaPayload) ProtoMessage() {}
func (x *OtaPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[15]
mi := &file_uart_messages_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1332,7 +1464,7 @@ func (x *OtaPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaPayload.ProtoReflect.Descriptor instead.
func (*OtaPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{15}
return file_uart_messages_proto_rawDescGZIP(), []int{17}
}
func (x *OtaPayload) GetSeq() uint32 {
@@ -1358,7 +1490,7 @@ type OtaEndPayload struct {
func (x *OtaEndPayload) Reset() {
*x = OtaEndPayload{}
mi := &file_uart_messages_proto_msgTypes[16]
mi := &file_uart_messages_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1370,7 +1502,7 @@ func (x *OtaEndPayload) String() string {
func (*OtaEndPayload) ProtoMessage() {}
func (x *OtaEndPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[16]
mi := &file_uart_messages_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1383,7 +1515,7 @@ func (x *OtaEndPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaEndPayload.ProtoReflect.Descriptor instead.
func (*OtaEndPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{16}
return file_uart_messages_proto_rawDescGZIP(), []int{18}
}
// Device → host status (also used as ACK after each 4 KiB written).
@@ -1400,7 +1532,7 @@ type OtaStatusPayload struct {
func (x *OtaStatusPayload) Reset() {
*x = OtaStatusPayload{}
mi := &file_uart_messages_proto_msgTypes[17]
mi := &file_uart_messages_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1412,7 +1544,7 @@ func (x *OtaStatusPayload) String() string {
func (*OtaStatusPayload) ProtoMessage() {}
func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[17]
mi := &file_uart_messages_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1425,7 +1557,7 @@ func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaStatusPayload.ProtoReflect.Descriptor instead.
func (*OtaStatusPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{17}
return file_uart_messages_proto_rawDescGZIP(), []int{19}
}
func (x *OtaStatusPayload) GetStatus() uint32 {
@@ -1466,7 +1598,7 @@ type OtaSlaveProgressRequest struct {
func (x *OtaSlaveProgressRequest) Reset() {
*x = OtaSlaveProgressRequest{}
mi := &file_uart_messages_proto_msgTypes[18]
mi := &file_uart_messages_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1478,7 +1610,7 @@ func (x *OtaSlaveProgressRequest) String() string {
func (*OtaSlaveProgressRequest) ProtoMessage() {}
func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[18]
mi := &file_uart_messages_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1491,7 +1623,7 @@ func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressRequest.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{18}
return file_uart_messages_proto_rawDescGZIP(), []int{20}
}
func (x *OtaSlaveProgressRequest) GetClientId() uint32 {
@@ -1515,7 +1647,7 @@ type OtaSlaveProgressEntry struct {
func (x *OtaSlaveProgressEntry) Reset() {
*x = OtaSlaveProgressEntry{}
mi := &file_uart_messages_proto_msgTypes[19]
mi := &file_uart_messages_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1527,7 +1659,7 @@ func (x *OtaSlaveProgressEntry) String() string {
func (*OtaSlaveProgressEntry) ProtoMessage() {}
func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[19]
mi := &file_uart_messages_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1540,7 +1672,7 @@ func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressEntry.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressEntry) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{19}
return file_uart_messages_proto_rawDescGZIP(), []int{21}
}
func (x *OtaSlaveProgressEntry) GetClientId() uint32 {
@@ -1591,7 +1723,7 @@ type OtaSlaveProgressResponse struct {
func (x *OtaSlaveProgressResponse) Reset() {
*x = OtaSlaveProgressResponse{}
mi := &file_uart_messages_proto_msgTypes[20]
mi := &file_uart_messages_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1603,7 +1735,7 @@ func (x *OtaSlaveProgressResponse) String() string {
func (*OtaSlaveProgressResponse) ProtoMessage() {}
func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[20]
mi := &file_uart_messages_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1616,7 +1748,7 @@ func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressResponse.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{20}
return file_uart_messages_proto_rawDescGZIP(), []int{22}
}
func (x *OtaSlaveProgressResponse) GetActive() bool {
@@ -1658,8 +1790,7 @@ var File_uart_messages_proto protoreflect.FileDescriptor
const file_uart_messages_proto_rawDesc = "" +
"\n" +
"\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\xc4\n" +
"\n" +
"\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\xeb\v\n" +
"\vUartMessage\x12%\n" +
"\x04type\x18\x01 \x01(\x0e2\x11.alox.MessageTypeR\x04type\x12,\n" +
"\vack_payload\x18\x02 \x01(\v2\t.alox.AckH\x00R\n" +
@@ -1682,7 +1813,9 @@ const file_uart_messages_proto_rawDesc = "" +
"\x1aota_slave_progress_request\x18\x0f \x01(\v2\x1d.alox.OtaSlaveProgressRequestH\x00R\x17otaSlaveProgressRequest\x12_\n" +
"\x1bota_slave_progress_response\x18\x10 \x01(\v2\x1e.alox.OtaSlaveProgressResponseH\x00R\x18otaSlaveProgressResponse\x12Y\n" +
"\x19led_ring_progress_request\x18\x11 \x01(\v2\x1c.alox.LedRingProgressRequestH\x00R\x16ledRingProgressRequest\x12\\\n" +
"\x1aled_ring_progress_response\x18\x12 \x01(\v2\x1d.alox.LedRingProgressResponseH\x00R\x17ledRingProgressResponseB\t\n" +
"\x1aled_ring_progress_response\x18\x12 \x01(\v2\x1d.alox.LedRingProgressResponseH\x00R\x17ledRingProgressResponse\x12P\n" +
"\x16espnow_find_me_request\x18\x13 \x01(\v2\x19.alox.EspNowFindMeRequestH\x00R\x13espnowFindMeRequest\x12S\n" +
"\x17espnow_find_me_response\x18\x14 \x01(\v2\x1a.alox.EspNowFindMeResponseH\x00R\x14espnowFindMeResponseB\t\n" +
"\apayload\"\x05\n" +
"\x03Ack\"!\n" +
"\vEchoPayload\x12\x12\n" +
@@ -1741,7 +1874,12 @@ const file_uart_messages_proto_rawDesc = "" +
"\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\"0\n" +
"\x05digit\x18\x04 \x01(\rR\x05digit\"2\n" +
"\x13EspNowFindMeRequest\x12\x1b\n" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\"M\n" +
"\x14EspNowFindMeResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1b\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\"0\n" +
"\x0fOtaStartPayload\x12\x1d\n" +
"\n" +
"total_size\x18\x01 \x01(\rR\ttotalSize\":\n" +
@@ -1772,7 +1910,7 @@ const file_uart_messages_proto_rawDesc = "" +
"\x0faggregate_bytes\x18\x03 \x01(\rR\x0eaggregateBytes\x12\x1f\n" +
"\vslave_count\x18\x04 \x01(\rR\n" +
"slaveCount\x12:\n" +
"\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\x83\x02\n" +
"\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\x90\x02\n" +
"\vMessageType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\a\n" +
"\x03ACK\x10\x01\x12\b\n" +
@@ -1789,7 +1927,8 @@ const file_uart_messages_proto_rawDesc = "" +
"\n" +
"OTA_STATUS\x10\x13\x12\x14\n" +
"\x10OTA_START_ESPNOW\x10\x14\x12\x16\n" +
"\x12OTA_SLAVE_PROGRESS\x10\x15b\x06proto3"
"\x12OTA_SLAVE_PROGRESS\x10\x15\x12\v\n" +
"\aFIND_ME\x10\x16b\x06proto3"
var (
file_uart_messages_proto_rawDescOnce sync.Once
@@ -1804,7 +1943,7 @@ func file_uart_messages_proto_rawDescGZIP() []byte {
}
var file_uart_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
var file_uart_messages_proto_goTypes = []any{
(MessageType)(0), // 0: alox.MessageType
(*UartMessage)(nil), // 1: alox.UartMessage
@@ -1821,13 +1960,15 @@ var file_uart_messages_proto_goTypes = []any{
(*EspNowUnicastTestResponse)(nil), // 12: alox.EspNowUnicastTestResponse
(*LedRingProgressRequest)(nil), // 13: alox.LedRingProgressRequest
(*LedRingProgressResponse)(nil), // 14: alox.LedRingProgressResponse
(*OtaStartPayload)(nil), // 15: alox.OtaStartPayload
(*OtaPayload)(nil), // 16: alox.OtaPayload
(*OtaEndPayload)(nil), // 17: alox.OtaEndPayload
(*OtaStatusPayload)(nil), // 18: alox.OtaStatusPayload
(*OtaSlaveProgressRequest)(nil), // 19: alox.OtaSlaveProgressRequest
(*OtaSlaveProgressEntry)(nil), // 20: alox.OtaSlaveProgressEntry
(*OtaSlaveProgressResponse)(nil), // 21: alox.OtaSlaveProgressResponse
(*EspNowFindMeRequest)(nil), // 15: alox.EspNowFindMeRequest
(*EspNowFindMeResponse)(nil), // 16: alox.EspNowFindMeResponse
(*OtaStartPayload)(nil), // 17: alox.OtaStartPayload
(*OtaPayload)(nil), // 18: alox.OtaPayload
(*OtaEndPayload)(nil), // 19: alox.OtaEndPayload
(*OtaStatusPayload)(nil), // 20: alox.OtaStatusPayload
(*OtaSlaveProgressRequest)(nil), // 21: alox.OtaSlaveProgressRequest
(*OtaSlaveProgressEntry)(nil), // 22: alox.OtaSlaveProgressEntry
(*OtaSlaveProgressResponse)(nil), // 23: alox.OtaSlaveProgressResponse
}
var file_uart_messages_proto_depIdxs = []int32{
0, // 0: alox.UartMessage.type:type_name -> alox.MessageType
@@ -1836,26 +1977,28 @@ var file_uart_messages_proto_depIdxs = []int32{
4, // 3: alox.UartMessage.version_response:type_name -> alox.VersionResponse
6, // 4: alox.UartMessage.client_info_response:type_name -> alox.ClientInfoResponse
8, // 5: alox.UartMessage.client_input_response:type_name -> alox.ClientInputResponse
15, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload
16, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload
17, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload
18, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload
17, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload
18, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload
19, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload
20, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload
9, // 10: alox.UartMessage.accel_deadzone_request:type_name -> alox.AccelDeadzoneRequest
10, // 11: alox.UartMessage.accel_deadzone_response:type_name -> alox.AccelDeadzoneResponse
11, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest
12, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse
19, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest
21, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse
21, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest
23, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse
13, // 16: alox.UartMessage.led_ring_progress_request:type_name -> alox.LedRingProgressRequest
14, // 17: alox.UartMessage.led_ring_progress_response:type_name -> alox.LedRingProgressResponse
5, // 18: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo
7, // 19: alox.ClientInputResponse.clients:type_name -> alox.ClientInput
20, // 20: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry
21, // [21:21] is the sub-list for method output_type
21, // [21:21] is the sub-list for method input_type
21, // [21:21] is the sub-list for extension type_name
21, // [21:21] is the sub-list for extension extendee
0, // [0:21] is the sub-list for field type_name
15, // 18: alox.UartMessage.espnow_find_me_request:type_name -> alox.EspNowFindMeRequest
16, // 19: alox.UartMessage.espnow_find_me_response:type_name -> alox.EspNowFindMeResponse
5, // 20: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo
7, // 21: alox.ClientInputResponse.clients:type_name -> alox.ClientInput
22, // 22: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry
23, // [23:23] is the sub-list for method output_type
23, // [23:23] is the sub-list for method input_type
23, // [23:23] is the sub-list for extension type_name
23, // [23:23] is the sub-list for extension extendee
0, // [0:23] is the sub-list for field type_name
}
func init() { file_uart_messages_proto_init() }
@@ -1881,6 +2024,8 @@ func file_uart_messages_proto_init() {
(*UartMessage_OtaSlaveProgressResponse)(nil),
(*UartMessage_LedRingProgressRequest)(nil),
(*UartMessage_LedRingProgressResponse)(nil),
(*UartMessage_EspnowFindMeRequest)(nil),
(*UartMessage_EspnowFindMeResponse)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -1888,7 +2033,7 @@ func file_uart_messages_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_uart_messages_proto_rawDesc), len(file_uart_messages_proto_rawDesc)),
NumEnums: 1,
NumMessages: 21,
NumMessages: 23,
NumExtensions: 0,
NumServices: 0,
},
+33
View File
@@ -227,6 +227,12 @@
:disabled="busy || !state.uart_connected">
Setzen
</button>
<button type="button" class="btn btn-outline-warning btn-sm"
@click="findMe()"
:disabled="busy || !state.uart_connected"
title="LED-Ring: Rot/Grün/Blau je 3×">
Find me
</button>
</div>
<p class="text-muted small mt-2 mb-0" x-show="!state.uart_connected">
UART nicht verbunden — Eingabe gesperrt.
@@ -300,6 +306,12 @@
title="ESP-NOW Unicast-Test">
Test
</button>
<button type="button" class="btn btn-outline-warning btn-sm"
@click="findMe(c.id)"
:disabled="busy || !state.uart_connected || !c.available"
title="LED-Ring Find me (ESP-NOW)">
Find me
</button>
</div>
</td>
</tr>
@@ -726,6 +738,27 @@
} finally {
this.busy = false;
}
},
async findMe(clientId = 0) {
this.busy = true;
try {
const r = await fetch('/api/find-me', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId })
});
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || 'Find me fehlgeschlagen', false);
return;
}
const label = clientId === 0 ? 'Master' : `Slave ${clientId}`;
this.flash(`Find me gestartet (${label})`, true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
}
};
}