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:
2026-05-29 20:14:28 +02:00
co-authored by Cursor
parent eb67a46158
commit 3cb0b5bbe9
30 changed files with 1618 additions and 150 deletions
+40 -20
View File
@@ -6,7 +6,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h"
#include "freertos/queue.h"
#include <stdint.h>
#include <string.h>
static const char *TAG_BTN = "[BTN]";
static const char *TAG_LIPO = "[LIPO]";
@@ -14,6 +14,8 @@ static const char *TAG_LIPO = "[LIPO]";
#define LIPO_SAMPLE_INTERVAL_MS 10000
#define BUTTON_QUEUE_LEN 4
#define BUTTON_DEBOUNCE_MS 80
#define LIPO_ADC_FULL_SCALE_MV 3300
#define LIPO_ADC_MAX_RAW 4095
static QueueHandle_t s_button_queue;
static adc_oneshot_unit_handle_t s_adc;
@@ -47,33 +49,51 @@ static esp_err_t adc_init_channel(int gpio, adc_channel_t *out_ch, bool *out_ok)
return ESP_OK;
}
static uint32_t raw_to_mv(int raw) {
if (raw < 0) {
return 0;
}
return (uint32_t)((raw * LIPO_ADC_FULL_SCALE_MV) / LIPO_ADC_MAX_RAW);
}
static void sample_one_channel(adc_channel_t ch, bool ok, uint32_t *mv_out,
bool *valid_out) {
*valid_out = false;
*mv_out = 0;
if (!ok || s_adc == NULL) {
return;
}
int raw = 0;
if (adc_oneshot_read(s_adc, ch, &raw) == ESP_OK) {
*valid_out = true;
*mv_out = raw_to_mv(raw);
}
}
void board_input_read_lipo(board_lipo_reading_t *out) {
if (out == NULL) {
return;
}
memset(out, 0, sizeof(*out));
sample_one_channel(s_lipo1_ch, s_lipo1_ok, &out->lipo1_mv, &out->lipo1_valid);
sample_one_channel(s_lipo2_ch, s_lipo2_ok, &out->lipo2_mv, &out->lipo2_valid);
}
static void lipo_monitor_task(void *param) {
(void)param;
ESP_LOGI(TAG_LIPO, "monitor task (interval %d ms)", LIPO_SAMPLE_INTERVAL_MS);
while (1) {
int raw1 = -1;
int raw2 = -1;
int mv1 = -1;
int mv2 = -1;
if (s_lipo1_ok) {
raw1 = 0;
if (adc_oneshot_read(s_adc, s_lipo1_ch, &raw1) == ESP_OK) {
mv1 = (raw1 * 3300) / 4095;
}
}
if (s_lipo2_ok) {
raw2 = 0;
if (adc_oneshot_read(s_adc, s_lipo2_ch, &raw2) == ESP_OK) {
mv2 = (raw2 * 3300) / 4095;
}
}
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
ESP_LOGI(TAG_LIPO,
"LIPO1 GPIO%d raw=%d (~%d mV) LIPO2 GPIO%d raw=%d (~%d mV)",
V_LIPO_1_GPIO, raw1, mv1, V_LIPO_2_GPIO, raw2, mv2);
"LIPO1 GPIO%d %s %lu mV LIPO2 GPIO%d %s %lu mV",
V_LIPO_1_GPIO, reading.lipo1_valid ? "ok" : "n/a",
(unsigned long)reading.lipo1_mv, V_LIPO_2_GPIO,
reading.lipo2_valid ? "ok" : "n/a",
(unsigned long)reading.lipo2_mv);
vTaskDelay(pdMS_TO_TICKS(LIPO_SAMPLE_INTERVAL_MS));
}