From cd4c99f125674664a98ef701c70b0bf243d347c6 Mon Sep 17 00:00:00 2001 From: acidburns Date: Tue, 3 Feb 2026 22:12:48 +0100 Subject: [PATCH] Calibrate battery ADC and document LiPo curve - Add BATTERY_CAL config and debug logging for raw ADC samples - Use LiPo voltage curve (4.2V full, 2.9V empty) for % mapping - Document battery calibration, curve, and debug output in README --- README.md | 12 +++++--- include/config.h | 1 + src/power_manager.cpp | 69 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f507527..247c5f2 100644 --- a/README.md +++ b/README.md @@ -251,17 +251,20 @@ inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = { 0xF19C }; ## OLED Behavior - Sender: OLED stays on for `OLED_AUTO_OFF_MS` after boot or last activity. -- Activity is detected while `PIN_OLED_CTRL` is held high, or on the high→low edge when the control is released. +- Activity is detected while `PIN_OLED_CTRL` is held high, or on the high->low edge when the control is released. - Receiver: OLED is always on (no auto-off). - Pages rotate every 4s. ## Power & Battery -- Sender disables WiFi/BLE, reads VBAT via ADC, uses linear SoC map: - - 3.0 V = 0% +- Sender disables WiFi/BLE, reads VBAT via ADC, and converts voltage to % using a LiPo curve: - 4.2 V = 100% + - 2.9 V = 0% + - linear interpolation between curve points - Uses deep sleep between cycles (`SENDER_WAKE_INTERVAL_SEC`). - Sender CPU is throttled to 80 MHz and LoRa RX is only enabled in short windows (ACK wait or time-sync). - Battery sampling averages 5 ADC reads and updates at most once per `BATTERY_SAMPLE_INTERVAL_MS` (default 60s). +- `BATTERY_CAL` applies a scale factor to match measured VBAT. +- When `SERIAL_DEBUG_MODE` is enabled, each ADC read logs the 5 raw samples, average, and computed voltage. ## Web UI - AP SSID: `DD3-Bridge-` (prefix configurable) @@ -316,6 +319,7 @@ Key timing settings in `include/config.h`: - `METER_SAMPLE_INTERVAL_MS` - `METER_SEND_INTERVAL_MS` - `BATTERY_SAMPLE_INTERVAL_MS` + - `BATTERY_CAL` - `BATCH_ACK_TIMEOUT_MS` - `BATCH_MAX_RETRIES` - `BATCH_QUEUE_DEPTH` @@ -333,7 +337,7 @@ Key timing settings in `include/config.h`: - **Compression**: MeterData uses lightweight RLE (good for JSON but not optimal). - **OBIS parsing**: supports IEC 62056-21 ASCII (Mode D); may need tuning for some meters. - **Payload size**: single JSON frames < 256 bytes (ArduinoJson static doc); binary batch frames are chunked and reassembled (typically 1 chunk). -- **Battery ADC**: uses simple linear calibration constant in `power_manager.cpp`. +- **Battery ADC**: uses a divider (R44/R45 = 100K/100K) with a configurable `BATTERY_CAL` scale and LiPo % curve. - **OLED**: no hardware reset line is used (matches working reference). - **Batch ACKs**: sender waits for ACK after a batch and retries up to `BATCH_MAX_RETRIES` with `BATCH_ACK_TIMEOUT_MS` between attempts. diff --git a/include/config.h b/include/config.h index 038338b..76cb9cd 100644 --- a/include/config.h +++ b/include/config.h @@ -70,6 +70,7 @@ constexpr uint32_t SENDER_OLED_READ_MS = 10000; constexpr uint32_t METER_SAMPLE_INTERVAL_MS = 1000; constexpr uint32_t METER_SEND_INTERVAL_MS = 30000; constexpr uint32_t BATTERY_SAMPLE_INTERVAL_MS = 60000; +constexpr float BATTERY_CAL = 1.083f; constexpr uint32_t BATCH_ACK_TIMEOUT_MS = 3000; constexpr uint8_t BATCH_MAX_RETRIES = 2; constexpr uint8_t METER_BATCH_MAX_SAMPLES = 30; diff --git a/src/power_manager.cpp b/src/power_manager.cpp index 2b599fd..c439bf6 100644 --- a/src/power_manager.cpp +++ b/src/power_manager.cpp @@ -6,7 +6,6 @@ #include static constexpr float BATTERY_DIVIDER = 2.0f; -static constexpr float BATTERY_CAL = 1.0f; static constexpr float ADC_REF_V = 3.3f; void power_sender_init() { @@ -35,25 +34,79 @@ void power_configure_unused_pins_sender() { void read_battery(MeterData &data) { uint32_t sum = 0; + uint16_t samples[5] = {}; for (uint8_t i = 0; i < 5; ++i) { - sum += analogRead(PIN_BAT_ADC); + samples[i] = analogRead(PIN_BAT_ADC); + sum += samples[i]; } float avg = static_cast(sum) / 5.0f; float v = (avg / 4095.0f) * ADC_REF_V * BATTERY_DIVIDER * BATTERY_CAL; + if (SERIAL_DEBUG_MODE) { + Serial.printf("bat_adc: %u %u %u %u %u avg=%.1f v=%.3f\n", + samples[0], samples[1], samples[2], samples[3], samples[4], + static_cast(avg), static_cast(v)); + } data.battery_voltage_v = v; data.battery_percent = battery_percent_from_voltage(v); } uint8_t battery_percent_from_voltage(float voltage_v) { - float pct = (voltage_v - 3.0f) / (4.2f - 3.0f) * 100.0f; - if (pct < 0.0f) { - pct = 0.0f; + if (isnan(voltage_v)) { + return 0; } - if (pct > 100.0f) { - pct = 100.0f; + struct LutPoint { + float v; + uint8_t pct; + }; + static const LutPoint kCurve[] = { + {4.20f, 100}, + {4.15f, 95}, + {4.11f, 90}, + {4.08f, 85}, + {4.02f, 80}, + {3.98f, 75}, + {3.95f, 70}, + {3.91f, 60}, + {3.87f, 50}, + {3.85f, 45}, + {3.84f, 40}, + {3.82f, 35}, + {3.80f, 30}, + {3.77f, 25}, + {3.75f, 20}, + {3.73f, 15}, + {3.70f, 10}, + {3.65f, 5}, + {3.60f, 2}, + {2.90f, 0}, + }; + if (voltage_v >= kCurve[0].v) { + return kCurve[0].pct; } - return static_cast(pct + 0.5f); + if (voltage_v <= kCurve[sizeof(kCurve) / sizeof(kCurve[0]) - 1].v) { + return 0; + } + for (size_t i = 0; i + 1 < sizeof(kCurve) / sizeof(kCurve[0]); ++i) { + const LutPoint &hi = kCurve[i]; + const LutPoint &lo = kCurve[i + 1]; + if (voltage_v <= hi.v && voltage_v >= lo.v) { + float span = hi.v - lo.v; + if (span <= 0.0f) { + return lo.pct; + } + float t = (voltage_v - lo.v) / span; + float pct = lo.pct + t * (hi.pct - lo.pct); + if (pct < 0.0f) { + pct = 0.0f; + } + if (pct > 100.0f) { + pct = 100.0f; + } + return static_cast(pct + 0.5f); + } + } + return 0; } void light_sleep_ms(uint32_t ms) {