battery chart: estimated life left (d/h/m) from the discharge slope, skipping charge

Signed-off-by: Dan Vybiral <dan.vybiral@greencode.cz>
This commit is contained in:
Dan Vybiral committed 2026-06-17 12:43:02 +02:00
1 parent 17981029a7
commit be7fb32f23
1 file changed
+44 -1
+44 -1
View File
@@ -10048,6 +10048,38 @@ static void battChartTickCb(lv_event_t* e) {
lv_snprintf(dsc->text, dsc->text_length, "%d.%01d",
(int)(dsc->value / 1000), (int)((dsc->value % 1000) / 100));
}
// Estimate remaining battery life from the logged samples. Charging samples
// (mv > calibrated full) are ignored, and only the discharge segment AFTER the
// most recent charge is used. A least-squares slope over that segment gives
// mV/s; time-to-empty targets 3300 mV (the Li-ion empty point used by the % curve).
static void batteryEstimateText(const uint32_t* eps, const uint16_t* mvs, int n,
int full_mv, char* out, size_t cap) {
int start = 0;
for (int i = 1; i < n; ++i) {
if ((int)mvs[i] > full_mv) start = i + 1; // charging spike -> resume after
else if ((int)mvs[i] - (int)mvs[i - 1] >= 25) start = i; // a >=25 mV rise = charge event
}
double sx = 0, sy = 0, sxx = 0, sxy = 0; int m = 0;
uint32_t t0 = 0, last_t = 0; int last_v = 0;
for (int i = start; i < n; ++i) {
if ((int)mvs[i] > full_mv) continue; // skip any charging spikes in the window
if (m == 0) t0 = eps[i];
const double x = (double)(eps[i] - t0), y = (double)mvs[i];
sx += x; sy += y; sxx += x * x; sxy += x * y; ++m; last_v = mvs[i]; last_t = eps[i];
}
if (m < 3 || (last_t - t0) < 900) { snprintf(out, cap, "Battery life: gathering data\xe2\x80\xa6"); return; }
const double denom = (double)m * sxx - sx * sx;
if (denom <= 0) { snprintf(out, cap, "Battery life: \xe2\x80\x94"); return; }
const double slope = ((double)m * sxy - sx * sy) / denom; // mV/s (negative = discharging)
if (slope >= -1e-7) { snprintf(out, cap, "Battery life: charging / steady"); return; }
double secs = (double)(last_v - 3300) / (-slope);
if (secs < 0) secs = 0;
long s = (long)secs;
const int d = (int)(s / 86400); s %= 86400;
const int h = (int)(s / 3600); s %= 3600;
const int mn = (int)(s / 60);
snprintf(out, cap, "Est. life: %dd %dh %dm", d, h, mn);
}
static void openBatteryChartWindow() {
if (s_batt_chart_root) return;
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
@@ -10064,7 +10096,7 @@ static void openBatteryChartWindow() {
const lv_coord_t cardw = sw - 24;
lv_obj_t* card = lv_obj_create(s_batt_chart_root);
lv_obj_remove_style_all(card);
lv_obj_set_size(card, cardw, 216);
lv_obj_set_size(card, cardw, 236);
lv_obj_align(card, LV_ALIGN_TOP_MID, 0, 8);
styleSurface(card, COLOR_PANEL, 8);
lv_obj_set_style_border_color(card, lv_color_hex(0x18191A), LV_PART_MAIN);
@@ -10085,6 +10117,7 @@ static void openBatteryChartWindow() {
static const int k_max_pts = 288; // 24h / 5min
static uint16_t mvs[k_max_pts];
static uint16_t cpus[k_max_pts];
static uint32_t eps[k_max_pts];
int n = 0;
if (SD.cardType() != CARD_NONE) {
markSdIo();
@@ -10100,6 +10133,7 @@ static void openBatteryChartWindow() {
const int t4 = ln.indexOf('\t', t3 + 1);
const int mv = ln.substring(t2 + 1, t3).toInt();
if (mv <= 0) continue;
eps[n] = (uint32_t)strtoul(ln.c_str(), nullptr, 10); // first column = epoch
mvs[n] = (uint16_t)mv;
cpus[n] = (uint16_t)((t4 >= 0) ? ln.substring(t4 + 1).toInt() : 0);
++n;
@@ -10184,6 +10218,15 @@ static void openBatteryChartWindow() {
lv_obj_set_style_text_color(sl, lv_color_hex(COLOR_SUB), LV_PART_MAIN);
lv_obj_align(sl, LV_ALIGN_BOTTOM_LEFT, 0, 0);
// Estimated remaining battery life (discharge-rate fit over the logged window).
char est[44];
batteryEstimateText(eps, mvs, n, (int)full_mv, est, sizeof est);
lv_obj_t* el = lv_label_create(card);
lv_label_set_text(el, est);
lv_obj_set_style_text_font(el, &g_font_14, LV_PART_MAIN);
lv_obj_set_style_text_color(el, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN);
lv_obj_align(el, LV_ALIGN_BOTTOM_LEFT, 0, -18);
// Clear the battery history (with confirmation).
lv_obj_t* clr = lv_btn_create(card);
lv_obj_set_size(clr, 30, 26);