#include "DisplayDriverGFX.h" // ═══════════════════════════════════════════════════════════ // CH422G IO Expander // ═══════════════════════════════════════════════════════════ void DisplayDriverGFX::ch422gInit() { Wire.begin(I2C_SDA, I2C_SCL, I2C_FREQ); // Enable OC output mode Wire.beginTransmission(CH422G_SET_MODE >> 1); Wire.write(0x01); // OC output enable Wire.endTransmission(); // Deassert resets, backlight OFF initially _exioBits = EXIO_TP_RST | EXIO_LCD_RST | EXIO_SD_CS; ch422gWrite(_exioBits); delay(100); Serial.println("[IO] CH422G initialized"); } void DisplayDriverGFX::ch422gWrite(uint8_t val) { Wire.beginTransmission(CH422G_WRITE_OC >> 1); Wire.write(val); Wire.endTransmission(); } void DisplayDriverGFX::exioSet(uint8_t bit, bool on) { if (on) _exioBits |= bit; else _exioBits &= ~bit; ch422gWrite(_exioBits); } // ═══════════════════════════════════════════════════════════ // GT911 Touch (minimal implementation) // ═══════════════════════════════════════════════════════════ void DisplayDriverGFX::gt911Init() { // GT911 is on the same I2C bus (Wire), already started by ch422gInit. // Reset sequence: pull TP_RST low then high (via CH422G EXIO1). exioSet(EXIO_TP_RST, false); delay(10); exioSet(EXIO_TP_RST, true); delay(50); Serial.println("[TOUCH] GT911 initialized"); } bool DisplayDriverGFX::gt911Read(int& x, int& y) { // Read status register 0x814E Wire.beginTransmission(GT911_ADDR); Wire.write(0x81); Wire.write(0x4E); if (Wire.endTransmission() != 0) return false; Wire.requestFrom((uint8_t)GT911_ADDR, (uint8_t)1); if (!Wire.available()) return false; uint8_t status = Wire.read(); uint8_t touches = status & 0x0F; bool ready = status & 0x80; if (!ready || touches == 0 || touches > 5) { // Clear status Wire.beginTransmission(GT911_ADDR); Wire.write(0x81); Wire.write(0x4E); Wire.write(0x00); Wire.endTransmission(); return false; } // Read first touch point (0x8150..0x8157) Wire.beginTransmission(GT911_ADDR); Wire.write(0x81); Wire.write(0x50); Wire.endTransmission(); Wire.requestFrom((uint8_t)GT911_ADDR, (uint8_t)4); if (Wire.available() >= 4) { uint8_t xl = Wire.read(); uint8_t xh = Wire.read(); uint8_t yl = Wire.read(); uint8_t yh = Wire.read(); x = (xh << 8) | xl; y = (yh << 8) | yl; } // Clear status Wire.beginTransmission(GT911_ADDR); Wire.write(0x81); Wire.write(0x4E); Wire.write(0x00); Wire.endTransmission(); return true; } // ═══════════════════════════════════════════════════════════ // Display Init // ═══════════════════════════════════════════════════════════ void DisplayDriverGFX::begin() { // 1. IO expander first (controls resets + backlight) ch422gInit(); // 2. Create RGB bus with corrected Waveshare timing Arduino_ESP32RGBPanel* rgbPanel = new Arduino_ESP32RGBPanel( LCD_DE, LCD_VSYNC, LCD_HSYNC, LCD_PCLK, LCD_R0, LCD_R1, LCD_R2, LCD_R3, LCD_R4, LCD_G0, LCD_G1, LCD_G2, LCD_G3, LCD_G4, LCD_G5, LCD_B0, LCD_B1, LCD_B2, LCD_B3, LCD_B4, // ─── Corrected timing for ST7262 / Waveshare 4.3" ─── 1, // hsync_polarity 10, // hsync_front_porch 8, // hsync_pulse_width 50, // hsync_back_porch 1, // vsync_polarity 10, // vsync_front_porch 8, // vsync_pulse_width 20, // vsync_back_porch 1, // pclk_active_neg *** CRITICAL — must be 1 *** 16000000 // prefer_speed = 16 MHz PCLK ); // 3. Create display _gfx = new Arduino_RGB_Display( DISPLAY_WIDTH, DISPLAY_HEIGHT, rgbPanel, DISPLAY_ROTATION, true // auto_flush ); if (!_gfx->begin()) { Serial.println("[GFX] *** Display init FAILED ***"); return; } Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT); // PSRAM diagnostic Serial.printf("[MEM] Free heap: %d | Free PSRAM: %d\n", ESP.getFreeHeap(), ESP.getFreePsram()); if (ESP.getFreePsram() == 0) { Serial.println("[MEM] *** WARNING: PSRAM not detected! " "Display will likely be blank. " "Ensure PSRAM=opi in board config. ***"); } // 4. Init touch gt911Init(); // 5. Show boot screen (backlight still off) _gfx->fillScreen(BLACK); drawBoot(); // 6. Backlight ON exioSet(EXIO_LCD_BL, true); Serial.println("[GFX] Backlight ON"); } void DisplayDriverGFX::setBacklight(bool on) { exioSet(EXIO_LCD_BL, on); } // ═══════════════════════════════════════════════════════════ // Rendering // ═══════════════════════════════════════════════════════════ void DisplayDriverGFX::render(const ScreenState& st) { if (st.screen != _lastScreen) { _needsRedraw = true; _lastScreen = st.screen; } switch (st.screen) { case ScreenID::BOOT: if (_needsRedraw) { drawBoot(); _needsRedraw = false; } break; case ScreenID::ALERT: drawAlert(st); // redraws every frame (pulse animation) break; case ScreenID::DASHBOARD: if (_needsRedraw) { drawDashboard(st); _needsRedraw = false; } break; case ScreenID::OFF: if (_needsRedraw) { _gfx->fillScreen(BLACK); _needsRedraw = false; } break; } } void DisplayDriverGFX::drawBoot() { _gfx->fillScreen(BLACK); _gfx->setTextColor(WHITE); _gfx->setTextSize(3); _gfx->setCursor(40, 40); _gfx->printf("KLUBHAUS ALERT v%s", FW_VERSION); _gfx->setTextSize(2); _gfx->setCursor(40, 100); _gfx->print(BOARD_NAME); _gfx->setCursor(40, 140); _gfx->print("Booting..."); } void DisplayDriverGFX::drawAlert(const ScreenState& st) { // Pulsing red background uint32_t elapsed = millis() - st.alertStartMs; uint8_t pulse = 180 + (uint8_t)(75.0f * sinf(elapsed / 300.0f)); uint16_t bg = _gfx->color565(pulse, 0, 0); _gfx->fillScreen(bg); _gfx->setTextColor(WHITE); // Title _gfx->setTextSize(5); _gfx->setCursor(40, 80); _gfx->print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT"); // Body _gfx->setTextSize(3); _gfx->setCursor(40, 200); _gfx->print(st.alertBody); // Hold hint _gfx->setTextSize(2); _gfx->setCursor(40, DISPLAY_HEIGHT - 60); _gfx->print("Hold to silence..."); } void DisplayDriverGFX::drawDashboard(const ScreenState& st) { _gfx->fillScreen(BLACK); _gfx->setTextColor(WHITE); // Title bar _gfx->setTextSize(2); _gfx->setCursor(20, 10); _gfx->printf("KLUBHAUS — %s", deviceStateStr(st.deviceState)); // Info tiles (simple text grid) int y = 60; int dy = 50; _gfx->setTextSize(2); _gfx->setCursor(20, y); _gfx->printf("WiFi: %s RSSI: %d", st.wifiSsid.c_str(), st.wifiRssi); y += dy; _gfx->setCursor(20, y); _gfx->printf("IP: %s", st.ipAddr.c_str()); y += dy; _gfx->setCursor(20, y); _gfx->printf("Uptime: %lus", st.uptimeMs / 1000); y += dy; _gfx->setCursor(20, y); _gfx->printf("Heap: %d PSRAM: %d", ESP.getFreeHeap(), ESP.getFreePsram()); y += dy; _gfx->setCursor(20, y); _gfx->printf("Last poll: %lus ago", st.lastPollMs > 0 ? (millis() - st.lastPollMs) / 1000 : 0); } // ═══════════════════════════════════════════════════════════ // Touch // ═══════════════════════════════════════════════════════════ TouchEvent DisplayDriverGFX::readTouch() { TouchEvent evt; int x, y; if (gt911Read(x, y)) { evt.pressed = true; evt.x = x; evt.y = y; } return evt; } int DisplayDriverGFX::dashboardTouch(int x, int y) { // Simple 2-column, 4-row tile grid int col = x / (DISPLAY_WIDTH / 2); int row = (y - 60) / 80; if (row < 0 || row > 3) return -1; return row * 2 + col; } HoldState DisplayDriverGFX::updateHold(unsigned long holdMs) { HoldState h; TouchEvent t = readTouch(); if (t.pressed) { if (!_holdActive) { _holdActive = true; _holdStartMs = millis(); } uint32_t held = millis() - _holdStartMs; h.active = true; h.progress = constrain((float)held / (float)holdMs, 0.0f, 1.0f); h.completed = (held >= holdMs); // Draw progress arc if (h.active && !h.completed) { int cx = DISPLAY_WIDTH / 2; int cy = DISPLAY_HEIGHT / 2; int r = 100; float angle = h.progress * 360.0f; // Simple progress: draw filled arc sector for (float a = 0; a < angle; a += 2.0f) { float rad = a * PI / 180.0f; int px = cx + (int)(r * cosf(rad - PI / 2)); int py = cy + (int)(r * sinf(rad - PI / 2)); _gfx->fillCircle(px, py, 4, WHITE); } } } else { _holdActive = false; } _lastTouched = t.pressed; return h; } void DisplayDriverGFX::updateHint() { // Subtle pulsing ring to hint "hold here" float t = (millis() % 2000) / 2000.0f; uint8_t alpha = (uint8_t)(60.0f + 40.0f * sinf(t * 2 * PI)); uint16_t col = _gfx->color565(alpha, alpha, alpha); int cx = DISPLAY_WIDTH / 2; int cy = DISPLAY_HEIGHT / 2 + 60; _gfx->drawCircle(cx, cy, 80, col); _gfx->drawCircle(cx, cy, 81, col); }