From 71719e1d94d0c103e1048a60470547d927c51d6f Mon Sep 17 00:00:00 2001 From: jpirnay Date: Fri, 20 Mar 2026 04:33:29 +0100 Subject: [PATCH] feat: battery charging indicator (mirroring PR #537) (#1427) ## Summary * **What is the goal of this PR?** All praise goes to @didacta for his PR #537. Just picked up the reviewer comments to contain the changes as suggested (there was no response for more than 6 weeks, so I wanted to reanimate this feature). Just one addition: should recognize usb cable plug ins / retractions and update the icon immediately * **What changes are included?** ## Additional Context see #537 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< NO >**_ --- lib/hal/HalGPIO.cpp | 9 ++- lib/hal/HalGPIO.h | 8 ++ src/components/themes/BaseTheme.cpp | 36 ++++++++- src/components/themes/lyra/LyraTheme.cpp | 99 +++++++++++------------- src/main.cpp | 6 ++ 5 files changed, 100 insertions(+), 58 deletions(-) diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 64a251de..ec0176a2 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -7,7 +7,14 @@ void HalGPIO::begin() { pinMode(UART0_RXD, INPUT); } -void HalGPIO::update() { inputMgr.update(); } +void HalGPIO::update() { + inputMgr.update(); + const bool connected = isUsbConnected(); + usbStateChanged = (connected != lastUsbConnected); + lastUsbConnected = connected; +} + +bool HalGPIO::wasUsbStateChanged() const { return usbStateChanged; } bool HalGPIO::isPressed(uint8_t buttonIndex) const { return inputMgr.isPressed(buttonIndex); } diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index 45ca50a5..a283ed60 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -23,6 +23,9 @@ class HalGPIO { InputManager inputMgr; #endif + bool lastUsbConnected = false; + bool usbStateChanged = false; + public: HalGPIO() = default; @@ -41,6 +44,9 @@ class HalGPIO { // Check if USB is connected bool isUsbConnected() const; + // Returns true once per edge (plug or unplug) since the last update() + bool wasUsbStateChanged() const; + enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other }; WakeupReason getWakeupReason() const; @@ -54,3 +60,5 @@ class HalGPIO { static constexpr uint8_t BTN_DOWN = 5; static constexpr uint8_t BTN_POWER = 6; }; + +extern HalGPIO gpio; // Singleton diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 53d82a99..9c563eb1 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -34,13 +35,40 @@ void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, i renderer.drawPixel(x + battWidth - 1, y + rectHeight - 4); renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rectHeight - 5); + const bool charging = gpio.isUsbConnected(); + // The +1 is to round up, so that we always fill at least one pixel - int filledWidth = percentage * (battWidth - 5) / 100 + 1; - if (filledWidth > battWidth - 5) { - filledWidth = battWidth - 5; // Ensure we don't overflow + const int maxFillWidth = battWidth - 5; + const int fillHeight = rectHeight - 4; + if (maxFillWidth <= 0 || fillHeight <= 0) { + return; + } + int filledWidth = percentage * maxFillWidth / 100 + 1; + if (filledWidth > maxFillWidth) { + filledWidth = maxFillWidth; } - renderer.fillRect(x + 2, y + 2, filledWidth, rectHeight - 4); + // When charging, ensure minimum fill so lightning bolt is fully visible + constexpr int minFillForBolt = 8; + if (charging && filledWidth < minFillForBolt) { + filledWidth = std::min(minFillForBolt, maxFillWidth); + } + + renderer.fillRect(x + 2, y + 2, filledWidth, fillHeight); + + // Draw lightning bolt when charging (white/inverted on black fill for visibility) + if (charging) { + const int boltX = x + 4; + const int boltY = y + 2; + renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false); + renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false); + renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false); + renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false); + renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false); + renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false); + renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false); + renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false); + } } } // namespace diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 36c19501..58dabeab 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -1,6 +1,7 @@ #include "LyraTheme.h" #include +#include #include #include #include @@ -42,6 +43,47 @@ constexpr int listIconSize = 24; constexpr int mainMenuColumns = 2; int coverWidth = 0; +void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, + uint16_t percentage) { + // Top line + renderer.drawLine(x + 1, y, x + battWidth - 3, y); + // Bottom line + renderer.drawLine(x + 1, y + rectHeight - 1, x + battWidth - 3, y + rectHeight - 1); + // Left line + renderer.drawLine(x, y + 1, x, y + rectHeight - 2); + // Battery end + renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rectHeight - 2); + renderer.drawPixel(x + battWidth - 1, y + 3); + renderer.drawPixel(x + battWidth - 1, y + rectHeight - 4); + renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rectHeight - 5); + + const bool charging = gpio.isUsbConnected(); + + // Draw bars + if (percentage > 10 || charging) { + renderer.fillRect(x + 2, y + 2, 3, rectHeight - 4); + } + if (percentage > 40 || charging) { + renderer.fillRect(x + 6, y + 2, 3, rectHeight - 4); + } + if (percentage > 70) { + renderer.fillRect(x + 10, y + 2, 3, rectHeight - 4); + } + + if (charging) { + const int boltX = x + 4; + const int boltY = y + 2; + renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false); + renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false); + renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false); + renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false); + renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false); + renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false); + renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false); + renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false); + } +} + const uint8_t* iconForName(UIIcon icon, int size) { if (size == 24) { switch (icon) { @@ -87,45 +129,19 @@ const uint8_t* iconForName(UIIcon icon, int size) { void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { // Left aligned: icon on left, percentage on right (reader mode) const uint16_t percentage = powerManager.getBatteryPercentage(); - const int y = rect.y + 6; - const int battWidth = LyraMetrics::values.batteryWidth; if (showPercentage) { const auto percentageText = std::to_string(percentage) + "%"; - renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + battWidth, rect.y, percentageText.c_str()); + renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + LyraMetrics::values.batteryWidth, rect.y, + percentageText.c_str()); } - // Draw icon - const int x = rect.x; - // Top line - renderer.drawLine(x + 1, y, x + battWidth - 3, y); - // Bottom line - renderer.drawLine(x + 1, y + rect.height - 1, x + battWidth - 3, y + rect.height - 1); - // Left line - renderer.drawLine(x, y + 1, x, y + rect.height - 2); - // Battery end - renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rect.height - 2); - renderer.drawPixel(x + battWidth - 1, y + 3); - renderer.drawPixel(x + battWidth - 1, y + rect.height - 4); - renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rect.height - 5); - - // Draw bars - if (percentage > 10) { - renderer.fillRect(x + 2, y + 2, 3, rect.height - 4); - } - if (percentage > 40) { - renderer.fillRect(x + 6, y + 2, 3, rect.height - 4); - } - if (percentage > 70) { - renderer.fillRect(x + 10, y + 2, 3, rect.height - 4); - } + drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage); } void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { // Right aligned: percentage on left, icon on right (UI headers) const uint16_t percentage = powerManager.getBatteryPercentage(); - const int y = rect.y + 6; - const int battWidth = LyraMetrics::values.batteryWidth; if (showPercentage) { const auto percentageText = std::to_string(percentage) + "%"; @@ -137,30 +153,7 @@ void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str()); } - // Draw icon at rect.x - const int x = rect.x; - // Top line - renderer.drawLine(x + 1, y, x + battWidth - 3, y); - // Bottom line - renderer.drawLine(x + 1, y + rect.height - 1, x + battWidth - 3, y + rect.height - 1); - // Left line - renderer.drawLine(x, y + 1, x, y + rect.height - 2); - // Battery end - renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rect.height - 2); - renderer.drawPixel(x + battWidth - 1, y + 3); - renderer.drawPixel(x + battWidth - 1, y + rect.height - 4); - renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rect.height - 5); - - // Draw bars - if (percentage > 10) { - renderer.fillRect(x + 2, y + 2, 3, rect.height - 4); - } - if (percentage > 40) { - renderer.fillRect(x + 6, y + 2, 3, rect.height - 4); - } - if (percentage > 70) { - renderer.fillRect(x + 10, y + 2, 3, rect.height - 4); - } + drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage); } void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const { diff --git a/src/main.cpp b/src/main.cpp index 7bd21f37..75bf69c5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -379,6 +379,12 @@ void loop() { return; } + // Refresh the battery icon when USB is plugged or unplugged. + // Placed after sleep guards so we never queue a render that won't be processed. + if (gpio.wasUsbStateChanged()) { + activityManager.requestUpdate(); + } + const unsigned long activityStartTime = millis(); activityManager.loop(); const unsigned long activityDuration = millis() - activityStartTime;