Refactor ESP-NOW air protocol to nanopb protobuf.

Add esp_now_messages.proto with EspNowMessage types, encode/decode helpers,
and Makefile targets to regenerate firmware and UART schemas together.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-18 22:44:57 +02:00
co-authored by Cursor
parent 6cdca4f3ad
commit 755bdd92d7
9 changed files with 405 additions and 100 deletions
+83 -76
View File
@@ -1,5 +1,6 @@
#include "client_registry.h"
#include "esp_now_comm.h"
#include "esp_now_proto.h"
#include "esp_err.h"
#include "esp_event.h"
#include "esp_log.h"
@@ -17,10 +18,6 @@
#define POWERPOD_FW_VERSION 1u
#endif
#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
@@ -33,24 +30,6 @@ static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff,
static const char *TAG = "[ESPNOW]";
typedef struct __attribute__((packed)) {
uint8_t magic;
uint8_t type;
uint8_t network;
uint8_t reserved;
} espnow_discover_packet_t;
typedef struct __attribute__((packed)) {
uint8_t magic;
uint8_t type;
uint8_t network;
uint8_t mac[ESP_NOW_ETH_ALEN];
uint32_t version;
uint32_t slave_id;
uint8_t available;
uint8_t used;
} 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];
@@ -98,30 +77,54 @@ 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 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 fill_presence(alox_EspNowSlavePresence *presence) {
presence->network = s_config.network;
presence->version = POWERPOD_FW_VERSION;
presence->slave_id = s_own_mac[5];
presence->available = true;
presence->used = false;
esp_now_proto_setup_presence_encode(presence, s_own_mac);
}
static void send_slave_packet(const uint8_t *dest_mac, uint8_t type) {
espnow_slave_packet_t pkt;
build_slave_packet(&pkt, type);
static esp_err_t send_message(const uint8_t *dest_mac,
const alox_EspNowMessage *msg) {
uint8_t buf[ESPNOW_PB_MAX_SIZE];
size_t len = 0;
esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len);
if (err != ESP_OK) {
ESP_LOGW(TAG, "encode failed");
return err;
}
if (ensure_peer(dest_mac) != ESP_OK) {
return;
return ESP_FAIL;
}
esp_err_t err = esp_now_send(dest_mac, (const uint8_t *)&pkt, sizeof(pkt));
err = esp_now_send(dest_mac, buf, len);
if (err != ESP_OK) {
ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)type,
ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type,
esp_err_to_name(err));
}
return err;
}
static void send_presence(const uint8_t *dest_mac,
alox_EspNowMessageType type) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
alox_EspNowSlavePresence *presence = NULL;
msg.type = type;
if (type == alox_EspNowMessageType_ESPNOW_SLAVE_INFO) {
msg.which_payload = alox_EspNowMessage_slave_info_tag;
presence = &msg.payload.slave_info;
} else {
msg.which_payload = alox_EspNowMessage_heartbeat_tag;
presence = &msg.payload.heartbeat;
}
fill_presence(presence);
send_message(dest_mac, &msg);
}
static void slave_reset_join(void) {
@@ -130,15 +133,16 @@ static void slave_reset_join(void) {
s_last_discover_ms = 0;
}
static void handle_client_packet(const espnow_slave_packet_t *pkt) {
if (pkt->network != s_config.network) {
static void handle_client_presence(const alox_EspNowSlavePresence *presence,
const uint8_t mac[CLIENT_MAC_LEN]) {
if (presence->network != s_config.network) {
return;
}
bool is_new = false;
bool reactivated = false;
esp_err_t err = client_registry_heartbeat(
pkt->mac, pkt->slave_id, pkt->version, pkt->used != 0, &is_new,
mac, presence->slave_id, presence->version, presence->used, &is_new,
&reactivated);
if (err != ESP_OK) {
ESP_LOGW(TAG, "client registry full");
@@ -146,20 +150,20 @@ static void handle_client_packet(const espnow_slave_packet_t *pkt) {
}
char mac_str[18];
mac_to_str(pkt->mac, mac_str, sizeof(mac_str));
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)pkt->slave_id, mac_str,
(unsigned long)pkt->version);
(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)pkt->slave_id, mac_str);
(unsigned long)presence->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) {
const alox_EspNowDiscover *discover) {
if (discover->network != s_config.network) {
return;
}
@@ -183,9 +187,10 @@ static void handle_discover(const uint8_t *sender_mac,
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);
ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)discover->network,
mac_str);
send_slave_packet(sender_mac, ESPNOW_MSG_SLAVE_INFO);
send_presence(sender_mac, alox_EspNowMessageType_ESPNOW_SLAVE_INFO);
}
static void slave_check_master_timeout(void) {
@@ -220,7 +225,7 @@ static void slave_heartbeat_task(void *param) {
continue;
}
send_slave_packet(s_master_mac, ESPNOW_MSG_HEARTBEAT);
send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_HEARTBEAT);
}
}
@@ -238,51 +243,53 @@ static void master_monitor_task(void *param) {
static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
int len) {
if (info == NULL || data == NULL || len < 3) {
if (info == NULL || data == NULL || len <= 0) {
return;
}
if (data[0] != ESPNOW_MAGIC) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
uint8_t peer_mac[CLIENT_MAC_LEN];
esp_now_proto_setup_message_decode(&msg, peer_mac);
if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) {
return;
}
switch (data[1]) {
case ESPNOW_MSG_DISCOVER:
if (!s_config.master && len >= (int)sizeof(espnow_discover_packet_t)) {
handle_discover(info->src_addr,
(const espnow_discover_packet_t *)data);
if (!s_config.master) {
if (msg.type != alox_EspNowMessageType_ESPNOW_DISCOVER ||
msg.which_payload != alox_EspNowMessage_discover_tag) {
return;
}
break;
case ESPNOW_MSG_SLAVE_INFO:
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:
break;
handle_discover(info->src_addr, &msg.payload.discover);
return;
}
const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg);
if (presence == NULL) {
return;
}
if (msg.type != alox_EspNowMessageType_ESPNOW_SLAVE_INFO &&
msg.type != alox_EspNowMessageType_ESPNOW_HEARTBEAT) {
return;
}
handle_client_presence(presence, peer_mac);
}
static void master_discover_task(void *param) {
(void)param;
espnow_discover_packet_t pkt = {
.magic = ESPNOW_MAGIC,
.type = ESPNOW_MSG_DISCOVER,
.network = s_config.network,
.reserved = 0,
};
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_DISCOVER;
msg.which_payload = alox_EspNowMessage_discover_tag;
msg.payload.discover.network = s_config.network;
ESP_LOGI(TAG, "master discover task on network %u ch %u",
(unsigned)s_config.network, (unsigned)s_wifi_channel);
while (1) {
esp_err_t err =
esp_now_send(ESPNOW_BCAST, (const uint8_t *)&pkt, sizeof(pkt));
if (err != ESP_OK) {
ESP_LOGW(TAG, "discover broadcast failed: %s", esp_err_to_name(err));
}
send_message(ESPNOW_BCAST, &msg);
vTaskDelay(pdMS_TO_TICKS(ESPNOW_DISCOVER_INTERVAL_MS));
}
}