Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc65b19cf2 | ||
|
|
e1bca5a81a | ||
|
|
845e4f9509 |
+30
-7
@@ -1,3 +1,5 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
#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;
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.2.3
|
||||
1.2.4
|
||||
|
||||
@@ -21,7 +21,16 @@ logger = logging.getLogger(__name__)
|
||||
# Battery-history / estimate tuning (see /frame/battery and battery_estimate_s).
|
||||
BATTERY_HISTORY_MAX = 500 # ~20 days at hourly reports
|
||||
BATTERY_LOG_MAX = 20000 # ~2 years at hourly reports -- cap on the battery_log table per frame
|
||||
RECHARGE_JUMP_PCT = 5 # a report this much above the previous one = battery was recharged
|
||||
RECHARGE_JUMP_PCT = 5 # a report this much above the recent baseline = battery was recharged
|
||||
# How many of the most recent reports make up that baseline. A lone noisy
|
||||
# reading (ADC/regulator glitch -- see firmware/main/battery.c) can still
|
||||
# dip or spike a single report; comparing against just the one immediately
|
||||
# previous report meant that a normal reading right after a noisy dip
|
||||
# looked like a 5%+ jump and falsely registered as a recharge. Comparing
|
||||
# against the max of the last few reports instead means an actual recharge
|
||||
# still needs to clear all of them, while a single stray low one doesn't
|
||||
# get to set the bar.
|
||||
RECHARGE_LOOKBACK = 3
|
||||
MIN_ESTIMATE_SPAN_S = 2 * 3600 # need at least this much observed time...
|
||||
MIN_ESTIMATE_DROP_PCT = 2 # ...and this much observed drop before estimating
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ from .common import (
|
||||
BATTERY_HISTORY_MAX,
|
||||
BATTERY_LOG_MAX,
|
||||
RECHARGE_JUMP_PCT,
|
||||
RECHARGE_LOOKBACK,
|
||||
immich_client_for,
|
||||
immich_creds,
|
||||
list_assets,
|
||||
@@ -202,7 +203,12 @@ def frame_battery(
|
||||
alert_frame_name = ""
|
||||
with frame_locked(db, frame.id) as locked:
|
||||
locked.stats_battery_reports += 1
|
||||
if locked.battery_history and body.percent >= locked.battery_history[-1][1] + RECHARGE_JUMP_PCT:
|
||||
# See RECHARGE_LOOKBACK: compared against the max of the last few
|
||||
# reports, not just the single previous one, so a lone noisy dip
|
||||
# can't make the next normal reading look like a recharge.
|
||||
recent = locked.battery_history[-RECHARGE_LOOKBACK:]
|
||||
recent_max = max((pct for _, pct in recent), default=None)
|
||||
if recent_max is not None and body.percent >= recent_max + RECHARGE_JUMP_PCT:
|
||||
# Percent jumped up meaningfully -- the battery was recharged
|
||||
# (or swapped). Start a fresh discharge cycle so runtime and
|
||||
# discharge-rate estimates never span a charge -- and let a
|
||||
|
||||
Reference in New Issue
Block a user