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
+117 -7
View File
@@ -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);
}
}