Slaves push BMA456 samples at 16ms when enabled; the master caches per client and exposes ACCEL_SNAPSHOT and ACCEL_STREAM over UART. goTool adds dashboard stream controls, HTTP accel-stream routes, and an external WebSocket API with per-connection receive/interval and slave stream commands. Co-authored-by: Cursor <cursoragent@cursor.com>
869 lines
26 KiB
C
869 lines
26 KiB
C
#include "bosch456.h"
|
|
#include "client_registry.h"
|
|
#include "esp_now_comm.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 <stdio.h>
|
|
#include <string.h>
|
|
|
|
#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;
|
|
|
|
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 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_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_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;
|
|
}
|
|
|
|
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 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);
|
|
|
|
send_presence(sender_mac, alox_EspNowMessageType_ESPNOW_SLAVE_INFO);
|
|
}
|
|
|
|
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;
|
|
|
|
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) {
|
|
continue;
|
|
}
|
|
|
|
send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_HEARTBEAT);
|
|
}
|
|
}
|
|
|
|
static void master_monitor_task(void *param) {
|
|
(void)param;
|
|
|
|
ESP_LOGI(TAG, "master monitor task (timeout %u ms)",
|
|
(unsigned)ESPNOW_CLIENT_TIMEOUT_MS);
|
|
|
|
while (1) {
|
|
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
|
|
client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS);
|
|
}
|
|
}
|
|
|
|
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_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;
|
|
}
|
|
|
|
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 {
|
|
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;
|
|
}
|