snapshot
This commit is contained in:
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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user