refactor(doorbell): migrate ESP32-S3 to ESP_IOExpander library

This commit is contained in:
2026-02-16 20:51:56 -08:00
parent c7ea8d5817
commit 89f3248747
6 changed files with 225 additions and 2126 deletions

View File

@@ -1,157 +1,176 @@
#include "DisplayDriverGFX.h"
#include "board_config.h"
#include <ESP_IOExpander_Library.h>
#include <Wire.h>
// Arduino_GFX uses RGB565 values directly; define convenience names
#ifndef BLACK
#define BLACK 0x0000
#endif
#ifndef WHITE
#define WHITE 0xFFFF
#endif
#ifndef RED
#define RED 0xF800
#endif
#ifndef GREEN
#define GREEN 0x07E0
#endif
#ifndef BLUE
#define BLUE 0x001F
#endif
#ifndef YELLOW
#define YELLOW 0xFFE0
#endif
#ifndef CYAN
#define CYAN 0x07FF
#endif
#ifndef MAGENTA
#define MAGENTA 0xF81F
#endif
#ifndef ORANGE
#define ORANGE 0xFD20
#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
// ═══════════════════════════════════════════════════════════
// CH422G IO Expander
// ═══════════════════════════════════════════════════════════
static ESP_IOExpander* expander = nullptr;
void DisplayDriverGFX::ch422gInit() {
Wire.begin(I2C_SDA, I2C_SCL, I2C_FREQ);
// ── IO Expander ──
// 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);
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::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;
void DisplayDriverGFX::setBacklight(bool on) {
if (expander) {
expander->digitalWrite(EXIO_LCD_BL, on ? HIGH : LOW);
Serial.printf("[GFX] Backlight %s\n", on ? "ON" : "OFF");
}
}
// Read first touch point (0x8150..0x8157)
// ── Touch ──
void DisplayDriverGFX::touchInit() {
Wire.begin(I2C_MASTER_SDA, I2C_MASTER_SCL);
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;
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.write(0x81);
Wire.write(0x4E);
Wire.write(0x00);
Wire.endTransmission();
return true;
return ev;
}
// ═══════════════════════════════════════════════════════════
// Display Init
// ═══════════════════════════════════════════════════════════
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. IO expander first (controls resets + backlight)
ch422gInit();
// 1. Touch (I2C on GPIO 8/9)
touchInit();
delay(200);
// 2. Create RGB bus with corrected Waveshare timing
// 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,
// ─── 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
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
);
// 3. Create display
_gfx = new Arduino_RGB_Display(
DISPLAY_WIDTH, DISPLAY_HEIGHT, rgbPanel,
DISPLAY_ROTATION,
true // auto_flush
DISPLAY_ROTATION, true
);
if (!_gfx->begin()) {
@@ -160,197 +179,74 @@ void DisplayDriverGFX::begin() {
}
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)
// 4. Clear and backlight on
_gfx->fillScreen(BLACK);
setBacklight(true);
drawBoot();
// 6. Backlight ON
exioSet(EXIO_LCD_BL, true);
Serial.println("[GFX] Backlight ON");
}
void DisplayDriverGFX::setBacklight(bool on) {
exioSet(EXIO_LCD_BL, on);
}
int DisplayDriverGFX::width() { return _gfx ? _gfx->width() : DISPLAY_WIDTH; }
int DisplayDriverGFX::height() { return _gfx ? _gfx->height() : DISPLAY_HEIGHT; }
// ═══════════════════════════════════════════════════════════
// Rendering
// ═══════════════════════════════════════════════════════════
// ── Render (state machine driven) ──
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::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(40, 40);
_gfx->printf("KLUBHAUS ALERT v%s", FW_VERSION);
_gfx->setCursor(20, 20);
_gfx->println("KLUBHAUS DASHBOARD");
_gfx->setTextSize(2);
_gfx->setCursor(40, 100);
_gfx->print(BOARD_NAME);
_gfx->setCursor(40, 140);
_gfx->print("Booting...");
_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& 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);
void DisplayDriverGFX::drawAlert(const ScreenState& state) {
if (!_gfx) return;
_gfx->fillScreen(RED);
_gfx->setTextColor(WHITE);
// Title
_gfx->setTextSize(5);
_gfx->setCursor(40, 80);
_gfx->print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT");
// Body
_gfx->setCursor(200, 180);
_gfx->println("DOORBELL!");
_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...");
_gfx->setCursor(100, 280);
_gfx->println(state.alertMessage ? state.alertMessage : "Someone is at the door");
}
void DisplayDriverGFX::drawDashboard(const ScreenState& st) {
void DisplayDriverGFX::drawOff() {
if (!_gfx) return;
_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);
setBacklight(false);
}
// ═══════════════════════════════════════════════════════════
// 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);
}