diff --git a/.gitignore b/.gitignore index 5562009..3fc7a0f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Vendored libraries (re-created by install-libs) vendor/esp32-32e/TFT_eSPI/ +vendor/esp32-32e-4/TFT_eSPI/ vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino/ # IDE diff --git a/boards/esp32-32e-4/DisplayDriverTFT.cpp b/boards/esp32-32e-4/DisplayDriverTFT.cpp new file mode 100644 index 0000000..acb22c5 --- /dev/null +++ b/boards/esp32-32e-4/DisplayDriverTFT.cpp @@ -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(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); +} diff --git a/boards/esp32-32e-4/DisplayDriverTFT.h b/boards/esp32-32e-4/DisplayDriverTFT.h new file mode 100644 index 0000000..c8e1c18 --- /dev/null +++ b/boards/esp32-32e-4/DisplayDriverTFT.h @@ -0,0 +1,31 @@ +#pragma once + +#include "board_config.h" + +#include +#include + +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; +}; diff --git a/boards/esp32-32e-4/board_config.h b/boards/esp32-32e-4/board_config.h new file mode 100644 index 0000000..52612cf --- /dev/null +++ b/boards/esp32-32e-4/board_config.h @@ -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 \ No newline at end of file diff --git a/boards/esp32-32e-4/esp32-32e-4.ino b/boards/esp32-32e-4/esp32-32e-4.ino new file mode 100644 index 0000000..56f0cb4 --- /dev/null +++ b/boards/esp32-32e-4/esp32-32e-4.ino @@ -0,0 +1,71 @@ +// +// Klubhaus Doorbell — ESP32-32E-4" target +// + +#include "DisplayDriverTFT.h" +#include "board_config.h" +#include "secrets.h" + +#include + +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); + } +} \ No newline at end of file diff --git a/boards/esp32-32e-4/secrets.h.example b/boards/esp32-32e-4/secrets.h.example new file mode 100644 index 0000000..82f6704 --- /dev/null +++ b/boards/esp32-32e-4/secrets.h.example @@ -0,0 +1,11 @@ +#pragma once +#include + +// 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]); diff --git a/boards/esp32-32e-4/tft_user_setup.h b/boards/esp32-32e-4/tft_user_setup.h new file mode 100644 index 0000000..5f36bef --- /dev/null +++ b/boards/esp32-32e-4/tft_user_setup.h @@ -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 \ No newline at end of file diff --git a/mise.toml b/mise.toml index 0ef8b07..a2349e4 100644 --- a/mise.toml +++ b/mise.toml @@ -26,6 +26,22 @@ cp boards/esp32-32e/tft_user_setup.h vendor/esp32-32e/TFT_eSPI/User_Setup.h echo "[OK] TFT_eSPI 2.5.43 vendored + configured" """ +[tasks.install-libs-32e-4] +description = "Vendor TFT_eSPI into vendor/esp32-32e-4 (ST7796 320x480)" +run = """ +#!/usr/bin/env bash +set -euo pipefail +if [ ! -d "vendor/esp32-32e-4/TFT_eSPI" ]; then + echo "Cloning TFT_eSPI..." + git clone --depth 1 --branch V2.5.43 \ + https://github.com/Bodmer/TFT_eSPI.git \ + vendor/esp32-32e-4/TFT_eSPI +fi +echo "Copying board-specific User_Setup.h..." +cp boards/esp32-32e-4/tft_user_setup.h vendor/esp32-32e-4/TFT_eSPI/User_Setup.h +echo "[OK] TFT_eSPI 2.5.43 vendored + configured for 4 inch" +""" + [tasks.install-libs-s3-43] description = "Vendor LovyanGFX into vendor/esp32-s3-lcd-43" run = """ @@ -68,7 +84,7 @@ echo "[OK] LovyanGFX vendored" [tasks.install-libs] description = "Install all libraries (shared + vendored)" -depends = ["install-libs-shared", "install-libs-32e", "install-libs-s3-43"] +depends = ["install-libs-shared", "install-libs-32e", "install-libs-32e-4", "install-libs-s3-43"] # ── ESP32-32E ──────────────────────────────────────────── @@ -77,7 +93,7 @@ description = "Compile ESP32-32E sketch" depends = ["install-libs"] run = """ arduino-cli compile \ - --fqbn "esp32:esp32@2.0.11:esp32:FlashSize=4M,PartitionScheme=default" \ + --fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \ --libraries ./libraries \ --libraries ./vendor/esp32-32e/TFT_eSPI \ --build-property "compiler.cpp.extra_flags=-DDEBUG_MODE -DBOARD_HAS_PSRAM" \ @@ -100,6 +116,36 @@ run = """ arduino-cli monitor --port "${PORT:-/dev/ttyUSB0}" --config baudrate=115200 """ +# ── ESP32-32E-4 inch ───────────────────────────────────────── + +[tasks.compile-32e-4] +description = "Compile ESP32-32E-4 inch sketch (ST7796 320x480)" +depends = ["install-libs-32e-4"] +run = """ +arduino-cli compile \ + --fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \ + --libraries ./libraries \ + --libraries ./vendor/esp32-32e-4/TFT_eSPI \ + --build-property "compiler.cpp.extra_flags=-DDEBUG_MODE -DBOARD_HAS_PSRAM" \ + --warnings default \ + ./boards/esp32-32e-4 +""" + +[tasks.upload-32e-4] +description = "Upload to ESP32-32E-4\"" +run = """ +arduino-cli upload \ + --fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \ + --port "${PORT:-/dev/ttyUSB0}" \ + ./boards/esp32-32e-4 +""" + +[tasks.monitor-32e-4] +description = "Serial monitor for ESP32-32E-4\"" +run = """ +arduino-cli monitor --port "${PORT:-/dev/ttyUSB0}" --config baudrate=115200 +""" + # ── ESP32-S3-LCD-4.3 ──────────────────────────────────── [tasks.compile-s3-43] @@ -137,6 +183,7 @@ description = "Remove build artifacts" run = """ rm -rf vendor/ rm -rf boards/esp32-32e/build +rm -rf boards/esp32-32e-4/build rm -rf boards/esp32-s3-lcd-43/build echo "[OK] Build artifacts cleaned" """ @@ -147,6 +194,9 @@ clang-format -i --style=file \ boards/esp32-32e/*.cpp \ boards/esp32-32e/*.h \ boards/esp32-32e/*.ino \ + boards/esp32-32e-4/*.cpp \ + boards/esp32-32e-4/*.h \ + boards/esp32-32e-4/*.ino \ boards/esp32-s3-lcd-43/*.cpp \ boards/esp32-s3-lcd-43/*.h \ boards/esp32-s3-lcd-43/*.ino \