Quiet pods no longer spam 100 Hz samples; mute wifi logs and throttle BLE notifies during slave distribution so the wifi task stack does not panic. Co-authored-by: Cursor <cursoragent@cursor.com>
988 lines
31 KiB
C
988 lines
31 KiB
C
#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 poll cadence (10 ms frame). TX only when sample exceeds the
|
||
* BMA456 deadzone vs last sent sample. 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;
|
||
if (cfg->enable) {
|
||
bma456_accel_stream_reset_baseline();
|
||
}
|
||
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_battery_uv(const uint8_t *master_mac,
|
||
const alox_EspNowBatteryUv *msg) {
|
||
if (!client_id_matches(msg->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, "UV protection %s from master %s (id=%lu)",
|
||
msg->enabled ? "enabled" : "disabled", mac_str,
|
||
(unsigned long)my_client_id());
|
||
|
||
if (pod_settings_set_uv_enabled(msg->enabled) != ESP_OK) {
|
||
ESP_LOGW(TAG, "UV enable save failed");
|
||
return;
|
||
}
|
||
if (!msg->enabled) {
|
||
(void)pod_settings_clear_uv_latched();
|
||
}
|
||
}
|
||
|
||
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());
|
||
|
||
led_ring_connected_anim();
|
||
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 (poll %u ms, deadzone TX, %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;
|
||
}
|
||
if (!bma456_accel_stream_take_if_changed(x, y, z)) {
|
||
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_battery_uv_tag:
|
||
if (from_joined_master(info->src_addr)) {
|
||
handle_battery_uv(info->src_addr, &msg.payload.battery_uv);
|
||
}
|
||
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;
|
||
}
|