From 845e4f9509da78b9b9d4c4ee654eca353ed71502 Mon Sep 17 00:00:00 2001 From: Thomas Faour Date: Wed, 22 Jul 2026 16:51:28 -0400 Subject: [PATCH] Reduce battery-reading noise with a trimmed-mean ADC sample Sometimes a single reading came in noticeably off from the real trend (a regulator/RF transient during sampling), and the next normal reading would then look like a big jump relative to that bad one -- server-side, enough to misfire the recharge-cycle heuristic (see the paired server commit). Went from 8 raw-averaged samples to 16, sorted, with the 3 extreme samples on each end dropped before averaging the remaining 10 -- a handful of outliers can no longer skew the result the way a plain average let them. --- firmware/main/battery.c | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/firmware/main/battery.c b/firmware/main/battery.c index 55dd0ca..46edfb6 100644 --- a/firmware/main/battery.c +++ b/firmware/main/battery.c @@ -1,3 +1,5 @@ +#include + #include "driver/gpio.h" #include "esp_adc/adc_cali_scheme.h" #include "esp_adc/adc_oneshot.h" @@ -11,7 +13,13 @@ static const char *TAG = "battery"; #if CONFIG_FRAME_BATTERY_ADC_GPIO >= 0 #define BATTERY_ADC_GPIO CONFIG_FRAME_BATTERY_ADC_GPIO -#define BATTERY_SAMPLES 8 +#define BATTERY_SAMPLES 16 +/* Trimmed mean: the extreme BATTERY_TRIM samples on each end (regulator/ + * RF transients, not the true resting voltage) are dropped before + * averaging the rest -- a plain average lets even one or two of those + * skew the result enough to read as a real percent change downstream + * (see the recharge-jump handling in routers/device.py). */ +#define BATTERY_TRIM 3 /* The external divider halves the battery voltage (2x200k, per the * Seeed-documented XIAO wiring) so a full 4.2V cell reads ~2.1V at the * pin, inside the 12dB-attenuation ADC range. */ @@ -34,6 +42,11 @@ static const struct { { 3300, 5 }, { 3000, 0 }, }; +static int int_cmp(const void *a, const void *b) +{ + return *(const int *)a - *(const int *)b; +} + static int mv_to_percent(int mv) { int n = sizeof(LIPO_CURVE) / sizeof(LIPO_CURVE[0]); @@ -139,19 +152,17 @@ int battery_read_percent(void) ESP_LOGW(TAG, "ADC calibration unavailable, using nominal scaling"); } - int mv_sum = 0; + int mv_samples[BATTERY_SAMPLES]; int samples = 0; for (int i = 0; i < BATTERY_SAMPLES; i++) { int value; if (calibrated) { if (adc_oneshot_get_calibrated_result(adc, cali, channel, &value) == ESP_OK) { - mv_sum += value; - samples++; + mv_samples[samples++] = value; } } else { if (adc_oneshot_read(adc, channel, &value) == ESP_OK) { - mv_sum += value * 3300 / 4095; /* nominal 12-bit full scale at 12dB */ - samples++; + mv_samples[samples++] = value * 3300 / 4095; /* nominal 12-bit full scale at 12dB */ } } } @@ -167,7 +178,19 @@ int battery_read_percent(void) return -1; } - int battery_mv = (mv_sum / samples) * BATTERY_DIVIDER_RATIO; + /* Only trim if there's enough left afterward to still be a + * meaningful average -- falls back to a plain average of whatever + * came in on a wake where most reads failed. */ + qsort(mv_samples, samples, sizeof(int), int_cmp); + int trim = (samples > 2 * BATTERY_TRIM) ? BATTERY_TRIM : 0; + int mv_sum = 0; + int kept = 0; + for (int i = trim; i < samples - trim; i++) { + mv_sum += mv_samples[i]; + kept++; + } + + int battery_mv = (mv_sum / kept) * BATTERY_DIVIDER_RATIO; if (battery_mv < BATTERY_MV_MIN || battery_mv > BATTERY_MV_MAX) { ESP_LOGI(TAG, "Reading %dmV outside plausible battery range, ignoring", battery_mv); return -1;