refactor(doorbell-touch): remove entire sketch for rebuild
This commit is contained in:
@@ -1,18 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
// Board selector — driven by build flags
|
|
||||||
// Pass -DTARGET_E32R35T or -DTARGET_WAVESHARE_S3_43
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
#if defined(TARGET_E32R35T)
|
|
||||||
#include "boards/board_e32r35t.h"
|
|
||||||
|
|
||||||
#elif defined(TARGET_WAVESHARE_S3_43)
|
|
||||||
#include "boards/board_waveshare_s3.h"
|
|
||||||
|
|
||||||
#else
|
|
||||||
// Default to E32R35T for backward compatibility with existing builds
|
|
||||||
#pragma message("No TARGET_* defined — defaulting to E32R35T")
|
|
||||||
#define TARGET_E32R35T
|
|
||||||
#include "boards/board_e32r35t.h"
|
|
||||||
#endif
|
|
||||||
59
Config.h
59
Config.h
@@ -1,59 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "BoardConfig.h"
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Debug
|
|
||||||
// =====================================================================
|
|
||||||
#define DEBUG_MODE 1
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// WiFi Credentials
|
|
||||||
// =====================================================================
|
|
||||||
struct WiFiCred { const char *ssid; const char *pass; };
|
|
||||||
static WiFiCred wifiNetworks[] = {
|
|
||||||
{ "Dobro Veče", "goodnight" },
|
|
||||||
{ "iot-2GHz", "lesson-greater" },
|
|
||||||
};
|
|
||||||
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
|
|
||||||
|
|
||||||
// Change since=10s to since=20s (must be > poll interval of 15s, but not too large)
|
|
||||||
#define ALERT_URL NTFY_BASE "/ALERT_klubhaus_topic" TOPIC_SUFFIX "/json?since=20s&poll=1"
|
|
||||||
#define SILENCE_URL NTFY_BASE "/SILENCE_klubhaus_topic" TOPIC_SUFFIX "/json?since=20s&poll=1"
|
|
||||||
#define ADMIN_URL NTFY_BASE "/ADMIN_klubhaus_topic" TOPIC_SUFFIX "/json?since=20s&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 WAKE_DISPLAY_MS 5000
|
|
||||||
#define TOUCH_DEBOUNCE_MS 300
|
|
||||||
#define HOLD_DURATION_MS 2000
|
|
||||||
#define HEARTBEAT_INTERVAL_MS 30000
|
|
||||||
|
|
||||||
#if DEBUG_MODE
|
|
||||||
#define BOOT_GRACE_MS 5000
|
|
||||||
#else
|
|
||||||
#define BOOT_GRACE_MS 30000
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Hardware pins are now in boards/board_*.h via BoardConfig.h
|
|
||||||
// Screen dimensions (SCREEN_WIDTH, SCREEN_HEIGHT) also come from there.
|
|
||||||
// =====================================================================
|
|
||||||
196
Dashboard.cpp
196
Dashboard.cpp
@@ -1,196 +0,0 @@
|
|||||||
#include "Dashboard.h"
|
|
||||||
|
|
||||||
#define COL_BG 0x1082
|
|
||||||
#define COL_BAR 0x2104
|
|
||||||
#define COL_RED 0xF800
|
|
||||||
#define COL_ORANGE 0xFBE0
|
|
||||||
#define COL_GREEN 0x07E0
|
|
||||||
#define COL_CYAN 0x07FF
|
|
||||||
#define COL_PURPLE 0x780F
|
|
||||||
#define COL_WHITE 0xFFFF
|
|
||||||
#define COL_GRAY 0x8410
|
|
||||||
#define COL_DARK_TILE 0x18E3
|
|
||||||
|
|
||||||
static const uint16_t tileBG[] = {
|
|
||||||
COL_RED, COL_ORANGE, COL_CYAN, COL_PURPLE, COL_DARK_TILE, COL_DARK_TILE
|
|
||||||
};
|
|
||||||
static const uint16_t tileFG[] = {
|
|
||||||
COL_WHITE, COL_WHITE, 0x0000, COL_WHITE, COL_WHITE, COL_WHITE
|
|
||||||
};
|
|
||||||
|
|
||||||
Dashboard::Dashboard(Gfx& tft)
|
|
||||||
: _tft(tft), _sprite(&tft)
|
|
||||||
{
|
|
||||||
_tiles[TILE_LAST_ALERT] = { "!", "LAST ALERT", "none", "", 0, 0, true };
|
|
||||||
_tiles[TILE_STATS] = { "#", "TODAY", "0 alerts", "", 0, 0, true };
|
|
||||||
_tiles[TILE_NETWORK] = { "~", "NETWORK", "---", "", 0, 0, true };
|
|
||||||
_tiles[TILE_MUTE] = { "M", "MUTE", "OFF", "", 0, 0, true };
|
|
||||||
_tiles[TILE_HISTORY] = { ">", "HISTORY", "tap to view", "", 0, 0, true };
|
|
||||||
_tiles[TILE_SYSTEM] = { "*", "SYSTEM", "---", "", 0, 0, true };
|
|
||||||
|
|
||||||
for (int i = 0; i < TILE_COUNT; i++) {
|
|
||||||
_tiles[i].bgColor = tileBG[i];
|
|
||||||
_tiles[i].fgColor = tileFG[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Dashboard::begin() {
|
|
||||||
_sprite.createSprite(TILE_W, TILE_H);
|
|
||||||
_sprite.setTextDatum(MC_DATUM);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Dashboard::drawAll() {
|
|
||||||
_tft.fillScreen(COL_BG);
|
|
||||||
drawTopBar("--:--", 0, false);
|
|
||||||
for (int i = 0; i < TILE_COUNT; i++) {
|
|
||||||
drawTile((TileID)i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Dashboard::tilePosition(TileID id, int& x, int& y) {
|
|
||||||
int col = id % DASH_COLS;
|
|
||||||
int row = id / DASH_COLS;
|
|
||||||
x = DASH_MARGIN + col * (TILE_W + DASH_MARGIN);
|
|
||||||
y = DASH_TOP_BAR + DASH_MARGIN + row * (TILE_H + DASH_MARGIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Dashboard::drawTile(TileID id) {
|
|
||||||
TileData& t = _tiles[id];
|
|
||||||
int tx, ty;
|
|
||||||
tilePosition(id, tx, ty);
|
|
||||||
|
|
||||||
_sprite.fillSprite(COL_BG);
|
|
||||||
_sprite.fillRoundRect(0, 0, TILE_W, TILE_H, 8, t.bgColor);
|
|
||||||
_sprite.drawRoundRect(0, 0, TILE_W, TILE_H, 8, COL_GRAY);
|
|
||||||
|
|
||||||
_sprite.setTextColor(t.fgColor, t.bgColor);
|
|
||||||
_sprite.setTextFont(4);
|
|
||||||
_sprite.setTextSize(2);
|
|
||||||
_sprite.setTextDatum(TC_DATUM);
|
|
||||||
_sprite.drawString(t.icon, TILE_W / 2, 8);
|
|
||||||
|
|
||||||
_sprite.setTextSize(1);
|
|
||||||
_sprite.setTextFont(2);
|
|
||||||
_sprite.setTextDatum(MC_DATUM);
|
|
||||||
_sprite.drawString(t.label, TILE_W / 2, TILE_H / 2 + 5);
|
|
||||||
|
|
||||||
_sprite.setTextFont(2);
|
|
||||||
_sprite.setTextDatum(BC_DATUM);
|
|
||||||
_sprite.drawString(t.value, TILE_W / 2, TILE_H - 25);
|
|
||||||
|
|
||||||
if (strlen(t.sub) > 0) {
|
|
||||||
_sprite.setTextFont(1);
|
|
||||||
_sprite.setTextDatum(BC_DATUM);
|
|
||||||
_sprite.drawString(t.sub, TILE_W / 2, TILE_H - 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sprite.pushSprite(tx, ty);
|
|
||||||
t.dirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Dashboard::drawTopBar(const char* time, int rssi, bool wifiOk) {
|
|
||||||
_tft.fillRect(0, 0, SCREEN_WIDTH, DASH_TOP_BAR, COL_BAR);
|
|
||||||
_tft.setTextColor(COL_WHITE, COL_BAR);
|
|
||||||
_tft.setTextFont(4);
|
|
||||||
_tft.setTextSize(1);
|
|
||||||
|
|
||||||
_tft.setTextDatum(ML_DATUM);
|
|
||||||
_tft.drawString("KLUBHAUS ALERT", DASH_MARGIN, DASH_TOP_BAR / 2);
|
|
||||||
|
|
||||||
_tft.setTextDatum(MR_DATUM);
|
|
||||||
_tft.drawString(time, SCREEN_WIDTH - 10, DASH_TOP_BAR / 2);
|
|
||||||
|
|
||||||
int bars = 0;
|
|
||||||
if (wifiOk) {
|
|
||||||
if (rssi > -50) bars = 4;
|
|
||||||
else if (rssi > -60) bars = 3;
|
|
||||||
else if (rssi > -70) bars = 2;
|
|
||||||
else bars = 1;
|
|
||||||
}
|
|
||||||
int barX = SCREEN_WIDTH - 110, barW = 6, barGap = 3;
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
int barH = 6 + i * 5;
|
|
||||||
int barY = DASH_TOP_BAR - 8 - barH;
|
|
||||||
uint16_t col = (i < bars) ? COL_GREEN : COL_GRAY;
|
|
||||||
_tft.fillRect(barX + i * (barW + barGap), barY, barW, barH, col);
|
|
||||||
}
|
|
||||||
|
|
||||||
strncpy(_barTime, time, sizeof(_barTime) - 1);
|
|
||||||
_barRSSI = rssi;
|
|
||||||
_barWifiOk = wifiOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Dashboard::updateTile(TileID id, const char* value, const char* sub) {
|
|
||||||
TileData& t = _tiles[id];
|
|
||||||
bool changed = (strcmp(t.value, value) != 0);
|
|
||||||
if (sub && strcmp(t.sub, sub) != 0) changed = true;
|
|
||||||
if (!changed && !t.dirty) return;
|
|
||||||
|
|
||||||
strncpy(t.value, value, 31);
|
|
||||||
t.value[31] = '\0';
|
|
||||||
if (sub) {
|
|
||||||
strncpy(t.sub, sub, 31);
|
|
||||||
t.sub[31] = '\0';
|
|
||||||
}
|
|
||||||
t.dirty = true;
|
|
||||||
drawTile(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
int Dashboard::handleTouch(int x, int y) {
|
|
||||||
for (int i = 0; i < TILE_COUNT; i++) {
|
|
||||||
int tx, ty;
|
|
||||||
tilePosition((TileID)i, tx, ty);
|
|
||||||
if (x >= tx && x < tx + TILE_W && y >= ty && y < ty + TILE_H) return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Dashboard::refreshFromState(const ScreenState& state) {
|
|
||||||
bool barChanged = (strcmp(_barTime, state.timeString) != 0)
|
|
||||||
|| (_barRSSI != state.wifiRSSI)
|
|
||||||
|| (_barWifiOk != state.wifiConnected);
|
|
||||||
if (barChanged) {
|
|
||||||
drawTopBar(state.timeString, state.wifiRSSI, state.wifiConnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.alertHistoryCount > 0) {
|
|
||||||
updateTile(TILE_LAST_ALERT, state.alertHistory[0].message,
|
|
||||||
state.alertHistory[0].timestamp);
|
|
||||||
} else {
|
|
||||||
updateTile(TILE_LAST_ALERT, "none", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
char statsBuf[32];
|
|
||||||
snprintf(statsBuf, sizeof(statsBuf), "%d alert%s",
|
|
||||||
state.alertHistoryCount,
|
|
||||||
state.alertHistoryCount == 1 ? "" : "s");
|
|
||||||
updateTile(TILE_STATS, statsBuf, "this session");
|
|
||||||
|
|
||||||
if (state.wifiConnected) {
|
|
||||||
char rssiBuf[16];
|
|
||||||
snprintf(rssiBuf, sizeof(rssiBuf), "%d dBm", state.wifiRSSI);
|
|
||||||
updateTile(TILE_NETWORK, rssiBuf, state.wifiSSID);
|
|
||||||
} else {
|
|
||||||
updateTile(TILE_NETWORK, "DOWN", "reconnecting...");
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTile(TILE_MUTE, "OFF", "tap to mute");
|
|
||||||
|
|
||||||
if (state.alertHistoryCount > 1) {
|
|
||||||
char histBuf[48];
|
|
||||||
snprintf(histBuf, sizeof(histBuf), "%s %.20s",
|
|
||||||
state.alertHistory[1].timestamp,
|
|
||||||
state.alertHistory[1].message);
|
|
||||||
const char* sub = (state.alertHistoryCount > 2)
|
|
||||||
? state.alertHistory[2].message : "";
|
|
||||||
updateTile(TILE_HISTORY, histBuf, sub);
|
|
||||||
} else {
|
|
||||||
updateTile(TILE_HISTORY, "no history", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
char heapBuf[16];
|
|
||||||
snprintf(heapBuf, sizeof(heapBuf), "%lu KB", state.freeHeapKB);
|
|
||||||
char uptimeBuf[20];
|
|
||||||
snprintf(uptimeBuf, sizeof(uptimeBuf), "up %lum", state.uptimeMinutes);
|
|
||||||
updateTile(TILE_SYSTEM, heapBuf, uptimeBuf);
|
|
||||||
}
|
|
||||||
56
Dashboard.h
56
Dashboard.h
@@ -1,56 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "DisplayDriver.h"
|
|
||||||
#include "ScreenData.h"
|
|
||||||
|
|
||||||
#define DASH_COLS 3
|
|
||||||
#define DASH_ROWS 2
|
|
||||||
#define DASH_MARGIN 8
|
|
||||||
#define DASH_TOP_BAR 40
|
|
||||||
|
|
||||||
#define TILE_W ((SCREEN_WIDTH - (DASH_COLS + 1) * DASH_MARGIN) / DASH_COLS)
|
|
||||||
#define TILE_H ((SCREEN_HEIGHT - DASH_TOP_BAR - (DASH_ROWS + 1) * DASH_MARGIN) / DASH_ROWS)
|
|
||||||
|
|
||||||
enum TileID : uint8_t {
|
|
||||||
TILE_LAST_ALERT = 0,
|
|
||||||
TILE_STATS,
|
|
||||||
TILE_NETWORK,
|
|
||||||
TILE_MUTE,
|
|
||||||
TILE_HISTORY,
|
|
||||||
TILE_SYSTEM,
|
|
||||||
TILE_COUNT
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TileData {
|
|
||||||
const char* icon;
|
|
||||||
const char* label;
|
|
||||||
char value[32];
|
|
||||||
char sub[32];
|
|
||||||
uint16_t bgColor;
|
|
||||||
uint16_t fgColor;
|
|
||||||
bool dirty;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Dashboard {
|
|
||||||
public:
|
|
||||||
Dashboard(Gfx& tft);
|
|
||||||
|
|
||||||
void begin();
|
|
||||||
void drawAll();
|
|
||||||
void drawTopBar(const char* time, int rssi, bool wifiOk);
|
|
||||||
void updateTile(TileID id, const char* value, const char* sub = nullptr);
|
|
||||||
int handleTouch(int x, int y);
|
|
||||||
void refreshFromState(const ScreenState& state);
|
|
||||||
|
|
||||||
private:
|
|
||||||
Gfx& _tft;
|
|
||||||
GfxSprite _sprite;
|
|
||||||
TileData _tiles[TILE_COUNT];
|
|
||||||
|
|
||||||
char _barTime[12] = "";
|
|
||||||
int _barRSSI = 0;
|
|
||||||
bool _barWifiOk = false;
|
|
||||||
|
|
||||||
void drawTile(TileID id);
|
|
||||||
void tilePosition(TileID id, int& x, int& y);
|
|
||||||
};
|
|
||||||
117
DisplayDriver.h
117
DisplayDriver.h
@@ -1,117 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "BoardConfig.h"
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
// Display driver abstraction
|
|
||||||
//
|
|
||||||
// TFT_eSPI path: zero-cost typedefs — compiles identically to before
|
|
||||||
// Arduino_GFX path: adapter classes providing TFT_eSPI-compatible API
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
#if USE_TFT_ESPI
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
// TFT_eSPI — straight typedefs, zero overhead
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
#include <TFT_eSPI.h>
|
|
||||||
|
|
||||||
using Gfx = TFT_eSPI;
|
|
||||||
using GfxSprite = TFT_eSprite;
|
|
||||||
|
|
||||||
#elif USE_ARDUINO_GFX
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
// Arduino_GFX — adapter wrapping Arduino_GFX with a
|
|
||||||
// TFT_eSPI-compatible interface for Dashboard / DisplayManager
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
#include <Arduino_GFX_Library.h>
|
|
||||||
|
|
||||||
// Text datum constants (matching TFT_eSPI definitions)
|
|
||||||
#ifndef MC_DATUM
|
|
||||||
#define TL_DATUM 0
|
|
||||||
#define TC_DATUM 1
|
|
||||||
#define TR_DATUM 2
|
|
||||||
#define ML_DATUM 3
|
|
||||||
#define CL_DATUM 3
|
|
||||||
#define MC_DATUM 4
|
|
||||||
#define CC_DATUM 4
|
|
||||||
#define MR_DATUM 5
|
|
||||||
#define CR_DATUM 5
|
|
||||||
#define BL_DATUM 6
|
|
||||||
#define BC_DATUM 7
|
|
||||||
#define BR_DATUM 8
|
|
||||||
#endif
|
|
||||||
|
|
||||||
class Gfx; // forward declaration for GfxSprite
|
|
||||||
|
|
||||||
// ── Sprite adapter ──────────────────────────────────────────────
|
|
||||||
class GfxSprite {
|
|
||||||
public:
|
|
||||||
explicit GfxSprite(Gfx* parent);
|
|
||||||
|
|
||||||
void createSprite(int16_t w, int16_t h);
|
|
||||||
void deleteSprite();
|
|
||||||
void fillSprite(uint16_t color);
|
|
||||||
void fillRoundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
|
||||||
int32_t r, uint16_t color);
|
|
||||||
void drawRoundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
|
||||||
int32_t r, uint16_t color);
|
|
||||||
void setTextColor(uint16_t fg, uint16_t bg);
|
|
||||||
void setTextFont(uint8_t font);
|
|
||||||
void setTextSize(uint8_t size);
|
|
||||||
void setTextDatum(uint8_t datum);
|
|
||||||
void drawString(const char* str, int32_t x, int32_t y);
|
|
||||||
void pushSprite(int32_t x, int32_t y);
|
|
||||||
|
|
||||||
private:
|
|
||||||
Gfx* _parent;
|
|
||||||
int16_t _w = 0;
|
|
||||||
int16_t _h = 0;
|
|
||||||
int32_t _pushX = 0;
|
|
||||||
int32_t _pushY = 0;
|
|
||||||
uint8_t _textDatum = TL_DATUM;
|
|
||||||
uint8_t _textSize = 1;
|
|
||||||
uint16_t _textFg = 0xFFFF;
|
|
||||||
uint16_t _textBg = 0x0000;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Display adapter ─────────────────────────────────────────────
|
|
||||||
class Gfx {
|
|
||||||
public:
|
|
||||||
Gfx();
|
|
||||||
|
|
||||||
void init();
|
|
||||||
void setRotation(uint8_t r);
|
|
||||||
|
|
||||||
// Drawing primitives
|
|
||||||
void fillScreen(uint16_t color);
|
|
||||||
void fillRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
|
||||||
uint16_t color);
|
|
||||||
void fillRoundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
|
||||||
int32_t r, uint16_t color);
|
|
||||||
void drawRoundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
|
||||||
int32_t r, uint16_t color);
|
|
||||||
void drawFastVLine(int32_t x, int32_t y, int32_t h, uint16_t color);
|
|
||||||
|
|
||||||
// Text (datum-based API matching TFT_eSPI)
|
|
||||||
void setTextColor(uint16_t fg, uint16_t bg);
|
|
||||||
void setTextFont(uint8_t font);
|
|
||||||
void setTextSize(uint8_t size);
|
|
||||||
void setTextDatum(uint8_t datum);
|
|
||||||
void drawString(const char* str, int32_t x, int32_t y);
|
|
||||||
int16_t textWidth(const char* str);
|
|
||||||
void setCursor(int32_t x, int32_t y);
|
|
||||||
void print(const char* str);
|
|
||||||
|
|
||||||
// Escape hatch for direct Arduino_GFX access
|
|
||||||
Arduino_GFX* raw() { return _gfx; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
Arduino_GFX* _gfx = nullptr;
|
|
||||||
uint8_t _textDatum = TL_DATUM;
|
|
||||||
uint8_t _textSize = 1;
|
|
||||||
uint16_t _textFg = 0xFFFF;
|
|
||||||
uint16_t _textBg = 0x0000;
|
|
||||||
};
|
|
||||||
|
|
||||||
#else
|
|
||||||
#error "No display driver selected — check BoardConfig.h"
|
|
||||||
#endif
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
// Arduino_GFX adapter implementation
|
|
||||||
// Only compiled when USE_ARDUINO_GFX is set (Waveshare path).
|
|
||||||
// For the TFT_eSPI path this file compiles to nothing.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
#include "BoardConfig.h"
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
#if USE_ARDUINO_GFX
|
|
||||||
|
|
||||||
#include "DisplayDriver.h"
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
// Gfx adapter
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
Gfx::Gfx() {}
|
|
||||||
|
|
||||||
void Gfx::init() {
|
|
||||||
// Waveshare ESP32-S3 Touch LCD 4.3" — RGB parallel, ST7262 panel
|
|
||||||
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,
|
|
||||||
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 */,
|
|
||||||
16000000 /* prefer_speed = 16MHz PCLK */
|
|
||||||
);
|
|
||||||
|
|
||||||
_gfx = new Arduino_RGB_Display(
|
|
||||||
SCREEN_WIDTH, SCREEN_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", SCREEN_WIDTH, SCREEN_HEIGHT);
|
|
||||||
_gfx->fillScreen(0xF800); // RED TEST
|
|
||||||
delay(2000);
|
|
||||||
_gfx->fillScreen(0x07E0); // GREEN TEST
|
|
||||||
delay(2000);
|
|
||||||
_gfx->fillScreen(0x001F); // BLUE TEST
|
|
||||||
delay(2000); _gfx->fillScreen(0x07E0); // GREEN TEST delay(2000); _gfx->fillScreen(0x001F); // BLUE TEST delay(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Gfx::setRotation(uint8_t r) { if (_gfx) _gfx->setRotation(r); }
|
|
||||||
void Gfx::fillScreen(uint16_t c) { if (_gfx) _gfx->fillScreen(c); }
|
|
||||||
|
|
||||||
void Gfx::fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t c) {
|
|
||||||
if (_gfx) _gfx->fillRect(x, y, w, h, c);
|
|
||||||
}
|
|
||||||
void Gfx::fillRoundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
|
||||||
int32_t r, uint16_t c) {
|
|
||||||
if (_gfx) _gfx->fillRoundRect(x, y, w, h, r, c);
|
|
||||||
}
|
|
||||||
void Gfx::drawRoundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
|
||||||
int32_t r, uint16_t c) {
|
|
||||||
if (_gfx) _gfx->drawRoundRect(x, y, w, h, r, c);
|
|
||||||
}
|
|
||||||
void Gfx::drawFastVLine(int32_t x, int32_t y, int32_t h, uint16_t c) {
|
|
||||||
if (_gfx) _gfx->drawFastVLine(x, y, h, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Gfx::setTextColor(uint16_t fg, uint16_t bg) {
|
|
||||||
_textFg = fg; _textBg = bg;
|
|
||||||
if (_gfx) _gfx->setTextColor(fg, bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Gfx::setTextFont(uint8_t font) {
|
|
||||||
// TFT_eSPI font IDs don't map 1:1 to Arduino_GFX.
|
|
||||||
// Using default built-in font; setTextSize controls scale.
|
|
||||||
// TODO: Map to GFXfont pointers for better visual fidelity.
|
|
||||||
(void)font;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Gfx::setTextSize(uint8_t s) {
|
|
||||||
_textSize = s;
|
|
||||||
if (_gfx) _gfx->setTextSize(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Gfx::setTextDatum(uint8_t d) { _textDatum = d; }
|
|
||||||
|
|
||||||
void Gfx::drawString(const char* str, int32_t x, int32_t y) {
|
|
||||||
if (!_gfx || !str) return;
|
|
||||||
|
|
||||||
int16_t bx, by;
|
|
||||||
uint16_t tw, th;
|
|
||||||
_gfx->getTextBounds(str, 0, 0, &bx, &by, &tw, &th);
|
|
||||||
|
|
||||||
// Horizontal alignment from datum
|
|
||||||
int hAlign = _textDatum % 3;
|
|
||||||
if (hAlign == 1) x -= (int32_t)tw / 2; // center
|
|
||||||
else if (hAlign == 2) x -= (int32_t)tw; // right
|
|
||||||
|
|
||||||
// Vertical alignment from datum
|
|
||||||
int vAlign = _textDatum / 3;
|
|
||||||
if (vAlign == 1) y -= (int32_t)th / 2; // middle
|
|
||||||
else if (vAlign == 2) y -= (int32_t)th; // bottom
|
|
||||||
|
|
||||||
_gfx->setCursor(x - bx, y - by);
|
|
||||||
_gfx->print(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
int16_t Gfx::textWidth(const char* str) {
|
|
||||||
if (!_gfx || !str) return 0;
|
|
||||||
int16_t bx, by;
|
|
||||||
uint16_t tw, th;
|
|
||||||
_gfx->getTextBounds(str, 0, 0, &bx, &by, &tw, &th);
|
|
||||||
return (int16_t)tw;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Gfx::setCursor(int32_t x, int32_t y) {
|
|
||||||
if (_gfx) _gfx->setCursor(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Gfx::print(const char* str) {
|
|
||||||
if (_gfx) _gfx->print(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
// GfxSprite adapter
|
|
||||||
//
|
|
||||||
// On the 800x480 RGB panel the LCD controller has its own GRAM,
|
|
||||||
// so direct drawing rarely tears. This implementation is
|
|
||||||
// intentionally minimal — it renders tiles as direct draws.
|
|
||||||
//
|
|
||||||
// TODO: For flicker-free tile updates, allocate an Arduino_Canvas
|
|
||||||
// backed by PSRAM and blit via draw16bitBeRGBBitmap().
|
|
||||||
// ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
GfxSprite::GfxSprite(Gfx* parent) : _parent(parent) {}
|
|
||||||
|
|
||||||
void GfxSprite::createSprite(int16_t w, int16_t h) {
|
|
||||||
_w = w; _h = h;
|
|
||||||
Serial.printf("[GFX] Sprite %dx%d created (direct-draw mode)\n", w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GfxSprite::deleteSprite() { _w = 0; _h = 0; }
|
|
||||||
|
|
||||||
void GfxSprite::fillSprite(uint16_t color) {
|
|
||||||
if (_parent && _parent->raw())
|
|
||||||
_parent->raw()->fillRect(_pushX, _pushY, _w, _h, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GfxSprite::fillRoundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
|
||||||
int32_t r, uint16_t c) {
|
|
||||||
if (_parent && _parent->raw())
|
|
||||||
_parent->raw()->fillRoundRect(_pushX + x, _pushY + y, w, h, r, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GfxSprite::drawRoundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
|
||||||
int32_t r, uint16_t c) {
|
|
||||||
if (_parent && _parent->raw())
|
|
||||||
_parent->raw()->drawRoundRect(_pushX + x, _pushY + y, w, h, r, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GfxSprite::setTextColor(uint16_t fg, uint16_t bg) {
|
|
||||||
_textFg = fg; _textBg = bg;
|
|
||||||
if (_parent && _parent->raw()) _parent->raw()->setTextColor(fg, bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GfxSprite::setTextFont(uint8_t font) { (void)font; }
|
|
||||||
|
|
||||||
void GfxSprite::setTextSize(uint8_t size) {
|
|
||||||
_textSize = size;
|
|
||||||
if (_parent && _parent->raw()) _parent->raw()->setTextSize(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GfxSprite::setTextDatum(uint8_t datum) { _textDatum = datum; }
|
|
||||||
|
|
||||||
void GfxSprite::drawString(const char* str, int32_t x, int32_t y) {
|
|
||||||
if (!_parent) return;
|
|
||||||
_parent->setTextDatum(_textDatum);
|
|
||||||
_parent->setTextSize(_textSize);
|
|
||||||
_parent->setTextColor(_textFg, _textBg);
|
|
||||||
_parent->drawString(str, _pushX + x, _pushY + y);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GfxSprite::pushSprite(int32_t x, int32_t y) {
|
|
||||||
// Record the offset for subsequent draw calls in the next cycle.
|
|
||||||
// NOTE: In the TFT_eSPI path, sprite drawing happens BEFORE pushSprite
|
|
||||||
// (draws into offscreen buffer, then blits). In this direct-draw stub,
|
|
||||||
// the offset from the PREVIOUS pushSprite call is used. After one full
|
|
||||||
// drawAll() cycle, all tiles render at the correct positions.
|
|
||||||
_pushX = x;
|
|
||||||
_pushY = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // USE_ARDUINO_GFX
|
|
||||||
@@ -1,483 +0,0 @@
|
|||||||
#include "DisplayManager.h"
|
|
||||||
#include "Config.h"
|
|
||||||
|
|
||||||
#if USE_TOUCH_GT911
|
|
||||||
#include "TouchDriver.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
DisplayManager::DisplayManager() : _dash(_tft) { }
|
|
||||||
|
|
||||||
void DisplayManager::begin() {
|
|
||||||
pinMode(PIN_LCD_BL, OUTPUT);
|
|
||||||
setBacklight(true);
|
|
||||||
_tft.init();
|
|
||||||
_tft.setRotation(DISPLAY_ROTATION);
|
|
||||||
_tft.fillScreen(COL_BLACK);
|
|
||||||
|
|
||||||
#if USE_TOUCH_XPT2046
|
|
||||||
uint16_t calData[5] = { 300, 3600, 300, 3600, 1 };
|
|
||||||
_tft.setTouch(calData);
|
|
||||||
#elif USE_TOUCH_GT911
|
|
||||||
TouchDriver::begin();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void DisplayDriverGFX::setBacklight(bool on) {
|
|
||||||
// Cannot control after gfx->begin() — GPIO 8/9 are LCD data.
|
|
||||||
// Backlight is permanently ON, set during ch422gInit().
|
|
||||||
(void)on;
|
|
||||||
}
|
|
||||||
|
|
||||||
TouchEvent DisplayManager::readTouch() {
|
|
||||||
TouchEvent evt;
|
|
||||||
uint16_t x, y;
|
|
||||||
bool touched = false;
|
|
||||||
|
|
||||||
#if USE_TOUCH_XPT2046
|
|
||||||
touched = _tft.getTouch(&x, &y);
|
|
||||||
#elif USE_TOUCH_GT911
|
|
||||||
touched = TouchDriver::read(x, y);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (touched) {
|
|
||||||
evt.pressed = true;
|
|
||||||
evt.x = x;
|
|
||||||
evt.y = y;
|
|
||||||
}
|
|
||||||
return evt;
|
|
||||||
}
|
|
||||||
|
|
||||||
int DisplayManager::dashboardTouch(uint16_t x, uint16_t y) {
|
|
||||||
if (_lastScreen != ScreenID::DASHBOARD) return -1;
|
|
||||||
return _dash.handleTouch(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Hold detection — charge up, then release to confirm
|
|
||||||
// =====================================================================
|
|
||||||
HoldState DisplayManager::updateHold(unsigned long requiredMs) {
|
|
||||||
HoldState h;
|
|
||||||
h.targetMs = requiredMs;
|
|
||||||
|
|
||||||
uint16_t tx, ty;
|
|
||||||
bool touching = false;
|
|
||||||
|
|
||||||
#if USE_TOUCH_XPT2046
|
|
||||||
touching = _tft.getTouch(&tx, &ty);
|
|
||||||
#elif USE_TOUCH_GT911
|
|
||||||
touching = TouchDriver::read(tx, ty);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (touching) {
|
|
||||||
if (!_holdActive) {
|
|
||||||
_holdActive = true;
|
|
||||||
_holdCharged = false;
|
|
||||||
_holdStartMs = millis();
|
|
||||||
_holdChargeMs = 0;
|
|
||||||
_holdX = tx;
|
|
||||||
_holdY = ty;
|
|
||||||
}
|
|
||||||
|
|
||||||
h.active = true;
|
|
||||||
h.x = _holdX;
|
|
||||||
h.y = _holdY;
|
|
||||||
h.holdMs = millis() - _holdStartMs;
|
|
||||||
h.progress = min((float)h.holdMs / (float)requiredMs, 1.0f);
|
|
||||||
_holdProgress = h.progress;
|
|
||||||
|
|
||||||
if (h.holdMs >= requiredMs) {
|
|
||||||
_holdCharged = true;
|
|
||||||
if (_holdChargeMs == 0) _holdChargeMs = millis();
|
|
||||||
h.charged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (_holdActive) {
|
|
||||||
if (_holdCharged) {
|
|
||||||
h.completed = true;
|
|
||||||
h.x = _holdX;
|
|
||||||
h.y = _holdY;
|
|
||||||
Serial.println("[HOLD] Charged + released -> completed!");
|
|
||||||
} else {
|
|
||||||
h.cancelled = true;
|
|
||||||
Serial.println("[HOLD] Released early -> cancelled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_holdActive = false;
|
|
||||||
_holdCharged = false;
|
|
||||||
_holdProgress = 0.0f;
|
|
||||||
_holdChargeMs = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
|
|
||||||
float DisplayManager::holdProgress() const {
|
|
||||||
if (!_holdActive) return 0.0f;
|
|
||||||
return constrain((float)(millis() - _holdStartMs) / (float)HOLD_DURATION_MS,
|
|
||||||
0.0f, 1.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Render
|
|
||||||
// =====================================================================
|
|
||||||
void DisplayManager::render(const ScreenState& state) {
|
|
||||||
if (state.screen != _lastScreen) {
|
|
||||||
_needsFullRedraw = true;
|
|
||||||
if (state.screen == ScreenID::OFF) {
|
|
||||||
setBacklight(false);
|
|
||||||
} else if (_lastScreen == ScreenID::OFF) {
|
|
||||||
setBacklight(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:
|
|
||||||
if (_needsFullRedraw || state.blinkPhase != _lastBlink) {
|
|
||||||
drawAlertScreen(state);
|
|
||||||
_lastBlink = state.blinkPhase;
|
|
||||||
}
|
|
||||||
if (_holdProgress > 0.0f || _holdCharged) {
|
|
||||||
drawSilenceProgress(_holdProgress, _holdCharged);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ScreenID::STATUS:
|
|
||||||
if (_needsFullRedraw) drawStatusScreen(state);
|
|
||||||
break;
|
|
||||||
case ScreenID::DASHBOARD:
|
|
||||||
drawDashboard(state);
|
|
||||||
break;
|
|
||||||
case ScreenID::OFF:
|
|
||||||
if (_needsFullRedraw) {
|
|
||||||
_tft.fillScreen(COL_BLACK);
|
|
||||||
_dashSpriteReady = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_needsFullRedraw = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Dashboard
|
|
||||||
// =====================================================================
|
|
||||||
void DisplayManager::drawDashboard(const ScreenState& s) {
|
|
||||||
if (_needsFullRedraw) {
|
|
||||||
if (!_dashSpriteReady) {
|
|
||||||
_dash.begin();
|
|
||||||
_dashSpriteReady = true;
|
|
||||||
}
|
|
||||||
_dash.drawAll();
|
|
||||||
_dash.refreshFromState(s);
|
|
||||||
_lastDashRefresh = millis();
|
|
||||||
} else if (millis() - _lastDashRefresh > 2000) {
|
|
||||||
_lastDashRefresh = millis();
|
|
||||||
_dash.refreshFromState(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Silence progress bar
|
|
||||||
// =====================================================================
|
|
||||||
void DisplayManager::drawSilenceProgress(float progress, bool charged) {
|
|
||||||
const int barX = 20;
|
|
||||||
const int barY = SCREEN_HEIGHT - 50;
|
|
||||||
const int barW = SCREEN_WIDTH - 40;
|
|
||||||
const int barH = 26;
|
|
||||||
const int radius = 6;
|
|
||||||
|
|
||||||
_tft.fillRoundRect(barX, barY, barW, barH, radius, COL_DARK_GRAY);
|
|
||||||
|
|
||||||
if (charged) {
|
|
||||||
float breath = (sinf(millis() / 150.0f) + 1.0f) / 2.0f;
|
|
||||||
uint8_t gLo = 42, gHi = 63;
|
|
||||||
uint8_t g = gLo + (uint8_t)(breath * (float)(gHi - gLo));
|
|
||||||
uint16_t pulseCol = (g << 5);
|
|
||||||
|
|
||||||
_tft.fillRoundRect(barX, barY, barW, barH, radius, pulseCol);
|
|
||||||
_tft.drawRoundRect(barX, barY, barW, barH, radius, COL_WHITE);
|
|
||||||
|
|
||||||
_tft.setTextDatum(MC_DATUM);
|
|
||||||
_tft.setTextFont(2);
|
|
||||||
_tft.setTextSize(1);
|
|
||||||
_tft.setTextColor(COL_WHITE, pulseCol);
|
|
||||||
_tft.drawString("RELEASE", barX + barW / 2, barY + barH / 2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float eased = 1.0f - powf(1.0f - progress, 3.0f);
|
|
||||||
int fillW = max(1, (int)(eased * (float)barW));
|
|
||||||
|
|
||||||
uint8_t gMin = 16, gMax = 58;
|
|
||||||
for (int i = 0; i < fillW; i++) {
|
|
||||||
float frac = (float)i / (float)barW;
|
|
||||||
uint8_t g = gMin + (uint8_t)(frac * (float)(gMax - gMin));
|
|
||||||
_tft.drawFastVLine(barX + i, barY + 1, barH - 2, (uint16_t)(g << 5));
|
|
||||||
}
|
|
||||||
|
|
||||||
_tft.drawRoundRect(barX, barY, barW, barH, radius, COL_WHITE);
|
|
||||||
|
|
||||||
_tft.setTextDatum(MC_DATUM);
|
|
||||||
_tft.setTextFont(2);
|
|
||||||
_tft.setTextSize(1);
|
|
||||||
_tft.setTextColor(COL_WHITE, COL_DARK_GRAY);
|
|
||||||
_tft.drawString("HOLD", barX + barW / 2, barY + barH / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Helpers
|
|
||||||
// =====================================================================
|
|
||||||
void DisplayManager::drawCentered(const char* txt, int y, int sz, uint16_t col) {
|
|
||||||
_tft.setTextFont(1);
|
|
||||||
_tft.setTextSize(sz);
|
|
||||||
_tft.setTextColor(col, COL_BLACK);
|
|
||||||
int w = _tft.textWidth(txt);
|
|
||||||
_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.setTextFont(1);
|
|
||||||
_tft.setTextSize(1);
|
|
||||||
_tft.setTextColor(col, COL_BLACK);
|
|
||||||
_tft.setCursor(x, y);
|
|
||||||
_tft.print(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DisplayManager::drawHeaderBar(uint16_t col, const char* label,
|
|
||||||
const char* timeStr) {
|
|
||||||
_tft.setTextFont(1);
|
|
||||||
_tft.setTextSize(2);
|
|
||||||
_tft.setTextColor(col, COL_BLACK);
|
|
||||||
_tft.setCursor(8, 8);
|
|
||||||
_tft.print(label);
|
|
||||||
int tw = _tft.textWidth(timeStr);
|
|
||||||
_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);
|
|
||||||
|
|
||||||
char verBuf[48];
|
|
||||||
snprintf(verBuf, sizeof(verBuf), "v5.1 [%s]", BOARD_NAME);
|
|
||||||
drawCentered(verBuf, 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);
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_holdProgress == 0.0f && !_holdCharged) {
|
|
||||||
drawCentered("HOLD 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, sp = 22, 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 (s.alertHistoryCount > 0) {
|
|
||||||
drawInfoLine(x, y, COL_MINT, "Recent Alerts:");
|
|
||||||
y += sp;
|
|
||||||
for (int i = 0; i < s.alertHistoryCount; i++) {
|
|
||||||
uint16_t col = (i == 0) ? COL_YELLOW : COL_DARK_GRAY;
|
|
||||||
snprintf(buf, sizeof(buf), "%s %.35s",
|
|
||||||
s.alertHistory[i].timestamp,
|
|
||||||
s.alertHistory[i].message);
|
|
||||||
drawInfoLine(x, y, col, buf);
|
|
||||||
y += sp;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
drawInfoLine(x, y, COL_DARK_GRAY, "No alerts yet");
|
|
||||||
y += sp;
|
|
||||||
}
|
|
||||||
|
|
||||||
drawCentered("tap to dismiss", SCREEN_HEIGHT - 18, 1, COL_DARK_GRAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Hint animation
|
|
||||||
// =====================================================================
|
|
||||||
|
|
||||||
void DisplayManager::startHintCycle() {
|
|
||||||
_hint = HintAnim{};
|
|
||||||
_hint.lastPlayMs = millis();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DisplayManager::stopHint() {
|
|
||||||
_hint.running = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DisplayManager::updateHint() {
|
|
||||||
unsigned long now = millis();
|
|
||||||
|
|
||||||
if (_holdActive) {
|
|
||||||
_hint.running = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_hint.running) {
|
|
||||||
unsigned long gap = _hint.lastPlayMs == 0
|
|
||||||
? HintAnim::INITIAL_DELAY
|
|
||||||
: HintAnim::REPEAT_DELAY;
|
|
||||||
|
|
||||||
if (now - _hint.lastPlayMs >= gap) {
|
|
||||||
_hint.running = true;
|
|
||||||
_hint.startMs = now;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long elapsed = now - _hint.startMs;
|
|
||||||
|
|
||||||
if (elapsed > _hint.totalDur()) {
|
|
||||||
_hint.running = false;
|
|
||||||
_hint.lastPlayMs = now;
|
|
||||||
drawSilenceProgress(0.0f, false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
float progress = 0.0f;
|
|
||||||
|
|
||||||
if (elapsed < HintAnim::FILL_DUR) {
|
|
||||||
float t = (float)elapsed / (float)HintAnim::FILL_DUR;
|
|
||||||
progress = HintAnim::PEAK * (t * t);
|
|
||||||
|
|
||||||
} else if (elapsed < HintAnim::FILL_DUR + HintAnim::HOLD_DUR) {
|
|
||||||
progress = HintAnim::PEAK;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
float t = (float)(elapsed - HintAnim::FILL_DUR - HintAnim::HOLD_DUR)
|
|
||||||
/ (float)HintAnim::DRAIN_DUR;
|
|
||||||
progress = HintAnim::PEAK * (1.0f - t * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawHintBar(progress);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DisplayManager::drawHintBar(float progress) {
|
|
||||||
const int barX = 20;
|
|
||||||
const int barY = SCREEN_HEIGHT - 50;
|
|
||||||
const int barW = SCREEN_WIDTH - 40;
|
|
||||||
const int barH = 26;
|
|
||||||
const int radius = 6;
|
|
||||||
|
|
||||||
_tft.fillRoundRect(barX, barY, barW, barH, radius, COL_DARK_GRAY);
|
|
||||||
|
|
||||||
if (progress > 0.001f) {
|
|
||||||
int fillW = max(1, (int)(progress * (float)barW));
|
|
||||||
|
|
||||||
for (int i = 0; i < fillW; i++) {
|
|
||||||
float frac = (float)i / (float)barW;
|
|
||||||
uint8_t g = 12 + (uint8_t)(frac * 18.0f);
|
|
||||||
uint8_t b = 8 + (uint8_t)(frac * 10.0f);
|
|
||||||
uint16_t col = (g << 5) | b;
|
|
||||||
_tft.drawFastVLine(barX + i, barY + 1, barH - 2, col);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_tft.drawRoundRect(barX, barY, barW, barH, radius, COL_DARK_GRAY);
|
|
||||||
|
|
||||||
_tft.setTextDatum(MC_DATUM);
|
|
||||||
_tft.setTextFont(2);
|
|
||||||
_tft.setTextSize(1);
|
|
||||||
_tft.setTextColor(COL_DARK_GRAY, COL_DARK_GRAY);
|
|
||||||
_tft.drawString("HOLD TO SILENCE", barX + barW / 2, barY + barH / 2);
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "DisplayDriver.h"
|
|
||||||
#include "ScreenData.h"
|
|
||||||
#include "Dashboard.h"
|
|
||||||
|
|
||||||
// Hold gesture result
|
|
||||||
struct HoldState {
|
|
||||||
bool active = false;
|
|
||||||
bool charged = false;
|
|
||||||
bool completed = false;
|
|
||||||
bool cancelled = false;
|
|
||||||
uint16_t x = 0;
|
|
||||||
uint16_t y = 0;
|
|
||||||
unsigned long holdMs = 0;
|
|
||||||
unsigned long targetMs = 0;
|
|
||||||
float progress = 0.0f;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hint animation state
|
|
||||||
struct HintAnim {
|
|
||||||
bool running = false;
|
|
||||||
unsigned long startMs = 0;
|
|
||||||
unsigned long lastPlayMs = 0;
|
|
||||||
|
|
||||||
static const unsigned long INITIAL_DELAY = 1500;
|
|
||||||
static const unsigned long FILL_DUR = 400;
|
|
||||||
static const unsigned long HOLD_DUR = 250;
|
|
||||||
static const unsigned long DRAIN_DUR = 500;
|
|
||||||
static const unsigned long REPEAT_DELAY = 5000;
|
|
||||||
|
|
||||||
static constexpr float PEAK = 0.35f;
|
|
||||||
|
|
||||||
unsigned long totalDur() const { return FILL_DUR + HOLD_DUR + DRAIN_DUR; }
|
|
||||||
};
|
|
||||||
|
|
||||||
class DisplayManager {
|
|
||||||
public:
|
|
||||||
DisplayManager();
|
|
||||||
void begin();
|
|
||||||
void render(const ScreenState& state);
|
|
||||||
void setBacklight(bool on);
|
|
||||||
TouchEvent readTouch();
|
|
||||||
|
|
||||||
int dashboardTouch(uint16_t x, uint16_t y);
|
|
||||||
HoldState updateHold(unsigned long requiredMs);
|
|
||||||
void startHintCycle();
|
|
||||||
void stopHint();
|
|
||||||
bool updateHint();
|
|
||||||
float holdProgress() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
HintAnim _hint;
|
|
||||||
void drawHintBar(float progress);
|
|
||||||
|
|
||||||
Gfx _tft;
|
|
||||||
Dashboard _dash;
|
|
||||||
|
|
||||||
ScreenID _lastScreen = ScreenID::BOOT_SPLASH;
|
|
||||||
bool _needsFullRedraw = true;
|
|
||||||
bool _lastBlink = false;
|
|
||||||
bool _dashSpriteReady = false;
|
|
||||||
unsigned long _lastDashRefresh = 0;
|
|
||||||
|
|
||||||
// Hold tracking
|
|
||||||
bool _holdActive = false;
|
|
||||||
bool _holdCharged = false;
|
|
||||||
unsigned long _holdStartMs = 0;
|
|
||||||
unsigned long _holdChargeMs = 0;
|
|
||||||
uint16_t _holdX = 0;
|
|
||||||
uint16_t _holdY = 0;
|
|
||||||
float _holdProgress = 0.0f;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
void drawDashboard(const ScreenState& s);
|
|
||||||
void drawSilenceProgress(float progress, bool charged);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
};
|
|
||||||
@@ -1,576 +0,0 @@
|
|||||||
#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() {
|
|
||||||
_instance = this;
|
|
||||||
|
|
||||||
WiFi.mode(WIFI_STA);
|
|
||||||
WiFi.setSleep(false);
|
|
||||||
WiFi.setAutoReconnect(true);
|
|
||||||
WiFi.onEvent(onWiFiEvent);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[CONFIG] ALERT_URL: %s\n", ALERT_URL);
|
|
||||||
Serial.printf("[CONFIG] SILENCE_URL: %s\n", SILENCE_URL);
|
|
||||||
Serial.printf("[CONFIG] ADMIN_URL: %s\n", ADMIN_URL);
|
|
||||||
|
|
||||||
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 - _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;
|
|
||||||
uint32_t heap = ESP.getFreeHeap();
|
|
||||||
Serial.printf("[%lus] %s | WiFi:%s RSSI:%d | heap:%dKB | minHeap:%dKB\n",
|
|
||||||
now / 1000,
|
|
||||||
_state == DeviceState::SILENT ? "SILENT" :
|
|
||||||
_state == DeviceState::ALERTING ? "ALERT" : "WAKE",
|
|
||||||
WiFi.isConnected() ? "OK" : "DOWN",
|
|
||||||
WiFi.RSSI(),
|
|
||||||
heap / 1024,
|
|
||||||
ESP.getMinFreeHeap() / 1024);
|
|
||||||
|
|
||||||
if (heap < 20000) {
|
|
||||||
Serial.println("[HEAP] CRITICAL — rebooting!");
|
|
||||||
queueStatus("REBOOTING", "low heap");
|
|
||||||
flushStatus();
|
|
||||||
delay(200);
|
|
||||||
ESP.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
_alertMsgEpoch = 0;
|
|
||||||
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::DASHBOARD; // ← CHANGED from STATUS
|
|
||||||
Serial.println("-> WAKE (dashboard)"); // ← CHANGED
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Message Handlers
|
|
||||||
// =====================================================================
|
|
||||||
void DoorbellLogic::handleAlert(const String& msg) {
|
|
||||||
if (_state == DeviceState::ALERTING && _currentMessage == msg) return;
|
|
||||||
_currentMessage = msg;
|
|
||||||
_alertMsgEpoch = _lastParsedMsgEpoch;
|
|
||||||
|
|
||||||
for (int i = ALERT_HISTORY_SIZE - 1; i > 0; i--) {
|
|
||||||
_screen.alertHistory[i] = _screen.alertHistory[i - 1];
|
|
||||||
}
|
|
||||||
strncpy(_screen.alertHistory[0].message, msg.c_str(), 63);
|
|
||||||
_screen.alertHistory[0].message[63] = '\0';
|
|
||||||
strncpy(_screen.alertHistory[0].timestamp,
|
|
||||||
_ntpSynced ? _timeClient->getFormattedTime().c_str() : "??:??:??", 11);
|
|
||||||
_screen.alertHistory[0].timestamp[11] = '\0';
|
|
||||||
|
|
||||||
if (_screen.alertHistoryCount < ALERT_HISTORY_SIZE)
|
|
||||||
_screen.alertHistoryCount++;
|
|
||||||
|
|
||||||
Serial.printf("[ALERT] Accepted. ntfy time=%ld history=%d\n",
|
|
||||||
(long)_alertMsgEpoch, _screen.alertHistoryCount);
|
|
||||||
transitionTo(DeviceState::ALERTING);
|
|
||||||
queueStatus("ALERTING", msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DoorbellLogic::handleSilence(const String& msg) {
|
|
||||||
if (_state != DeviceState::ALERTING) {
|
|
||||||
Serial.println("[SILENCE] Ignored — not alerting");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_lastParsedMsgEpoch > 0 && _alertMsgEpoch > 0 &&
|
|
||||||
_lastParsedMsgEpoch <= _alertMsgEpoch) {
|
|
||||||
Serial.printf("[SILENCE] Ignored — predates alert (silence:%ld <= alert:%ld)\n",
|
|
||||||
(long)_lastParsedMsgEpoch, (long)_alertMsgEpoch);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[SILENCE] Accepted (silence:%ld > alert:%ld)\n",
|
|
||||||
(long)_lastParsedMsgEpoch, (long)_alertMsgEpoch);
|
|
||||||
_currentMessage = "";
|
|
||||||
_alertMsgEpoch = 0;
|
|
||||||
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() {
|
|
||||||
Serial.printf("[POLL] Starting poll cycle... WiFi:%s NTP:%d Grace:%d\n",
|
|
||||||
WiFi.isConnected() ? "OK" : "DOWN", _ntpSynced, _inBootGrace);
|
|
||||||
|
|
||||||
pollTopic(ALERT_URL, &DoorbellLogic::handleAlert, "ALERT", _lastAlertId);
|
|
||||||
yield();
|
|
||||||
pollTopic(SILENCE_URL, &DoorbellLogic::handleSilence, "SILENCE", _lastSilenceId);
|
|
||||||
yield();
|
|
||||||
pollTopic(ADMIN_URL, &DoorbellLogic::handleAdmin, "ADMIN", _lastAdminId);
|
|
||||||
|
|
||||||
Serial.printf("[POLL] Done. Heap: %dKB\n", ESP.getFreeHeap() / 1024);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DoorbellLogic::pollTopic(const char* url,
|
|
||||||
void (DoorbellLogic::*handler)(const String&),
|
|
||||||
const char* name, String& lastId) {
|
|
||||||
Serial.printf("[%s] Polling: %s\n", name, url);
|
|
||||||
|
|
||||||
if (!WiFi.isConnected()) {
|
|
||||||
Serial.printf("[%s] SKIPPED — WiFi down\n", name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiClientSecure client;
|
|
||||||
client.setInsecure();
|
|
||||||
client.setTimeout(10);
|
|
||||||
|
|
||||||
HTTPClient http;
|
|
||||||
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
|
||||||
http.setTimeout(10000);
|
|
||||||
http.setReuse(false);
|
|
||||||
|
|
||||||
if (!http.begin(client, url)) {
|
|
||||||
Serial.printf("[%s] begin() FAILED\n", name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int code = http.GET();
|
|
||||||
Serial.printf("[%s] HTTP %d\n", name, code);
|
|
||||||
|
|
||||||
if (code == HTTP_CODE_OK) {
|
|
||||||
String response = http.getString();
|
|
||||||
Serial.printf("[%s] %d bytes\n", name, response.length());
|
|
||||||
if (response.length() > 0) {
|
|
||||||
parseMessages(response, name, handler, lastId);
|
|
||||||
} else {
|
|
||||||
Serial.printf("[%s] Empty response\n", name);
|
|
||||||
}
|
|
||||||
} else if (code < 0) {
|
|
||||||
Serial.printf("[%s] ERROR: %s\n", name, http.errorToString(code).c_str());
|
|
||||||
_networkOK = false;
|
|
||||||
} else {
|
|
||||||
Serial.printf("[%s] Unexpected code: %d\n", name, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
http.end();
|
|
||||||
client.stop();
|
|
||||||
yield();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DoorbellLogic::parseMessages(String& response, const char* name,
|
|
||||||
void (DoorbellLogic::*handler)(const String&),
|
|
||||||
String& lastId) {
|
|
||||||
Serial.printf("[%s] parseMessages: grace=%d ntp=%d epoch=%ld\n",
|
|
||||||
name, _inBootGrace, _ntpSynced, (long)_lastEpoch);
|
|
||||||
|
|
||||||
if (_inBootGrace || !_ntpSynced || _lastEpoch == 0) {
|
|
||||||
Serial.printf("[%s] SKIPPED — guard failed\n", name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int lineStart = 0;
|
|
||||||
int msgCount = 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) {
|
|
||||||
msgCount++;
|
|
||||||
JsonDocument doc;
|
|
||||||
if (deserializeJson(doc, line)) {
|
|
||||||
Serial.printf("[%s] JSON parse FAILED on line %d\n", name, msgCount);
|
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* event = doc["event"];
|
|
||||||
const char* msgId = doc["id"];
|
|
||||||
const char* message = doc["message"];
|
|
||||||
time_t msgTime = doc["time"] | 0;
|
|
||||||
|
|
||||||
Serial.printf("[%s] msg#%d: event=%s id=%s time=%ld msg=%.30s\n",
|
|
||||||
name, msgCount,
|
|
||||||
event ? event : "null",
|
|
||||||
msgId ? msgId : "null",
|
|
||||||
(long)msgTime,
|
|
||||||
message ? message : "null");
|
|
||||||
|
|
||||||
if (event && strcmp(event, "message") != 0) {
|
|
||||||
Serial.printf("[%s] SKIP — not a message event (event=%s)\n", name, event);
|
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message || strlen(message) == 0) {
|
|
||||||
Serial.printf("[%s] SKIP — empty message\n", name);
|
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String idStr = msgId ? String(msgId) : "";
|
|
||||||
if (idStr.length() > 0 && idStr == lastId) {
|
|
||||||
Serial.printf("[%s] SKIP — dedup (id=%s)\n", name, msgId);
|
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msgTime > 0 && (_lastEpoch - msgTime) > (time_t)STALE_MSG_THRESHOLD_S) {
|
|
||||||
Serial.printf("[%s] SKIP — stale (age=%llds, threshold=%ds)\n",
|
|
||||||
name, (long long)(_lastEpoch - msgTime), STALE_MSG_THRESHOLD_S);
|
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[%s] ACCEPTED: %.50s\n", name, message);
|
|
||||||
if (idStr.length() > 0) lastId = idStr;
|
|
||||||
|
|
||||||
_lastParsedMsgEpoch = msgTime;
|
|
||||||
(this->*handler)(String(message));
|
|
||||||
_lastParsedMsgEpoch = 0;
|
|
||||||
}
|
|
||||||
lineStart = lineEnd + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[%s] Parsed %d JSON objects\n", name, msgCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Status Publishing
|
|
||||||
// =====================================================================
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
DoorbellLogic* DoorbellLogic::_instance = nullptr;
|
|
||||||
|
|
||||||
void DoorbellLogic::onWiFiEvent(WiFiEvent_t event) {
|
|
||||||
if (!_instance) return;
|
|
||||||
switch (event) {
|
|
||||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
|
||||||
Serial.println("[WIFI] Disconnected — will reconnect");
|
|
||||||
WiFi.reconnect();
|
|
||||||
break;
|
|
||||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
|
||||||
Serial.println("[WIFI] Reconnected to AP");
|
|
||||||
break;
|
|
||||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
|
||||||
Serial.printf("[WIFI] Got IP: %s\n", WiFi.localIP().toString().c_str());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#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;
|
|
||||||
|
|
||||||
// Stale silence protection
|
|
||||||
time_t _alertMsgEpoch = 0; // ntfy timestamp of the alert that started ALERTING
|
|
||||||
time_t _lastParsedMsgEpoch = 0; // ntfy timestamp of message currently being handled
|
|
||||||
|
|
||||||
|
|
||||||
// Deferred status publish
|
|
||||||
bool _pendingStatus = false;
|
|
||||||
String _pendStatusState;
|
|
||||||
String _pendStatusMsg;
|
|
||||||
|
|
||||||
// NTP — pointer because NTPClient has no default constructor
|
|
||||||
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();
|
|
||||||
static DoorbellLogic* _instance; // for static event callback
|
|
||||||
static void onWiFiEvent(WiFiEvent_t event);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
58
ScreenData.h
58
ScreenData.h
@@ -1,58 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
enum class DeviceState : uint8_t {
|
|
||||||
SILENT,
|
|
||||||
ALERTING,
|
|
||||||
WAKE
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class ScreenID : uint8_t {
|
|
||||||
BOOT_SPLASH,
|
|
||||||
WIFI_CONNECTING,
|
|
||||||
WIFI_CONNECTED,
|
|
||||||
WIFI_FAILED,
|
|
||||||
ALERT,
|
|
||||||
STATUS,
|
|
||||||
DASHBOARD,
|
|
||||||
OFF
|
|
||||||
};
|
|
||||||
|
|
||||||
#define ALERT_HISTORY_SIZE 3
|
|
||||||
|
|
||||||
struct AlertRecord {
|
|
||||||
char message[64] = "";
|
|
||||||
char timestamp[12] = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ScreenState {
|
|
||||||
ScreenID screen = ScreenID::BOOT_SPLASH;
|
|
||||||
DeviceState deviceState = DeviceState::SILENT;
|
|
||||||
bool blinkPhase = false;
|
|
||||||
|
|
||||||
char alertMessage[64] = "";
|
|
||||||
|
|
||||||
bool wifiConnected = false;
|
|
||||||
char wifiSSID[33] = "";
|
|
||||||
int wifiRSSI = 0;
|
|
||||||
char wifiIP[16] = "";
|
|
||||||
|
|
||||||
bool ntpSynced = false;
|
|
||||||
char timeString[12] = "";
|
|
||||||
|
|
||||||
uint32_t uptimeMinutes = 0;
|
|
||||||
uint32_t freeHeapKB = 0;
|
|
||||||
bool networkOK = false;
|
|
||||||
|
|
||||||
bool debugMode = false;
|
|
||||||
|
|
||||||
AlertRecord alertHistory[ALERT_HISTORY_SIZE] = {};
|
|
||||||
int alertHistoryCount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TouchEvent {
|
|
||||||
bool pressed = false;
|
|
||||||
uint16_t x = 0;
|
|
||||||
uint16_t y = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "BoardConfig.h"
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
// Touch driver abstraction
|
|
||||||
//
|
|
||||||
// XPT2046: integrated in TFT_eSPI — DisplayManager calls
|
|
||||||
// _tft.getTouch() / _tft.setTouch() directly inside
|
|
||||||
// #if USE_TOUCH_XPT2046 blocks.
|
|
||||||
//
|
|
||||||
// GT911: separate I2C controller — namespace below.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
#if USE_TOUCH_GT911
|
|
||||||
|
|
||||||
namespace TouchDriver {
|
|
||||||
void begin();
|
|
||||||
bool read(uint16_t &x, uint16_t &y);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#include "BoardConfig.h"
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
#if USE_TOUCH_GT911
|
|
||||||
|
|
||||||
#include "TouchDriver.h"
|
|
||||||
#include <Wire.h>
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
// GT911 capacitive touch — Waveshare ESP32-S3 Touch LCD 4.3"
|
|
||||||
//
|
|
||||||
// This is a compilable stub. To enable actual touch:
|
|
||||||
// 1. arduino-cli lib install "TAMC_GT911"
|
|
||||||
// 2. Uncomment the TAMC_GT911 lines below.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// #include <TAMC_GT911.h>
|
|
||||||
// static TAMC_GT911 ts(TOUCH_SDA, TOUCH_SCL, TOUCH_INT, TOUCH_RST,
|
|
||||||
// SCREEN_WIDTH, SCREEN_HEIGHT);
|
|
||||||
|
|
||||||
namespace TouchDriver {
|
|
||||||
|
|
||||||
void begin() {
|
|
||||||
Wire.begin(TOUCH_SDA, TOUCH_SCL);
|
|
||||||
// ts.begin();
|
|
||||||
// ts.setRotation(TOUCH_MAP_ROTATION);
|
|
||||||
Serial.println("[TOUCH] GT911 stub initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool read(uint16_t &x, uint16_t &y) {
|
|
||||||
// ts.read();
|
|
||||||
// if (ts.isTouched) {
|
|
||||||
// x = ts.points[0].x;
|
|
||||||
// y = ts.points[0].y;
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
(void)x; (void)y;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace TouchDriver
|
|
||||||
|
|
||||||
#endif // USE_TOUCH_GT911
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
// Board: E32R35T — ESP32-WROOM-32E + 3.5" ST7796S SPI + XPT2046
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
#define BOARD_NAME "E32R35T"
|
|
||||||
|
|
||||||
// ── Display ─────────────────────────────────────────────────────
|
|
||||||
#define SCREEN_WIDTH 480
|
|
||||||
#define SCREEN_HEIGHT 320
|
|
||||||
#define DISPLAY_ROTATION 1
|
|
||||||
|
|
||||||
// ── Driver selection ────────────────────────────────────────────
|
|
||||||
#define USE_TFT_ESPI 1
|
|
||||||
#define USE_ARDUINO_GFX 0
|
|
||||||
#define USE_TOUCH_XPT2046 1
|
|
||||||
#define USE_TOUCH_GT911 0
|
|
||||||
|
|
||||||
// ── Hardware capabilities ───────────────────────────────────────
|
|
||||||
#define HAS_PSRAM 0
|
|
||||||
|
|
||||||
// ── 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
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
// Board: Waveshare ESP32-S3 Touch LCD 4.3"
|
|
||||||
// 800x480 RGB parallel + GT911 capacitive touch
|
|
||||||
//
|
|
||||||
// NOTE: Pin assignments are typical for this board revision.
|
|
||||||
// Verify against your specific board's schematic.
|
|
||||||
// The Arduino board variant 'waveshare_esp32_s3_touch_lcd_43'
|
|
||||||
// may override some of these via its pins_arduino.h.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
#define BOARD_NAME "WS_S3_43"
|
|
||||||
|
|
||||||
// ── Display ─────────────────────────────────────────────────────
|
|
||||||
#define SCREEN_WIDTH 800
|
|
||||||
#define SCREEN_HEIGHT 480
|
|
||||||
#define DISPLAY_ROTATION 0 // native landscape
|
|
||||||
|
|
||||||
// ── Driver selection ────────────────────────────────────────────
|
|
||||||
#define USE_TFT_ESPI 0
|
|
||||||
#define USE_ARDUINO_GFX 1
|
|
||||||
#define USE_TOUCH_XPT2046 0
|
|
||||||
#define USE_TOUCH_GT911 1
|
|
||||||
|
|
||||||
// ── Hardware capabilities ───────────────────────────────────────
|
|
||||||
#define HAS_PSRAM 1
|
|
||||||
|
|
||||||
// ── Backlight ───────────────────────────────────────────────────
|
|
||||||
#define PIN_LCD_BL 2
|
|
||||||
|
|
||||||
// ── GT911 I2C touch controller ──────────────────────────────────
|
|
||||||
#define TOUCH_SDA 17
|
|
||||||
#define TOUCH_SCL 18
|
|
||||||
#define TOUCH_INT -1
|
|
||||||
#define TOUCH_RST 38
|
|
||||||
|
|
||||||
// ── RGB LCD data pins (ESP32-S3 LCD_CAM peripheral) ─────────────
|
|
||||||
// Adjust if your board revision differs
|
|
||||||
#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
|
|
||||||
|
|
||||||
// ── Peripherals not present on this board ───────────────────────
|
|
||||||
// These are left undefined intentionally. Code that uses them
|
|
||||||
// should guard with #ifdef PIN_LED_RED etc.
|
|
||||||
// Uncomment and set values if your carrier board adds them.
|
|
||||||
//
|
|
||||||
// #define PIN_LED_RED -1
|
|
||||||
// #define PIN_LED_GREEN -1
|
|
||||||
// #define PIN_LED_BLUE -1
|
|
||||||
// #define PIN_AUDIO_EN -1
|
|
||||||
// #define PIN_AUDIO_DAC -1
|
|
||||||
// #define PIN_BAT_ADC -1
|
|
||||||
// #define PIN_SD_CS -1
|
|
||||||
// #define PIN_TOUCH_CS -1
|
|
||||||
// #define PIN_TOUCH_IRQ -1
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
/*
|
|
||||||
* KLUBHAUS ALERT v5.1 — " BOARD_NAME " Edition
|
|
||||||
*
|
|
||||||
* Target: LCDWiki E32R35T (ESP32-WROOM-32E + 3.5" ST7796S + XPT2046)
|
|
||||||
*
|
|
||||||
* Hold-and-release interaction model:
|
|
||||||
* - Hold finger → progress bar fills
|
|
||||||
* - Bar full → jitter/flash ("RELEASE!")
|
|
||||||
* - Lift finger → action fires (finger already off screen)
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <SPI.h>
|
|
||||||
#include "Config.h"
|
|
||||||
#include "DisplayManager.h"
|
|
||||||
#include "DoorbellLogic.h"
|
|
||||||
#include "BoardConfig.h"
|
|
||||||
|
|
||||||
#include <TFT_eSPI.h>
|
|
||||||
#ifndef LOAD_GLCD
|
|
||||||
#error "LOAD_GLCD is NOT defined — fonts missing!"
|
|
||||||
#endif
|
|
||||||
#if USE_TFT_ESPI
|
|
||||||
#ifndef ST7796_DRIVER
|
|
||||||
#if USE_TFT_ESPI
|
|
||||||
#error "TFT_eSPI setup mismatch — ST7796_DRIVER expected for E32R35T"
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
#define HOLD_TO_SILENCE_MS 1000
|
|
||||||
|
|
||||||
DoorbellLogic logic;
|
|
||||||
DisplayManager display;
|
|
||||||
|
|
||||||
void setup() {
|
|
||||||
Serial.begin(115200);
|
|
||||||
unsigned long t = millis();
|
|
||||||
while (!Serial && millis() - t < 3000) delay(10);
|
|
||||||
delay(500);
|
|
||||||
|
|
||||||
Serial.println("\n========================================");
|
|
||||||
Serial.println(" KLUBHAUS ALERT v5.1 — " BOARD_NAME "");
|
|
||||||
#if DEBUG_MODE
|
|
||||||
Serial.println(" *** DEBUG MODE — _test topics ***");
|
|
||||||
#endif
|
|
||||||
Serial.println("========================================");
|
|
||||||
|
|
||||||
display.begin();
|
|
||||||
|
|
||||||
logic.begin();
|
|
||||||
display.render(logic.getScreenState());
|
|
||||||
delay(1500);
|
|
||||||
|
|
||||||
logic.beginWiFi();
|
|
||||||
display.render(logic.getScreenState());
|
|
||||||
|
|
||||||
logic.connectWiFiBlocking();
|
|
||||||
display.render(logic.getScreenState());
|
|
||||||
delay(1500);
|
|
||||||
|
|
||||||
logic.finishBoot();
|
|
||||||
display.setBacklight(false);
|
|
||||||
|
|
||||||
Serial.printf("[MEM] Free heap: %d | Free PSRAM: %d\n",
|
|
||||||
ESP.getFreeHeap(), ESP.getFreePsram());
|
|
||||||
Serial.println("[BOOT] Ready — monitoring ntfy.sh\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ── Silence handler (delegates to DoorbellLogic) ────────────────
|
|
||||||
void silenceAlerts() {
|
|
||||||
Serial.println("[SILENCE] User completed hold-to-silence gesture");
|
|
||||||
logic.onTouch(TouchEvent{true, 0, 0});
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
logic.update();
|
|
||||||
display.render(logic.getScreenState());
|
|
||||||
|
|
||||||
// Touch → hold-to-silence gesture
|
|
||||||
TouchEvent evt = display.readTouch();
|
|
||||||
if (evt.pressed) {
|
|
||||||
// Dashboard tile tap
|
|
||||||
if (logic.getScreenState().screen == ScreenID::DASHBOARD) {
|
|
||||||
int tile = display.dashboardTouch(evt.x, evt.y);
|
|
||||||
if (tile >= 0) Serial.printf("[DASH] Tile %d tapped\n", tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hold-to-silence during ALERT
|
|
||||||
if (logic.getScreenState().deviceState == DeviceState::ALERTING) {
|
|
||||||
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
|
|
||||||
if (h.completed) silenceAlerts();
|
|
||||||
|
|
||||||
// Hint animation when not touching
|
|
||||||
if (!h.active) display.updateHint();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serial commands
|
|
||||||
if (Serial.available()) {
|
|
||||||
String cmd = Serial.readStringUntil('\n');
|
|
||||||
cmd.trim();
|
|
||||||
if (cmd.length() > 0) logic.onSerialCommand(cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
logic.update();
|
|
||||||
display.render(logic.getScreenState());
|
|
||||||
|
|
||||||
// Touch → hold-to-silence gesture
|
|
||||||
TouchEvent evt = display.readTouch();
|
|
||||||
if (evt.pressed) {
|
|
||||||
// Dashboard tile tap
|
|
||||||
if (logic.getScreenState().screen == ScreenID::DASHBOARD) {
|
|
||||||
int tile = display.dashboardTouch(evt.x, evt.y);
|
|
||||||
if (tile >= 0) Serial.printf("[DASH] Tile %d tapped\n", tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hold-to-silence during ALERT
|
|
||||||
if (logic.getScreenState().deviceState == DeviceState::ALERTING) {
|
|
||||||
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
|
|
||||||
if (h.completed) silenceAlerts();
|
|
||||||
|
|
||||||
// Hint animation when not touching
|
|
||||||
if (!h.active) display.updateHint();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serial commands
|
|
||||||
if (Serial.available()) {
|
|
||||||
String cmd = Serial.readStringUntil('\n');
|
|
||||||
cmd.trim();
|
|
||||||
if (cmd.length() > 0) logic.onSerialCommand(cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user