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;