diff --git a/goTool/api_serve.go b/goTool/api_serve.go index 419a384..1483ef9 100644 --- a/goTool/api_serve.go +++ b/goTool/api_serve.go @@ -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 { diff --git a/goTool/autotest/config.go b/goTool/autotest/config.go index 99fda9e..60c0e91 100644 --- a/goTool/autotest/config.go +++ b/goTool/autotest/config.go @@ -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 } diff --git a/goTool/autotest/config_test.go b/goTool/autotest/config_test.go index 2c41f6e..96d6537 100644 --- a/goTool/autotest/config_test.go +++ b/goTool/autotest/config_test.go @@ -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) } } diff --git a/goTool/autotest/runner.go b/goTool/autotest/runner.go index 0590c16..65f49b0 100644 --- a/goTool/autotest/runner.go +++ b/goTool/autotest/runner.go @@ -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 { diff --git a/goTool/cmd_clients_reset_ids.go b/goTool/cmd_clients_reset_ids.go new file mode 100644 index 0000000..8bf52bb --- /dev/null +++ b/goTool/cmd_clients_reset_ids.go @@ -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) +} diff --git a/goTool/main.go b/goTool/main.go index a611c19..59a07bc 100644 --- a/goTool/main.go +++ b/goTool/main.go @@ -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": diff --git a/goTool/pb/uart_messages.pb.go b/goTool/pb/uart_messages.pb.go index 742504e..73092cc 100644 --- a/goTool/pb/uart_messages.pb.go +++ b/goTool/pb/uart_messages.pb.go @@ -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, }, diff --git a/goTool/testdata/configs/example-lab.json b/goTool/testdata/configs/example-lab.json index 52cd528..81044ab 100644 --- a/goTool/testdata/configs/example-lab.json +++ b/goTool/testdata/configs/example-lab.json @@ -12,7 +12,7 @@ { "id": "pod-1", "mac": "50:78:7d:18:01:10", - "client_id": 16, + "client_id": 1, "console": "/dev/ttyACM1" } ] diff --git a/goTool/webui/index.html b/goTool/webui/index.html index c143dea..1215b62 100644 --- a/goTool/webui/index.html +++ b/goTool/webui/index.html @@ -540,6 +540,12 @@ title="Master-Tap-Config an alle Slaves (ESP-NOW)"> Tap-Cfg alle + @@ -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 { diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 738c0db..f513ecb 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -29,6 +29,7 @@ idf_component_register( "cmd/cmd_espnow_echo_ping.c" "cmd/cmd_espnow_find_me.c" "cmd/cmd_restart.c" + "cmd/cmd_client_id_map_reset.c" "pod_reboot.c" "cmd/cmd_led_ring.c" "cmd/cmd_battery.c" diff --git a/main/client_registry.c b/main/client_registry.c index 9385117..a049990 100644 --- a/main/client_registry.c +++ b/main/client_registry.c @@ -1,4 +1,5 @@ #include "client_registry.h" +#include "pod_settings.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/idf_additions.h" @@ -13,12 +14,68 @@ typedef struct { } client_slot_t; static client_slot_t s_clients[CLIENT_REGISTRY_MAX]; +static pod_id_map_t s_id_map; static struct { board_lipo_reading_t reading; uint32_t updated_at; } s_master_battery; +static bool id_in_use(uint32_t id, const uint8_t *except_mac) { + for (uint8_t i = 0; i < s_id_map.count; i++) { + if (s_id_map.entries[i].id != id) { + continue; + } + if (except_mac != NULL && + memcmp(s_id_map.entries[i].mac, except_mac, CLIENT_MAC_LEN) == 0) { + continue; + } + return true; + } + return false; +} + +static int find_map_index(const uint8_t mac[CLIENT_MAC_LEN]) { + for (uint8_t i = 0; i < s_id_map.count; i++) { + if (memcmp(s_id_map.entries[i].mac, mac, CLIENT_MAC_LEN) == 0) { + return (int)i; + } + } + return -1; +} + +static esp_err_t persist_id_map(void) { + return pod_settings_save_id_map(&s_id_map); +} + +static uint32_t alloc_next_free_id(void) { + if (s_id_map.count >= POD_ID_MAP_MAX) { + return 0; + } + uint8_t start = s_id_map.next_id; + if (start < POD_CLIENT_ID_MIN) { + start = (uint8_t)POD_CLIENT_ID_MIN; + } + for (uint32_t n = 0; n < POD_CLIENT_ID_MAX; n++) { + uint32_t id = (uint32_t)start + n; + if (id > POD_CLIENT_ID_MAX) { + id = id - POD_CLIENT_ID_MAX; + } + if (id < POD_CLIENT_ID_MIN) { + continue; + } + if (!id_in_use(id, NULL)) { + uint32_t next = id + 1; + if (next > POD_CLIENT_ID_MAX) { + next = POD_CLIENT_ID_MIN; + } + s_id_map.next_id = (uint8_t)next; + return id; + } + } + return 0; +} + uint32_t client_registry_now_ms(void) { return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); } @@ -95,7 +152,84 @@ static client_slot_t *alloc_slot(const uint8_t mac[CLIENT_MAC_LEN], return NULL; } -void client_registry_init(void) { memset(s_clients, 0, sizeof(s_clients)); } +void client_registry_init(void) { + memset(s_clients, 0, sizeof(s_clients)); + memset(&s_id_map, 0, sizeof(s_id_map)); + s_id_map.next_id = (uint8_t)POD_CLIENT_ID_MIN; + (void)pod_settings_load_id_map(&s_id_map); + if (s_id_map.next_id < POD_CLIENT_ID_MIN) { + s_id_map.next_id = (uint8_t)POD_CLIENT_ID_MIN; + } +} + +esp_err_t client_registry_assign_id(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t preferred_id, uint32_t *out_id) { + if (mac == NULL || out_id == NULL) { + return ESP_ERR_INVALID_ARG; + } + + int idx = find_map_index(mac); + if (idx >= 0) { + *out_id = s_id_map.entries[idx].id; + return ESP_OK; + } + + uint32_t id = 0; + if (preferred_id >= POD_CLIENT_ID_MIN && preferred_id <= POD_CLIENT_ID_MAX && + !id_in_use(preferred_id, mac)) { + id = preferred_id; + } else { + id = alloc_next_free_id(); + } + if (id == 0) { + return ESP_ERR_NO_MEM; + } + + if (s_id_map.count >= POD_ID_MAP_MAX) { + return ESP_ERR_NO_MEM; + } + + memcpy(s_id_map.entries[s_id_map.count].mac, mac, CLIENT_MAC_LEN); + s_id_map.entries[s_id_map.count].id = (uint8_t)id; + s_id_map.count++; + if (id >= s_id_map.next_id) { + uint32_t next = id + 1; + if (next > POD_CLIENT_ID_MAX) { + next = POD_CLIENT_ID_MIN; + } + s_id_map.next_id = (uint8_t)next; + } + + esp_err_t err = persist_id_map(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "persist id_map failed after assign id=%lu: %s", + (unsigned long)id, esp_err_to_name(err)); + } + *out_id = id; + return ESP_OK; +} + +void client_registry_clear_id_map(void) { + memset(s_clients, 0, sizeof(s_clients)); + memset(&s_id_map, 0, sizeof(s_id_map)); + s_id_map.next_id = (uint8_t)POD_CLIENT_ID_MIN; + esp_err_t err = pod_settings_clear_id_map(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "clear id_map NVS failed: %s", esp_err_to_name(err)); + } + ESP_LOGI(TAG, "cleared id map and live registry"); +} + +void client_registry_mark_assign_sent(const uint8_t mac[CLIENT_MAC_LEN]) { + if (mac == NULL) { + return; + } + client_slot_t *slot = find_slot(mac); + if (slot == NULL) { + return; + } + slot->info.last_assign_sent_at = now_ms(); +} const client_info_t * client_registry_find_by_mac(const uint8_t mac[CLIENT_MAC_LEN]) { @@ -171,18 +305,19 @@ void client_registry_check_timeouts(uint32_t timeout_ms) { uint32_t now = now_ms(); for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) { - if (!s_clients[i].active || !s_clients[i].info.available) { + if (!s_clients[i].active) { continue; } uint32_t elapsed = now - s_clients[i].info.last_success_ping_at; if (elapsed > timeout_ms) { - s_clients[i].info.available = false; char mac_str[18]; mac_to_str(s_clients[i].info.mac, mac_str, sizeof(mac_str)); - ESP_LOGW(TAG, "client inactive id=%lu mac=%s (no heartbeat for %lu ms)", + ESP_LOGW(TAG, "client dropped id=%lu mac=%s (no heartbeat for %lu ms)", (unsigned long)s_clients[i].info.id, mac_str, (unsigned long)elapsed); + s_clients[i].active = false; + memset(&s_clients[i].info, 0, sizeof(s_clients[i].info)); } } } @@ -552,24 +687,15 @@ esp_err_t client_registry_update_battery(const uint8_t mac[CLIENT_MAC_LEN], client_slot_t *slot = find_slot(mac); if (slot == NULL) { - bool is_new = false; - esp_err_t err = client_registry_upsert(mac, slave_id, 0, true, false, &is_new); - if (err != ESP_OK) { - return err; - } - slot = find_slot(mac); - if (slot == NULL) { - return ESP_ERR_NOT_FOUND; - } - ESP_LOGI(TAG, "battery auto-registered id=%lu (report before heartbeat)", - (unsigned long)slave_id); + /* Ids are assigned on presence only; ignore early battery reports. */ + return ESP_ERR_NOT_FOUND; } - if (slot->info.id != slave_id) { - ESP_LOGW(TAG, "battery id %lu → %lu for mac %02x:…:%02x", + if (slave_id != 0 && slot->info.id != slave_id) { + ESP_LOGW(TAG, "battery id mismatch slot=%lu report=%lu mac %02x:…:%02x", (unsigned long)slot->info.id, (unsigned long)slave_id, mac[0], mac[5]); - slot->info.id = slave_id; + return ESP_ERR_INVALID_STATE; } slot->info.lipo1_valid = lipo1_valid; diff --git a/main/client_registry.h b/main/client_registry.h index fd804b9..3820353 100644 --- a/main/client_registry.h +++ b/main/client_registry.h @@ -47,6 +47,8 @@ typedef struct { uint32_t lipo1_mv; uint32_t lipo2_mv; uint32_t battery_updated_at; + /** Master: last ESPNOW_ASSIGN_ID send time (rate-limit resends). */ + uint32_t last_assign_sent_at; } client_info_t; #define CLIENT_REGISTRY_DEFAULT_ACCEL_DEADZONE 100u @@ -55,6 +57,23 @@ typedef struct { void client_registry_init(void); +/** + * Assign a stable logical id for mac (1…255). Honors preferred if free or + * already owned by mac; otherwise allocates the next free id. Persists the + * master id map to NVS. + */ +esp_err_t client_registry_assign_id(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t preferred_id, uint32_t *out_id); + +/** + * Clear NVS id map, RAM map, and all live registry slots. Does not notify + * slaves (caller sends CLEAR_ID_BINDING). + */ +void client_registry_clear_id_map(void); + +/** Record that ASSIGN_ID was sent to this MAC (for resend rate limiting). */ +void client_registry_mark_assign_sent(const uint8_t mac[CLIENT_MAC_LEN]); + /** Milliseconds since boot (same clock as stored ping timestamps). */ uint32_t client_registry_now_ms(void); @@ -75,7 +94,10 @@ esp_err_t client_registry_heartbeat(const uint8_t mac[CLIENT_MAC_LEN], uint32_t id, uint32_t version, bool used, bool *out_is_new, bool *out_reactivated); -/** Mark clients inactive when last_success_ping_at is older than timeout_ms. */ +/** + * Drop live registry slots whose last_success_ping_at is older than + * timeout_ms (NVS id map unchanged). Offline clients disappear from CLIENT_INFO. + */ void client_registry_check_timeouts(uint32_t timeout_ms); size_t client_registry_count(void); diff --git a/main/cmd/cmd_client_id_map_reset.c b/main/cmd/cmd_client_id_map_reset.c new file mode 100644 index 0000000..e67e711 --- /dev/null +++ b/main/cmd/cmd_client_id_map_reset.c @@ -0,0 +1,35 @@ +#include "cmd_client_id_map_reset.h" +#include "esp_log.h" +#include "esp_now_comm.h" +#include "esp_now_core.h" +#include "uart_cmd.h" + +static const char *TAG = "[ID_MAP_RESET]"; + +static void reply(bool success) { + alox_UartMessage response; + uart_cmd_init_response(&response, alox_MessageType_CLIENT_ID_MAP_RESET, + alox_UartMessage_client_id_map_reset_response_tag); + response.payload.client_id_map_reset_response.success = success; + uart_cmd_send(&response, TAG); +} + +static void handle_client_id_map_reset(const uint8_t *data, size_t len) { + (void)data; + (void)len; + + esp_err_t err; + if (esp_now_core_is_master()) { + ESP_LOGI(TAG, "reset master id map + notify online slaves"); + err = esp_now_comm_reset_client_id_map(); + } else { + ESP_LOGI(TAG, "clear local slave id binding"); + err = esp_now_comm_clear_local_id_binding(); + } + reply(err == ESP_OK); +} + +void cmd_client_id_map_reset_register(void) { + uart_cmd_register(alox_MessageType_CLIENT_ID_MAP_RESET, + handle_client_id_map_reset); +} diff --git a/main/cmd/cmd_client_id_map_reset.h b/main/cmd/cmd_client_id_map_reset.h new file mode 100644 index 0000000..75b3a88 --- /dev/null +++ b/main/cmd/cmd_client_id_map_reset.h @@ -0,0 +1,3 @@ +#pragma once + +void cmd_client_id_map_reset_register(void); diff --git a/main/esp_now_comm.h b/main/esp_now_comm.h index 663a313..a8abc5b 100644 --- a/main/esp_now_comm.h +++ b/main/esp_now_comm.h @@ -55,6 +55,22 @@ esp_err_t esp_now_comm_send_led_ring(const uint8_t mac[CLIENT_MAC_LEN], esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN], uint32_t client_id); +/** Master: tell slave its assigned logical client id. */ +esp_err_t esp_now_comm_send_assign_id(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t assigned_id); + +/** Master: tell slave to forget persisted master/id binding. */ +esp_err_t esp_now_comm_send_clear_id_binding(const uint8_t mac[CLIENT_MAC_LEN]); + +/** + * Master: clear NVS id map + live registry and unicast CLEAR_ID_BINDING to + * every currently registered live slave (snapshot before clear). + */ +esp_err_t esp_now_comm_reset_client_id_map(void); + +/** Slave: clear local NVS master/id binding (UART reset on slave). */ +esp_err_t esp_now_comm_clear_local_id_binding(void); + /** Master → slave OTA (unicast). */ esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN], uint32_t total_size); diff --git a/main/esp_now_master.c b/main/esp_now_master.c index 51392ff..0ed8d98 100644 --- a/main/esp_now_master.c +++ b/main/esp_now_master.c @@ -22,6 +22,8 @@ static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff, #define ESPNOW_CLIENT_TIMEOUT_MS \ (ESPNOW_HEARTBEAT_INTERVAL_MS * ESPNOW_HEARTBEAT_MISS_COUNT) #define ESPNOW_BATTERY_INTERVAL_MS 30000 +/** Min gap between ASSIGN_ID resends when slave has not yet echoed the id. */ +#define ESPNOW_ASSIGN_RESEND_MS 2000u static const char *TAG = "[ESPNOW_M]"; @@ -168,6 +170,22 @@ static esp_err_t send_restart(const uint8_t *dest_mac, uint32_t client_id) { return esp_now_core_send(dest_mac, &msg); } +static esp_err_t send_assign_id(const uint8_t *dest_mac, uint32_t assigned_id) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_ASSIGN_ID; + msg.which_payload = alox_EspNowMessage_assign_id_tag; + msg.payload.assign_id.assigned_id = assigned_id; + /* Fast path: avoid 8× NO_MEM retries blocking the shared send lock. */ + return esp_now_core_send_fast(dest_mac, &msg); +} + +static esp_err_t send_clear_id_binding(const uint8_t *dest_mac) { + alox_EspNowMessage msg = alox_EspNowMessage_init_zero; + msg.type = alox_EspNowMessageType_ESPNOW_CLEAR_ID_BINDING; + msg.which_payload = alox_EspNowMessage_clear_id_binding_tag; + return esp_now_core_send_fast(dest_mac, &msg); +} + static esp_err_t send_tap_notify(const uint8_t *dest_mac, uint32_t client_id, bool single, bool double_tap, bool triple) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; @@ -251,6 +269,70 @@ esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN], return err; } +esp_err_t esp_now_comm_send_assign_id(const uint8_t mac[CLIENT_MAC_LEN], + uint32_t assigned_id) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + esp_err_t err = send_assign_id(mac, assigned_id); + if (err == ESP_OK) { + ESP_LOGI(TAG, "unicast ASSIGN_ID %lu to %s", (unsigned long)assigned_id, + mac_str); + } else { + ESP_LOGW(TAG, "unicast ASSIGN_ID to %s failed: %s", mac_str, + esp_err_to_name(err)); + } + return err; +} + +esp_err_t esp_now_comm_send_clear_id_binding(const uint8_t mac[CLIENT_MAC_LEN]) { + if (mac == NULL || !esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + char mac_str[18]; + esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); + esp_err_t err = send_clear_id_binding(mac); + if (err == ESP_OK) { + ESP_LOGI(TAG, "unicast CLEAR_ID_BINDING to %s", mac_str); + } else { + ESP_LOGW(TAG, "unicast CLEAR_ID_BINDING to %s failed: %s", mac_str, + esp_err_to_name(err)); + } + return err; +} + +esp_err_t esp_now_comm_reset_client_id_map(void) { + if (!esp_now_core_is_master()) { + return ESP_ERR_INVALID_STATE; + } + + uint8_t macs[CLIENT_REGISTRY_MAX][CLIENT_MAC_LEN]; + size_t n = client_registry_count(); + if (n > CLIENT_REGISTRY_MAX) { + n = CLIENT_REGISTRY_MAX; + } + size_t copied = 0; + for (size_t i = 0; i < n; i++) { + const client_info_t *c = client_registry_at(i); + if (c == NULL) { + continue; + } + memcpy(macs[copied], c->mac, CLIENT_MAC_LEN); + copied++; + } + + client_registry_clear_id_map(); + + for (size_t i = 0; i < copied; i++) { + (void)esp_now_comm_send_clear_id_binding(macs[i]); + } + ESP_LOGI(TAG, "client id map reset; notified %u slave(s)", (unsigned)copied); + return ESP_OK; +} + + esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN], uint32_t client_id) { if (mac == NULL || !esp_now_core_is_master()) { @@ -495,25 +577,53 @@ static void handle_client_presence(const alox_EspNowSlavePresence *presence, esp_now_core_ensure_peer(mac); + uint32_t assigned_id = 0; + esp_err_t err = + client_registry_assign_id(mac, presence->slave_id, &assigned_id); + if (err != ESP_OK) { + ESP_LOGW(TAG, "id assign failed: %s", esp_err_to_name(err)); + return; + } + bool is_new = false; bool reactivated = false; - esp_err_t err = client_registry_heartbeat( - mac, presence->slave_id, presence->version, presence->used, &is_new, - &reactivated); + err = client_registry_heartbeat(mac, assigned_id, presence->version, + presence->used, &is_new, &reactivated); if (err != ESP_OK) { ESP_LOGW(TAG, "client registry full"); return; } + /* + * Send ASSIGN when new/rejoined, or when slave has not echoed the id yet. + * Rate-limit mismatch resends so failed assigns do not flood ESP-NOW buffers + * (ESP_ERR_ESPNOW_NO_MEM) and block other unicast traffic. + */ + const bool needs_assign = + is_new || reactivated || presence->slave_id != assigned_id; + if (needs_assign) { + const client_info_t *client = client_registry_find_by_mac(mac); + const bool due = + is_new || reactivated || client == NULL || + client->last_assign_sent_at == 0 || + client_registry_ms_since(client->last_assign_sent_at) >= + ESPNOW_ASSIGN_RESEND_MS; + if (due) { + if (esp_now_comm_send_assign_id(mac, assigned_id) == ESP_OK) { + client_registry_mark_assign_sent(mac); + } + } + } + char mac_str[18]; esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); if (is_new) { - ESP_LOGI(TAG, "client registered id=%lu mac=%s ver=%lu", - (unsigned long)presence->slave_id, mac_str, - (unsigned long)presence->version); + ESP_LOGI(TAG, "client registered id=%lu (preferred=%lu) mac=%s ver=%lu", + (unsigned long)assigned_id, (unsigned long)presence->slave_id, + mac_str, (unsigned long)presence->version); } else if (reactivated) { ESP_LOGI(TAG, "client reconnected id=%lu mac=%s", - (unsigned long)presence->slave_id, mac_str); + (unsigned long)assigned_id, mac_str); } } diff --git a/main/esp_now_slave.c b/main/esp_now_slave.c index c8d3d07..d41a123 100644 --- a/main/esp_now_slave.c +++ b/main/esp_now_slave.c @@ -37,6 +37,11 @@ static bool s_tap_notify_double; static bool s_tap_notify_triple; static uint8_t s_master_mac[ESP_NOW_ETH_ALEN]; static uint32_t s_last_discover_ms; +/** Logical id assigned by current/last master (0 = unbound). */ +static uint32_t s_assigned_id; +/** Master MAC from NVS binding (may differ from current s_master_mac). */ +static uint8_t s_bound_master_mac[ESP_NOW_ETH_ALEN]; +static bool s_has_binding; typedef enum { SLAVE_TX_SLAVE_INFO = 1, @@ -45,15 +50,91 @@ typedef enum { static QueueHandle_t s_tx_queue; +static void queue_tx(slave_tx_op_t op); + static bool from_joined_master(const uint8_t *master_mac) { return s_joined && esp_now_core_mac_equal(master_mac, s_master_mac); } +static uint32_t my_client_id(void) { return s_assigned_id; } + +/** True when command targets this slave (0 = broadcast / any). */ +static bool client_id_matches(uint32_t client_id) { + if (client_id == 0) { + return true; + } + return s_assigned_id != 0 && client_id == s_assigned_id; +} + +static uint32_t preferred_id_for_master(void) { + if (!s_has_binding || s_assigned_id == 0) { + return 0; + } + if (!s_joined || !esp_now_core_mac_equal(s_master_mac, s_bound_master_mac)) { + return 0; + } + return s_assigned_id; +} + +static void load_id_binding(void) { + uint8_t mac[ESP_NOW_ETH_ALEN]; + uint32_t id = 0; + s_has_binding = pod_settings_load_id_binding(mac, &id); + if (s_has_binding) { + memcpy(s_bound_master_mac, mac, ESP_NOW_ETH_ALEN); + s_assigned_id = id; + ESP_LOGI(TAG, "loaded id binding id=%lu master=%02x:…:%02x", + (unsigned long)id, mac[0], mac[5]); + } else { + memset(s_bound_master_mac, 0, sizeof(s_bound_master_mac)); + s_assigned_id = 0; + } +} + +static void apply_assigned_id(uint32_t assigned_id) { + if (assigned_id < POD_CLIENT_ID_MIN || assigned_id > POD_CLIENT_ID_MAX) { + ESP_LOGW(TAG, "ignore invalid assigned_id=%lu", (unsigned long)assigned_id); + return; + } + const bool same = + s_has_binding && s_assigned_id == assigned_id && + esp_now_core_mac_equal(s_bound_master_mac, s_master_mac); + s_assigned_id = assigned_id; + memcpy(s_bound_master_mac, s_master_mac, ESP_NOW_ETH_ALEN); + s_has_binding = true; + if (same) { + return; + } + if (pod_settings_save_id_binding(s_master_mac, assigned_id) != ESP_OK) { + ESP_LOGW(TAG, "assigned id=%lu but NVS save failed", + (unsigned long)assigned_id); + } else { + ESP_LOGI(TAG, "assigned id=%lu (persisted)", (unsigned long)assigned_id); + } + queue_tx(SLAVE_TX_BATTERY); +} + +static void clear_id_binding_local(void) { + s_assigned_id = 0; + s_has_binding = false; + memset(s_bound_master_mac, 0, sizeof(s_bound_master_mac)); + (void)pod_settings_clear_id_binding(); + ESP_LOGI(TAG, "cleared local id binding"); +} + +esp_err_t esp_now_comm_clear_local_id_binding(void) { + if (esp_now_core_is_master()) { + return ESP_OK; + } + clear_id_binding_local(); + return ESP_OK; +} + static void fill_presence(alox_EspNowSlavePresence *presence) { const uint8_t *own = esp_now_core_own_mac(); presence->network = esp_now_core_network(); presence->version = POWERPOD_FW_VERSION; - presence->slave_id = own[5]; + presence->slave_id = preferred_id_for_master(); presence->available = true; presence->used = false; esp_now_proto_setup_presence_encode(presence, own); @@ -139,7 +220,7 @@ static void queue_tx(slave_tx_op_t op) { } static void send_battery_to_master(void) { - if (!s_joined) { + if (!s_joined || s_assigned_id == 0) { return; } @@ -147,7 +228,7 @@ static void send_battery_to_master(void) { board_input_read_lipo(&reading); alox_EspNowBatteryReport report = alox_EspNowBatteryReport_init_zero; - report.client_id = esp_now_core_own_mac()[5]; + report.client_id = s_assigned_id; report.lipo1_valid = reading.lipo1_valid; report.lipo2_valid = reading.lipo2_valid; report.lipo1_mv = reading.lipo1_mv; @@ -261,10 +342,7 @@ static void handle_echo_ping(const uint8_t *master_mac, static void handle_restart(const uint8_t *master_mac, const alox_EspNowRestart *req) { - const uint8_t *own = esp_now_core_own_mac(); - uint32_t my_id = own[5]; - - if (req->client_id != 0 && req->client_id != my_id) { + if (!client_id_matches(req->client_id)) { return; } if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { @@ -273,14 +351,14 @@ static void handle_restart(const uint8_t *master_mac, char mac_str[18]; esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, "RESTART from master %s (id=%lu)", mac_str, (unsigned long)my_id); + ESP_LOGI(TAG, "RESTART from master %s (id=%lu)", mac_str, + (unsigned long)my_client_id()); pod_schedule_restart(); } static void handle_battery_query(const uint8_t *master_mac, const alox_EspNowBatteryQuery *query) { - uint32_t my_id = esp_now_core_own_mac()[5]; - if (query->client_id != 0 && query->client_id != my_id) { + if (!client_id_matches(query->client_id)) { return; } if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { @@ -291,8 +369,7 @@ static void handle_battery_query(const uint8_t *master_mac, static void handle_led_ring(const uint8_t *master_mac, const alox_EspNowLedRing *msg) { - uint32_t my_id = esp_now_core_own_mac()[5]; - if (msg->client_id != 0 && msg->client_id != my_id) { + if (!client_id_matches(msg->client_id)) { return; } if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { @@ -313,13 +390,12 @@ static void handle_led_ring(const uint8_t *master_mac, char mac_str[18]; esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); ESP_LOGI(TAG, "LED_RING mode %lu from master %s (id=%lu)", - (unsigned long)req.mode, mac_str, (unsigned long)my_id); + (unsigned long)req.mode, mac_str, (unsigned long)my_client_id()); cmd_led_ring_apply(&req); } static void handle_find_me(const uint8_t *master_mac, const alox_EspNowFindMe *req) { - uint32_t my_id = esp_now_core_own_mac()[5]; - if (req->client_id != 0 && req->client_id != my_id) { + if (!client_id_matches(req->client_id)) { return; } if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { @@ -327,14 +403,14 @@ static void handle_find_me(const uint8_t *master_mac, const alox_EspNowFindMe *r } char mac_str[18]; esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, "FIND_ME from master %s (id=%lu)", mac_str, (unsigned long)my_id); + ESP_LOGI(TAG, "FIND_ME from master %s (id=%lu)", mac_str, + (unsigned long)my_client_id()); led_ring_find_me(); } static void handle_accel_stream(const uint8_t *master_mac, const alox_EspNowAccelStream *cfg) { - uint32_t my_id = esp_now_core_own_mac()[5]; - if (cfg->client_id != 0 && cfg->client_id != my_id) { + if (!client_id_matches(cfg->client_id)) { return; } if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { @@ -344,13 +420,12 @@ static void handle_accel_stream(const uint8_t *master_mac, char mac_str[18]; esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); ESP_LOGI(TAG, "accel stream %s from master %s (id=%lu)", - cfg->enable ? "on" : "off", mac_str, (unsigned long)my_id); + cfg->enable ? "on" : "off", mac_str, (unsigned long)my_client_id()); } static void handle_tap_notify(const uint8_t *master_mac, const alox_EspNowTapNotify *cfg) { - uint32_t my_id = esp_now_core_own_mac()[5]; - if (cfg->client_id != 0 && cfg->client_id != my_id) { + if (!client_id_matches(cfg->client_id)) { return; } if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { @@ -364,13 +439,12 @@ static void handle_tap_notify(const uint8_t *master_mac, ESP_LOGI(TAG, "tap notify single=%d double=%d triple=%d from master %s (id=%lu)", cfg->single, cfg->double_tap, cfg->triple, mac_str, - (unsigned long)my_id); + (unsigned long)my_client_id()); } static void handle_accel_deadzone(const uint8_t *master_mac, const alox_EspNowAccelDeadzone *cfg) { - uint32_t my_id = esp_now_core_own_mac()[5]; - if (cfg->client_id != 0 && cfg->client_id != my_id) { + if (!client_id_matches(cfg->client_id)) { return; } if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { @@ -380,7 +454,7 @@ static void handle_accel_deadzone(const uint8_t *master_mac, esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); ESP_LOGI(TAG, "accel deadzone from master %s: %lu LSB id=%lu (sensor %s)", mac_str, - (unsigned long)cfg->deadzone, (unsigned long)my_id, + (unsigned long)cfg->deadzone, (unsigned long)my_client_id(), bma456_is_ready() ? "ok" : "not installed"); bma456_set_accel_deadzone(cfg->deadzone); if (pod_settings_save_accel_deadzone(cfg->deadzone) != ESP_OK) { @@ -391,8 +465,7 @@ static void handle_accel_deadzone(const uint8_t *master_mac, static void handle_tap_config(const uint8_t *master_mac, const alox_EspNowTapConfig *msg) { - uint32_t my_id = esp_now_core_own_mac()[5]; - if (msg->client_id != 0 && msg->client_id != my_id) { + if (!client_id_matches(msg->client_id)) { return; } if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) { @@ -415,7 +488,7 @@ static void handle_tap_config(const uint8_t *master_mac, esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str)); ESP_LOGI(TAG, "tap config from master %s: sens=%u id=%lu (sensor %s)", mac_str, - (unsigned)cfg.tap_sens_thres, (unsigned long)my_id, + (unsigned)cfg.tap_sens_thres, (unsigned long)my_client_id(), bma456_is_ready() ? "ok" : "not installed"); if (!bma456_tap_config_valid(&cfg)) { @@ -431,6 +504,21 @@ static void handle_tap_config(const uint8_t *master_mac, } } +static void handle_assign_id(const uint8_t *master_mac, + const alox_EspNowAssignId *msg) { + if (!from_joined_master(master_mac)) { + return; + } + apply_assigned_id(msg->assigned_id); +} + +static void handle_clear_id_binding(const uint8_t *master_mac) { + if (!from_joined_master(master_mac)) { + return; + } + clear_id_binding_local(); +} + static void handle_discover(const uint8_t *sender_mac, const alox_EspNowDiscover *discover) { if (discover->network != esp_now_core_network()) { @@ -464,11 +552,12 @@ static void handle_discover(const uint8_t *sender_mac, char mac_str[18]; esp_now_core_mac_to_str(sender_mac, mac_str, sizeof(mac_str)); - ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)discover->network, - mac_str); + ESP_LOGI(TAG, "joined network %u, master %s (preferred_id=%lu)", + (unsigned)discover->network, mac_str, + (unsigned long)preferred_id_for_master()); queue_tx(SLAVE_TX_SLAVE_INFO); - queue_tx(SLAVE_TX_BATTERY); + /* Battery after ASSIGN_ID so reports carry the definitive client_id. */ } static void check_master_timeout(void) { @@ -489,14 +578,14 @@ static void check_master_timeout(void) { static void accel_stream_task(void *param) { (void)param; - const uint8_t *own = esp_now_core_own_mac(); ESP_LOGI(TAG, "accel stream task (interval %u ms)", (unsigned)ESPNOW_ACCEL_INTERVAL_MS); while (1) { vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS)); - if (!s_joined || !s_accel_stream_enabled || !bma456_is_ready()) { + if (!s_joined || s_assigned_id == 0 || !s_accel_stream_enabled || + !bma456_is_ready()) { continue; } int16_t x = 0; @@ -505,13 +594,13 @@ static void accel_stream_task(void *param) { if (bma456_read_accel(&x, &y, &z) != ESP_OK) { continue; } - (void)send_accel_sample(s_master_mac, own[5], x, y, z); + (void)send_accel_sample(s_master_mac, s_assigned_id, x, y, z); } } static void on_bma456_tap(bma456_tap_kind_t kind, void *ctx) { (void)ctx; - if (!s_joined) { + if (!s_joined || s_assigned_id == 0) { return; } bool enabled = false; @@ -531,7 +620,7 @@ static void on_bma456_tap(bma456_tap_kind_t kind, void *ctx) { if (!enabled) { return; } - (void)send_tap_event(s_master_mac, esp_now_core_own_mac()[5], (uint32_t)kind); + (void)send_tap_event(s_master_mac, s_assigned_id, (uint32_t)kind); } static void heartbeat_task(void *param) { @@ -654,6 +743,12 @@ void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data, handle_restart(info->src_addr, &msg.payload.restart); } break; + case alox_EspNowMessage_assign_id_tag: + handle_assign_id(info->src_addr, &msg.payload.assign_id); + break; + case alox_EspNowMessage_clear_id_binding_tag: + handle_clear_id_binding(info->src_addr); + break; case alox_EspNowMessage_ota_start_tag: if (from_joined_master(info->src_addr)) { ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start); @@ -668,6 +763,7 @@ void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data, esp_err_t esp_now_slave_start(void) { reset_join(); + load_id_binding(); s_tx_queue = xQueueCreate(4, sizeof(slave_tx_op_t)); if (s_tx_queue == NULL) { diff --git a/main/pod_settings.c b/main/pod_settings.c index a2b23e6..2faa7c4 100644 --- a/main/pod_settings.c +++ b/main/pod_settings.c @@ -10,9 +10,31 @@ static const char *NS = "powerpod"; static const char *KEY_ACCEL_DZ = "accel_dz"; static const char *KEY_UV_LATCH = "uv_latch"; static const char *KEY_TAP_CFG = "tap_cfg"; +static const char *KEY_ID_MAP = "id_map"; +static const char *KEY_ID_BIND = "id_bind"; #define ACCEL_DEADZONE_MAX 4095u #define TAP_CFG_NVS_VERSION 1u +#define ID_MAP_NVS_VERSION 1u +#define ID_BIND_NVS_VERSION 1u + +typedef struct __attribute__((packed)) { + uint8_t mac[POD_ID_MAC_LEN]; + uint8_t id; +} id_map_entry_nvs_t; + +typedef struct __attribute__((packed)) { + uint8_t version; + uint8_t next_id; + uint8_t count; + id_map_entry_nvs_t entries[POD_ID_MAP_MAX]; +} id_map_nvs_t; + +typedef struct __attribute__((packed)) { + uint8_t version; + uint8_t master_mac[POD_ID_MAC_LEN]; + uint8_t assigned_id; +} id_bind_nvs_t; /** Packed layout for NVS blob (stable across rebuilds). */ typedef struct __attribute__((packed)) { @@ -302,3 +324,232 @@ esp_err_t pod_settings_set_uv_latched(bool latched) { esp_err_t pod_settings_clear_uv_latched(void) { return pod_settings_set_uv_latched(false); } + +esp_err_t pod_settings_load_id_map(pod_id_map_t *out) { + if (out == NULL) { + return ESP_ERR_INVALID_ARG; + } + memset(out, 0, sizeof(*out)); + out->next_id = (uint8_t)POD_CLIENT_ID_MIN; + if (!s_nvs_ready) { + return ESP_OK; + } + + nvs_handle_t handle; + esp_err_t err = nvs_open(NS, NVS_READONLY, &handle); + if (err != ESP_OK) { + return ESP_OK; + } + + id_map_nvs_t blob; + memset(&blob, 0, sizeof(blob)); + size_t len = sizeof(blob); + err = nvs_get_blob(handle, KEY_ID_MAP, &blob, &len); + nvs_close(handle); + + if (err == ESP_ERR_NVS_NOT_FOUND) { + return ESP_OK; + } + if (err != ESP_OK || len < 3 || blob.version != ID_MAP_NVS_VERSION) { + ESP_LOGW(TAG, "read id_map failed: %s (len=%u)", esp_err_to_name(err), + (unsigned)len); + return ESP_OK; + } + if (blob.count > POD_ID_MAP_MAX) { + ESP_LOGW(TAG, "id_map count %u invalid", (unsigned)blob.count); + return ESP_OK; + } + if (blob.next_id < POD_CLIENT_ID_MIN) { + blob.next_id = (uint8_t)POD_CLIENT_ID_MIN; + } + + out->next_id = blob.next_id; + out->count = blob.count; + for (uint8_t i = 0; i < blob.count; i++) { + memcpy(out->entries[i].mac, blob.entries[i].mac, POD_ID_MAC_LEN); + out->entries[i].id = blob.entries[i].id; + } + ESP_LOGI(TAG, "loaded id_map count=%u next_id=%u", (unsigned)out->count, + (unsigned)out->next_id); + return ESP_OK; +} + +esp_err_t pod_settings_save_id_map(const pod_id_map_t *map) { + if (map == NULL || map->count > POD_ID_MAP_MAX) { + return ESP_ERR_INVALID_ARG; + } + if (map->next_id < POD_CLIENT_ID_MIN) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t err = ensure_nvs(); + if (err != ESP_OK) { + return err; + } + + id_map_nvs_t blob; + memset(&blob, 0, sizeof(blob)); + blob.version = ID_MAP_NVS_VERSION; + blob.next_id = map->next_id; + blob.count = map->count; + for (uint8_t i = 0; i < map->count; i++) { + memcpy(blob.entries[i].mac, map->entries[i].mac, POD_ID_MAC_LEN); + blob.entries[i].id = map->entries[i].id; + } + + nvs_handle_t handle; + err = nvs_open(NS, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); + return err; + } + + err = nvs_set_blob(handle, KEY_ID_MAP, &blob, sizeof(blob)); + if (err == ESP_OK) { + err = nvs_commit(handle); + } + nvs_close(handle); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "save id_map failed: %s", esp_err_to_name(err)); + return err; + } + ESP_LOGI(TAG, "saved id_map count=%u next_id=%u", (unsigned)map->count, + (unsigned)map->next_id); + return ESP_OK; +} + +esp_err_t pod_settings_clear_id_map(void) { + esp_err_t err = ensure_nvs(); + if (err != ESP_OK) { + return err; + } + + nvs_handle_t handle; + err = nvs_open(NS, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); + return err; + } + + err = nvs_erase_key(handle, KEY_ID_MAP); + if (err == ESP_ERR_NVS_NOT_FOUND) { + err = ESP_OK; + } + if (err == ESP_OK) { + err = nvs_commit(handle); + } + nvs_close(handle); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "clear id_map failed: %s", esp_err_to_name(err)); + return err; + } + ESP_LOGI(TAG, "cleared id_map"); + return ESP_OK; +} + +bool pod_settings_load_id_binding(uint8_t master_mac[POD_ID_MAC_LEN], + uint32_t *assigned_id_out) { + if (master_mac == NULL || assigned_id_out == NULL) { + return false; + } + memset(master_mac, 0, POD_ID_MAC_LEN); + *assigned_id_out = 0; + if (!s_nvs_ready) { + return false; + } + + nvs_handle_t handle; + esp_err_t err = nvs_open(NS, NVS_READONLY, &handle); + if (err != ESP_OK) { + return false; + } + + id_bind_nvs_t blob = {0}; + size_t len = sizeof(blob); + err = nvs_get_blob(handle, KEY_ID_BIND, &blob, &len); + nvs_close(handle); + + if (err != ESP_OK || len != sizeof(blob) || + blob.version != ID_BIND_NVS_VERSION) { + return false; + } + if (blob.assigned_id < POD_CLIENT_ID_MIN) { + return false; + } + + memcpy(master_mac, blob.master_mac, POD_ID_MAC_LEN); + *assigned_id_out = blob.assigned_id; + return true; +} + +esp_err_t pod_settings_save_id_binding(const uint8_t master_mac[POD_ID_MAC_LEN], + uint32_t assigned_id) { + if (master_mac == NULL || assigned_id < POD_CLIENT_ID_MIN || + assigned_id > POD_CLIENT_ID_MAX) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t err = ensure_nvs(); + if (err != ESP_OK) { + return err; + } + + id_bind_nvs_t blob = { + .version = ID_BIND_NVS_VERSION, + .assigned_id = (uint8_t)assigned_id, + }; + memcpy(blob.master_mac, master_mac, POD_ID_MAC_LEN); + + nvs_handle_t handle; + err = nvs_open(NS, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); + return err; + } + + err = nvs_set_blob(handle, KEY_ID_BIND, &blob, sizeof(blob)); + if (err == ESP_OK) { + err = nvs_commit(handle); + } + nvs_close(handle); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "save id_bind failed: %s", esp_err_to_name(err)); + return err; + } + ESP_LOGI(TAG, "saved id_bind id=%lu master=%02x:…:%02x", + (unsigned long)assigned_id, master_mac[0], master_mac[5]); + return ESP_OK; +} + +esp_err_t pod_settings_clear_id_binding(void) { + esp_err_t err = ensure_nvs(); + if (err != ESP_OK) { + return err; + } + + nvs_handle_t handle; + err = nvs_open(NS, NVS_READWRITE, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); + return err; + } + + err = nvs_erase_key(handle, KEY_ID_BIND); + if (err == ESP_ERR_NVS_NOT_FOUND) { + err = ESP_OK; + } + if (err == ESP_OK) { + err = nvs_commit(handle); + } + nvs_close(handle); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "clear id_bind failed: %s", esp_err_to_name(err)); + return err; + } + ESP_LOGI(TAG, "cleared id_bind"); + return ESP_OK; +} diff --git a/main/pod_settings.h b/main/pod_settings.h index f472cb2..46fcdbc 100644 --- a/main/pod_settings.h +++ b/main/pod_settings.h @@ -35,4 +35,36 @@ bool pod_settings_is_uv_latched(void); esp_err_t pod_settings_set_uv_latched(bool latched); esp_err_t pod_settings_clear_uv_latched(void); +/** Logical slave ids: 1…POD_CLIENT_ID_MAX (0 reserved for local master). */ +#define POD_CLIENT_ID_MIN 1u +#define POD_CLIENT_ID_MAX 255u +#define POD_ID_MAP_MAX 255u +#define POD_ID_MAC_LEN 6u + +typedef struct { + uint8_t mac[POD_ID_MAC_LEN]; + uint8_t id; +} pod_id_map_entry_t; + +typedef struct { + uint8_t next_id; /**< Next candidate when allocating (1…255). */ + uint8_t count; + pod_id_map_entry_t entries[POD_ID_MAP_MAX]; +} pod_id_map_t; + +/** Master: load MAC→id map from NVS (zeros *out if unset). */ +esp_err_t pod_settings_load_id_map(pod_id_map_t *out); +esp_err_t pod_settings_save_id_map(const pod_id_map_t *map); +esp_err_t pod_settings_clear_id_map(void); + +/** + * Slave: load persisted {master_mac, assigned_id}. + * Returns true when a valid binding was loaded. + */ +bool pod_settings_load_id_binding(uint8_t master_mac[POD_ID_MAC_LEN], + uint32_t *assigned_id_out); +esp_err_t pod_settings_save_id_binding(const uint8_t master_mac[POD_ID_MAC_LEN], + uint32_t assigned_id); +esp_err_t pod_settings_clear_id_binding(void); + #endif diff --git a/main/powerpod.c b/main/powerpod.c index 32cd55c..8c04378 100644 --- a/main/powerpod.c +++ b/main/powerpod.c @@ -9,6 +9,7 @@ #include "cmd_espnow_echo_ping.h" #include "cmd_espnow_find_me.h" #include "cmd_restart.h" +#include "cmd_client_id_map_reset.h" #include "cmd_client_info.h" #include "cmd_version.h" #include "cmd_ota.h" @@ -226,6 +227,7 @@ void app_main(void) { cmd_espnow_echo_ping_register(); cmd_espnow_find_me_register(); cmd_restart_register(); + cmd_client_id_map_reset_register(); cmd_led_ring_register(); cmd_battery_register(); cmd_ota_register(); diff --git a/main/proto/esp_now_messages.pb.c b/main/proto/esp_now_messages.pb.c index 9f767b5..4b278c7 100644 --- a/main/proto/esp_now_messages.pb.c +++ b/main/proto/esp_now_messages.pb.c @@ -27,6 +27,12 @@ PB_BIND(alox_EspNowDiscover, alox_EspNowDiscover, AUTO) PB_BIND(alox_EspNowSlavePresence, alox_EspNowSlavePresence, AUTO) +PB_BIND(alox_EspNowAssignId, alox_EspNowAssignId, AUTO) + + +PB_BIND(alox_EspNowClearIdBinding, alox_EspNowClearIdBinding, AUTO) + + PB_BIND(alox_EspNowAccelDeadzone, alox_EspNowAccelDeadzone, AUTO) diff --git a/main/proto/esp_now_messages.pb.h b/main/proto/esp_now_messages.pb.h index cafaab3..6788646 100644 --- a/main/proto/esp_now_messages.pb.h +++ b/main/proto/esp_now_messages.pb.h @@ -32,7 +32,11 @@ typedef enum _alox_EspNowMessageType { alox_EspNowMessageType_ESPNOW_TAP_EVENT = 18, alox_EspNowMessageType_ESPNOW_ECHO_PING = 19, alox_EspNowMessageType_ESPNOW_ECHO_PONG = 20, - alox_EspNowMessageType_ESPNOW_SET_TAP_CONFIG = 21 + alox_EspNowMessageType_ESPNOW_SET_TAP_CONFIG = 21, + /* * Master → slave: assigned logical client id after join/presence. */ + alox_EspNowMessageType_ESPNOW_ASSIGN_ID = 22, + /* * Master → slave: forget persisted master/id binding (debug reset). */ + alox_EspNowMessageType_ESPNOW_CLEAR_ID_BINDING = 23 } alox_EspNowMessageType; /* Struct definitions */ @@ -74,11 +78,22 @@ typedef struct _alox_EspNowSlavePresence { uint32_t network; pb_callback_t mac; uint32_t version; + /* * Preferred id from NVS for this master (0 = none / unbound). */ uint32_t slave_id; bool available; bool used; } alox_EspNowSlavePresence; +/* * Master → slave: definitive logical client id for this session/master. */ +typedef struct _alox_EspNowAssignId { + uint32_t assigned_id; +} alox_EspNowAssignId; + +/* * Master → slave: clear persisted {master_mac, assigned_id} binding. */ +typedef struct _alox_EspNowClearIdBinding { + char dummy_field; +} alox_EspNowClearIdBinding; + typedef struct _alox_EspNowAccelDeadzone { uint32_t deadzone; uint32_t client_id; /* 0 = all slaves; otherwise only matching slave_id applies */ @@ -204,6 +219,8 @@ typedef struct _alox_EspNowMessage { alox_EspNowEchoPing echo_ping; alox_EspNowEchoPong echo_pong; alox_EspNowTapConfig tap_config; + alox_EspNowAssignId assign_id; + alox_EspNowClearIdBinding clear_id_binding; } payload; } alox_EspNowMessage; @@ -214,8 +231,10 @@ extern "C" { /* Helper constants for enums */ #define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN -#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_SET_TAP_CONFIG -#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_SET_TAP_CONFIG+1)) +#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_CLEAR_ID_BINDING +#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_CLEAR_ID_BINDING+1)) + + @@ -248,6 +267,8 @@ extern "C" { #define alox_EspNowRestart_init_default {0} #define alox_EspNowDiscover_init_default {0, 0} #define alox_EspNowSlavePresence_init_default {0, {{NULL}, NULL}, 0, 0, 0, 0} +#define alox_EspNowAssignId_init_default {0} +#define alox_EspNowClearIdBinding_init_default {0} #define alox_EspNowAccelDeadzone_init_default {0, 0} #define alox_EspNowAccelStream_init_default {0, 0} #define alox_EspNowAccelSample_init_default {0, 0, 0, 0} @@ -269,6 +290,8 @@ extern "C" { #define alox_EspNowRestart_init_zero {0} #define alox_EspNowDiscover_init_zero {0, 0} #define alox_EspNowSlavePresence_init_zero {0, {{NULL}, NULL}, 0, 0, 0, 0} +#define alox_EspNowAssignId_init_zero {0} +#define alox_EspNowClearIdBinding_init_zero {0} #define alox_EspNowAccelDeadzone_init_zero {0, 0} #define alox_EspNowAccelStream_init_zero {0, 0} #define alox_EspNowAccelSample_init_zero {0, 0, 0, 0} @@ -300,6 +323,7 @@ extern "C" { #define alox_EspNowSlavePresence_slave_id_tag 4 #define alox_EspNowSlavePresence_available_tag 5 #define alox_EspNowSlavePresence_used_tag 6 +#define alox_EspNowAssignId_assigned_id_tag 1 #define alox_EspNowAccelDeadzone_deadzone_tag 1 #define alox_EspNowAccelDeadzone_client_id_tag 2 #define alox_EspNowAccelStream_enable_tag 1 @@ -368,6 +392,8 @@ extern "C" { #define alox_EspNowMessage_echo_ping_tag 20 #define alox_EspNowMessage_echo_pong_tag 21 #define alox_EspNowMessage_tap_config_tag 22 +#define alox_EspNowMessage_assign_id_tag 23 +#define alox_EspNowMessage_clear_id_binding_tag 24 /* Struct field encoding specification for nanopb */ #define alox_EspNowUnicastTest_FIELDLIST(X, a) \ @@ -413,6 +439,16 @@ X(a, STATIC, SINGULAR, BOOL, used, 6) #define alox_EspNowSlavePresence_CALLBACK pb_default_field_callback #define alox_EspNowSlavePresence_DEFAULT NULL +#define alox_EspNowAssignId_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, assigned_id, 1) +#define alox_EspNowAssignId_CALLBACK NULL +#define alox_EspNowAssignId_DEFAULT NULL + +#define alox_EspNowClearIdBinding_FIELDLIST(X, a) \ + +#define alox_EspNowClearIdBinding_CALLBACK NULL +#define alox_EspNowClearIdBinding_DEFAULT NULL + #define alox_EspNowAccelDeadzone_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, deadzone, 1) \ X(a, STATIC, SINGULAR, UINT32, client_id, 2) @@ -534,7 +570,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify,payload.tap_notify), 18) X(a, STATIC, ONEOF, MESSAGE, (payload,tap_event,payload.tap_event), 19) \ X(a, STATIC, ONEOF, MESSAGE, (payload,echo_ping,payload.echo_ping), 20) \ X(a, STATIC, ONEOF, MESSAGE, (payload,echo_pong,payload.echo_pong), 21) \ -X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config,payload.tap_config), 22) +X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config,payload.tap_config), 22) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,assign_id,payload.assign_id), 23) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,clear_id_binding,payload.clear_id_binding), 24) #define alox_EspNowMessage_CALLBACK NULL #define alox_EspNowMessage_DEFAULT NULL #define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover @@ -558,6 +596,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config,payload.tap_config), 22) #define alox_EspNowMessage_payload_echo_ping_MSGTYPE alox_EspNowEchoPing #define alox_EspNowMessage_payload_echo_pong_MSGTYPE alox_EspNowEchoPong #define alox_EspNowMessage_payload_tap_config_MSGTYPE alox_EspNowTapConfig +#define alox_EspNowMessage_payload_assign_id_MSGTYPE alox_EspNowAssignId +#define alox_EspNowMessage_payload_clear_id_binding_MSGTYPE alox_EspNowClearIdBinding extern const pb_msgdesc_t alox_EspNowUnicastTest_msg; extern const pb_msgdesc_t alox_EspNowEchoPing_msg; @@ -566,6 +606,8 @@ extern const pb_msgdesc_t alox_EspNowFindMe_msg; extern const pb_msgdesc_t alox_EspNowRestart_msg; extern const pb_msgdesc_t alox_EspNowDiscover_msg; extern const pb_msgdesc_t alox_EspNowSlavePresence_msg; +extern const pb_msgdesc_t alox_EspNowAssignId_msg; +extern const pb_msgdesc_t alox_EspNowClearIdBinding_msg; extern const pb_msgdesc_t alox_EspNowAccelDeadzone_msg; extern const pb_msgdesc_t alox_EspNowAccelStream_msg; extern const pb_msgdesc_t alox_EspNowAccelSample_msg; @@ -589,6 +631,8 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg; #define alox_EspNowRestart_fields &alox_EspNowRestart_msg #define alox_EspNowDiscover_fields &alox_EspNowDiscover_msg #define alox_EspNowSlavePresence_fields &alox_EspNowSlavePresence_msg +#define alox_EspNowAssignId_fields &alox_EspNowAssignId_msg +#define alox_EspNowClearIdBinding_fields &alox_EspNowClearIdBinding_msg #define alox_EspNowAccelDeadzone_fields &alox_EspNowAccelDeadzone_msg #define alox_EspNowAccelStream_fields &alox_EspNowAccelStream_msg #define alox_EspNowAccelSample_fields &alox_EspNowAccelSample_msg @@ -611,8 +655,10 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg; #define alox_EspNowAccelDeadzone_size 12 #define alox_EspNowAccelSample_size 24 #define alox_EspNowAccelStream_size 8 +#define alox_EspNowAssignId_size 6 #define alox_EspNowBatteryQuery_size 6 #define alox_EspNowBatteryReport_size 22 +#define alox_EspNowClearIdBinding_size 0 #define alox_EspNowDiscover_size 8 #define alox_EspNowEchoPing_size 22 #define alox_EspNowEchoPong_size 22 diff --git a/main/proto/esp_now_messages.proto b/main/proto/esp_now_messages.proto index 593dccf..c70f137 100644 --- a/main/proto/esp_now_messages.proto +++ b/main/proto/esp_now_messages.proto @@ -27,6 +27,10 @@ enum EspNowMessageType { ESPNOW_ECHO_PING = 19; ESPNOW_ECHO_PONG = 20; ESPNOW_SET_TAP_CONFIG = 21; + /** Master → slave: assigned logical client id after join/presence. */ + ESPNOW_ASSIGN_ID = 22; + /** Master → slave: forget persisted master/id binding (debug reset). */ + ESPNOW_CLEAR_ID_BINDING = 23; } message EspNowUnicastTest { @@ -67,11 +71,20 @@ message EspNowSlavePresence { uint32 network = 1; bytes mac = 2; uint32 version = 3; + /** Preferred id from NVS for this master (0 = none / unbound). */ uint32 slave_id = 4; bool available = 5; bool used = 6; } +/** Master → slave: definitive logical client id for this session/master. */ +message EspNowAssignId { + uint32 assigned_id = 1; +} + +/** Master → slave: clear persisted {master_mac, assigned_id} binding. */ +message EspNowClearIdBinding {} + message EspNowAccelDeadzone { uint32 deadzone = 1; uint32 client_id = 2; // 0 = all slaves; otherwise only matching slave_id applies @@ -193,5 +206,7 @@ message EspNowMessage { EspNowEchoPing echo_ping = 20; EspNowEchoPong echo_pong = 21; EspNowTapConfig tap_config = 22; + EspNowAssignId assign_id = 23; + EspNowClearIdBinding clear_id_binding = 24; } } diff --git a/main/proto/uart_messages.pb.c b/main/proto/uart_messages.pb.c index 245104f..7c2936b 100644 --- a/main/proto/uart_messages.pb.c +++ b/main/proto/uart_messages.pb.c @@ -114,6 +114,12 @@ PB_BIND(alox_RestartRequest, alox_RestartRequest, AUTO) PB_BIND(alox_RestartResponse, alox_RestartResponse, AUTO) +PB_BIND(alox_ClientIdMapResetRequest, alox_ClientIdMapResetRequest, AUTO) + + +PB_BIND(alox_ClientIdMapResetResponse, alox_ClientIdMapResetResponse, AUTO) + + PB_BIND(alox_SetLogLevelRequest, alox_SetLogLevelRequest, AUTO) diff --git a/main/proto/uart_messages.pb.h b/main/proto/uart_messages.pb.h index 440d953..dd328ed 100644 --- a/main/proto/uart_messages.pb.h +++ b/main/proto/uart_messages.pb.h @@ -40,7 +40,11 @@ typedef enum _alox_MessageType { /* * Host → master: get/set BMA456H multitap detection parameters. */ alox_MessageType_TAP_CONFIG = 32, /* * Master → host (BLE only): periodic pushed cache snapshot with ESP timestamp. */ - alox_MessageType_BLE_STREAM_TICK = 33 + alox_MessageType_BLE_STREAM_TICK = 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. */ + alox_MessageType_CLIENT_ID_MAP_RESET = 34 } alox_MessageType; typedef enum _alox_TapKind { @@ -323,6 +327,15 @@ typedef struct _alox_RestartResponse { uint32_t client_id; } alox_RestartResponse; +/* * Host → master or slave: clear persisted client-id assignment state. */ +typedef struct _alox_ClientIdMapResetRequest { + char dummy_field; +} alox_ClientIdMapResetRequest; + +typedef struct _alox_ClientIdMapResetResponse { + bool success; +} alox_ClientIdMapResetResponse; + /* * Host → master: read/write global log level (esp_log_level_set("*", …)). */ typedef struct _alox_SetLogLevelRequest { bool write; @@ -456,6 +469,8 @@ typedef struct _alox_UartMessage { alox_TapConfigRequest tap_config_request; alox_TapConfigResponse tap_config_response; alox_BleStreamTick ble_stream_tick; + alox_ClientIdMapResetRequest client_id_map_reset_request; + alox_ClientIdMapResetResponse client_id_map_reset_response; } payload; } alox_UartMessage; @@ -466,8 +481,8 @@ extern "C" { /* Helper constants for enums */ #define _alox_MessageType_MIN alox_MessageType_UNKNOWN -#define _alox_MessageType_MAX alox_MessageType_BLE_STREAM_TICK -#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_BLE_STREAM_TICK+1)) +#define _alox_MessageType_MAX alox_MessageType_CLIENT_ID_MAP_RESET +#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_CLIENT_ID_MAP_RESET+1)) #define _alox_TapKind_MIN alox_TapKind_TAP_NONE #define _alox_TapKind_MAX alox_TapKind_TAP_TRIPLE @@ -521,6 +536,8 @@ extern "C" { + + @@ -561,6 +578,8 @@ extern "C" { #define alox_EspNowFindMeResponse_init_default {0, 0} #define alox_RestartRequest_init_default {0} #define alox_RestartResponse_init_default {0, 0} +#define alox_ClientIdMapResetRequest_init_default {0} +#define alox_ClientIdMapResetResponse_init_default {0} #define alox_SetLogLevelRequest_init_default {0, 0} #define alox_SetLogLevelResponse_init_default {0, 0} #define alox_TapConfigRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} @@ -608,6 +627,8 @@ extern "C" { #define alox_EspNowFindMeResponse_init_zero {0, 0} #define alox_RestartRequest_init_zero {0} #define alox_RestartResponse_init_zero {0, 0} +#define alox_ClientIdMapResetRequest_init_zero {0} +#define alox_ClientIdMapResetResponse_init_zero {0} #define alox_SetLogLevelRequest_init_zero {0, 0} #define alox_SetLogLevelResponse_init_zero {0, 0} #define alox_TapConfigRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} @@ -739,6 +760,7 @@ extern "C" { #define alox_RestartRequest_client_id_tag 1 #define alox_RestartResponse_success_tag 1 #define alox_RestartResponse_client_id_tag 2 +#define alox_ClientIdMapResetResponse_success_tag 1 #define alox_SetLogLevelRequest_write_tag 1 #define alox_SetLogLevelRequest_level_tag 2 #define alox_SetLogLevelResponse_success_tag 1 @@ -822,6 +844,8 @@ extern "C" { #define alox_UartMessage_tap_config_request_tag 39 #define alox_UartMessage_tap_config_response_tag 40 #define alox_UartMessage_ble_stream_tick_tag 41 +#define alox_UartMessage_client_id_map_reset_request_tag 42 +#define alox_UartMessage_client_id_map_reset_response_tag 43 /* Struct field encoding specification for nanopb */ #define alox_UartMessage_FIELDLIST(X, a) \ @@ -861,7 +885,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,set_log_level_request,payload.set_lo X(a, STATIC, ONEOF, MESSAGE, (payload,set_log_level_response,payload.set_log_level_response), 38) \ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config_request,payload.tap_config_request), 39) \ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config_response,payload.tap_config_response), 40) \ -X(a, STATIC, ONEOF, MESSAGE, (payload,ble_stream_tick,payload.ble_stream_tick), 41) +X(a, STATIC, ONEOF, MESSAGE, (payload,ble_stream_tick,payload.ble_stream_tick), 41) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,client_id_map_reset_request,payload.client_id_map_reset_request), 42) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,client_id_map_reset_response,payload.client_id_map_reset_response), 43) #define alox_UartMessage_CALLBACK NULL #define alox_UartMessage_DEFAULT NULL #define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack @@ -900,6 +926,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,ble_stream_tick,payload.ble_stream_t #define alox_UartMessage_payload_tap_config_request_MSGTYPE alox_TapConfigRequest #define alox_UartMessage_payload_tap_config_response_MSGTYPE alox_TapConfigResponse #define alox_UartMessage_payload_ble_stream_tick_MSGTYPE alox_BleStreamTick +#define alox_UartMessage_payload_client_id_map_reset_request_MSGTYPE alox_ClientIdMapResetRequest +#define alox_UartMessage_payload_client_id_map_reset_response_MSGTYPE alox_ClientIdMapResetResponse #define alox_Ack_FIELDLIST(X, a) \ @@ -1170,6 +1198,16 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 2) #define alox_RestartResponse_CALLBACK NULL #define alox_RestartResponse_DEFAULT NULL +#define alox_ClientIdMapResetRequest_FIELDLIST(X, a) \ + +#define alox_ClientIdMapResetRequest_CALLBACK NULL +#define alox_ClientIdMapResetRequest_DEFAULT NULL + +#define alox_ClientIdMapResetResponse_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, success, 1) +#define alox_ClientIdMapResetResponse_CALLBACK NULL +#define alox_ClientIdMapResetResponse_DEFAULT NULL + #define alox_SetLogLevelRequest_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, write, 1) \ X(a, STATIC, SINGULAR, UINT32, level, 2) @@ -1298,6 +1336,8 @@ extern const pb_msgdesc_t alox_EspNowFindMeRequest_msg; extern const pb_msgdesc_t alox_EspNowFindMeResponse_msg; extern const pb_msgdesc_t alox_RestartRequest_msg; extern const pb_msgdesc_t alox_RestartResponse_msg; +extern const pb_msgdesc_t alox_ClientIdMapResetRequest_msg; +extern const pb_msgdesc_t alox_ClientIdMapResetResponse_msg; extern const pb_msgdesc_t alox_SetLogLevelRequest_msg; extern const pb_msgdesc_t alox_SetLogLevelResponse_msg; extern const pb_msgdesc_t alox_TapConfigRequest_msg; @@ -1347,6 +1387,8 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg; #define alox_EspNowFindMeResponse_fields &alox_EspNowFindMeResponse_msg #define alox_RestartRequest_fields &alox_RestartRequest_msg #define alox_RestartResponse_fields &alox_RestartResponse_msg +#define alox_ClientIdMapResetRequest_fields &alox_ClientIdMapResetRequest_msg +#define alox_ClientIdMapResetResponse_fields &alox_ClientIdMapResetResponse_msg #define alox_SetLogLevelRequest_fields &alox_SetLogLevelRequest_msg #define alox_SetLogLevelResponse_fields &alox_SetLogLevelResponse_msg #define alox_TapConfigRequest_fields &alox_TapConfigRequest_msg @@ -1382,6 +1424,8 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg; #define alox_CacheClientTap_size 8 #define alox_CacheStatusRequest_size 0 #define alox_CacheStatusResponse_size 736 +#define alox_ClientIdMapResetRequest_size 0 +#define alox_ClientIdMapResetResponse_size 2 #define alox_ClientInput_size 22 #define alox_EspNowEchoPingRequest_size 17 #define alox_EspNowEchoPingResponse_size 25 diff --git a/main/proto/uart_messages.proto b/main/proto/uart_messages.proto index ef112d4..4d80abd 100644 --- a/main/proto/uart_messages.proto +++ b/main/proto/uart_messages.proto @@ -37,6 +37,11 @@ enum MessageType { TAP_CONFIG = 32; /** Master → host (BLE only): periodic pushed cache snapshot with ESP timestamp. */ BLE_STREAM_TICK = 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. + */ + CLIENT_ID_MAP_RESET = 34; } message UartMessage { @@ -78,6 +83,8 @@ message UartMessage { TapConfigRequest tap_config_request = 39; TapConfigResponse tap_config_response = 40; BleStreamTick ble_stream_tick = 41; + ClientIdMapResetRequest client_id_map_reset_request = 42; + ClientIdMapResetResponse client_id_map_reset_response = 43; } } @@ -349,6 +356,13 @@ message RestartResponse { uint32 client_id = 2; } +/** Host → master or slave: clear persisted client-id assignment state. */ +message ClientIdMapResetRequest {} + +message ClientIdMapResetResponse { + bool success = 1; +} + /** Host → master: read/write global log level (esp_log_level_set("*", …)). */ message SetLogLevelRequest { bool write = 1;