move to new multi-board setup
This commit is contained in:
16
sketches/doorbell-touch-esp32-32e/.gitignore
vendored
Normal file
16
sketches/doorbell-touch-esp32-32e/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Secrets (WiFi passwords etc.)
|
||||||
|
**/secrets.h
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
**/build/
|
||||||
|
|
||||||
|
# Vendored libraries (re-created by install-libs)
|
||||||
|
vendor/esp32-32e/TFT_eSPI/
|
||||||
|
vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
91
sketches/doorbell-touch-esp32-32e/README.md
Normal file
91
sketches/doorbell-touch-esp32-32e/README.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Klubhaus Doorbell
|
||||||
|
|
||||||
|
Multi-target doorbell alert system powered by [ntfy.sh](https://ntfy.sh).
|
||||||
|
|
||||||
|
## Targets
|
||||||
|
|
||||||
|
| Board | Display | Library | Build Task |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ESP32-32E | SPI TFT (ILI9341 etc.) | TFT_eSPI | `mise run compile-32e` |
|
||||||
|
| ESP32-S3-Touch-LCD-4.3 | 800x480 RGB parallel | Arduino_GFX | `mise run compile-s3-43` |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Install prerequisites: arduino-cli (with esp32:esp32 platform) and mise.
|
||||||
|
|
||||||
|
2. Edit WiFi credentials:
|
||||||
|
|
||||||
|
cp boards/esp32-32e/secrets.h.example boards/esp32-32e/secrets.h
|
||||||
|
cp boards/esp32-s3-lcd-43/secrets.h.example boards/esp32-s3-lcd-43/secrets.h
|
||||||
|
|
||||||
|
Then edit both secrets.h files with your WiFi SSIDs and passwords.
|
||||||
|
|
||||||
|
3. Build and upload:
|
||||||
|
|
||||||
|
mise run compile-s3-43
|
||||||
|
mise run upload-s3-43
|
||||||
|
|
||||||
|
mise run compile-32e
|
||||||
|
mise run upload-32e
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
.
|
||||||
|
├── libraries/
|
||||||
|
│ └── KlubhausCore/ Shared Arduino library
|
||||||
|
│ └── src/
|
||||||
|
│ ├── KlubhausCore.h Umbrella include
|
||||||
|
│ ├── Config.h Shared constants
|
||||||
|
│ ├── ScreenState.h State enums / structs
|
||||||
|
│ ├── IDisplayDriver.h Abstract display interface
|
||||||
|
│ ├── DisplayManager.h Thin delegation wrapper
|
||||||
|
│ ├── NetManager.* WiFi, NTP, HTTP
|
||||||
|
│ └── DoorbellLogic.* State machine, ntfy polling
|
||||||
|
├── boards/
|
||||||
|
│ ├── esp32-32e/ ESP32-32E sketch
|
||||||
|
│ │ ├── esp32-32e.ino
|
||||||
|
│ │ ├── board_config.h
|
||||||
|
│ │ ├── secrets.h (gitignored)
|
||||||
|
│ │ ├── tft_user_setup.h
|
||||||
|
│ │ └── DisplayDriverTFT.*
|
||||||
|
│ └── esp32-s3-lcd-43/ ESP32-S3-LCD-4.3 sketch
|
||||||
|
│ ├── esp32-s3-lcd-43.ino
|
||||||
|
│ ├── board_config.h
|
||||||
|
│ ├── secrets.h (gitignored)
|
||||||
|
│ └── DisplayDriverGFX.*
|
||||||
|
├── vendor/ Per-board vendored display libs
|
||||||
|
│ ├── esp32-32e/TFT_eSPI/
|
||||||
|
│ └── esp32-s3-lcd-43/GFX_Library_for_Arduino/
|
||||||
|
└── mise.toml Build harness
|
||||||
|
|
||||||
|
## Serial Commands
|
||||||
|
|
||||||
|
Type into the serial monitor at 115200 baud:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
|---|---|
|
||||||
|
| alert | Trigger a test alert |
|
||||||
|
| silence | Silence current alert |
|
||||||
|
| dashboard | Show dashboard screen |
|
||||||
|
| off | Turn off display |
|
||||||
|
| status | Print state + memory info |
|
||||||
|
| reboot | Restart device |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Each board target gets its own sketch directory with a concrete IDisplayDriver
|
||||||
|
implementation. The shared KlubhausCore library contains all business logic,
|
||||||
|
networking, and the abstract interface. Arduino CLI's --libraries flag ensures
|
||||||
|
each board only links its own vendored display library -- no preprocessor conflicts.
|
||||||
|
|
||||||
|
Board sketch (.ino)
|
||||||
|
|
|
||||||
|
+-- #include <KlubhausCore.h> (shared library)
|
||||||
|
| +-- DoorbellLogic (state machine + ntfy polling)
|
||||||
|
| +-- NetManager (WiFi, HTTP, NTP)
|
||||||
|
| +-- DisplayManager (delegates to IDisplayDriver)
|
||||||
|
| +-- IDisplayDriver (pure virtual interface)
|
||||||
|
|
|
||||||
|
+-- DisplayDriverXxx (board-specific, concrete driver)
|
||||||
|
+-- links against vendored display lib
|
||||||
|
(TFT_eSPI or Arduino_GFX, never both)
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
#include "DisplayDriverTFT.h"
|
||||||
|
|
||||||
|
void DisplayDriverTFT::begin() {
|
||||||
|
// Backlight
|
||||||
|
pinMode(PIN_LCD_BL, OUTPUT);
|
||||||
|
digitalWrite(PIN_LCD_BL, LOW);
|
||||||
|
|
||||||
|
_tft.init();
|
||||||
|
_tft.setRotation(DISPLAY_ROTATION);
|
||||||
|
_tft.fillScreen(TFT_BLACK);
|
||||||
|
|
||||||
|
Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||||
|
|
||||||
|
drawBoot();
|
||||||
|
|
||||||
|
digitalWrite(PIN_LCD_BL, HIGH);
|
||||||
|
Serial.println("[GFX] Backlight ON");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverTFT::setBacklight(bool on) {
|
||||||
|
digitalWrite(PIN_LCD_BL, on ? HIGH : LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rendering ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
void DisplayDriverTFT::render(const ScreenState& st) {
|
||||||
|
if (st.screen != _lastScreen) {
|
||||||
|
_needsRedraw = true;
|
||||||
|
_lastScreen = st.screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (st.screen) {
|
||||||
|
case ScreenID::BOOT:
|
||||||
|
if (_needsRedraw) { drawBoot(); _needsRedraw = false; }
|
||||||
|
break;
|
||||||
|
case ScreenID::ALERT:
|
||||||
|
drawAlert(st);
|
||||||
|
break;
|
||||||
|
case ScreenID::DASHBOARD:
|
||||||
|
if (_needsRedraw) { drawDashboard(st); _needsRedraw = false; }
|
||||||
|
break;
|
||||||
|
case ScreenID::OFF:
|
||||||
|
if (_needsRedraw) { _tft.fillScreen(TFT_BLACK); _needsRedraw = false; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverTFT::drawBoot() {
|
||||||
|
_tft.fillScreen(TFT_BLACK);
|
||||||
|
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
_tft.setTextSize(2);
|
||||||
|
_tft.setCursor(10, 10);
|
||||||
|
_tft.printf("KLUBHAUS v%s", FW_VERSION);
|
||||||
|
_tft.setTextSize(1);
|
||||||
|
_tft.setCursor(10, 40);
|
||||||
|
_tft.print(BOARD_NAME);
|
||||||
|
_tft.setCursor(10, 60);
|
||||||
|
_tft.print("Booting...");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverTFT::drawAlert(const ScreenState& st) {
|
||||||
|
uint32_t elapsed = millis() - st.alertStartMs;
|
||||||
|
uint8_t pulse = 180 + (uint8_t)(75.0f * sinf(elapsed / 300.0f));
|
||||||
|
uint16_t bg = _tft.color565(pulse, 0, 0);
|
||||||
|
|
||||||
|
_tft.fillScreen(bg);
|
||||||
|
_tft.setTextColor(TFT_WHITE, bg);
|
||||||
|
|
||||||
|
_tft.setTextSize(3);
|
||||||
|
_tft.setCursor(10, 20);
|
||||||
|
_tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT");
|
||||||
|
|
||||||
|
_tft.setTextSize(2);
|
||||||
|
_tft.setCursor(10, 80);
|
||||||
|
_tft.print(st.alertBody);
|
||||||
|
|
||||||
|
_tft.setTextSize(1);
|
||||||
|
_tft.setCursor(10, DISPLAY_HEIGHT - 20);
|
||||||
|
_tft.print("Hold to silence...");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
|
||||||
|
_tft.fillScreen(TFT_BLACK);
|
||||||
|
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
|
||||||
|
_tft.setTextSize(1);
|
||||||
|
_tft.setCursor(5, 5);
|
||||||
|
_tft.printf("KLUBHAUS — %s", deviceStateStr(st.deviceState));
|
||||||
|
|
||||||
|
int y = 30;
|
||||||
|
_tft.setCursor(5, y); y += 18;
|
||||||
|
_tft.printf("WiFi: %s %ddBm", st.wifiSsid.c_str(), st.wifiRssi);
|
||||||
|
|
||||||
|
_tft.setCursor(5, y); y += 18;
|
||||||
|
_tft.printf("IP: %s", st.ipAddr.c_str());
|
||||||
|
|
||||||
|
_tft.setCursor(5, y); y += 18;
|
||||||
|
_tft.printf("Up: %lus Heap: %d", st.uptimeMs / 1000, ESP.getFreeHeap());
|
||||||
|
|
||||||
|
_tft.setCursor(5, y); y += 18;
|
||||||
|
_tft.printf("Last poll: %lus ago",
|
||||||
|
st.lastPollMs > 0 ? (millis() - st.lastPollMs) / 1000 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Touch ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TouchEvent DisplayDriverTFT::readTouch() {
|
||||||
|
TouchEvent evt;
|
||||||
|
uint16_t tx, ty;
|
||||||
|
if (_tft.getTouch(&tx, &ty)) {
|
||||||
|
evt.pressed = true;
|
||||||
|
evt.x = tx;
|
||||||
|
evt.y = ty;
|
||||||
|
}
|
||||||
|
return evt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int DisplayDriverTFT::dashboardTouch(int x, int y) {
|
||||||
|
// 2-column, 4-row
|
||||||
|
int col = x / (DISPLAY_WIDTH / 2);
|
||||||
|
int row = (y - 30) / 40;
|
||||||
|
if (row < 0 || row > 3) return -1;
|
||||||
|
return row * 2 + col;
|
||||||
|
}
|
||||||
|
|
||||||
|
HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) {
|
||||||
|
HoldState h;
|
||||||
|
TouchEvent t = readTouch();
|
||||||
|
|
||||||
|
if (t.pressed) {
|
||||||
|
if (!_holdActive) {
|
||||||
|
_holdActive = true;
|
||||||
|
_holdStartMs = millis();
|
||||||
|
}
|
||||||
|
uint32_t held = millis() - _holdStartMs;
|
||||||
|
h.active = true;
|
||||||
|
h.progress = constrain((float)held / (float)holdMs, 0.0f, 1.0f);
|
||||||
|
h.completed = (held >= holdMs);
|
||||||
|
|
||||||
|
// Simple progress bar at bottom of screen
|
||||||
|
int barW = (int)(DISPLAY_WIDTH * h.progress);
|
||||||
|
_tft.fillRect(0, DISPLAY_HEIGHT - 8, barW, 8, TFT_WHITE);
|
||||||
|
_tft.fillRect(barW, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH - barW, 8, TFT_DARKGREY);
|
||||||
|
} else {
|
||||||
|
_holdActive = false;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverTFT::updateHint() {
|
||||||
|
float t = (millis() % 2000) / 2000.0f;
|
||||||
|
uint8_t v = (uint8_t)(30.0f + 30.0f * sinf(t * 2 * PI));
|
||||||
|
uint16_t col = _tft.color565(v, v, v);
|
||||||
|
_tft.drawRect(DISPLAY_WIDTH / 2 - 40, DISPLAY_HEIGHT / 2 + 20, 80, 40, col);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <KlubhausCore.h>
|
||||||
|
#include <TFT_eSPI.h>
|
||||||
|
#include "board_config.h"
|
||||||
|
|
||||||
|
class DisplayDriverTFT : public IDisplayDriver {
|
||||||
|
public:
|
||||||
|
void begin() override;
|
||||||
|
void setBacklight(bool on) override;
|
||||||
|
void render(const ScreenState& state) override;
|
||||||
|
TouchEvent readTouch() override;
|
||||||
|
int dashboardTouch(int x, int y) override;
|
||||||
|
HoldState updateHold(unsigned long holdMs) override;
|
||||||
|
void updateHint() override;
|
||||||
|
int width() override { return DISPLAY_WIDTH; }
|
||||||
|
int height() override { return DISPLAY_HEIGHT; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void drawBoot();
|
||||||
|
void drawAlert(const ScreenState& st);
|
||||||
|
void drawDashboard(const ScreenState& st);
|
||||||
|
|
||||||
|
TFT_eSPI _tft;
|
||||||
|
|
||||||
|
bool _holdActive = false;
|
||||||
|
uint32_t _holdStartMs = 0;
|
||||||
|
ScreenID _lastScreen = ScreenID::BOOT;
|
||||||
|
bool _needsRedraw = true;
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define BOARD_NAME "WS_32E"
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// TODO: Set these to match YOUR display + wiring.
|
||||||
|
// Defaults below are for a common ILI9341 320×240 SPI TFT.
|
||||||
|
// The actual pin mapping must also be set in tft_user_setup.h
|
||||||
|
// (which gets copied into the vendored TFT_eSPI library).
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
#define DISPLAY_WIDTH 320
|
||||||
|
#define DISPLAY_HEIGHT 240
|
||||||
|
#define DISPLAY_ROTATION 1 // landscape
|
||||||
|
|
||||||
|
// Backlight GPIO (directly wired)
|
||||||
|
#define PIN_LCD_BL 22
|
||||||
|
|
||||||
|
// Touch — if using XPT2046 via TFT_eSPI, set TOUCH_CS in tft_user_setup.h
|
||||||
|
// If using capacitive touch (e.g. FT6236), configure I2C pins here:
|
||||||
|
// #define TOUCH_SDA 21
|
||||||
|
// #define TOUCH_SCL 22
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// Klubhaus Doorbell — ESP32-32E target
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <KlubhausCore.h>
|
||||||
|
#include "board_config.h"
|
||||||
|
#include "secrets.h"
|
||||||
|
#include "DisplayDriverTFT.h"
|
||||||
|
|
||||||
|
DisplayDriverTFT tftDriver;
|
||||||
|
DisplayManager display(&tftDriver);
|
||||||
|
DoorbellLogic logic(&display);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(500);
|
||||||
|
|
||||||
|
logic.begin(FW_VERSION, BOARD_NAME, wifiNetworks, wifiNetworkCount);
|
||||||
|
logic.finishBoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// ── State machine tick ──
|
||||||
|
logic.update();
|
||||||
|
|
||||||
|
// ── Render current screen ──
|
||||||
|
display.render(logic.getScreenState());
|
||||||
|
|
||||||
|
// ── Touch handling ──
|
||||||
|
const ScreenState& st = logic.getScreenState();
|
||||||
|
|
||||||
|
if (st.deviceState == DeviceState::ALERTING) {
|
||||||
|
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
|
||||||
|
if (h.completed) {
|
||||||
|
logic.silenceAlert();
|
||||||
|
}
|
||||||
|
if (!h.active) {
|
||||||
|
display.updateHint();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TouchEvent evt = display.readTouch();
|
||||||
|
if (evt.pressed && st.screen == ScreenID::DASHBOARD) {
|
||||||
|
int tile = display.dashboardTouch(evt.x, evt.y);
|
||||||
|
if (tile >= 0) Serial.printf("[DASH] Tile %d tapped\n", tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Serial console ──
|
||||||
|
if (Serial.available()) {
|
||||||
|
String cmd = Serial.readStringUntil('\n');
|
||||||
|
cmd.trim();
|
||||||
|
if (cmd.length() > 0) logic.onSerialCommand(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <KlubhausCore.h>
|
||||||
|
|
||||||
|
// Copy this file to secrets.h and fill in your credentials.
|
||||||
|
// secrets.h is gitignored.
|
||||||
|
|
||||||
|
static const WiFiCred wifiNetworks[] = {
|
||||||
|
{ "Your_SSID_1", "password1" },
|
||||||
|
{ "Your_SSID_2", "password2" },
|
||||||
|
};
|
||||||
|
static const int wifiNetworkCount = sizeof(wifiNetworks) / sizeof(wifiNetworks[0]);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// TFT_eSPI User_Setup for ESP32-32E target
|
||||||
|
// This file is copied over vendor/esp32-32e/TFT_eSPI/User_Setup.h
|
||||||
|
// by the install-libs-32e task.
|
||||||
|
//
|
||||||
|
// TODO: Change the driver, pins, and dimensions to match your display.
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
#define USER_SETUP_ID 200
|
||||||
|
|
||||||
|
// ── Driver ──
|
||||||
|
#define ILI9341_DRIVER
|
||||||
|
// #define ST7789_DRIVER
|
||||||
|
// #define ILI9488_DRIVER
|
||||||
|
|
||||||
|
// ── Resolution ──
|
||||||
|
#define TFT_WIDTH 240
|
||||||
|
#define TFT_HEIGHT 320
|
||||||
|
|
||||||
|
// ── SPI Pins ──
|
||||||
|
#define TFT_MOSI 23
|
||||||
|
#define TFT_SCLK 18
|
||||||
|
#define TFT_CS 5
|
||||||
|
#define TFT_DC 27
|
||||||
|
#define TFT_RST 33
|
||||||
|
|
||||||
|
// ── Backlight (optional, can also use GPIO directly) ──
|
||||||
|
// #define TFT_BL 22
|
||||||
|
// #define TFT_BACKLIGHT_ON HIGH
|
||||||
|
|
||||||
|
// ── Touch (XPT2046 resistive) ──
|
||||||
|
#define TOUCH_CS 14
|
||||||
|
|
||||||
|
// ── SPI speed ──
|
||||||
|
#define SPI_FREQUENCY 40000000
|
||||||
|
#define SPI_READ_FREQUENCY 20000000
|
||||||
|
#define SPI_TOUCH_FREQUENCY 2500000
|
||||||
|
|
||||||
|
// ── Misc ──
|
||||||
|
#define LOAD_GLCD
|
||||||
|
#define LOAD_FONT2
|
||||||
|
#define LOAD_FONT4
|
||||||
|
#define LOAD_FONT6
|
||||||
|
#define LOAD_FONT7
|
||||||
|
#define LOAD_FONT8
|
||||||
|
#define LOAD_GFXFF
|
||||||
|
#define SMOOTH_FONT
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
#include "DisplayDriverGFX.h"
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// CH422G IO Expander
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void DisplayDriverGFX::ch422gInit() {
|
||||||
|
Wire.begin(I2C_SDA, I2C_SCL, I2C_FREQ);
|
||||||
|
|
||||||
|
// Enable OC output mode
|
||||||
|
Wire.beginTransmission(CH422G_SET_MODE >> 1);
|
||||||
|
Wire.write(0x01); // OC output enable
|
||||||
|
Wire.endTransmission();
|
||||||
|
|
||||||
|
// Deassert resets, backlight OFF initially
|
||||||
|
_exioBits = EXIO_TP_RST | EXIO_LCD_RST | EXIO_SD_CS;
|
||||||
|
ch422gWrite(_exioBits);
|
||||||
|
|
||||||
|
delay(100);
|
||||||
|
Serial.println("[IO] CH422G initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverGFX::ch422gWrite(uint8_t val) {
|
||||||
|
Wire.beginTransmission(CH422G_WRITE_OC >> 1);
|
||||||
|
Wire.write(val);
|
||||||
|
Wire.endTransmission();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverGFX::exioSet(uint8_t bit, bool on) {
|
||||||
|
if (on) _exioBits |= bit;
|
||||||
|
else _exioBits &= ~bit;
|
||||||
|
ch422gWrite(_exioBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// GT911 Touch (minimal implementation)
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void DisplayDriverGFX::gt911Init() {
|
||||||
|
// GT911 is on the same I2C bus (Wire), already started by ch422gInit.
|
||||||
|
// Reset sequence: pull TP_RST low then high (via CH422G EXIO1).
|
||||||
|
exioSet(EXIO_TP_RST, false);
|
||||||
|
delay(10);
|
||||||
|
exioSet(EXIO_TP_RST, true);
|
||||||
|
delay(50);
|
||||||
|
Serial.println("[TOUCH] GT911 initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DisplayDriverGFX::gt911Read(int& x, int& y) {
|
||||||
|
// Read status register 0x814E
|
||||||
|
Wire.beginTransmission(GT911_ADDR);
|
||||||
|
Wire.write(0x81); Wire.write(0x4E);
|
||||||
|
if (Wire.endTransmission() != 0) return false;
|
||||||
|
|
||||||
|
Wire.requestFrom((uint8_t)GT911_ADDR, (uint8_t)1);
|
||||||
|
if (!Wire.available()) return false;
|
||||||
|
uint8_t status = Wire.read();
|
||||||
|
|
||||||
|
uint8_t touches = status & 0x0F;
|
||||||
|
bool ready = status & 0x80;
|
||||||
|
|
||||||
|
if (!ready || touches == 0 || touches > 5) {
|
||||||
|
// Clear status
|
||||||
|
Wire.beginTransmission(GT911_ADDR);
|
||||||
|
Wire.write(0x81); Wire.write(0x4E); Wire.write(0x00);
|
||||||
|
Wire.endTransmission();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read first touch point (0x8150..0x8157)
|
||||||
|
Wire.beginTransmission(GT911_ADDR);
|
||||||
|
Wire.write(0x81); Wire.write(0x50);
|
||||||
|
Wire.endTransmission();
|
||||||
|
Wire.requestFrom((uint8_t)GT911_ADDR, (uint8_t)4);
|
||||||
|
if (Wire.available() >= 4) {
|
||||||
|
uint8_t xl = Wire.read();
|
||||||
|
uint8_t xh = Wire.read();
|
||||||
|
uint8_t yl = Wire.read();
|
||||||
|
uint8_t yh = Wire.read();
|
||||||
|
x = (xh << 8) | xl;
|
||||||
|
y = (yh << 8) | yl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear status
|
||||||
|
Wire.beginTransmission(GT911_ADDR);
|
||||||
|
Wire.write(0x81); Wire.write(0x4E); Wire.write(0x00);
|
||||||
|
Wire.endTransmission();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Display Init
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void DisplayDriverGFX::begin() {
|
||||||
|
// 1. IO expander first (controls resets + backlight)
|
||||||
|
ch422gInit();
|
||||||
|
|
||||||
|
// 2. Create RGB bus with corrected Waveshare timing
|
||||||
|
Arduino_ESP32RGBPanel* rgbPanel = new Arduino_ESP32RGBPanel(
|
||||||
|
LCD_DE, LCD_VSYNC, LCD_HSYNC, LCD_PCLK,
|
||||||
|
LCD_R0, LCD_R1, LCD_R2, LCD_R3, LCD_R4,
|
||||||
|
LCD_G0, LCD_G1, LCD_G2, LCD_G3, LCD_G4, LCD_G5,
|
||||||
|
LCD_B0, LCD_B1, LCD_B2, LCD_B3, LCD_B4,
|
||||||
|
// ─── Corrected timing for ST7262 / Waveshare 4.3" ───
|
||||||
|
1, // hsync_polarity
|
||||||
|
10, // hsync_front_porch
|
||||||
|
8, // hsync_pulse_width
|
||||||
|
50, // hsync_back_porch
|
||||||
|
1, // vsync_polarity
|
||||||
|
10, // vsync_front_porch
|
||||||
|
8, // vsync_pulse_width
|
||||||
|
20, // vsync_back_porch
|
||||||
|
1, // pclk_active_neg *** CRITICAL — must be 1 ***
|
||||||
|
16000000 // prefer_speed = 16 MHz PCLK
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Create display
|
||||||
|
_gfx = new Arduino_RGB_Display(
|
||||||
|
DISPLAY_WIDTH, DISPLAY_HEIGHT, rgbPanel,
|
||||||
|
DISPLAY_ROTATION,
|
||||||
|
true // auto_flush
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!_gfx->begin()) {
|
||||||
|
Serial.println("[GFX] *** Display init FAILED ***");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||||
|
|
||||||
|
// PSRAM diagnostic
|
||||||
|
Serial.printf("[MEM] Free heap: %d | Free PSRAM: %d\n",
|
||||||
|
ESP.getFreeHeap(), ESP.getFreePsram());
|
||||||
|
if (ESP.getFreePsram() == 0) {
|
||||||
|
Serial.println("[MEM] *** WARNING: PSRAM not detected! "
|
||||||
|
"Display will likely be blank. "
|
||||||
|
"Ensure PSRAM=opi in board config. ***");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Init touch
|
||||||
|
gt911Init();
|
||||||
|
|
||||||
|
// 5. Show boot screen (backlight still off)
|
||||||
|
_gfx->fillScreen(BLACK);
|
||||||
|
drawBoot();
|
||||||
|
|
||||||
|
// 6. Backlight ON
|
||||||
|
exioSet(EXIO_LCD_BL, true);
|
||||||
|
Serial.println("[GFX] Backlight ON");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverGFX::setBacklight(bool on) {
|
||||||
|
exioSet(EXIO_LCD_BL, on);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Rendering
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void DisplayDriverGFX::render(const ScreenState& st) {
|
||||||
|
if (st.screen != _lastScreen) {
|
||||||
|
_needsRedraw = true;
|
||||||
|
_lastScreen = st.screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (st.screen) {
|
||||||
|
case ScreenID::BOOT:
|
||||||
|
if (_needsRedraw) { drawBoot(); _needsRedraw = false; }
|
||||||
|
break;
|
||||||
|
case ScreenID::ALERT:
|
||||||
|
drawAlert(st); // redraws every frame (pulse animation)
|
||||||
|
break;
|
||||||
|
case ScreenID::DASHBOARD:
|
||||||
|
if (_needsRedraw) { drawDashboard(st); _needsRedraw = false; }
|
||||||
|
break;
|
||||||
|
case ScreenID::OFF:
|
||||||
|
if (_needsRedraw) { _gfx->fillScreen(BLACK); _needsRedraw = false; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverGFX::drawBoot() {
|
||||||
|
_gfx->fillScreen(BLACK);
|
||||||
|
_gfx->setTextColor(WHITE);
|
||||||
|
_gfx->setTextSize(3);
|
||||||
|
_gfx->setCursor(40, 40);
|
||||||
|
_gfx->printf("KLUBHAUS ALERT v%s", FW_VERSION);
|
||||||
|
_gfx->setTextSize(2);
|
||||||
|
_gfx->setCursor(40, 100);
|
||||||
|
_gfx->print(BOARD_NAME);
|
||||||
|
_gfx->setCursor(40, 140);
|
||||||
|
_gfx->print("Booting...");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverGFX::drawAlert(const ScreenState& st) {
|
||||||
|
// Pulsing red background
|
||||||
|
uint32_t elapsed = millis() - st.alertStartMs;
|
||||||
|
uint8_t pulse = 180 + (uint8_t)(75.0f * sinf(elapsed / 300.0f));
|
||||||
|
uint16_t bg = _gfx->color565(pulse, 0, 0);
|
||||||
|
|
||||||
|
_gfx->fillScreen(bg);
|
||||||
|
_gfx->setTextColor(WHITE);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
_gfx->setTextSize(5);
|
||||||
|
_gfx->setCursor(40, 80);
|
||||||
|
_gfx->print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT");
|
||||||
|
|
||||||
|
// Body
|
||||||
|
_gfx->setTextSize(3);
|
||||||
|
_gfx->setCursor(40, 200);
|
||||||
|
_gfx->print(st.alertBody);
|
||||||
|
|
||||||
|
// Hold hint
|
||||||
|
_gfx->setTextSize(2);
|
||||||
|
_gfx->setCursor(40, DISPLAY_HEIGHT - 60);
|
||||||
|
_gfx->print("Hold to silence...");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverGFX::drawDashboard(const ScreenState& st) {
|
||||||
|
_gfx->fillScreen(BLACK);
|
||||||
|
_gfx->setTextColor(WHITE);
|
||||||
|
|
||||||
|
// Title bar
|
||||||
|
_gfx->setTextSize(2);
|
||||||
|
_gfx->setCursor(20, 10);
|
||||||
|
_gfx->printf("KLUBHAUS — %s", deviceStateStr(st.deviceState));
|
||||||
|
|
||||||
|
// Info tiles (simple text grid)
|
||||||
|
int y = 60;
|
||||||
|
int dy = 50;
|
||||||
|
_gfx->setTextSize(2);
|
||||||
|
|
||||||
|
_gfx->setCursor(20, y);
|
||||||
|
_gfx->printf("WiFi: %s RSSI: %d", st.wifiSsid.c_str(), st.wifiRssi);
|
||||||
|
y += dy;
|
||||||
|
|
||||||
|
_gfx->setCursor(20, y);
|
||||||
|
_gfx->printf("IP: %s", st.ipAddr.c_str());
|
||||||
|
y += dy;
|
||||||
|
|
||||||
|
_gfx->setCursor(20, y);
|
||||||
|
_gfx->printf("Uptime: %lus", st.uptimeMs / 1000);
|
||||||
|
y += dy;
|
||||||
|
|
||||||
|
_gfx->setCursor(20, y);
|
||||||
|
_gfx->printf("Heap: %d PSRAM: %d", ESP.getFreeHeap(), ESP.getFreePsram());
|
||||||
|
y += dy;
|
||||||
|
|
||||||
|
_gfx->setCursor(20, y);
|
||||||
|
_gfx->printf("Last poll: %lus ago",
|
||||||
|
st.lastPollMs > 0 ? (millis() - st.lastPollMs) / 1000 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
// Touch
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
TouchEvent DisplayDriverGFX::readTouch() {
|
||||||
|
TouchEvent evt;
|
||||||
|
int x, y;
|
||||||
|
if (gt911Read(x, y)) {
|
||||||
|
evt.pressed = true;
|
||||||
|
evt.x = x;
|
||||||
|
evt.y = y;
|
||||||
|
}
|
||||||
|
return evt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int DisplayDriverGFX::dashboardTouch(int x, int y) {
|
||||||
|
// Simple 2-column, 4-row tile grid
|
||||||
|
int col = x / (DISPLAY_WIDTH / 2);
|
||||||
|
int row = (y - 60) / 80;
|
||||||
|
if (row < 0 || row > 3) return -1;
|
||||||
|
return row * 2 + col;
|
||||||
|
}
|
||||||
|
|
||||||
|
HoldState DisplayDriverGFX::updateHold(unsigned long holdMs) {
|
||||||
|
HoldState h;
|
||||||
|
TouchEvent t = readTouch();
|
||||||
|
|
||||||
|
if (t.pressed) {
|
||||||
|
if (!_holdActive) {
|
||||||
|
_holdActive = true;
|
||||||
|
_holdStartMs = millis();
|
||||||
|
}
|
||||||
|
uint32_t held = millis() - _holdStartMs;
|
||||||
|
h.active = true;
|
||||||
|
h.progress = constrain((float)held / (float)holdMs, 0.0f, 1.0f);
|
||||||
|
h.completed = (held >= holdMs);
|
||||||
|
|
||||||
|
// Draw progress arc
|
||||||
|
if (h.active && !h.completed) {
|
||||||
|
int cx = DISPLAY_WIDTH / 2;
|
||||||
|
int cy = DISPLAY_HEIGHT / 2;
|
||||||
|
int r = 100;
|
||||||
|
float angle = h.progress * 360.0f;
|
||||||
|
// Simple progress: draw filled arc sector
|
||||||
|
for (float a = 0; a < angle; a += 2.0f) {
|
||||||
|
float rad = a * PI / 180.0f;
|
||||||
|
int px = cx + (int)(r * cosf(rad - PI / 2));
|
||||||
|
int py = cy + (int)(r * sinf(rad - PI / 2));
|
||||||
|
_gfx->fillCircle(px, py, 4, WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_holdActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastTouched = t.pressed;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayDriverGFX::updateHint() {
|
||||||
|
// Subtle pulsing ring to hint "hold here"
|
||||||
|
float t = (millis() % 2000) / 2000.0f;
|
||||||
|
uint8_t alpha = (uint8_t)(60.0f + 40.0f * sinf(t * 2 * PI));
|
||||||
|
uint16_t col = _gfx->color565(alpha, alpha, alpha);
|
||||||
|
int cx = DISPLAY_WIDTH / 2;
|
||||||
|
int cy = DISPLAY_HEIGHT / 2 + 60;
|
||||||
|
_gfx->drawCircle(cx, cy, 80, col);
|
||||||
|
_gfx->drawCircle(cx, cy, 81, col);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <KlubhausCore.h>
|
||||||
|
#include <Arduino_GFX_Library.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
#include "board_config.h"
|
||||||
|
|
||||||
|
class DisplayDriverGFX : public IDisplayDriver {
|
||||||
|
public:
|
||||||
|
void begin() override;
|
||||||
|
void setBacklight(bool on) override;
|
||||||
|
void render(const ScreenState& state) override;
|
||||||
|
TouchEvent readTouch() override;
|
||||||
|
int dashboardTouch(int x, int y) override;
|
||||||
|
HoldState updateHold(unsigned long holdMs) override;
|
||||||
|
void updateHint() override;
|
||||||
|
int width() override { return DISPLAY_WIDTH; }
|
||||||
|
int height() override { return DISPLAY_HEIGHT; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// CH422G helpers
|
||||||
|
void ch422gInit();
|
||||||
|
void ch422gWrite(uint8_t val);
|
||||||
|
uint8_t _exioBits = 0;
|
||||||
|
void exioSet(uint8_t bit, bool on);
|
||||||
|
|
||||||
|
// GT911 helpers
|
||||||
|
void gt911Init();
|
||||||
|
bool gt911Read(int& x, int& y);
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
void drawBoot();
|
||||||
|
void drawAlert(const ScreenState& st);
|
||||||
|
void drawDashboard(const ScreenState& st);
|
||||||
|
|
||||||
|
Arduino_GFX* _gfx = nullptr;
|
||||||
|
|
||||||
|
// Hold-to-silence tracking
|
||||||
|
bool _holdActive = false;
|
||||||
|
uint32_t _holdStartMs = 0;
|
||||||
|
bool _lastTouched = false;
|
||||||
|
|
||||||
|
// Render-change tracking
|
||||||
|
ScreenID _lastScreen = ScreenID::BOOT;
|
||||||
|
bool _needsRedraw = true;
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define BOARD_NAME "WS_S3_43"
|
||||||
|
#define DISPLAY_WIDTH 800
|
||||||
|
#define DISPLAY_HEIGHT 480
|
||||||
|
#define DISPLAY_ROTATION 0 // landscape, USB-C on left
|
||||||
|
|
||||||
|
// ── RGB parallel bus pins (directly to ST7262 panel) ──
|
||||||
|
#define LCD_DE 40
|
||||||
|
#define LCD_VSYNC 41
|
||||||
|
#define LCD_HSYNC 39
|
||||||
|
#define LCD_PCLK 42
|
||||||
|
|
||||||
|
#define LCD_R0 45
|
||||||
|
#define LCD_R1 48
|
||||||
|
#define LCD_R2 47
|
||||||
|
#define LCD_R3 21
|
||||||
|
#define LCD_R4 14
|
||||||
|
|
||||||
|
#define LCD_G0 5
|
||||||
|
#define LCD_G1 6
|
||||||
|
#define LCD_G2 7
|
||||||
|
#define LCD_G3 15
|
||||||
|
#define LCD_G4 16
|
||||||
|
#define LCD_G5 4
|
||||||
|
|
||||||
|
#define LCD_B0 8
|
||||||
|
#define LCD_B1 3
|
||||||
|
#define LCD_B2 46
|
||||||
|
#define LCD_B3 9
|
||||||
|
#define LCD_B4 1
|
||||||
|
|
||||||
|
// ── CH422G I2C IO expander ──
|
||||||
|
// Controls LCD_RST, TP_RST, LCD_BL, SD_CS via I2C
|
||||||
|
#define I2C_SDA 17
|
||||||
|
#define I2C_SCL 18
|
||||||
|
#define I2C_FREQ 100000
|
||||||
|
|
||||||
|
// CH422G I2C command addresses
|
||||||
|
#define CH422G_WRITE_OC 0x46
|
||||||
|
#define CH422G_SET_MODE 0x48
|
||||||
|
#define CH422G_READ_IN 0x4C
|
||||||
|
|
||||||
|
// EXIO bit positions
|
||||||
|
#define EXIO_TP_RST (1 << 0) // EXIO1
|
||||||
|
#define EXIO_LCD_BL (1 << 1) // EXIO2 — also drives DISP signal!
|
||||||
|
#define EXIO_LCD_RST (1 << 2) // EXIO3
|
||||||
|
#define EXIO_SD_CS (1 << 3) // EXIO4
|
||||||
|
|
||||||
|
// ── GT911 Touch ──
|
||||||
|
#define GT911_ADDR 0x5D
|
||||||
|
#define TOUCH_INT -1 // not wired to a readable GPIO on this board
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// Klubhaus Doorbell — ESP32-S3-Touch-LCD-4.3 target
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <KlubhausCore.h>
|
||||||
|
#include "board_config.h"
|
||||||
|
#include "secrets.h"
|
||||||
|
#include "DisplayDriverGFX.h"
|
||||||
|
|
||||||
|
DisplayDriverGFX gfxDriver;
|
||||||
|
DisplayManager display(&gfxDriver);
|
||||||
|
DoorbellLogic logic(&display);
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(500);
|
||||||
|
|
||||||
|
logic.begin(FW_VERSION, BOARD_NAME, wifiNetworks, wifiNetworkCount);
|
||||||
|
logic.finishBoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// ── State machine tick ──
|
||||||
|
logic.update();
|
||||||
|
|
||||||
|
// ── Render current screen ──
|
||||||
|
display.render(logic.getScreenState());
|
||||||
|
|
||||||
|
// ── Touch handling ──
|
||||||
|
const ScreenState& st = logic.getScreenState();
|
||||||
|
|
||||||
|
if (st.deviceState == DeviceState::ALERTING) {
|
||||||
|
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
|
||||||
|
if (h.completed) {
|
||||||
|
logic.silenceAlert();
|
||||||
|
}
|
||||||
|
if (!h.active) {
|
||||||
|
display.updateHint();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TouchEvent evt = display.readTouch();
|
||||||
|
if (evt.pressed && st.screen == ScreenID::DASHBOARD) {
|
||||||
|
int tile = display.dashboardTouch(evt.x, evt.y);
|
||||||
|
if (tile >= 0) Serial.printf("[DASH] Tile %d tapped\n", tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Serial console ──
|
||||||
|
if (Serial.available()) {
|
||||||
|
String cmd = Serial.readStringUntil('\n');
|
||||||
|
cmd.trim();
|
||||||
|
if (cmd.length() > 0) logic.onSerialCommand(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <KlubhausCore.h>
|
||||||
|
|
||||||
|
// Copy this file to secrets.h and fill in your credentials.
|
||||||
|
// secrets.h is gitignored.
|
||||||
|
|
||||||
|
static const WiFiCred wifiNetworks[] = {
|
||||||
|
{ "Your_SSID_1", "password1" },
|
||||||
|
{ "Your_SSID_2", "password2" },
|
||||||
|
};
|
||||||
|
static const int wifiNetworkCount = sizeof(wifiNetworks) / sizeof(wifiNetworks[0]);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name=KlubhausCore
|
||||||
|
version=5.1.0
|
||||||
|
author=David
|
||||||
|
maintainer=David
|
||||||
|
sentence=Core logic for Klubhaus doorbell alert system
|
||||||
|
paragraph=Shared state machine, ntfy.sh polling, networking, and abstract display interface.
|
||||||
|
category=Other
|
||||||
|
url=https://git.notsosm.art/david/klubhaus-doorbell
|
||||||
|
architectures=esp32
|
||||||
|
includes=KlubhausCore.h
|
||||||
|
depends=ArduinoJson,NTPClient
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define FW_VERSION "5.1"
|
||||||
|
|
||||||
|
// ── ntfy.sh ──
|
||||||
|
#define NTFY_SERVER "ntfy.sh"
|
||||||
|
#define ALERT_TOPIC "ALERT_klubhaus_topic"
|
||||||
|
#define SILENCE_TOPIC "SILENCE_klubhaus_topic"
|
||||||
|
#define ADMIN_TOPIC "ADMIN_klubhaus_topic"
|
||||||
|
#define STATUS_TOPIC "STATUS_klubhaus_topic"
|
||||||
|
|
||||||
|
// ── Timing ──
|
||||||
|
#define POLL_INTERVAL_MS 15000
|
||||||
|
#define HEARTBEAT_INTERVAL_MS 300000
|
||||||
|
#define BOOT_GRACE_MS 5000
|
||||||
|
#define HOLD_TO_SILENCE_MS 3000
|
||||||
|
#define ALERT_TIMEOUT_MS 120000
|
||||||
|
#define SILENCE_DISPLAY_MS 10000
|
||||||
|
#define WIFI_CONNECT_TIMEOUT_MS 15000
|
||||||
|
#define HTTP_TIMEOUT_MS 10000
|
||||||
|
|
||||||
|
// ── WiFi credential struct (populated in each board's secrets.h) ──
|
||||||
|
struct WiFiCred {
|
||||||
|
const char* ssid;
|
||||||
|
const char* pass;
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "IDisplayDriver.h"
|
||||||
|
|
||||||
|
/// Owns a pointer to the concrete driver; all calls delegate.
|
||||||
|
/// Board sketch creates the concrete driver and passes it in.
|
||||||
|
class DisplayManager {
|
||||||
|
public:
|
||||||
|
DisplayManager() : _drv(nullptr) {}
|
||||||
|
explicit DisplayManager(IDisplayDriver* drv) : _drv(drv) {}
|
||||||
|
void setDriver(IDisplayDriver* drv) { _drv = drv; }
|
||||||
|
|
||||||
|
void begin() { if (_drv) _drv->begin(); }
|
||||||
|
void setBacklight(bool on) { if (_drv) _drv->setBacklight(on); }
|
||||||
|
void render(const ScreenState& st) { if (_drv) _drv->render(st); }
|
||||||
|
TouchEvent readTouch() { return _drv ? _drv->readTouch() : TouchEvent{}; }
|
||||||
|
int dashboardTouch(int x, int y) { return _drv ? _drv->dashboardTouch(x, y) : -1; }
|
||||||
|
HoldState updateHold(unsigned long ms) { return _drv ? _drv->updateHold(ms) : HoldState{}; }
|
||||||
|
void updateHint() { if (_drv) _drv->updateHint(); }
|
||||||
|
int width() { return _drv ? _drv->width() : 0; }
|
||||||
|
int height() { return _drv ? _drv->height() : 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
IDisplayDriver* _drv;
|
||||||
|
};
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
#include "DoorbellLogic.h"
|
||||||
|
|
||||||
|
DoorbellLogic::DoorbellLogic(DisplayManager* display)
|
||||||
|
: _display(display) {}
|
||||||
|
|
||||||
|
// ── URL builder ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
String DoorbellLogic::topicUrl(const char* base) {
|
||||||
|
String suffix = _debug ? "_test" : "";
|
||||||
|
return String("https://") + NTFY_SERVER + "/" + base + suffix
|
||||||
|
+ "/json?since=20s&poll=1";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
void DoorbellLogic::begin(const char* version, const char* boardName,
|
||||||
|
const WiFiCred* creds, int credCount) {
|
||||||
|
_version = version;
|
||||||
|
_board = boardName;
|
||||||
|
#ifdef DEBUG_MODE
|
||||||
|
_debug = true;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Serial.println(F("========================================"));
|
||||||
|
Serial.printf( " KLUBHAUS ALERT v%s — %s\n", _version, _board);
|
||||||
|
if (_debug) Serial.println(F(" *** DEBUG MODE — _test topics ***"));
|
||||||
|
Serial.println(F("========================================\n"));
|
||||||
|
|
||||||
|
// Display
|
||||||
|
_display->begin();
|
||||||
|
|
||||||
|
// Network
|
||||||
|
_net.begin(creds, credCount);
|
||||||
|
|
||||||
|
if (_net.isConnected()) {
|
||||||
|
_net.syncNTP();
|
||||||
|
Serial.printf("[NET] WiFi:%s RSSI:%d IP:%s\n",
|
||||||
|
_net.getSSID().c_str(), _net.getRSSI(),
|
||||||
|
_net.getIP().c_str());
|
||||||
|
_net.dnsCheck(NTFY_SERVER);
|
||||||
|
_net.tlsCheck(NTFY_SERVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Topic URLs
|
||||||
|
_alertUrl = topicUrl(ALERT_TOPIC);
|
||||||
|
_silenceUrl = topicUrl(SILENCE_TOPIC);
|
||||||
|
_adminUrl = topicUrl(ADMIN_TOPIC);
|
||||||
|
String sfx = _debug ? "_test" : "";
|
||||||
|
_statusUrl = String("https://") + NTFY_SERVER + "/" + STATUS_TOPIC + sfx;
|
||||||
|
|
||||||
|
Serial.printf("[CONFIG] ALERT_URL: %s\n", _alertUrl.c_str());
|
||||||
|
Serial.printf("[CONFIG] SILENCE_URL: %s\n", _silenceUrl.c_str());
|
||||||
|
Serial.printf("[CONFIG] ADMIN_URL: %s\n", _adminUrl.c_str());
|
||||||
|
|
||||||
|
// Boot status
|
||||||
|
flushStatus(String("BOOTED — ") + _net.getSSID() + " "
|
||||||
|
+ _net.getIP() + " RSSI:" + String(_net.getRSSI()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::finishBoot() {
|
||||||
|
transition(DeviceState::SILENT);
|
||||||
|
_state.screen = ScreenID::OFF;
|
||||||
|
_display->setBacklight(false);
|
||||||
|
_state.backlightOn = false;
|
||||||
|
_bootGraceEnd = millis() + BOOT_GRACE_MS;
|
||||||
|
Serial.printf("[BOOT] Grace period: %d ms\n", BOOT_GRACE_MS);
|
||||||
|
Serial.printf("[BOOT] Ready — monitoring %s\n", NTFY_SERVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main loop tick ──────────────────────────────────────────
|
||||||
|
|
||||||
|
void DoorbellLogic::update() {
|
||||||
|
uint32_t now = millis();
|
||||||
|
_state.uptimeMs = now;
|
||||||
|
|
||||||
|
if (_net.isConnected()) {
|
||||||
|
_state.wifiRssi = _net.getRSSI();
|
||||||
|
_state.wifiSsid = _net.getSSID();
|
||||||
|
_state.ipAddr = _net.getIP();
|
||||||
|
}
|
||||||
|
if (!_net.checkConnection()) return;
|
||||||
|
|
||||||
|
// Poll
|
||||||
|
if (now - _lastPollMs >= POLL_INTERVAL_MS) {
|
||||||
|
_lastPollMs = now;
|
||||||
|
pollTopics();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat
|
||||||
|
if (now - _lastHeartbeatMs >= HEARTBEAT_INTERVAL_MS) {
|
||||||
|
_lastHeartbeatMs = now;
|
||||||
|
heartbeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-transitions
|
||||||
|
switch (_state.deviceState) {
|
||||||
|
case DeviceState::ALERTING:
|
||||||
|
if (now - _state.alertStartMs > ALERT_TIMEOUT_MS) {
|
||||||
|
Serial.println("[STATE] Alert timed out → SILENT");
|
||||||
|
transition(DeviceState::SILENT);
|
||||||
|
_state.screen = ScreenID::OFF;
|
||||||
|
_display->setBacklight(false);
|
||||||
|
_state.backlightOn = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DeviceState::SILENCED:
|
||||||
|
if (now - _state.silenceStartMs > SILENCE_DISPLAY_MS) {
|
||||||
|
Serial.println("[STATE] Silence display done → SILENT");
|
||||||
|
transition(DeviceState::SILENT);
|
||||||
|
_state.screen = ScreenID::OFF;
|
||||||
|
_display->setBacklight(false);
|
||||||
|
_state.backlightOn = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Polling ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void DoorbellLogic::pollTopics() {
|
||||||
|
pollTopic(_alertUrl, "ALERT");
|
||||||
|
pollTopic(_silenceUrl, "SILENCE");
|
||||||
|
pollTopic(_adminUrl, "ADMIN");
|
||||||
|
_state.lastPollMs = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::pollTopic(const String& url, const char* label) {
|
||||||
|
String body;
|
||||||
|
int code = _net.httpGet(url.c_str(), body);
|
||||||
|
if (code != 200 || body.length() == 0) return;
|
||||||
|
|
||||||
|
// ntfy returns newline-delimited JSON
|
||||||
|
int pos = 0;
|
||||||
|
while (pos < (int)body.length()) {
|
||||||
|
int nl = body.indexOf('\n', pos);
|
||||||
|
if (nl < 0) nl = body.length();
|
||||||
|
String line = body.substring(pos, nl);
|
||||||
|
line.trim();
|
||||||
|
pos = nl + 1;
|
||||||
|
if (line.length() == 0) continue;
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
if (deserializeJson(doc, line)) continue;
|
||||||
|
|
||||||
|
const char* evt = doc["event"] | "";
|
||||||
|
if (strcmp(evt, "message") != 0) continue;
|
||||||
|
|
||||||
|
const char* title = doc["title"] | "";
|
||||||
|
const char* message = doc["message"] | "";
|
||||||
|
Serial.printf("[%s] title=\"%s\" message=\"%s\"\n",
|
||||||
|
label, title, message);
|
||||||
|
|
||||||
|
if (strcmp(label, "ALERT") == 0) onAlert(String(title), String(message));
|
||||||
|
else if (strcmp(label, "SILENCE") == 0) onSilence();
|
||||||
|
else if (strcmp(label, "ADMIN") == 0) onAdmin(String(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Event handlers ──────────────────────────────────────────
|
||||||
|
|
||||||
|
void DoorbellLogic::onAlert(const String& title, const String& body) {
|
||||||
|
if (millis() < _bootGraceEnd) {
|
||||||
|
Serial.println("[ALERT] Ignored (boot grace)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Serial.printf("[ALERT] %s: %s\n", title.c_str(), body.c_str());
|
||||||
|
_state.alertTitle = title;
|
||||||
|
_state.alertBody = body;
|
||||||
|
_state.alertStartMs = millis();
|
||||||
|
transition(DeviceState::ALERTING);
|
||||||
|
_state.screen = ScreenID::ALERT;
|
||||||
|
_display->setBacklight(true);
|
||||||
|
_state.backlightOn = true;
|
||||||
|
flushStatus("ALERT: " + title);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::onSilence() {
|
||||||
|
if (_state.deviceState != DeviceState::ALERTING) return;
|
||||||
|
Serial.println("[SILENCE] Alert silenced");
|
||||||
|
_state.silenceStartMs = millis();
|
||||||
|
transition(DeviceState::SILENCED);
|
||||||
|
_state.screen = ScreenID::OFF;
|
||||||
|
_display->setBacklight(false);
|
||||||
|
_state.backlightOn = false;
|
||||||
|
flushStatus("SILENCED");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::silenceAlert() { onSilence(); }
|
||||||
|
|
||||||
|
void DoorbellLogic::onAdmin(const String& cmd) {
|
||||||
|
Serial.printf("[ADMIN] %s\n", cmd.c_str());
|
||||||
|
if (cmd == "reboot") {
|
||||||
|
flushStatus("REBOOTING (admin)");
|
||||||
|
delay(500);
|
||||||
|
ESP.restart();
|
||||||
|
} else if (cmd == "dashboard") {
|
||||||
|
_state.screen = ScreenID::DASHBOARD;
|
||||||
|
_display->setBacklight(true);
|
||||||
|
_state.backlightOn = true;
|
||||||
|
} else if (cmd == "off") {
|
||||||
|
_state.screen = ScreenID::OFF;
|
||||||
|
_display->setBacklight(false);
|
||||||
|
_state.backlightOn = false;
|
||||||
|
} else if (cmd == "status") {
|
||||||
|
heartbeat(); // re-uses heartbeat message format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Status / heartbeat ─────────────────────────────────────
|
||||||
|
|
||||||
|
void DoorbellLogic::flushStatus(const String& message) {
|
||||||
|
Serial.printf("[STATUS] Queued: %s\n", message.c_str());
|
||||||
|
String full = String(_board) + " " + message;
|
||||||
|
int code = _net.httpPost(_statusUrl.c_str(), full);
|
||||||
|
Serial.printf("[STATUS] Sent (%d): %s\n", code, message.c_str());
|
||||||
|
_state.lastHeartbeatMs = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::heartbeat() {
|
||||||
|
String m = String("HEARTBEAT ") + deviceStateStr(_state.deviceState)
|
||||||
|
+ " up:" + String(millis() / 1000) + "s"
|
||||||
|
+ " RSSI:" + String(_net.getRSSI())
|
||||||
|
+ " heap:" + String(ESP.getFreeHeap());
|
||||||
|
#ifdef BOARD_HAS_PSRAM
|
||||||
|
m += " psram:" + String(ESP.getFreePsram());
|
||||||
|
#endif
|
||||||
|
flushStatus(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::transition(DeviceState s) {
|
||||||
|
Serial.printf("-> %s\n", deviceStateStr(s));
|
||||||
|
_state.deviceState = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Serial console ──────────────────────────────────────────
|
||||||
|
|
||||||
|
void DoorbellLogic::onSerialCommand(const String& cmd) {
|
||||||
|
Serial.printf("[CMD] %s\n", cmd.c_str());
|
||||||
|
if (cmd == "alert") onAlert("Test Alert", "Serial test");
|
||||||
|
else if (cmd == "silence") onSilence();
|
||||||
|
else if (cmd == "reboot") ESP.restart();
|
||||||
|
else if (cmd == "dashboard") onAdmin("dashboard");
|
||||||
|
else if (cmd == "off") onAdmin("off");
|
||||||
|
else if (cmd == "status") {
|
||||||
|
Serial.printf("[STATE] %s screen:%s bl:%s\n",
|
||||||
|
deviceStateStr(_state.deviceState),
|
||||||
|
screenIdStr(_state.screen),
|
||||||
|
_state.backlightOn ? "ON" : "OFF");
|
||||||
|
Serial.printf("[MEM] heap:%d", ESP.getFreeHeap());
|
||||||
|
#ifdef BOARD_HAS_PSRAM
|
||||||
|
Serial.printf(" psram:%d", ESP.getFreePsram());
|
||||||
|
#endif
|
||||||
|
Serial.println();
|
||||||
|
Serial.printf("[NET] %s RSSI:%d IP:%s\n",
|
||||||
|
_state.wifiSsid.c_str(), _state.wifiRssi,
|
||||||
|
_state.ipAddr.c_str());
|
||||||
|
}
|
||||||
|
else Serial.println(F("[CMD] alert|silence|reboot|dashboard|off|status"));
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include "Config.h"
|
||||||
|
#include "ScreenState.h"
|
||||||
|
#include "DisplayManager.h"
|
||||||
|
#include "NetManager.h"
|
||||||
|
|
||||||
|
class DoorbellLogic {
|
||||||
|
public:
|
||||||
|
explicit DoorbellLogic(DisplayManager* display);
|
||||||
|
|
||||||
|
/// Call from setup(). Pass board-specific WiFi creds.
|
||||||
|
void begin(const char* version, const char* boardName,
|
||||||
|
const WiFiCred* creds, int credCount);
|
||||||
|
/// Call from loop() — polls topics, runs timers, transitions state.
|
||||||
|
void update();
|
||||||
|
/// Transition out of BOOTED → SILENT. Call at end of setup().
|
||||||
|
void finishBoot();
|
||||||
|
/// Serial debug console.
|
||||||
|
void onSerialCommand(const String& cmd);
|
||||||
|
|
||||||
|
const ScreenState& getScreenState() const { return _state; }
|
||||||
|
|
||||||
|
/// Externally trigger silence (e.g. hold-to-silence gesture).
|
||||||
|
void silenceAlert();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void pollTopics();
|
||||||
|
void pollTopic(const String& url, const char* label);
|
||||||
|
void onAlert(const String& title, const String& body);
|
||||||
|
void onSilence();
|
||||||
|
void onAdmin(const String& command);
|
||||||
|
void flushStatus(const String& message);
|
||||||
|
void heartbeat();
|
||||||
|
void transition(DeviceState s);
|
||||||
|
String topicUrl(const char* base);
|
||||||
|
|
||||||
|
DisplayManager* _display;
|
||||||
|
NetManager _net;
|
||||||
|
ScreenState _state;
|
||||||
|
|
||||||
|
const char* _version = "";
|
||||||
|
const char* _board = "";
|
||||||
|
bool _debug = false;
|
||||||
|
|
||||||
|
uint32_t _lastPollMs = 0;
|
||||||
|
uint32_t _lastHeartbeatMs = 0;
|
||||||
|
uint32_t _bootGraceEnd = 0;
|
||||||
|
|
||||||
|
String _alertUrl;
|
||||||
|
String _silenceUrl;
|
||||||
|
String _adminUrl;
|
||||||
|
String _statusUrl;
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "ScreenState.h"
|
||||||
|
|
||||||
|
struct TouchEvent {
|
||||||
|
bool pressed = false;
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HoldState {
|
||||||
|
bool active = false;
|
||||||
|
bool completed = false;
|
||||||
|
float progress = 0.0f; // 0.0 – 1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Abstract display driver — implemented per-board.
|
||||||
|
class IDisplayDriver {
|
||||||
|
public:
|
||||||
|
virtual ~IDisplayDriver() = default;
|
||||||
|
|
||||||
|
virtual void begin() = 0;
|
||||||
|
virtual void setBacklight(bool on) = 0;
|
||||||
|
|
||||||
|
/// Called every loop() iteration; draw the screen described by `state`.
|
||||||
|
virtual void render(const ScreenState& state) = 0;
|
||||||
|
|
||||||
|
// ── Touch ──
|
||||||
|
virtual TouchEvent readTouch() = 0;
|
||||||
|
/// Returns tile index at (x,y), or -1 if none.
|
||||||
|
virtual int dashboardTouch(int x, int y) = 0;
|
||||||
|
/// Track a long-press gesture; returns progress/completion.
|
||||||
|
virtual HoldState updateHold(unsigned long holdMs) = 0;
|
||||||
|
/// Idle hint animation (e.g. pulsing ring) while alert is showing.
|
||||||
|
virtual void updateHint() = 0;
|
||||||
|
|
||||||
|
virtual int width() = 0;
|
||||||
|
virtual int height() = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Umbrella header — board sketches just #include <KlubhausCore.h>
|
||||||
|
#include "Config.h"
|
||||||
|
#include "ScreenState.h"
|
||||||
|
#include "IDisplayDriver.h"
|
||||||
|
#include "DisplayManager.h"
|
||||||
|
#include "NetManager.h"
|
||||||
|
#include "DoorbellLogic.h"
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#include "NetManager.h"
|
||||||
|
|
||||||
|
// ── WiFi ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void NetManager::begin(const WiFiCred* creds, int count) {
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
_multi.addAP(creds[i].ssid, creds[i].pass);
|
||||||
|
|
||||||
|
Serial.println("[WIFI] Connecting...");
|
||||||
|
unsigned long t0 = millis();
|
||||||
|
while (_multi.run() != WL_CONNECTED) {
|
||||||
|
if (millis() - t0 > WIFI_CONNECT_TIMEOUT_MS) {
|
||||||
|
Serial.println("[WIFI] Timeout!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delay(250);
|
||||||
|
}
|
||||||
|
Serial.printf("[WIFI] Connected: %s %s\n",
|
||||||
|
getSSID().c_str(), getIP().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetManager::checkConnection() {
|
||||||
|
if (WiFi.status() == WL_CONNECTED) return true;
|
||||||
|
Serial.println("[WIFI] Reconnecting...");
|
||||||
|
return _multi.run(WIFI_CONNECT_TIMEOUT_MS) == WL_CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetManager::isConnected() { return WiFi.status() == WL_CONNECTED; }
|
||||||
|
String NetManager::getSSID() { return WiFi.SSID(); }
|
||||||
|
String NetManager::getIP() { return WiFi.localIP().toString(); }
|
||||||
|
int NetManager::getRSSI() { return WiFi.RSSI(); }
|
||||||
|
|
||||||
|
// ── NTP ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
bool NetManager::syncNTP() {
|
||||||
|
Serial.println("[NTP] Starting sync...");
|
||||||
|
if (!_ntp) _ntp = new NTPClient(_udp, "pool.ntp.org", 0, 60000);
|
||||||
|
_ntp->begin();
|
||||||
|
_ntp->forceUpdate();
|
||||||
|
_ntpReady = _ntp->isTimeSet();
|
||||||
|
Serial.printf("[NTP] %s: %s UTC\n",
|
||||||
|
_ntpReady ? "Synced" : "FAILED",
|
||||||
|
_ntpReady ? _ntp->getFormattedTime().c_str() : "--");
|
||||||
|
return _ntpReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
String NetManager::getTimeStr() {
|
||||||
|
return (_ntp && _ntpReady) ? _ntp->getFormattedTime() : "??:??:??";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Diagnostics ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
bool NetManager::dnsCheck(const char* host) {
|
||||||
|
IPAddress ip;
|
||||||
|
bool ok = WiFi.hostByName(host, ip);
|
||||||
|
Serial.printf("[NET] DNS %s: %s\n", ok ? "OK" : "FAIL",
|
||||||
|
ok ? ip.toString().c_str() : "");
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetManager::tlsCheck(const char* host) {
|
||||||
|
WiFiClientSecure c;
|
||||||
|
c.setInsecure();
|
||||||
|
c.setTimeout(HTTP_TIMEOUT_MS);
|
||||||
|
bool ok = c.connect(host, 443);
|
||||||
|
if (ok) c.stop();
|
||||||
|
Serial.printf("[NET] TLS %s\n", ok ? "OK" : "FAIL");
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HTTP helpers ────────────────────────────────────────────
|
||||||
|
|
||||||
|
int NetManager::httpGet(const char* url, String& response) {
|
||||||
|
WiFiClientSecure client;
|
||||||
|
client.setInsecure();
|
||||||
|
client.setTimeout(HTTP_TIMEOUT_MS);
|
||||||
|
|
||||||
|
HTTPClient http;
|
||||||
|
http.setTimeout(HTTP_TIMEOUT_MS);
|
||||||
|
http.begin(client, url);
|
||||||
|
|
||||||
|
int code = http.GET();
|
||||||
|
if (code > 0) response = http.getString();
|
||||||
|
http.end();
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
int NetManager::httpPost(const char* url, const String& body) {
|
||||||
|
WiFiClientSecure client;
|
||||||
|
client.setInsecure();
|
||||||
|
client.setTimeout(HTTP_TIMEOUT_MS);
|
||||||
|
|
||||||
|
HTTPClient http;
|
||||||
|
http.setTimeout(HTTP_TIMEOUT_MS);
|
||||||
|
http.begin(client, url);
|
||||||
|
http.addHeader("Content-Type", "text/plain");
|
||||||
|
|
||||||
|
int code = http.POST(body);
|
||||||
|
http.end();
|
||||||
|
return code;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WiFiMulti.h>
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <NTPClient.h>
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include "Config.h"
|
||||||
|
|
||||||
|
class NetManager {
|
||||||
|
public:
|
||||||
|
void begin(const WiFiCred* creds, int count);
|
||||||
|
bool checkConnection();
|
||||||
|
bool isConnected();
|
||||||
|
|
||||||
|
String getSSID();
|
||||||
|
String getIP();
|
||||||
|
int getRSSI();
|
||||||
|
|
||||||
|
bool syncNTP();
|
||||||
|
String getTimeStr();
|
||||||
|
|
||||||
|
bool dnsCheck(const char* host);
|
||||||
|
bool tlsCheck(const char* host);
|
||||||
|
|
||||||
|
/// HTTPS GET; writes body into `response`. Returns HTTP status code.
|
||||||
|
int httpGet(const char* url, String& response);
|
||||||
|
/// HTTPS POST with text/plain body. Returns HTTP status code.
|
||||||
|
int httpPost(const char* url, const String& body);
|
||||||
|
|
||||||
|
private:
|
||||||
|
WiFiMulti _multi;
|
||||||
|
WiFiUDP _udp;
|
||||||
|
NTPClient* _ntp = nullptr;
|
||||||
|
bool _ntpReady = false;
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
enum class DeviceState {
|
||||||
|
BOOTED,
|
||||||
|
SILENT,
|
||||||
|
ALERTING,
|
||||||
|
SILENCED
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ScreenID {
|
||||||
|
BOOT,
|
||||||
|
OFF,
|
||||||
|
ALERT,
|
||||||
|
DASHBOARD
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScreenState {
|
||||||
|
DeviceState deviceState = DeviceState::BOOTED;
|
||||||
|
ScreenID screen = ScreenID::BOOT;
|
||||||
|
|
||||||
|
String alertTitle;
|
||||||
|
String alertBody;
|
||||||
|
uint32_t alertStartMs = 0;
|
||||||
|
uint32_t silenceStartMs = 0;
|
||||||
|
|
||||||
|
bool backlightOn = false;
|
||||||
|
int wifiRssi = 0;
|
||||||
|
String wifiSsid;
|
||||||
|
String ipAddr;
|
||||||
|
uint32_t uptimeMs = 0;
|
||||||
|
uint32_t lastPollMs = 0;
|
||||||
|
uint32_t lastHeartbeatMs= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const char* deviceStateStr(DeviceState s) {
|
||||||
|
switch (s) {
|
||||||
|
case DeviceState::BOOTED: return "BOOTED";
|
||||||
|
case DeviceState::SILENT: return "SILENT";
|
||||||
|
case DeviceState::ALERTING: return "ALERTING";
|
||||||
|
case DeviceState::SILENCED: return "SILENCED";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const char* screenIdStr(ScreenID s) {
|
||||||
|
switch (s) {
|
||||||
|
case ScreenID::BOOT: return "BOOT";
|
||||||
|
case ScreenID::OFF: return "OFF";
|
||||||
|
case ScreenID::ALERT: return "ALERT";
|
||||||
|
case ScreenID::DASHBOARD: return "DASHBOARD";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
@@ -1,112 +1,115 @@
|
|||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# Klubhaus Doorbell — Multi-Target Build Harness
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
[tools]
|
[tasks.install-libs-shared]
|
||||||
arduino-cli = "latest"
|
description = "Install shared (platform-independent) libraries"
|
||||||
lazygit = "latest"
|
run = """
|
||||||
python = "latest"
|
arduino-cli lib install "ArduinoJson@7.4.1"
|
||||||
ruby = "latest"
|
arduino-cli lib install "NTPClient@3.2.1"
|
||||||
|
echo "[OK] Shared libraries installed"
|
||||||
|
"""
|
||||||
|
|
||||||
[tasks.install-core]
|
[tasks.install-libs-32e]
|
||||||
run = "arduino-cli core update-index && arduino-cli core install esp32:esp32"
|
description = "Vendor TFT_eSPI into vendor/esp32-32e"
|
||||||
|
run = """
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [ ! -d "vendor/esp32-32e/TFT_eSPI" ]; then
|
||||||
|
echo "Cloning TFT_eSPI..."
|
||||||
|
git clone --depth 1 --branch 2.5.43 \
|
||||||
|
https://github.com/Bodmer/TFT_eSPI.git \
|
||||||
|
vendor/esp32-32e/TFT_eSPI
|
||||||
|
fi
|
||||||
|
echo "Copying board-specific User_Setup.h..."
|
||||||
|
cp boards/esp32-32e/tft_user_setup.h vendor/esp32-32e/TFT_eSPI/User_Setup.h
|
||||||
|
echo "[OK] TFT_eSPI 2.5.43 vendored + configured"
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.install-libs-s3-43]
|
||||||
|
description = "Vendor Arduino_GFX into vendor/esp32-s3-lcd-43"
|
||||||
|
run = """
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [ ! -d "vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino" ]; then
|
||||||
|
echo "Cloning Arduino_GFX..."
|
||||||
|
git clone --depth 1 --branch v1.6.5 \
|
||||||
|
https://github.com/moononournation/Arduino_GFX.git \
|
||||||
|
vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino
|
||||||
|
fi
|
||||||
|
echo "[OK] Arduino_GFX 1.6.5 vendored"
|
||||||
|
"""
|
||||||
|
|
||||||
[tasks.install-libs]
|
[tasks.install-libs]
|
||||||
|
description = "Install all libraries (shared + vendored)"
|
||||||
|
depends = ["install-libs-shared", "install-libs-32e", "install-libs-s3-43"]
|
||||||
|
|
||||||
|
# ── ESP32-32E ────────────────────────────────────────────
|
||||||
|
|
||||||
|
[tasks.compile-32e]
|
||||||
|
description = "Compile ESP32-32E sketch"
|
||||||
|
depends = ["install-libs"]
|
||||||
run = """
|
run = """
|
||||||
#!/usr/bin/env bash
|
arduino-cli compile \
|
||||||
set -euo pipefail
|
--fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \
|
||||||
arduino-cli lib install "ArduinoJson"
|
--libraries ./libraries \
|
||||||
arduino-cli lib install "NTPClient"
|
--libraries ./vendor/esp32-32e \
|
||||||
case "${BOARD_TARGET:-e32r35t}" in
|
--build-property "compiler.cpp.extra_flags=-DDEBUG_MODE" \
|
||||||
e32r35t)
|
--warnings default \
|
||||||
arduino-cli lib install "TFT_eSPI"
|
./boards/esp32-32e
|
||||||
;;
|
|
||||||
waveshare_s3)
|
|
||||||
arduino-cli lib install "GFX Library for Arduino"
|
|
||||||
# Uncomment when enabling GT911 touch:
|
|
||||||
# arduino-cli lib install "TAMC_GT911"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "ERROR: Unknown BOARD_TARGET: $BOARD_TARGET"
|
|
||||||
echo "Valid targets: e32r35t, waveshare_s3"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
echo "Libraries installed for target: ${BOARD_TARGET:-e32r35t}"
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[tasks.compile]
|
[tasks.upload-32e]
|
||||||
|
description = "Upload to ESP32-32E"
|
||||||
run = """
|
run = """
|
||||||
#!/usr/bin/env bash
|
arduino-cli upload \
|
||||||
set -euo pipefail
|
--fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \
|
||||||
case "${BOARD_TARGET:-e32r35t}" in
|
--port "${PORT:-/dev/ttyUSB0}" \
|
||||||
e32r35t)
|
./boards/esp32-32e
|
||||||
FQBN="esp32:esp32:esp32:UploadSpeed=921600,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=default,DebugLevel=info,EraseFlash=none"
|
|
||||||
BUILD_FLAGS="-DTARGET_E32R35T"
|
|
||||||
;;
|
|
||||||
waveshare_s3)
|
|
||||||
FQBN="esp32:esp32:waveshare_esp32_s3_touch_lcd_43:UploadSpeed=921600,USBMode=hwcdc,CDCOnBoot=cdc,CPUFreq=240,FlashMode=qio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,DebugLevel=info,PSRAM=enabled,LoopCore=1,EventsCore=1,EraseFlash=none"
|
|
||||||
BUILD_FLAGS="-DTARGET_WAVESHARE_S3_43"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "ERROR: Unknown BOARD_TARGET: $BOARD_TARGET"
|
|
||||||
echo "Valid targets: e32r35t, waveshare_s3"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
echo "╔══════════════════════════════════════════════════╗"
|
|
||||||
echo "║ Building: ${BOARD_TARGET:-e32r35t}"
|
|
||||||
echo "║ FQBN: $FQBN"
|
|
||||||
echo "║ Flags: $BUILD_FLAGS"
|
|
||||||
echo "╚══════════════════════════════════════════════════╝"
|
|
||||||
arduino-cli compile --fqbn "$FQBN" \
|
|
||||||
--build-property "compiler.cpp.extra_flags=$BUILD_FLAGS" .
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[tasks.upload]
|
[tasks.monitor-32e]
|
||||||
depends = ["compile"]
|
description = "Serial monitor for ESP32-32E"
|
||||||
run = """
|
run = """
|
||||||
#!/usr/bin/env bash
|
arduino-cli monitor --port "${PORT:-/dev/ttyUSB0}" --config baudrate=115200
|
||||||
set -euo pipefail
|
|
||||||
case "${BOARD_TARGET:-e32r35t}" in
|
|
||||||
e32r35t)
|
|
||||||
FQBN="esp32:esp32:esp32:UploadSpeed=921600,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=default,DebugLevel=info,EraseFlash=none"
|
|
||||||
PORT_PATTERN='ttyUSB|CP210|CH340'
|
|
||||||
;;
|
|
||||||
waveshare_s3)
|
|
||||||
FQBN="esp32:esp32:waveshare_esp32_s3_touch_lcd_43:UploadSpeed=921600,USBMode=hwcdc,CDCOnBoot=cdc,CPUFreq=240,FlashMode=qio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,DebugLevel=info,PSRAM=enabled,LoopCore=1,EventsCore=1,EraseFlash=none"
|
|
||||||
PORT_PATTERN='ttyACM'
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "ERROR: Unknown BOARD_TARGET: $BOARD_TARGET"; exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
PORT=$(arduino-cli board list | grep -iE "$PORT_PATTERN" | head -1 | awk '{print $1}')
|
|
||||||
if [ -z "$PORT" ]; then
|
|
||||||
echo "No matching port for pattern: $PORT_PATTERN"
|
|
||||||
echo "Available boards:"
|
|
||||||
arduino-cli board list
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Uploading to $PORT ..."
|
|
||||||
arduino-cli upload --fqbn "$FQBN" -p "$PORT" .
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[tasks.monitor]
|
# ── ESP32-S3-LCD-4.3 ────────────────────────────────────
|
||||||
|
|
||||||
|
[tasks.compile-s3-43]
|
||||||
|
description = "Compile ESP32-S3-LCD-4.3 sketch"
|
||||||
|
depends = ["install-libs"]
|
||||||
run = """
|
run = """
|
||||||
#!/usr/bin/env bash
|
arduino-cli compile \
|
||||||
set -euo pipefail
|
--fqbn "esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=opi,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB" \
|
||||||
case "${BOARD_TARGET:-e32r35t}" in
|
--libraries ./libraries \
|
||||||
e32r35t) PORT_PATTERN='ttyUSB|CP210|CH340' ;;
|
--libraries ./vendor/esp32-s3-lcd-43 \
|
||||||
waveshare_s3) PORT_PATTERN='ttyACM' ;;
|
--build-property "compiler.cpp.extra_flags=-DDEBUG_MODE -DBOARD_HAS_PSRAM" \
|
||||||
*) echo "Unknown BOARD_TARGET"; exit 1 ;;
|
--warnings default \
|
||||||
esac
|
./boards/esp32-s3-lcd-43
|
||||||
PORT=$(arduino-cli board list | grep -iE "$PORT_PATTERN" | head -1 | awk '{print $1}')
|
|
||||||
if [ -z "$PORT" ]; then
|
|
||||||
echo "No matching port for pattern: $PORT_PATTERN"
|
|
||||||
arduino-cli board list
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
arduino-cli monitor -p "$PORT" -c baudrate=115200
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[tasks.all]
|
[tasks.upload-s3-43]
|
||||||
depends = ["upload"]
|
description = "Upload to ESP32-S3-LCD-4.3"
|
||||||
run = "mise run monitor"
|
run = """
|
||||||
|
arduino-cli upload \
|
||||||
|
--fqbn "esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=opi,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB" \
|
||||||
|
--port "${PORT:-/dev/ttyACM0}" \
|
||||||
|
./boards/esp32-s3-lcd-43
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.monitor-s3-43]
|
||||||
|
description = "Serial monitor for ESP32-S3-LCD-4.3"
|
||||||
|
run = """
|
||||||
|
arduino-cli monitor --port "${PORT:-/dev/ttyACM0}" --config baudrate=115200
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ── Convenience ──────────────────────────────────────────
|
||||||
|
|
||||||
|
[tasks.clean]
|
||||||
|
description = "Remove build artifacts"
|
||||||
|
run = """
|
||||||
|
rm -rf boards/esp32-32e/build
|
||||||
|
rm -rf boards/esp32-s3-lcd-43/build
|
||||||
|
echo "[OK] Build artifacts cleaned"
|
||||||
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user