From fc1a14b5858951d545a9b41149d1e3d7a5270cfd Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 14:16:01 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`feature?= =?UTF-8?q?/gc9a01-display-usermod`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @srg74. * https://github.com/wled/WLED/pull/4989#issuecomment-3763889073 The following files were modified: * `usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp` * `usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h` * `usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp` --- .../usermod_v2_gc9a01_display.cpp | 1223 +++++++++++++++++ .../usermod_v2_gc9a01_display.h | 276 ++++ .../usermod_v2_rotary_encoder_ui_ALT.cpp | 470 ++++++- 3 files changed, 1932 insertions(+), 37 deletions(-) create mode 100644 usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp create mode 100644 usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h diff --git a/usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp b/usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp new file mode 100644 index 0000000000..dcb7dd3735 --- /dev/null +++ b/usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp @@ -0,0 +1,1223 @@ +// WLED usermod for GC9A01 240x240 TFT display +// Designed to work with TFT_eSPI library and compatible displays +// Inspired by 4-line display usermod and rotary encoder usermod +// Written by AI copilot based on user request + +#include "usermod_v2_gc9a01_display.h" +#include "logo_data.h" + +#ifdef USERMOD_GC9A01_DISPLAY + +// Static instance definition for singleton pattern +UsermodGC9A01Display* UsermodGC9A01Display::instance = nullptr; + +/** + * @brief Initialize the GC9A01 TFT display and show the welcome logo. + * + * If the display is disabled this function returns immediately. When enabled, + * it configures the backlight pin (if TFT_BL is defined), initializes the + * TFT controller, applies rotation according to the flip setting, clears the + * screen, applies the current backlight brightness, marks the welcome screen + * as active (sets showingWelcomeScreen and welcomeScreenStartTime), and + * renders the centered WLED logo. + */ +void UsermodGC9A01Display::initDisplay() { + if (!displayEnabled) return; + + DEBUG_PRINTLN(F("[GC9A01] Initializing TFT display...")); + + // Configure backlight pin + #ifdef TFT_BL + pinMode(TFT_BL, OUTPUT); + digitalWrite(TFT_BL, HIGH); + DEBUG_PRINTF("[GC9A01] Backlight pin %d set to HIGH\n", TFT_BL); + #endif + + // Initialize TFT + tft.init(); + DEBUG_PRINTLN(F("[GC9A01] TFT init() completed")); + + tft.setRotation(flip ? 2 : 0); // Apply flip setting (0 or 180 degrees) + tft.fillScreen(TFT_BLACK); + setBacklight(backlight); // Apply backlight brightness + + DEBUG_PRINTLN(F("[GC9A01] Display initialization complete")); + + // Set up welcome screen + showingWelcomeScreen = true; + welcomeScreenStartTime = millis(); + drawWLEDLogo(); +} + +/** + * @brief Update the GC9A01 display contents based on current device and overlay state. + * + * This method evaluates welcome-screen timing, overlay activity, brightness/mode/palette/speed/intensity/color changes, + * sleep/clock timeouts, and wakes or sleeps the display as needed before drawing the appropriate screen (welcome logo, + * overlay, main interface, or clock). It also updates internal "known" state used to minimize redraws. + * + * @param forceRedraw If true, forces a full redraw of the main interface regardless of detected state changes. + */ +void UsermodGC9A01Display::redraw(bool forceRedraw) { + if (!displayEnabled) return; + + bool needRedraw = false; + unsigned long now = millis(); + + // Handle welcome screen transition + if (showingWelcomeScreen) { + if (now - welcomeScreenStartTime > 2000) { // Show logo for 2 seconds + showingWelcomeScreen = false; + forceRedraw = true; + DEBUG_PRINTLN(F("[GC9A01] Transitioning from logo to main interface")); + } else { + // Still showing welcome screen - don't redraw + return; + } + } + + // Handle active overlay mode (always takes priority) + if (activeOverlayMode >= 0) { + if (now >= overlayUntil) { + // Overlay has expired - return to main/clock interface + DEBUG_PRINTF("[GC9A01] Overlay mode %d expired - returning to main interface\n", activeOverlayMode); + activeOverlayMode = -1; + overlayUntil = 0; + forceRedraw = true; + needRedraw = true; // Force redraw to show main screen + lastRedraw = now; // Reset sleep timer when returning to main screen + } else { + // Overlay is still active - check if state changed + if (knownBrightness != bri) { + if (displayTurnedOff) needRedraw = true; + else { knownBrightness = bri; drawMainInterface(activeOverlayMode); lastRedraw = now; return; } + } else if (knownMode != effectCurrent) { + if (displayTurnedOff) needRedraw = true; + else { knownMode = effectCurrent; drawMainInterface(activeOverlayMode); lastRedraw = now; return; } + } else if (knownPalette != effectPalette) { + if (displayTurnedOff) needRedraw = true; + else { knownPalette = effectPalette; drawMainInterface(activeOverlayMode); lastRedraw = now; return; } + } else if (knownEffectSpeed != effectSpeed) { + if (displayTurnedOff) needRedraw = true; + else { knownEffectSpeed = effectSpeed; drawMainInterface(activeOverlayMode); lastRedraw = now; return; } + } else if (knownEffectIntensity != effectIntensity) { + if (displayTurnedOff) needRedraw = true; + else { knownEffectIntensity = effectIntensity; drawMainInterface(activeOverlayMode); lastRedraw = now; return; } + } + + if (!needRedraw) return; // Overlay active, no changes, nothing to do + } + } + + // Get current segment colors (same as web UI csl0, csl1, csl2) + uint32_t currentColor = strip.getMainSegment().colors[0]; // csl0 - Primary/FX + uint32_t currentBgColor = strip.getMainSegment().colors[1]; // csl1 - Secondary/BG + uint32_t currentCustomColor = strip.getMainSegment().colors[2]; // csl2 - Tertiary/CS + + // Check for state changes (Four Line Display ALT pattern) + if (forceRedraw) { + needRedraw = true; + } else if (knownMode != effectCurrent || knownPalette != effectPalette) { + if (displayTurnedOff) needRedraw = true; + else { + knownMode = effectCurrent; + knownPalette = effectPalette; + drawMainInterface(-1); + lastRedraw = now; + return; + } + } else if (knownColor != currentColor || knownBgColor != currentBgColor || knownCustomColor != currentCustomColor) { + if (displayTurnedOff) needRedraw = true; + else { + knownColor = currentColor; + knownBgColor = currentBgColor; + knownCustomColor = currentCustomColor; + drawMainInterface(-1); + lastRedraw = now; + return; + } + } else if (knownBrightness != bri) { + if (displayTurnedOff) needRedraw = true; + else { + knownBrightness = bri; + drawMainInterface(-1); + lastRedraw = now; + return; + } + } else if (knownEffectSpeed != effectSpeed) { + if (displayTurnedOff) needRedraw = true; + else { + knownEffectSpeed = effectSpeed; + drawMainInterface(-1); + lastRedraw = now; + return; + } + } else if (knownEffectIntensity != effectIntensity) { + if (displayTurnedOff) needRedraw = true; + else { + knownEffectIntensity = effectIntensity; + drawMainInterface(-1); + lastRedraw = now; + return; + } + } + + // Nothing changed - check what to do + if (!needRedraw) { + // Turn off display after configured timeout (or show clock if enabled) + if (sleepMode && displayTimeout > 0 && !displayTurnedOff && lastRedraw != ULONG_MAX && (now - lastRedraw > displayTimeout)) { + sleepOrClock(true); + } else if (displayTurnedOff && clockMode) { + // Keep updating clock while display is "off" (showing clock) + static unsigned long lastClockUpdate = 0; + if (now - lastClockUpdate > 30000) { + drawClockScreen(); + lastClockUpdate = now; + } + } + return; + } + + // State changed or need to redraw - wake up if sleeping and redraw main screen + lastRedraw = now; + wakeDisplayFromSleep(); + + // Update all known values + knownBrightness = bri; + knownMode = effectCurrent; + knownPalette = effectPalette; + knownEffectSpeed = effectSpeed; + knownEffectIntensity = effectIntensity; + knownColor = currentColor; + knownBgColor = currentBgColor; + knownCustomColor = currentCustomColor; + + // Do full redraw of main screen (not overlay) + drawMainInterface(-1); +} + +/** + * @brief Render the main display UI or a specific overlay on the GC9A01 240x240 TFT. + * + * Renders either the primary interface (time, WiFi, power state, effect name, + * color indicators, and a brightness ring) or a focused overlay showing a + * single adjustable property. When an overlay is active, the function draws + * a themed bezel and overlay title/value (or network information) and returns + * without drawing the normal main-screen elements. + * + * Supported overlayMode values: + * - -1 : normal main interface + * - 0 : Brightness overlay (percentage + brightness arc) + * - 1 : Effect overlay (effect name) + * - 2 : Speed overlay (percentage) + * - 3 : Intensity overlay (percentage) + * - 4 : Palette overlay (palette name) + * - 99 : Network info overlay (IP/MAC and WiFi icon) + * + * @param overlayMode Overlay selector; use -1 for the standard main screen or + * one of the values listed above to render the corresponding overlay. + */ +void UsermodGC9A01Display::drawMainInterface(int overlayMode) { + tft.fillScreen(TFT_BLACK); + + showingClock = false; // Clear clock flag - we're showing main interface + + // Determine bezel color based on mode + uint16_t bezelColor = TFT_BLUE; // Default blue bezel for normal mode + bool showBrightnessRing = true; + bool showAllElements = true; + String overlayTitle = ""; + uint16_t overlayColor = TFT_WHITE; + + // Modify display for overlay modes (>= 0) + if (overlayMode >= 0) { + showAllElements = false; + + // Use the stored overlayText if available, otherwise use default titles + overlayTitle = (overlayText.length() > 0) ? overlayText : "Mode"; + + switch (overlayMode) { + case 0: // Brightness mode + bezelColor = TFT_WHITE; + overlayColor = TFT_WHITE; + showBrightnessRing = true; + break; + case 1: // Effect mode + bezelColor = TFT_CYAN; + overlayColor = TFT_CYAN; + showBrightnessRing = false; + break; + case 2: // Speed mode + bezelColor = TFT_GREEN; + overlayColor = TFT_GREEN; + showBrightnessRing = false; + break; + case 3: // Intensity mode + bezelColor = TFT_ORANGE; + overlayColor = TFT_ORANGE; + showBrightnessRing = false; + break; + case 4: // Palette mode + bezelColor = TFT_MAGENTA; + overlayColor = TFT_MAGENTA; + showBrightnessRing = false; + break; + case 99: // Network info mode + bezelColor = TFT_GREEN; + overlayColor = TFT_GREEN; + showBrightnessRing = false; + showAllElements = false; + break; + } + } + + // Get static segment colors + uint32_t fxColor = strip.getMainSegment().colors[0]; // Primary color + uint8_t fx_r = (fxColor >> 16) & 0xFF; + uint8_t fx_g = (fxColor >> 8) & 0xFF; + uint8_t fx_b = fxColor & 0xFF; + uint16_t fxColor565 = tft.color565(fx_r, fx_g, fx_b); + + uint32_t bgColor = strip.getMainSegment().colors[1]; // Secondary color + uint8_t bg_r = (bgColor >> 16) & 0xFF; + uint8_t bg_g = (bgColor >> 8) & 0xFF; + uint8_t bg_b = bgColor & 0xFF; + uint16_t bgColor565 = tft.color565(bg_r, bg_g, bg_b); + + // Draw outer circle border + tft.drawCircle(120, 120, 115, bezelColor); + tft.drawCircle(120, 120, 114, bezelColor); + + // Special handling for network info + if (overlayMode == 99) { + tft.setTextDatum(MC_DATUM); + bool wifiConnected = (WiFi.status() == WL_CONNECTED); + int wifiRSSI = wifiConnected ? WiFi.RSSI() : -100; + + tft.fillCircle(120, 40, 12, TFT_BLUE); + drawWiFiIcon(120, 36, wifiConnected, wifiRSSI); + + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextDatum(MC_DATUM); + + if (wifiConnected) { + String ipStr = WiFi.localIP().toString(); + tft.drawString(ipStr, 120, 110, 4); + String macStr = WiFi.macAddress(); + tft.drawString(macStr, 120, 140, 2); + } else { + tft.drawString("NOT CONNECTED", 120, 110, 4); + String macStr = WiFi.macAddress(); + tft.drawString(macStr, 120, 140, 2); + } + return; + } + + // Draw brightness ring from 8 o'clock to 4 o'clock (only on main screen or brightness overlay) + int brightnessPercent = (bri > 0) ? map(bri, 0, 255, 0, 100) : 0; + + if (showBrightnessRing && (overlayMode == -1 || overlayMode == 0) && brightnessPercent >= 0 && brightnessPercent <= 100) { + float startAngle = 240; // 8 o'clock + float arcLength = 240; + float progressAngle = (brightnessPercent > 0) ? map(brightnessPercent, 0, 100, 0, arcLength) : 0; + + // Draw background arc + for (float angle = 0; angle < arcLength; angle += 6) { + float currentAngle = startAngle + angle; + float rad = radians(currentAngle - 90); + + for (int ringWidth = 0; ringWidth < 3; ringWidth++) { + int radius = 108 - ringWidth; + int x = 120 + radius * cos(rad); + int y = 120 + radius * sin(rad); + + if (x >= 0 && x < 240 && y >= 0 && y < 240) { + tft.drawPixel(x, y, TFT_DARKGREY); + } + } + } + + // Draw progress arc + if (brightnessPercent > 0 && progressAngle > 0) { + for (float angle = 0; angle < progressAngle; angle += 3) { + float currentAngle = startAngle + angle; + float rad = radians(currentAngle - 90); + + for (int ringWidth = 0; ringWidth < 3; ringWidth++) { + int radius = 108 - ringWidth; + int x = 120 + radius * cos(rad); + int y = 120 + radius * sin(rad); + + if (x >= 0 && x < 240 && y >= 0 && y < 240) { + tft.drawPixel(x, y, TFT_WHITE); + } + } + } + } + } + + // Show overlay-specific content if in overlay mode + if (overlayMode >= 0 && overlayTitle.length() > 0) { + // Draw overlay title at top (white color, moved down 20px total) + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextDatum(MC_DATUM); + tft.drawString(overlayTitle, 120, 70, 4); + + // Get current value and calculate percentage based on overlay mode + int currentValue = 0; + int maxValue = 255; + String valueText = ""; + + switch (overlayMode) { + case 1: // Effect mode + currentValue = effectCurrent; + maxValue = strip.getModeCount() - 1; + if (currentValue < strip.getModeCount()) { + char lineBuffer[64]; + if (extractModeName(currentValue, JSON_mode_names, lineBuffer, 63)) { + // Remove note symbol from effect names (special character prefix) + String modeName = String(lineBuffer); + if (modeName.length() > 0 && modeName.charAt(0) == ' ' && modeName.charAt(1) > 127) { + modeName = modeName.substring(5); // Remove 5-byte UTF-8 note symbol + } + if (modeName.length() > 12) modeName = modeName.substring(0, 10) + ".."; + valueText = modeName; + } else { + valueText = "Effect " + String(currentValue); + } + } + break; + case 2: // Speed mode + currentValue = effectSpeed; + valueText = String(map(effectSpeed, 0, 255, 0, 100)) + "%"; + break; + case 3: // Intensity mode + currentValue = effectIntensity; + valueText = String(map(effectIntensity, 0, 255, 0, 100)) + "%"; + break; + case 4: // Palette mode + currentValue = effectPalette; + maxValue = getPaletteCount() - 1; + if (currentValue < getPaletteCount()) { + char lineBuffer[64]; + if (extractModeName(currentValue, JSON_palette_names, lineBuffer, 63)) { + // Remove "* " prefix from dynamic palettes + String paletteName = String(lineBuffer); + if (paletteName.startsWith("* ")) { + paletteName = paletteName.substring(2); + } + if (paletteName.length() > 12) paletteName = paletteName.substring(0, 10) + ".."; + valueText = paletteName; + } else { + valueText = "Palette " + String(currentValue); + } + } else { + valueText = "Palette " + String(effectPalette); + } + break; + default: // Fallback + currentValue = bri; + valueText = String(map(bri, 0, 255, 0, 100)) + "%"; + break; + } + + // Draw value arc from 8 o'clock to 4 o'clock (only for percentage-based overlays) + // Effect (1) and Palette (4) overlays don't need arcs as they show names + bool showArc = (overlayMode != 1 && overlayMode != 4); + + if (showArc) { + int valuePercent = map(currentValue, 0, maxValue, 0, 100); + float startAngle = 240; // 8 o'clock + float arcLength = 240; + float progressAngle = (valuePercent > 0) ? map(valuePercent, 0, 100, 0, arcLength) : 0; + + // Draw background arc (darkgrey) + for (float angle = 0; angle < arcLength; angle += 6) { + float currentAngle = startAngle + angle; + float rad = radians(currentAngle - 90); + + for (int ringWidth = 0; ringWidth < 3; ringWidth++) { + int radius = 108 - ringWidth; + int x = 120 + radius * cos(rad); + int y = 120 + radius * sin(rad); + + if (x >= 0 && x < 240 && y >= 0 && y < 240) { + tft.drawPixel(x, y, TFT_DARKGREY); + } + } + } + + // Draw progress arc (white progress) + if (valuePercent > 0 && progressAngle > 0) { + for (float angle = 0; angle < progressAngle; angle += 3) { + float currentAngle = startAngle + angle; + float rad = radians(currentAngle - 90); + + for (int ringWidth = 0; ringWidth < 3; ringWidth++) { + int radius = 108 - ringWidth; + int x = 120 + radius * cos(rad); + int y = 120 + radius * sin(rad); + + if (x >= 0 && x < 240 && y >= 0 && y < 240) { + tft.drawPixel(x, y, TFT_WHITE); + } + } + } + } + } + + // Draw value text in center + // Use smaller font (4) for Effect and Palette names, large font (6) for percentages + int fontSize = (overlayMode == 1 || overlayMode == 4) ? 4 : 6; + int yOffset = (overlayMode == 1 || overlayMode == 4) ? 120 : 130; // Slightly higher for smaller font + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextDatum(MC_DATUM); + tft.drawString(valueText, 126, yOffset, fontSize); + + // Don't draw main screen elements - return early + return; + } + + // === Main screen elements (only drawn when NOT in overlay mode) === + + // WiFi icon + tft.setTextDatum(MC_DATUM); + bool wifiConnected = (WiFi.status() == WL_CONNECTED); + int wifiRSSI = wifiConnected ? WiFi.RSSI() : -100; + + tft.fillCircle(120, 40, 12, TFT_BLUE); + drawWiFiIcon(120, 36, wifiConnected, wifiRSSI); + + // Time display + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextDatum(MC_DATUM); + + String timeStr = "--:--"; + if (localTime != 0) { + uint8_t currentHour = hour(localTime); + uint8_t currentMinute = minute(localTime); + + timeStr = ""; + if (currentHour < 10) timeStr += "0"; + timeStr += String(currentHour); + timeStr += ":"; + if (currentMinute < 10) timeStr += "0"; + timeStr += String(currentMinute); + } + tft.drawString(timeStr, 120, 95, 6); + + // Power switch + bool powerState = (bri > 0); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextDatum(MC_DATUM); + + if (powerState) { + tft.fillRoundRect(105, 123, 30, 15, 7, TFT_GREEN); + tft.fillCircle(125, 130, 6, TFT_WHITE); + tft.drawString("ON", 150, 130, 2); + } else { + tft.fillRoundRect(105, 123, 30, 15, 7, TFT_RED); + tft.fillCircle(115, 130, 6, TFT_WHITE); + tft.drawString("OFF", 90, 130, 2); + } + + // CSL2 button (tertiary color) + uint32_t csl2Color = strip.getMainSegment().colors[2]; + uint8_t csl2_r = (csl2Color >> 16) & 0xFF; + uint8_t csl2_g = (csl2Color >> 8) & 0xFF; + uint8_t csl2_b = csl2Color & 0xFF; + uint16_t csl2Color565 = tft.color565(csl2_r, csl2_g, csl2_b); + + tft.setTextDatum(TL_DATUM); + tft.drawString("CS", 94, 154, 2); + tft.setTextDatum(MC_DATUM); + tft.fillCircle(120, 162, 8, csl2Color565); + tft.drawCircle(120, 162, 8, TFT_WHITE); + + // Effect name + tft.setTextColor(TFT_CYAN, TFT_BLACK); + String effectName = ""; + if (currentPlaylist >= 0) { + effectName = "Playlist"; + } else if (knownMode < strip.getModeCount() && knownMode >= 0) { + char modeBuffer[64]; + strncpy_P(modeBuffer, strip.getModeData(knownMode), sizeof(modeBuffer)-1); + modeBuffer[sizeof(modeBuffer)-1] = '\0'; + + char* sepPtr = strpbrk(modeBuffer, "@;,|="); + if (sepPtr) *sepPtr = '\0'; + + effectName = String(modeBuffer); + + String cleanName = ""; + for (int i = 0; i < effectName.length(); i++) { + char c = effectName.charAt(i); + if (c >= 32 && c <= 126 && c != '@') { + cleanName += c; + } + } + effectName = cleanName; + + if (effectName.length() == 0) { + effectName = "Effect " + String(knownMode); + } + } else { + effectName = "Unknown"; + } + + if (effectName.length() > 12) { + effectName = effectName.substring(0, 9) + "..."; + } + tft.drawString(effectName, 120, 190, 2); + + // Color indicators + tft.setTextColor(TFT_WHITE, TFT_BLACK); + + // FX indicator at 7:30 position + int fx_x = 120 + 90 * cos(radians(225 - 90)); + int fx_y = 120 + 90 * sin(radians(225 - 90)); + tft.drawString("FX", fx_x, fx_y - 15, 2); + tft.fillCircle(fx_x, fx_y, 8, fxColor565); + tft.drawCircle(fx_x, fx_y, 8, TFT_WHITE); + + // BG indicator at 4:30 position + int bg_x = 120 + 90 * cos(radians(135 - 90)); + int bg_y = 120 + 90 * sin(radians(135 - 90)); + tft.drawString("BG", bg_x, bg_y - 15, 2); + tft.fillCircle(bg_x, bg_y, 8, bgColor565); + tft.drawCircle(bg_x, bg_y, 8, TFT_WHITE); + + // Brightness percentage + tft.setTextColor(TFT_WHITE, TFT_BLACK); + String brightStr = String(brightnessPercent) + "%"; + tft.drawString(brightStr, 120, 210, 2); +} + +/** + * @brief Draws a four-bar WiFi signal icon at the given screen coordinates. + * + * Draws four vertical bars of increasing height representing signal strength and, + * when not connected, overlays a red "X". RSSI (in dBm) is mapped to 0–4 bars: + * ≥ -50 → 4, ≥ -60 → 3, ≥ -70 → 2, ≥ -80 → 1, otherwise 0. + * + * @param x X coordinate of the icon's center baseline (pixels). + * @param y Y coordinate of the icon's top baseline (pixels). + * @param connected If true, active bars are drawn in white and inactive in dark gray; if false, all bars are dark gray and a red "X" is drawn. + * @param rssi WiFi signal strength in dBm (used only when `connected` is true). + */ + +void UsermodGC9A01Display::drawWiFiIcon(int x, int y, bool connected, int rssi) { + // Draw WiFi icon as signal strength bars (like mobile phone signal) + // More recognizable than arcs on small displays + + uint16_t strongColor, weakColor; + int signalStrength = 0; + + if (connected) { + strongColor = TFT_WHITE; + weakColor = TFT_DARKGREY; + + // Convert RSSI to signal strength (0-4 bars) + if (rssi >= -50) signalStrength = 4; // Excellent (100%) + else if (rssi >= -60) signalStrength = 3; // Good (75%) + else if (rssi >= -70) signalStrength = 2; // Fair (50%) + else if (rssi >= -80) signalStrength = 1; // Poor (25%) + else signalStrength = 0; // Very poor + } else { + strongColor = TFT_DARKGREY; + weakColor = TFT_DARKGREY; + signalStrength = 0; + } + + // Draw 4 signal bars of increasing height (like phone signal indicator) + // Bar 1 (shortest, leftmost) + uint16_t bar1Color = (signalStrength >= 1) ? strongColor : weakColor; + tft.fillRect(x - 6, y + 6, 2, 2, bar1Color); + + // Bar 2 + uint16_t bar2Color = (signalStrength >= 2) ? strongColor : weakColor; + tft.fillRect(x - 3, y + 4, 2, 4, bar2Color); + + // Bar 3 + uint16_t bar3Color = (signalStrength >= 3) ? strongColor : weakColor; + tft.fillRect(x, y + 2, 2, 6, bar3Color); + + // Bar 4 (tallest, rightmost) + uint16_t bar4Color = (signalStrength >= 4) ? strongColor : weakColor; + tft.fillRect(x + 3, y, 2, 8, bar4Color); + + // Draw X if disconnected + if (!connected) { + tft.drawLine(x - 6, y, x + 6, y + 8, TFT_RED); + tft.drawLine(x + 6, y, x - 6, y + 8, TFT_RED); + } +} + +/** + * @brief Render the WLED logo centered on the 240x240 display. + * + * Clears the screen to black and draws the 120x120 WLED logo centered (60px offset on each side). + * The bitmap is read from program memory (epd_bitmap_), converted from 32-bit ARGB888 to 16-bit RGB565, + * and streamed to the display within a single draw window. + */ +void UsermodGC9A01Display::drawWLEDLogo() { + // Display the WLED logo bitmap centered on the display + // The bitmap is 120x120 pixels, centered on 240x240 display (60px offset on each side) + + DEBUG_PRINTLN(F("[GC9A01] Drawing WLED logo bitmap...")); + + // Clear screen with black background + tft.fillScreen(TFT_BLACK); + + // Calculate center position for 120x120 logo on 240x240 display + const int LOGO_SIZE = 120; + const int OFFSET_X = (240 - LOGO_SIZE) / 2; // 60px offset + const int OFFSET_Y = (240 - LOGO_SIZE) / 2; // 60px offset + + // Set drawing window to the logo area (centered) + tft.setAddrWindow(OFFSET_X, OFFSET_Y, LOGO_SIZE, LOGO_SIZE); + + // Start data transmission + tft.startWrite(); + + // Process each pixel in the bitmap + for (int i = 0; i < 14400; i++) { // 120 * 120 = 14,400 pixels + uint32_t pixel = pgm_read_dword(&epd_bitmap_[i]); + + // Extract RGB components from 32-bit ARGB (format: 0x00RRGGBB) + uint8_t r = (pixel >> 16) & 0xFF; + uint8_t g = (pixel >> 8) & 0xFF; + uint8_t b = pixel & 0xFF; + + // Convert to 16-bit RGB565 format + uint16_t color = tft.color565(r, g, b); + + // Write pixel to display + tft.pushColor(color); + } + + // End data transmission + tft.endWrite(); + + DEBUG_PRINTLN(F("[GC9A01] WLED logo bitmap rendered successfully - 120x120 centered")); +} + +/** + * @brief Accepts a legacy 0–255 brightness value and applies it to the display backlight. + * + * Maps the 0–255 input range to a 0–100% backlight level and updates the display backlight. + * + * @param bri Brightness in the legacy 0–255 scale. + */ +void UsermodGC9A01Display::setBrightness(uint8_t bri) { + // Legacy method - map 0-255 brightness to 0-100% backlight + uint8_t percent = map(bri, 0, 255, 0, 100); + setBacklight(percent); +} + +/** + * @brief Set the display backlight brightness as a percentage. + * + * Clamps the provided percentage to the 0–100 range, converts it to an 8-bit + * PWM value, and writes that PWM value to the configured backlight pin. + * + * @param percent Desired backlight level (0-100). Values outside this range are clamped. + */ +void UsermodGC9A01Display::setBacklight(uint8_t percent) { + backlight = min(percent, (uint8_t)100); // Clamp to 0-100% range + uint8_t pwm = map(backlight, 0, 100, 0, 255); // Map to PWM range (0-255) + #ifdef TFT_BL + analogWrite(TFT_BL, pwm); + #endif + DEBUG_PRINTF("[GC9A01] Backlight set to %d%% (PWM: %d)\n", backlight, pwm); +} + +/** + * @brief Render a minimalist full-screen clock interface and Wi‑Fi status on the TFT. + * + * Clears the display and draws a centered large-format time using the configured + * 12/24-hour mode, an optional AM/PM indicator (when 12-hour mode is enabled), + * a two-ring blue bezel, and a Wi‑Fi signal icon with current RSSI at the top. + * + * Side effects: + * - Marks the usermod as showing the clock (updates `showingClock`). + * - Updates local time before rendering. + */ +void UsermodGC9A01Display::drawClockScreen() { + // Minimalist clock-only display for idle mode + tft.fillScreen(TFT_BLACK); + + showingClock = true; // Mark that we're showing clock (for rotary encoder state reset) + + // Draw blue bezel circle (same as main screen) + tft.drawCircle(120, 120, 119, TFT_BLUE); + tft.drawCircle(120, 120, 118, TFT_BLUE); + + // Get current time + updateLocalTime(); + int hrs = hour(localTime); + int mins = minute(localTime); + bool isPmTime = isPM(localTime); + + // Convert to 12-hour format if needed + if (clock12hour) { + if (hrs == 0) { + hrs = 12; // Midnight = 12 AM + } else if (hrs > 12) { + hrs = hrs - 12; // Convert to 12-hour + } + } + + // Draw time in large font at center + tft.setTextDatum(MC_DATUM); // Middle center + tft.setTextColor(TFT_WHITE, TFT_BLACK); + + // Format time string + char timeStr[10]; + sprintf(timeStr, "%02d:%02d", hrs, mins); + + tft.drawString(timeStr, 120, 120, 7); // Large font + + // Draw AM/PM indicator if 12-hour format + if (clock12hour) { + tft.setTextColor(TFT_CYAN, TFT_BLACK); + tft.drawString(isPmTime ? "PM" : "AM", 120, 160, 4); + } + + // Draw WiFi icon at top + bool wifiConnected = (WiFi.status() == WL_CONNECTED); + int wifiRSSI = wifiConnected ? WiFi.RSSI() : -100; + drawWiFiIcon(120, 30, wifiConnected, wifiRSSI); + + DEBUG_PRINTLN(F("[GC9A01] Clock screen drawn")); +} + +/** + * @brief Enter sleep/clock mode or wake the display. + * + * If enabled is true, marks the display as turned off and either shows the clock + * (when clockMode is enabled) or turns the panel backlight off. If enabled is + * false, wakes the display, clears sleep/clock state, and restores the backlight + * to the configured brightness. + * + * @param enabled true to enter sleep/clock mode, false to wake the display. + */ +void UsermodGC9A01Display::sleepOrClock(bool enabled) { + if (enabled) { + displayTurnedOff = true; + lastRedraw = ULONG_MAX; // Reset sleep timer + if (clockMode) { + // Show clock instead of turning off + showingClock = true; + drawClockScreen(); + DEBUG_PRINTLN(F("[GC9A01] Timeout reached - showing clock")); + } else { + // Turn off backlight (sleep) + showingClock = false; + #ifdef TFT_BL + analogWrite(TFT_BL, 0); + #endif + DEBUG_PRINTLN(F("[GC9A01] Timeout reached - sleeping (backlight off)")); + } + } else { + // Wake up display + displayTurnedOff = false; + showingClock = false; + setBacklight(backlight); + DEBUG_PRINTF("[GC9A01] Display waking - restoring backlight to %d%%\n", backlight); + } +} + +/** + * @brief Put the display into a sleeping state and turn off its backlight. + * + * Marks the display as turned off and resets the internal sleep timer so it + * will restart after the next interaction. If a backlight pin is defined, + * the backlight PWM is set to zero. + */ +void UsermodGC9A01Display::sleepDisplay() { + #ifdef TFT_BL + analogWrite(TFT_BL, 0); // Turn off backlight completely + #endif + displayTurnedOff = true; + lastRedraw = ULONG_MAX; // Reset sleep timer - will start again after next interaction + DEBUG_PRINTLN(F("[GC9A01] Display sleeping - backlight off, sleep timer reset")); +} + +/** + * @brief Wakes the display if it is currently turned off and restores its visible state. + * + * If the display was turned off, this restores the configured backlight level, clears the + * turned-off flag, requests a redraw and resets the sleep timeout timer. It also forces + * refresh of tracked state values so the next redraw updates all on-screen indicators. + * + * @return `true` if the display was sleeping and was woken; `false` if the display was already awake. + */ + +bool UsermodGC9A01Display::wakeDisplayFromSleep() { + if (displayTurnedOff) { + setBacklight(backlight); // Restore configured backlight level + displayTurnedOff = false; + needsRedraw = true; + lastRedraw = millis(); // Reset sleep timeout to prevent immediate sleep + + // Force update of all known values to ensure display shows current state + knownBrightness = 255; // Force brightness update + knownMode = 255; // Force effect update + knownPowerState = !knownPowerState; // Force power state update + + DEBUG_PRINTF("[GC9A01] Display waking from sleep - restoring backlight to %d%%\n", backlight); + return true; // Was sleeping + } + return false; // Was already awake +} + +/** + * @brief Wakes the display if it is currently sleeping. + * + * Restores display state and backlight when the display is asleep. + * + * @return `true` if the display was sleeping and has been woken, `false` otherwise. + */ +bool UsermodGC9A01Display::wakeDisplay() { + return wakeDisplayFromSleep(); // Return true if was sleeping +} + +/** + * @brief Refreshes the overlay timeout and prevents the display from sleeping after encoder activity. + * + * If an overlay is currently active, extends its expiration to now plus the configured + * overlay inactivity timeout and requests a redraw. Always updates the last-redraw + * timestamp to prevent the display sleep timeout. + */ +void UsermodGC9A01Display::updateRedrawTime() { + // Called when encoder is rotated - extend overlay timeout + if (activeOverlayMode >= 0) { + // We're in overlay mode - extend the timeout + overlayUntil = millis() + overlayInactivityTimeout; + DEBUG_PRINTF("[GC9A01] Overlay timeout extended to %lu (encoder activity detected)\n", overlayUntil); + needsRedraw = true; // Trigger redraw to show updated values + } + lastRedraw = millis(); // Prevent display sleep timeout +} + +/** + * @brief Activate an on-screen overlay and render it immediately. + * + * Sets and displays an overlay with the provided text for a limited time, optionally waking + * the display if it was sleeping. The overlay mode is derived from the supplied glyphType, + * the overlay timeout is extended for user interaction, and an immediate redraw of the UI + * is triggered. + * + * @param line1 Text to show on the overlay (may be null). + * @param showHowLong Requested display duration in milliseconds (caller hint; actual timeout uses the usermod's interaction timeout). + * @param glyphType Glyph identifier used to choose the overlay mode: + * - 1 → Brightness + * - 2 → Speed + * - 3 → Intensity + * - 4 → Palette + * - 5 → Effect + * - 7,8,10,11 and other mapped values → treated as Brightness (UI proxy) + * - 12 → Network information + */ +void UsermodGC9A01Display::overlay(const char* line1, long showHowLong, byte glyphType) { + if (!displayEnabled) { + DEBUG_PRINTLN(F("[GC9A01] Overlay called but display disabled")); + return; + } + + DEBUG_PRINTF("[GC9A01] *** OVERLAY CALLED *** : '%s' for %ld ms (glyph: %d)\n", line1 ? line1 : "NULL", showHowLong, glyphType); + + // Wake display if sleeping (like 4-line display) + if (displayTurnedOff) { + wakeDisplayFromSleep(); + DEBUG_PRINTLN(F("[GC9A01] Display was sleeping - woken up for overlay")); + } + + // Map glyph type to overlay mode + // Rotary encoder states: 0=Brightness, 1=Speed, 2=Intensity, 3=Palette, 4=Effect, 5=Hue, 6=Sat, 7=CCT, 8=Preset, 9-11=Custom + int overlayMode = 0; // Default to brightness + + // Map glyphType to overlay modes based on rotary encoder glyph assignments + // Rotary encoder calls changeState() with these glyph types for each state + switch (glyphType) { + case 1: overlayMode = 0; break; // Sun glyph → Brightness (encoder state 0) + case 2: overlayMode = 2; break; // Skip forward glyph → Speed (encoder state 1) + case 3: overlayMode = 3; break; // Fire glyph → Intensity (encoder state 2) + case 4: overlayMode = 4; break; // Custom palette glyph → Palette (encoder state 3) + case 5: overlayMode = 1; break; // Puzzle piece glyph → Effect (encoder state 4) + case 7: overlayMode = 0; break; // Brush glyph → Hue (encoder state 5) - show as brightness + case 8: overlayMode = 0; break; // Contrast glyph → Saturation (encoder state 6) - show as brightness + case 10: overlayMode = 0; break; // Star glyph → CCT/custom (encoder states 7,9-11) - show as brightness + case 11: overlayMode = 0; break; // Heart glyph → Preset (encoder state 8) - show as brightness + case 12: overlayMode = 99; break; // Network glyph → Network info + default: overlayMode = 0; break; // Fallback to brightness + } + + // Activate overlay mode and store the text + activeOverlayMode = overlayMode; + overlayText = String(line1 ? line1 : ""); // Store the text for display + overlayUntil = millis() + overlayInactivityTimeout; // Use longer timeout for user interaction + lastRedraw = millis(); // Start/reset sleep timer on user interaction + + // Draw the overlay interface immediately + drawMainInterface(overlayMode); + + DEBUG_PRINTF("[GC9A01] Overlay mode %d activated with text '%s' - will expire at %lu\n", overlayMode, overlayText.c_str(), overlayUntil); +} + +/** + * @brief Initializes the GC9A01 display usermod and primes internal state for the first redraw. + * + * Initializes display hardware and configuration, marks the display as needing an initial redraw, records the initial redraw timestamp, and sets sentinel values for known mode and brightness to force the first UI update. + */ +void UsermodGC9A01Display::setup() { + DEBUG_PRINTLN(F("")); + DEBUG_PRINTLN(F("=== GC9A01 Display Usermod ===")); + DEBUG_PRINTLN(F("[GC9A01] Usermod successfully registered and setup() called")); + DEBUG_PRINTF("[GC9A01] Usermod ID: %d\n", getId()); + DEBUG_PRINTF("[GC9A01] Instance pointer: %p\n", this); + DEBUG_PRINTF("[GC9A01] Static instance: %p\n", instance); + DEBUG_PRINT(F("[GC9A01] TFT_eSPI library version: ")); + DEBUG_PRINTLN(TFT_ESPI_VERSION); + + initDisplay(); + + DEBUG_PRINTLN(F("[GC9A01] Display initialization complete")); + needsRedraw = true; + lastRedraw = millis(); // Initialize timeout tracking + + // Initialize known values to force initial update + knownMode = 255; // Force initial effect name update + knownBrightness = 255; // Force initial brightness update +} + +/** + * @brief Periodically checks whether the display needs updating and triggers a redraw when due. + * + * Checks display enablement and strip update state, enforces the refresh interval, advances the internal + * next-update timestamp, and invokes redraw for the display. + */ +void UsermodGC9A01Display::loop() { + // Follow the proven pattern from Four Line Display ALT + if (!displayEnabled || strip.isUpdating()) return; + + unsigned long now = millis(); + if (now < nextUpdate) return; + + nextUpdate = now + refreshRate; + + redraw(false); +} + +/** + * @brief Appends GC9A01 display status information to the provided JSON info object. + * + * Adds an entry under the top-level "u" object with an array labeled "GC9A01 Display" + * containing the display state ("Enabled" or "Disabled") and a placeholder string. + * + * @param root JSON object to augment; a nested "u" object is created if missing and the + * "GC9A01 Display" array is appended under it. + */ +void UsermodGC9A01Display::addToJsonInfo(JsonObject& root) { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray temp = user.createNestedArray(F("GC9A01 Display")); + temp.add(displayEnabled ? F("Enabled") : F("Disabled")); + temp.add(F(" ")); +} + +/** + * @brief Update display enabled state from a JSON state object. + * + * Reads the "gc9a01.on" boolean value from the provided JSON object and, if different + * from the current state, updates the internal displayEnabled flag and either wakes + * or sleeps the display accordingly. + * + * @param root JSON object containing the "gc9a01" state subtree (expects "gc9a01.on"). + */ +void UsermodGC9A01Display::readFromJsonState(JsonObject& root) { + if (root[F("gc9a01")] != nullptr) { + if (root[F("gc9a01")][F("on")] != nullptr) { + bool newState = root[F("gc9a01")][F("on")]; + if (newState != displayEnabled) { + displayEnabled = newState; + if (displayEnabled) { + wakeDisplay(); + } else { + sleepDisplay(); + } + } + } + } +} + +/** + * @brief Appends the GC9A01 display runtime state to the given JSON object. + * + * Adds a nested "gc9a01" object containing the "on" property set to the current displayEnabled value. + * + * @param root JSON object to which the display state will be added. + */ +void UsermodGC9A01Display::addToJsonState(JsonObject& root) { + JsonObject gc9a01 = root.createNestedObject(F("gc9a01")); + gc9a01[F("on")] = displayEnabled; +} + +/** + * @brief Loads GC9A01 display configuration from the given JSON object and applies it. + * + * Reads the "GC9A01" object and updates displayEnabled, sleepMode, clockMode, + * clock12hour, flip, displayTimeout (converts seconds to milliseconds and clamps to 5–300 seconds), + * and backlight. If the backlight value changes, setBacklight is invoked. + * + * @param root Root JSON object expected to contain a "GC9A01" object with configuration keys. + * @return true if a "GC9A01" configuration object was present and processed, false if it was missing. + */ +bool UsermodGC9A01Display::readFromConfig(JsonObject& root) { + JsonObject top = root[FPSTR("GC9A01")]; + if (top.isNull()) { + return false; + } + + displayEnabled = top[FPSTR("enabled")] | displayEnabled; + sleepMode = top[FPSTR("sleepMode")] | sleepMode; + clockMode = top[FPSTR("clockMode")] | clockMode; + clock12hour = top[FPSTR("clock12hour")] | clock12hour; + flip = top[FPSTR("flip")] | flip; + + // Convert seconds from UI to milliseconds for internal use + // Clamp to 5-300 seconds range (5 sec min, 5 min max) + uint16_t timeoutSeconds = top[FPSTR("screenTimeOutSec")] | (displayTimeout / 1000); + timeoutSeconds = max((uint16_t)5, min((uint16_t)300, timeoutSeconds)); + displayTimeout = timeoutSeconds * 1000; + + // Load backlight setting (0-100% range) + uint8_t newBacklight = top[FPSTR("backlight")] | backlight; + if (newBacklight != backlight) { + setBacklight(newBacklight); + } + + return true; +} + +/** + * @brief Appends GC9A01 display configuration to the provided JSON object. + * + * Creates a nested "GC9A01" object on @p root and writes the display settings: + * - "enabled": whether the display is enabled + * - "sleepMode": whether sleep mode is enabled + * - "screenTimeOutSec": screen timeout in seconds (converted from internal milliseconds) + * - "clockMode": whether the clock screen is enabled + * - "clock12hour": whether 12-hour clock format is used + * - "flip": display flip setting + * - "backlight": backlight percentage (0–100) + * + * @param root JSON object to which the GC9A01 configuration will be added (modified in place). + */ +void UsermodGC9A01Display::addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR("GC9A01")); + top[FPSTR("enabled")] = displayEnabled; + top[FPSTR("sleepMode")] = sleepMode; + // Convert milliseconds to seconds for UI display + top[FPSTR("screenTimeOutSec")] = displayTimeout / 1000; + top[FPSTR("clockMode")] = clockMode; + top[FPSTR("clock12hour")] = clock12hour; + top[FPSTR("flip")] = flip; + top[FPSTR("backlight")] = backlight; +} + +/** + * @brief Appends GC9A01 display configuration metadata to the global UI/help registry. + * + * Adds informational entries for the following configurable keys: `GC9A01:enabled`, + * `GC9A01:sleepMode`, `GC9A01:screenTimeOutSec` (seconds, clamped to 5–300 on save), + * `GC9A01:clockMode`, `GC9A01:clock12hour`, `GC9A01:flip`, and `GC9A01:backlight`. + */ +void UsermodGC9A01Display::appendConfigData() { + oappend(SET_F("addInfo('GC9A01:enabled', 1, 'Enable/disable display');")); + oappend(SET_F("addInfo('GC9A01:sleepMode', 1, 'Enable sleep mode after timeout');")); + oappend(SET_F("addInfo('GC9A01:screenTimeOutSec', 1, 'Screen timeout in seconds (5-300 range, clamped on save)');")); + oappend(SET_F("addInfo('GC9A01:clockMode', 1, 'Show clock only when idle (bypasses sleep)');")); + oappend(SET_F("addInfo('GC9A01:clock12hour', 1, 'Checked=12H format (9:01 PM), Unchecked=24H format (21:01)');")); + oappend(SET_F("addInfo('GC9A01:flip', 1, 'Rotate display 180 degrees (requires reboot)');")); + oappend(SET_F("addInfo('GC9A01:backlight', 1, 'Backlight brightness (0-100%, default 75%)');")); +} + +/** + * @brief Maps an encoder overlay mode index to a human-readable name. + * + * @param mode Mode index where 0=Brightness, 1=Effect, 2=Speed, 3=Intensity, 4=Palette. + * @return const char* Name for the given mode, or "Unknown" if the index is out of range. + */ +const char* UsermodGC9A01Display::getEncoderModeName(uint8_t mode) { + // For rotary encoder integration - just provide names for overlay display + const char* modeNames[] = {"Brightness", "Effect", "Speed", "Intensity", "Palette"}; + if (mode < 5) return modeNames[mode]; + return "Unknown"; +} + +/** + * @brief No-op placeholder retained for API compatibility. + * + * This function intentionally performs no action; mode display is handled elsewhere. + */ +void UsermodGC9A01Display::drawCurrentModeIndicator() { + // No longer needed - overlay handles mode display +} + +/** + * @brief Placeholder no-op kept for interface compatibility. + * + * This method intentionally performs no action because mode display is handled + * by the overlay rendering path. + */ +void UsermodGC9A01Display::drawModeOverlay() { + // No longer needed - overlay handles mode display +} + +/** + * @brief Indicates whether an on-screen overlay is currently active and not expired. + * + * @return true if an overlay mode is set and the current time is before the overlay timeout, false otherwise. + */ +bool UsermodGC9A01Display::isOverlayActive() { + return (activeOverlayMode >= 0 && millis() < overlayUntil); +} + +/** + * @brief Retrieve the currently active overlay mode or indicate absence. + * + * @return `-1` if no overlay is active or the overlay has expired; otherwise the active overlay mode ID. + */ +int UsermodGC9A01Display::getActiveOverlayMode() { + if (millis() >= overlayUntil) { + return -1; // Overlay has expired + } + return activeOverlayMode; +} + +/** + * @brief Indicates whether the display is currently considered asleep, including when showing the clock. + * + * @return `true` if the display is turned off or the clock screen is active, `false` otherwise. + */ +bool UsermodGC9A01Display::isDisplayAsleep() { + return displayTurnedOff || showingClock; // Consider clock mode as "asleep" for rotary encoder state reset +} + +/** + * @brief Provides the usermod identifier for the GC9A01 display. + * + * @return uint16_t The numeric usermod ID constant USERMOD_ID_GC9A01_DISPLAY. + */ +uint16_t UsermodGC9A01Display::getId() { + return USERMOD_ID_GC9A01_DISPLAY; +} + +// Registration +UsermodGC9A01Display gc9a01DisplayUsermod; +REGISTER_USERMOD(gc9a01DisplayUsermod); + +#endif // USERMOD_GC9A01_DISPLAY \ No newline at end of file diff --git a/usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h b/usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h new file mode 100644 index 0000000000..79ffb15c57 --- /dev/null +++ b/usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h @@ -0,0 +1,276 @@ +#pragma once +#include "wled.h" + +#ifdef USERMOD_GC9A01_DISPLAY + +#include +#include + +// Pin definitions - use TFT_eSPI definitions when available +#ifndef TFT_CS + #define TFT_CS 5 // Default fallback +#endif +#define GC9A01_CS_PIN TFT_CS + +#ifndef TFT_DC + #define TFT_DC 16 // Default fallback +#endif +#define GC9A01_DC_PIN TFT_DC + +#ifndef TFT_RST + #define TFT_RST 17 // Default fallback +#endif +#define GC9A01_RST_PIN TFT_RST + +// Use TFT_eSPI's backlight pin definition +#ifndef TFT_BL + #define TFT_BL 4 // Default fallback if not defined by TFT_eSPI +#endif + +#ifndef USERMOD_ID_GC9A01_DISPLAY + #define USERMOD_ID_GC9A01_DISPLAY 59 // Use the official ID from const.h +#endif + +/** + * Create the UsermodGC9A01Display singleton instance if one does not already exist. + */ + +/** + * Return the singleton instance of UsermodGC9A01Display. + * @returns Pointer to the singleton instance, or nullptr if not constructed. + */ + +/** + * Initialize the TFT display hardware and internal display state. + */ + +/** + * Render the main interface. If overlayMode is >= 0, render the specified overlay instead of the normal UI. + * @param overlayMode Overlay mode to render, or -1 to render the normal interface. + */ + +/** + * Draw the WLED logo on the display. + */ + +/** + * Draw a WiFi connectivity icon at the specified position. + * @param x X coordinate in pixels. + * @param y Y coordinate in pixels. + * @param connected `true` to render as connected, `false` to render as disconnected. + * @param rssi RSSI value to reflect signal strength (optional). + */ + +/** + * Set display brightness used for UI elements and indicators. + * @param bri Brightness value (0-255). + */ + +/** + * Set the physical backlight level as a percentage. + * @param percent Backlight brightness percentage (0-100). + */ + +/** + * Switch display between sleep behavior and clock-only behavior based on `enabled`. + * @param enabled If `true`, enable clock/sleep behavior; if `false`, disable it. + */ + +/** + * Put the display into a low-power or off state. + */ + +/** + * Wake the display from sleep if it is sleeping. + * @returns `true` if the display was sleeping and was woken, `false` otherwise. + */ + +/** + * Render the clock-only screen. + */ + +/** + * Render the current mode overlay (mode-specific information). + */ + +/** + * Draw an indicator for the currently active WLED mode. + */ + +/** + * Return a human-readable name for the given encoder mode. + * @param mode Encoder mode identifier. + * @returns Null-terminated string describing the mode. + */ + +/** + * Wake the display if it is sleeping. + * @returns `true` if the display was sleeping and was woken, `false` otherwise. + */ + +/** + * Reset or update the inactivity timer to prevent the display from timing out. + */ + +/** + * Show a temporary overlay with a single line of text and optional glyph. + * @param line1 Null-terminated string to display. + * @param showHowLong Duration in milliseconds to show the overlay. + * @param glyphType Optional glyph type identifier (default 0). + */ + +/** + * Trigger a display redraw. + * @param forceRedraw If `true`, redraw regardless of internal change tracking. + */ + +/** + * Check whether an overlay is currently active. + * @returns `true` if an overlay is active, `false` otherwise. + */ + +/** + * Get the currently active overlay mode. + * @returns Active overlay mode number, or -1 if none is active. + */ + +/** + * Check whether the display is currently asleep. + * @returns `true` if the display is sleeping, `false` otherwise. + */ + +/** + * Usermod setup hook called once after initialization to configure the display and state. + */ + +/** + * Usermod loop hook called regularly to handle updates, timeouts, and redraw scheduling. + */ + +/** + * Append informational state about the display to the provided JSON object. + * @param root JSON object to which information will be added. + */ + +/** + * Read transient state values from JSON (runtime state). + * @param root JSON object containing state values. + */ + +/** + * Append transient state values to JSON (runtime state). + * @param root JSON object to populate with state values. + */ + +/** + * Read persistent configuration for the display from JSON. + * @param root JSON object containing configuration. + * @returns `true` if configuration was successfully read, `false` otherwise. + */ + +/** + * Append persistent configuration for the display to JSON. + * @param root JSON object to populate with configuration values. + */ + +/** + * Append additional configuration data to the global configuration payload. + */ + +/** + * Return the unique usermod ID for this display usermod. + * @returns Numeric usermod ID. + */ +class UsermodGC9A01Display : public Usermod { + private: + // Singleton pattern - allows rotary encoder usermod to find us + static UsermodGC9A01Display* instance; + + public: + UsermodGC9A01Display() { if (!instance) instance = this; } + static UsermodGC9A01Display* getInstance(void) { return instance; } + + private: + TFT_eSPI tft = TFT_eSPI(); + + bool displayEnabled = true; + bool needsRedraw = true; + bool displayTurnedOff = false; + bool showingWelcomeScreen = true; + bool showingClock = false; // Track if currently showing clock (for rotary encoder state reset) + unsigned long welcomeScreenStartTime = 0; + uint8_t backlight = 75; // Backlight brightness percentage (0-100%, default 75%) + uint16_t displayTimeout = 60000; // 60 seconds default + bool sleepMode = true; // Enable sleep mode by default + bool clockMode = false; // Show clock only when idle + bool flip = false; // Display rotation (0 or 2) + bool clock12hour = false; // false = 24h format, true = 12h format with AM/PM + + // Proper state tracking like 4-line display usermod + uint8_t knownBrightness = 255; + uint8_t knownMode = 255; + uint8_t knownPalette = 255; + uint8_t knownEffectSpeed = 255; + uint8_t knownEffectIntensity = 255; + uint32_t knownColor = 0; // colors[0] - Primary/FX + uint32_t knownBgColor = 0; // colors[1] - Secondary/BG + uint32_t knownCustomColor = 0; // colors[2] - Tertiary/CS + bool knownPowerState = true; + unsigned long nextUpdate = 0; + unsigned long lastRedraw = ULONG_MAX; // Initialize to max value - sleep timer starts after first interaction + uint16_t refreshRate = 1000; // Match 4-line display usermod (1 second) for better performance + + // Time tracking for display updates + uint8_t knownMinute = 99; + uint8_t knownHour = 99; + + // Integration with rotary encoder usermod (no direct pin handling) + unsigned long overlayUntil = 0; // When overlay should expire (millis) + int activeOverlayMode = -1; // Which overlay mode is active (-1 = none, 0-4 = overlay modes) + unsigned long overlayInactivityTimeout = 3000; // 3 seconds of inactivity before returning to main screen + String overlayText = ""; + + // Private method declarations + void initDisplay(); + void drawMainInterface(int overlayMode = -1); // -1 = normal, 0+ = overlay mode + void drawWLEDLogo(); + void drawWiFiIcon(int x, int y, bool connected, int rssi = 0); + void setBrightness(uint8_t bri); + void setBacklight(uint8_t percent); // Set backlight brightness (0-100%) + void sleepOrClock(bool enabled); // Sleep display or show clock based on settings + void sleepDisplay(); + bool wakeDisplayFromSleep(); // Return true if display was sleeping + void drawClockScreen(); // Clock-only display mode + + // Mode-specific drawing methods + void drawModeOverlay(); + void drawCurrentModeIndicator(); + + // Get encoder state from rotary encoder usermod + const char* getEncoderModeName(uint8_t mode); + + public: + + // Public interface methods for rotary encoder usermod (like 4-line display) + bool wakeDisplay(); // Return true if was sleeping + void updateRedrawTime(); // Prevent display timeout + void overlay(const char* line1, long showHowLong, byte glyphType = 0); // Match 4-line interface + void redraw(bool forceRedraw); // Force display update + bool isOverlayActive(); // Check if overlay is currently showing + int getActiveOverlayMode(); // Get current overlay mode (-1 = none) + bool isDisplayAsleep(); // Check if display is sleeping + + // Usermod API + // Public method declarations (Usermod interface) + void setup() override; + void loop() override; + void addToJsonInfo(JsonObject& root) override; + void readFromJsonState(JsonObject& root) override; + void addToJsonState(JsonObject& root) override; + bool readFromConfig(JsonObject& root) override; + void addToConfig(JsonObject& root) override; + void appendConfigData() override; + uint16_t getId() override; +}; + +#endif // USERMOD_GC9A01_DISPLAY \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp index f7e908651b..aced7c3406 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp @@ -31,6 +31,10 @@ #include "usermod_v2_four_line_display.h" #endif +#ifdef USERMOD_GC9A01_DISPLAY +#include "../usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h" +#endif + #ifdef USERMOD_MODE_SORT #error "Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini" #endif @@ -178,6 +182,12 @@ class RotaryEncoderUIUsermod : public Usermod { void* display; #endif + #ifdef USERMOD_GC9A01_DISPLAY + UsermodGC9A01Display *gc9a01Display; + #else + void* gc9a01Display; + #endif + // Pointers the start of the mode names within JSON_mode_names const char **modes_qstrings; @@ -251,6 +261,16 @@ class RotaryEncoderUIUsermod : public Usermod { public: + /** + * @brief Construct a RotaryEncoderUIUsermod with default runtime configuration. + * + * Initializes internal state and safe defaults for pins, UI state, color values, + * effect/palette indices, display pointers, preset ranges, and feature flags. + * + * The constructor does not allocate hardware resources or register interrupts; + * it only sets member variables to their default values so setup() can perform + * initialization later. + */ RotaryEncoderUIUsermod() : fadeAmount(5) , buttonPressedTime(0) @@ -265,6 +285,7 @@ class RotaryEncoderUIUsermod : public Usermod { , currentSat1(255) , currentCCT(128) , display(nullptr) + , gc9a01Display(nullptr) , modes_qstrings(nullptr) , modes_alpha_indexes(nullptr) , palettes_qstrings(nullptr) @@ -401,19 +422,19 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() { re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(customPalettes.size()); - palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount()); // allocates memory for all palette names - palettes_alpha_indexes = re_initIndexArray(getPaletteCount()); // allocates memory for all palette indexes + palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount()); + palettes_alpha_indexes = re_initIndexArray(getPaletteCount()); if (customPalettes.size()) { for (int i=0; i LAST_UI_STATE) newState = 0; } while (!changedState); - if (display != nullptr) { - switch (newState) { - case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun - case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward - case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire - case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette - case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece - case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush - case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast - case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart - case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star - case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + // Support both Four Line Display and GC9A01 display + if (display != nullptr || gc9a01Display != nullptr) { + // Special handling for state 0 (Brightness) with GC9A01 - no overlay needed + if (newState == 0 && gc9a01Display != nullptr && display == nullptr) { + // For GC9A01 only (no four line display), just update main screen for brightness + if (gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + changedState = false; // Throw away wake up input + } else { + gc9a01Display->redraw(false); // Update main screen directly + changedState = true; + } + } else { + // Normal overlay handling for all other states and for Four Line Display + switch (newState) { + case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; //1 = sun + case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; //2 = skip forward + case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; //3 = fire + case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; //4 = custom palette + case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; //5 = puzzle piece + case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; //7 = brush + case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; //8 = contrast + case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; //11 = heart + case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; //10 = star + } } } if (changedState) select_state = newState; } + // Check if GC9A01 overlay has expired or display is asleep and reset to brightness mode (state 0) + #ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display != nullptr && select_state > 0) { + if (!gc9a01Display->isOverlayActive() || gc9a01Display->isDisplayAsleep()) { + // Overlay has expired or display is asleep, return to brightness mode + select_state = 0; + } + } + #endif + + // Check if current state is valid for current effect (for states 1=speed, 2=intensity, 9-11=custom) + if (select_state == 1 || select_state == 2 || (select_state >= 9 && select_state <= 11)) { + char tempBuffer[64]; + int sliderIndex = (select_state <= 2) ? (select_state - 1) : (select_state - 7); + if (!extractModeSlider(effectCurrent, sliderIndex, tempBuffer, 63)) { + // Current effect doesn't have this slider, reset to brightness + select_state = 0; + } + } + Enc_A = readPin(pinA); // Read encoder pins Enc_B = readPin(pinB); if ((Enc_A) && (!Enc_A_prev)) @@ -681,12 +757,32 @@ void RotaryEncoderUIUsermod::loop() } } +/** + * @brief Show a temporary "NETWORK INFO" overlay on any attached display. + * + * Displays a "NETWORK INFO" overlay for 10 seconds on supported displays. + * When a GC9A01 display is present, the overlay uses the network glyph. + */ void RotaryEncoderUIUsermod::displayNetworkInfo() { #ifdef USERMOD_FOUR_LINE_DISPLAY display->networkOverlay(PSTR("NETWORK INFO"), 10000); #endif + + #ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display != nullptr) { + gc9a01Display->overlay(PSTR("NETWORK INFO"), 10000, 12); // Use network glyph + } + #endif } +/** + * @brief Resolve and cache the indices of the currently active effect and palette for UI navigation. + * + * Searches available modes and palettes to determine the display indices corresponding to + * the current effect and palette identifiers, stores the resolved indices in + * `effectCurrentIndex` and `effectPaletteIndex`, and marks `currentEffectAndPaletteInitialized`. + * If no match is found for either, the corresponding index is set to 0. + */ void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { DEBUG_PRINTLN(F("Finding current mode and palette.")); currentEffectAndPaletteInitialized = true; @@ -702,7 +798,7 @@ void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { effectPaletteIndex = 0; DEBUG_PRINTLN(effectPalette); - for (unsigned i = 0; i < getPaletteCount()+customPalettes.size(); i++) { + for (unsigned i = 0; i < getPaletteCount(); i++) { if (palettes_alpha_indexes[i] == effectPalette) { effectPaletteIndex = i; DEBUG_PRINTLN(F("Found palette.")); @@ -711,6 +807,18 @@ void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { } } +/** + * @brief Show a short-term UI overlay on any attached display and optionally mark a line/column. + * + * Displays the given text with an optional glyph on the integrated FourLineDisplay and/or GC9A01 display. + * If a display was sleeping and is awakened by this call, the wake is consumed (a redraw is performed) and the overlay is not shown. + * + * @param stateName Text to show in the overlay. + * @param markedLine Line index to mark on the FourLineDisplay (ignored if not applicable). + * @param markedCol Column index to mark on the FourLineDisplay (ignored if not applicable). + * @param glyph Glyph index to display alongside the text (display-dependent). + * @return true if the overlay was actually shown, false if a display wake-up was consumed instead. + */ bool RotaryEncoderUIUsermod::changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display != nullptr) { @@ -723,6 +831,20 @@ bool RotaryEncoderUIUsermod::changeState(const char *stateName, byte markedLine, display->setMarkLine(markedLine, markedCol); } #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display != nullptr) { + DEBUG_PRINTF("[RotaryEncoder] Calling GC9A01 overlay: '%s' glyph=%d\n", stateName, glyph); + if (gc9a01Display->wakeDisplay()) { + // Throw away wake up input + gc9a01Display->redraw(true); + return false; + } + gc9a01Display->overlay(stateName, 750, glyph); + } else { + DEBUG_PRINTLN(F("[RotaryEncoder] gc9a01Display is NULL!")); + } +#endif return true; } @@ -734,6 +856,17 @@ void RotaryEncoderUIUsermod::lampUdated() { updateInterfaces(CALL_MODE_BUTTON); } +/** + * @brief Adjusts the global brightness up or down and updates attached displays. + * + * Changes the global brightness by a configured step, using smaller steps when the + * current brightness is below 40 to provide finer control at low levels. After + * changing brightness the method notifies the system of the update and refreshes + * any attached displays; display behavior will present an overlay or update the + * main screen depending on the active UI state and the specific display type. + * + * @param increase True to increase brightness, false to decrease it. + */ void RotaryEncoderUIUsermod::changeBrightness(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { @@ -743,6 +876,16 @@ void RotaryEncoderUIUsermod::changeBrightness(bool increase) { } display->updateRedrawTime(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif + //bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); if (bri < 40) bri = max(min((increase ? bri+fadeAmount/2 : bri-fadeAmount/2), 255), 0); // slower steps when brightness < 16% else bri = max(min((increase ? bri+fadeAmount : bri-fadeAmount), 255), 0); @@ -750,9 +893,34 @@ void RotaryEncoderUIUsermod::changeBrightness(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY display->updateBrightness(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display) { + // Only show overlay if we're NOT in default state (state 0) + // In state 0, brightness changes should update the main screen directly + if (select_state == 0) { + gc9a01Display->redraw(false); // Update main interface directly (no overlay) + } else { + // Show brightness overlay like other modes (when in overlay mode) + char brightnessStr[16]; + sprintf(brightnessStr, "Brightness %d%%", (bri * 100) / 255); + gc9a01Display->overlay(brightnessStr, 500, 10); + } + } +#endif } +/** + * @brief Change the currently selected effect and apply it to segments. + * + * Updates the internal effect index (clamped to the available mode range), sets the selected + * effect on either all active segments or just the main segment depending on `applyToAll`, + * marks the usermod state as changed, triggers a lamp update, and refreshes any attached + * displays (FourLineDisplay or GC9A01) including wake/overlay handling. + * + * @param increase `true` to advance to the next effect, `false` to go to the previous effect. + */ void RotaryEncoderUIUsermod::changeEffect(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { @@ -762,6 +930,15 @@ void RotaryEncoderUIUsermod::changeEffect(bool increase) { } display->updateRedrawTime(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif effectCurrentIndex = max(min((increase ? effectCurrentIndex+1 : effectCurrentIndex-1), strip.getModeCount()-1), 0); effectCurrent = modes_alpha_indexes[effectCurrentIndex]; stateChanged = true; @@ -782,6 +959,15 @@ void RotaryEncoderUIUsermod::changeEffect(bool increase) { } +/** + * @brief Adjusts the global effect speed and applies the change to segments and displays. + * + * Updates the stored effect speed by adding or subtracting the configured step (clamped to 0–255), + * marks the state as changed, applies the new speed to either all active segments or the main segment + * depending on the `applyToAll` flag, notifies the system of the update, and refreshes any attached displays. + * + * @param increase If `true`, increases the effect speed; if `false`, decreases it. + */ void RotaryEncoderUIUsermod::changeEffectSpeed(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { @@ -791,6 +977,16 @@ void RotaryEncoderUIUsermod::changeEffectSpeed(bool increase) { } display->updateRedrawTime(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif + effectSpeed = max(min((increase ? effectSpeed+fadeAmount : effectSpeed-fadeAmount), 255), 0); stateChanged = true; if (applyToAll) { @@ -807,9 +1003,24 @@ void RotaryEncoderUIUsermod::changeEffectSpeed(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY display->updateSpeed(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display) { + gc9a01Display->redraw(false); // Update speed display + } +#endif } +/** + * @brief Adjusts the current effect intensity up or down and applies the change to segments and UI. + * + * Updates the stored effect intensity by one step (bounded to 0–255), marks state as changed, + * applies the new intensity to either all active segments or the main segment depending on configuration, + * notifies the system of the update, and refreshes any attached display overlays. + * + * @param increase If `true`, increase intensity; if `false`, decrease intensity. + */ void RotaryEncoderUIUsermod::changeEffectIntensity(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { @@ -819,6 +1030,16 @@ void RotaryEncoderUIUsermod::changeEffectIntensity(bool increase) { } display->updateRedrawTime(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif + effectIntensity = max(min((increase ? effectIntensity+fadeAmount : effectIntensity-fadeAmount), 255), 0); stateChanged = true; if (applyToAll) { @@ -835,9 +1056,29 @@ void RotaryEncoderUIUsermod::changeEffectIntensity(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY display->updateIntensity(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display) { + gc9a01Display->redraw(false); // Update intensity display + } +#endif } +/** + * @brief Adjusts a segment custom parameter (custom1–custom3), updates segments and UI. + * + * Increases or decreases the selected custom parameter, clamps the resulting value to 0–255, + * marks the usermod state as changed, notifies the system via lampUdated(), and shows an + * overlay with the new value on any attached display. + * + * @param par Index of the custom parameter to change: 1, 2, or 3. Values other than 2 or 3 + * are treated as 1. + * @param increase `true` to increase the parameter, `false` to decrease it. + * + * @note If `applyToAll` is true, the change is copied to all other active segments; otherwise + * only the main segment is modified. + */ void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) { uint8_t val = 0; #ifdef USERMOD_FOUR_LINE_DISPLAY @@ -848,6 +1089,16 @@ void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) { } display->updateRedrawTime(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif + stateChanged = true; if (applyToAll) { uint8_t id = strip.getFirstSelectedSegId(); @@ -880,9 +1131,27 @@ void RotaryEncoderUIUsermod::changeCustom(uint8_t par, bool increase) { sprintf(lineBuffer, "%d", val); display->overlay(lineBuffer, 500, 10); // use star #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display) { + char lineBuffer[64]; + sprintf(lineBuffer, "Custom%d: %d", par, val); + gc9a01Display->overlay(lineBuffer, 500, 10); // use star glyph + } +#endif } +/** + * @brief Change the selected color palette by one step and apply it to segments and displays. + * + * Updates the current palette index (advances if `increase` is true, otherwise moves backward), + * constrains it to the valid palette range, sets the active palette, marks the usermod state as changed, + * applies the new palette to either all active segments or only the main segment depending on configuration, + * signals a lamp update to commit the change, and refreshes any attached displays (including wake handling). + * + * @param increase `true` to move to the next palette, `false` to move to the previous palette. + */ void RotaryEncoderUIUsermod::changePalette(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { @@ -892,7 +1161,17 @@ void RotaryEncoderUIUsermod::changePalette(bool increase) { } display->updateRedrawTime(); #endif - effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), getPaletteCount()+customPalettes.size()-1), 0U); + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif + + effectPaletteIndex = max(min((int)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), (int)(getPaletteCount()-1)), 0); effectPalette = palettes_alpha_indexes[effectPaletteIndex]; stateChanged = true; if (applyToAll) { @@ -909,9 +1188,26 @@ void RotaryEncoderUIUsermod::changePalette(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY display->showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display) { + gc9a01Display->redraw(false); // Update palette display + } +#endif } +/** + * Adjusts the primary hue value and applies the computed color to the active segment(s), then updates displays. + * + * This clamps the hue to the 0–255 range, converts the hue and current saturation to RGB(W), sets the internal + * changed-state flag, applies the new color to either all active segments or the main segment depending on + * configuration, and notifies the system of the update. If a connected display is sleeping, a wake event will + * trigger a redraw and consume the input without changing the hue; otherwise an overlay showing the hue value + * is displayed. + * + * @param increase If `true`, increases the hue by the configured step; if `false`, decreases the hue. + */ void RotaryEncoderUIUsermod::changeHue(bool increase){ #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { @@ -921,6 +1217,16 @@ void RotaryEncoderUIUsermod::changeHue(bool increase){ } display->updateRedrawTime(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif + currentHue1 = max(min((increase ? currentHue1+fadeAmount : currentHue1-fadeAmount), 255), 0); colorHStoRGB(currentHue1*256, currentSat1, colPri); stateChanged = true; @@ -940,8 +1246,23 @@ void RotaryEncoderUIUsermod::changeHue(bool increase){ sprintf(lineBuffer, "%d", currentHue1); display->overlay(lineBuffer, 500, 7); // use brush #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display) { + char lineBuffer[64]; + sprintf(lineBuffer, "Hue: %d", currentHue1); + gc9a01Display->overlay(lineBuffer, 500, 7); // use brush glyph + } +#endif } +/** + * @brief Adjusts the current saturation and applies the change to LEDs and displays. + * + * Increments or decrements the main saturation value by the configured step, clamps it to the range 0–255, updates the primary RGBW color, applies the new color to either all active segments or the main segment (depending on `applyToAll`), triggers a lamp update, and shows the updated saturation on attached display overlays. + * + * @param increase If true, increase saturation; if false, decrease saturation. + */ void RotaryEncoderUIUsermod::changeSat(bool increase){ #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { @@ -951,6 +1272,16 @@ void RotaryEncoderUIUsermod::changeSat(bool increase){ } display->updateRedrawTime(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif + currentSat1 = max(min((increase ? currentSat1+fadeAmount : currentSat1-fadeAmount), 255), 0); colorHStoRGB(currentHue1*256, currentSat1, colPri); if (applyToAll) { @@ -969,8 +1300,28 @@ void RotaryEncoderUIUsermod::changeSat(bool increase){ sprintf(lineBuffer, "%d", currentSat1); display->overlay(lineBuffer, 500, 8); // use contrast #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display) { + char lineBuffer[64]; + sprintf(lineBuffer, "Sat: %d", currentSat1); + gc9a01Display->overlay(lineBuffer, 500, 8); // use contrast glyph + } +#endif } +/** + * @brief Apply or clear a saved preset range and show a brief overlay. + * + * Constructs a preset state from the configured presetLow/presetHigh range and + * applies it when `increase` is true or clears it when `increase` is false. + * If no valid preset range is configured (presetHigh <= presetLow or either is 0), + * the function does nothing. After applying/clearing the preset it notifies the + * system of the change and, when a supported display is attached, shows a + * short overlay indicating the current preset. + * + * @param increase `true` to apply the preset range, `false` to remove/clear it. + */ void RotaryEncoderUIUsermod::changePreset(bool increase) { #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { @@ -980,6 +1331,16 @@ void RotaryEncoderUIUsermod::changePreset(bool increase) { } display->updateRedrawTime(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif + if (presetHigh && presetLow && presetHigh > presetLow) { StaticJsonDocument<64> root; char str[64]; @@ -1000,9 +1361,26 @@ void RotaryEncoderUIUsermod::changePreset(bool increase) { sprintf(str, "%d", currentPreset); display->overlay(str, 500, 11); // use heart #endif + + #ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display) { + char lineBuffer[64]; + sprintf(lineBuffer, "Preset: %d", currentPreset); + gc9a01Display->overlay(lineBuffer, 500, 11); // use heart glyph + } + #endif } } +/** + * @brief Adjusts the correlated color temperature (CCT) and applies it to active segments. + * + * Increments or decrements the stored CCT by the configured step, clamps the result to 0–255, + * assigns the value to every active segment, triggers a lamp update, and shows a brief overlay + * on any attached display. + * + * @param increase True to increase CCT, false to decrease. + */ void RotaryEncoderUIUsermod::changeCCT(bool increase){ #ifdef USERMOD_FOUR_LINE_DISPLAY if (display && display->wakeDisplay()) { @@ -1012,6 +1390,16 @@ void RotaryEncoderUIUsermod::changeCCT(bool increase){ } display->updateRedrawTime(); #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display && gc9a01Display->wakeDisplay()) { + gc9a01Display->redraw(true); + // Throw away wake up input + return; + } + if (gc9a01Display) gc9a01Display->updateRedrawTime(); +#endif + currentCCT = max(min((increase ? currentCCT+fadeAmount : currentCCT-fadeAmount), 255), 0); // if (applyToAll) { for (unsigned i=0; ioverlay(lineBuffer, 500, 10); // use star #endif + +#ifdef USERMOD_GC9A01_DISPLAY + if (gc9a01Display) { + char lineBuffer[64]; + sprintf(lineBuffer, "CCT: %d", currentCCT); + gc9a01Display->overlay(lineBuffer, 500, 10); // use star glyph + } +#endif } /*