/* * display.cpp - Display functions for OLED * TJ-56-654 Weather Clock v1.9.3 */ #include "globals.h" // Update main time display void ICACHE_FLASH_ATTR updateDisplay() { static unsigned long lastUpdate = 0; // Update display only every 500ms (not every loop cycle) // BUT skip throttle during transition for smooth animation if (!inTransition && millis() - lastUpdate < 500) return; lastUpdate = millis(); display.clearDisplay(); // Check if we have ANY time source (NTPClient or async sync) bool hasTime = timeClient.isTimeSet() || timeIsSynced; if (!hasTime) { display.setTextSize(3); display.setTextColor(SSD1306_WHITE); display.setCursor(20, 24); display.println("--:--"); // Show WiFi status in yellow zone display.setTextSize(1); display.setCursor(25, 52); if (wifiConnState == WIFI_CONN_CONNECTED) { display.print("Syncing NTP..."); } else { display.print("No WiFi"); } if (!inTransition) { display.display(); } return; } // Get epoch from best available source unsigned long epochTime = timeIsSynced ? getAsyncEpoch() : timeClient.getEpochTime(); unsigned long localTime = epochTime + getTotalOffset(epochTime); int hours = (localTime / 3600) % 24; int minutes = (localTime / 60) % 60; // Convert to 12h format if needed if (!config.hour_format_24) { if (hours == 0) hours = 12; else if (hours > 12) hours -= 12; } // === YELLOW ZONE (Y: 48-63): Date (size 2 = 16px height) === display.setTextSize(2); time_t t = epochTime; struct tm *ptm = gmtime(&t); // Format: "Thu 02.01" or "! Thu 02.01" if no WiFi const char* days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; char dateStr[20]; if (wifiConnState != WIFI_CONN_CONNECTED) { // Show "!" indicator when WiFi is down sprintf(dateStr, "!%s %02d.%02d", days[ptm->tm_wday], ptm->tm_mday, ptm->tm_mon + 1); } else { sprintf(dateStr, "%s %02d.%02d", days[ptm->tm_wday], ptm->tm_mday, ptm->tm_mon + 1); } // Center in yellow zone (Y: 48-63) int dateWidth = strlen(dateStr) * 12; // Size 2 = ~12px per char int dateX = (128 - dateWidth) / 2; display.setCursor(dateX, 48); display.print(dateStr); // === BLUE ZONE (Y: 0-47): Large time (size 3 = 24px height) === display.setTextSize(3); display.setTextColor(SSD1306_WHITE); // Calculate center position for HH:MM display.setCursor(10, 12); // Y=12 centers in blue zone (0-47) display.printf("%02d", hours); // Blinking colon if (colonBlink) { display.print(":"); } else { display.print(" "); } display.printf("%02d", minutes); // Don't send to screen during transition - crossfade will do it if (!inTransition) { display.display(); } } // Weather display void ICACHE_FLASH_ATTR displayWeather() { display.clearDisplay(); if (!weather.valid) { display.setTextSize(3); display.setTextColor(SSD1306_WHITE); display.setCursor(20, 24); display.println("No Data"); if (!inTransition) { display.display(); } return; } // === YELLOW ZONE (Y: 48-63): City name (size 2 = 16px) === display.setTextSize(2); int cityWidth = strlen(config.city_name) * 12; int cityX = (128 - cityWidth) / 2; display.setCursor(cityX, 48); display.print(config.city_name); // === BLUE ZONE (Y: 0-47): Temperature (size 3 = 24px height) === display.setTextSize(3); display.setTextColor(SSD1306_WHITE); // Format temperature value only char tempStr[16]; sprintf(tempStr, "%.1f", weather.temperature); // Center temperature + degree symbol int tempValueWidth = strlen(tempStr) * 18; // Size 3 = ~18px per char int degreeSymbolWidth = 6; // Size 1 = 6px per char int totalWidth = tempValueWidth + degreeSymbolWidth + 6; int startX = (128 - totalWidth) / 2; // Print temperature value display.setCursor(startX, 12); display.print(tempStr); // Print degree symbol and C (small, raised) display.setTextSize(1); int degreeX = startX + tempValueWidth; display.setCursor(degreeX, 12); display.print("\xF8" "c"); // °c if (!inTransition) { display.display(); } } // Sunrise/Sunset display void ICACHE_FLASH_ATTR displaySunTimes() { display.clearDisplay(); if (sunTimes.lastDay == -1) { display.setTextSize(3); display.setTextColor(SSD1306_WHITE); display.setCursor(30, 24); display.println("----"); if (!inTransition) { display.display(); } return; } // === YELLOW ZONE (Y: 48-63): Daylight duration === int daylightMinutes = sunTimes.sunsetMinutes - sunTimes.sunriseMinutes; int daylightHours = daylightMinutes / 60; int daylightMins = daylightMinutes % 60; char daylightStr[16]; sprintf(daylightStr, "Day %dh %dm", daylightHours, daylightMins); display.setTextSize(1); int textWidth = strlen(daylightStr) * 6; int textX = (128 - textWidth) / 2; display.setCursor(textX, 52); display.print(daylightStr); // === BLUE ZONE (Y: 0-47): Sunrise and Sunset times === display.setTextSize(2); display.setTextColor(SSD1306_WHITE); // Line 1: Sunrise display.setCursor(5, 4); display.print("\x18 "); // Up arrow display.print(sunTimes.sunrise); // Line 2: Sunset display.setCursor(5, 28); display.print("\x19 "); // Down arrow display.print(sunTimes.sunset); if (!inTransition) { display.display(); } } // Apply dissolve effect with optional drift (Thanos-style) void ICACHE_FLASH_ATTR applyDissolveEffect(uint8_t hidePercent, bool withDrift) { // Apply drift effect - shift buffer to the right if (withDrift && hidePercent > 10) { uint8_t* buffer = display.getBuffer(); // SSD1306 buffer: 8 pages (8 rows each), 128 bytes per page for (int page = 0; page < 8; page++) { int pageOffset = page * SCREEN_WIDTH; // Shift right by 2 pixels per frame for (int x = SCREEN_WIDTH - 1; x > 1; x--) { buffer[pageOffset + x] = buffer[pageOffset + x - 2]; } buffer[pageOffset] = 0; buffer[pageOffset + 1] = 0; } } // Multiply by 4 to compensate for random overlaps uint32_t pixelsToHide = ((uint32_t)SCREEN_WIDTH * SCREEN_HEIGHT * hidePercent * 4) / 100; for (uint32_t i = 0; i < pixelsToHide; i++) { uint8_t x = random(SCREEN_WIDTH); uint8_t y = random(SCREEN_HEIGHT); display.drawPixel(x, y, SSD1306_BLACK); } display.display(); } // Display rotation with dissolve transition void ICACHE_FLASH_ATTR updateDisplayRotation() { unsigned long now = millis(); unsigned long interval = config.display_rotation_sec * 1000UL; // Handle active dissolve transition (two phases) if (inTransition) { unsigned long elapsed = now - transitionStart; if (elapsed >= DISSOLVE_DURATION) { displayMode = nextDisplayMode; inTransition = false; return; } if (now - lastDissolveFrame < DISSOLVE_FRAME_INTERVAL) { return; } lastDissolveFrame = now; uint8_t currentMode; uint8_t hidePercent; unsigned long halfDuration = DISSOLVE_DURATION / 2; bool isDriftPhase; if (elapsed < halfDuration) { // Phase 1: dissolve OUT old content currentMode = displayMode; hidePercent = (elapsed * 100) / halfDuration; isDriftPhase = true; } else { // Phase 2: dissolve IN new content currentMode = nextDisplayMode; unsigned long phase2Elapsed = elapsed - halfDuration; hidePercent = 100 - (phase2Elapsed * 100) / halfDuration; isDriftPhase = false; } switch(currentMode) { case 0: updateDisplay(); break; case 1: displayWeather(); break; case 2: displaySunTimes(); break; } applyDissolveEffect(hidePercent, isDriftPhase); return; } // Check if time to switch modes if (now - lastModeSwitch > interval) { uint8_t attempts = 0; nextDisplayMode = displayMode; do { nextDisplayMode = (nextDisplayMode + 1) % 3; attempts++; if (attempts >= 3) { nextDisplayMode = 0; Serial.println("WARNING: No display mode enabled, forcing time mode"); break; } } while (!isModeEnabled(nextDisplayMode)); inTransition = true; transitionStart = now; lastModeSwitch = now; lastDissolveFrame = 0; return; } // Normal display update switch(displayMode) { case 0: updateDisplay(); break; case 1: displayWeather(); break; case 2: displaySunTimes(); break; } } // Check if display mode is enabled bool ICACHE_FLASH_ATTR isModeEnabled(uint8_t mode) { switch(mode) { case 0: return true; // Time always enabled case 1: return config.show_weather && weather.valid; case 2: return config.show_sunrise_sunset && sunTimes.lastDay != -1; } return false; } // Clear display void ICACHE_FLASH_ATTR clearDisplay() { display.clearDisplay(); display.display(); } // Show number on OLED void ICACHE_FLASH_ATTR showNumber(int num, bool leadingZeros) { display.clearDisplay(); display.setTextSize(3); display.setTextColor(SSD1306_WHITE); display.setCursor(20, 20); if (leadingZeros) { display.printf("%04d", num); } else { display.print(num); } display.display(); } // Show "No WiFi" status void ICACHE_FLASH_ATTR showNoWiFi(unsigned long nextRetrySeconds) { display.clearDisplay(); display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(20, 8); display.println("No WiFi"); display.setTextSize(1); display.setCursor(10, 52); if (nextRetrySeconds < 60) { display.printf("Retry in %lu sec", nextRetrySeconds); } else { display.printf("Retry in %lu min", nextRetrySeconds / 60); } display.display(); } // Startup animation - just logo and version // Layout: Blue zone (Y 0-47), Yellow zone (Y 48-63) void ICACHE_FLASH_ATTR showStartupAnimation() { Serial.println(" Boot: Show logo"); display.clearDisplay(); display.setTextColor(SSD1306_WHITE); // "TJ-56" centered in BLUE zone // 5 chars × 10px + 4 gaps × 2px = 58px → X = (128-58)/2 = 35 display.setTextSize(2); display.setCursor(35, 10); display.print("TJ-56"); // "Weather Clock" centered in BLUE zone // 13 chars × 5px + 12 gaps × 1px = 77px → X = (128-77)/2 = 26 display.setTextSize(1); display.setCursor(26, 30); display.print("Weather Clock"); // Version in YELLOW zone (centered) // 6 chars × 5px + 5 gaps × 1px = 35px → X = (128-35)/2 = 47 display.setTextSize(1); display.setCursor(47, 52); display.print("v"); display.print(FIRMWARE_VERSION); display.display(); delay(1500); Serial.println(" Boot: Logo done"); } // Show WiFi connecting animation with dots // Layout: Blue zone (Y 0-47), Yellow zone (Y 48-63) void ICACHE_FLASH_ATTR showWiFiConnecting(int step) { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); // "WiFi..." in BLUE zone (centered) // 7 chars × 10px + 6 gaps × 2px = 82px → X = 23 display.setTextSize(2); display.setCursor(23, 10); display.print("WiFi..."); // Animated dots in BLUE zone (centered) // "* * * * * * " = 12 chars × 5px + 11 gaps × 1px = 71px → X = 29 display.setTextSize(1); display.setCursor(29, 32); int dots = (step % 6) + 1; for (int i = 0; i < 6; i++) { if (i < dots) { display.print("* "); } else { display.print(" "); } } // "Connecting" in YELLOW zone (centered) // 10 chars × 5px + 9 gaps × 1px = 59px → X = 35 display.setTextSize(1); display.setCursor(35, 52); display.print("Connecting"); display.display(); } // Show connected status with SSID and IP // Layout: Blue zone (Y 0-47), Yellow zone (Y 48-63) void ICACHE_FLASH_ATTR showConnected() { display.clearDisplay(); display.setTextColor(SSD1306_WHITE); // SSID in BLUE zone (centered) // Formula: n chars × 5px + (n-1) gaps × 1px display.setTextSize(1); String ssid = WiFi.SSID(); int ssidLen = ssid.length(); int ssidWidth = ssidLen * 5 + (ssidLen - 1) * 1; int ssidX = (128 - ssidWidth) / 2; display.setCursor(ssidX, 8); display.print(ssid); // IP address in BLUE zone IPAddress ip = WiFi.localIP(); char ipStr[16]; sprintf(ipStr, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); int ipLen = strlen(ipStr); // Try size 2 first: n chars × 10px + (n-1) gaps × 2px int ipWidth2 = ipLen * 10 + (ipLen - 1) * 2; if (ipWidth2 <= 128) { display.setTextSize(2); int ipX = (128 - ipWidth2) / 2; display.setCursor(ipX, 24); } else { // Fallback to size 1: n chars × 5px + (n-1) gaps × 1px display.setTextSize(1); int ipWidth1 = ipLen * 5 + (ipLen - 1) * 1; int ipX = (128 - ipWidth1) / 2; display.setCursor(ipX, 24); } display.print(ipStr); // "OK" in YELLOW zone (centered) // 2 chars × 5px + 1 gap × 1px = 11px → X = 59 display.setTextSize(1); display.setCursor(59, 52); display.print("OK"); display.display(); delay(2000); } // Show IP address (legacy, for reconnection) void ICACHE_FLASH_ATTR showIP() { showConnected(); // Use new unified function }