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>
197 lines
5.0 KiB
C
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
|