Files
powerpods/main/esp_now_slave.c
T
skrueckenandCursor c3b20887bf 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>
2026-11-01 22:35:47 +01:00

950 lines
30 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "esp_now_slave.h"
#include "bosch456.h"
#include "cmd_led_ring.h"
#include "esp_now_comm.h"
#include "esp_now_core.h"
#include "esp_now_proto.h"
#include "board_input.h"
#include "led_ring.h"
#include "ota_espnow.h"
#include "ota_uart.h"
#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
#define POWERPOD_FW_VERSION 1u
#endif
#define ESPNOW_HEARTBEAT_INTERVAL_MS 1000
#define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5)
/** While master or slave OTA is in progress (discover may be sparse). */
#define SLAVE_MASTER_OTA_GRACE_MS 300000u
/**
* 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
static const char *TAG = "[ESPNOW_S]";
static bool s_joined;
static bool s_master_ota_grace;
static bool s_accel_stream_enabled;
static bool s_tap_notify_single;
static bool s_tap_notify_double;
static bool s_tap_notify_triple;
/** Accel/tap TX path; default matches historical best-effort gaming input. */
static uint32_t s_input_send_mode =
(uint32_t)alox_InputSendMode_INPUT_SEND_BEST_EFFORT;
static uint8_t s_master_mac[ESP_NOW_ETH_ALEN];
static uint32_t s_last_discover_ms;
/** Logical id assigned by current/last master (0 = unbound). */
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,
SLAVE_TX_BATTERY,
} slave_tx_op_t;
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);
}
static uint32_t my_client_id(void) { return s_assigned_id; }
/** True when command targets this slave (0 = broadcast / any). */
static bool client_id_matches(uint32_t client_id) {
if (client_id == 0) {
return true;
}
return s_assigned_id != 0 && client_id == s_assigned_id;
}
static uint32_t preferred_id_for_master(void) {
if (!s_has_binding || s_assigned_id == 0) {
return 0;
}
if (!s_joined || !esp_now_core_mac_equal(s_master_mac, s_bound_master_mac)) {
return 0;
}
return s_assigned_id;
}
static void load_id_binding(void) {
uint8_t mac[ESP_NOW_ETH_ALEN];
uint32_t id = 0;
s_has_binding = pod_settings_load_id_binding(mac, &id);
if (s_has_binding) {
memcpy(s_bound_master_mac, mac, ESP_NOW_ETH_ALEN);
s_assigned_id = id;
ESP_LOGI(TAG, "loaded id binding id=%lu master=%02x:…:%02x",
(unsigned long)id, mac[0], mac[5]);
} else {
memset(s_bound_master_mac, 0, sizeof(s_bound_master_mac));
s_assigned_id = 0;
}
}
static void apply_assigned_id(uint32_t assigned_id) {
if (assigned_id < POD_CLIENT_ID_MIN || assigned_id > POD_CLIENT_ID_MAX) {
ESP_LOGW(TAG, "ignore invalid assigned_id=%lu", (unsigned long)assigned_id);
return;
}
const bool same =
s_has_binding && s_assigned_id == assigned_id &&
esp_now_core_mac_equal(s_bound_master_mac, s_master_mac);
s_assigned_id = assigned_id;
memcpy(s_bound_master_mac, s_master_mac, ESP_NOW_ETH_ALEN);
s_has_binding = true;
if (same) {
return;
}
if (pod_settings_save_id_binding(s_master_mac, assigned_id) != ESP_OK) {
ESP_LOGW(TAG, "assigned id=%lu but NVS save failed",
(unsigned long)assigned_id);
} else {
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);
}
static void clear_id_binding_local(void) {
s_assigned_id = 0;
s_has_binding = false;
memset(s_bound_master_mac, 0, sizeof(s_bound_master_mac));
(void)pod_settings_clear_id_binding();
ESP_LOGI(TAG, "cleared local id binding");
}
esp_err_t esp_now_comm_clear_local_id_binding(void) {
if (esp_now_core_is_master()) {
return ESP_OK;
}
clear_id_binding_local();
return ESP_OK;
}
static void fill_presence(alox_EspNowSlavePresence *presence) {
const uint8_t *own = esp_now_core_own_mac();
presence->network = esp_now_core_network();
presence->version = POWERPOD_FW_VERSION;
presence->slave_id = preferred_id_for_master();
presence->available = true;
presence->used = false;
esp_now_proto_setup_presence_encode(presence, own);
}
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);
esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_input_message(const uint8_t *dest_mac,
const alox_EspNowMessage *msg) {
switch (s_input_send_mode) {
case (uint32_t)alox_InputSendMode_INPUT_SEND_FAST:
return esp_now_core_send_fast(dest_mac, msg);
case (uint32_t)alox_InputSendMode_INPUT_SEND_WAIT:
return esp_now_core_send_wait(dest_mac, msg);
case (uint32_t)alox_InputSendMode_INPUT_SEND_RELIABLE:
return esp_now_core_send_reliable(dest_mac, msg);
case (uint32_t)alox_InputSendMode_INPUT_SEND_BEST_EFFORT:
default:
return esp_now_core_send_best_effort(dest_mac, msg);
}
}
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_input_message(dest_mac, &msg);
}
static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id,
uint32_t kind) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_TAP_EVENT;
msg.which_payload = alox_EspNowMessage_tap_event_tag;
msg.payload.tap_event.slave_id = slave_id;
msg.payload.tap_event.kind = kind;
return send_input_message(dest_mac, &msg);
}
static esp_err_t send_battery_report(const uint8_t *dest_mac,
const alox_EspNowBatteryReport *report) {
if (report == NULL) {
return ESP_ERR_INVALID_ARG;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_BATTERY_REPORT;
msg.which_payload = alox_EspNowMessage_battery_report_tag;
msg.payload.battery_report = *report;
return esp_now_core_send(dest_mac, &msg);
}
static uint32_t slave_master_lost_ms(void) {
if (ota_uart_is_active() || s_master_ota_grace) {
return SLAVE_MASTER_OTA_GRACE_MS;
}
return SLAVE_MASTER_LOST_MS;
}
static void touch_master_presence(uint32_t now) { s_last_discover_ms = now; }
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) {
xQueueReset(s_tx_queue);
}
}
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;
}
if (xQueueSend(s_tx_queue, &op, 0) != pdTRUE) {
ESP_LOGW(TAG, "tx queue full (op=%d)", (int)op);
}
}
static void send_battery_to_master(void) {
if (!s_joined || s_assigned_id == 0) {
return;
}
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
alox_EspNowBatteryReport report = alox_EspNowBatteryReport_init_zero;
report.client_id = s_assigned_id;
report.lipo1_valid = reading.lipo1_valid;
report.lipo2_valid = reading.lipo2_valid;
report.lipo1_mv = reading.lipo1_mv;
report.lipo2_mv = reading.lipo2_mv;
esp_err_t err = send_battery_report(s_master_mac, &report);
if (err != ESP_OK) {
ESP_LOGW(TAG, "battery report send failed id=%lu: %s",
(unsigned long)report.client_id, esp_err_to_name(err));
} else {
ESP_LOGI(TAG, "battery report sent id=%lu L1=%s %lu mV L2=%s %lu mV",
(unsigned long)report.client_id,
report.lipo1_valid ? "ok" : "n/a",
(unsigned long)report.lipo1_mv,
report.lipo2_valid ? "ok" : "n/a",
(unsigned long)report.lipo2_mv);
}
}
esp_err_t esp_now_comm_send_ota_status(const uint8_t master_mac[CLIENT_MAC_LEN],
uint32_t status, uint32_t bytes_written,
uint32_t error) {
if (master_mac == NULL || esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_OTA_STATUS;
msg.which_payload = alox_EspNowMessage_ota_status_tag;
msg.payload.ota_status.status = status;
msg.payload.ota_status.bytes_written = bytes_written;
msg.payload.ota_status.error = error;
return esp_now_core_send_wait(master_mac, &msg);
}
bool esp_now_comm_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) {
return esp_now_slave_get_master_mac(mac_out);
}
bool esp_now_slave_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) {
if (mac_out == NULL || !s_joined) {
return false;
}
memcpy(mac_out, s_master_mac, CLIENT_MAC_LEN);
return true;
}
static void tx_task(void *param) {
(void)param;
slave_tx_op_t op;
ESP_LOGI(TAG, "deferred tx task ready");
while (1) {
if (xQueueReceive(s_tx_queue, &op, portMAX_DELAY) != pdTRUE) {
continue;
}
if (!s_joined) {
continue;
}
switch (op) {
case SLAVE_TX_SLAVE_INFO:
send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_SLAVE_INFO);
break;
case SLAVE_TX_BATTERY:
vTaskDelay(pdMS_TO_TICKS(SLAVE_BATTERY_AFTER_JOIN_MS));
send_battery_to_master();
break;
default:
break;
}
}
}
static void handle_unicast_test(const uint8_t *master_mac,
const alox_EspNowUnicastTest *test) {
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "UNICAST TEST OK from master %s seq=%lu (joined=%d)", mac_str,
(unsigned long)test->seq, (int)s_joined);
}
static void handle_echo_ping(const uint8_t *master_mac,
const alox_EspNowEchoPing *ping) {
if (ping == NULL || !s_joined ||
!esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "ESP-NOW PING recv from %s host_ts=%llu master_time_us=%llu",
mac_str, (unsigned long long)ping->host_timestamp_us,
(unsigned long long)ping->master_time_us);
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_ECHO_PONG;
msg.which_payload = alox_EspNowMessage_echo_pong_tag;
msg.payload.echo_pong.host_timestamp_us = ping->host_timestamp_us;
msg.payload.echo_pong.master_time_us = ping->master_time_us;
esp_err_t err = esp_now_core_send_fast(s_master_mac, &msg);
if (err != ESP_OK) {
ESP_LOGW(TAG, "ECHO PONG send failed: %s", esp_err_to_name(err));
return;
}
ESP_LOGI(TAG, "ESP-NOW PONG send to %s host_ts=%llu master_time_us=%llu",
mac_str, (unsigned long long)ping->host_timestamp_us,
(unsigned long long)ping->master_time_us);
}
static void handle_restart(const uint8_t *master_mac,
const alox_EspNowRestart *req) {
if (!client_id_matches(req->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "RESTART from master %s (id=%lu)", mac_str,
(unsigned long)my_client_id());
pod_schedule_restart();
}
static void handle_battery_query(const uint8_t *master_mac,
const alox_EspNowBatteryQuery *query) {
if (!client_id_matches(query->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
send_battery_to_master();
}
static void handle_led_ring(const uint8_t *master_mac,
const alox_EspNowLedRing *msg) {
if (!client_id_matches(msg->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
alox_LedRingProgressRequest req = alox_LedRingProgressRequest_init_zero;
req.mode = msg->mode;
req.progress = msg->progress;
req.digit = msg->digit;
req.r = msg->r;
req.g = msg->g;
req.b = msg->b;
req.intensity = msg->intensity;
req.blink_ms = msg->blink_ms;
req.blink_count = msg->blink_count;
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "LED_RING mode %lu from master %s (id=%lu)",
(unsigned long)req.mode, mac_str, (unsigned long)my_client_id());
cmd_led_ring_apply(&req);
}
static void handle_find_me(const uint8_t *master_mac, const alox_EspNowFindMe *req) {
if (!client_id_matches(req->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "FIND_ME from master %s (id=%lu)", mac_str,
(unsigned long)my_client_id());
led_ring_find_me();
}
static void handle_accel_stream(const uint8_t *master_mac,
const alox_EspNowAccelStream *cfg) {
if (!client_id_matches(cfg->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
s_accel_stream_enabled = cfg->enable;
char mac_str[18];
esp_now_core_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_client_id());
}
static const char *input_send_mode_name(uint32_t mode) {
switch (mode) {
case (uint32_t)alox_InputSendMode_INPUT_SEND_BEST_EFFORT:
return "best_effort";
case (uint32_t)alox_InputSendMode_INPUT_SEND_FAST:
return "fast";
case (uint32_t)alox_InputSendMode_INPUT_SEND_WAIT:
return "wait";
case (uint32_t)alox_InputSendMode_INPUT_SEND_RELIABLE:
return "reliable";
default:
return "unknown";
}
}
static void handle_input_send_mode(const uint8_t *master_mac,
const alox_EspNowInputSendMode *cfg) {
if (!client_id_matches(cfg->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
if (cfg->mode > (uint32_t)alox_InputSendMode_INPUT_SEND_RELIABLE) {
ESP_LOGW(TAG, "input send mode %lu invalid (id=%lu)",
(unsigned long)cfg->mode, (unsigned long)my_client_id());
return;
}
s_input_send_mode = cfg->mode;
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "input send mode %s (%lu) from master %s (id=%lu)",
input_send_mode_name(s_input_send_mode),
(unsigned long)s_input_send_mode, mac_str,
(unsigned long)my_client_id());
}
static void handle_tap_notify(const uint8_t *master_mac,
const alox_EspNowTapNotify *cfg) {
if (!client_id_matches(cfg->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
s_tap_notify_single = cfg->single;
s_tap_notify_double = cfg->double_tap;
s_tap_notify_triple = cfg->triple;
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG,
"tap notify single=%d double=%d triple=%d from master %s (id=%lu)",
cfg->single, cfg->double_tap, cfg->triple, mac_str,
(unsigned long)my_client_id());
}
static void handle_accel_deadzone(const uint8_t *master_mac,
const alox_EspNowAccelDeadzone *cfg) {
if (!client_id_matches(cfg->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG,
"accel deadzone from master %s: %lu LSB id=%lu (sensor %s)", mac_str,
(unsigned long)cfg->deadzone, (unsigned long)my_client_id(),
bma456_is_ready() ? "ok" : "not installed");
bma456_set_accel_deadzone(cfg->deadzone);
if (pod_settings_save_accel_deadzone(cfg->deadzone) != ESP_OK) {
ESP_LOGW(TAG, "deadzone %lu applied but not saved to NVS",
(unsigned long)cfg->deadzone);
}
}
static void handle_tap_config(const uint8_t *master_mac,
const alox_EspNowTapConfig *msg) {
if (!client_id_matches(msg->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
bma456_tap_config_t cfg = {
.tap_sens_thres = (uint16_t)msg->tap_sens_thres,
.max_gest_dur = (uint16_t)msg->max_gest_dur,
.tap_shock_dur = (uint16_t)msg->tap_shock_dur,
.quite_time_after_gest = (uint16_t)msg->quite_time_after_gest,
.wait_for_timeout = (uint16_t)msg->wait_for_timeout,
.axis_sel = (uint16_t)msg->axis_sel,
.enable_single = msg->enable_single,
.enable_double = msg->enable_double,
.enable_triple = msg->enable_triple,
};
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG,
"tap config from master %s: sens=%u id=%lu (sensor %s)", mac_str,
(unsigned)cfg.tap_sens_thres, (unsigned long)my_client_id(),
bma456_is_ready() ? "ok" : "not installed");
if (!bma456_tap_config_valid(&cfg)) {
ESP_LOGW(TAG, "invalid tap config from master; ignored");
return;
}
if (bma456_set_tap_config(&cfg) != ESP_OK) {
ESP_LOGW(TAG, "tap config apply failed");
return;
}
if (pod_settings_save_tap_config(&cfg) != ESP_OK) {
ESP_LOGW(TAG, "tap config applied but not saved to NVS");
}
}
static void handle_assign_id(const uint8_t *master_mac,
const alox_EspNowAssignId *msg) {
if (!from_joined_master(master_mac)) {
return;
}
apply_assigned_id(msg->assigned_id);
}
static void handle_clear_id_binding(const uint8_t *master_mac) {
if (!from_joined_master(master_mac)) {
return;
}
clear_id_binding_local();
}
static void handle_discover(const uint8_t *sender_mac,
const alox_EspNowDiscover *discover) {
if (discover->network != esp_now_core_network()) {
return;
}
uint32_t now = esp_now_core_now_ms();
if (s_joined) {
if (!esp_now_core_mac_equal(sender_mac, s_master_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;
}
if (ota_uart_is_active()) {
touch_master_presence(now);
return;
}
ESP_LOGW(TAG, "master lost, rejoining");
reset_join();
}
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);
char mac_str[18];
esp_now_core_mac_to_str(sender_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "joined network %u, master %s (preferred_id=%lu)",
(unsigned)discover->network, mac_str,
(unsigned long)preferred_id_for_master());
queue_tx(SLAVE_TX_SLAVE_INFO);
/* Battery after ASSIGN_ID so reports carry the definitive client_id. */
}
static void check_master_timeout(void) {
if (!s_joined || s_last_discover_ms == 0) {
return;
}
if (ota_uart_is_active()) {
return;
}
uint32_t now = esp_now_core_now_ms();
uint32_t limit = slave_master_lost_ms();
if ((now - s_last_discover_ms) > limit) {
ESP_LOGW(TAG, "no master discover for %u ms (limit %u), reconnecting",
(unsigned)(now - s_last_discover_ms), (unsigned)limit);
reset_join();
}
}
static void accel_stream_task(void *param) {
(void)param;
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);
while (1) {
wait_next_accel_slot();
if (!s_joined || s_assigned_id == 0 || !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_assigned_id, x, y, z);
}
}
static void on_bma456_tap(bma456_tap_kind_t kind, void *ctx) {
(void)ctx;
if (!s_joined || s_assigned_id == 0) {
return;
}
bool enabled = false;
switch (kind) {
case BMA456_TAP_SINGLE:
enabled = s_tap_notify_single;
break;
case BMA456_TAP_DOUBLE:
enabled = s_tap_notify_double;
break;
case BMA456_TAP_TRIPLE:
enabled = s_tap_notify_triple;
break;
default:
return;
}
if (!enabled) {
return;
}
(void)send_tap_event(s_master_mac, s_assigned_id, (uint32_t)kind);
}
static void heartbeat_task(void *param) {
(void)param;
uint32_t last_battery_ms = 0;
ESP_LOGI(TAG, "heartbeat task (interval %u ms)",
(unsigned)ESPNOW_HEARTBEAT_INTERVAL_MS);
while (1) {
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
check_master_timeout();
if (!s_joined) {
last_battery_ms = 0;
continue;
}
send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_HEARTBEAT);
uint32_t now = esp_now_core_now_ms();
if (last_battery_ms == 0 ||
(now - last_battery_ms) >= ESPNOW_BATTERY_INTERVAL_MS) {
send_battery_to_master();
last_battery_ms = now;
}
}
}
void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
int len) {
if (info == NULL || data == NULL || len <= 0) {
return;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed (%d bytes)", len);
return;
}
if (from_joined_master(info->src_addr)) {
esp_now_core_ensure_peer(info->src_addr);
}
if (ota_uart_is_active()) {
switch (msg.which_payload) {
case alox_EspNowMessage_discover_tag:
handle_discover(info->src_addr, &msg.payload.discover);
break;
case alox_EspNowMessage_ota_start_tag:
case alox_EspNowMessage_ota_payload_tag:
case alox_EspNowMessage_ota_end_tag:
if (!from_joined_master(info->src_addr)) {
break;
}
touch_master_presence(esp_now_core_now_ms());
if (msg.which_payload == alox_EspNowMessage_ota_start_tag) {
ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start);
} else if (msg.which_payload == alox_EspNowMessage_ota_payload_tag) {
ota_espnow_slave_on_payload(info->src_addr, &msg.payload.ota_payload);
} else {
ota_espnow_slave_on_end(info->src_addr);
}
break;
default:
break;
}
return;
}
switch (msg.which_payload) {
case alox_EspNowMessage_discover_tag:
handle_discover(info->src_addr, &msg.payload.discover);
break;
case alox_EspNowMessage_unicast_test_tag:
if (from_joined_master(info->src_addr)) {
handle_unicast_test(info->src_addr, &msg.payload.unicast_test);
}
break;
case alox_EspNowMessage_echo_ping_tag:
if (from_joined_master(info->src_addr)) {
handle_echo_ping(info->src_addr, &msg.payload.echo_ping);
}
break;
case alox_EspNowMessage_accel_deadzone_tag:
if (from_joined_master(info->src_addr)) {
handle_accel_deadzone(info->src_addr, &msg.payload.accel_deadzone);
}
break;
case alox_EspNowMessage_tap_config_tag:
if (from_joined_master(info->src_addr)) {
handle_tap_config(info->src_addr, &msg.payload.tap_config);
}
break;
case alox_EspNowMessage_accel_stream_tag:
if (from_joined_master(info->src_addr)) {
handle_accel_stream(info->src_addr, &msg.payload.accel_stream);
}
break;
case alox_EspNowMessage_tap_notify_tag:
if (from_joined_master(info->src_addr)) {
handle_tap_notify(info->src_addr, &msg.payload.tap_notify);
}
break;
case alox_EspNowMessage_input_send_mode_tag:
if (from_joined_master(info->src_addr)) {
handle_input_send_mode(info->src_addr, &msg.payload.input_send_mode);
}
break;
case alox_EspNowMessage_battery_query_tag:
if (from_joined_master(info->src_addr)) {
handle_battery_query(info->src_addr, &msg.payload.battery_query);
}
break;
case alox_EspNowMessage_led_ring_tag:
if (from_joined_master(info->src_addr)) {
handle_led_ring(info->src_addr, &msg.payload.led_ring);
}
break;
case alox_EspNowMessage_find_me_tag:
if (from_joined_master(info->src_addr)) {
handle_find_me(info->src_addr, &msg.payload.find_me);
}
break;
case alox_EspNowMessage_restart_tag:
if (from_joined_master(info->src_addr)) {
handle_restart(info->src_addr, &msg.payload.restart);
}
break;
case alox_EspNowMessage_assign_id_tag:
handle_assign_id(info->src_addr, &msg.payload.assign_id);
break;
case alox_EspNowMessage_clear_id_binding_tag:
handle_clear_id_binding(info->src_addr);
break;
case alox_EspNowMessage_ota_start_tag:
if (from_joined_master(info->src_addr)) {
ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start);
}
break;
default:
ESP_LOGW(TAG, "unhandled which=%u type=%u", msg.which_payload,
(unsigned)msg.type);
break;
}
}
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");
return ESP_ERR_NO_MEM;
}
if (xTaskCreate(tx_task, "espnow_stx", 4096, NULL, 5, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create tx task");
return ESP_FAIL;
}
if (xTaskCreate(heartbeat_task, "espnow_hb", 4096, NULL, 4, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create heartbeat task");
return ESP_FAIL;
}
if (xTaskCreate(accel_stream_task, "espnow_accel", 4096, NULL, 5, NULL) !=
pdPASS) {
ESP_LOGE(TAG, "failed to create accel stream task");
return ESP_FAIL;
}
ota_espnow_slave_init();
bma456_set_tap_handler(on_bma456_tap, NULL);
return ESP_OK;
}