feat(boards): add ESP32-32E-4 (4" ST7796) board support

This commit is contained in:
2026-02-17 22:07:51 -08:00
parent a42415e64e
commit cbe7efe883
8 changed files with 405 additions and 2 deletions

View File

@@ -0,0 +1,178 @@
#include "DisplayDriverTFT.h"
void DisplayDriverTFT::begin() {
// Backlight
pinMode(PIN_LCD_BL, OUTPUT);
digitalWrite(PIN_LCD_BL, LOW);
_tft.init();
_tft.setRotation(DISPLAY_ROTATION);
_tft.fillScreen(TFT_BLACK);
Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT);
drawBoot();
digitalWrite(PIN_LCD_BL, HIGH);
Serial.println("[GFX] Backlight ON");
}
void DisplayDriverTFT::setBacklight(bool on) { digitalWrite(PIN_LCD_BL, on ? HIGH : LOW); }
// ── Rendering ───────────────────────────────────────────────
void DisplayDriverTFT::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);
break;
case ScreenID::DASHBOARD:
if(_needsRedraw) {
drawDashboard(st);
_needsRedraw = false;
}
break;
case ScreenID::OFF:
if(_needsRedraw) {
_tft.fillScreen(TFT_BLACK);
_needsRedraw = false;
}
break;
}
}
void DisplayDriverTFT::drawBoot() {
_tft.fillScreen(TFT_BLACK);
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
_tft.setTextSize(2);
_tft.setCursor(10, 10);
_tft.printf("KLUBHAUS v%s", FW_VERSION);
_tft.setTextSize(1);
_tft.setCursor(10, 40);
_tft.print(BOARD_NAME);
_tft.setCursor(10, 60);
_tft.print("Booting...");
}
void DisplayDriverTFT::drawAlert(const ScreenState& st) {
uint32_t elapsed = millis() - st.alertStartMs;
uint8_t pulse = 180 + (uint8_t)(75.0f * sinf(elapsed / 300.0f));
uint16_t bg = _tft.color565(pulse, 0, 0);
_tft.fillScreen(bg);
_tft.setTextColor(TFT_WHITE, bg);
_tft.setTextSize(3);
_tft.setCursor(10, 20);
_tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT");
_tft.setTextSize(2);
_tft.setCursor(10, 80);
_tft.print(st.alertBody);
_tft.setTextSize(1);
_tft.setCursor(10, DISPLAY_HEIGHT - 20);
_tft.print("Hold to silence...");
}
void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
_tft.fillScreen(TFT_BLACK);
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
_tft.setTextSize(1);
_tft.setCursor(5, 5);
_tft.printf("KLUBHAUS — %s", deviceStateStr(st.deviceState));
int y = 30;
_tft.setCursor(5, y);
y += 18;
_tft.printf("WiFi: %s %ddBm", st.wifiSsid.c_str(), st.wifiRssi);
_tft.setCursor(5, y);
y += 18;
_tft.printf("IP: %s", st.ipAddr.c_str());
_tft.setCursor(5, y);
y += 18;
_tft.printf("Up: %lus Heap: %d", st.uptimeMs / 1000, ESP.getFreeHeap());
_tft.setCursor(5, y);
y += 18;
_tft.printf("Last poll: %lus ago", st.lastPollMs > 0 ? (millis() - st.lastPollMs) / 1000 : 0);
}
// ── Touch ───────────────────────────────────────────────────
TouchEvent DisplayDriverTFT::readTouch() {
TouchEvent evt;
uint16_t tx, ty;
if(_tft.getTouch(&tx, &ty)) {
evt.pressed = true;
evt.x = tx;
evt.y = ty;
}
return evt;
}
int DisplayDriverTFT::dashboardTouch(int x, int y) {
// 2x2 grid, accounting for 30px header
if(y < 30)
return -1;
int col = (x * 2) / DISPLAY_WIDTH; // 0 or 1
int row = ((y - 30) * 2) / (DISPLAY_HEIGHT - 30); // 0 or 1
if(col < 0 || col > 1 || row < 0 || row > 1)
return -1;
return row * 2 + col; // 0, 1, 2, or 3
}
HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) {
HoldState h;
TouchEvent t = readTouch();
if(t.pressed) {
if(!_holdActive) {
_holdActive = true;
_holdStartMs = millis();
h.started = true;
}
uint32_t held = millis() - _holdStartMs;
h.active = true;
h.progress = constrain((float)held / (float)holdMs, 0.0f, 1.0f);
h.completed = (held >= holdMs);
// Simple progress bar at bottom of screen
int barW = (int)(DISPLAY_WIDTH * h.progress);
_tft.fillRect(0, DISPLAY_HEIGHT - 8, barW, 8, TFT_WHITE);
_tft.fillRect(barW, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH - barW, 8, TFT_DARKGREY);
} else {
if(_holdActive) {
// Clear the progress bar when released
_tft.fillRect(0, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH, 8, TFT_DARKGREY);
}
_holdActive = false;
}
return h;
}
void DisplayDriverTFT::updateHint(int x, int y, bool active) {
float period = active ? 500.0f : 2000.0f;
float t = fmodf(millis(), period) / period;
uint8_t v = static_cast<uint8_t>(30.0f + 30.0f * sinf(t * 2.0f * PI));
uint16_t col = _tft.color565(v, v, v);
_tft.drawRect(x - 40, y - 20, 80, 40, col);
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include "board_config.h"
#include <KlubhausCore.h>
#include <TFT_eSPI.h>
class DisplayDriverTFT : public IDisplayDriver {
public:
void begin() override;
void setBacklight(bool on) override;
void render(const ScreenState& state) override;
TouchEvent readTouch() override;
int dashboardTouch(int x, int y) override;
HoldState updateHold(unsigned long holdMs) override;
void updateHint(int x, int y, bool active) override;
int width() override { return DISPLAY_WIDTH; }
int height() override { return DISPLAY_HEIGHT; }
private:
void drawBoot();
void drawAlert(const ScreenState& st);
void drawDashboard(const ScreenState& st);
TFT_eSPI _tft;
bool _holdActive = false;
uint32_t _holdStartMs = 0;
ScreenID _lastScreen = ScreenID::BOOT;
bool _needsRedraw = true;
};

View File

@@ -0,0 +1,18 @@
#pragma once
#define BOARD_NAME "WS_32E_4"
// ══════════════════════════════════════════════════════════
// Hosyond ESP32-32E 4" (320x480) with ST7796 + XPT2046
// Pin mapping from lcdwiki.com/4.0inch_ESP32-32E_Display
// ══════════════════════════════════════════════════════════
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 480
#define DISPLAY_ROTATION 1 // landscape
// Backlight GPIO (HIGH = on)
#define PIN_LCD_BL 27
// Touch — XPT2046 configured in tft_user_setup.h
// Touch CS: GPIO33, Touch IRQ: GPIO36

View File

@@ -0,0 +1,71 @@
//
// Klubhaus Doorbell — ESP32-32E-4" target
//
#include "DisplayDriverTFT.h"
#include "board_config.h"
#include "secrets.h"
#include <KlubhausCore.h>
DisplayDriverTFT tftDriver;
DisplayManager display(&tftDriver);
DoorbellLogic logic(&display);
void setup() {
Serial.begin(115200);
delay(500);
logic.begin(FW_VERSION, BOARD_NAME, wifiNetworks, wifiNetworkCount);
logic.finishBoot();
}
void loop() {
// ── State machine tick ──
logic.update();
// ── Render current screen ──
display.render(logic.getScreenState());
// ── Touch handling ──
const ScreenState& st = logic.getScreenState();
static int holdStartX = -1;
static int holdStartY = -1;
if(st.deviceState == DeviceState::ALERTING) {
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
if(h.completed) {
logic.silenceAlert();
holdStartX = -1;
holdStartY = -1;
}
if(h.started) {
TouchEvent t = display.readTouch();
holdStartX = t.x;
holdStartY = t.y;
}
if(holdStartX >= 0) {
display.updateHint(holdStartX, holdStartY, h.active);
}
} else {
holdStartX = -1;
holdStartY = -1;
}
if(st.screen == ScreenID::DASHBOARD) {
TouchEvent evt = display.readTouch();
if(evt.pressed) {
int tile = display.dashboardTouch(evt.x, evt.y);
if(tile >= 0)
Serial.printf("[DASH] Tile %d tapped\n", tile);
}
}
// ── Serial console ──
if(Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if(cmd.length() > 0)
logic.onSerialCommand(cmd);
}
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include <KlubhausCore.h>
// Copy this file to secrets.h and fill in your credentials.
// secrets.h is gitignored.
static const WiFiCred wifiNetworks[] = {
{ "Your_SSID_1", "password1" },
{ "Your_SSID_2", "password2" },
};
static const int wifiNetworkCount = sizeof(wifiNetworks) / sizeof(wifiNetworks[0]);

View File

@@ -0,0 +1,43 @@
// ══════════════════════════════════════════════════════════
// TFT_eSPI User_Setup for ESP32-32E-4" (Hosyond 320x480)
// This file is copied over vendor/esp32-32e-4/TFT_eSPI/User_Setup.h
// by the install-libs-32e-4 task.
// ══════════════════════════════════════════════════════════
#define USER_SETUP_ID 201
// ── Driver ──
#define ST7796_DRIVER
// ── Resolution ──
#define TFT_WIDTH 320
#define TFT_HEIGHT 480
// ── SPI Pins (from lcdwiki.com/4.0inch_ESP32-32E_Display) ──
#define TFT_MOSI 13
#define TFT_SCLK 14
#define TFT_CS 15
#define TFT_DC 2
#define TFT_RST -1 // tied to EN, handled by board
// ── Backlight ──
#define TFT_BL 27
#define TFT_BACKLIGHT_ON HIGH
// ── Touch (XPT2046 resistive) ──
#define TOUCH_CS 33
// ── SPI speed ──
#define SPI_FREQUENCY 40000000
#define SPI_READ_FREQUENCY 20000000
#define SPI_TOUCH_FREQUENCY 2500000
// ── Misc ──
#define LOAD_GLCD
#define LOAD_FONT2
#define LOAD_FONT4
#define LOAD_FONT6
#define LOAD_FONT7
#define LOAD_FONT8
#define LOAD_GFXFF
#define SMOOTH_FONT