Stream slave accel via ESP-NOW with master snapshot cache.

Slaves push BMA456 samples at 16ms when enabled; the master caches per
client and exposes ACCEL_SNAPSHOT and ACCEL_STREAM over UART. goTool adds
dashboard stream controls, HTTP accel-stream routes, and an external
WebSocket API with per-connection receive/interval and slave stream commands.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 19:11:36 +02:00
co-authored by Cursor
parent ba20544762
commit 47c75110c9
35 changed files with 2409 additions and 300 deletions
+123
View File
@@ -29,6 +29,7 @@
#define ESPNOW_CLIENT_TIMEOUT_MS \
(ESPNOW_HEARTBEAT_INTERVAL_MS * ESPNOW_HEARTBEAT_MISS_COUNT)
#define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5)
#define ESPNOW_ACCEL_INTERVAL_MS 16
static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff,
0xff, 0xff, 0xff};
@@ -39,6 +40,7 @@ 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 bool s_accel_stream_enabled;
static uint8_t s_master_mac[ESP_NOW_ETH_ALEN];
static uint32_t s_last_discover_ms;
@@ -111,6 +113,18 @@ static esp_err_t send_message(const uint8_t *dest_mac,
return send_message_ex(dest_mac, msg, false);
}
static esp_err_t send_accel_sample(const uint8_t *dest_mac, uint32_t slave_id,
int16_t x, int16_t y, int16_t z) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_ACCEL_SAMPLE;
msg.which_payload = alox_EspNowMessage_accel_sample_tag;
msg.payload.accel_sample.slave_id = slave_id;
msg.payload.accel_sample.x = x;
msg.payload.accel_sample.y = y;
msg.payload.accel_sample.z = z;
return send_message(dest_mac, &msg);
}
static esp_err_t send_message_ex(const uint8_t *dest_mac,
const alox_EspNowMessage *msg, bool wait_done) {
uint8_t buf[ESPNOW_PB_MAX_SIZE];
@@ -151,6 +165,18 @@ static esp_err_t send_message_ex(const uint8_t *dest_mac,
return err;
}
static esp_err_t send_accel_stream(const uint8_t *dest_mac, uint32_t client_id,
bool enable) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM;
msg.which_payload = alox_EspNowMessage_accel_stream_tag;
msg.payload.accel_stream.enable = enable;
msg.payload.accel_stream.client_id = client_id;
return send_message(dest_mac, &msg);
}
static esp_err_t send_accel_deadzone(const uint8_t *dest_mac, uint32_t client_id,
uint32_t deadzone) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
@@ -338,6 +364,25 @@ esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
return err;
}
esp_err_t esp_now_comm_send_accel_stream(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id, bool enable) {
if (mac == NULL || !s_config.master) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err = send_accel_stream(mac, client_id, enable);
if (err == ESP_OK) {
ESP_LOGI(TAG, "unicast SET_ACCEL_STREAM to %s: %s client_id=%lu", mac_str,
enable ? "on" : "off", (unsigned long)client_id);
} else {
ESP_LOGW(TAG, "unicast SET_ACCEL_STREAM to %s failed: %s", mac_str,
esp_err_to_name(err));
}
return err;
}
esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id, uint32_t deadzone) {
if (mac == NULL || !s_config.master) {
@@ -377,6 +422,7 @@ static void send_presence(const uint8_t *dest_mac,
static void slave_reset_join(void) {
s_slave_joined = false;
s_accel_stream_enabled = false;
memset(s_master_mac, 0, sizeof(s_master_mac));
s_last_discover_ms = 0;
}
@@ -426,6 +472,25 @@ static void handle_slave_find_me(const uint8_t *master_mac,
led_ring_find_me();
}
static void handle_slave_accel_stream(const uint8_t *master_mac,
const alox_EspNowAccelStream *cfg) {
uint32_t my_id = s_own_mac[5];
if (cfg->client_id != 0 && cfg->client_id != my_id) {
return;
}
if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) {
return;
}
s_accel_stream_enabled = cfg->enable;
char mac_str[18];
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);
}
static void handle_slave_accel_deadzone(const uint8_t *master_mac,
const alox_EspNowAccelDeadzone *cfg) {
uint32_t my_id = s_own_mac[5];
@@ -453,6 +518,23 @@ static void handle_slave_accel_deadzone(const uint8_t *master_mac,
}
}
static void handle_master_accel_sample(const uint8_t mac[CLIENT_MAC_LEN],
const alox_EspNowAccelSample *sample) {
if (sample == NULL) {
return;
}
esp_err_t err = client_registry_update_accel(
mac, sample->slave_id, (int16_t)sample->x, (int16_t)sample->y,
(int16_t)sample->z);
if (err == ESP_ERR_NOT_FOUND) {
return;
}
if (err != ESP_OK) {
ESP_LOGW(TAG, "accel sample id mismatch from %02x:…:%02x", mac[0], mac[5]);
}
}
static void handle_client_presence(const alox_EspNowSlavePresence *presence,
const uint8_t mac[CLIENT_MAC_LEN]) {
if (presence->network != s_config.network) {
@@ -533,6 +615,30 @@ static void slave_check_master_timeout(void) {
}
}
static void slave_accel_stream_task(void *param) {
(void)param;
ESP_LOGI(TAG, "slave accel stream task (interval %u ms)",
(unsigned)ESPNOW_ACCEL_INTERVAL_MS);
while (1) {
vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS));
if (!s_slave_joined || !s_accel_stream_enabled || !bma456_is_ready()) {
continue;
}
int16_t x = 0;
int16_t y = 0;
int16_t z = 0;
if (bma456_read_accel(&x, &y, &z) != ESP_OK) {
continue;
}
(void)send_accel_sample(s_master_mac, s_own_mac[5], x, y, z);
}
}
static void slave_heartbeat_task(void *param) {
(void)param;
@@ -592,6 +698,12 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
case alox_EspNowMessage_accel_deadzone_tag:
handle_slave_accel_deadzone(info->src_addr, &msg.payload.accel_deadzone);
break;
case alox_EspNowMessage_accel_stream_tag:
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) {
break;
}
handle_slave_accel_stream(info->src_addr, &msg.payload.accel_stream);
break;
case alox_EspNowMessage_find_me_tag:
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) {
break;
@@ -639,6 +751,12 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
return;
}
if (msg.which_payload == alox_EspNowMessage_accel_sample_tag) {
ensure_peer(info->src_addr);
handle_master_accel_sample(info->src_addr, &msg.payload.accel_sample);
return;
}
const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg);
if (presence != NULL) {
/* Registry key is the ESP-NOW sender MAC, not the optional protobuf mac field. */
@@ -739,6 +857,11 @@ esp_err_t esp_now_comm_init(const app_config_t *config) {
ESP_LOGE(TAG, "failed to create heartbeat task");
return ESP_FAIL;
}
if (xTaskCreate(slave_accel_stream_task, "espnow_accel", 4096, NULL, 5,
NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create accel stream task");
return ESP_FAIL;
}
}
return ESP_OK;