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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-01 17:25:07 +01:00
co-authored by Cursor
parent af20635047
commit 567dab6d0a
26 changed files with 1261 additions and 163 deletions
+144 -18
View File
@@ -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;