snapshot
This commit is contained in:
90
sketches/doorbell-touch-esp32-32e/Config.h
Normal file
90
sketches/doorbell-touch-esp32-32e/Config.h
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Debug
|
||||||
|
// =====================================================================
|
||||||
|
#define DEBUG_MODE 1
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// WiFi Credentials
|
||||||
|
// =====================================================================
|
||||||
|
struct WiFiCred { const char *ssid; const char *pass; };
|
||||||
|
static WiFiCred wifiNetworks[] = {
|
||||||
|
{ "Dobro Veče", "goodnight" },
|
||||||
|
{ "berylpunk", "dhgwilliam" },
|
||||||
|
};
|
||||||
|
static const int NUM_WIFI = sizeof(wifiNetworks) / sizeof(wifiNetworks[0]);
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// ntfy.sh Topics
|
||||||
|
// =====================================================================
|
||||||
|
#define NTFY_BASE "https://ntfy.sh"
|
||||||
|
|
||||||
|
#if DEBUG_MODE
|
||||||
|
#define TOPIC_SUFFIX "_test"
|
||||||
|
#else
|
||||||
|
#define TOPIC_SUFFIX ""
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define ALERT_URL NTFY_BASE "/ALERT_klubhaus_topic" TOPIC_SUFFIX "/json?since=10s&poll=1"
|
||||||
|
#define SILENCE_URL NTFY_BASE "/SILENCE_klubhaus_topic" TOPIC_SUFFIX "/json?since=10s&poll=1"
|
||||||
|
#define ADMIN_URL NTFY_BASE "/ADMIN_klubhaus_topic" TOPIC_SUFFIX "/json?since=10s&poll=1"
|
||||||
|
#define STATUS_URL NTFY_BASE "/STATUS_klubhaus_topic" TOPIC_SUFFIX
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Timing
|
||||||
|
// =====================================================================
|
||||||
|
#define POLL_INTERVAL_MS 15000
|
||||||
|
#define BLINK_INTERVAL_MS 500
|
||||||
|
#define STALE_MSG_THRESHOLD_S 600
|
||||||
|
#define NTP_SYNC_INTERVAL_MS 3600000
|
||||||
|
#define ALERT_TIMEOUT_MS 300000
|
||||||
|
#define WAKE_DISPLAY_MS 5000
|
||||||
|
#define TOUCH_DEBOUNCE_MS 300
|
||||||
|
#define HEARTBEAT_INTERVAL_MS 30000
|
||||||
|
|
||||||
|
#if DEBUG_MODE
|
||||||
|
#define BOOT_GRACE_MS 5000
|
||||||
|
#else
|
||||||
|
#define BOOT_GRACE_MS 30000
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// E32R35T Hardware Pins (hardwired on PCB — do not change)
|
||||||
|
// =====================================================================
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Display
|
||||||
|
// =====================================================================
|
||||||
|
#define SCREEN_WIDTH 480 // landscape
|
||||||
|
#define SCREEN_HEIGHT 320
|
||||||
|
|
||||||
202
sketches/doorbell-touch-esp32-32e/DisplayManager.cpp
Normal file
202
sketches/doorbell-touch-esp32-32e/DisplayManager.cpp
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
#include "DisplayManager.h"
|
||||||
|
#include "Config.h"
|
||||||
|
|
||||||
|
void DisplayManager::begin() {
|
||||||
|
pinMode(PIN_LCD_BL, OUTPUT);
|
||||||
|
setBacklight(true);
|
||||||
|
|
||||||
|
_tft.init();
|
||||||
|
_tft.setRotation(1); // landscape: 480x320
|
||||||
|
_tft.fillScreen(COL_BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayManager::setBacklight(bool on) {
|
||||||
|
digitalWrite(PIN_LCD_BL, on ? HIGH : LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
TouchEvent DisplayManager::readTouch() {
|
||||||
|
TouchEvent evt;
|
||||||
|
uint16_t x, y;
|
||||||
|
if (_tft.getTouch(&x, &y)) {
|
||||||
|
evt.pressed = true;
|
||||||
|
evt.x = x;
|
||||||
|
evt.y = y;
|
||||||
|
}
|
||||||
|
return evt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayManager::render(const ScreenState& state) {
|
||||||
|
// Detect screen change → force full redraw
|
||||||
|
if (state.screen != _lastScreen) {
|
||||||
|
_needsFullRedraw = true;
|
||||||
|
_lastScreen = state.screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state.screen) {
|
||||||
|
case ScreenID::BOOT_SPLASH:
|
||||||
|
if (_needsFullRedraw) drawBootSplash(state);
|
||||||
|
break;
|
||||||
|
case ScreenID::WIFI_CONNECTING:
|
||||||
|
if (_needsFullRedraw) drawWifiConnecting();
|
||||||
|
break;
|
||||||
|
case ScreenID::WIFI_CONNECTED:
|
||||||
|
if (_needsFullRedraw) drawWifiConnected(state);
|
||||||
|
break;
|
||||||
|
case ScreenID::WIFI_FAILED:
|
||||||
|
if (_needsFullRedraw) drawWifiFailed();
|
||||||
|
break;
|
||||||
|
case ScreenID::ALERT:
|
||||||
|
// Alert redraws every blink cycle
|
||||||
|
if (_needsFullRedraw || state.blinkPhase != _lastBlink) {
|
||||||
|
drawAlertScreen(state);
|
||||||
|
_lastBlink = state.blinkPhase;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ScreenID::STATUS:
|
||||||
|
if (_needsFullRedraw) drawStatusScreen(state);
|
||||||
|
break;
|
||||||
|
case ScreenID::OFF:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_needsFullRedraw = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Helpers -----
|
||||||
|
|
||||||
|
void DisplayManager::drawCentered(const char* txt, int y, int sz, uint16_t col) {
|
||||||
|
_tft.setTextSize(sz);
|
||||||
|
_tft.setTextColor(col);
|
||||||
|
int w = strlen(txt) * 6 * sz;
|
||||||
|
_tft.setCursor(max(0, (SCREEN_WIDTH - w) / 2), y);
|
||||||
|
_tft.print(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayManager::drawInfoLine(int x, int y, uint16_t col, const char* text) {
|
||||||
|
_tft.setTextSize(1);
|
||||||
|
_tft.setTextColor(col);
|
||||||
|
_tft.setCursor(x, y);
|
||||||
|
_tft.print(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayManager::drawHeaderBar(uint16_t col, const char* label, const char* timeStr) {
|
||||||
|
_tft.setTextSize(2);
|
||||||
|
_tft.setTextColor(col);
|
||||||
|
_tft.setCursor(8, 8);
|
||||||
|
_tft.print(label);
|
||||||
|
|
||||||
|
int tw = strlen(timeStr) * 12;
|
||||||
|
_tft.setCursor(SCREEN_WIDTH - tw - 8, 8);
|
||||||
|
_tft.print(timeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Screens -----
|
||||||
|
|
||||||
|
void DisplayManager::drawBootSplash(const ScreenState& s) {
|
||||||
|
_tft.fillScreen(COL_BLACK);
|
||||||
|
drawCentered("KLUBHAUS", 60, 4, COL_NEON_TEAL);
|
||||||
|
drawCentered("ALERT", 110, 4, COL_HOT_FUCHSIA);
|
||||||
|
drawCentered("v5.0 E32R35T", 180, 2, COL_DARK_GRAY);
|
||||||
|
if (s.debugMode) {
|
||||||
|
drawCentered("DEBUG MODE", 210, 2, COL_YELLOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayManager::drawWifiConnecting() {
|
||||||
|
_tft.fillScreen(COL_BLACK);
|
||||||
|
drawCentered("Connecting", 130, 3, COL_NEON_TEAL);
|
||||||
|
drawCentered("to WiFi...", 170, 3, COL_NEON_TEAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayManager::drawWifiConnected(const ScreenState& s) {
|
||||||
|
_tft.fillScreen(COL_BLACK);
|
||||||
|
drawCentered("Connected!", 100, 3, COL_GREEN);
|
||||||
|
drawCentered(s.wifiSSID, 150, 2, COL_WHITE);
|
||||||
|
drawCentered(s.wifiIP, 180, 2, COL_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayManager::drawWifiFailed() {
|
||||||
|
_tft.fillScreen(COL_BLACK);
|
||||||
|
drawCentered("WiFi FAILED", 140, 3, COL_RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayManager::drawAlertScreen(const ScreenState& s) {
|
||||||
|
uint16_t bg = s.blinkPhase ? COL_NEON_TEAL : COL_HOT_FUCHSIA;
|
||||||
|
uint16_t fg = s.blinkPhase ? COL_BLACK : COL_WHITE;
|
||||||
|
|
||||||
|
_tft.fillScreen(bg);
|
||||||
|
drawHeaderBar(fg, "ALERT", s.timeString);
|
||||||
|
|
||||||
|
// Scale text to fit 480px wide screen
|
||||||
|
int sz = 5;
|
||||||
|
int len = strlen(s.alertMessage);
|
||||||
|
if (len > 10) sz = 4;
|
||||||
|
if (len > 18) sz = 3;
|
||||||
|
if (len > 30) sz = 2;
|
||||||
|
|
||||||
|
if (len > 12) {
|
||||||
|
// Two-line split
|
||||||
|
String msg(s.alertMessage);
|
||||||
|
int mid = len / 2;
|
||||||
|
int sp = msg.lastIndexOf(' ', mid);
|
||||||
|
if (sp < 0) sp = mid;
|
||||||
|
String l1 = msg.substring(0, sp);
|
||||||
|
String l2 = msg.substring(sp + 1);
|
||||||
|
int lh = 8 * sz + 8;
|
||||||
|
int y1 = (SCREEN_HEIGHT - lh * 2) / 2;
|
||||||
|
drawCentered(l1.c_str(), y1, sz, fg);
|
||||||
|
drawCentered(l2.c_str(), y1 + lh, sz, fg);
|
||||||
|
} else {
|
||||||
|
drawCentered(s.alertMessage, (SCREEN_HEIGHT - 8 * sz) / 2, sz, fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCentered("TAP TO SILENCE", SCREEN_HEIGHT - 25, 2, fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayManager::drawStatusScreen(const ScreenState& s) {
|
||||||
|
_tft.fillScreen(COL_BLACK);
|
||||||
|
drawHeaderBar(COL_MINT, "KLUBHAUS", s.timeString);
|
||||||
|
|
||||||
|
drawCentered("MONITORING", 60, 3, COL_WHITE);
|
||||||
|
|
||||||
|
char buf[80];
|
||||||
|
int y = 110;
|
||||||
|
int sp = 22;
|
||||||
|
int x = 20;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "WiFi: %s (%ddBm)",
|
||||||
|
s.wifiConnected ? s.wifiSSID : "DOWN", s.wifiRSSI);
|
||||||
|
drawInfoLine(x, y, COL_WHITE, buf); y += sp;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "IP: %s",
|
||||||
|
s.wifiConnected ? s.wifiIP : "---");
|
||||||
|
drawInfoLine(x, y, COL_WHITE, buf); y += sp;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "Up: %lu min Heap: %lu KB",
|
||||||
|
s.uptimeMinutes, s.freeHeapKB);
|
||||||
|
drawInfoLine(x, y, COL_WHITE, buf); y += sp;
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "NTP: %s UTC",
|
||||||
|
s.ntpSynced ? s.timeString : "not synced");
|
||||||
|
drawInfoLine(x, y, COL_WHITE, buf); y += sp;
|
||||||
|
|
||||||
|
const char* stName = s.deviceState == DeviceState::SILENT ? "SILENT" :
|
||||||
|
s.deviceState == DeviceState::ALERTING ? "ALERTING" : "WAKE";
|
||||||
|
snprintf(buf, sizeof(buf), "State: %s Net: %s",
|
||||||
|
stName, s.networkOK ? "OK" : "FAIL");
|
||||||
|
uint16_t stCol = s.deviceState == DeviceState::ALERTING ? COL_RED :
|
||||||
|
s.deviceState == DeviceState::SILENT ? COL_GREEN : COL_NEON_TEAL;
|
||||||
|
drawInfoLine(x, y, stCol, buf); y += sp;
|
||||||
|
|
||||||
|
if (strlen(s.alertMessage) > 0) {
|
||||||
|
snprintf(buf, sizeof(buf), "Last: %.40s", s.alertMessage);
|
||||||
|
drawInfoLine(x, y, COL_DARK_GRAY, buf); y += sp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.debugMode) {
|
||||||
|
drawInfoLine(x, y, COL_YELLOW, "DEBUG MODE - _test topics");
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCentered("tap to dismiss", SCREEN_HEIGHT - 18, 1, COL_DARK_GRAY);
|
||||||
|
}
|
||||||
|
|
||||||
42
sketches/doorbell-touch-esp32-32e/DisplayManager.h
Normal file
42
sketches/doorbell-touch-esp32-32e/DisplayManager.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <TFT_eSPI.h>
|
||||||
|
#include "ScreenData.h"
|
||||||
|
|
||||||
|
class DisplayManager {
|
||||||
|
public:
|
||||||
|
void begin();
|
||||||
|
void render(const ScreenState& state);
|
||||||
|
void setBacklight(bool on);
|
||||||
|
TouchEvent readTouch();
|
||||||
|
|
||||||
|
private:
|
||||||
|
TFT_eSPI _tft;
|
||||||
|
ScreenID _lastScreen = ScreenID::BOOT_SPLASH;
|
||||||
|
bool _needsFullRedraw = true;
|
||||||
|
bool _lastBlink = false;
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
static constexpr uint16_t COL_NEON_TEAL = 0x07D7;
|
||||||
|
static constexpr uint16_t COL_HOT_FUCHSIA = 0xF81F;
|
||||||
|
static constexpr uint16_t COL_WHITE = 0xFFDF;
|
||||||
|
static constexpr uint16_t COL_BLACK = 0x0000;
|
||||||
|
static constexpr uint16_t COL_MINT = 0x67F5;
|
||||||
|
static constexpr uint16_t COL_DARK_GRAY = 0x2104;
|
||||||
|
static constexpr uint16_t COL_GREEN = 0x07E0;
|
||||||
|
static constexpr uint16_t COL_RED = 0xF800;
|
||||||
|
static constexpr uint16_t COL_YELLOW = 0xFFE0;
|
||||||
|
|
||||||
|
// Screen renderers
|
||||||
|
void drawBootSplash(const ScreenState& s);
|
||||||
|
void drawWifiConnecting();
|
||||||
|
void drawWifiConnected(const ScreenState& s);
|
||||||
|
void drawWifiFailed();
|
||||||
|
void drawAlertScreen(const ScreenState& s);
|
||||||
|
void drawStatusScreen(const ScreenState& s);
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
void drawCentered(const char* txt, int y, int sz, uint16_t col);
|
||||||
|
void drawInfoLine(int x, int y, uint16_t col, const char* text);
|
||||||
|
void drawHeaderBar(uint16_t col, const char* label, const char* timeStr);
|
||||||
|
};
|
||||||
|
|
||||||
453
sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp
Normal file
453
sketches/doorbell-touch-esp32-32e/DoorbellLogic.cpp
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
#include "DoorbellLogic.h"
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Lifecycle
|
||||||
|
// =====================================================================
|
||||||
|
void DoorbellLogic::begin() {
|
||||||
|
_bootTime = millis();
|
||||||
|
_timeClient = new NTPClient(_ntpUDP, "pool.ntp.org", 0, NTP_SYNC_INTERVAL_MS);
|
||||||
|
|
||||||
|
_screen.debugMode = DEBUG_MODE;
|
||||||
|
_screen.screen = ScreenID::BOOT_SPLASH;
|
||||||
|
updateScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::beginWiFi() {
|
||||||
|
for (int i = 0; i < NUM_WIFI; i++) {
|
||||||
|
_wifiMulti.addAP(wifiNetworks[i].ssid, wifiNetworks[i].pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
_screen.screen = ScreenID::WIFI_CONNECTING;
|
||||||
|
updateScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::connectWiFiBlocking() {
|
||||||
|
Serial.println("[WIFI] Connecting...");
|
||||||
|
|
||||||
|
int tries = 0;
|
||||||
|
while (_wifiMulti.run() != WL_CONNECTED && tries++ < 40) {
|
||||||
|
Serial.print(".");
|
||||||
|
delay(500);
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
if (WiFi.isConnected()) {
|
||||||
|
Serial.printf("[WIFI] Connected: %s %s\n",
|
||||||
|
WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
|
||||||
|
updateScreenState();
|
||||||
|
_screen.screen = ScreenID::WIFI_CONNECTED;
|
||||||
|
} else {
|
||||||
|
Serial.println("[WIFI] FAILED — no networks reachable");
|
||||||
|
_screen.screen = ScreenID::WIFI_FAILED;
|
||||||
|
}
|
||||||
|
updateScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::finishBoot() {
|
||||||
|
if (WiFi.isConnected()) {
|
||||||
|
_timeClient->begin();
|
||||||
|
Serial.println("[NTP] Starting sync...");
|
||||||
|
|
||||||
|
for (int i = 0; i < 5 && !_ntpSynced; i++) {
|
||||||
|
syncNTP();
|
||||||
|
if (!_ntpSynced) delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ntpSynced) {
|
||||||
|
Serial.printf("[NTP] Synced: %s UTC\n",
|
||||||
|
_timeClient->getFormattedTime().c_str());
|
||||||
|
} else {
|
||||||
|
Serial.println("[NTP] Initial sync failed — will retry in update()");
|
||||||
|
}
|
||||||
|
|
||||||
|
checkNetwork();
|
||||||
|
|
||||||
|
char bootMsg[80];
|
||||||
|
snprintf(bootMsg, sizeof(bootMsg), "%s %s RSSI:%d",
|
||||||
|
WiFi.SSID().c_str(),
|
||||||
|
WiFi.localIP().toString().c_str(),
|
||||||
|
WiFi.RSSI());
|
||||||
|
queueStatus("BOOTED", bootMsg);
|
||||||
|
flushStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionTo(DeviceState::SILENT);
|
||||||
|
Serial.printf("[BOOT] Grace period: %d ms\n", BOOT_GRACE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Main Update Loop
|
||||||
|
// =====================================================================
|
||||||
|
void DoorbellLogic::update() {
|
||||||
|
unsigned long now = millis();
|
||||||
|
|
||||||
|
if (_inBootGrace && (now - _bootTime >= BOOT_GRACE_MS)) {
|
||||||
|
_inBootGrace = false;
|
||||||
|
Serial.println("[BOOT] Grace period ended");
|
||||||
|
}
|
||||||
|
|
||||||
|
syncNTP();
|
||||||
|
|
||||||
|
if (!WiFi.isConnected()) {
|
||||||
|
if (_wifiMulti.run() == WL_CONNECTED) {
|
||||||
|
Serial.println("[WIFI] Reconnected");
|
||||||
|
queueStatus("RECONNECTED", WiFi.SSID().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - _lastPoll >= POLL_INTERVAL_MS) {
|
||||||
|
_lastPoll = now;
|
||||||
|
if (WiFi.isConnected() && _ntpSynced) {
|
||||||
|
pollTopics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flushStatus();
|
||||||
|
|
||||||
|
switch (_state) {
|
||||||
|
case DeviceState::ALERTING:
|
||||||
|
if (now - _alertStart > ALERT_TIMEOUT_MS) {
|
||||||
|
Serial.println("[ALERT] Timeout — auto-silencing");
|
||||||
|
handleSilence("timeout");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (now - _lastBlink >= BLINK_INTERVAL_MS) {
|
||||||
|
_lastBlink = now;
|
||||||
|
_blinkState = !_blinkState;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DeviceState::WAKE:
|
||||||
|
if (now - _wakeStart > WAKE_DISPLAY_MS) {
|
||||||
|
transitionTo(DeviceState::SILENT);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DeviceState::SILENT:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - _lastHeartbeat >= HEARTBEAT_INTERVAL_MS) {
|
||||||
|
_lastHeartbeat = now;
|
||||||
|
Serial.printf("[%lus] %s | WiFi:%s | heap:%dKB\n",
|
||||||
|
now / 1000,
|
||||||
|
_state == DeviceState::SILENT ? "SILENT" :
|
||||||
|
_state == DeviceState::ALERTING ? "ALERT" : "WAKE",
|
||||||
|
WiFi.isConnected() ? "OK" : "DOWN",
|
||||||
|
ESP.getFreeHeap() / 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Input Events
|
||||||
|
// =====================================================================
|
||||||
|
void DoorbellLogic::onTouch(const TouchEvent& evt) {
|
||||||
|
if (!evt.pressed) return;
|
||||||
|
|
||||||
|
static unsigned long lastAction = 0;
|
||||||
|
unsigned long now = millis();
|
||||||
|
if (now - lastAction < TOUCH_DEBOUNCE_MS) return;
|
||||||
|
lastAction = now;
|
||||||
|
|
||||||
|
Serial.printf("[TOUCH] x=%d y=%d state=%d\n", evt.x, evt.y, (int)_state);
|
||||||
|
|
||||||
|
switch (_state) {
|
||||||
|
case DeviceState::ALERTING:
|
||||||
|
handleSilence("touch");
|
||||||
|
break;
|
||||||
|
case DeviceState::SILENT:
|
||||||
|
transitionTo(DeviceState::WAKE);
|
||||||
|
break;
|
||||||
|
case DeviceState::WAKE:
|
||||||
|
transitionTo(DeviceState::SILENT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::onSerialCommand(const String& cmd) {
|
||||||
|
if (cmd == "CLEAR_DEDUP") {
|
||||||
|
_lastAlertId = _lastSilenceId = _lastAdminId = "";
|
||||||
|
Serial.println("[CMD] Dedup cleared");
|
||||||
|
} else if (cmd == "NET") {
|
||||||
|
checkNetwork();
|
||||||
|
} else if (cmd == "STATUS") {
|
||||||
|
Serial.printf("[CMD] State:%s WiFi:%s RSSI:%d Heap:%dKB NTP:%s\n",
|
||||||
|
_state == DeviceState::SILENT ? "SILENT" :
|
||||||
|
_state == DeviceState::ALERTING ? "ALERT" : "WAKE",
|
||||||
|
WiFi.isConnected() ? WiFi.SSID().c_str() : "DOWN",
|
||||||
|
WiFi.RSSI(),
|
||||||
|
ESP.getFreeHeap() / 1024,
|
||||||
|
_ntpSynced ? _timeClient->getFormattedTime().c_str() : "no");
|
||||||
|
} else if (cmd == "WAKE") {
|
||||||
|
transitionTo(DeviceState::WAKE);
|
||||||
|
} else if (cmd == "TEST") {
|
||||||
|
handleAlert("TEST ALERT");
|
||||||
|
} else if (cmd == "REBOOT") {
|
||||||
|
Serial.println("[CMD] Rebooting...");
|
||||||
|
queueStatus("REBOOTING", "serial");
|
||||||
|
flushStatus();
|
||||||
|
delay(200);
|
||||||
|
ESP.restart();
|
||||||
|
} else {
|
||||||
|
Serial.printf("[CMD] Unknown: %s\n", cmd.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// State Transitions
|
||||||
|
// =====================================================================
|
||||||
|
void DoorbellLogic::transitionTo(DeviceState newState) {
|
||||||
|
_state = newState;
|
||||||
|
unsigned long now = millis();
|
||||||
|
|
||||||
|
switch (newState) {
|
||||||
|
case DeviceState::SILENT:
|
||||||
|
_screen.screen = ScreenID::OFF;
|
||||||
|
Serial.println("-> SILENT");
|
||||||
|
break;
|
||||||
|
case DeviceState::ALERTING:
|
||||||
|
_alertStart = now;
|
||||||
|
_lastBlink = now;
|
||||||
|
_blinkState = false;
|
||||||
|
_screen.screen = ScreenID::ALERT;
|
||||||
|
Serial.printf("-> ALERTING: %s\n", _currentMessage.c_str());
|
||||||
|
break;
|
||||||
|
case DeviceState::WAKE:
|
||||||
|
_wakeStart = now;
|
||||||
|
_screen.screen = ScreenID::STATUS;
|
||||||
|
Serial.println("-> WAKE");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Message Handlers
|
||||||
|
// =====================================================================
|
||||||
|
void DoorbellLogic::handleAlert(const String& msg) {
|
||||||
|
if (_state == DeviceState::ALERTING && _currentMessage == msg) return;
|
||||||
|
_currentMessage = msg;
|
||||||
|
transitionTo(DeviceState::ALERTING);
|
||||||
|
queueStatus("ALERTING", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::handleSilence(const String& msg) {
|
||||||
|
_currentMessage = "";
|
||||||
|
transitionTo(DeviceState::SILENT);
|
||||||
|
queueStatus("SILENT", "silenced");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::handleAdmin(const String& msg) {
|
||||||
|
Serial.printf("[ADMIN] %s\n", msg.c_str());
|
||||||
|
|
||||||
|
if (msg == "SILENCE") handleSilence("admin");
|
||||||
|
else if (msg == "PING") queueStatus("PONG", "ping");
|
||||||
|
else if (msg == "test") handleAlert("TEST ALERT");
|
||||||
|
else if (msg == "status") {
|
||||||
|
char buf[128];
|
||||||
|
snprintf(buf, sizeof(buf), "State:%s WiFi:%s RSSI:%d Heap:%dKB",
|
||||||
|
_state == DeviceState::SILENT ? "SILENT" :
|
||||||
|
_state == DeviceState::ALERTING ? "ALERT" : "WAKE",
|
||||||
|
WiFi.SSID().c_str(), WiFi.RSSI(), ESP.getFreeHeap() / 1024);
|
||||||
|
queueStatus("STATUS", buf);
|
||||||
|
}
|
||||||
|
else if (msg == "wake") {
|
||||||
|
transitionTo(DeviceState::WAKE);
|
||||||
|
}
|
||||||
|
else if (msg == "REBOOT") {
|
||||||
|
queueStatus("REBOOTING", "admin");
|
||||||
|
flushStatus();
|
||||||
|
delay(200);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Screen State Sync
|
||||||
|
// =====================================================================
|
||||||
|
void DoorbellLogic::updateScreenState() {
|
||||||
|
_screen.deviceState = _state;
|
||||||
|
_screen.blinkPhase = _blinkState;
|
||||||
|
strncpy(_screen.alertMessage, _currentMessage.c_str(), sizeof(_screen.alertMessage) - 1);
|
||||||
|
_screen.alertMessage[sizeof(_screen.alertMessage) - 1] = '\0';
|
||||||
|
|
||||||
|
_screen.wifiConnected = WiFi.isConnected();
|
||||||
|
if (_screen.wifiConnected) {
|
||||||
|
strncpy(_screen.wifiSSID, WiFi.SSID().c_str(), sizeof(_screen.wifiSSID) - 1);
|
||||||
|
_screen.wifiSSID[sizeof(_screen.wifiSSID) - 1] = '\0';
|
||||||
|
strncpy(_screen.wifiIP, WiFi.localIP().toString().c_str(), sizeof(_screen.wifiIP) - 1);
|
||||||
|
_screen.wifiIP[sizeof(_screen.wifiIP) - 1] = '\0';
|
||||||
|
_screen.wifiRSSI = WiFi.RSSI();
|
||||||
|
}
|
||||||
|
|
||||||
|
_screen.ntpSynced = _ntpSynced;
|
||||||
|
if (_ntpSynced) {
|
||||||
|
strncpy(_screen.timeString, _timeClient->getFormattedTime().c_str(),
|
||||||
|
sizeof(_screen.timeString) - 1);
|
||||||
|
_screen.timeString[sizeof(_screen.timeString) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
_screen.uptimeMinutes = (millis() - _bootTime) / 60000;
|
||||||
|
_screen.freeHeapKB = ESP.getFreeHeap() / 1024;
|
||||||
|
_screen.networkOK = _networkOK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// WiFi & Network
|
||||||
|
// =====================================================================
|
||||||
|
void DoorbellLogic::syncNTP() {
|
||||||
|
if (_timeClient->update()) {
|
||||||
|
_ntpSynced = true;
|
||||||
|
_lastEpoch = _timeClient->getEpochTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::checkNetwork() {
|
||||||
|
Serial.printf("[NET] WiFi:%s RSSI:%d IP:%s\n",
|
||||||
|
WiFi.SSID().c_str(), WiFi.RSSI(), WiFi.localIP().toString().c_str());
|
||||||
|
|
||||||
|
IPAddress ip;
|
||||||
|
if (!WiFi.hostByName("ntfy.sh", ip)) {
|
||||||
|
Serial.println("[NET] DNS FAILED");
|
||||||
|
_networkOK = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Serial.printf("[NET] DNS OK: %s\n", ip.toString().c_str());
|
||||||
|
|
||||||
|
WiFiClientSecure tls;
|
||||||
|
tls.setInsecure();
|
||||||
|
if (tls.connect("ntfy.sh", 443, 15000)) {
|
||||||
|
Serial.println("[NET] TLS OK");
|
||||||
|
tls.stop();
|
||||||
|
_networkOK = true;
|
||||||
|
} else {
|
||||||
|
Serial.println("[NET] TLS FAILED");
|
||||||
|
_networkOK = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// ntfy Polling
|
||||||
|
// =====================================================================
|
||||||
|
void DoorbellLogic::pollTopics() {
|
||||||
|
pollTopic(ALERT_URL, &DoorbellLogic::handleAlert, "ALERT", _lastAlertId);
|
||||||
|
pollTopic(SILENCE_URL, &DoorbellLogic::handleSilence, "SILENCE", _lastSilenceId);
|
||||||
|
pollTopic(ADMIN_URL, &DoorbellLogic::handleAdmin, "ADMIN", _lastAdminId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::pollTopic(const char* url,
|
||||||
|
void (DoorbellLogic::*handler)(const String&),
|
||||||
|
const char* name, String& lastId) {
|
||||||
|
WiFiClientSecure client;
|
||||||
|
client.setInsecure();
|
||||||
|
|
||||||
|
HTTPClient http;
|
||||||
|
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||||
|
http.setTimeout(10000);
|
||||||
|
|
||||||
|
if (!http.begin(client, url)) {
|
||||||
|
Serial.printf("[%s] begin() failed\n", name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int code = http.GET();
|
||||||
|
if (code == HTTP_CODE_OK) {
|
||||||
|
String response = http.getString();
|
||||||
|
if (response.length() > 0) {
|
||||||
|
parseMessages(response, name, handler, lastId);
|
||||||
|
}
|
||||||
|
} else if (code < 0) {
|
||||||
|
Serial.printf("[%s] HTTP error: %s\n", name, http.errorToString(code).c_str());
|
||||||
|
}
|
||||||
|
http.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::parseMessages(String& response, const char* name,
|
||||||
|
void (DoorbellLogic::*handler)(const String&),
|
||||||
|
String& lastId) {
|
||||||
|
if (_inBootGrace || !_ntpSynced || _lastEpoch == 0) return;
|
||||||
|
|
||||||
|
int lineStart = 0;
|
||||||
|
while (lineStart < (int)response.length()) {
|
||||||
|
int lineEnd = response.indexOf('\n', lineStart);
|
||||||
|
if (lineEnd == -1) lineEnd = response.length();
|
||||||
|
|
||||||
|
String line = response.substring(lineStart, lineEnd);
|
||||||
|
line.trim();
|
||||||
|
|
||||||
|
if (line.length() > 0 && line.indexOf('{') >= 0) {
|
||||||
|
JsonDocument doc;
|
||||||
|
if (!deserializeJson(doc, line)) {
|
||||||
|
const char* event = doc["event"];
|
||||||
|
const char* msgId = doc["id"];
|
||||||
|
const char* message = doc["message"];
|
||||||
|
time_t msgTime = doc["time"] | 0;
|
||||||
|
|
||||||
|
if (event && strcmp(event, "message") != 0) {
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message || strlen(message) == 0) {
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String idStr = msgId ? String(msgId) : "";
|
||||||
|
if (idStr.length() > 0 && idStr == lastId) {
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgTime > 0 && (_lastEpoch - msgTime) > (time_t)STALE_MSG_THRESHOLD_S) {
|
||||||
|
Serial.printf("[%s] Stale: %.30s (age %llds)\n",
|
||||||
|
name, message, (long long)(_lastEpoch - msgTime));
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%s] %.50s\n", name, message);
|
||||||
|
if (idStr.length() > 0) lastId = idStr;
|
||||||
|
(this->*handler)(String(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Status Publishing (deferred)
|
||||||
|
// =====================================================================
|
||||||
|
void DoorbellLogic::queueStatus(const char* st, const String& msg) {
|
||||||
|
_pendingStatus = true;
|
||||||
|
_pendStatusState = st;
|
||||||
|
_pendStatusMsg = msg;
|
||||||
|
Serial.printf("[STATUS] Queued: %s — %s\n", st, msg.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::flushStatus() {
|
||||||
|
if (!_pendingStatus || !WiFi.isConnected()) return;
|
||||||
|
_pendingStatus = false;
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
doc["state"] = _pendStatusState;
|
||||||
|
doc["message"] = _pendStatusMsg;
|
||||||
|
doc["timestamp"] = _ntpSynced ? (long long)_timeClient->getEpochTime() * 1000LL : 0LL;
|
||||||
|
|
||||||
|
String payload;
|
||||||
|
serializeJson(doc, payload);
|
||||||
|
|
||||||
|
WiFiClientSecure client;
|
||||||
|
client.setInsecure();
|
||||||
|
|
||||||
|
HTTPClient http;
|
||||||
|
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||||
|
http.begin(client, STATUS_URL);
|
||||||
|
http.addHeader("Content-Type", "application/json");
|
||||||
|
int code = http.POST(payload);
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
Serial.printf("[STATUS] Sent (%d): %s\n", code, _pendStatusState.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
87
sketches/doorbell-touch-esp32-32e/DoorbellLogic.h
Normal file
87
sketches/doorbell-touch-esp32-32e/DoorbellLogic.h
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WiFiMulti.h>
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <NTPClient.h>
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include "Config.h"
|
||||||
|
#include "ScreenData.h"
|
||||||
|
|
||||||
|
class DoorbellLogic {
|
||||||
|
public:
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
// Boot sequence (called individually so .ino can render between steps)
|
||||||
|
void beginWiFi();
|
||||||
|
void connectWiFiBlocking();
|
||||||
|
void finishBoot();
|
||||||
|
|
||||||
|
void update();
|
||||||
|
|
||||||
|
const ScreenState& getScreenState() const { return _screen; }
|
||||||
|
|
||||||
|
// Input events from the outside
|
||||||
|
void onTouch(const TouchEvent& evt);
|
||||||
|
void onSerialCommand(const String& cmd);
|
||||||
|
|
||||||
|
private:
|
||||||
|
ScreenState _screen;
|
||||||
|
|
||||||
|
// State
|
||||||
|
DeviceState _state = DeviceState::SILENT;
|
||||||
|
String _currentMessage = "";
|
||||||
|
unsigned long _bootTime = 0;
|
||||||
|
bool _inBootGrace = true;
|
||||||
|
bool _networkOK = false;
|
||||||
|
|
||||||
|
// Dedup
|
||||||
|
String _lastAlertId;
|
||||||
|
String _lastSilenceId;
|
||||||
|
String _lastAdminId;
|
||||||
|
|
||||||
|
// Timing
|
||||||
|
unsigned long _lastPoll = 0;
|
||||||
|
unsigned long _lastBlink = 0;
|
||||||
|
unsigned long _alertStart = 0;
|
||||||
|
unsigned long _wakeStart = 0;
|
||||||
|
unsigned long _lastHeartbeat = 0;
|
||||||
|
bool _blinkState = false;
|
||||||
|
|
||||||
|
// Deferred status publish
|
||||||
|
bool _pendingStatus = false;
|
||||||
|
String _pendStatusState;
|
||||||
|
String _pendStatusMsg;
|
||||||
|
|
||||||
|
// NTP
|
||||||
|
WiFiUDP _ntpUDP;
|
||||||
|
NTPClient _timeClient = nullptr;
|
||||||
|
bool _ntpSynced = false;
|
||||||
|
time_t _lastEpoch = 0;
|
||||||
|
|
||||||
|
// WiFi
|
||||||
|
WiFiMulti _wifiMulti;
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
void checkNetwork();
|
||||||
|
void syncNTP();
|
||||||
|
|
||||||
|
void pollTopics();
|
||||||
|
void pollTopic(const char* url, void (DoorbellLogic::*handler)(const String&),
|
||||||
|
const char* name, String& lastId);
|
||||||
|
void parseMessages(String& response, const char* name,
|
||||||
|
void (DoorbellLogic::*handler)(const String&),
|
||||||
|
String& lastId);
|
||||||
|
|
||||||
|
void handleAlert(const String& msg);
|
||||||
|
void handleSilence(const String& msg);
|
||||||
|
void handleAdmin(const String& msg);
|
||||||
|
|
||||||
|
void transitionTo(DeviceState newState);
|
||||||
|
void queueStatus(const char* state, const String& msg);
|
||||||
|
void flushStatus();
|
||||||
|
|
||||||
|
void updateScreenState();
|
||||||
|
};
|
||||||
|
|
||||||
58
sketches/doorbell-touch-esp32-32e/ScreenData.h
Normal file
58
sketches/doorbell-touch-esp32-32e/ScreenData.h
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Shared enums and structs — NO library dependencies
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
enum class DeviceState : uint8_t {
|
||||||
|
SILENT,
|
||||||
|
ALERTING,
|
||||||
|
WAKE
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ScreenID : uint8_t {
|
||||||
|
BOOT_SPLASH,
|
||||||
|
WIFI_CONNECTING,
|
||||||
|
WIFI_CONNECTED,
|
||||||
|
WIFI_FAILED,
|
||||||
|
ALERT,
|
||||||
|
STATUS,
|
||||||
|
OFF // backlight off, nothing to draw
|
||||||
|
};
|
||||||
|
|
||||||
|
// Everything the display needs to render any screen
|
||||||
|
struct ScreenState {
|
||||||
|
ScreenID screen = ScreenID::BOOT_SPLASH;
|
||||||
|
DeviceState deviceState = DeviceState::SILENT;
|
||||||
|
bool blinkPhase = false;
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
char alertMessage[64] = "";
|
||||||
|
|
||||||
|
// WiFi
|
||||||
|
bool wifiConnected = false;
|
||||||
|
char wifiSSID[33] = "";
|
||||||
|
int wifiRSSI = 0;
|
||||||
|
char wifiIP[16] = "";
|
||||||
|
|
||||||
|
// NTP
|
||||||
|
bool ntpSynced = false;
|
||||||
|
char timeString[12] = "";
|
||||||
|
|
||||||
|
// System
|
||||||
|
uint32_t uptimeMinutes = 0;
|
||||||
|
uint32_t freeHeapKB = 0;
|
||||||
|
bool networkOK = false;
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
bool debugMode = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Touch event passed from display to logic
|
||||||
|
struct TouchEvent {
|
||||||
|
bool pressed = false;
|
||||||
|
uint16_t x = 0;
|
||||||
|
uint16_t y = 0;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -1,58 +1,78 @@
|
|||||||
#include <TFT_eSPI.h>
|
/*
|
||||||
#include <SPI.h>
|
* KLUBHAUS ALERT v5.0 — E32R35T Edition
|
||||||
|
*
|
||||||
|
* Target: LCDWiki E32R35T (ESP32-WROOM-32E + 3.5" ST7796S + XPT2046)
|
||||||
|
*
|
||||||
|
* Refactored: business logic separated from display code.
|
||||||
|
* Business logic knows nothing about TFT_eSPI.
|
||||||
|
* Display knows nothing about ntfy/WiFi/state machine.
|
||||||
|
* They communicate through ScreenState (plain struct).
|
||||||
|
*/
|
||||||
|
|
||||||
TFT_eSPI tft = TFT_eSPI();
|
#include "Config.h"
|
||||||
|
#include "DisplayManager.h"
|
||||||
|
#include "DoorbellLogic.h"
|
||||||
|
|
||||||
|
DisplayManager display;
|
||||||
|
DoorbellLogic logic;
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
delay(2000);
|
unsigned long t = millis();
|
||||||
|
while (!Serial && millis() - t < 3000) delay(10);
|
||||||
|
delay(500);
|
||||||
|
|
||||||
// Backlight ON
|
Serial.println("\n========================================");
|
||||||
pinMode(27, OUTPUT);
|
Serial.println(" KLUBHAUS ALERT v5.0 — E32R35T");
|
||||||
digitalWrite(27, HIGH);
|
#if DEBUG_MODE
|
||||||
|
Serial.println(" *** DEBUG MODE — _test topics ***");
|
||||||
// Config verification
|
|
||||||
Serial.println("\n=== E32R35T Config ===");
|
|
||||||
#if defined(USER_SETUP_LOADED)
|
|
||||||
Serial.println("User_Setup: LOADED");
|
|
||||||
#else
|
|
||||||
Serial.println("User_Setup: DEFAULT (BAD!)");
|
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_HSPI_PORT)
|
Serial.println("========================================");
|
||||||
Serial.println("SPI Bus: HSPI");
|
|
||||||
#else
|
|
||||||
Serial.println("SPI Bus: VSPI");
|
|
||||||
#endif
|
|
||||||
Serial.printf("MOSI:%d SCLK:%d MISO:%d\n", TFT_MOSI, TFT_SCLK, TFT_MISO);
|
|
||||||
Serial.printf("CS:%d DC:%d RST:%d BL:%d\n", TFT_CS, TFT_DC, TFT_RST, TFT_BL);
|
|
||||||
Serial.printf("TOUCH_CS:%d\n", TOUCH_CS);
|
|
||||||
Serial.printf("SPI_FREQ:%d\n", SPI_FREQUENCY);
|
|
||||||
Serial.println("======================\n");
|
|
||||||
|
|
||||||
tft.init();
|
// 1. Init display hardware
|
||||||
tft.setRotation(1);
|
display.begin();
|
||||||
|
|
||||||
uint16_t colors[] = {TFT_RED, TFT_GREEN, TFT_BLUE, TFT_WHITE, TFT_BLACK};
|
// 2. Init logic (sets boot splash screen state)
|
||||||
const char* names[] = {"RED", "GREEN", "BLUE", "WHITE", "BLACK"};
|
logic.begin();
|
||||||
|
display.render(logic.getScreenState());
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
tft.fillScreen(colors[i]);
|
|
||||||
Serial.printf("Screen: %s\n", names[i]);
|
|
||||||
delay(1500);
|
delay(1500);
|
||||||
}
|
|
||||||
|
|
||||||
tft.fillScreen(TFT_BLACK);
|
// 3. WiFi (logic updates screen state, we render each phase)
|
||||||
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
// We need a small coupling here for the blocking WiFi connect
|
||||||
tft.setTextSize(3);
|
// This could be made async later
|
||||||
tft.drawString("E32R35T Works!", 30, 100);
|
logic.beginWiFi(); // sets screen to WIFI_CONNECTING
|
||||||
Serial.println("Done!");
|
display.render(logic.getScreenState());
|
||||||
|
|
||||||
|
logic.connectWiFiBlocking(); // blocks, sets CONNECTED or FAILED
|
||||||
|
display.render(logic.getScreenState());
|
||||||
|
delay(1500);
|
||||||
|
|
||||||
|
// 4. Finish boot
|
||||||
|
logic.finishBoot();
|
||||||
|
display.setBacklight(false);
|
||||||
|
|
||||||
|
Serial.println("[BOOT] Ready — monitoring ntfy.sh\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
uint16_t x, y;
|
// 1. Read touch
|
||||||
if (tft.getTouch(&x, &y)) {
|
TouchEvent touch = display.readTouch();
|
||||||
Serial.printf("Touch: %d, %d\n", x, y);
|
logic.onTouch(touch);
|
||||||
tft.fillCircle(x, y, 4, TFT_YELLOW);
|
|
||||||
}
|
// 2. Read serial commands
|
||||||
|
if (Serial.available()) {
|
||||||
|
String cmd = Serial.readStringUntil('\n');
|
||||||
|
cmd.trim();
|
||||||
|
logic.onSerialCommand(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Update business logic
|
||||||
|
logic.update();
|
||||||
|
|
||||||
|
// 4. Render
|
||||||
|
const ScreenState& state = logic.getScreenState();
|
||||||
|
display.setBacklight(state.screen != ScreenID::OFF);
|
||||||
|
display.render(state);
|
||||||
|
|
||||||
|
delay(20);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ lazygit = "latest"
|
|||||||
|
|
||||||
[env]
|
[env]
|
||||||
FQBN = "esp32:esp32:esp32:UploadSpeed=921600,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=default,DebugLevel=info,EraseFlash=none"
|
FQBN = "esp32:esp32:esp32:UploadSpeed=921600,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=default,DebugLevel=info,EraseFlash=none"
|
||||||
SKETCH_DIR = "{{cwd}}"
|
|
||||||
|
|
||||||
[tasks.install-core]
|
[tasks.install-core]
|
||||||
run = "arduino-cli core update-index && arduino-cli core install esp32:esp32"
|
run = "arduino-cli core update-index && arduino-cli core install esp32:esp32"
|
||||||
@@ -25,8 +24,8 @@ depends = ["compile"]
|
|||||||
run = "arduino-cli upload --fqbn $FQBN -p $(arduino-cli board list | grep -i 'esp32\\|usb\\|ttyUSB\\|CP210\\|CH340' | head -1 | awk '{print $1}') ."
|
run = "arduino-cli upload --fqbn $FQBN -p $(arduino-cli board list | grep -i 'esp32\\|usb\\|ttyUSB\\|CP210\\|CH340' | head -1 | awk '{print $1}') ."
|
||||||
|
|
||||||
[tasks.monitor]
|
[tasks.monitor]
|
||||||
depends = ["upload"]
|
run = "tio /dev/ttyUSB0"
|
||||||
run = "arduino-cli monitor -p $(arduino-cli board list | grep -i 'esp32\\|usb\\|ttyUSB\\|CP210\\|CH340' | head -1 | awk '{print $1}') -c baudrate=115200"
|
|
||||||
|
|
||||||
[tasks.all]
|
[tasks.all]
|
||||||
depends = ["monitor"]
|
depends = ["upload"]
|
||||||
|
run = "mise run monitor"
|
||||||
|
|||||||
Reference in New Issue
Block a user