Assign sequential pod IDs from the master with NVS-stable bindings.

Replaces MAC-byte self-IDs with master allocation (1–255), slave preferred-ID reconnect, debug reset via UART/UI, and rate-limited ASSIGN to avoid ESP-NOW NO_MEM storms.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-01 17:25:07 +01:00
co-authored by Cursor
parent af20635047
commit 567dab6d0a
26 changed files with 1261 additions and 163 deletions
+25
View File
@@ -64,6 +64,11 @@ type findMeAPIResponse struct {
Error string `json:"error,omitempty"`
}
type resetIdMapAPIResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
type restartAPIRequest struct {
ClientID uint32 `json:"client_id"`
}
@@ -139,6 +144,9 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCt
}
serveRestart(w, r, link)
})
mux.HandleFunc("POST /api/clients/reset-id-map", func(w http.ResponseWriter, r *http.Request) {
serveResetClientIdMap(w, r, link)
})
mux.HandleFunc("/api/log-level", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
@@ -361,6 +369,23 @@ func serveRestart(w http.ResponseWriter, r *http.Request, link *managedSerial) {
writeJSON(w, http.StatusOK, restartAPIResponse{Success: true, ClientID: body.ClientID})
}
func serveResetClientIdMap(w http.ResponseWriter, r *http.Request, link *managedSerial) {
if !link.IsConnected() {
writeJSON(w, http.StatusServiceUnavailable, resetIdMapAPIResponse{Error: "host not connected"})
return
}
resp, err := link.resetClientIdMap()
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, resetIdMapAPIResponse{Error: err.Error()})
return
}
if !resp.GetSuccess() {
writeJSON(w, http.StatusOK, resetIdMapAPIResponse{Success: false, Error: "rejected by device"})
return
}
writeJSON(w, http.StatusOK, resetIdMapAPIResponse{Success: true})
}
func serveFindMe(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body findMeAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+4 -7
View File
@@ -84,13 +84,7 @@ func NewBench(cfg Config) (*Bench, error) {
return nil, fmt.Errorf("config %q slave %q: %w", cfg.ID, s.ID, err)
}
s.MAC = mac
if s.ClientID == nil {
parts := strings.Split(mac, ":")
var last byte
fmt.Sscanf(parts[5], "%02x", &last)
id := uint(last)
s.ClientID = &id
}
/* client_id is master-assigned (sequential); omit until known from `gotool clients`. */
b.slaveByID[s.ID] = s
}
return b, nil
@@ -112,6 +106,9 @@ func (b *Bench) ResolveClientID(slaveID string) (uint32, error) {
if err != nil {
return 0, err
}
if s.ClientID == nil {
return 0, fmt.Errorf("slave %q: client_id required (master-assigned; set in config after join)", slaveID)
}
return uint32(*s.ClientID), nil
}
+34 -3
View File
@@ -2,7 +2,7 @@ package autotest
import "testing"
func TestNewBenchClientIDFromMAC(t *testing.T) {
func TestNewBenchNoDefaultClientIDFromMAC(t *testing.T) {
cfg := Config{
ID: "t",
Network: 1,
@@ -20,8 +20,39 @@ func TestNewBenchClientIDFromMAC(t *testing.T) {
t.Fatal(err)
}
s, _ := b.Slave("pod")
if *s.ClientID != 16 {
t.Fatalf("client_id=%d want 16", *s.ClientID)
if s.ClientID != nil {
t.Fatalf("client_id should be unset, got %d", *s.ClientID)
}
if _, err := b.ResolveClientID("pod"); err == nil {
t.Fatal("ResolveClientID should fail without client_id")
}
}
func TestNewBenchExplicitClientID(t *testing.T) {
id := uint(3)
cfg := Config{
ID: "t",
Network: 1,
MasterMAC: "aa:bb:cc:dd:ee:ff",
UART: UARTConfig{
Master: "/dev/ttyUSB0",
},
Slaves: []SlaveNode{{
ID: "pod",
MAC: "50:78:7d:18:01:10",
ClientID: &id,
}},
}
b, err := NewBench(cfg)
if err != nil {
t.Fatal(err)
}
got, err := b.ResolveClientID("pod")
if err != nil {
t.Fatal(err)
}
if got != 3 {
t.Fatalf("client_id=%d want 3", got)
}
}
+1 -1
View File
@@ -195,7 +195,7 @@ func checkClients(bench *Bench, step Step, client MasterClient) error {
if found == nil {
return fmt.Errorf("slave %q mac %s not in client list", name, want.MAC)
}
if uint32(*want.ClientID) != found.GetId() {
if want.ClientID != nil && uint32(*want.ClientID) != found.GetId() {
return fmt.Errorf("slave %q id=%d want client_id=%d", name, found.GetId(), *want.ClientID)
}
if e.Available != nil && found.GetAvailable() != *e.Available {
+55
View File
@@ -0,0 +1,55 @@
package main
import (
"fmt"
"google.golang.org/protobuf/proto"
"powerpod/gotool/pb"
)
func runClientsResetIds(sp *serialPort) error {
resp, err := sp.resetClientIdMap()
if err != nil {
return err
}
if !resp.GetSuccess() {
return fmt.Errorf("client id map reset rejected")
}
fmt.Println("client id map cleared (master map + live registry; online slaves notified)")
return nil
}
func decodeClientIdMapResetPayload(payload []byte) (*pb.ClientIdMapResetResponse, error) {
if len(payload) < 2 {
return nil, fmt.Errorf("short client id map reset response")
}
if payload[0] != byte(pb.MessageType_CLIENT_ID_MAP_RESET) {
return nil, fmt.Errorf("unexpected command id 0x%02x (want 0x%02x)",
payload[0], byte(pb.MessageType_CLIENT_ID_MAP_RESET))
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(payload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetClientIdMapResetResponse()
if r == nil {
return nil, fmt.Errorf("missing client_id_map_reset_response")
}
return r, nil
}
func (s *serialPort) resetClientIdMap() (*pb.ClientIdMapResetResponse, error) {
payload, err := s.exchange(byte(pb.MessageType_CLIENT_ID_MAP_RESET), "CLIENT_ID_MAP_RESET")
if err != nil {
return nil, err
}
return decodeClientIdMapResetPayload(payload)
}
func (m *managedSerial) resetClientIdMap() (*pb.ClientIdMapResetResponse, error) {
payload, err := m.exchange(byte(pb.MessageType_CLIENT_ID_MAP_RESET), "CLIENT_ID_MAP_RESET")
if err != nil {
return nil, err
}
return decodeClientIdMapResetPayload(payload)
}
+10 -2
View File
@@ -16,7 +16,8 @@ func usage() {
fmt.Fprintf(os.Stderr, " test uses uart.master from bench config when -port is omitted\n\n")
fmt.Fprintf(os.Stderr, "commands:\n")
fmt.Fprintf(os.Stderr, " version firmware version and git hash\n")
fmt.Fprintf(os.Stderr, " clients registered ESP-NOW slaves on the master\n")
fmt.Fprintf(os.Stderr, " clients registered ESP-NOW slaves on the master\n")
fmt.Fprintf(os.Stderr, " clients-reset-ids clear master id map + live registry (notifies online slaves)\n")
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n")
fmt.Fprintf(os.Stderr, " tap-config get/set BMA456 tap detection parameters\n")
fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n")
@@ -72,7 +73,7 @@ func main() {
bleName: *bleName,
settle: time.Duration(*bleSettle * float64(time.Second)),
}, flag.Args()[1:])
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-config", "tap_config", "tap-notify", "tap_notify", "cache-status", "cache_status", "accel-stream", "accel_stream", "unicast-test", "unicast_test", "echo-ping", "echo_ping", "ble-rtt", "ble_rtt", "ble-stream", "ble_stream", "led-ring", "led_ring", "find-me", "find_me", "restart", "log-level", "log_level", "ota", "ota-progress", "ota_progress":
case "version", "clients", "client-info", "clients-reset-ids", "clients_reset_ids", "deadzone", "accel-deadzone", "tap-config", "tap_config", "tap-notify", "tap_notify", "cache-status", "cache_status", "accel-stream", "accel_stream", "unicast-test", "unicast_test", "echo-ping", "echo_ping", "ble-rtt", "ble_rtt", "ble-stream", "ble_stream", "led-ring", "led_ring", "find-me", "find_me", "restart", "log-level", "log_level", "ota", "ota-progress", "ota_progress":
if *useBLE && *portName != "" {
fmt.Fprintf(os.Stderr, "use either -port or -ble, not both\n")
os.Exit(2)
@@ -112,6 +113,13 @@ func main() {
runErr = runVersionHost(h)
case "clients", "client-info":
runErr = runClientsHost(h)
case "clients-reset-ids", "clients_reset_ids":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runClientsResetIds(sp)
case "cache-status", "cache_status":
runErr = runCacheStatusHost(h)
case "accel-stream", "accel_stream":
+210 -79
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v7.36.0
// protoc v7.36.1
// source: uart_messages.proto
package pb
@@ -54,6 +54,10 @@ const (
MessageType_TAP_CONFIG MessageType = 32
// * Master → host (BLE only): periodic pushed cache snapshot with ESP timestamp.
MessageType_BLE_STREAM_TICK MessageType = 33
// *
// Host → device: clear master client-id NVS map + live registry (and notify
// online slaves), or on a slave clear local master/id binding only.
MessageType_CLIENT_ID_MAP_RESET MessageType = 34
)
// Enum value maps for MessageType.
@@ -84,6 +88,7 @@ var (
31: "SET_LOG_LEVEL",
32: "TAP_CONFIG",
33: "BLE_STREAM_TICK",
34: "CLIENT_ID_MAP_RESET",
}
MessageType_value = map[string]int32{
"UNKNOWN": 0,
@@ -111,6 +116,7 @@ var (
"SET_LOG_LEVEL": 31,
"TAP_CONFIG": 32,
"BLE_STREAM_TICK": 33,
"CLIENT_ID_MAP_RESET": 34,
}
)
@@ -234,6 +240,8 @@ type UartMessage struct {
// *UartMessage_TapConfigRequest
// *UartMessage_TapConfigResponse
// *UartMessage_BleStreamTick
// *UartMessage_ClientIdMapResetRequest
// *UartMessage_ClientIdMapResetResponse
Payload isUartMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -607,6 +615,24 @@ func (x *UartMessage) GetBleStreamTick() *BleStreamTick {
return nil
}
func (x *UartMessage) GetClientIdMapResetRequest() *ClientIdMapResetRequest {
if x != nil {
if x, ok := x.Payload.(*UartMessage_ClientIdMapResetRequest); ok {
return x.ClientIdMapResetRequest
}
}
return nil
}
func (x *UartMessage) GetClientIdMapResetResponse() *ClientIdMapResetResponse {
if x != nil {
if x, ok := x.Payload.(*UartMessage_ClientIdMapResetResponse); ok {
return x.ClientIdMapResetResponse
}
}
return nil
}
type isUartMessage_Payload interface {
isUartMessage_Payload()
}
@@ -755,6 +781,14 @@ type UartMessage_BleStreamTick struct {
BleStreamTick *BleStreamTick `protobuf:"bytes,41,opt,name=ble_stream_tick,json=bleStreamTick,proto3,oneof"`
}
type UartMessage_ClientIdMapResetRequest struct {
ClientIdMapResetRequest *ClientIdMapResetRequest `protobuf:"bytes,42,opt,name=client_id_map_reset_request,json=clientIdMapResetRequest,proto3,oneof"`
}
type UartMessage_ClientIdMapResetResponse struct {
ClientIdMapResetResponse *ClientIdMapResetResponse `protobuf:"bytes,43,opt,name=client_id_map_reset_response,json=clientIdMapResetResponse,proto3,oneof"`
}
func (*UartMessage_AckPayload) isUartMessage_Payload() {}
func (*UartMessage_EchoPayload) isUartMessage_Payload() {}
@@ -827,6 +861,10 @@ func (*UartMessage_TapConfigResponse) isUartMessage_Payload() {}
func (*UartMessage_BleStreamTick) isUartMessage_Payload() {}
func (*UartMessage_ClientIdMapResetRequest) isUartMessage_Payload() {}
func (*UartMessage_ClientIdMapResetResponse) isUartMessage_Payload() {}
type Ack struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@@ -3071,6 +3109,87 @@ func (x *RestartResponse) GetClientId() uint32 {
return 0
}
// * Host → master or slave: clear persisted client-id assignment state.
type ClientIdMapResetRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ClientIdMapResetRequest) Reset() {
*x = ClientIdMapResetRequest{}
mi := &file_uart_messages_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ClientIdMapResetRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ClientIdMapResetRequest) ProtoMessage() {}
func (x *ClientIdMapResetRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[36]
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 ClientIdMapResetRequest.ProtoReflect.Descriptor instead.
func (*ClientIdMapResetRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{36}
}
type ClientIdMapResetResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ClientIdMapResetResponse) Reset() {
*x = ClientIdMapResetResponse{}
mi := &file_uart_messages_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ClientIdMapResetResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ClientIdMapResetResponse) ProtoMessage() {}
func (x *ClientIdMapResetResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[37]
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 ClientIdMapResetResponse.ProtoReflect.Descriptor instead.
func (*ClientIdMapResetResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{37}
}
func (x *ClientIdMapResetResponse) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
// * Host → master: read/write global log level (esp_log_level_set("*", …)).
type SetLogLevelRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -3083,7 +3202,7 @@ type SetLogLevelRequest struct {
func (x *SetLogLevelRequest) Reset() {
*x = SetLogLevelRequest{}
mi := &file_uart_messages_proto_msgTypes[36]
mi := &file_uart_messages_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3095,7 +3214,7 @@ func (x *SetLogLevelRequest) String() string {
func (*SetLogLevelRequest) ProtoMessage() {}
func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[36]
mi := &file_uart_messages_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3108,7 +3227,7 @@ func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetLogLevelRequest.ProtoReflect.Descriptor instead.
func (*SetLogLevelRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{36}
return file_uart_messages_proto_rawDescGZIP(), []int{38}
}
func (x *SetLogLevelRequest) GetWrite() bool {
@@ -3135,7 +3254,7 @@ type SetLogLevelResponse struct {
func (x *SetLogLevelResponse) Reset() {
*x = SetLogLevelResponse{}
mi := &file_uart_messages_proto_msgTypes[37]
mi := &file_uart_messages_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3147,7 +3266,7 @@ func (x *SetLogLevelResponse) String() string {
func (*SetLogLevelResponse) ProtoMessage() {}
func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[37]
mi := &file_uart_messages_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3160,7 +3279,7 @@ func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetLogLevelResponse.ProtoReflect.Descriptor instead.
func (*SetLogLevelResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{37}
return file_uart_messages_proto_rawDescGZIP(), []int{39}
}
func (x *SetLogLevelResponse) GetSuccess() bool {
@@ -3199,7 +3318,7 @@ type TapConfigRequest struct {
func (x *TapConfigRequest) Reset() {
*x = TapConfigRequest{}
mi := &file_uart_messages_proto_msgTypes[38]
mi := &file_uart_messages_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3211,7 +3330,7 @@ func (x *TapConfigRequest) String() string {
func (*TapConfigRequest) ProtoMessage() {}
func (x *TapConfigRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[38]
mi := &file_uart_messages_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3224,7 +3343,7 @@ func (x *TapConfigRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use TapConfigRequest.ProtoReflect.Descriptor instead.
func (*TapConfigRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{38}
return file_uart_messages_proto_rawDescGZIP(), []int{40}
}
func (x *TapConfigRequest) GetWrite() bool {
@@ -3331,7 +3450,7 @@ type TapConfigResponse struct {
func (x *TapConfigResponse) Reset() {
*x = TapConfigResponse{}
mi := &file_uart_messages_proto_msgTypes[39]
mi := &file_uart_messages_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3343,7 +3462,7 @@ func (x *TapConfigResponse) String() string {
func (*TapConfigResponse) ProtoMessage() {}
func (x *TapConfigResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[39]
mi := &file_uart_messages_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3356,7 +3475,7 @@ func (x *TapConfigResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use TapConfigResponse.ProtoReflect.Descriptor instead.
func (*TapConfigResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{39}
return file_uart_messages_proto_rawDescGZIP(), []int{41}
}
func (x *TapConfigResponse) GetClientId() uint32 {
@@ -3453,7 +3572,7 @@ type OtaStartPayload struct {
func (x *OtaStartPayload) Reset() {
*x = OtaStartPayload{}
mi := &file_uart_messages_proto_msgTypes[40]
mi := &file_uart_messages_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3465,7 +3584,7 @@ func (x *OtaStartPayload) String() string {
func (*OtaStartPayload) ProtoMessage() {}
func (x *OtaStartPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[40]
mi := &file_uart_messages_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3478,7 +3597,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{40}
return file_uart_messages_proto_rawDescGZIP(), []int{42}
}
func (x *OtaStartPayload) GetTotalSize() uint32 {
@@ -3499,7 +3618,7 @@ type OtaPayload struct {
func (x *OtaPayload) Reset() {
*x = OtaPayload{}
mi := &file_uart_messages_proto_msgTypes[41]
mi := &file_uart_messages_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3511,7 +3630,7 @@ func (x *OtaPayload) String() string {
func (*OtaPayload) ProtoMessage() {}
func (x *OtaPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[41]
mi := &file_uart_messages_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3524,7 +3643,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{41}
return file_uart_messages_proto_rawDescGZIP(), []int{43}
}
func (x *OtaPayload) GetSeq() uint32 {
@@ -3550,7 +3669,7 @@ type OtaEndPayload struct {
func (x *OtaEndPayload) Reset() {
*x = OtaEndPayload{}
mi := &file_uart_messages_proto_msgTypes[42]
mi := &file_uart_messages_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3562,7 +3681,7 @@ func (x *OtaEndPayload) String() string {
func (*OtaEndPayload) ProtoMessage() {}
func (x *OtaEndPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[42]
mi := &file_uart_messages_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3575,7 +3694,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{42}
return file_uart_messages_proto_rawDescGZIP(), []int{44}
}
// Device → host status (also used as ACK after each 4 KiB written).
@@ -3592,7 +3711,7 @@ type OtaStatusPayload struct {
func (x *OtaStatusPayload) Reset() {
*x = OtaStatusPayload{}
mi := &file_uart_messages_proto_msgTypes[43]
mi := &file_uart_messages_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3604,7 +3723,7 @@ func (x *OtaStatusPayload) String() string {
func (*OtaStatusPayload) ProtoMessage() {}
func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[43]
mi := &file_uart_messages_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3617,7 +3736,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{43}
return file_uart_messages_proto_rawDescGZIP(), []int{45}
}
func (x *OtaStatusPayload) GetStatus() uint32 {
@@ -3658,7 +3777,7 @@ type OtaSlaveProgressRequest struct {
func (x *OtaSlaveProgressRequest) Reset() {
*x = OtaSlaveProgressRequest{}
mi := &file_uart_messages_proto_msgTypes[44]
mi := &file_uart_messages_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3670,7 +3789,7 @@ func (x *OtaSlaveProgressRequest) String() string {
func (*OtaSlaveProgressRequest) ProtoMessage() {}
func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[44]
mi := &file_uart_messages_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3683,7 +3802,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{44}
return file_uart_messages_proto_rawDescGZIP(), []int{46}
}
func (x *OtaSlaveProgressRequest) GetClientId() uint32 {
@@ -3707,7 +3826,7 @@ type OtaSlaveProgressEntry struct {
func (x *OtaSlaveProgressEntry) Reset() {
*x = OtaSlaveProgressEntry{}
mi := &file_uart_messages_proto_msgTypes[45]
mi := &file_uart_messages_proto_msgTypes[47]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3719,7 +3838,7 @@ func (x *OtaSlaveProgressEntry) String() string {
func (*OtaSlaveProgressEntry) ProtoMessage() {}
func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[45]
mi := &file_uart_messages_proto_msgTypes[47]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3732,7 +3851,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{45}
return file_uart_messages_proto_rawDescGZIP(), []int{47}
}
func (x *OtaSlaveProgressEntry) GetClientId() uint32 {
@@ -3783,7 +3902,7 @@ type OtaSlaveProgressResponse struct {
func (x *OtaSlaveProgressResponse) Reset() {
*x = OtaSlaveProgressResponse{}
mi := &file_uart_messages_proto_msgTypes[46]
mi := &file_uart_messages_proto_msgTypes[48]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3795,7 +3914,7 @@ func (x *OtaSlaveProgressResponse) String() string {
func (*OtaSlaveProgressResponse) ProtoMessage() {}
func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[46]
mi := &file_uart_messages_proto_msgTypes[48]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3808,7 +3927,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{46}
return file_uart_messages_proto_rawDescGZIP(), []int{48}
}
func (x *OtaSlaveProgressResponse) GetActive() bool {
@@ -3850,7 +3969,7 @@ var File_uart_messages_proto protoreflect.FileDescriptor
const file_uart_messages_proto_rawDesc = "" +
"\n" +
"\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\x92\x16\n" +
"\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\xd3\x17\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" +
@@ -3892,7 +4011,9 @@ const file_uart_messages_proto_rawDesc = "" +
"\x16set_log_level_response\x18& \x01(\v2\x19.alox.SetLogLevelResponseH\x00R\x13setLogLevelResponse\x12F\n" +
"\x12tap_config_request\x18' \x01(\v2\x16.alox.TapConfigRequestH\x00R\x10tapConfigRequest\x12I\n" +
"\x13tap_config_response\x18( \x01(\v2\x17.alox.TapConfigResponseH\x00R\x11tapConfigResponse\x12=\n" +
"\x0fble_stream_tick\x18) \x01(\v2\x13.alox.BleStreamTickH\x00R\rbleStreamTickB\t\n" +
"\x0fble_stream_tick\x18) \x01(\v2\x13.alox.BleStreamTickH\x00R\rbleStreamTick\x12]\n" +
"\x1bclient_id_map_reset_request\x18* \x01(\v2\x1d.alox.ClientIdMapResetRequestH\x00R\x17clientIdMapResetRequest\x12`\n" +
"\x1cclient_id_map_reset_response\x18+ \x01(\v2\x1e.alox.ClientIdMapResetResponseH\x00R\x18clientIdMapResetResponseB\t\n" +
"\apayload\"\x05\n" +
"\x03Ack\"!\n" +
"\vEchoPayload\x12\x12\n" +
@@ -4060,7 +4181,10 @@ const file_uart_messages_proto_rawDesc = "" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\"H\n" +
"\x0fRestartResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1b\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\"@\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\"\x19\n" +
"\x17ClientIdMapResetRequest\"4\n" +
"\x18ClientIdMapResetResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\"@\n" +
"\x12SetLogLevelRequest\x12\x14\n" +
"\x05write\x18\x01 \x01(\bR\x05write\x12\x14\n" +
"\x05level\x18\x02 \x01(\rR\x05level\"E\n" +
@@ -4128,7 +4252,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*\xbf\x03\n" +
"\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\xd8\x03\n" +
"\vMessageType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\a\n" +
"\x03ACK\x10\x01\x12\b\n" +
@@ -4157,7 +4281,8 @@ const file_uart_messages_proto_rawDesc = "" +
"\rSET_LOG_LEVEL\x10\x1f\x12\x0e\n" +
"\n" +
"TAP_CONFIG\x10 \x12\x13\n" +
"\x0fBLE_STREAM_TICK\x10!\"\x04\b\x18\x10\x18\"\x04\b\x1c\x10\x1c*G\n" +
"\x0fBLE_STREAM_TICK\x10!\x12\x17\n" +
"\x13CLIENT_ID_MAP_RESET\x10\"\"\x04\b\x18\x10\x18\"\x04\b\x1c\x10\x1c*G\n" +
"\aTapKind\x12\f\n" +
"\bTAP_NONE\x10\x00\x12\x0e\n" +
"\n" +
@@ -4180,7 +4305,7 @@ func file_uart_messages_proto_rawDescGZIP() []byte {
}
var file_uart_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 47)
var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 49)
var file_uart_messages_proto_goTypes = []any{
(MessageType)(0), // 0: alox.MessageType
(TapKind)(0), // 1: alox.TapKind
@@ -4220,17 +4345,19 @@ var file_uart_messages_proto_goTypes = []any{
(*EspNowFindMeResponse)(nil), // 35: alox.EspNowFindMeResponse
(*RestartRequest)(nil), // 36: alox.RestartRequest
(*RestartResponse)(nil), // 37: alox.RestartResponse
(*SetLogLevelRequest)(nil), // 38: alox.SetLogLevelRequest
(*SetLogLevelResponse)(nil), // 39: alox.SetLogLevelResponse
(*TapConfigRequest)(nil), // 40: alox.TapConfigRequest
(*TapConfigResponse)(nil), // 41: alox.TapConfigResponse
(*OtaStartPayload)(nil), // 42: alox.OtaStartPayload
(*OtaPayload)(nil), // 43: alox.OtaPayload
(*OtaEndPayload)(nil), // 44: alox.OtaEndPayload
(*OtaStatusPayload)(nil), // 45: alox.OtaStatusPayload
(*OtaSlaveProgressRequest)(nil), // 46: alox.OtaSlaveProgressRequest
(*OtaSlaveProgressEntry)(nil), // 47: alox.OtaSlaveProgressEntry
(*OtaSlaveProgressResponse)(nil), // 48: alox.OtaSlaveProgressResponse
(*ClientIdMapResetRequest)(nil), // 38: alox.ClientIdMapResetRequest
(*ClientIdMapResetResponse)(nil), // 39: alox.ClientIdMapResetResponse
(*SetLogLevelRequest)(nil), // 40: alox.SetLogLevelRequest
(*SetLogLevelResponse)(nil), // 41: alox.SetLogLevelResponse
(*TapConfigRequest)(nil), // 42: alox.TapConfigRequest
(*TapConfigResponse)(nil), // 43: alox.TapConfigResponse
(*OtaStartPayload)(nil), // 44: alox.OtaStartPayload
(*OtaPayload)(nil), // 45: alox.OtaPayload
(*OtaEndPayload)(nil), // 46: alox.OtaEndPayload
(*OtaStatusPayload)(nil), // 47: alox.OtaStatusPayload
(*OtaSlaveProgressRequest)(nil), // 48: alox.OtaSlaveProgressRequest
(*OtaSlaveProgressEntry)(nil), // 49: alox.OtaSlaveProgressEntry
(*OtaSlaveProgressResponse)(nil), // 50: alox.OtaSlaveProgressResponse
}
var file_uart_messages_proto_depIdxs = []int32{
0, // 0: alox.UartMessage.type:type_name -> alox.MessageType
@@ -4239,16 +4366,16 @@ var file_uart_messages_proto_depIdxs = []int32{
5, // 3: alox.UartMessage.version_response:type_name -> alox.VersionResponse
7, // 4: alox.UartMessage.client_info_response:type_name -> alox.ClientInfoResponse
9, // 5: alox.UartMessage.client_input_response:type_name -> alox.ClientInputResponse
42, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload
43, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload
44, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload
45, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload
44, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload
45, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload
46, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload
47, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload
10, // 10: alox.UartMessage.accel_deadzone_request:type_name -> alox.AccelDeadzoneRequest
11, // 11: alox.UartMessage.accel_deadzone_response:type_name -> alox.AccelDeadzoneResponse
28, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest
29, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse
46, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest
48, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse
48, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest
50, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse
32, // 16: alox.UartMessage.led_ring_progress_request:type_name -> alox.LedRingProgressRequest
33, // 17: alox.UartMessage.led_ring_progress_response:type_name -> alox.LedRingProgressResponse
34, // 18: alox.UartMessage.espnow_find_me_request:type_name -> alox.EspNowFindMeRequest
@@ -4265,28 +4392,30 @@ var file_uart_messages_proto_depIdxs = []int32{
26, // 29: alox.UartMessage.cache_status_response:type_name -> alox.CacheStatusResponse
30, // 30: alox.UartMessage.espnow_echo_ping_request:type_name -> alox.EspNowEchoPingRequest
31, // 31: alox.UartMessage.espnow_echo_ping_response:type_name -> alox.EspNowEchoPingResponse
38, // 32: alox.UartMessage.set_log_level_request:type_name -> alox.SetLogLevelRequest
39, // 33: alox.UartMessage.set_log_level_response:type_name -> alox.SetLogLevelResponse
40, // 34: alox.UartMessage.tap_config_request:type_name -> alox.TapConfigRequest
41, // 35: alox.UartMessage.tap_config_response:type_name -> alox.TapConfigResponse
40, // 32: alox.UartMessage.set_log_level_request:type_name -> alox.SetLogLevelRequest
41, // 33: alox.UartMessage.set_log_level_response:type_name -> alox.SetLogLevelResponse
42, // 34: alox.UartMessage.tap_config_request:type_name -> alox.TapConfigRequest
43, // 35: alox.UartMessage.tap_config_response:type_name -> alox.TapConfigResponse
27, // 36: alox.UartMessage.ble_stream_tick:type_name -> alox.BleStreamTick
6, // 37: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo
8, // 38: alox.ClientInputResponse.clients:type_name -> alox.ClientInput
15, // 39: alox.BatterySample.lipo1:type_name -> alox.LipoReading
15, // 40: alox.BatterySample.lipo2:type_name -> alox.LipoReading
16, // 41: alox.BatteryStatusResponse.samples:type_name -> alox.BatterySample
1, // 42: alox.TapEvent.kind:type_name -> alox.TapKind
1, // 43: alox.CacheClientTap.kind:type_name -> alox.TapKind
23, // 44: alox.CacheClientStatus.accel:type_name -> alox.CacheClientAccel
24, // 45: alox.CacheClientStatus.tap:type_name -> alox.CacheClientTap
25, // 46: alox.CacheStatusResponse.clients:type_name -> alox.CacheClientStatus
25, // 47: alox.BleStreamTick.clients:type_name -> alox.CacheClientStatus
47, // 48: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry
49, // [49:49] is the sub-list for method output_type
49, // [49:49] is the sub-list for method input_type
49, // [49:49] is the sub-list for extension type_name
49, // [49:49] is the sub-list for extension extendee
0, // [0:49] is the sub-list for field type_name
38, // 37: alox.UartMessage.client_id_map_reset_request:type_name -> alox.ClientIdMapResetRequest
39, // 38: alox.UartMessage.client_id_map_reset_response:type_name -> alox.ClientIdMapResetResponse
6, // 39: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo
8, // 40: alox.ClientInputResponse.clients:type_name -> alox.ClientInput
15, // 41: alox.BatterySample.lipo1:type_name -> alox.LipoReading
15, // 42: alox.BatterySample.lipo2:type_name -> alox.LipoReading
16, // 43: alox.BatteryStatusResponse.samples:type_name -> alox.BatterySample
1, // 44: alox.TapEvent.kind:type_name -> alox.TapKind
1, // 45: alox.CacheClientTap.kind:type_name -> alox.TapKind
23, // 46: alox.CacheClientStatus.accel:type_name -> alox.CacheClientAccel
24, // 47: alox.CacheClientStatus.tap:type_name -> alox.CacheClientTap
25, // 48: alox.CacheStatusResponse.clients:type_name -> alox.CacheClientStatus
25, // 49: alox.BleStreamTick.clients:type_name -> alox.CacheClientStatus
49, // 50: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry
51, // [51:51] is the sub-list for method output_type
51, // [51:51] is the sub-list for method input_type
51, // [51:51] is the sub-list for extension type_name
51, // [51:51] is the sub-list for extension extendee
0, // [0:51] is the sub-list for field type_name
}
func init() { file_uart_messages_proto_init() }
@@ -4331,6 +4460,8 @@ func file_uart_messages_proto_init() {
(*UartMessage_TapConfigRequest)(nil),
(*UartMessage_TapConfigResponse)(nil),
(*UartMessage_BleStreamTick)(nil),
(*UartMessage_ClientIdMapResetRequest)(nil),
(*UartMessage_ClientIdMapResetResponse)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -4338,7 +4469,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: 2,
NumMessages: 47,
NumMessages: 49,
NumExtensions: 0,
NumServices: 0,
},
+1 -1
View File
@@ -12,7 +12,7 @@
{
"id": "pod-1",
"mac": "50:78:7d:18:01:10",
"client_id": 16,
"client_id": 1,
"console": "/dev/ttyACM1"
}
]
+26
View File
@@ -540,6 +540,12 @@
title="Master-Tap-Config an alle Slaves (ESP-NOW)">
Tap-Cfg alle
</button>
<button type="button" class="btn btn-outline-danger btn-sm"
@click="resetClientIdMap()"
:disabled="busy || !hostUp()"
title="NVS-ID-Map + Live-Registry leeren; Online-Slaves verlieren Binding">
ID-Map leeren
</button>
<span class="badge bg-secondary" x-text="(state.clients || []).length + ' registered'"></span>
</div>
</div>
@@ -1957,6 +1963,26 @@
this.busy = false;
}
},
async resetClientIdMap() {
if (!confirm('ID-Map leeren? Master vergisst alle MAC→ID-Zuordnungen; Online-Slaves verlieren ihr Binding.')) {
return;
}
this.busy = true;
try {
const r = await fetch('/api/clients/reset-id-map', { method: 'POST' });
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || 'ID-Map leeren fehlgeschlagen', false);
return;
}
this.state = { ...this.state, clients: [] };
this.flash('ID-Map geleert — Slaves melden sich neu an', true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
async restart(clientId = 0) {
this.busy = true;
try {