#include "DisplayDriverGFX.h" #include "board_config.h" #include #include #ifndef BLACK #define BLACK 0x0000 #endif #ifndef WHITE #define WHITE 0xFFFF #endif #ifndef RED #define RED 0xF800 #endif // CH422G logical pin numbers #define EXIO_TP_RST IO_EXPANDER_PIN_NUM_1 #define EXIO_LCD_BL IO_EXPANDER_PIN_NUM_2 #define EXIO_LCD_RST IO_EXPANDER_PIN_NUM_3 #define EXIO_SD_CS IO_EXPANDER_PIN_NUM_4 #define EXIO_USB_SEL IO_EXPANDER_PIN_NUM_5 static ESP_IOExpander* expander = nullptr; // ── IO Expander ── void DisplayDriverGFX::expanderInit() { Serial.println("[IO] IO expander init..."); expander = new ESP_IOExpander_CH422G( I2C_MASTER_NUM, ESP_IO_EXPANDER_I2C_CH422G_ADDRESS ); expander->init(); expander->begin(); expander->multiPinMode( EXIO_TP_RST | EXIO_LCD_BL | EXIO_LCD_RST | EXIO_SD_CS | EXIO_USB_SEL, OUTPUT ); // Deassert resets, backlight OFF for now expander->multiDigitalWrite( EXIO_TP_RST | EXIO_LCD_RST | EXIO_SD_CS, 0xFF ); expander->digitalWrite(EXIO_LCD_BL, LOW); Serial.println("[IO] CH422G initialized"); } void DisplayDriverGFX::setBacklight(bool on) { if (expander) { expander->digitalWrite(EXIO_LCD_BL, on ? HIGH : LOW); Serial.printf("[GFX] Backlight %s\n", on ? "ON" : "OFF"); } } // ── Touch ── void DisplayDriverGFX::touchInit() { Wire.begin(I2C_MASTER_SDA, I2C_MASTER_SCL); Wire.beginTransmission(GT911_ADDR); uint8_t err = Wire.endTransmission(); if (err == 0) { Serial.println("[TOUCH] GT911 initialized"); } else { Serial.printf("[TOUCH] GT911 not found (I2C err %d)\n", err); } } TouchEvent DisplayDriverGFX::readTouch() { TouchEvent ev = { false, 0, 0 }; Wire.beginTransmission(GT911_ADDR); if (Wire.endTransmission() != 0) return ev; // Read touch status register (0x814E) Wire.beginTransmission(GT911_ADDR); Wire.write(0x81); Wire.write(0x4E); Wire.endTransmission(false); Wire.requestFrom((uint8_t)GT911_ADDR, (uint8_t)1); if (!Wire.available()) return ev; uint8_t status = Wire.read(); uint8_t touches = status & 0x0F; if ((status & 0x80) && touches > 0 && touches <= 5) { // Read first touch point (0x8150) Wire.beginTransmission(GT911_ADDR); Wire.write(0x81); Wire.write(0x50); Wire.endTransmission(false); 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(); ev.pressed = true; ev.x = (xh << 8) | xl; ev.y = (yh << 8) | yl; } } // Clear status Wire.beginTransmission(GT911_ADDR); Wire.write(0x81); Wire.write(0x4E); Wire.write(0x00); Wire.endTransmission(); return ev; } int DisplayDriverGFX::dashboardTouch(int x, int y) { // Simple 2x2 grid: 4 tiles int col = (x < DISPLAY_WIDTH / 2) ? 0 : 1; int row = (y < DISPLAY_HEIGHT / 2) ? 0 : 1; return row * 2 + col; } HoldState DisplayDriverGFX::updateHold(unsigned long holdMs) { TouchEvent ev = readTouch(); if (ev.pressed) { if (!_lastTouched) { _holdStart = millis(); _lastTouched = true; } unsigned long elapsed = millis() - _holdStart; if (elapsed >= holdMs) { return {false, true}; } return {true, false}; } else { _lastTouched = false; _holdStart = 0; return {false, false}; } } void DisplayDriverGFX::updateHint() { // placeholder for idle hint animation } // ── Display ── void DisplayDriverGFX::begin() { // 1. Touch (I2C on GPIO 8/9) touchInit(); delay(200); // 2. IO expander expanderInit(); // 3. RGB display Serial.println("[GFX] GFX init..."); 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, 0, // hsync_polarity 40, // hsync_front_porch 48, // hsync_pulse_width 88, // hsync_back_porch 0, // vsync_polarity 13, // vsync_front_porch 3, // vsync_pulse_width 32, // vsync_back_porch 1, // pclk_active_neg 16000000 // prefer_speed ); _gfx = new Arduino_RGB_Display( DISPLAY_WIDTH, DISPLAY_HEIGHT, rgbPanel, DISPLAY_ROTATION, true ); if (!_gfx->begin()) { Serial.println("[GFX] *** Display init FAILED ***"); return; } Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT); Serial.printf("[MEM] Free heap: %d | Free PSRAM: %d\n", ESP.getFreeHeap(), ESP.getFreePsram()); // 4. Clear and backlight on _gfx->fillScreen(BLACK); setBacklight(true); drawBoot(); } int DisplayDriverGFX::width() { return _gfx ? _gfx->width() : DISPLAY_WIDTH; } int DisplayDriverGFX::height() { return _gfx ? _gfx->height() : DISPLAY_HEIGHT; } // ── Render (state machine driven) ── void DisplayDriverGFX::render(const ScreenState& state) { if (!_gfx) return; switch (state.screen) { case ScreenID::BOOT_SPLASH: drawBoot(); break; case ScreenID::DASHBOARD: drawDashboard(state); break; case ScreenID::ALERT: drawAlert(state); break; case ScreenID::OFF: drawOff(); break; default: drawBoot(); break; } } void DisplayDriverGFX::drawBoot() { if (!_gfx) return; _gfx->fillScreen(BLACK); _gfx->setTextColor(WHITE); _gfx->setTextSize(4); _gfx->setCursor(250, 200); _gfx->println("Klubhaus Alert"); _gfx->setTextSize(2); _gfx->setCursor(300, 260); _gfx->println("Booting..."); } void DisplayDriverGFX::drawDashboard(const ScreenState& state) { if (!_gfx) return; _gfx->fillScreen(BLACK); _gfx->setTextColor(WHITE); _gfx->setTextSize(3); _gfx->setCursor(20, 20); _gfx->println("KLUBHAUS DASHBOARD"); _gfx->setTextSize(2); _gfx->setCursor(20, 80); _gfx->printf("IP: %s", state.wifiIP); _gfx->setCursor(20, 110); _gfx->printf("RSSI: %d dBm", state.wifiRSSI); _gfx->setCursor(20, 140); _gfx->printf("Time: %s", state.timeString); } void DisplayDriverGFX::drawAlert(const ScreenState& state) { if (!_gfx) return; _gfx->fillScreen(RED); _gfx->setTextColor(WHITE); _gfx->setTextSize(5); _gfx->setCursor(200, 180); _gfx->println("DOORBELL!"); _gfx->setTextSize(3); _gfx->setCursor(100, 280); _gfx->println(state.alertMessage ? state.alertMessage : "Someone is at the door"); } void DisplayDriverGFX::drawOff() { if (!_gfx) return; _gfx->fillScreen(BLACK); setBacklight(false); }