#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 #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; }