A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 >**_

authored by

jpirnay and committed by
GitHub
71719e1d d6951f81

+100 -58
+8 -1
lib/hal/HalGPIO.cpp
··· 7 7 pinMode(UART0_RXD, INPUT); 8 8 } 9 9 10 - void HalGPIO::update() { inputMgr.update(); } 10 + void HalGPIO::update() { 11 + inputMgr.update(); 12 + const bool connected = isUsbConnected(); 13 + usbStateChanged = (connected != lastUsbConnected); 14 + lastUsbConnected = connected; 15 + } 16 + 17 + bool HalGPIO::wasUsbStateChanged() const { return usbStateChanged; } 11 18 12 19 bool HalGPIO::isPressed(uint8_t buttonIndex) const { return inputMgr.isPressed(buttonIndex); } 13 20
+8
lib/hal/HalGPIO.h
··· 23 23 InputManager inputMgr; 24 24 #endif 25 25 26 + bool lastUsbConnected = false; 27 + bool usbStateChanged = false; 28 + 26 29 public: 27 30 HalGPIO() = default; 28 31 ··· 41 44 // Check if USB is connected 42 45 bool isUsbConnected() const; 43 46 47 + // Returns true once per edge (plug or unplug) since the last update() 48 + bool wasUsbStateChanged() const; 49 + 44 50 enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other }; 45 51 46 52 WakeupReason getWakeupReason() const; ··· 54 60 static constexpr uint8_t BTN_DOWN = 5; 55 61 static constexpr uint8_t BTN_POWER = 6; 56 62 }; 63 + 64 + extern HalGPIO gpio; // Singleton
+32 -4
src/components/themes/BaseTheme.cpp
··· 5 5 #include <HalStorage.h> 6 6 #include <Logging.h> 7 7 8 + #include <algorithm> 8 9 #include <cstdint> 9 10 #include <string> 10 11 ··· 34 35 renderer.drawPixel(x + battWidth - 1, y + rectHeight - 4); 35 36 renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rectHeight - 5); 36 37 38 + const bool charging = gpio.isUsbConnected(); 39 + 37 40 // The +1 is to round up, so that we always fill at least one pixel 38 - int filledWidth = percentage * (battWidth - 5) / 100 + 1; 39 - if (filledWidth > battWidth - 5) { 40 - filledWidth = battWidth - 5; // Ensure we don't overflow 41 + const int maxFillWidth = battWidth - 5; 42 + const int fillHeight = rectHeight - 4; 43 + if (maxFillWidth <= 0 || fillHeight <= 0) { 44 + return; 45 + } 46 + int filledWidth = percentage * maxFillWidth / 100 + 1; 47 + if (filledWidth > maxFillWidth) { 48 + filledWidth = maxFillWidth; 41 49 } 42 50 43 - renderer.fillRect(x + 2, y + 2, filledWidth, rectHeight - 4); 51 + // When charging, ensure minimum fill so lightning bolt is fully visible 52 + constexpr int minFillForBolt = 8; 53 + if (charging && filledWidth < minFillForBolt) { 54 + filledWidth = std::min(minFillForBolt, maxFillWidth); 55 + } 56 + 57 + renderer.fillRect(x + 2, y + 2, filledWidth, fillHeight); 58 + 59 + // Draw lightning bolt when charging (white/inverted on black fill for visibility) 60 + if (charging) { 61 + const int boltX = x + 4; 62 + const int boltY = y + 2; 63 + renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false); 64 + renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false); 65 + renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false); 66 + renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false); 67 + renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false); 68 + renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false); 69 + renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false); 70 + renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false); 71 + } 44 72 } 45 73 } // namespace 46 74
+46 -53
src/components/themes/lyra/LyraTheme.cpp
··· 1 1 #include "LyraTheme.h" 2 2 3 3 #include <GfxRenderer.h> 4 + #include <HalGPIO.h> 4 5 #include <HalPowerManager.h> 5 6 #include <HalStorage.h> 6 7 #include <I18n.h> ··· 42 43 constexpr int mainMenuColumns = 2; 43 44 int coverWidth = 0; 44 45 46 + void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, 47 + uint16_t percentage) { 48 + // Top line 49 + renderer.drawLine(x + 1, y, x + battWidth - 3, y); 50 + // Bottom line 51 + renderer.drawLine(x + 1, y + rectHeight - 1, x + battWidth - 3, y + rectHeight - 1); 52 + // Left line 53 + renderer.drawLine(x, y + 1, x, y + rectHeight - 2); 54 + // Battery end 55 + renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rectHeight - 2); 56 + renderer.drawPixel(x + battWidth - 1, y + 3); 57 + renderer.drawPixel(x + battWidth - 1, y + rectHeight - 4); 58 + renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rectHeight - 5); 59 + 60 + const bool charging = gpio.isUsbConnected(); 61 + 62 + // Draw bars 63 + if (percentage > 10 || charging) { 64 + renderer.fillRect(x + 2, y + 2, 3, rectHeight - 4); 65 + } 66 + if (percentage > 40 || charging) { 67 + renderer.fillRect(x + 6, y + 2, 3, rectHeight - 4); 68 + } 69 + if (percentage > 70) { 70 + renderer.fillRect(x + 10, y + 2, 3, rectHeight - 4); 71 + } 72 + 73 + if (charging) { 74 + const int boltX = x + 4; 75 + const int boltY = y + 2; 76 + renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false); 77 + renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false); 78 + renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false); 79 + renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false); 80 + renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false); 81 + renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false); 82 + renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false); 83 + renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false); 84 + } 85 + } 86 + 45 87 const uint8_t* iconForName(UIIcon icon, int size) { 46 88 if (size == 24) { 47 89 switch (icon) { ··· 87 129 void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { 88 130 // Left aligned: icon on left, percentage on right (reader mode) 89 131 const uint16_t percentage = powerManager.getBatteryPercentage(); 90 - const int y = rect.y + 6; 91 - const int battWidth = LyraMetrics::values.batteryWidth; 92 132 93 133 if (showPercentage) { 94 134 const auto percentageText = std::to_string(percentage) + "%"; 95 - renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + battWidth, rect.y, percentageText.c_str()); 135 + renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + LyraMetrics::values.batteryWidth, rect.y, 136 + percentageText.c_str()); 96 137 } 97 138 98 - // Draw icon 99 - const int x = rect.x; 100 - // Top line 101 - renderer.drawLine(x + 1, y, x + battWidth - 3, y); 102 - // Bottom line 103 - renderer.drawLine(x + 1, y + rect.height - 1, x + battWidth - 3, y + rect.height - 1); 104 - // Left line 105 - renderer.drawLine(x, y + 1, x, y + rect.height - 2); 106 - // Battery end 107 - renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rect.height - 2); 108 - renderer.drawPixel(x + battWidth - 1, y + 3); 109 - renderer.drawPixel(x + battWidth - 1, y + rect.height - 4); 110 - renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rect.height - 5); 111 - 112 - // Draw bars 113 - if (percentage > 10) { 114 - renderer.fillRect(x + 2, y + 2, 3, rect.height - 4); 115 - } 116 - if (percentage > 40) { 117 - renderer.fillRect(x + 6, y + 2, 3, rect.height - 4); 118 - } 119 - if (percentage > 70) { 120 - renderer.fillRect(x + 10, y + 2, 3, rect.height - 4); 121 - } 139 + drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage); 122 140 } 123 141 124 142 void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { 125 143 // Right aligned: percentage on left, icon on right (UI headers) 126 144 const uint16_t percentage = powerManager.getBatteryPercentage(); 127 - const int y = rect.y + 6; 128 - const int battWidth = LyraMetrics::values.batteryWidth; 129 145 130 146 if (showPercentage) { 131 147 const auto percentageText = std::to_string(percentage) + "%"; ··· 137 153 renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str()); 138 154 } 139 155 140 - // Draw icon at rect.x 141 - const int x = rect.x; 142 - // Top line 143 - renderer.drawLine(x + 1, y, x + battWidth - 3, y); 144 - // Bottom line 145 - renderer.drawLine(x + 1, y + rect.height - 1, x + battWidth - 3, y + rect.height - 1); 146 - // Left line 147 - renderer.drawLine(x, y + 1, x, y + rect.height - 2); 148 - // Battery end 149 - renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rect.height - 2); 150 - renderer.drawPixel(x + battWidth - 1, y + 3); 151 - renderer.drawPixel(x + battWidth - 1, y + rect.height - 4); 152 - renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rect.height - 5); 153 - 154 - // Draw bars 155 - if (percentage > 10) { 156 - renderer.fillRect(x + 2, y + 2, 3, rect.height - 4); 157 - } 158 - if (percentage > 40) { 159 - renderer.fillRect(x + 6, y + 2, 3, rect.height - 4); 160 - } 161 - if (percentage > 70) { 162 - renderer.fillRect(x + 10, y + 2, 3, rect.height - 4); 163 - } 156 + drawLyraBatteryIcon(renderer, rect.x, rect.y + 6, LyraMetrics::values.batteryWidth, rect.height, percentage); 164 157 } 165 158 166 159 void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const {
+6
src/main.cpp
··· 379 379 return; 380 380 } 381 381 382 + // Refresh the battery icon when USB is plugged or unplugged. 383 + // Placed after sleep guards so we never queue a render that won't be processed. 384 + if (gpio.wasUsbStateChanged()) { 385 + activityManager.requestUpdate(); 386 + } 387 + 382 388 const unsigned long activityStartTime = millis(); 383 389 activityManager.loop(); 384 390 const unsigned long activityDuration = millis() - activityStartTime;