#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