Files
powerpods/main/battery_uv.c
T
skrueckenandCursor 7f28a48a10 Add runtime UV protection toggle and treat near-zero sense as external power.
Per-pod NVS enable (default off) via UART/ESP-NOW/dashboard, and skip UV latch when LiPo ADC reads ~0 mV so USB/external supply still boots.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 22:51:17 +01:00

197 lines
5.0 KiB
C

#include "battery_uv.h"
#if POWERPOD_BATTERY_UV_ENABLE
#include "led_ring.h"
#include "pod_reboot.h"
#include "pod_settings.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "[BATTERY_UV]";
#define UV_DEBOUNCE_SAMPLES 2u
#define UV_POLL_INTERVAL_MS 30000u
static uint8_t s_uv_streak;
static uint8_t s_charge_streak;
bool battery_uv_is_latched(void) { return pod_settings_is_uv_latched(); }
static bool channel_is_absent(uint32_t mv) {
return mv < BATTERY_UV_ABSENT_ADC_MV;
}
static bool channel_is_under(uint32_t mv) {
return !channel_is_absent(mv) && mv < BATTERY_UV_ADC_MV;
}
static void log_lipo_reading(const char *ctx, const board_lipo_reading_t *reading) {
if (reading == NULL) {
return;
}
ESP_LOGI(TAG,
"%s LIPO1 %s %lu mV LIPO2 %s %lu mV", ctx,
reading->lipo1_valid ? "ok" : "n/a",
(unsigned long)reading->lipo1_mv, reading->lipo2_valid ? "ok" : "n/a",
(unsigned long)reading->lipo2_mv);
}
bool battery_uv_reading_is_external(const board_lipo_reading_t *reading) {
if (reading == NULL) {
return false;
}
bool any_valid = false;
if (reading->lipo1_valid) {
any_valid = true;
if (!channel_is_absent(reading->lipo1_mv)) {
return false;
}
}
if (reading->lipo2_valid) {
any_valid = true;
if (!channel_is_absent(reading->lipo2_mv)) {
return false;
}
}
return any_valid;
}
bool battery_uv_reading_is_under(const board_lipo_reading_t *reading) {
if (reading == NULL) {
return false;
}
if (reading->lipo1_valid && channel_is_under(reading->lipo1_mv)) {
return true;
}
if (reading->lipo2_valid && channel_is_under(reading->lipo2_mv)) {
return true;
}
return false;
}
bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading) {
if (reading == NULL) {
return false;
}
bool any_pack = false;
if (reading->lipo1_valid && !channel_is_absent(reading->lipo1_mv)) {
any_pack = true;
if (reading->lipo1_mv < BATTERY_CHARGE_ADC_MV) {
return false;
}
}
if (reading->lipo2_valid && !channel_is_absent(reading->lipo2_mv)) {
any_pack = true;
if (reading->lipo2_mv < BATTERY_CHARGE_ADC_MV) {
return false;
}
}
return any_pack;
}
bool battery_uv_evaluate_boot(void) {
if (!pod_settings_is_uv_enabled()) {
ESP_LOGI(TAG, "UV protection disabled — normal boot");
return false;
}
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
log_lipo_reading("boot check", &reading);
if (battery_uv_reading_is_external(&reading)) {
ESP_LOGI(TAG, "near-zero LiPo sense — external supply, normal boot");
if (pod_settings_is_uv_latched()) {
pod_settings_clear_uv_latched();
}
return false;
}
if (pod_settings_is_uv_latched() && battery_uv_reading_is_charged(&reading)) {
ESP_LOGI(TAG, "charged — clearing UV latch");
pod_settings_clear_uv_latched();
return false;
}
if (pod_settings_is_uv_latched() || battery_uv_reading_is_under(&reading)) {
if (!pod_settings_is_uv_latched()) {
ESP_LOGW(TAG, "under-voltage at boot — latching UV");
} else {
ESP_LOGW(TAG, "UV latched — energy-save boot");
}
pod_settings_set_uv_latched(true);
return true;
}
return false;
}
void battery_uv_poll(const board_lipo_reading_t *reading) {
if (reading == NULL || !pod_settings_is_uv_enabled() ||
pod_settings_is_uv_latched()) {
return;
}
if (battery_uv_reading_is_external(reading)) {
s_uv_streak = 0;
return;
}
if (battery_uv_reading_is_under(reading)) {
s_uv_streak++;
log_lipo_reading("under-voltage", reading);
ESP_LOGW(TAG, "under-voltage sample %u/%u", (unsigned)s_uv_streak,
(unsigned)UV_DEBOUNCE_SAMPLES);
if (s_uv_streak >= UV_DEBOUNCE_SAMPLES) {
ESP_LOGW(TAG, "UV confirmed — latching and restarting");
pod_settings_set_uv_latched(true);
pod_schedule_restart();
}
return;
}
s_uv_streak = 0;
}
void battery_uv_run_mode(void) {
s_charge_streak = 0;
s_uv_streak = 0;
led_ring_init();
led_ring_show_battery_low();
ESP_LOGW(TAG, "energy-save mode — ADC poll every %u ms", UV_POLL_INTERVAL_MS);
while (1) {
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
log_lipo_reading("energy-save", &reading);
if (battery_uv_reading_is_external(&reading)) {
ESP_LOGI(TAG, "external supply — clearing UV latch and restarting");
pod_settings_clear_uv_latched();
pod_schedule_restart();
} else if (battery_uv_reading_is_charged(&reading)) {
s_charge_streak++;
ESP_LOGI(TAG, "charge sample %u/%u", (unsigned)s_charge_streak,
(unsigned)UV_DEBOUNCE_SAMPLES);
if (s_charge_streak >= UV_DEBOUNCE_SAMPLES) {
ESP_LOGI(TAG, "charged — clearing UV latch and restarting");
pod_settings_clear_uv_latched();
pod_schedule_restart();
}
} else {
s_charge_streak = 0;
}
vTaskDelay(pdMS_TO_TICKS(UV_POLL_INTERVAL_MS));
}
}
#endif