#include "bosch456.h" #include "client_registry.h" #include "esp_now_comm.h" #include "board_input.h" #include "cmd_led_ring.h" #include "led_ring.h" #include "ota_espnow.h" #include "pod_reboot.h" #include "pod_settings.h" #include "esp_now_proto.h" #include "esp_err.h" #include "esp_event.h" #include "esp_log.h" #include "esp_mac.h" #include "esp_netif.h" #include "esp_now.h" #include "esp_wifi.h" #include "freertos/FreeRTOS.h" #include "freertos/idf_additions.h" #include "ota_uart.h" #include #include #ifndef POWERPOD_FW_VERSION #define POWERPOD_FW_VERSION 1u #endif #define ESPNOW_DISCOVER_INTERVAL_MS 500 #define ESPNOW_HEARTBEAT_INTERVAL_MS 1000 #define ESPNOW_HEARTBEAT_MISS_COUNT 3 #define ESPNOW_CLIENT_TIMEOUT_MS \ (ESPNOW_HEARTBEAT_INTERVAL_MS * ESPNOW_HEARTBEAT_MISS_COUNT) #define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5) #define ESPNOW_ACCEL_INTERVAL_MS 16 static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; static const char *TAG = "[ESPNOW]"; static app_config_t s_config; static uint8_t s_wifi_channel; static uint8_t s_own_mac[ESP_NOW_ETH_ALEN]; static bool s_slave_joined; static bool s_accel_stream_enabled; static uint8_t s_master_mac[ESP_NOW_ETH_ALEN]; static uint32_t s_last_discover_ms; static SemaphoreHandle_t s_send_done; static bool s_send_cb_ready; #define ESPNOW_BATTERY_INTERVAL_MS 30000 #define SLAVE_BATTERY_AFTER_JOIN_MS 150 typedef enum { SLAVE_TX_SLAVE_INFO = 1, SLAVE_TX_BATTERY, } slave_tx_op_t; static QueueHandle_t s_slave_tx_queue; static uint32_t now_ms(void) { return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); } static uint8_t network_to_channel(uint8_t network) { if (network < 1 || network > 13) { return 1; } return network; } static bool mac_equal(const uint8_t *a, const uint8_t *b) { return memcmp(a, b, ESP_NOW_ETH_ALEN) == 0; } static void mac_to_str(const uint8_t *mac, char *out, size_t out_len) { snprintf(out, out_len, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); } static esp_err_t ensure_peer(const uint8_t *mac) { if (esp_now_is_peer_exist(mac)) { return ESP_OK; } esp_now_peer_info_t peer = {0}; memcpy(peer.peer_addr, mac, ESP_NOW_ETH_ALEN); peer.channel = s_wifi_channel; peer.ifidx = WIFI_IF_STA; peer.encrypt = false; esp_err_t err = esp_now_add_peer(&peer); if (err != ESP_OK) { ESP_LOGW(TAG, "add peer failed: %s", esp_err_to_name(err)); } return err; } static esp_err_t ensure_broadcast_peer(void) { return ensure_peer(ESPNOW_BCAST); } static esp_err_t send_message_ex(const uint8_t *dest_mac, const alox_EspNowMessage *msg, bool wait_done); static void slave_send_battery_report_to_master(void); static void fill_presence(alox_EspNowSlavePresence *presence) { presence->network = s_config.network; presence->version = POWERPOD_FW_VERSION; presence->slave_id = s_own_mac[5]; presence->available = true; presence->used = false; esp_now_proto_setup_presence_encode(presence, s_own_mac); } static void espnow_send_done_cb(const esp_now_send_info_t *tx_info, esp_now_send_status_t status) { (void)tx_info; (void)status; if (s_send_done != NULL) { xSemaphoreGive(s_send_done); } } static esp_err_t send_message(const uint8_t *dest_mac, const alox_EspNowMessage *msg) { return send_message_ex(dest_mac, msg, false); } 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_message(dest_mac, &msg); } static esp_err_t send_message_ex(const uint8_t *dest_mac, const alox_EspNowMessage *msg, bool wait_done) { uint8_t buf[ESPNOW_PB_MAX_SIZE]; size_t len = 0; esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len); if (err != ESP_OK) { ESP_LOGW(TAG, "encode failed"); return err; } if (len > ESP_NOW_MAX_DATA_LEN) { ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len, (unsigned)ESP_NOW_MAX_DATA_LEN); return ESP_ERR_INVALID_SIZE; } if (ensure_peer(dest_mac) != ESP_OK) { return ESP_FAIL; } if (wait_done && s_send_cb_ready && s_send_done != NULL) { xSemaphoreTake(s_send_done, 0); } err = esp_now_send(dest_mac, buf, len); if (err != ESP_OK) { ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type, esp_err_to_name(err)); return err; } if (wait_done && s_send_cb_ready && s_send_done != NULL) { if (xSemaphoreTake(s_send_done, pdMS_TO_TICKS(50)) != pdTRUE) { ESP_LOGW(TAG, "send type=%u done timeout", (unsigned)msg->type); } } return err; } static esp_err_t send_accel_stream(const uint8_t *dest_mac, uint32_t client_id, bool enable) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM; msg.which_payload = alox_EspNowMessage_accel_stream_tag; msg.payload.accel_stream.enable = enable; msg.payload.accel_stream.client_id = client_id; return send_message(dest_mac, &msg); } static esp_err_t send_accel_deadzone(const uint8_t *dest_mac, uint32_t client_id, uint32_t deadzone) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_DEADZONE; msg.which_payload = alox_EspNowMessage_accel_deadzone_tag; msg.payload.accel_deadzone.deadzone = deadzone; msg.payload.accel_deadzone.client_id = client_id; return send_message(dest_mac, &msg); } static esp_err_t send_unicast_test(const uint8_t *dest_mac, uint32_t seq) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_UNICAST_TEST; msg.which_payload = alox_EspNowMessage_unicast_test_tag; msg.payload.unicast_test.seq = seq; return send_message(dest_mac, &msg); } static esp_err_t send_find_me(const uint8_t *dest_mac, uint32_t client_id) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_FIND_ME; msg.which_payload = alox_EspNowMessage_find_me_tag; msg.payload.find_me.client_id = client_id; return send_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 send_message(dest_mac, &msg); } static esp_err_t send_led_ring(const uint8_t *dest_mac, uint32_t client_id, const alox_LedRingProgressRequest *req) { if (req == NULL) { return ESP_ERR_INVALID_ARG; } alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_LED_RING; msg.which_payload = alox_EspNowMessage_led_ring_tag; msg.payload.led_ring.client_id = client_id; msg.payload.led_ring.mode = req->mode; msg.payload.led_ring.progress = req->progress; msg.payload.led_ring.digit = req->digit; msg.payload.led_ring.r = req->r; msg.payload.led_ring.g = req->g; msg.payload.led_ring.b = req->b; msg.payload.led_ring.intensity = req->intensity; msg.payload.led_ring.blink_ms = req->blink_ms; msg.payload.led_ring.blink_count = req->blink_count; return send_message(dest_mac, &msg); } static esp_err_t send_restart(const uint8_t *dest_mac, uint32_t client_id) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_RESTART; msg.which_payload = alox_EspNowMessage_restart_tag; msg.payload.restart.client_id = client_id; return send_message(dest_mac, &msg); } static esp_err_t send_ota_start(const uint8_t *dest_mac, uint32_t total_size) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_OTA_START; msg.which_payload = alox_EspNowMessage_ota_start_tag; msg.payload.ota_start.total_size = total_size; return send_message_ex(dest_mac, &msg, true); } static esp_err_t send_ota_payload(const uint8_t *dest_mac, uint32_t seq, const uint8_t *data, size_t len) { if (data == NULL || len == 0 || len > OTA_UART_HOST_CHUNK_SIZE) { return ESP_ERR_INVALID_ARG; } alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_OTA_PAYLOAD; msg.which_payload = alox_EspNowMessage_ota_payload_tag; msg.payload.ota_payload.seq = seq; msg.payload.ota_payload.data.size = len; memcpy(msg.payload.ota_payload.data.bytes, data, len); return send_message_ex(dest_mac, &msg, true); } static esp_err_t send_ota_end(const uint8_t *dest_mac) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_OTA_END; msg.which_payload = alox_EspNowMessage_ota_end_tag; return send_message_ex(dest_mac, &msg, true); } static esp_err_t send_ota_status(const uint8_t *dest_mac, uint32_t status, uint32_t bytes_written, uint32_t error) { 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 send_message_ex(dest_mac, &msg, true); } esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN], uint32_t total_size) { if (mac == NULL || !s_config.master) { return ESP_ERR_INVALID_STATE; } return send_ota_start(mac, total_size); } esp_err_t esp_now_comm_send_ota_payload(const uint8_t mac[CLIENT_MAC_LEN], uint32_t seq, const uint8_t *data, size_t len) { if (mac == NULL || !s_config.master) { return ESP_ERR_INVALID_STATE; } return send_ota_payload(mac, seq, data, len); } esp_err_t esp_now_comm_send_ota_end(const uint8_t mac[CLIENT_MAC_LEN]) { if (mac == NULL || !s_config.master) { return ESP_ERR_INVALID_STATE; } return send_ota_end(mac); } 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 || s_config.master) { return ESP_ERR_INVALID_STATE; } return send_ota_status(master_mac, status, bytes_written, error); } bool esp_now_comm_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) { if (mac_out == NULL || s_config.master || !s_slave_joined) { return false; } memcpy(mac_out, s_master_mac, CLIENT_MAC_LEN); return true; } esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN], uint32_t client_id) { if (mac == NULL || !s_config.master) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; mac_to_str(mac, mac_str, sizeof(mac_str)); esp_err_t err = send_restart(mac, client_id); if (err == ESP_OK) { ESP_LOGI(TAG, "unicast RESTART to %s client_id=%lu", mac_str, (unsigned long)client_id); } else { ESP_LOGW(TAG, "unicast RESTART to %s failed: %s", mac_str, esp_err_to_name(err)); } return err; } esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN], uint32_t client_id) { if (mac == NULL || !s_config.master) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; mac_to_str(mac, mac_str, sizeof(mac_str)); esp_err_t err = send_find_me(mac, client_id); if (err == ESP_OK) { ESP_LOGI(TAG, "unicast FIND_ME to %s client_id=%lu", mac_str, (unsigned long)client_id); } else { ESP_LOGW(TAG, "unicast FIND_ME to %s failed: %s", mac_str, esp_err_to_name(err)); } return err; } esp_err_t esp_now_comm_send_led_ring(const uint8_t mac[CLIENT_MAC_LEN], uint32_t client_id, const alox_LedRingProgressRequest *req) { if (mac == NULL || !s_config.master || req == NULL) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; mac_to_str(mac, mac_str, sizeof(mac_str)); esp_err_t err = send_led_ring(mac, client_id, req); if (err == ESP_OK) { ESP_LOGI(TAG, "unicast LED_RING mode %lu to %s client_id=%lu", (unsigned long)req->mode, mac_str, (unsigned long)client_id); } else { ESP_LOGW(TAG, "unicast LED_RING to %s failed: %s", mac_str, esp_err_to_name(err)); } return err; } esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN], uint32_t seq) { if (mac == NULL || !s_config.master) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; mac_to_str(mac, mac_str, sizeof(mac_str)); esp_err_t err = send_unicast_test(mac, seq); if (err == ESP_OK) { ESP_LOGI(TAG, "unicast TEST to %s seq=%lu", mac_str, (unsigned long)seq); } else { ESP_LOGW(TAG, "unicast TEST to %s failed: %s", mac_str, esp_err_to_name(err)); } return err; } esp_err_t esp_now_comm_send_accel_stream(const uint8_t mac[CLIENT_MAC_LEN], uint32_t client_id, bool enable) { if (mac == NULL || !s_config.master) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; mac_to_str(mac, mac_str, sizeof(mac_str)); esp_err_t err = send_accel_stream(mac, client_id, enable); if (err == ESP_OK) { ESP_LOGI(TAG, "unicast SET_ACCEL_STREAM to %s: %s client_id=%lu", mac_str, enable ? "on" : "off", (unsigned long)client_id); } else { ESP_LOGW(TAG, "unicast SET_ACCEL_STREAM to %s failed: %s", mac_str, esp_err_to_name(err)); } return err; } esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN], uint32_t client_id, uint32_t deadzone) { if (mac == NULL || !s_config.master) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; mac_to_str(mac, mac_str, sizeof(mac_str)); esp_err_t err = send_accel_deadzone(mac, client_id, deadzone); if (err == ESP_OK) { ESP_LOGI(TAG, "unicast SET_ACCEL_DEADZONE to %s: deadzone=%lu client_id=%lu", mac_str, (unsigned long)deadzone, (unsigned long)client_id); } else { ESP_LOGW(TAG, "unicast SET_ACCEL_DEADZONE to %s failed: %s", mac_str, esp_err_to_name(err)); } return err; } 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); send_message(dest_mac, &msg); } static void slave_reset_join(void) { s_slave_joined = false; s_accel_stream_enabled = false; memset(s_master_mac, 0, sizeof(s_master_mac)); s_last_discover_ms = 0; if (s_slave_tx_queue != NULL) { xQueueReset(s_slave_tx_queue); } } static void slave_queue_tx(slave_tx_op_t op) { if (s_slave_tx_queue == NULL) { return; } if (xQueueSend(s_slave_tx_queue, &op, 0) != pdTRUE) { ESP_LOGW(TAG, "slave tx queue full (op=%d)", (int)op); } } static void slave_tx_task(void *param) { (void)param; slave_tx_op_t op; ESP_LOGI(TAG, "slave tx task ready"); while (1) { if (xQueueReceive(s_slave_tx_queue, &op, portMAX_DELAY) != pdTRUE) { continue; } if (!s_slave_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)); slave_send_battery_report_to_master(); break; default: break; } } } static void handle_slave_unicast_test(const uint8_t *master_mac, const alox_EspNowUnicastTest *test) { char mac_str[18]; 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_slave_joined); } static void handle_slave_restart(const uint8_t *master_mac, const alox_EspNowRestart *req) { uint32_t my_id = s_own_mac[5]; if (req->client_id != 0 && req->client_id != my_id) { return; } if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { return; } char mac_str[18]; mac_to_str(master_mac, mac_str, sizeof(mac_str)); ESP_LOGI(TAG, "RESTART from master %s (id=%lu)", mac_str, (unsigned long)my_id); pod_schedule_restart(); } static void slave_send_battery_report_to_master(void) { if (!s_slave_joined) { return; } board_lipo_reading_t reading; board_input_read_lipo(&reading); alox_EspNowBatteryReport report = alox_EspNowBatteryReport_init_zero; report.client_id = s_own_mac[5]; 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); } } static void handle_slave_battery_query(const uint8_t *master_mac, const alox_EspNowBatteryQuery *query) { uint32_t my_id = s_own_mac[5]; if (query->client_id != 0 && query->client_id != my_id) { return; } if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { return; } slave_send_battery_report_to_master(); } static void handle_master_battery_report(const uint8_t *mac, const alox_EspNowBatteryReport *report) { if (report == NULL || mac == NULL) { return; } esp_err_t err = client_registry_update_battery( mac, report->client_id, report->lipo1_valid, report->lipo1_mv, report->lipo2_valid, report->lipo2_mv); if (err == ESP_ERR_NOT_FOUND) { ESP_LOGW(TAG, "battery report from unregistered slave id=%lu", (unsigned long)report->client_id); return; } if (err != ESP_OK) { ESP_LOGW(TAG, "battery report id=%lu rejected: %s", (unsigned long)report->client_id, esp_err_to_name(err)); return; } ESP_LOGI(TAG, "battery cached 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); } static void handle_slave_led_ring(const uint8_t *master_mac, const alox_EspNowLedRing *msg) { uint32_t my_id = s_own_mac[5]; if (msg->client_id != 0 && msg->client_id != my_id) { return; } if (s_slave_joined && !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]; 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_id); cmd_led_ring_apply(&req); } static void handle_slave_find_me(const uint8_t *master_mac, const alox_EspNowFindMe *req) { uint32_t my_id = s_own_mac[5]; if (req->client_id != 0 && req->client_id != my_id) { return; } if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { return; } char mac_str[18]; 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_id); led_ring_find_me(); } static void handle_slave_accel_stream(const uint8_t *master_mac, const alox_EspNowAccelStream *cfg) { uint32_t my_id = s_own_mac[5]; if (cfg->client_id != 0 && cfg->client_id != my_id) { return; } if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { return; } s_accel_stream_enabled = cfg->enable; char mac_str[18]; 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_id); } static void handle_slave_accel_deadzone(const uint8_t *master_mac, const alox_EspNowAccelDeadzone *cfg) { uint32_t my_id = s_own_mac[5]; if (cfg->client_id != 0 && cfg->client_id != my_id) { return; } if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) { return; } char mac_str[18]; 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_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, "slave deadzone %lu applied but not saved to NVS", (unsigned long)cfg->deadzone); } } static void handle_master_accel_sample(const uint8_t mac[CLIENT_MAC_LEN], const alox_EspNowAccelSample *sample) { if (sample == NULL) { return; } esp_err_t err = client_registry_update_accel( mac, sample->slave_id, (int16_t)sample->x, (int16_t)sample->y, (int16_t)sample->z); if (err == ESP_ERR_NOT_FOUND) { return; } if (err != ESP_OK) { ESP_LOGW(TAG, "accel sample id mismatch from %02x:…:%02x", mac[0], mac[5]); } } static void handle_client_presence(const alox_EspNowSlavePresence *presence, const uint8_t mac[CLIENT_MAC_LEN]) { if (presence->network != s_config.network) { return; } ensure_peer(mac); bool is_new = false; bool reactivated = false; esp_err_t err = client_registry_heartbeat( mac, presence->slave_id, presence->version, presence->used, &is_new, &reactivated); if (err != ESP_OK) { ESP_LOGW(TAG, "client registry full"); return; } char mac_str[18]; mac_to_str(mac, mac_str, sizeof(mac_str)); if (is_new) { ESP_LOGI(TAG, "client registered id=%lu mac=%s ver=%lu", (unsigned long)presence->slave_id, mac_str, (unsigned long)presence->version); } else if (reactivated) { ESP_LOGI(TAG, "client reconnected id=%lu mac=%s", (unsigned long)presence->slave_id, mac_str); } } static void handle_discover(const uint8_t *sender_mac, const alox_EspNowDiscover *discover) { if (discover->network != s_config.network) { return; } uint32_t now = now_ms(); if (s_slave_joined) { if (!mac_equal(sender_mac, s_master_mac)) { return; } if ((now - s_last_discover_ms) <= SLAVE_MASTER_LOST_MS) { s_last_discover_ms = now; return; } ESP_LOGW(TAG, "master lost, rejoining"); slave_reset_join(); } memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN); s_slave_joined = true; s_last_discover_ms = now; ensure_peer(sender_mac); char mac_str[18]; mac_to_str(sender_mac, mac_str, sizeof(mac_str)); ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)discover->network, mac_str); /* Do not esp_now_send from recv callback — defer to slave_tx_task. */ slave_queue_tx(SLAVE_TX_SLAVE_INFO); slave_queue_tx(SLAVE_TX_BATTERY); } static void slave_check_master_timeout(void) { if (!s_slave_joined) { return; } uint32_t now = now_ms(); if (s_last_discover_ms == 0) { return; } if ((now - s_last_discover_ms) > SLAVE_MASTER_LOST_MS) { ESP_LOGW(TAG, "no master discover for %u ms, reconnecting", (unsigned)(now - s_last_discover_ms)); slave_reset_join(); } } static void slave_accel_stream_task(void *param) { (void)param; ESP_LOGI(TAG, "slave accel stream task (interval %u ms)", (unsigned)ESPNOW_ACCEL_INTERVAL_MS); while (1) { vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS)); if (!s_slave_joined || !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_own_mac[5], x, y, z); } } static void slave_heartbeat_task(void *param) { (void)param; uint32_t last_battery_ms = 0; ESP_LOGI(TAG, "slave heartbeat task (interval %u ms)", (unsigned)ESPNOW_HEARTBEAT_INTERVAL_MS); while (1) { vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS)); slave_check_master_timeout(); if (!s_slave_joined) { last_battery_ms = 0; continue; } send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_HEARTBEAT); uint32_t now = now_ms(); if (last_battery_ms == 0 || (now - last_battery_ms) >= ESPNOW_BATTERY_INTERVAL_MS) { slave_send_battery_report_to_master(); last_battery_ms = now; } } } static void master_monitor_task(void *param) { (void)param; uint32_t last_local_battery_ms = 0; ESP_LOGI(TAG, "master monitor task (timeout %u ms)", (unsigned)ESPNOW_CLIENT_TIMEOUT_MS); board_lipo_reading_t reading; board_input_read_lipo(&reading); client_registry_set_master_battery(&reading); last_local_battery_ms = now_ms(); while (1) { vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS)); client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS); uint32_t t = now_ms(); if (t - last_local_battery_ms >= ESPNOW_BATTERY_INTERVAL_MS) { board_input_read_lipo(&reading); client_registry_set_master_battery(&reading); last_local_battery_ms = t; } } } static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data, int len) { if (info == NULL || data == NULL || len <= 0) { return; } if (!s_config.master) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) { ESP_LOGW(TAG, "slave: ESP-NOW decode failed (%d bytes)", len); return; } if (s_slave_joined && mac_equal(info->src_addr, s_master_mac)) { ensure_peer(info->src_addr); } switch (msg.which_payload) { case alox_EspNowMessage_discover_tag: handle_discover(info->src_addr, &msg.payload.discover); break; case alox_EspNowMessage_unicast_test_tag: handle_slave_unicast_test(info->src_addr, &msg.payload.unicast_test); break; case alox_EspNowMessage_accel_deadzone_tag: handle_slave_accel_deadzone(info->src_addr, &msg.payload.accel_deadzone); break; case alox_EspNowMessage_accel_stream_tag: if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { break; } handle_slave_accel_stream(info->src_addr, &msg.payload.accel_stream); break; case alox_EspNowMessage_battery_query_tag: if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { break; } handle_slave_battery_query(info->src_addr, &msg.payload.battery_query); break; case alox_EspNowMessage_led_ring_tag: if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { break; } handle_slave_led_ring(info->src_addr, &msg.payload.led_ring); break; case alox_EspNowMessage_find_me_tag: if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { break; } handle_slave_find_me(info->src_addr, &msg.payload.find_me); break; case alox_EspNowMessage_restart_tag: if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { break; } handle_slave_restart(info->src_addr, &msg.payload.restart); break; case alox_EspNowMessage_ota_start_tag: case alox_EspNowMessage_ota_payload_tag: case alox_EspNowMessage_ota_end_tag: if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) { break; } 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: ESP_LOGW(TAG, "slave: unhandled ESP-NOW which=%u type=%u", msg.which_payload, (unsigned)msg.type); break; } return; } alox_EspNowMessage msg = alox_EspNowMessage_init_zero; if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) { ESP_LOGW(TAG, "master: ESP-NOW decode failed (%d bytes)", len); return; } if (msg.which_payload == alox_EspNowMessage_ota_status_tag) { ensure_peer(info->src_addr); ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status); return; } if (msg.which_payload == alox_EspNowMessage_accel_sample_tag) { ensure_peer(info->src_addr); handle_master_accel_sample(info->src_addr, &msg.payload.accel_sample); return; } if (msg.which_payload == alox_EspNowMessage_battery_report_tag) { ensure_peer(info->src_addr); handle_master_battery_report(info->src_addr, &msg.payload.battery_report); return; } if (msg.type == alox_EspNowMessageType_ESPNOW_BATTERY_REPORT && msg.which_payload != alox_EspNowMessage_battery_report_tag) { ESP_LOGW(TAG, "master: BATTERY_REPORT type but which=%u", msg.which_payload); } const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg); if (presence != NULL) { /* Registry key is the ESP-NOW sender MAC, not the optional protobuf mac field. */ ensure_peer(info->src_addr); handle_client_presence(presence, info->src_addr); } } static void master_discover_task(void *param) { (void)param; alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_DISCOVER; msg.which_payload = alox_EspNowMessage_discover_tag; msg.payload.discover.network = s_config.network; ESP_LOGI(TAG, "master discover task on network %u ch %u", (unsigned)s_config.network, (unsigned)s_wifi_channel); while (1) { send_message(ESPNOW_BCAST, &msg); vTaskDelay(pdMS_TO_TICKS(ESPNOW_DISCOVER_INTERVAL_MS)); } } static esp_err_t init_wifi_stack(uint8_t channel) { ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); wifi_config_t wifi_config = {0}; wifi_config.sta.channel = channel; wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); ESP_ERROR_CHECK(esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE)); return ESP_OK; } esp_err_t esp_now_comm_init(const app_config_t *config) { if (config == NULL) { return ESP_ERR_INVALID_ARG; } memset(&s_config, 0, sizeof(s_config)); memcpy(&s_config, config, sizeof(s_config)); client_registry_init(); slave_reset_join(); s_wifi_channel = network_to_channel(config->network); ESP_ERROR_CHECK(esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA)); char mac_str[18]; mac_to_str(s_own_mac, mac_str, sizeof(mac_str)); ESP_LOGI(TAG, "role=%s network=%u channel=%u mac=%s", config->master ? "master" : "slave", (unsigned)config->network, (unsigned)s_wifi_channel, mac_str); esp_err_t err = init_wifi_stack(s_wifi_channel); if (err != ESP_OK) { ESP_LOGE(TAG, "wifi init failed: %s", esp_err_to_name(err)); return err; } ESP_ERROR_CHECK(esp_now_init()); ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_cb)); s_send_done = xSemaphoreCreateBinary(); if (s_send_done != NULL && esp_now_register_send_cb(espnow_send_done_cb) == ESP_OK) { s_send_cb_ready = true; } else { ESP_LOGW(TAG, "ESP-NOW send-done callback unavailable (OTA may drop packets)"); } if (config->master) { ESP_ERROR_CHECK(ensure_broadcast_peer()); if (xTaskCreate(master_discover_task, "espnow_disc", 4096, NULL, 4, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create discover task"); return ESP_FAIL; } if (xTaskCreate(master_monitor_task, "espnow_mon", 4096, NULL, 4, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create monitor task"); return ESP_FAIL; } } else { s_slave_tx_queue = xQueueCreate(4, sizeof(slave_tx_op_t)); if (s_slave_tx_queue == NULL) { ESP_LOGE(TAG, "failed to create slave tx queue"); return ESP_ERR_NO_MEM; } if (xTaskCreate(slave_tx_task, "espnow_stx", 4096, NULL, 5, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create slave tx task"); return ESP_FAIL; } if (xTaskCreate(slave_heartbeat_task, "espnow_hb", 4096, NULL, 4, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create heartbeat task"); return ESP_FAIL; } if (xTaskCreate(slave_accel_stream_task, "espnow_accel", 4096, NULL, 5, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create accel stream task"); return ESP_FAIL; } } return ESP_OK; }