consolidate sketches

This commit is contained in:
2026-02-16 17:53:06 -08:00
parent 75c3f5706b
commit 838afaa36f
42 changed files with 5655 additions and 817 deletions

50
boards/board_e32r35t.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
// ═══════════════════════════════════════════════════════════════════
// Board: E32R35T — ESP32-WROOM-32E + 3.5" ST7796S SPI + XPT2046
// ═══════════════════════════════════════════════════════════════════
#define BOARD_NAME "E32R35T"
// ── Display ─────────────────────────────────────────────────────
#define SCREEN_WIDTH 480
#define SCREEN_HEIGHT 320
#define DISPLAY_ROTATION 1
// ── Driver selection ────────────────────────────────────────────
#define USE_TFT_ESPI 1
#define USE_ARDUINO_GFX 0
#define USE_TOUCH_XPT2046 1
#define USE_TOUCH_GT911 0
// ── Hardware capabilities ───────────────────────────────────────
#define HAS_PSRAM 0
// ── LCD (HSPI) ──────────────────────────────────────────────────
#define PIN_LCD_CS 15
#define PIN_LCD_DC 2
#define PIN_LCD_MOSI 13
#define PIN_LCD_SCLK 14
#define PIN_LCD_MISO 12
#define PIN_LCD_BL 27
// ── Touch (XPT2046, shares HSPI) ───────────────────────────────
#define PIN_TOUCH_CS 33
#define PIN_TOUCH_IRQ 36
// ── SD Card (VSPI — for future use) ────────────────────────────
#define PIN_SD_CS 5
#define PIN_SD_MOSI 23
#define PIN_SD_SCLK 18
#define PIN_SD_MISO 19
// ── RGB LED (active low) ───────────────────────────────────────
#define PIN_LED_RED 22
#define PIN_LED_GREEN 16
#define PIN_LED_BLUE 17
// ── Audio ───────────────────────────────────────────────────────
#define PIN_AUDIO_EN 4
#define PIN_AUDIO_DAC 26
// ── Battery ADC ─────────────────────────────────────────────────
#define PIN_BAT_ADC 34

View File

@@ -0,0 +1,73 @@
#pragma once
// ═══════════════════════════════════════════════════════════════════
// Board: Waveshare ESP32-S3 Touch LCD 4.3"
// 800x480 RGB parallel + GT911 capacitive touch
//
// NOTE: Pin assignments are typical for this board revision.
// Verify against your specific board's schematic.
// The Arduino board variant 'waveshare_esp32_s3_touch_lcd_43'
// may override some of these via its pins_arduino.h.
// ═══════════════════════════════════════════════════════════════════
#define BOARD_NAME "WS_S3_43"
// ── Display ─────────────────────────────────────────────────────
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 480
#define DISPLAY_ROTATION 0 // native landscape
// ── Driver selection ────────────────────────────────────────────
#define USE_TFT_ESPI 0
#define USE_ARDUINO_GFX 1
#define USE_TOUCH_XPT2046 0
#define USE_TOUCH_GT911 1
// ── Hardware capabilities ───────────────────────────────────────
#define HAS_PSRAM 1
// ── Backlight ───────────────────────────────────────────────────
#define PIN_LCD_BL 2
// ── GT911 I2C touch controller ──────────────────────────────────
#define TOUCH_SDA 17
#define TOUCH_SCL 18
#define TOUCH_INT -1
#define TOUCH_RST 38
// ── RGB LCD data pins (ESP32-S3 LCD_CAM peripheral) ─────────────
// Adjust if your board revision differs
#define LCD_DE 40
#define LCD_VSYNC 41
#define LCD_HSYNC 39
#define LCD_PCLK 42
#define LCD_R0 45
#define LCD_R1 48
#define LCD_R2 47
#define LCD_R3 21
#define LCD_R4 14
#define LCD_G0 5
#define LCD_G1 6
#define LCD_G2 7
#define LCD_G3 15
#define LCD_G4 16
#define LCD_G5 4
#define LCD_B0 8
#define LCD_B1 3
#define LCD_B2 46
#define LCD_B3 9
#define LCD_B4 1
// ── Peripherals not present on this board ───────────────────────
// These are left undefined intentionally. Code that uses them
// should guard with #ifdef PIN_LED_RED etc.
// Uncomment and set values if your carrier board adds them.
//
// #define PIN_LED_RED -1
// #define PIN_LED_GREEN -1
// #define PIN_LED_BLUE -1
// #define PIN_AUDIO_EN -1
// #define PIN_AUDIO_DAC -1
// #define PIN_BAT_ADC -1
// #define PIN_SD_CS -1
// #define PIN_TOUCH_CS -1
// #define PIN_TOUCH_IRQ -1

View File

@@ -0,0 +1,155 @@
#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) {
// 2-column, 4-row
int col = x / (DISPLAY_WIDTH / 2);
int row = (y - 30) / 40;
if (row < 0 || row > 3) return -1;
return row * 2 + col;
}
HoldState DisplayDriverTFT::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);
// 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 {
_holdActive = false;
}
return h;
}
void DisplayDriverTFT::updateHint() {
float t = (millis() % 2000) / 2000.0f;
uint8_t v = (uint8_t)(30.0f + 30.0f * sinf(t * 2 * PI));
uint16_t col = _tft.color565(v, v, v);
_tft.drawRect(DISPLAY_WIDTH / 2 - 40, DISPLAY_HEIGHT / 2 + 20, 80, 40, col);
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <KlubhausCore.h>
#include <TFT_eSPI.h>
#include "board_config.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() 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,22 @@
#pragma once
#define BOARD_NAME "WS_32E"
// ══════════════════════════════════════════════════════════
// TODO: Set these to match YOUR display + wiring.
// Defaults below are for a common ILI9341 320×240 SPI TFT.
// The actual pin mapping must also be set in tft_user_setup.h
// (which gets copied into the vendored TFT_eSPI library).
// ══════════════════════════════════════════════════════════
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define DISPLAY_ROTATION 1 // landscape
// Backlight GPIO (directly wired)
#define PIN_LCD_BL 22
// Touch — if using XPT2046 via TFT_eSPI, set TOUCH_CS in tft_user_setup.h
// If using capacitive touch (e.g. FT6236), configure I2C pins here:
// #define TOUCH_SDA 21
// #define TOUCH_SCL 22

View File

@@ -0,0 +1,54 @@
//
// Klubhaus Doorbell — ESP32-32E target
//
#include <KlubhausCore.h>
#include "board_config.h"
#include "secrets.h"
#include "DisplayDriverTFT.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();
if (st.deviceState == DeviceState::ALERTING) {
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
if (h.completed) {
logic.silenceAlert();
}
if (!h.active) {
display.updateHint();
}
} else {
TouchEvent evt = display.readTouch();
if (evt.pressed && st.screen == ScreenID::DASHBOARD) {
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,47 @@
// ═══════════════════════════════════════════════════════════
// TFT_eSPI User_Setup for ESP32-32E target
// This file is copied over vendor/esp32-32e/TFT_eSPI/User_Setup.h
// by the install-libs-32e task.
//
// TODO: Change the driver, pins, and dimensions to match your display.
// ═══════════════════════════════════════════════════════════
#define USER_SETUP_ID 200
// ── Driver ──
#define ILI9341_DRIVER
// #define ST7789_DRIVER
// #define ILI9488_DRIVER
// ── Resolution ──
#define TFT_WIDTH 240
#define TFT_HEIGHT 320
// ── SPI Pins ──
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS 5
#define TFT_DC 27
#define TFT_RST 33
// ── Backlight (optional, can also use GPIO directly) ──
// #define TFT_BL 22
// #define TFT_BACKLIGHT_ON HIGH
// ── Touch (XPT2046 resistive) ──
#define TOUCH_CS 14
// ── 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

View File

@@ -0,0 +1,325 @@
#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);
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <KlubhausCore.h>
#include <Arduino_GFX_Library.h>
#include <Wire.h>
#include "board_config.h"
class DisplayDriverGFX : 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() override;
int width() override { return DISPLAY_WIDTH; }
int height() override { return DISPLAY_HEIGHT; }
private:
// CH422G helpers
void ch422gInit();
void ch422gWrite(uint8_t val);
uint8_t _exioBits = 0;
void exioSet(uint8_t bit, bool on);
// GT911 helpers
void gt911Init();
bool gt911Read(int& x, int& y);
// Rendering
void drawBoot();
void drawAlert(const ScreenState& st);
void drawDashboard(const ScreenState& st);
Arduino_GFX* _gfx = nullptr;
// Hold-to-silence tracking
bool _holdActive = false;
uint32_t _holdStartMs = 0;
bool _lastTouched = false;
// Render-change tracking
ScreenID _lastScreen = ScreenID::BOOT;
bool _needsRedraw = true;
};

View File

@@ -0,0 +1,52 @@
#pragma once
#define BOARD_NAME "WS_S3_43"
#define DISPLAY_WIDTH 800
#define DISPLAY_HEIGHT 480
#define DISPLAY_ROTATION 0 // landscape, USB-C on left
// ── RGB parallel bus pins (directly to ST7262 panel) ──
#define LCD_DE 40
#define LCD_VSYNC 41
#define LCD_HSYNC 39
#define LCD_PCLK 42
#define LCD_R0 45
#define LCD_R1 48
#define LCD_R2 47
#define LCD_R3 21
#define LCD_R4 14
#define LCD_G0 5
#define LCD_G1 6
#define LCD_G2 7
#define LCD_G3 15
#define LCD_G4 16
#define LCD_G5 4
#define LCD_B0 8
#define LCD_B1 3
#define LCD_B2 46
#define LCD_B3 9
#define LCD_B4 1
// ── CH422G I2C IO expander ──
// Controls LCD_RST, TP_RST, LCD_BL, SD_CS via I2C
#define I2C_SDA 17
#define I2C_SCL 18
#define I2C_FREQ 100000
// CH422G I2C command addresses
#define CH422G_WRITE_OC 0x46
#define CH422G_SET_MODE 0x48
#define CH422G_READ_IN 0x4C
// EXIO bit positions
#define EXIO_TP_RST (1 << 0) // EXIO1
#define EXIO_LCD_BL (1 << 1) // EXIO2 — also drives DISP signal!
#define EXIO_LCD_RST (1 << 2) // EXIO3
#define EXIO_SD_CS (1 << 3) // EXIO4
// ── GT911 Touch ──
#define GT911_ADDR 0x5D
#define TOUCH_INT -1 // not wired to a readable GPIO on this board

View File

@@ -0,0 +1,54 @@
//
// Klubhaus Doorbell — ESP32-S3-Touch-LCD-4.3 target
//
#include <KlubhausCore.h>
#include "board_config.h"
#include "secrets.h"
#include "DisplayDriverGFX.h"
DisplayDriverGFX gfxDriver;
DisplayManager display(&gfxDriver);
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();
if (st.deviceState == DeviceState::ALERTING) {
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
if (h.completed) {
logic.silenceAlert();
}
if (!h.active) {
display.updateHint();
}
} else {
TouchEvent evt = display.readTouch();
if (evt.pressed && st.screen == ScreenID::DASHBOARD) {
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]);