Add LiPo battery monitoring with ESP-NOW cache and dashboard API.
Slaves report pack voltages every 30s; the master caches them for fast BATTERY_STATUS reads. goTool exposes REST/WebSocket and shows values in the dashboard, with a nanopb fix so optional lipo submessages encode. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+191
-1
@@ -1,6 +1,7 @@
|
||||
#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"
|
||||
@@ -48,6 +49,16 @@ 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);
|
||||
}
|
||||
@@ -90,6 +101,7 @@ 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;
|
||||
@@ -210,6 +222,21 @@ static esp_err_t send_find_me(const uint8_t *dest_mac, uint32_t 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) {
|
||||
@@ -470,6 +497,47 @@ static void slave_reset_join(void) {
|
||||
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,
|
||||
@@ -499,6 +567,77 @@ static void handle_slave_restart(const uint8_t *master_mac,
|
||||
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];
|
||||
@@ -670,7 +809,9 @@ static void handle_discover(const uint8_t *sender_mac,
|
||||
ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)discover->network,
|
||||
mac_str);
|
||||
|
||||
send_presence(sender_mac, alox_EspNowMessageType_ESPNOW_SLAVE_INFO);
|
||||
/* 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) {
|
||||
@@ -716,6 +857,7 @@ static void slave_accel_stream_task(void *param) {
|
||||
|
||||
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);
|
||||
@@ -726,22 +868,43 @@ static void slave_heartbeat_task(void *param) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,6 +942,12 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
|
||||
}
|
||||
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;
|
||||
@@ -838,6 +1007,17 @@ static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
|
||||
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. */
|
||||
@@ -933,6 +1113,16 @@ esp_err_t esp_now_comm_init(const app_config_t *config) {
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user