snapshot
This commit is contained in:
630
doorbell-touch.ino
Normal file
630
doorbell-touch.ino
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
/*
|
||||||
|
* Doorbell Touch — Waveshare ESP32-S3-Touch-LCD-4.3 (non-B)
|
||||||
|
*
|
||||||
|
* Ported from ESP32-C6 + 1.44" TFT_eSPI version.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Polls ntfy.sh topics (ALERT, SILENCE, ADMIN, STATUS)
|
||||||
|
* - 800×480 RGB display with blinking neon teal / hot fuchsia alerts
|
||||||
|
* - GT911 touch: tap to silence, double-tap to wake status screen
|
||||||
|
* - Backlight completely OFF when not alerting
|
||||||
|
* - Multi-WiFi support
|
||||||
|
* - NTP time sync, message age filtering, deduplication
|
||||||
|
* - 30-second boot grace period ignores stale messages
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WiFiMulti.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <NTPClient.h>
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include <Arduino_GFX_Library.h>
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// WiFi — add as many networks as you like
|
||||||
|
// =====================================================================
|
||||||
|
struct WiFiCred { const char *ssid; const char *pass; };
|
||||||
|
|
||||||
|
WiFiCred wifiNetworks[] = {
|
||||||
|
{ "Dobro Veče", "goodnight" },
|
||||||
|
{ "iot-2GHz", "lesson-greater" },
|
||||||
|
};
|
||||||
|
const int NUM_WIFI = sizeof(wifiNetworks) / sizeof(wifiNetworks[0]);
|
||||||
|
|
||||||
|
WiFiMulti wifiMulti;
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// ntfy.sh Topics
|
||||||
|
// =====================================================================
|
||||||
|
#define NTFY_BASE "https://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 5000
|
||||||
|
#define BOOT_GRACE_MS 30000
|
||||||
|
#define BLINK_INTERVAL_MS 500
|
||||||
|
#define ALERT_TIMEOUT_SEC 300 // 5 min auto-silence
|
||||||
|
#define MSG_MAX_AGE_SEC 60 // ignore messages older than 60s
|
||||||
|
#define WAKE_DISPLAY_MS 5000 // show status screen for 5s
|
||||||
|
#define DOUBLE_TAP_WINDOW_MS 500 // max gap between taps
|
||||||
|
#define TOUCH_DEBOUNCE_MS 250
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// RGB565 Colors
|
||||||
|
// =====================================================================
|
||||||
|
#define COL_BLACK 0x0000
|
||||||
|
#define COL_WHITE 0xFFFF
|
||||||
|
#define COL_NEON_TEAL 0x07FA // #00FFD0
|
||||||
|
#define COL_HOT_PINK 0xF814 // #FF00A0
|
||||||
|
#define COL_DARK_GRAY 0x2104
|
||||||
|
#define COL_GREEN 0x07E0
|
||||||
|
#define COL_RED 0xF800
|
||||||
|
#define COL_YELLOW 0xFFE0
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// I2C
|
||||||
|
// =====================================================================
|
||||||
|
#define I2C_SDA 8
|
||||||
|
#define I2C_SCL 9
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// CH422G IO Expander (direct Wire — no library needed)
|
||||||
|
// =====================================================================
|
||||||
|
#define CH422G_SYS 0x24 // system / mode register
|
||||||
|
#define CH422G_OUT 0x38 // output register
|
||||||
|
#define CH422G_OE 0x01 // enable push-pull outputs
|
||||||
|
|
||||||
|
#define IO_TP_RST (1 << 1) // touch reset
|
||||||
|
#define IO_LCD_BL (1 << 2) // backlight
|
||||||
|
#define IO_LCD_RST (1 << 3) // LCD reset
|
||||||
|
|
||||||
|
static uint8_t ioState = 0; // shadow of CH422G output register
|
||||||
|
|
||||||
|
void ch422g_write(uint8_t addr, uint8_t data) {
|
||||||
|
Wire.beginTransmission(addr);
|
||||||
|
Wire.write(data);
|
||||||
|
Wire.endTransmission();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setBacklight(bool on) {
|
||||||
|
if (on) ioState |= IO_LCD_BL;
|
||||||
|
else ioState &= ~IO_LCD_BL;
|
||||||
|
ch422g_write(CH422G_OUT, ioState);
|
||||||
|
Serial.printf("Backlight %s\n", on ? "ON" : "OFF");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// GT911 Touch (direct I2C — avoids driver_ng conflict)
|
||||||
|
// =====================================================================
|
||||||
|
#define GT911_ADDR1 0x14
|
||||||
|
#define GT911_ADDR2 0x5D
|
||||||
|
#define GT911_IRQ 4
|
||||||
|
|
||||||
|
static uint8_t gt911Addr = 0;
|
||||||
|
static bool touchAvailable = false;
|
||||||
|
|
||||||
|
void gt911_writeReg(uint16_t reg, uint8_t val) {
|
||||||
|
Wire.beginTransmission(gt911Addr);
|
||||||
|
Wire.write(reg >> 8);
|
||||||
|
Wire.write(reg & 0xFF);
|
||||||
|
Wire.write(val);
|
||||||
|
Wire.endTransmission();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool gt911_init() {
|
||||||
|
pinMode(GT911_IRQ, INPUT);
|
||||||
|
|
||||||
|
// Probe both possible addresses
|
||||||
|
for (uint8_t addr : { GT911_ADDR1, GT911_ADDR2 }) {
|
||||||
|
Wire.beginTransmission(addr);
|
||||||
|
if (Wire.endTransmission() == 0) {
|
||||||
|
gt911Addr = addr;
|
||||||
|
touchAvailable = true;
|
||||||
|
Serial.printf("GT911 found at 0x%02X\n", addr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serial.println("GT911 not found — touch disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if a touch point was read
|
||||||
|
bool gt911_read(int16_t *x, int16_t *y) {
|
||||||
|
if (!touchAvailable) return false;
|
||||||
|
|
||||||
|
// Read status register 0x814E
|
||||||
|
Wire.beginTransmission(gt911Addr);
|
||||||
|
Wire.write(0x81); Wire.write(0x4E);
|
||||||
|
Wire.endTransmission(false);
|
||||||
|
Wire.requestFrom(gt911Addr, (uint8_t)1);
|
||||||
|
if (!Wire.available()) return false;
|
||||||
|
uint8_t status = Wire.read();
|
||||||
|
|
||||||
|
bool ready = status & 0x80;
|
||||||
|
uint8_t pts = status & 0x0F;
|
||||||
|
|
||||||
|
// Always clear the status flag
|
||||||
|
gt911_writeReg(0x814E, 0x00);
|
||||||
|
|
||||||
|
if (!ready || pts == 0) return false;
|
||||||
|
|
||||||
|
// Read first touch point from 0x8150 (need 4 bytes: xl, xh, yl, yh)
|
||||||
|
Wire.beginTransmission(gt911Addr);
|
||||||
|
Wire.write(0x81); Wire.write(0x50);
|
||||||
|
Wire.endTransmission(false);
|
||||||
|
Wire.requestFrom(gt911Addr, (uint8_t)4);
|
||||||
|
if (Wire.available() < 4) return false;
|
||||||
|
|
||||||
|
uint8_t xl = Wire.read();
|
||||||
|
uint8_t xh = Wire.read();
|
||||||
|
uint8_t yl = Wire.read();
|
||||||
|
uint8_t yh = Wire.read();
|
||||||
|
|
||||||
|
*x = xl | (xh << 8);
|
||||||
|
*y = yl | (yh << 8);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// RGB Display — correct pin map for 4.3 non-B
|
||||||
|
// =====================================================================
|
||||||
|
Arduino_ESP32RGBPanel *rgbpanel = new Arduino_ESP32RGBPanel(
|
||||||
|
5 /* DE */, 3 /* VSYNC */, 46 /* HSYNC */, 7 /* PCLK */,
|
||||||
|
1 /* R0 */, 2 /* R1 */, 42 /* R2 */, 41 /* R3 */, 40 /* R4 */,
|
||||||
|
39 /* G0 */, 0 /* G1 */, 45 /* G2 */, 48 /* G3 */, 47 /* G4 */, 21 /* G5 */,
|
||||||
|
14 /* B0 */, 38 /* B1 */, 18 /* B2 */, 17 /* B3 */, 10 /* B4 */,
|
||||||
|
0, 8, 4, 8, // hsync: pol, fp, pw, bp
|
||||||
|
0, 8, 4, 8, // vsync: pol, fp, pw, bp
|
||||||
|
1, 16000000 // pclk_neg, speed
|
||||||
|
);
|
||||||
|
|
||||||
|
Arduino_RGB_Display *gfx = new Arduino_RGB_Display(800, 480, rgbpanel, 0, true);
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// NTP
|
||||||
|
// =====================================================================
|
||||||
|
WiFiUDP ntpUDP;
|
||||||
|
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000);
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Application State
|
||||||
|
// =====================================================================
|
||||||
|
enum State { ST_IDLE, ST_ALERTING, ST_SILENCED, ST_WAKE };
|
||||||
|
|
||||||
|
State state = ST_IDLE;
|
||||||
|
String alertMessage = "";
|
||||||
|
String lastMsgId = "";
|
||||||
|
unsigned long bootTime = 0;
|
||||||
|
unsigned long lastPoll = 0;
|
||||||
|
unsigned long alertStart = 0;
|
||||||
|
unsigned long blinkTimer = 0;
|
||||||
|
bool blinkPhase = false;
|
||||||
|
unsigned long wakeStart = 0;
|
||||||
|
|
||||||
|
// Touch double-tap tracking
|
||||||
|
unsigned long lastTapMs = 0;
|
||||||
|
int tapCount = 0;
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Drawing Helpers
|
||||||
|
// =====================================================================
|
||||||
|
void drawCentered(const char *txt, int y, int sz, uint16_t col) {
|
||||||
|
gfx->setTextSize(sz);
|
||||||
|
gfx->setTextColor(col);
|
||||||
|
int w = strlen(txt) * 6 * sz; // approximate pixel width
|
||||||
|
gfx->setCursor(max(0, (800 - w) / 2), y);
|
||||||
|
gfx->print(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawAlertScreen(bool phase) {
|
||||||
|
uint16_t bg = phase ? COL_NEON_TEAL : COL_HOT_PINK;
|
||||||
|
uint16_t fg = phase ? COL_BLACK : COL_WHITE;
|
||||||
|
|
||||||
|
gfx->fillScreen(bg);
|
||||||
|
|
||||||
|
drawCentered("! DOORBELL !", 60, 6, fg);
|
||||||
|
|
||||||
|
if (alertMessage.length() > 0) {
|
||||||
|
gfx->setTextSize(3);
|
||||||
|
gfx->setTextColor(fg);
|
||||||
|
// Simple word-wrap: just let it overflow for now — message is usually short
|
||||||
|
gfx->setCursor(40, 220);
|
||||||
|
gfx->print(alertMessage.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCentered("TAP ANYWHERE TO SILENCE", 430, 2, fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawStatusScreen() {
|
||||||
|
gfx->fillScreen(COL_BLACK);
|
||||||
|
|
||||||
|
drawCentered("DOORBELL MONITOR", 20, 3, COL_NEON_TEAL);
|
||||||
|
gfx->drawFastHLine(50, 65, 700, COL_DARK_GRAY);
|
||||||
|
|
||||||
|
gfx->setTextSize(2);
|
||||||
|
int y = 90;
|
||||||
|
auto line = [&](uint16_t col, const char *fmt, ...) {
|
||||||
|
char buf[128];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
gfx->setTextColor(col);
|
||||||
|
gfx->setCursor(60, y);
|
||||||
|
gfx->print(buf);
|
||||||
|
y += 36;
|
||||||
|
};
|
||||||
|
|
||||||
|
line(COL_WHITE, "WiFi: %s", WiFi.isConnected() ? WiFi.SSID().c_str() : "DISCONNECTED");
|
||||||
|
line(COL_WHITE, "IP: %s", WiFi.isConnected() ? WiFi.localIP().toString().c_str() : "---");
|
||||||
|
line(COL_WHITE, "RSSI: %d dBm", WiFi.RSSI());
|
||||||
|
line(COL_WHITE, "Heap: %d KB", ESP.getFreeHeap() / 1024);
|
||||||
|
line(COL_WHITE, "PSRAM: %d MB free", ESP.getFreePsram() / (1024 * 1024));
|
||||||
|
line(COL_WHITE, "Time: %s UTC", timeClient.getFormattedTime().c_str());
|
||||||
|
line(COL_WHITE, "Up: %lu min", (millis() - bootTime) / 60000);
|
||||||
|
|
||||||
|
y += 10;
|
||||||
|
const char *stStr =
|
||||||
|
state == ST_IDLE ? "IDLE — Monitoring" :
|
||||||
|
state == ST_SILENCED ? "SILENCED" :
|
||||||
|
state == ST_ALERTING ? "ALERTING" : "AWAKE";
|
||||||
|
uint16_t stCol =
|
||||||
|
state == ST_IDLE ? COL_GREEN :
|
||||||
|
state == ST_SILENCED ? COL_YELLOW :
|
||||||
|
state == ST_ALERTING ? COL_RED : COL_NEON_TEAL;
|
||||||
|
line(stCol, "State: %s", stStr);
|
||||||
|
|
||||||
|
if (alertMessage.length() > 0) {
|
||||||
|
y += 10;
|
||||||
|
line(COL_DARK_GRAY, "Last: %s", alertMessage.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCentered("tap to dismiss | auto-sleeps in 5s", 445, 1, COL_DARK_GRAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// ntfy.sh Communication
|
||||||
|
// =====================================================================
|
||||||
|
void ntfyPublish(const char *topic, const char *msg) {
|
||||||
|
if (!WiFi.isConnected()) return;
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(String(NTFY_BASE) + "/" + topic);
|
||||||
|
http.addHeader("Content-Type", "text/plain");
|
||||||
|
http.POST(msg);
|
||||||
|
http.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll a topic; returns the message body of the newest unseen message, or ""
|
||||||
|
String ntfyPoll(const char *topic) {
|
||||||
|
if (!WiFi.isConnected()) return "";
|
||||||
|
|
||||||
|
HTTPClient http;
|
||||||
|
String url = String(NTFY_BASE) + "/" + topic + "/json?poll=1&since=5s";
|
||||||
|
http.begin(url);
|
||||||
|
http.setTimeout(5000);
|
||||||
|
|
||||||
|
int code = http.GET();
|
||||||
|
String result = "";
|
||||||
|
|
||||||
|
if (code == 200) {
|
||||||
|
String body = http.getString();
|
||||||
|
|
||||||
|
// ntfy returns newline-delimited JSON — grab the last line
|
||||||
|
int nl = body.lastIndexOf('\n', body.length() - 2);
|
||||||
|
String last = (nl >= 0) ? body.substring(nl + 1) : body;
|
||||||
|
last.trim();
|
||||||
|
|
||||||
|
if (last.length() > 2) {
|
||||||
|
JsonDocument doc;
|
||||||
|
if (!deserializeJson(doc, last) && doc["event"] == "message") {
|
||||||
|
String id = doc["id"].as<String>();
|
||||||
|
long ts = doc["time"].as<long>();
|
||||||
|
|
||||||
|
// Dedup
|
||||||
|
if (id != lastMsgId) {
|
||||||
|
// Age check
|
||||||
|
long now = timeClient.getEpochTime();
|
||||||
|
if (now <= 0 || (now - ts) <= MSG_MAX_AGE_SEC) {
|
||||||
|
lastMsgId = id;
|
||||||
|
result = doc["message"].as<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.end();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Touch Handling
|
||||||
|
// =====================================================================
|
||||||
|
void handleTouch() {
|
||||||
|
int16_t tx, ty;
|
||||||
|
if (!gt911_read(&tx, &ty)) return;
|
||||||
|
|
||||||
|
unsigned long now = millis();
|
||||||
|
static unsigned long lastDebounce = 0;
|
||||||
|
if (now - lastDebounce < TOUCH_DEBOUNCE_MS) return;
|
||||||
|
lastDebounce = now;
|
||||||
|
|
||||||
|
Serial.printf("Touch x=%d y=%d state=%d\n", tx, ty, state);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case ST_ALERTING:
|
||||||
|
// Single tap → silence
|
||||||
|
state = ST_SILENCED;
|
||||||
|
setBacklight(false);
|
||||||
|
ntfyPublish(STATUS_TOPIC, "Silenced by touch");
|
||||||
|
Serial.println("-> SILENCED (touch)");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ST_IDLE:
|
||||||
|
case ST_SILENCED:
|
||||||
|
// Double-tap detection → wake
|
||||||
|
if (now - lastTapMs < DOUBLE_TAP_WINDOW_MS)
|
||||||
|
tapCount++;
|
||||||
|
else
|
||||||
|
tapCount = 1;
|
||||||
|
lastTapMs = now;
|
||||||
|
|
||||||
|
if (tapCount >= 2) {
|
||||||
|
state = ST_WAKE;
|
||||||
|
wakeStart = now;
|
||||||
|
setBacklight(true);
|
||||||
|
drawStatusScreen();
|
||||||
|
Serial.println("-> WAKE (double-tap)");
|
||||||
|
tapCount = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ST_WAKE:
|
||||||
|
// Tap while awake → go back to sleep immediately
|
||||||
|
state = ST_IDLE;
|
||||||
|
setBacklight(false);
|
||||||
|
Serial.println("-> IDLE (tap dismiss)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// WiFi Connection
|
||||||
|
// =====================================================================
|
||||||
|
void connectWiFi() {
|
||||||
|
Serial.println("Connecting WiFi...");
|
||||||
|
for (int i = 0; i < NUM_WIFI; i++) {
|
||||||
|
wifiMulti.addAP(wifiNetworks[i].ssid, wifiNetworks[i].pass);
|
||||||
|
Serial.printf(" + %s\n", wifiNetworks[i].ssid);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBacklight(true);
|
||||||
|
gfx->fillScreen(COL_BLACK);
|
||||||
|
drawCentered("Connecting to WiFi...", 220, 2, COL_NEON_TEAL);
|
||||||
|
|
||||||
|
int tries = 0;
|
||||||
|
while (wifiMulti.run() != WL_CONNECTED && tries++ < 40) delay(500);
|
||||||
|
|
||||||
|
if (WiFi.isConnected()) {
|
||||||
|
Serial.printf("Connected: %s IP: %s\n",
|
||||||
|
WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
|
||||||
|
gfx->fillScreen(COL_BLACK);
|
||||||
|
drawCentered("Connected!", 180, 3, COL_GREEN);
|
||||||
|
char buf[64];
|
||||||
|
snprintf(buf, sizeof(buf), "%s %s",
|
||||||
|
WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
|
||||||
|
drawCentered(buf, 250, 2, COL_WHITE);
|
||||||
|
delay(2000);
|
||||||
|
} else {
|
||||||
|
Serial.println("WiFi FAILED");
|
||||||
|
gfx->fillScreen(COL_BLACK);
|
||||||
|
drawCentered("WiFi FAILED — retrying in background", 220, 2, COL_RED);
|
||||||
|
delay(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Hardware Init
|
||||||
|
// =====================================================================
|
||||||
|
void initHardware() {
|
||||||
|
Wire.begin(I2C_SDA, I2C_SCL);
|
||||||
|
|
||||||
|
// CH422G: enable outputs, assert resets, backlight off
|
||||||
|
ch422g_write(CH422G_SYS, CH422G_OE);
|
||||||
|
delay(10);
|
||||||
|
ioState = 0;
|
||||||
|
ch422g_write(CH422G_OUT, ioState);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
// Release resets (keep backlight off)
|
||||||
|
ioState = IO_TP_RST | IO_LCD_RST;
|
||||||
|
ch422g_write(CH422G_OUT, ioState);
|
||||||
|
delay(200);
|
||||||
|
|
||||||
|
// Display
|
||||||
|
if (!gfx->begin()) {
|
||||||
|
Serial.println("Display init FAILED");
|
||||||
|
while (true) delay(1000);
|
||||||
|
}
|
||||||
|
gfx->fillScreen(COL_BLACK);
|
||||||
|
|
||||||
|
// Touch
|
||||||
|
gt911_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Setup
|
||||||
|
// =====================================================================
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
unsigned long t = millis();
|
||||||
|
while (!Serial && millis() - t < 5000) delay(10);
|
||||||
|
delay(500);
|
||||||
|
|
||||||
|
Serial.println("\n========================================");
|
||||||
|
Serial.println(" Doorbell Touch v2.0");
|
||||||
|
Serial.println(" Waveshare ESP32-S3-Touch-LCD-4.3");
|
||||||
|
Serial.println("========================================");
|
||||||
|
Serial.printf("PSRAM: %d MB\n", ESP.getPsramSize() / (1024 * 1024));
|
||||||
|
|
||||||
|
if (ESP.getPsramSize() == 0) {
|
||||||
|
Serial.println("PSRAM required!");
|
||||||
|
while (true) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
initHardware();
|
||||||
|
|
||||||
|
// Boot splash
|
||||||
|
setBacklight(true);
|
||||||
|
gfx->fillScreen(COL_BLACK);
|
||||||
|
drawCentered("KLUBHAUS", 140, 6, COL_NEON_TEAL);
|
||||||
|
drawCentered("DOORBELL", 220, 6, COL_HOT_PINK);
|
||||||
|
drawCentered("v2.0", 310, 2, COL_DARK_GRAY);
|
||||||
|
delay(1500);
|
||||||
|
|
||||||
|
connectWiFi();
|
||||||
|
|
||||||
|
timeClient.begin();
|
||||||
|
timeClient.update();
|
||||||
|
|
||||||
|
bootTime = millis();
|
||||||
|
lastPoll = 0;
|
||||||
|
|
||||||
|
ntfyPublish(STATUS_TOPIC, "Doorbell Touch v2.0 booted");
|
||||||
|
|
||||||
|
// Enter idle — screen off
|
||||||
|
state = ST_IDLE;
|
||||||
|
setBacklight(false);
|
||||||
|
Serial.println("Ready — monitoring ntfy.sh\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Main Loop
|
||||||
|
// =====================================================================
|
||||||
|
void loop() {
|
||||||
|
unsigned long now = millis();
|
||||||
|
|
||||||
|
timeClient.update();
|
||||||
|
handleTouch();
|
||||||
|
|
||||||
|
// WiFi auto-reconnect
|
||||||
|
if (!WiFi.isConnected()) {
|
||||||
|
if (wifiMulti.run() == WL_CONNECTED) {
|
||||||
|
Serial.println("WiFi reconnected");
|
||||||
|
ntfyPublish(STATUS_TOPIC, "WiFi reconnected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Poll ntfy.sh ----
|
||||||
|
if (now - lastPoll >= POLL_INTERVAL_MS) {
|
||||||
|
lastPoll = now;
|
||||||
|
|
||||||
|
// Remote silence
|
||||||
|
String sil = ntfyPoll(SILENCE_TOPIC);
|
||||||
|
if (sil.length() > 0 && state == ST_ALERTING) {
|
||||||
|
state = ST_SILENCED;
|
||||||
|
setBacklight(false);
|
||||||
|
Serial.println("-> SILENCED (remote)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert
|
||||||
|
String alert = ntfyPoll(ALERT_TOPIC);
|
||||||
|
if (alert.length() > 0) {
|
||||||
|
if (now - bootTime < BOOT_GRACE_MS) {
|
||||||
|
Serial.printf("Grace period — ignoring: %s\n", alert.c_str());
|
||||||
|
} else {
|
||||||
|
alertMessage = alert;
|
||||||
|
state = ST_ALERTING;
|
||||||
|
alertStart = now;
|
||||||
|
blinkTimer = now;
|
||||||
|
blinkPhase = false;
|
||||||
|
setBacklight(true);
|
||||||
|
drawAlertScreen(blinkPhase);
|
||||||
|
ntfyPublish(STATUS_TOPIC, ("Alert: " + alert).c_str());
|
||||||
|
Serial.printf("-> ALERTING: %s\n", alert.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin commands
|
||||||
|
String admin = ntfyPoll(ADMIN_TOPIC);
|
||||||
|
if (admin.length() > 0) {
|
||||||
|
Serial.printf("Admin cmd: %s\n", admin.c_str());
|
||||||
|
|
||||||
|
if (admin == "status") {
|
||||||
|
char buf[256];
|
||||||
|
snprintf(buf, sizeof(buf),
|
||||||
|
"State:%s WiFi:%s RSSI:%d Heap:%dKB Up:%lus",
|
||||||
|
state == ST_IDLE ? "IDLE" : state == ST_ALERTING ? "ALERT" : "SILENCED",
|
||||||
|
WiFi.SSID().c_str(), WiFi.RSSI(),
|
||||||
|
ESP.getFreeHeap() / 1024, (now - bootTime) / 1000);
|
||||||
|
ntfyPublish(STATUS_TOPIC, buf);
|
||||||
|
|
||||||
|
} else if (admin == "test") {
|
||||||
|
alertMessage = "TEST ALERT";
|
||||||
|
state = ST_ALERTING;
|
||||||
|
alertStart = now;
|
||||||
|
blinkTimer = now;
|
||||||
|
blinkPhase = false;
|
||||||
|
setBacklight(true);
|
||||||
|
drawAlertScreen(blinkPhase);
|
||||||
|
|
||||||
|
} else if (admin == "wake") {
|
||||||
|
state = ST_WAKE;
|
||||||
|
wakeStart = now;
|
||||||
|
setBacklight(true);
|
||||||
|
drawStatusScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- State machine ----
|
||||||
|
switch (state) {
|
||||||
|
case ST_ALERTING:
|
||||||
|
if (now - alertStart > (unsigned long)ALERT_TIMEOUT_SEC * 1000) {
|
||||||
|
state = ST_SILENCED;
|
||||||
|
setBacklight(false);
|
||||||
|
ntfyPublish(STATUS_TOPIC, "Auto-silenced (timeout)");
|
||||||
|
Serial.println("-> SILENCED (timeout)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (now - blinkTimer >= BLINK_INTERVAL_MS) {
|
||||||
|
blinkTimer = now;
|
||||||
|
blinkPhase = !blinkPhase;
|
||||||
|
drawAlertScreen(blinkPhase);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ST_WAKE:
|
||||||
|
if (now - wakeStart > WAKE_DISPLAY_MS) {
|
||||||
|
state = ST_IDLE;
|
||||||
|
setBacklight(false);
|
||||||
|
Serial.println("-> IDLE (wake timeout)");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ST_IDLE:
|
||||||
|
case ST_SILENCED:
|
||||||
|
break; // backlight off, nothing to draw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat
|
||||||
|
static unsigned long lastHB = 0;
|
||||||
|
if (now - lastHB >= 30000) {
|
||||||
|
lastHB = now;
|
||||||
|
Serial.printf("[%lu s] %s | WiFi:%s | heap:%dKB\n",
|
||||||
|
now / 1000,
|
||||||
|
state == ST_IDLE ? "IDLE" : state == ST_ALERTING ? "ALERT" : "SILENCED",
|
||||||
|
WiFi.isConnected() ? "OK" : "DOWN",
|
||||||
|
ESP.getFreeHeap() / 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(20); // ~50 Hz touch polling
|
||||||
|
}
|
||||||
30
mise.toml
Normal file
30
mise.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[tools]
|
||||||
|
arduino-cli = "latest"
|
||||||
|
|
||||||
|
[env]
|
||||||
|
FQBN = "esp32:esp32:waveshare_esp32_s3_touch_lcd_43:UploadSpeed=921600,USBMode=hwcdc,CDCOnBoot=cdc,CPUFreq=240,FlashMode=qio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,DebugLevel=info,PSRAM=enabled,LoopCore=1,EventsCore=1,EraseFlash=none"
|
||||||
|
|
||||||
|
[tasks.install-core]
|
||||||
|
run = "arduino-cli core update-index && arduino-cli core install esp32:esp32"
|
||||||
|
|
||||||
|
[tasks.install-libs]
|
||||||
|
run = """
|
||||||
|
arduino-cli lib install "GFX Library for Arduino"
|
||||||
|
arduino-cli lib install "ArduinoJson"
|
||||||
|
arduino-cli lib install "NTPClient"
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.compile]
|
||||||
|
run = "arduino-cli compile --fqbn $FQBN ."
|
||||||
|
|
||||||
|
[tasks.upload]
|
||||||
|
depends = ["compile"]
|
||||||
|
run = "arduino-cli upload --fqbn $FQBN -p $(arduino-cli board list | grep -i 'esp32\\|usb\\|ttyACM' | head -1 | awk '{print $1}') ."
|
||||||
|
|
||||||
|
[tasks.monitor]
|
||||||
|
depends = ["upload"]
|
||||||
|
run = "arduino-cli monitor -p $(arduino-cli board list | grep -i 'esp32\\|usb\\|ttyACM' | head -1 | awk '{print $1}') -c baudrate=115200"
|
||||||
|
|
||||||
|
[tasks.all]
|
||||||
|
depends = ["monitor"]
|
||||||
|
|
||||||
Reference in New Issue
Block a user