#!/usr/bin/env bash set -euo pipefail # ═══════════════════════════════════════════════════════════════ # Klubhaus Doorbell — Project Scaffold v5.1 # Creates the full multi-target build with: # - Shared Arduino library (KlubhausCore) # - Per-board sketch directories # - Per-board vendored display libraries # - mise.toml multi-target build harness # ═══════════════════════════════════════════════════════════════ GREEN='\033[0;32m' BLUE='\033[0;34m' DIM='\033[2m' NC='\033[0m' info() { echo -e "${GREEN}[+]${NC} $1"; } section() { echo -e "\n${BLUE}━━━ $1 ━━━${NC}"; } section "Creating directory structure" mkdir -p libraries/KlubhausCore/src mkdir -p vendor/esp32-32e mkdir -p vendor/esp32-s3-lcd-43 mkdir -p boards/esp32-32e mkdir -p boards/esp32-s3-lcd-43 info "Directories created" # ───────────────────────────────────────────────────────────── section "Shared Library: KlubhausCore" # ───────────────────────────────────────────────────────────── # ── library.properties ────────────────────────────────────── cat << 'EOF' > libraries/KlubhausCore/library.properties name=KlubhausCore version=5.1.0 author=David maintainer=David sentence=Core logic for Klubhaus doorbell alert system paragraph=Shared state machine, ntfy.sh polling, networking, and abstract display interface. category=Other url=https://git.notsosm.art/david/klubhaus-doorbell architectures=esp32 includes=KlubhausCore.h depends=ArduinoJson,NTPClient EOF info "library.properties" # ── KlubhausCore.h (umbrella include) ────────────────────── cat << 'EOF' > libraries/KlubhausCore/src/KlubhausCore.h #pragma once // Umbrella header — board sketches just #include #include "Config.h" #include "ScreenState.h" #include "IDisplayDriver.h" #include "DisplayManager.h" #include "NetManager.h" #include "DoorbellLogic.h" EOF info "KlubhausCore.h" # ── Config.h (shared constants only — no pins, no secrets) ── cat << 'EOF' > libraries/KlubhausCore/src/Config.h #pragma once #define FW_VERSION "5.1" // ── ntfy.sh ── #define NTFY_SERVER "ntfy.sh" #define ALERT_TOPIC "ALERT_klubhaus_topic" #define SILENCE_TOPIC "SILENCE_klubhaus_topic" #define ADMIN_TOPIC "ADMIN_klubhaus_topic" #define STATUS_TOPIC "STATUS_klubhaus_topic" // ── Timing ── #define POLL_INTERVAL_MS 15000 #define HEARTBEAT_INTERVAL_MS 300000 #define BOOT_GRACE_MS 5000 #define HOLD_TO_SILENCE_MS 3000 #define ALERT_TIMEOUT_MS 120000 #define SILENCE_DISPLAY_MS 10000 #define WIFI_CONNECT_TIMEOUT_MS 15000 #define HTTP_TIMEOUT_MS 10000 // ── WiFi credential struct (populated in each board's secrets.h) ── struct WiFiCred { const char* ssid; const char* pass; }; EOF info "Config.h" # ── ScreenState.h ────────────────────────────────────────── cat << 'EOF' > libraries/KlubhausCore/src/ScreenState.h #pragma once #include enum class DeviceState { BOOTED, SILENT, ALERTING, SILENCED }; enum class ScreenID { BOOT, OFF, ALERT, DASHBOARD }; struct ScreenState { DeviceState deviceState = DeviceState::BOOTED; ScreenID screen = ScreenID::BOOT; String alertTitle; String alertBody; uint32_t alertStartMs = 0; uint32_t silenceStartMs = 0; bool backlightOn = false; int wifiRssi = 0; String wifiSsid; String ipAddr; uint32_t uptimeMs = 0; uint32_t lastPollMs = 0; uint32_t lastHeartbeatMs= 0; }; inline const char* deviceStateStr(DeviceState s) { switch (s) { case DeviceState::BOOTED: return "BOOTED"; case DeviceState::SILENT: return "SILENT"; case DeviceState::ALERTING: return "ALERTING"; case DeviceState::SILENCED: return "SILENCED"; } return "?"; } inline const char* screenIdStr(ScreenID s) { switch (s) { case ScreenID::BOOT: return "BOOT"; case ScreenID::OFF: return "OFF"; case ScreenID::ALERT: return "ALERT"; case ScreenID::DASHBOARD: return "DASHBOARD"; } return "?"; } EOF info "ScreenState.h" # ── IDisplayDriver.h (pure virtual interface) ────────────── cat << 'EOF' > libraries/KlubhausCore/src/IDisplayDriver.h #pragma once #include "ScreenState.h" struct TouchEvent { bool pressed = false; int x = 0; int y = 0; }; struct HoldState { bool active = false; bool completed = false; float progress = 0.0f; // 0.0 – 1.0 }; /// Abstract display driver — implemented per-board. class IDisplayDriver { public: virtual ~IDisplayDriver() = default; virtual void begin() = 0; virtual void setBacklight(bool on) = 0; /// Called every loop() iteration; draw the screen described by `state`. virtual void render(const ScreenState& state) = 0; // ── Touch ── virtual TouchEvent readTouch() = 0; /// Returns tile index at (x,y), or -1 if none. virtual int dashboardTouch(int x, int y) = 0; /// Track a long-press gesture; returns progress/completion. virtual HoldState updateHold(unsigned long holdMs) = 0; /// Idle hint animation (e.g. pulsing ring) while alert is showing. virtual void updateHint() = 0; virtual int width() = 0; virtual int height() = 0; }; EOF info "IDisplayDriver.h" # ── DisplayManager.h (thin delegation wrapper) ───────────── cat << 'EOF' > libraries/KlubhausCore/src/DisplayManager.h #pragma once #include "IDisplayDriver.h" /// Owns a pointer to the concrete driver; all calls delegate. /// Board sketch creates the concrete driver and passes it in. class DisplayManager { public: DisplayManager() : _drv(nullptr) {} explicit DisplayManager(IDisplayDriver* drv) : _drv(drv) {} void setDriver(IDisplayDriver* drv) { _drv = drv; } void begin() { if (_drv) _drv->begin(); } void setBacklight(bool on) { if (_drv) _drv->setBacklight(on); } void render(const ScreenState& st) { if (_drv) _drv->render(st); } TouchEvent readTouch() { return _drv ? _drv->readTouch() : TouchEvent{}; } int dashboardTouch(int x, int y) { return _drv ? _drv->dashboardTouch(x, y) : -1; } HoldState updateHold(unsigned long ms) { return _drv ? _drv->updateHold(ms) : HoldState{}; } void updateHint() { if (_drv) _drv->updateHint(); } int width() { return _drv ? _drv->width() : 0; } int height() { return _drv ? _drv->height() : 0; } private: IDisplayDriver* _drv; }; EOF info "DisplayManager.h" # ── NetManager.h ─────────────────────────────────────────── cat << 'EOF' > libraries/KlubhausCore/src/NetManager.h #pragma once #include #include #include #include #include #include #include #include "Config.h" class NetManager { public: void begin(const WiFiCred* creds, int count); bool checkConnection(); bool isConnected(); String getSSID(); String getIP(); int getRSSI(); bool syncNTP(); String getTimeStr(); bool dnsCheck(const char* host); bool tlsCheck(const char* host); /// HTTPS GET; writes body into `response`. Returns HTTP status code. int httpGet(const char* url, String& response); /// HTTPS POST with text/plain body. Returns HTTP status code. int httpPost(const char* url, const String& body); private: WiFiMulti _multi; WiFiUDP _udp; NTPClient* _ntp = nullptr; bool _ntpReady = false; }; EOF info "NetManager.h" # ── NetManager.cpp ───────────────────────────────────────── cat << 'EOF' > libraries/KlubhausCore/src/NetManager.cpp #include "NetManager.h" // ── WiFi ──────────────────────────────────────────────────── void NetManager::begin(const WiFiCred* creds, int count) { WiFi.mode(WIFI_STA); for (int i = 0; i < count; i++) _multi.addAP(creds[i].ssid, creds[i].pass); Serial.println("[WIFI] Connecting..."); unsigned long t0 = millis(); while (_multi.run() != WL_CONNECTED) { if (millis() - t0 > WIFI_CONNECT_TIMEOUT_MS) { Serial.println("[WIFI] Timeout!"); return; } delay(250); } Serial.printf("[WIFI] Connected: %s %s\n", getSSID().c_str(), getIP().c_str()); } bool NetManager::checkConnection() { if (WiFi.status() == WL_CONNECTED) return true; Serial.println("[WIFI] Reconnecting..."); return _multi.run(WIFI_CONNECT_TIMEOUT_MS) == WL_CONNECTED; } bool NetManager::isConnected() { return WiFi.status() == WL_CONNECTED; } String NetManager::getSSID() { return WiFi.SSID(); } String NetManager::getIP() { return WiFi.localIP().toString(); } int NetManager::getRSSI() { return WiFi.RSSI(); } // ── NTP ───────────────────────────────────────────────────── bool NetManager::syncNTP() { Serial.println("[NTP] Starting sync..."); if (!_ntp) _ntp = new NTPClient(_udp, "pool.ntp.org", 0, 60000); _ntp->begin(); _ntp->forceUpdate(); _ntpReady = _ntp->isTimeSet(); Serial.printf("[NTP] %s: %s UTC\n", _ntpReady ? "Synced" : "FAILED", _ntpReady ? _ntp->getFormattedTime().c_str() : "--"); return _ntpReady; } String NetManager::getTimeStr() { return (_ntp && _ntpReady) ? _ntp->getFormattedTime() : "??:??:??"; } // ── Diagnostics ───────────────────────────────────────────── bool NetManager::dnsCheck(const char* host) { IPAddress ip; bool ok = WiFi.hostByName(host, ip); Serial.printf("[NET] DNS %s: %s\n", ok ? "OK" : "FAIL", ok ? ip.toString().c_str() : ""); return ok; } bool NetManager::tlsCheck(const char* host) { WiFiClientSecure c; c.setInsecure(); c.setTimeout(HTTP_TIMEOUT_MS); bool ok = c.connect(host, 443); if (ok) c.stop(); Serial.printf("[NET] TLS %s\n", ok ? "OK" : "FAIL"); return ok; } // ── HTTP helpers ──────────────────────────────────────────── int NetManager::httpGet(const char* url, String& response) { WiFiClientSecure client; client.setInsecure(); client.setTimeout(HTTP_TIMEOUT_MS); HTTPClient http; http.setTimeout(HTTP_TIMEOUT_MS); http.begin(client, url); int code = http.GET(); if (code > 0) response = http.getString(); http.end(); return code; } int NetManager::httpPost(const char* url, const String& body) { WiFiClientSecure client; client.setInsecure(); client.setTimeout(HTTP_TIMEOUT_MS); HTTPClient http; http.setTimeout(HTTP_TIMEOUT_MS); http.begin(client, url); http.addHeader("Content-Type", "text/plain"); int code = http.POST(body); http.end(); return code; } EOF info "NetManager.cpp" # ── DoorbellLogic.h ──────────────────────────────────────── cat << 'EOF' > libraries/KlubhausCore/src/DoorbellLogic.h #pragma once #include #include #include "Config.h" #include "ScreenState.h" #include "DisplayManager.h" #include "NetManager.h" class DoorbellLogic { public: explicit DoorbellLogic(DisplayManager* display); /// Call from setup(). Pass board-specific WiFi creds. void begin(const char* version, const char* boardName, const WiFiCred* creds, int credCount); /// Call from loop() — polls topics, runs timers, transitions state. void update(); /// Transition out of BOOTED → SILENT. Call at end of setup(). void finishBoot(); /// Serial debug console. void onSerialCommand(const String& cmd); const ScreenState& getScreenState() const { return _state; } /// Externally trigger silence (e.g. hold-to-silence gesture). void silenceAlert(); private: void pollTopics(); void pollTopic(const String& url, const char* label); void onAlert(const String& title, const String& body); void onSilence(); void onAdmin(const String& command); void flushStatus(const String& message); void heartbeat(); void transition(DeviceState s); String topicUrl(const char* base); DisplayManager* _display; NetManager _net; ScreenState _state; const char* _version = ""; const char* _board = ""; bool _debug = false; uint32_t _lastPollMs = 0; uint32_t _lastHeartbeatMs = 0; uint32_t _bootGraceEnd = 0; String _alertUrl; String _silenceUrl; String _adminUrl; String _statusUrl; }; EOF info "DoorbellLogic.h" # ── DoorbellLogic.cpp ────────────────────────────────────── cat << 'EOF' > libraries/KlubhausCore/src/DoorbellLogic.cpp #include "DoorbellLogic.h" DoorbellLogic::DoorbellLogic(DisplayManager* display) : _display(display) {} // ── URL builder ───────────────────────────────────────────── String DoorbellLogic::topicUrl(const char* base) { String suffix = _debug ? "_test" : ""; return String("https://") + NTFY_SERVER + "/" + base + suffix + "/json?since=20s&poll=1"; } // ── Lifecycle ─────────────────────────────────────────────── void DoorbellLogic::begin(const char* version, const char* boardName, const WiFiCred* creds, int credCount) { _version = version; _board = boardName; #ifdef DEBUG_MODE _debug = true; #endif Serial.println(F("========================================")); Serial.printf( " KLUBHAUS ALERT v%s — %s\n", _version, _board); if (_debug) Serial.println(F(" *** DEBUG MODE — _test topics ***")); Serial.println(F("========================================\n")); // Display _display->begin(); // Network _net.begin(creds, credCount); if (_net.isConnected()) { _net.syncNTP(); Serial.printf("[NET] WiFi:%s RSSI:%d IP:%s\n", _net.getSSID().c_str(), _net.getRSSI(), _net.getIP().c_str()); _net.dnsCheck(NTFY_SERVER); _net.tlsCheck(NTFY_SERVER); } // Topic URLs _alertUrl = topicUrl(ALERT_TOPIC); _silenceUrl = topicUrl(SILENCE_TOPIC); _adminUrl = topicUrl(ADMIN_TOPIC); String sfx = _debug ? "_test" : ""; _statusUrl = String("https://") + NTFY_SERVER + "/" + STATUS_TOPIC + sfx; Serial.printf("[CONFIG] ALERT_URL: %s\n", _alertUrl.c_str()); Serial.printf("[CONFIG] SILENCE_URL: %s\n", _silenceUrl.c_str()); Serial.printf("[CONFIG] ADMIN_URL: %s\n", _adminUrl.c_str()); // Boot status flushStatus(String("BOOTED — ") + _net.getSSID() + " " + _net.getIP() + " RSSI:" + String(_net.getRSSI())); } void DoorbellLogic::finishBoot() { transition(DeviceState::SILENT); _state.screen = ScreenID::OFF; _display->setBacklight(false); _state.backlightOn = false; _bootGraceEnd = millis() + BOOT_GRACE_MS; Serial.printf("[BOOT] Grace period: %d ms\n", BOOT_GRACE_MS); Serial.printf("[BOOT] Ready — monitoring %s\n", NTFY_SERVER); } // ── Main loop tick ────────────────────────────────────────── void DoorbellLogic::update() { uint32_t now = millis(); _state.uptimeMs = now; if (_net.isConnected()) { _state.wifiRssi = _net.getRSSI(); _state.wifiSsid = _net.getSSID(); _state.ipAddr = _net.getIP(); } if (!_net.checkConnection()) return; // Poll if (now - _lastPollMs >= POLL_INTERVAL_MS) { _lastPollMs = now; pollTopics(); } // Heartbeat if (now - _lastHeartbeatMs >= HEARTBEAT_INTERVAL_MS) { _lastHeartbeatMs = now; heartbeat(); } // Auto-transitions switch (_state.deviceState) { case DeviceState::ALERTING: if (now - _state.alertStartMs > ALERT_TIMEOUT_MS) { Serial.println("[STATE] Alert timed out → SILENT"); transition(DeviceState::SILENT); _state.screen = ScreenID::OFF; _display->setBacklight(false); _state.backlightOn = false; } break; case DeviceState::SILENCED: if (now - _state.silenceStartMs > SILENCE_DISPLAY_MS) { Serial.println("[STATE] Silence display done → SILENT"); transition(DeviceState::SILENT); _state.screen = ScreenID::OFF; _display->setBacklight(false); _state.backlightOn = false; } break; default: break; } } // ── Polling ───────────────────────────────────────────────── void DoorbellLogic::pollTopics() { pollTopic(_alertUrl, "ALERT"); pollTopic(_silenceUrl, "SILENCE"); pollTopic(_adminUrl, "ADMIN"); _state.lastPollMs = millis(); } void DoorbellLogic::pollTopic(const String& url, const char* label) { String body; int code = _net.httpGet(url.c_str(), body); if (code != 200 || body.length() == 0) return; // ntfy returns newline-delimited JSON int pos = 0; while (pos < (int)body.length()) { int nl = body.indexOf('\n', pos); if (nl < 0) nl = body.length(); String line = body.substring(pos, nl); line.trim(); pos = nl + 1; if (line.length() == 0) continue; JsonDocument doc; if (deserializeJson(doc, line)) continue; const char* evt = doc["event"] | ""; if (strcmp(evt, "message") != 0) continue; const char* title = doc["title"] | ""; const char* message = doc["message"] | ""; Serial.printf("[%s] title=\"%s\" message=\"%s\"\n", label, title, message); if (strcmp(label, "ALERT") == 0) onAlert(String(title), String(message)); else if (strcmp(label, "SILENCE") == 0) onSilence(); else if (strcmp(label, "ADMIN") == 0) onAdmin(String(message)); } } // ── Event handlers ────────────────────────────────────────── void DoorbellLogic::onAlert(const String& title, const String& body) { if (millis() < _bootGraceEnd) { Serial.println("[ALERT] Ignored (boot grace)"); return; } Serial.printf("[ALERT] %s: %s\n", title.c_str(), body.c_str()); _state.alertTitle = title; _state.alertBody = body; _state.alertStartMs = millis(); transition(DeviceState::ALERTING); _state.screen = ScreenID::ALERT; _display->setBacklight(true); _state.backlightOn = true; flushStatus("ALERT: " + title); } void DoorbellLogic::onSilence() { if (_state.deviceState != DeviceState::ALERTING) return; Serial.println("[SILENCE] Alert silenced"); _state.silenceStartMs = millis(); transition(DeviceState::SILENCED); _state.screen = ScreenID::OFF; _display->setBacklight(false); _state.backlightOn = false; flushStatus("SILENCED"); } void DoorbellLogic::silenceAlert() { onSilence(); } void DoorbellLogic::onAdmin(const String& cmd) { Serial.printf("[ADMIN] %s\n", cmd.c_str()); if (cmd == "reboot") { flushStatus("REBOOTING (admin)"); delay(500); ESP.restart(); } else if (cmd == "dashboard") { _state.screen = ScreenID::DASHBOARD; _display->setBacklight(true); _state.backlightOn = true; } else if (cmd == "off") { _state.screen = ScreenID::OFF; _display->setBacklight(false); _state.backlightOn = false; } else if (cmd == "status") { heartbeat(); // re-uses heartbeat message format } } // ── Status / heartbeat ───────────────────────────────────── void DoorbellLogic::flushStatus(const String& message) { Serial.printf("[STATUS] Queued: %s\n", message.c_str()); String full = String(_board) + " " + message; int code = _net.httpPost(_statusUrl.c_str(), full); Serial.printf("[STATUS] Sent (%d): %s\n", code, message.c_str()); _state.lastHeartbeatMs = millis(); } void DoorbellLogic::heartbeat() { String m = String("HEARTBEAT ") + deviceStateStr(_state.deviceState) + " up:" + String(millis() / 1000) + "s" + " RSSI:" + String(_net.getRSSI()) + " heap:" + String(ESP.getFreeHeap()); #ifdef BOARD_HAS_PSRAM m += " psram:" + String(ESP.getFreePsram()); #endif flushStatus(m); } void DoorbellLogic::transition(DeviceState s) { Serial.printf("-> %s\n", deviceStateStr(s)); _state.deviceState = s; } // ── Serial console ────────────────────────────────────────── void DoorbellLogic::onSerialCommand(const String& cmd) { Serial.printf("[CMD] %s\n", cmd.c_str()); if (cmd == "alert") onAlert("Test Alert", "Serial test"); else if (cmd == "silence") onSilence(); else if (cmd == "reboot") ESP.restart(); else if (cmd == "dashboard") onAdmin("dashboard"); else if (cmd == "off") onAdmin("off"); else if (cmd == "status") { Serial.printf("[STATE] %s screen:%s bl:%s\n", deviceStateStr(_state.deviceState), screenIdStr(_state.screen), _state.backlightOn ? "ON" : "OFF"); Serial.printf("[MEM] heap:%d", ESP.getFreeHeap()); #ifdef BOARD_HAS_PSRAM Serial.printf(" psram:%d", ESP.getFreePsram()); #endif Serial.println(); Serial.printf("[NET] %s RSSI:%d IP:%s\n", _state.wifiSsid.c_str(), _state.wifiRssi, _state.ipAddr.c_str()); } else Serial.println(F("[CMD] alert|silence|reboot|dashboard|off|status")); } EOF info "DoorbellLogic.h + .cpp" # ───────────────────────────────────────────────────────────── section "Board: ESP32-S3-LCD-4.3" # ───────────────────────────────────────────────────────────── # ── board_config.h ───────────────────────────────────────── cat << 'EOF' > boards/esp32-s3-lcd-43/board_config.h #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 EOF info "board_config.h" # ── secrets.h.example ───────────────────────────────────── cat << 'EOF' > boards/esp32-s3-lcd-43/secrets.h.example #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]); EOF cp boards/esp32-s3-lcd-43/secrets.h.example boards/esp32-s3-lcd-43/secrets.h info "secrets.h.example (copied to secrets.h — edit before building)" # ── DisplayDriverGFX.h ──────────────────────────────────── cat << 'EOF' > boards/esp32-s3-lcd-43/DisplayDriverGFX.h #pragma once #include #include #include #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; }; EOF info "DisplayDriverGFX.h" # ── DisplayDriverGFX.cpp ────────────────────────────────── cat << 'EOF' > boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp #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); } EOF info "DisplayDriverGFX.h + .cpp" # ── Main sketch (S3) ────────────────────────────────────── cat << 'EOF' > boards/esp32-s3-lcd-43/esp32-s3-lcd-43.ino // // Klubhaus Doorbell — ESP32-S3-Touch-LCD-4.3 target // #include #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); } } EOF info "esp32-s3-lcd-43.ino" # ───────────────────────────────────────────────────────────── section "Board: ESP32-32E" # ───────────────────────────────────────────────────────────── # ── board_config.h ───────────────────────────────────────── cat << 'EOF' > boards/esp32-32e/board_config.h #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 EOF info "board_config.h" # ── tft_user_setup.h (copied into vendored TFT_eSPI) ────── cat << 'EOF' > boards/esp32-32e/tft_user_setup.h // ═══════════════════════════════════════════════════════════ // 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 EOF info "tft_user_setup.h" # ── secrets.h.example ───────────────────────────────────── cat << 'EOF' > boards/esp32-32e/secrets.h.example #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]); EOF cp boards/esp32-32e/secrets.h.example boards/esp32-32e/secrets.h info "secrets.h.example (copied to secrets.h — edit before building)" # ── DisplayDriverTFT.h ──────────────────────────────────── cat << 'EOF' > boards/esp32-32e/DisplayDriverTFT.h #pragma once #include #include #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; }; EOF info "DisplayDriverTFT.h" # ── DisplayDriverTFT.cpp ────────────────────────────────── cat << 'EOF' > boards/esp32-32e/DisplayDriverTFT.cpp #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); } EOF info "DisplayDriverTFT.h + .cpp" # ── Main sketch (32E) ───────────────────────────────────── cat << 'EOF' > boards/esp32-32e/esp32-32e.ino // // Klubhaus Doorbell — ESP32-32E target // #include #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); } } EOF info "esp32-32e.ino" # ───────────────────────────────────────────────────────────── section "Build Harness: mise.toml" # ───────────────────────────────────────────────────────────── cat << 'MISE_EOF' > mise.toml # ═══════════════════════════════════════════════════════════ # Klubhaus Doorbell — Multi-Target Build Harness # ═══════════════════════════════════════════════════════════ [tasks.install-libs-shared] description = "Install shared (platform-independent) libraries" run = """ arduino-cli lib install "ArduinoJson@7.4.1" arduino-cli lib install "NTPClient@3.2.1" echo "[OK] Shared libraries installed" """ [tasks.install-libs-32e] description = "Vendor TFT_eSPI into vendor/esp32-32e" run = """ #!/usr/bin/env bash set -euo pipefail if [ ! -d "vendor/esp32-32e/TFT_eSPI" ]; then echo "Cloning TFT_eSPI..." git clone --depth 1 --branch 2.5.43 \ https://github.com/Bodmer/TFT_eSPI.git \ vendor/esp32-32e/TFT_eSPI fi echo "Copying board-specific User_Setup.h..." 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-s3-43] description = "Vendor Arduino_GFX into vendor/esp32-s3-lcd-43" run = """ #!/usr/bin/env bash set -euo pipefail if [ ! -d "vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino" ]; then echo "Cloning Arduino_GFX..." git clone --depth 1 --branch v1.6.5 \ https://github.com/moononournation/Arduino_GFX.git \ vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino fi echo "[OK] Arduino_GFX 1.6.5 vendored" """ [tasks.install-libs] description = "Install all libraries (shared + vendored)" depends = ["install-libs-shared", "install-libs-32e", "install-libs-s3-43"] # ── ESP32-32E ──────────────────────────────────────────── [tasks.compile-32e] description = "Compile ESP32-32E sketch" depends = ["install-libs"] run = """ arduino-cli compile \ --fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \ --libraries ./libraries \ --libraries ./vendor/esp32-32e \ --build-property "compiler.cpp.extra_flags=-DDEBUG_MODE" \ --warnings default \ ./boards/esp32-32e """ [tasks.upload-32e] description = "Upload to ESP32-32E" run = """ arduino-cli upload \ --fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \ --port "${PORT:-/dev/ttyUSB0}" \ ./boards/esp32-32e """ [tasks.monitor-32e] description = "Serial monitor for ESP32-32E" run = """ arduino-cli monitor --port "${PORT:-/dev/ttyUSB0}" --config baudrate=115200 """ # ── ESP32-S3-LCD-4.3 ──────────────────────────────────── [tasks.compile-s3-43] description = "Compile ESP32-S3-LCD-4.3 sketch" depends = ["install-libs"] run = """ arduino-cli compile \ --fqbn "esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=opi,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB" \ --libraries ./libraries \ --libraries ./vendor/esp32-s3-lcd-43 \ --build-property "compiler.cpp.extra_flags=-DDEBUG_MODE -DBOARD_HAS_PSRAM" \ --warnings default \ ./boards/esp32-s3-lcd-43 """ [tasks.upload-s3-43] description = "Upload to ESP32-S3-LCD-4.3" run = """ arduino-cli upload \ --fqbn "esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=opi,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB" \ --port "${PORT:-/dev/ttyACM0}" \ ./boards/esp32-s3-lcd-43 """ [tasks.monitor-s3-43] description = "Serial monitor for ESP32-S3-LCD-4.3" run = """ arduino-cli monitor --port "${PORT:-/dev/ttyACM0}" --config baudrate=115200 """ # ── Convenience ────────────────────────────────────────── [tasks.clean] description = "Remove build artifacts" run = """ rm -rf boards/esp32-32e/build rm -rf boards/esp32-s3-lcd-43/build echo "[OK] Build artifacts cleaned" """ MISE_EOF info "mise.toml" # ───────────────────────────────────────────────────────────── section "Project Files" # ───────────────────────────────────────────────────────────── cat << 'EOF' > .gitignore # Secrets (WiFi passwords etc.) **/secrets.h # Build artifacts **/build/ # Vendored libraries (re-created by install-libs) vendor/esp32-32e/TFT_eSPI/ vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino/ # IDE .vscode/ *.swp *.swo *~ .DS_Store EOF info ".gitignore" cat << 'EOF' > README.md # Klubhaus Doorbell Multi-target doorbell alert system powered by [ntfy.sh](https://ntfy.sh). ## Targets | Board | Display | Library | Build Task | |---|---|---|---| | ESP32-32E | SPI TFT (ILI9341 etc.) | TFT_eSPI | `mise run compile-32e` | | ESP32-S3-Touch-LCD-4.3 | 800×480 RGB parallel | Arduino_GFX | `mise run compile-s3-43` | ## Quick Start ```bash # 1. Install prerequisites # - arduino-cli (with esp32:esp32 platform installed) # - mise (https://mise.jdx.dev) # 2. Edit WiFi credentials cp boards/esp32-32e/secrets.h.example boards/esp32-32e/secrets.h cp boards/esp32-s3-lcd-43/secrets.h.example boards/esp32-s3-lcd-43/secrets.h # Edit both secrets.h files with your WiFi SSIDs/passwords. # 3. Build & upload mise run compile-s3-43 mise run upload-s3-43 mise run compile-32e mise run upload-32e