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
+2 -2
View File
@@ -315,7 +315,7 @@ Alle Master-Sends laufen über `send_message()` / `send_message_ex()`:
| Nachricht | Task / Trigger | Master-Ziel |
|-----------|----------------|-------------|
| `ESPNOW_ACCEL_SAMPLE` | `slave_accel_stream_task` 10 ms (best-effort) | `client_registry_update_accel()` |
| `ESPNOW_ACCEL_SAMPLE` | `slave_accel_stream_task` 10 ms, 1 ms slots via Discover `master_time_us` | `client_registry_update_accel()` |
| `ESPNOW_TAP_EVENT` | BMA456-IRQ → `on_bma456_tap` | `client_registry_update_tap()` |
| `ESPNOW_BATTERY_REPORT` | Heartbeat + nach Join | `client_registry_update_battery()` |
| `ESPNOW_SLAVE_INFO` / `HEARTBEAT` | `slave_tx_task` / Heartbeat-Task | `client_registry_heartbeat()` |
@@ -366,7 +366,7 @@ flowchart TB
| `espnow_mon` | Master | 1 s Timeout + Master-Battery |
| `espnow_stx` | Slave | Queue: SLAVE_INFO, Battery nach Join |
| `espnow_hb` | Slave | 1 s Heartbeat + 30 s Battery |
| `espnow_accel` | Slave | 10 ms Accel best-effort wenn Stream an |
| `espnow_accel` | Slave | 10 ms Accel in ID-Slot (1 ms) wenn Stream an |
| `bma456_poll` | Beide | 10 Hz (`bosch456.c`) |
| `vTaskLedRing` | Beide | LED-Ring-Befehle |
+2 -2
View File
@@ -116,7 +116,7 @@ Schema: `proto/esp_now_messages.proto`. Encode/decode: `esp_now_proto.c`. The ES
| `EspNowMessageType` | Direction | `oneof` payload |
|---------------------|-----------|-----------------|
| `ESPNOW_DISCOVER` | Master → broadcast `FF:FF:FF:FF:FF:FF` | `EspNowDiscover` (`network`) |
| `ESPNOW_DISCOVER` | Master → broadcast `FF:FF:FF:FF:FF:FF` | `EspNowDiscover` (`network`, `master_time_us` for accel TX slot sync) |
| `ESPNOW_SLAVE_INFO` | Slave → master | `EspNowSlavePresence` |
| `ESPNOW_HEARTBEAT` | Slave → master | `EspNowSlavePresence` (same fields) |
| `ESPNOW_SET_ACCEL_DEADZONE` | Master → slave | `EspNowAccelDeadzone` (`deadzone` LSB) |
@@ -124,7 +124,7 @@ Schema: `proto/esp_now_messages.proto`. Encode/decode: `esp_now_proto.c`. The ES
| `ESPNOW_UNICAST_TEST` | Master → slave | `EspNowUnicastTest` (`seq`) |
| `ESPNOW_FIND_ME` | Master → slave | `EspNowFindMe` (`client_id` filter) — LED locate sequence |
| `ESPNOW_RESTART` | Master → slave | `EspNowRestart` (`client_id` filter) — reboot slave |
| `ESPNOW_ACCEL_SAMPLE` | Slave → master | `EspNowAccelSample` (`slave_id`, `x`, `y`, `z` raw LSB) — ~every 10 ms; TX path via `INPUT_SEND_MODE` (default best-effort) |
| `ESPNOW_ACCEL_SAMPLE` | Slave → master | `EspNowAccelSample` (`slave_id`, `x`, `y`, `z` raw LSB) — ~every 10 ms in 1 ms slots `offset_ms=(id-1)%8` via Discover sync; TX path via `INPUT_SEND_MODE` |
| `ESPNOW_SET_INPUT_SEND_MODE` | Master → slave | `EspNowInputSendMode` (`mode` 0..3 = best_effort/fast/wait/reliable) |
| `ESPNOW_SET_TAP_NOTIFY` | Master → slave | `EspNowTapNotify` (`client_id`, `single`, `double_tap`, `triple`) — which tap kinds to forward |
| `ESPNOW_TAP_EVENT` | Slave → master | `EspNowTapEvent` (`client_id`, `kind`) — on BMA456 tap interrupt if notify enabled |
+1
View File
@@ -730,6 +730,7 @@ static void discover_task(void *param) {
while (1) {
msg.payload.discover.master_ota_pending = ota_session_busy();
msg.payload.discover.master_time_us = (uint64_t)esp_timer_get_time();
if (!ota_espnow_distribution_active()) {
esp_now_core_send_fast(ESPNOW_BCAST, &msg);
}
+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");
+10 -4
View File
@@ -74,6 +74,10 @@ typedef struct _alox_EspNowDiscover {
uint32_t network;
/* * Master is in an OTA session (UART upload or ESP-NOW distribution). */
bool master_ota_pending;
/* *
Master esp_timer_get_time() (µs) at broadcast. Slaves use this to phase
accel TX into 1 ms slots: offset_ms = (assigned_id - 1) % 8. */
uint64_t master_time_us;
} alox_EspNowDiscover;
typedef struct _alox_EspNowSlavePresence {
@@ -277,7 +281,7 @@ extern "C" {
#define alox_EspNowEchoPong_init_default {0, 0}
#define alox_EspNowFindMe_init_default {0}
#define alox_EspNowRestart_init_default {0}
#define alox_EspNowDiscover_init_default {0, 0}
#define alox_EspNowDiscover_init_default {0, 0, 0}
#define alox_EspNowSlavePresence_init_default {0, {{NULL}, NULL}, 0, 0, 0, 0}
#define alox_EspNowAssignId_init_default {0}
#define alox_EspNowClearIdBinding_init_default {0}
@@ -301,7 +305,7 @@ extern "C" {
#define alox_EspNowEchoPong_init_zero {0, 0}
#define alox_EspNowFindMe_init_zero {0}
#define alox_EspNowRestart_init_zero {0}
#define alox_EspNowDiscover_init_zero {0, 0}
#define alox_EspNowDiscover_init_zero {0, 0, 0}
#define alox_EspNowSlavePresence_init_zero {0, {{NULL}, NULL}, 0, 0, 0, 0}
#define alox_EspNowAssignId_init_zero {0}
#define alox_EspNowClearIdBinding_init_zero {0}
@@ -331,6 +335,7 @@ extern "C" {
#define alox_EspNowRestart_client_id_tag 1
#define alox_EspNowDiscover_network_tag 1
#define alox_EspNowDiscover_master_ota_pending_tag 2
#define alox_EspNowDiscover_master_time_us_tag 3
#define alox_EspNowSlavePresence_network_tag 1
#define alox_EspNowSlavePresence_mac_tag 2
#define alox_EspNowSlavePresence_version_tag 3
@@ -442,7 +447,8 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 1)
#define alox_EspNowDiscover_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, network, 1) \
X(a, STATIC, SINGULAR, BOOL, master_ota_pending, 2)
X(a, STATIC, SINGULAR, BOOL, master_ota_pending, 2) \
X(a, STATIC, SINGULAR, UINT64, master_time_us, 3)
#define alox_EspNowDiscover_CALLBACK NULL
#define alox_EspNowDiscover_DEFAULT NULL
@@ -686,7 +692,7 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
#define alox_EspNowBatteryQuery_size 6
#define alox_EspNowBatteryReport_size 22
#define alox_EspNowClearIdBinding_size 0
#define alox_EspNowDiscover_size 8
#define alox_EspNowDiscover_size 19
#define alox_EspNowEchoPing_size 22
#define alox_EspNowEchoPong_size 22
#define alox_EspNowFindMe_size 6
+5
View File
@@ -67,6 +67,11 @@ message EspNowDiscover {
uint32 network = 1;
/** Master is in an OTA session (UART upload or ESP-NOW distribution). */
bool master_ota_pending = 2;
/**
* Master esp_timer_get_time() (µs) at broadcast. Slaves use this to phase
* accel TX into 1 ms slots: offset_ms = (assigned_id - 1) % 8.
*/
uint64 master_time_us = 3;
}
message EspNowSlavePresence {