Phase accel ESP-NOW TX into 1 ms slots from Discover sync.

Master broadcasts master_time_us; slaves schedule sends at
offset_ms=(assigned_id-1)%8 so concurrent streams collide less.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-01 22:35:47 +01:00
co-authored by Cursor
parent 1adaddd6c8
commit c3b20887bf
6 changed files with 122 additions and 19 deletions
+102 -11
View File
@@ -11,8 +11,10 @@
#include "pod_reboot.h"
#include "pod_settings.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h"
#include "freertos/semphr.h"
#include <string.h>
#ifndef POWERPOD_FW_VERSION
@@ -24,11 +26,15 @@
/** While master or slave OTA is in progress (discover may be sparse). */
#define SLAVE_MASTER_OTA_GRACE_MS 300000u
/**
* Accel stream cadence. TX path is selectable (INPUT_SEND_MODE; default
* best_effort). Must be >= one FreeRTOS tick (CONFIG_FREERTOS_HZ=100 → 10 ms);
* shorter values become 0 ticks and assert in vTaskDelayUntil.
* Accel stream cadence (10 ms frame). TX path is selectable (INPUT_SEND_MODE).
* With master Discover.master_time_us, slaves phase into 1 ms slots:
* offset_ms = (assigned_id - 1) % 8. Slot wait uses esp_timer (FreeRTOS tick
* is 10 ms — too coarse for intra-frame offsets).
*/
#define ESPNOW_ACCEL_INTERVAL_MS 10
#define ESPNOW_ACCEL_INTERVAL_US (ESPNOW_ACCEL_INTERVAL_MS * 1000ull)
#define ESPNOW_ACCEL_SLOT_COUNT 8u
#define ESPNOW_ACCEL_SLOT_US 1000ull
#define ESPNOW_BATTERY_INTERVAL_MS 30000
#define SLAVE_BATTERY_AFTER_JOIN_MS 150
@@ -50,6 +56,11 @@ static uint32_t s_assigned_id;
/** Master MAC from NVS binding (may differ from current s_master_mac). */
static uint8_t s_bound_master_mac[ESP_NOW_ETH_ALEN];
static bool s_has_binding;
/** estimated_master_us = esp_timer_get_time() + s_master_offset_us */
static int64_t s_master_offset_us;
static bool s_master_time_synced;
static SemaphoreHandle_t s_accel_slot_sem;
static esp_timer_handle_t s_accel_slot_timer;
typedef enum {
SLAVE_TX_SLAVE_INFO = 1,
@@ -59,6 +70,7 @@ typedef enum {
static QueueHandle_t s_tx_queue;
static void queue_tx(slave_tx_op_t op);
static uint32_t accel_slot_offset_us(void);
static bool from_joined_master(const uint8_t *master_mac) {
return s_joined && esp_now_core_mac_equal(master_mac, s_master_mac);
@@ -117,7 +129,9 @@ static void apply_assigned_id(uint32_t assigned_id) {
ESP_LOGW(TAG, "assigned id=%lu but NVS save failed",
(unsigned long)assigned_id);
} else {
ESP_LOGI(TAG, "assigned id=%lu (persisted)", (unsigned long)assigned_id);
ESP_LOGI(TAG, "assigned id=%lu (persisted, accel slot %lu us)",
(unsigned long)assigned_id,
(unsigned long)accel_slot_offset_us());
}
queue_tx(SLAVE_TX_BATTERY);
}
@@ -226,6 +240,8 @@ static void reset_join(void) {
s_joined = false;
s_master_ota_grace = false;
s_accel_stream_enabled = false;
s_master_time_synced = false;
s_master_offset_us = 0;
memset(s_master_mac, 0, sizeof(s_master_mac));
s_last_discover_ms = 0;
if (s_tx_queue != NULL) {
@@ -233,6 +249,66 @@ static void reset_join(void) {
}
}
static void apply_master_time_sync(uint64_t master_time_us) {
if (master_time_us == 0) {
return;
}
s_master_offset_us = (int64_t)master_time_us - esp_timer_get_time();
s_master_time_synced = true;
}
static uint32_t accel_slot_offset_us(void) {
if (s_assigned_id == 0) {
return 0;
}
return (uint32_t)(((s_assigned_id - 1u) % ESPNOW_ACCEL_SLOT_COUNT) *
ESPNOW_ACCEL_SLOT_US);
}
static void accel_slot_timer_cb(void *arg) {
(void)arg;
if (s_accel_slot_sem != NULL) {
xSemaphoreGive(s_accel_slot_sem);
}
}
/** Sleep until this slave's 1 ms slot in the shared 10 ms master frame. */
static void wait_next_accel_slot(void) {
if (!s_master_time_synced || s_accel_slot_sem == NULL ||
s_accel_slot_timer == NULL) {
const TickType_t period_ticks = pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS);
vTaskDelay(period_ticks > 0 ? period_ticks : 1);
return;
}
const int64_t period_us = (int64_t)ESPNOW_ACCEL_INTERVAL_US;
const int64_t slot_us = (int64_t)accel_slot_offset_us();
const int64_t master_now = esp_timer_get_time() + s_master_offset_us;
int64_t phase = master_now % period_us;
if (phase < 0) {
phase += period_us;
}
int64_t wait_us = slot_us - phase;
if (wait_us < 200) {
/* Too close / already past — take the next frame. */
wait_us += period_us;
}
(void)xSemaphoreTake(s_accel_slot_sem, 0);
(void)esp_timer_stop(s_accel_slot_timer);
esp_err_t err = esp_timer_start_once(s_accel_slot_timer, (uint64_t)wait_us);
if (err != ESP_OK) {
vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS) > 0
? pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS)
: 1);
return;
}
if (xSemaphoreTake(s_accel_slot_sem,
pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS * 3)) != pdTRUE) {
(void)esp_timer_stop(s_accel_slot_timer);
}
}
static void queue_tx(slave_tx_op_t op) {
if (s_tx_queue == NULL) {
return;
@@ -592,6 +668,7 @@ static void handle_discover(const uint8_t *sender_mac,
return;
}
s_master_ota_grace = discover->master_ota_pending;
apply_master_time_sync(discover->master_time_us);
if ((now - s_last_discover_ms) <= slave_master_lost_ms()) {
touch_master_presence(now);
return;
@@ -607,6 +684,7 @@ static void handle_discover(const uint8_t *sender_mac,
memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN);
s_joined = true;
s_master_ota_grace = discover->master_ota_pending;
apply_master_time_sync(discover->master_time_us);
touch_master_presence(now);
esp_now_core_ensure_peer(sender_mac);
@@ -639,15 +717,13 @@ static void check_master_timeout(void) {
static void accel_stream_task(void *param) {
(void)param;
ESP_LOGI(TAG, "accel stream task (interval %u ms, mode selectable)",
(unsigned)ESPNOW_ACCEL_INTERVAL_MS);
ESP_LOGI(TAG,
"accel stream task (interval %u ms, %u×%llu us slots, mode selectable)",
(unsigned)ESPNOW_ACCEL_INTERVAL_MS, (unsigned)ESPNOW_ACCEL_SLOT_COUNT,
(unsigned long long)ESPNOW_ACCEL_SLOT_US);
TickType_t last_wake = xTaskGetTickCount();
const TickType_t period_ticks = pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS);
/* pdMS_TO_TICKS can be 0 when ms < tick period — DelayUntil asserts. */
const TickType_t delay_ticks = period_ticks > 0 ? period_ticks : 1;
while (1) {
vTaskDelayUntil(&last_wake, delay_ticks);
wait_next_accel_slot();
if (!s_joined || s_assigned_id == 0 || !s_accel_stream_enabled ||
!bma456_is_ready()) {
continue;
@@ -834,6 +910,21 @@ esp_err_t esp_now_slave_start(void) {
reset_join();
load_id_binding();
s_accel_slot_sem = xSemaphoreCreateBinary();
if (s_accel_slot_sem == NULL) {
ESP_LOGE(TAG, "failed to create accel slot semaphore");
return ESP_ERR_NO_MEM;
}
const esp_timer_create_args_t slot_timer_args = {
.callback = &accel_slot_timer_cb,
.name = "accel_slot",
};
esp_err_t terr = esp_timer_create(&slot_timer_args, &s_accel_slot_timer);
if (terr != ESP_OK) {
ESP_LOGE(TAG, "failed to create accel slot timer: %s", esp_err_to_name(terr));
return terr;
}
s_tx_queue = xQueueCreate(4, sizeof(slave_tx_op_t));
if (s_tx_queue == NULL) {
ESP_LOGE(TAG, "failed to create tx queue");