From 9a1548189cc9cc28c95c1a5c9c3525e85a50020d Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Thu, 12 Feb 2026 16:55:27 -0500 Subject: [PATCH] fixes battery reading on x3 --- lib/hal/HalGPIO.cpp | 5 ++--- src/Battery.cpp | 9 +++++++++ src/Battery.h | 9 +++++++-- src/components/themes/BaseTheme.cpp | 4 ++-- src/components/themes/lyra/LyraTheme.cpp | 4 ++-- src/main.cpp | 6 ++++++ 6 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 src/Battery.cpp diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index f01b50875..2a39dee98 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -44,11 +44,10 @@ void HalGPIO::startDeepSleep() { int HalGPIO::getBatteryPercentage() const { if (_deviceType == DeviceType::X3) { - // X3 battery telemetry is not on ADC in stock fw; avoid fighting EPD DC on GPIO4. return 0; } - static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0); - return battery.readPercentage(); + static const BatteryMonitor bat(BAT_GPIO0); + return bat.readPercentage(); } bool HalGPIO::isUsbConnected() const { diff --git a/src/Battery.cpp b/src/Battery.cpp new file mode 100644 index 000000000..fc2277b8e --- /dev/null +++ b/src/Battery.cpp @@ -0,0 +1,9 @@ +#include "Battery.h" + +// Meyer's singleton — guaranteed single instance across all translation units. +// Constructed with GPIO0 (X4 ADC default). For X3, main.cpp calls +// battery().setI2CFuelGauge() to switch to BQ27220 I2C reads instead. +BatteryMonitor& battery() { + static BatteryMonitor instance(BAT_GPIO0); + return instance; +} diff --git a/src/Battery.h b/src/Battery.h index dcfcbf795..e66a63f92 100644 --- a/src/Battery.h +++ b/src/Battery.h @@ -1,6 +1,11 @@ #pragma once #include -#define BAT_GPIO0 0 // Battery voltage +#define BAT_GPIO0 0 // Battery voltage (X4 ADC pin) -static BatteryMonitor battery(BAT_GPIO0); +// Shared battery monitor singleton. Returns a single BatteryMonitor instance +// used by all callers (themes, activities, etc.). +// +// - X4: reads battery voltage via ADC on GPIO0 (default, no extra setup needed) +// - X3: reads SOC from BQ27220 fuel gauge via I2C (call setI2CFuelGauge() after Wire.begin()) +BatteryMonitor& battery(); diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 14783e558..8541af52b 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -22,7 +22,7 @@ constexpr int homeMarginTop = 30; void BaseTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { // Left aligned battery icon and percentage // TODO refactor this so the percentage doesnt change after we position it - const uint16_t percentage = battery.readPercentage(); + const uint16_t percentage = battery().readPercentage(); if (showPercentage) { const auto percentageText = std::to_string(percentage) + "%"; renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + BaseMetrics::values.batteryWidth, rect.y, @@ -232,7 +232,7 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; int batteryX = rect.x + rect.width - BaseMetrics::values.contentSidePadding - BaseMetrics::values.batteryWidth; if (showBatteryPercentage) { - const uint16_t percentage = battery.readPercentage(); + const uint16_t percentage = battery().readPercentage(); const auto percentageText = std::to_string(percentage) + "%"; batteryX -= renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); } diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 2e3ad4cd8..48482e1a0 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -22,7 +22,7 @@ constexpr int topHintButtonY = 345; void LyraTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { // Left aligned battery icon and percentage - const uint16_t percentage = battery.readPercentage(); + const uint16_t percentage = battery().readPercentage(); if (showPercentage) { const auto percentageText = std::to_string(percentage) + "%"; renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + LyraMetrics::values.batteryWidth, rect.y, @@ -64,7 +64,7 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; int batteryX = rect.x + rect.width - LyraMetrics::values.contentSidePadding - LyraMetrics::values.batteryWidth; if (showBatteryPercentage) { - const uint16_t percentage = battery.readPercentage(); + const uint16_t percentage = battery().readPercentage(); const auto percentageText = std::to_string(percentage) + "%"; batteryX -= renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); } diff --git a/src/main.cpp b/src/main.cpp index 060ed6f51..eb1a0bfff 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -259,6 +260,11 @@ void setupDisplayAndFonts() { display.setBwOnly(true); display.setControllerType(EInkDisplay::ControllerType::SSD1677); display.setDisplayDimensions(792, 528); + // X3 has a BQ27220 fuel gauge on I2C (addr 0x55) instead of an ADC voltage + // divider. SOC (0-100%) is read directly from register 0x2C. + // I2C bus: SDA=GPIO20, SCL=GPIO0, 400kHz (matches stock X3 firmware). + Wire.begin(20, 0, 400000); + battery().setI2CFuelGauge(0x55, 0x2C); } display.begin(); renderer.begin();