Add ESP-NOW heartbeat, client timeout, and slave reconnect.
Slaves send HEARTBEAT every 1s; the master marks clients inactive after 3s without traffic and reactivates on reconnect. CLIENT_INFO reports last_ping as milliseconds since the last packet, not uptime. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+135
-42
@@ -20,7 +20,14 @@
|
||||
#define ESPNOW_MAGIC 0xA1
|
||||
#define ESPNOW_MSG_DISCOVER 1
|
||||
#define ESPNOW_MSG_SLAVE_INFO 2
|
||||
#define ESPNOW_MSG_HEARTBEAT 3
|
||||
#define ESPNOW_DISCOVER_INTERVAL_MS 500
|
||||
#define ESPNOW_HEARTBEAT_INTERVAL_MS 1000
|
||||
#define ESPNOW_HEARTBEAT_MISS_COUNT 3
|
||||
#define ESPNOW_CLIENT_TIMEOUT_MS \
|
||||
(ESPNOW_HEARTBEAT_INTERVAL_MS * ESPNOW_HEARTBEAT_MISS_COUNT)
|
||||
#define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5)
|
||||
|
||||
static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff};
|
||||
|
||||
@@ -42,13 +49,18 @@ typedef struct __attribute__((packed)) {
|
||||
uint32_t slave_id;
|
||||
uint8_t available;
|
||||
uint8_t used;
|
||||
} espnow_slave_info_packet_t;
|
||||
} espnow_slave_packet_t;
|
||||
|
||||
static app_config_t s_config;
|
||||
static uint8_t s_wifi_channel;
|
||||
static uint8_t s_own_mac[ESP_NOW_ETH_ALEN];
|
||||
static bool s_slave_joined;
|
||||
static uint8_t s_master_mac[ESP_NOW_ETH_ALEN];
|
||||
static uint32_t s_last_discover_ms;
|
||||
|
||||
static uint32_t now_ms(void) {
|
||||
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static uint8_t network_to_channel(uint8_t network) {
|
||||
if (network < 1 || network > 13) {
|
||||
@@ -86,58 +98,48 @@ static esp_err_t ensure_peer(const uint8_t *mac) {
|
||||
|
||||
static esp_err_t ensure_broadcast_peer(void) { return ensure_peer(ESPNOW_BCAST); }
|
||||
|
||||
static void send_slave_info(const uint8_t *dest_mac) {
|
||||
espnow_slave_info_packet_t pkt = {
|
||||
.magic = ESPNOW_MAGIC,
|
||||
.type = ESPNOW_MSG_SLAVE_INFO,
|
||||
.network = s_config.network,
|
||||
.version = POWERPOD_FW_VERSION,
|
||||
.slave_id = s_own_mac[5],
|
||||
.available = 1,
|
||||
.used = 0,
|
||||
};
|
||||
memcpy(pkt.mac, s_own_mac, ESP_NOW_ETH_ALEN);
|
||||
static void build_slave_packet(espnow_slave_packet_t *pkt, uint8_t type) {
|
||||
pkt->magic = ESPNOW_MAGIC;
|
||||
pkt->type = type;
|
||||
pkt->network = s_config.network;
|
||||
memcpy(pkt->mac, s_own_mac, ESP_NOW_ETH_ALEN);
|
||||
pkt->version = POWERPOD_FW_VERSION;
|
||||
pkt->slave_id = s_own_mac[5];
|
||||
pkt->available = 1;
|
||||
pkt->used = 0;
|
||||
}
|
||||
|
||||
static void send_slave_packet(const uint8_t *dest_mac, uint8_t type) {
|
||||
espnow_slave_packet_t pkt;
|
||||
build_slave_packet(&pkt, type);
|
||||
|
||||
if (ensure_peer(dest_mac) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t err =
|
||||
esp_now_send(dest_mac, (const uint8_t *)&pkt, sizeof(pkt));
|
||||
esp_err_t err = esp_now_send(dest_mac, (const uint8_t *)&pkt, sizeof(pkt));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "slave info send failed: %s", esp_err_to_name(err));
|
||||
ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)type,
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_discover(const uint8_t *sender_mac,
|
||||
const espnow_discover_packet_t *pkt) {
|
||||
if (pkt->network != s_config.network) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_slave_joined && mac_equal(sender_mac, s_master_mac)) {
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN);
|
||||
s_slave_joined = true;
|
||||
|
||||
char mac_str[18];
|
||||
mac_to_str(sender_mac, mac_str, sizeof(mac_str));
|
||||
ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)pkt->network, mac_str);
|
||||
|
||||
send_slave_info(sender_mac);
|
||||
static void slave_reset_join(void) {
|
||||
s_slave_joined = false;
|
||||
memset(s_master_mac, 0, sizeof(s_master_mac));
|
||||
s_last_discover_ms = 0;
|
||||
}
|
||||
|
||||
static void handle_slave_info(const espnow_slave_info_packet_t *pkt) {
|
||||
static void handle_client_packet(const espnow_slave_packet_t *pkt) {
|
||||
if (pkt->network != s_config.network) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_new = false;
|
||||
esp_err_t err = client_registry_upsert(
|
||||
pkt->mac, pkt->slave_id, pkt->version, pkt->available != 0,
|
||||
pkt->used != 0, &is_new);
|
||||
bool reactivated = false;
|
||||
esp_err_t err = client_registry_heartbeat(
|
||||
pkt->mac, pkt->slave_id, pkt->version, pkt->used != 0, &is_new,
|
||||
&reactivated);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "client registry full");
|
||||
return;
|
||||
@@ -149,6 +151,88 @@ static void handle_slave_info(const espnow_slave_info_packet_t *pkt) {
|
||||
ESP_LOGI(TAG, "client registered id=%lu mac=%s ver=%lu",
|
||||
(unsigned long)pkt->slave_id, mac_str,
|
||||
(unsigned long)pkt->version);
|
||||
} else if (reactivated) {
|
||||
ESP_LOGI(TAG, "client reconnected id=%lu mac=%s",
|
||||
(unsigned long)pkt->slave_id, mac_str);
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_discover(const uint8_t *sender_mac,
|
||||
const espnow_discover_packet_t *pkt) {
|
||||
if (pkt->network != s_config.network) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = now_ms();
|
||||
|
||||
if (s_slave_joined) {
|
||||
if (!mac_equal(sender_mac, s_master_mac)) {
|
||||
return;
|
||||
}
|
||||
if ((now - s_last_discover_ms) <= SLAVE_MASTER_LOST_MS) {
|
||||
s_last_discover_ms = now;
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "master lost, rejoining");
|
||||
slave_reset_join();
|
||||
}
|
||||
|
||||
memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN);
|
||||
s_slave_joined = true;
|
||||
s_last_discover_ms = now;
|
||||
|
||||
char mac_str[18];
|
||||
mac_to_str(sender_mac, mac_str, sizeof(mac_str));
|
||||
ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)pkt->network, mac_str);
|
||||
|
||||
send_slave_packet(sender_mac, ESPNOW_MSG_SLAVE_INFO);
|
||||
}
|
||||
|
||||
static void slave_check_master_timeout(void) {
|
||||
if (!s_slave_joined) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = now_ms();
|
||||
if (s_last_discover_ms == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((now - s_last_discover_ms) > SLAVE_MASTER_LOST_MS) {
|
||||
ESP_LOGW(TAG, "no master discover for %u ms, reconnecting",
|
||||
(unsigned)(now - s_last_discover_ms));
|
||||
slave_reset_join();
|
||||
}
|
||||
}
|
||||
|
||||
static void slave_heartbeat_task(void *param) {
|
||||
(void)param;
|
||||
|
||||
ESP_LOGI(TAG, "slave heartbeat task (interval %u ms)",
|
||||
(unsigned)ESPNOW_HEARTBEAT_INTERVAL_MS);
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
|
||||
|
||||
slave_check_master_timeout();
|
||||
|
||||
if (!s_slave_joined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
send_slave_packet(s_master_mac, ESPNOW_MSG_HEARTBEAT);
|
||||
}
|
||||
}
|
||||
|
||||
static void master_monitor_task(void *param) {
|
||||
(void)param;
|
||||
|
||||
ESP_LOGI(TAG, "master monitor task (timeout %u ms)",
|
||||
(unsigned)ESPNOW_CLIENT_TIMEOUT_MS);
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
|
||||
client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,8 +254,9 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
|
||||
}
|
||||
break;
|
||||
case ESPNOW_MSG_SLAVE_INFO:
|
||||
if (s_config.master && len >= (int)sizeof(espnow_slave_info_packet_t)) {
|
||||
handle_slave_info((const espnow_slave_info_packet_t *)data);
|
||||
case ESPNOW_MSG_HEARTBEAT:
|
||||
if (s_config.master && len >= (int)sizeof(espnow_slave_packet_t)) {
|
||||
handle_client_packet((const espnow_slave_packet_t *)data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -241,8 +326,7 @@ esp_err_t esp_now_comm_init(const app_config_t *config) {
|
||||
memset(&s_config, 0, sizeof(s_config));
|
||||
memcpy(&s_config, config, sizeof(s_config));
|
||||
client_registry_init();
|
||||
s_slave_joined = false;
|
||||
memset(s_master_mac, 0, sizeof(s_master_mac));
|
||||
slave_reset_join();
|
||||
|
||||
s_wifi_channel = network_to_channel(config->network);
|
||||
ESP_ERROR_CHECK(esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA));
|
||||
@@ -269,8 +353,17 @@ esp_err_t esp_now_comm_init(const app_config_t *config) {
|
||||
ESP_LOGE(TAG, "failed to create discover task");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (xTaskCreate(master_monitor_task, "espnow_mon", 4096, NULL, 4, NULL) !=
|
||||
pdPASS) {
|
||||
ESP_LOGE(TAG, "failed to create monitor task");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "slave listening for master discover");
|
||||
if (xTaskCreate(slave_heartbeat_task, "espnow_hb", 4096, NULL, 4, NULL) !=
|
||||
pdPASS) {
|
||||
ESP_LOGE(TAG, "failed to create heartbeat task");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
|
||||
Reference in New Issue
Block a user