powerpods/main/cmd/cmd_battery.c
simon 3cb0b5bbe9 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>
2026-05-29 20:14:28 +02:00

120 lines
3.7 KiB
C

#include "cmd_battery.h"
#include "board_input.h"
#include "client_registry.h"
#include "esp_log.h"
#include "uart_cmd.h"
static const char *TAG = "[BATTERY]";
static void fill_lipo(alox_LipoReading *dst, bool *has_dst, bool valid,
uint32_t mv) {
if (dst == NULL || has_dst == NULL) {
return;
}
*has_dst = true;
dst->valid = valid;
dst->voltage_mv = valid ? mv : 0;
}
static bool append_battery_sample(alox_BatteryStatusResponse *resp,
uint32_t client_id, bool lipo1_valid,
uint32_t lipo1_mv, bool lipo2_valid,
uint32_t lipo2_mv, uint32_t age_ms) {
if (resp->samples_count >=
sizeof(resp->samples) / sizeof(resp->samples[0])) {
return false;
}
alox_BatterySample *sample = &resp->samples[resp->samples_count++];
sample->client_id = client_id;
fill_lipo(&sample->lipo1, &sample->has_lipo1, lipo1_valid, lipo1_mv);
fill_lipo(&sample->lipo2, &sample->has_lipo2, lipo2_valid, lipo2_mv);
sample->age_ms = age_ms;
return lipo1_valid || lipo2_valid;
}
static bool append_master_cached(alox_BatteryStatusResponse *resp) {
board_lipo_reading_t reading;
uint32_t age_ms = 0;
if (!client_registry_get_master_battery(&reading, &age_ms)) {
board_input_read_lipo(&reading);
client_registry_set_master_battery(&reading);
age_ms = 0;
}
return append_battery_sample(resp, 0, reading.lipo1_valid, reading.lipo1_mv,
reading.lipo2_valid, reading.lipo2_mv, age_ms);
}
static bool append_slave_cached(alox_BatteryStatusResponse *resp,
const client_info_t *client) {
if (client == NULL) {
return false;
}
if (client->battery_updated_at == 0) {
return false;
}
return append_battery_sample(
resp, client->id, client->lipo1_valid, client->lipo1_mv,
client->lipo2_valid, client->lipo2_mv,
client_registry_ms_since(client->battery_updated_at));
}
static void handle_battery_status(const uint8_t *data, size_t len) {
alox_BatteryStatusRequest req = alox_BatteryStatusRequest_init_zero;
if (len > 0) {
alox_UartMessage uart_msg;
if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) {
const alox_BatteryStatusRequest *req_ptr = UART_CMD_REQ(
&uart_msg, alox_UartMessage_battery_status_request_tag,
battery_status_request);
if (req_ptr != NULL) {
req = *req_ptr;
}
}
}
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_BATTERY_STATUS,
alox_UartMessage_battery_status_response_tag);
alox_BatteryStatusResponse *resp =
&response.payload.battery_status_response;
resp->success = false;
resp->samples_count = 0;
bool any = false;
if (req.all_clients) {
any |= append_master_cached(resp);
for (size_t i = 0; i < client_registry_count(); i++) {
const client_info_t *client = client_registry_at(i);
if (client == NULL) {
continue;
}
any |= append_slave_cached(resp, client);
}
ESP_LOGI(TAG, "battery cache all_clients → %u samples",
(unsigned)resp->samples_count);
} else if (req.client_id == 0) {
any = append_master_cached(resp);
ESP_LOGI(TAG, "battery cache master");
} else {
const client_info_t *client = client_registry_find_by_id(req.client_id);
if (client != NULL) {
any = append_slave_cached(resp, client);
} else {
ESP_LOGW(TAG, "client %lu not in registry", (unsigned long)req.client_id);
}
}
resp->success = any;
uart_cmd_send(&response, TAG);
}
void cmd_battery_register(void) {
uart_cmd_register(alox_MessageType_BATTERY_STATUS, handle_battery_status);
}