From 0ed4c7ffce2c3e3aef81c8d0aa7341e05c9003fa Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 16 Feb 2026 12:38:14 -0800 Subject: [PATCH] refactor: abstract hardware in DisplayManager - TFT_eSPI -> Gfx typedef (zero-cost on E32R35T) - Touch reads wrapped in #if USE_TOUCH_XPT2046 / USE_TOUCH_GT911 - Hardcoded rotation -> DISPLAY_ROTATION from BoardConfig - All 480/320 literals -> SCREEN_WIDTH / SCREEN_HEIGHT - Boot splash shows BOARD_NAME for target identification - Added holdProgress() convenience method using HOLD_DURATION_MS --- .../DisplayManager.cpp | 341 +++++++++--------- .../doorbell-touch-esp32-32e/DisplayManager.h | 74 ++-- 2 files changed, 209 insertions(+), 206 deletions(-) diff --git a/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp b/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp index 7d575d4..6bffdfb 100644 --- a/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp +++ b/sketches/doorbell-touch-esp32-32e/DisplayManager.cpp @@ -1,20 +1,25 @@ #include "DisplayManager.h" #include "Config.h" -DisplayManager::DisplayManager() - : _dash(_tft) -{ -} +#if USE_TOUCH_GT911 +#include "TouchDriver.h" +#endif + +DisplayManager::DisplayManager() : _dash(_tft) { } void DisplayManager::begin() { pinMode(PIN_LCD_BL, OUTPUT); setBacklight(true); _tft.init(); - _tft.setRotation(1); + _tft.setRotation(DISPLAY_ROTATION); _tft.fillScreen(COL_BLACK); +#if USE_TOUCH_XPT2046 uint16_t calData[5] = { 300, 3600, 300, 3600, 1 }; _tft.setTouch(calData); +#elif USE_TOUCH_GT911 + TouchDriver::begin(); +#endif } void DisplayManager::setBacklight(bool on) { @@ -24,7 +29,15 @@ void DisplayManager::setBacklight(bool on) { TouchEvent DisplayManager::readTouch() { TouchEvent evt; uint16_t x, y; - if (_tft.getTouch(&x, &y)) { + bool touched = false; + +#if USE_TOUCH_XPT2046 + touched = _tft.getTouch(&x, &y); +#elif USE_TOUCH_GT911 + touched = TouchDriver::read(x, y); +#endif + + if (touched) { evt.pressed = true; evt.x = x; evt.y = y; @@ -38,53 +51,54 @@ int DisplayManager::dashboardTouch(uint16_t x, uint16_t y) { } // ===================================================================== -// Hold detection — two-phase: charge up, then release to confirm +// Hold detection — charge up, then release to confirm // ===================================================================== HoldState DisplayManager::updateHold(unsigned long requiredMs) { HoldState h; h.targetMs = requiredMs; uint16_t tx, ty; - bool touching = _tft.getTouch(&tx, &ty); + bool touching = false; + +#if USE_TOUCH_XPT2046 + touching = _tft.getTouch(&tx, &ty); +#elif USE_TOUCH_GT911 + touching = TouchDriver::read(tx, ty); +#endif if (touching) { if (!_holdActive) { - // New press begins - _holdActive = true; - _holdCharged = false; - _holdStartMs = millis(); + _holdActive = true; + _holdCharged = false; + _holdStartMs = millis(); _holdChargeMs = 0; _holdX = tx; _holdY = ty; } h.active = true; - h.x = _holdX; - h.y = _holdY; + h.x = _holdX; + h.y = _holdY; h.holdMs = millis() - _holdStartMs; h.progress = min((float)h.holdMs / (float)requiredMs, 1.0f); _holdProgress = h.progress; if (h.holdMs >= requiredMs) { - // Charged! But don't fire yet — wait for release - _holdCharged = true; + _holdCharged = true; if (_holdChargeMs == 0) _holdChargeMs = millis(); h.charged = true; } } else { - // Finger lifted if (_holdActive) { if (_holdCharged) { - // Released after full charge → FIRE h.completed = true; h.x = _holdX; h.y = _holdY; - Serial.println("[HOLD] Charged + released → completed!"); + Serial.println("[HOLD] Charged + released -> completed!"); } else { - // Released too early → cancelled h.cancelled = true; - Serial.println("[HOLD] Released early → cancelled"); + Serial.println("[HOLD] Released early -> cancelled"); } } _holdActive = false; @@ -96,6 +110,12 @@ HoldState DisplayManager::updateHold(unsigned long requiredMs) { return h; } +float DisplayManager::holdProgress() const { + if (!_holdActive) return 0.0f; + return constrain((float)(millis() - _holdStartMs) / (float)HOLD_DURATION_MS, + 0.0f, 1.0f); +} + // ===================================================================== // Render // ===================================================================== @@ -128,7 +148,6 @@ void DisplayManager::render(const ScreenState& state) { drawAlertScreen(state); _lastBlink = state.blinkPhase; } - // Overlay progress bar when holding if (_holdProgress > 0.0f || _holdCharged) { drawSilenceProgress(_holdProgress, _holdCharged); } @@ -169,61 +188,51 @@ void DisplayManager::drawDashboard(const ScreenState& s) { } // ===================================================================== -// Silence progress bar — with jitter/flash when charged -// ===================================================================== -// ===================================================================== -// Silence progress bar — gradient fill + breathing plateau on charge +// Silence progress bar // ===================================================================== void DisplayManager::drawSilenceProgress(float progress, bool charged) { - const int barX = 20; - const int barY = SCREEN_HEIGHT - 50; // near bottom of alert screen - const int barW = SCREEN_WIDTH - 40; - const int barH = 26; - const int radius = 6; + const int barX = 20; + const int barY = SCREEN_HEIGHT - 50; + const int barW = SCREEN_WIDTH - 40; + const int barH = 26; + const int radius = 6; - // ── Background track ── - _tft.fillRoundRect(barX, barY, barW, barH, radius, COL_DARK_GRAY); + _tft.fillRoundRect(barX, barY, barW, barH, radius, COL_DARK_GRAY); - if (charged) { - // ── Plateau: full bar with breathing pulse ── - // Sine-based brightness oscillation (~1 Hz) - float breath = (sinf(millis() / 150.0f) + 1.0f) / 2.0f; // 0.0–1.0 - uint8_t gLo = 42, gHi = 63; // green channel range (RGB565 6-bit) - uint8_t g = gLo + (uint8_t)(breath * (float)(gHi - gLo)); - uint16_t pulseCol = (g << 5); // pure green in RGB565 + if (charged) { + float breath = (sinf(millis() / 150.0f) + 1.0f) / 2.0f; + uint8_t gLo = 42, gHi = 63; + uint8_t g = gLo + (uint8_t)(breath * (float)(gHi - gLo)); + uint16_t pulseCol = (g << 5); - _tft.fillRoundRect(barX, barY, barW, barH, radius, pulseCol); - _tft.drawRoundRect(barX, barY, barW, barH, radius, COL_WHITE); - - _tft.setTextDatum(MC_DATUM); - _tft.setTextFont(2); - _tft.setTextSize(1); - _tft.setTextColor(COL_WHITE, pulseCol); - _tft.drawString("RELEASE", barX + barW / 2, barY + barH / 2); - return; - } - - // ── Filling: ease-out cubic ── - float eased = 1.0f - powf(1.0f - progress, 3.0f); - int fillW = max(1, (int)(eased * (float)barW)); - - // Gradient slices: dark green → bright green - uint8_t gMin = 16, gMax = 58; // RGB565 green channel range - for (int i = 0; i < fillW; i++) { - float frac = (float)i / (float)barW; // position relative to full bar - uint8_t g = gMin + (uint8_t)(frac * (float)(gMax - gMin)); - _tft.drawFastVLine(barX + i, barY + 1, barH - 2, (uint16_t)(g << 5)); - } - - // Border on top of gradient slices + _tft.fillRoundRect(barX, barY, barW, barH, radius, pulseCol); _tft.drawRoundRect(barX, barY, barW, barH, radius, COL_WHITE); - // Label _tft.setTextDatum(MC_DATUM); _tft.setTextFont(2); _tft.setTextSize(1); - _tft.setTextColor(COL_WHITE, COL_DARK_GRAY); - _tft.drawString("HOLD", barX + barW / 2, barY + barH / 2); + _tft.setTextColor(COL_WHITE, pulseCol); + _tft.drawString("RELEASE", barX + barW / 2, barY + barH / 2); + return; + } + + float eased = 1.0f - powf(1.0f - progress, 3.0f); + int fillW = max(1, (int)(eased * (float)barW)); + + uint8_t gMin = 16, gMax = 58; + for (int i = 0; i < fillW; i++) { + float frac = (float)i / (float)barW; + uint8_t g = gMin + (uint8_t)(frac * (float)(gMax - gMin)); + _tft.drawFastVLine(barX + i, barY + 1, barH - 2, (uint16_t)(g << 5)); + } + + _tft.drawRoundRect(barX, barY, barW, barH, radius, COL_WHITE); + + _tft.setTextDatum(MC_DATUM); + _tft.setTextFont(2); + _tft.setTextSize(1); + _tft.setTextColor(COL_WHITE, COL_DARK_GRAY); + _tft.drawString("HOLD", barX + barW / 2, barY + barH / 2); } // ===================================================================== @@ -232,7 +241,7 @@ void DisplayManager::drawSilenceProgress(float progress, bool charged) { void DisplayManager::drawCentered(const char* txt, int y, int sz, uint16_t col) { _tft.setTextFont(1); _tft.setTextSize(sz); - _tft.setTextColor(col); + _tft.setTextColor(col, COL_BLACK); int w = _tft.textWidth(txt); _tft.setCursor(max(0, (SCREEN_WIDTH - w) / 2), y); _tft.print(txt); @@ -241,15 +250,16 @@ void DisplayManager::drawCentered(const char* txt, int y, int sz, uint16_t col) void DisplayManager::drawInfoLine(int x, int y, uint16_t col, const char* text) { _tft.setTextFont(1); _tft.setTextSize(1); - _tft.setTextColor(col); + _tft.setTextColor(col, COL_BLACK); _tft.setCursor(x, y); _tft.print(text); } -void DisplayManager::drawHeaderBar(uint16_t col, const char* label, const char* timeStr) { +void DisplayManager::drawHeaderBar(uint16_t col, const char* label, + const char* timeStr) { _tft.setTextFont(1); _tft.setTextSize(2); - _tft.setTextColor(col); + _tft.setTextColor(col, COL_BLACK); _tft.setCursor(8, 8); _tft.print(label); int tw = _tft.textWidth(timeStr); @@ -264,7 +274,11 @@ void DisplayManager::drawBootSplash(const ScreenState& s) { _tft.fillScreen(COL_BLACK); drawCentered("KLUBHAUS", 60, 4, COL_NEON_TEAL); drawCentered("ALERT", 110, 4, COL_HOT_FUCHSIA); - drawCentered("v5.1 E32R35T", 180, 2, COL_DARK_GRAY); + + char verBuf[48]; + snprintf(verBuf, sizeof(verBuf), "v5.1 [%s]", BOARD_NAME); + drawCentered(verBuf, 180, 2, COL_DARK_GRAY); + if (s.debugMode) { drawCentered("DEBUG MODE", 210, 2, COL_YELLOW); } @@ -290,7 +304,7 @@ void DisplayManager::drawWifiFailed() { void DisplayManager::drawAlertScreen(const ScreenState& s) { uint16_t bg = s.blinkPhase ? COL_NEON_TEAL : COL_HOT_FUCHSIA; - uint16_t fg = s.blinkPhase ? COL_BLACK : COL_WHITE; + uint16_t fg = s.blinkPhase ? COL_BLACK : COL_WHITE; _tft.fillScreen(bg); drawHeaderBar(fg, "ALERT", s.timeString); @@ -316,7 +330,6 @@ void DisplayManager::drawAlertScreen(const ScreenState& s) { drawCentered(s.alertMessage, (SCREEN_HEIGHT - 8 * sz) / 2, sz, fg); } - // Show hint text only when NOT interacting if (_holdProgress == 0.0f && !_holdCharged) { drawCentered("HOLD TO SILENCE", SCREEN_HEIGHT - 25, 2, fg); } @@ -331,148 +344,138 @@ void DisplayManager::drawStatusScreen(const ScreenState& s) { int y = 110, sp = 22, x = 20; snprintf(buf, sizeof(buf), "WiFi: %s (%ddBm)", - s.wifiConnected ? s.wifiSSID : "DOWN", s.wifiRSSI); + s.wifiConnected ? s.wifiSSID : "DOWN", s.wifiRSSI); drawInfoLine(x, y, COL_WHITE, buf); y += sp; - snprintf(buf, sizeof(buf), "IP: %s", s.wifiConnected ? s.wifiIP : "---"); + snprintf(buf, sizeof(buf), "IP: %s", + s.wifiConnected ? s.wifiIP : "---"); drawInfoLine(x, y, COL_WHITE, buf); y += sp; snprintf(buf, sizeof(buf), "Up: %lu min Heap: %lu KB", - s.uptimeMinutes, s.freeHeapKB); + s.uptimeMinutes, s.freeHeapKB); drawInfoLine(x, y, COL_WHITE, buf); y += sp; snprintf(buf, sizeof(buf), "NTP: %s UTC", - s.ntpSynced ? s.timeString : "not synced"); + s.ntpSynced ? s.timeString : "not synced"); drawInfoLine(x, y, COL_WHITE, buf); y += sp; - const char* stName = s.deviceState == DeviceState::SILENT ? "SILENT" : - s.deviceState == DeviceState::ALERTING ? "ALERTING" : "WAKE"; + const char* stName = s.deviceState == DeviceState::SILENT ? "SILENT" : + s.deviceState == DeviceState::ALERTING ? "ALERTING" : + "WAKE"; snprintf(buf, sizeof(buf), "State: %s Net: %s", - stName, s.networkOK ? "OK" : "FAIL"); - uint16_t stCol = s.deviceState == DeviceState::ALERTING ? COL_RED : - s.deviceState == DeviceState::SILENT ? COL_GREEN : COL_NEON_TEAL; + stName, s.networkOK ? "OK" : "FAIL"); + uint16_t stCol = s.deviceState == DeviceState::ALERTING ? COL_RED : + s.deviceState == DeviceState::SILENT ? COL_GREEN : + COL_NEON_TEAL; drawInfoLine(x, y, stCol, buf); y += sp; if (s.alertHistoryCount > 0) { - drawInfoLine(x, y, COL_MINT, "Recent Alerts:"); y += sp; + drawInfoLine(x, y, COL_MINT, "Recent Alerts:"); + y += sp; for (int i = 0; i < s.alertHistoryCount; i++) { uint16_t col = (i == 0) ? COL_YELLOW : COL_DARK_GRAY; - snprintf(buf, sizeof(buf), "%s %.35s", - s.alertHistory[i].timestamp, s.alertHistory[i].message); - drawInfoLine(x, y, col, buf); y += sp; + snprintf(buf, sizeof(buf), "%s %.35s", + s.alertHistory[i].timestamp, + s.alertHistory[i].message); + drawInfoLine(x, y, col, buf); + y += sp; } } else { - drawInfoLine(x, y, COL_DARK_GRAY, "No alerts yet"); y += sp; + drawInfoLine(x, y, COL_DARK_GRAY, "No alerts yet"); + y += sp; } drawCentered("tap to dismiss", SCREEN_HEIGHT - 18, 1, COL_DARK_GRAY); } // ===================================================================== -// Hint animation — "nohup" coaching affordance +// Hint animation // ===================================================================== void DisplayManager::startHintCycle() { - _hint = HintAnim{}; // reset everything - _hint.lastPlayMs = millis(); // will wait INITIAL_DELAY before first play + _hint = HintAnim{}; + _hint.lastPlayMs = millis(); } void DisplayManager::stopHint() { - _hint.running = false; + _hint.running = false; } bool DisplayManager::updateHint() { - unsigned long now = millis(); + unsigned long now = millis(); - // If a real hold is active, don't draw hints - // (caller should also just not call this, but belt-and-suspenders) - if (_holdActive) { - _hint.running = false; - return false; - } + if (_holdActive) { + _hint.running = false; + return false; + } - // ── Not currently animating: check if it's time to start ── - if (!_hint.running) { - unsigned long gap = _hint.lastPlayMs == 0 - ? HintAnim::INITIAL_DELAY - : HintAnim::REPEAT_DELAY; - - if (now - _hint.lastPlayMs >= gap) { - _hint.running = true; - _hint.startMs = now; - } else { - return false; - } - } - - // ── Currently animating ── - unsigned long elapsed = now - _hint.startMs; - - if (elapsed > _hint.totalDur()) { - // Animation complete — clear bar and schedule next - _hint.running = false; - _hint.lastPlayMs = now; - drawSilenceProgress(0.0f, false); // clear to empty track - return true; - } - - float progress = 0.0f; - - if (elapsed < HintAnim::FILL_DUR) { - // Phase 1: ease-in quadratic fill to peak - float t = (float)elapsed / (float)HintAnim::FILL_DUR; - progress = HintAnim::PEAK * (t * t); // ease-in - - } else if (elapsed < HintAnim::FILL_DUR + HintAnim::HOLD_DUR) { - // Phase 2: dwell at peak - progress = HintAnim::PEAK; + if (!_hint.running) { + unsigned long gap = _hint.lastPlayMs == 0 + ? HintAnim::INITIAL_DELAY + : HintAnim::REPEAT_DELAY; + if (now - _hint.lastPlayMs >= gap) { + _hint.running = true; + _hint.startMs = now; } else { - // Phase 3: ease-out quadratic drain - float t = (float)(elapsed - HintAnim::FILL_DUR - HintAnim::HOLD_DUR) - / (float)HintAnim::DRAIN_DUR; - progress = HintAnim::PEAK * (1.0f - t * t); // ease-out + return false; } + } - drawHintBar(progress); + unsigned long elapsed = now - _hint.startMs; + + if (elapsed > _hint.totalDur()) { + _hint.running = false; + _hint.lastPlayMs = now; + drawSilenceProgress(0.0f, false); return true; + } + + float progress = 0.0f; + + if (elapsed < HintAnim::FILL_DUR) { + float t = (float)elapsed / (float)HintAnim::FILL_DUR; + progress = HintAnim::PEAK * (t * t); + + } else if (elapsed < HintAnim::FILL_DUR + HintAnim::HOLD_DUR) { + progress = HintAnim::PEAK; + + } else { + float t = (float)(elapsed - HintAnim::FILL_DUR - HintAnim::HOLD_DUR) + / (float)HintAnim::DRAIN_DUR; + progress = HintAnim::PEAK * (1.0f - t * t); + } + + drawHintBar(progress); + return true; } void DisplayManager::drawHintBar(float progress) { - const int barX = 20; - const int barY = SCREEN_HEIGHT - 50; - const int barW = SCREEN_WIDTH - 40; - const int barH = 26; - const int radius = 6; + const int barX = 20; + const int barY = SCREEN_HEIGHT - 50; + const int barW = SCREEN_WIDTH - 40; + const int barH = 26; + const int radius = 6; - // Background track - _tft.fillRoundRect(barX, barY, barW, barH, radius, COL_DARK_GRAY); + _tft.fillRoundRect(barX, barY, barW, barH, radius, COL_DARK_GRAY); - if (progress > 0.001f) { - int fillW = max(1, (int)(progress * (float)barW)); + if (progress > 0.001f) { + int fillW = max(1, (int)(progress * (float)barW)); - // Ghost gradient: muted teal/cyan instead of green - // RGB565 pure-ish teal: low red, mid green, mid blue - for (int i = 0; i < fillW; i++) { - float frac = (float)i / (float)barW; - uint8_t g = 12 + (uint8_t)(frac * 18.0f); // green 12–30 (muted) - uint8_t b = 8 + (uint8_t)(frac * 10.0f); // blue 8–18 (teal tint) - uint16_t col = (g << 5) | b; - _tft.drawFastVLine(barX + i, barY + 1, barH - 2, col); - } + for (int i = 0; i < fillW; i++) { + float frac = (float)i / (float)barW; + uint8_t g = 12 + (uint8_t)(frac * 18.0f); + uint8_t b = 8 + (uint8_t)(frac * 10.0f); + uint16_t col = (g << 5) | b; + _tft.drawFastVLine(barX + i, barY + 1, barH - 2, col); } + } - _tft.drawRoundRect(barX, barY, barW, barH, radius, COL_GRAY); // muted border (not white) + _tft.drawRoundRect(barX, barY, barW, barH, radius, COL_DARK_GRAY); - // Ghost label - _tft.setTextDatum(MC_DATUM); - _tft.setTextFont(2); - _tft.setTextSize(1); - _tft.setTextColor(COL_GRAY, COL_DARK_GRAY); - _tft.drawString("HOLD TO SILENCE", barX + barW / 2, barY + barH / 2); -} - -float DisplayManager::holdProgress() const { - if (!_holdActive) return 0.0f; - return constrain((float)(millis() - _holdStartMs) / (float)HOLD_DURATION_MS, 0.0f, 1.0f); + _tft.setTextDatum(MC_DATUM); + _tft.setTextFont(2); + _tft.setTextSize(1); + _tft.setTextColor(COL_DARK_GRAY, COL_DARK_GRAY); + _tft.drawString("HOLD TO SILENCE", barX + barW / 2, barY + barH / 2); } diff --git a/sketches/doorbell-touch-esp32-32e/DisplayManager.h b/sketches/doorbell-touch-esp32-32e/DisplayManager.h index 66fcdbb..e6bdaef 100644 --- a/sketches/doorbell-touch-esp32-32e/DisplayManager.h +++ b/sketches/doorbell-touch-esp32-32e/DisplayManager.h @@ -1,37 +1,37 @@ #pragma once -#include + +#include "DisplayDriver.h" #include "ScreenData.h" #include "Dashboard.h" // Hold gesture result struct HoldState { - bool active = false; // finger currently down - bool charged = false; // hold duration met, waiting for release - bool completed = false; // finger RELEASED after being charged → act now - bool cancelled = false; // finger released before charge completed + bool active = false; + bool charged = false; + bool completed = false; + bool cancelled = false; uint16_t x = 0; uint16_t y = 0; unsigned long holdMs = 0; unsigned long targetMs = 0; - float progress = 0.0f; // 0.0 to 1.0 + float progress = 0.0f; }; -// Add near the top, alongside existing structs +// Hint animation state struct HintAnim { - bool running = false; - unsigned long startMs = 0; - unsigned long lastPlayMs = 0; + bool running = false; + unsigned long startMs = 0; + unsigned long lastPlayMs = 0; - // Timing (ms) - static const unsigned long INITIAL_DELAY = 1500; // pause before first hint - static const unsigned long FILL_DUR = 400; // ease-in to peak - static const unsigned long HOLD_DUR = 250; // dwell at peak - static const unsigned long DRAIN_DUR = 500; // ease-out back to 0 - static const unsigned long REPEAT_DELAY = 5000; // gap before replaying + static const unsigned long INITIAL_DELAY = 1500; + static const unsigned long FILL_DUR = 400; + static const unsigned long HOLD_DUR = 250; + static const unsigned long DRAIN_DUR = 500; + static const unsigned long REPEAT_DELAY = 5000; - static constexpr float PEAK = 0.35f; // fill to 35% + static constexpr float PEAK = 0.35f; - unsigned long totalDur() const { return FILL_DUR + HOLD_DUR + DRAIN_DUR; } + unsigned long totalDur() const { return FILL_DUR + HOLD_DUR + DRAIN_DUR; } }; class DisplayManager { @@ -44,31 +44,32 @@ public: int dashboardTouch(uint16_t x, uint16_t y); HoldState updateHold(unsigned long requiredMs); - void startHintCycle(); // call when entering alert screen - void stopHint(); // call when real hold begins or leaving alert - bool updateHint(); // call each loop; returns true if it drew something - + void startHintCycle(); + void stopHint(); + bool updateHint(); + float holdProgress() const; private: - HintAnim _hint; - void drawHintBar(float progress); // ← add this - TFT_eSPI _tft; - Dashboard _dash; + HintAnim _hint; + void drawHintBar(float progress); - ScreenID _lastScreen = ScreenID::BOOT_SPLASH; - bool _needsFullRedraw = true; - bool _lastBlink = false; - bool _dashSpriteReady = false; + Gfx _tft; + Dashboard _dash; + + ScreenID _lastScreen = ScreenID::BOOT_SPLASH; + bool _needsFullRedraw = true; + bool _lastBlink = false; + bool _dashSpriteReady = false; unsigned long _lastDashRefresh = 0; // Hold tracking - bool _holdActive = false; - bool _holdCharged = false; // bar is full, waiting for release + bool _holdActive = false; + bool _holdCharged = false; unsigned long _holdStartMs = 0; - unsigned long _holdChargeMs = 0; // when charge completed - uint16_t _holdX = 0; - uint16_t _holdY = 0; - float _holdProgress = 0.0f; + unsigned long _holdChargeMs = 0; + uint16_t _holdX = 0; + uint16_t _holdY = 0; + float _holdProgress = 0.0f; // Colors static constexpr uint16_t COL_NEON_TEAL = 0x07D7; @@ -96,4 +97,3 @@ private: void drawInfoLine(int x, int y, uint16_t col, const char* text); void drawHeaderBar(uint16_t col, const char* label, const char* timeStr); }; -