#include "esp_now_master.h" #include "client_registry.h" #include "esp_now_comm.h" #include "esp_now_core.h" #include "esp_now_proto.h" #include "board_input.h" #include "ota_espnow.h" #include "ota_uart.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/idf_additions.h" #include static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; #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 ESPNOW_BATTERY_INTERVAL_MS 30000 static const char *TAG = "[ESPNOW_M]"; 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 esp_now_core_send(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 esp_now_core_send(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 esp_now_core_send(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 esp_now_core_send(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 esp_now_core_send(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 esp_now_core_send(dest_mac, &msg); } static esp_err_t send_tap_notify(const uint8_t *dest_mac, uint32_t client_id, bool single, bool double_tap, bool triple) { alox_EspNowMessage msg = alox_EspNowMessage_init_zero; msg.type = alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY; msg.which_payload = alox_EspNowMessage_tap_notify_tag; msg.payload.tap_notify.client_id = client_id; msg.payload.tap_notify.single = single; msg.payload.tap_notify.double_tap = double_tap; msg.payload.tap_notify.triple = triple; return esp_now_core_send(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 esp_now_core_send_wait(dest_mac, &msg); } 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 esp_now_core_send_wait(dest_mac, &msg); } 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 esp_now_core_send_wait(dest_mac, &msg); } esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN], uint32_t total_size) { if (mac == NULL || !esp_now_core_is_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 || !esp_now_core_is_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 || !esp_now_core_is_master()) { return ESP_ERR_INVALID_STATE; } return send_ota_end(mac); } esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN], uint32_t client_id) { if (mac == NULL || !esp_now_core_is_master()) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; esp_now_core_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 || !esp_now_core_is_master()) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; esp_now_core_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 || !esp_now_core_is_master() || req == NULL) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; esp_now_core_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 || !esp_now_core_is_master()) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; esp_now_core_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 || !esp_now_core_is_master()) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; esp_now_core_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_tap_notify(const uint8_t mac[CLIENT_MAC_LEN], uint32_t client_id, bool single, bool double_tap, bool triple) { if (mac == NULL || !esp_now_core_is_master()) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str)); esp_err_t err = send_tap_notify(mac, client_id, single, double_tap, triple); if (err == ESP_OK) { ESP_LOGI(TAG, "unicast SET_TAP_NOTIFY to %s: single=%d double=%d triple=%d " "client_id=%lu", mac_str, single, double_tap, triple, (unsigned long)client_id); } else { ESP_LOGW(TAG, "unicast SET_TAP_NOTIFY 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 || !esp_now_core_is_master()) { return ESP_ERR_INVALID_STATE; } char mac_str[18]; esp_now_core_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 handle_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_tap_event(const uint8_t mac[CLIENT_MAC_LEN], const alox_EspNowTapEvent *event) { if (event == NULL) { return; } esp_err_t err = client_registry_update_tap(mac, event->slave_id, event->kind); if (err == ESP_ERR_NOT_FOUND) { return; } if (err != ESP_OK) { ESP_LOGW(TAG, "tap event id=%lu kind=%lu rejected from %02x:…:%02x", (unsigned long)event->slave_id, (unsigned long)event->kind, mac[0], mac[5]); } } static void handle_battery_report(const uint8_t mac[CLIENT_MAC_LEN], const alox_EspNowBatteryReport *report) { if (report == 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_client_presence(const alox_EspNowSlavePresence *presence, const uint8_t mac[CLIENT_MAC_LEN]) { if (presence->network != esp_now_core_network()) { return; } esp_now_core_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]; esp_now_core_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); } } void esp_now_master_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 (ota_espnow_distribution_active()) { if (msg.which_payload == alox_EspNowMessage_ota_status_tag) { esp_now_core_ensure_peer(info->src_addr); ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status); } return; } if (msg.which_payload == alox_EspNowMessage_ota_status_tag) { esp_now_core_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) { esp_now_core_ensure_peer(info->src_addr); handle_accel_sample(info->src_addr, &msg.payload.accel_sample); return; } if (msg.which_payload == alox_EspNowMessage_tap_event_tag) { esp_now_core_ensure_peer(info->src_addr); handle_tap_event(info->src_addr, &msg.payload.tap_event); return; } if (msg.which_payload == alox_EspNowMessage_battery_report_tag) { esp_now_core_ensure_peer(info->src_addr); handle_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, "BATTERY_REPORT type but which=%u", msg.which_payload); } const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg); if (presence != NULL) { esp_now_core_ensure_peer(info->src_addr); handle_client_presence(presence, info->src_addr); } } static void 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 = esp_now_core_network(); ESP_LOGI(TAG, "discover on network %u ch %u", (unsigned)esp_now_core_network(), (unsigned)esp_now_core_wifi_channel()); while (1) { esp_now_core_send(ESPNOW_BCAST, &msg); vTaskDelay(pdMS_TO_TICKS(ESPNOW_DISCOVER_INTERVAL_MS)); } } static void monitor_task(void *param) { (void)param; uint32_t last_local_battery_ms = 0; ESP_LOGI(TAG, "monitor (client 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 = esp_now_core_now_ms(); while (1) { vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS)); client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS); uint32_t t = esp_now_core_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; } } } esp_err_t esp_now_master_start(void) { ESP_ERROR_CHECK(esp_now_core_ensure_broadcast_peer()); if (xTaskCreate(discover_task, "espnow_disc", 4096, NULL, 4, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create discover task"); return ESP_FAIL; } if (xTaskCreate(monitor_task, "espnow_mon", 4096, NULL, 4, NULL) != pdPASS) { ESP_LOGE(TAG, "failed to create monitor task"); return ESP_FAIL; } return ESP_OK; }