From ec8ec4cd1880a732401ad823d8a6d070711df7b0 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Thu, 19 Feb 2026 14:48:25 -0800 Subject: [PATCH] refactor(Style): Add font abstraction and CSS-like styling constants --- .gitignore | 1 + AGENTS.md | 83 +++++++----- boards/esp32-32e-4/DisplayDriverTFT.cpp | 135 ++++++++++++++++++-- boards/esp32-32e-4/DisplayDriverTFT.h | 10 ++ boards/esp32-32e-4/board-config.sh | 2 +- boards/esp32-32e-4/board_config.h | 21 ++- boards/esp32-32e-4/esp32-32e-4.ino | 7 + boards/esp32-32e/DisplayDriverTFT.cpp | 108 ++++++++++++++++ boards/esp32-32e/DisplayDriverTFT.h | 12 +- boards/esp32-32e/board-config.sh | 2 +- boards/esp32-32e/board_config.h | 19 +++ boards/esp32-32e/esp32-32e.ino | 7 + boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp | 69 ++++++---- boards/esp32-s3-lcd-43/DisplayDriverGFX.h | 6 + boards/esp32-s3-lcd-43/board-config.sh | 2 +- boards/esp32-s3-lcd-43/board_config.h | 19 +++ libraries/KlubhausCore/src/IDisplayDriver.h | 6 + libraries/KlubhausCore/src/KlubhausCore.h | 1 + libraries/KlubhausCore/src/Style.h | 62 +++++++++ mise.toml | 2 +- 20 files changed, 492 insertions(+), 82 deletions(-) create mode 100644 libraries/KlubhausCore/src/Style.h diff --git a/.gitignore b/.gitignore index b622805..9a2a52e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino/ *~ .DS_Store compile_commands.json +.cache/ diff --git a/AGENTS.md b/AGENTS.md index f35ef37..4e29302 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,31 +4,38 @@ Multi-target Arduino/ESP32 doorbell alert system using ntfy.sh. ## Quick Reference +**Default BOARD**: `esp32-s3-lcd-43` (set in mise.toml). To switch boards: + ```bash -# Compile, upload, monitor -BOARD=esp32-32e-4 mise run compile # compile -BOARD=esp32-32e-4 mise run upload # upload (auto-kills monitor) -BOARD=esp32-32e-4 mise run monitor # start JSON monitor daemon -BOARD=esp32-32e-4 mise run log-tail # watch colored logs -BOARD=esp32-32e-4 mise run cmd COMMAND=dashboard # send command -BOARD=esp32-32e-4 mise run state # show device state +mise set BOARD=esp32-32e-4 # switch to ESP32-32E-4" +mise set BOARD=esp32-32e # switch to ESP32-32E +mise set BOARD=esp32-s3-lcd-43 # switch to ESP32-S3-LCD-4.3 +``` + +Then run commands without `BOARD=` prefix: + +```bash +mise run compile # compile +mise run upload # upload (auto-kills monitor) +mise run monitor # start JSON monitor daemon +mise run log-tail # watch colored logs +mise run cmd COMMAND=dashboard # send command +mise run state # show device state # Install libs (run after cloning) mise run install-libs-shared # shared libs (ArduinoJson, NTPClient) -BOARD=esp32-32e-4 mise run install # shared + board-specific libs +mise run install # shared + board-specific libs ``` -**Default BOARD**: `esp32-s3-lcd-43` (set in mise.toml) - ## Project Overview Three board targets share business logic via a common library: | Board | Display | Library | Build Command | |-------|---------|---------|--------------| -| ESP32-32E | SPI TFT 320x240 (ILI9341) | TFT_eSPI | `BOARD=esp32-32e mise run compile` | -| ESP32-32E-4" | SPI TFT 320x480 (ST7796) | TFT_eSPI | `BOARD=esp32-32e-4 mise run compile` | -| ESP32-S3-Touch-LCD-4.3 | 800x480 RGB parallel | LovyanGFX | `BOARD=esp32-s3-lcd-43 mise run compile` | +| ESP32-32E | SPI TFT 320x240 (ILI9341) | TFT_eSPI | `mise set BOARD=esp32-32e && mise run compile` | +| ESP32-32E-4" | SPI TFT 320x480 (ST7796) | TFT_eSPI | `mise set BOARD=esp32-32e-4 && mise run compile` | +| ESP32-S3-Touch-LCD-4.3 | 800x480 RGB parallel | LovyanGFX | `mise set BOARD=esp32-s3-lcd-43 && mise run compile` | ## Essential Commands @@ -39,18 +46,21 @@ All commands run via **mise**: mise run install-libs-shared mise run install # install shared + board-specific libs (requires BOARD env) -# Generic commands (set BOARD environment variable) -BOARD=esp32-32e mise run compile # compile for ESP32-32E -BOARD=esp32-32e mise run upload # upload to ESP32-32E -BOARD=esp32-32e mise run monitor # monitor ESP32-32E +# Generic commands (set BOARD with mise set first) +mise set BOARD=esp32-32e # switch to ESP32-32E +mise run compile # compile for ESP32-32E +mise run upload # upload to ESP32-32E +mise run monitor # monitor ESP32-32E -BOARD=esp32-32e-4 mise run compile # compile for ESP32-32E-4" -BOARD=esp32-32e-4 mise run upload # upload to ESP32-32E-4" -BOARD=esp32-32e-4 mise run monitor # monitor ESP32-32E-4" +mise set BOARD=esp32-32e-4 # switch to ESP32-32E-4" +mise run compile # compile for ESP32-32E-4" +mise run upload # upload to ESP32-32E-4" +mise run monitor # monitor ESP32-32E-4" -BOARD=esp32-s3-lcd-43 mise run compile # compile for ESP32-S3-LCD-4.3 -BOARD=esp32-s3-lcd-43 mise run upload # upload to ESP32-S3-LCD-4.3 -BOARD=esp32-s3-lcd-43 mise run monitor # monitor ESP32-S3-LCD-4.3 +mise set BOARD=esp32-s3-lcd-43 # switch to ESP32-S3-LCD-4.3 +mise run compile # compile for ESP32-S3-LCD-4.3 +mise run upload # upload to ESP32-S3-LCD-4.3 +mise run monitor # monitor ESP32-S3-LCD-4.3 # Other useful tasks mise run format # format code @@ -92,7 +102,7 @@ LIBS="--libraries ./vendor/esp32-32e-4/TFT_eSPI" OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM" ``` -**Port override**: `PORT=/dev/ttyXXX BOARD=esp32-32e mise run upload` +**Port override**: `mise set PORT=/dev/ttyXXX` before running upload/monitor commands. **Prerequisites**: arduino-cli with `esp32:esp32` platform installed, mise. @@ -193,8 +203,8 @@ Use `#pragma once` (not `#ifndef` guards). **No unit tests exist** - This is an embedded Arduino sketch. Verify changes by building and deploying to hardware: ```bash -BOARD=esp32-s3-lcd-43 mise run compile # compile for default board -BOARD=esp32-32e-4 mise run compile # compile for ESP32-32E-4" +mise set BOARD=esp32-s3-lcd-43 # compile for esp32-s3-lcd-43 +mise set BOARD=esp32-32e-4 # compile for ESP32-32E-4" ``` **Serial commands** (type into serial monitor at 115200 baud): @@ -232,10 +242,11 @@ The build system includes a Python-based monitor agent that provides JSON loggin Commands to interact with the daemon: ```bash -BOARD=esp32-32e mise run monitor # Start monitor daemon (background) -BOARD=esp32-32e mise run log-tail # Tail colored logs -BOARD=esp32-32e mise run cmd COMMAND=dashboard # Send command -BOARD=esp32-32e mise run state # Show current device state +mise set BOARD=esp32-32e +mise run monitor # Start monitor daemon (background) +mise run log-tail # Tail colored logs +mise run cmd COMMAND=dashboard # Send command +mise run state # Show current device state ``` The monitor daemon automatically starts after upload and is killed before upload to avoid serial port conflicts. @@ -260,7 +271,7 @@ The monitor daemon automatically starts after upload and is killed before upload 6. **WiFi credentials are per-board**: Each board directory has its own `secrets.h` because boards may be on different networks. -7. **Use BOARD environment variable**: All build commands require the `BOARD` environment variable (e.g., `BOARD=esp32-32e mise run compile`). The default BOARD is `esp32-s3-lcd-43` (set in mise.toml). +7. **Use BOARD environment variable**: All build commands require the `BOARD` environment variable. The default BOARD is `esp32-s3-lcd-43` (set in mise.toml). Use `mise set BOARD=xxx` to switch boards. 8. **Serial port contention**: The `upload`, `monitor`, and `monitor-raw` tasks automatically depend on `kill` which uses `fuser` to terminate any process using the serial port before starting. @@ -325,9 +336,9 @@ Track changes that were reverted to avoid flapping: | Issue | Solution | |-------|----------| -| "Another instance is running" error | Run `mise run kill BOARD=` or `FORCE=1 mise run BOARD=` | +| "Another instance is running" error | Run `mise run kill` to stop monitor daemon and release port | | Upload fails - port in use | Run `mise run kill` to stop monitor daemon and release port | -| Build fails - missing libraries | Run `mise run install-libs-shared` then `BOARD= mise run install` | +| Build fails - missing libraries | Run `mise run install-libs-shared` then `mise run install` | | LSP shows errors but build works | Run `mise run gen-compile-commands` to generate compile_commands.json for your BOARD | | No serial output | Check baud rate is set to 115200 in serial monitor | | State file not updating | Ensure serial output contains `[STATE]` or `[ADMIN]` tags | @@ -355,13 +366,15 @@ The project uses **clangd** for C++ via the `compile_commands.json` generated by **Generate compile_commands.json** for accurate IDE diagnostics: ```bash -BOARD=esp32-32e-4 mise run gen-compile-commands +mise set BOARD=esp32-32e-4 +mise run gen-compile-commands ``` **Generate static .crush.json** for a specific board: ```bash -BOARD=esp32-32e-4 mise run gen-crush-config +mise set BOARD=esp32-32e-4 +mise run gen-crush-config ``` ## Hardware Research Log diff --git a/boards/esp32-32e-4/DisplayDriverTFT.cpp b/boards/esp32-32e-4/DisplayDriverTFT.cpp index ca2e3cb..44e1dd4 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.cpp +++ b/boards/esp32-32e-4/DisplayDriverTFT.cpp @@ -1,9 +1,86 @@ #include "DisplayDriverTFT.h" +#include #include +#include extern DisplayManager display; +// ── Fonts ─────────────────────────────────────────────────── +// TFT_eSPI built-in fonts for 320x480 display (scaled from 800x480) +// Using FreeFonts - scaled bitmap fonts via setTextSize would be too pixelated +// Note: FreeFonts are enabled via LOAD_GFXFF=1 in board-config.sh + +void DisplayDriverTFT::setTitleFont() { _tft.setFreeFont(&FreeSansBold18pt7b); } + +void DisplayDriverTFT::setBodyFont() { _tft.setFreeFont(&FreeSans12pt7b); } + +void DisplayDriverTFT::setLabelFont() { _tft.setFreeFont(&FreeSans9pt7b); } + +void DisplayDriverTFT::setDefaultFont() { _tft.setTextFont(2); } + +// ── Test harness ─────────────────────────────────────────────── + +// Test harness: parse serial commands to inject synthetic touches +// Commands: +// TEST:touch x y press - simulate press at (x, y) +// TEST:touch x y release - simulate release at (x, y) +// TEST:touch clear - clear test mode +bool DisplayDriverTFT::parseTestTouch(int* outX, int* outY, bool* outPressed) { + if(!Serial.available()) + return false; + + if(Serial.peek() != 'T') { + return false; + } + + String cmd = Serial.readStringUntil('\n'); + cmd.trim(); + + if(!cmd.startsWith("TEST:touch")) + return false; + + int firstSpace = cmd.indexOf(' '); + if(firstSpace < 0) + return false; + + String args = cmd.substring(firstSpace + 1); + args.trim(); + + if(args.equals("clear")) { + _testMode = false; + Serial.println("[TEST] Test mode cleared"); + return false; + } + + int secondSpace = args.indexOf(' '); + if(secondSpace < 0) + return false; + + String xStr = args.substring(0, secondSpace); + String yState = args.substring(secondSpace + 1); + yState.trim(); + + int x = xStr.toInt(); + int y = yState.substring(0, yState.indexOf(' ')).toInt(); + String state = yState.substring(yState.indexOf(' ') + 1); + state.trim(); + + bool pressed = state.equals("press"); + + Serial.printf("[TEST] Injecting touch: (%d,%d) %s\n", x, y, pressed ? "press" : "release"); + + if(outX) + *outX = x; + if(outY) + *outY = y; + if(outPressed) + *outPressed = pressed; + + _testMode = true; + return true; +} + void DisplayDriverTFT::begin() { // Backlight pinMode(PIN_LCD_BL, OUTPUT); @@ -74,14 +151,17 @@ void DisplayDriverTFT::drawBoot(const ScreenState& st) { _tft.fillScreen(TFT_BLACK); _tft.setTextColor(TFT_WHITE, TFT_BLACK); - _tft.setTextSize(2); + + setTitleFont(); _tft.setCursor(10, 10); - _tft.printf("KLUBHAUS v%s", FW_VERSION); - _tft.setTextSize(1); + _tft.print("KLUBHAUS"); + + setBodyFont(); _tft.setCursor(10, 40); _tft.print(BOARD_NAME); // Show boot stage status + setLabelFont(); _tft.setCursor(10, 70); switch(stage) { case BootStage::SPLASH: @@ -113,15 +193,15 @@ void DisplayDriverTFT::drawAlert(const ScreenState& st) { _tft.fillScreen(bg); _tft.setTextColor(TFT_WHITE, bg); - _tft.setTextSize(3); + setTitleFont(); _tft.setCursor(10, 20); _tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT"); - _tft.setTextSize(2); + setBodyFont(); _tft.setCursor(10, 80); _tft.print(st.alertBody); - _tft.setTextSize(1); + setLabelFont(); _tft.setCursor(10, DISPLAY_HEIGHT - 20); _tft.print("Hold to silence..."); } @@ -130,14 +210,15 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) { _tft.fillScreen(TFT_BLACK); // Header - _tft.setTextColor(TFT_WHITE, TFT_BLACK); - _tft.setTextSize(1); - _tft.setCursor(5, 5); - _tft.printf("KLUBHAUS"); + _tft.fillRect(0, 0, DISPLAY_WIDTH, 30, 0x1A1A); // Dark gray header + setBodyFont(); + _tft.setTextColor(TFT_WHITE); + _tft.setCursor(5, 10); + _tft.print("KLUBHAUS"); // WiFi indicator - _tft.setCursor(DISPLAY_WIDTH - 50, 5); - _tft.printf("WiFi:%s", st.wifiSsid.length() > 0 ? "ON" : "OFF"); + _tft.setCursor(DISPLAY_WIDTH - 60, 10); + _tft.print(st.wifiSsid.length() > 0 ? "WiFi:ON" : "WiFi:OFF"); // Get tile layouts from library helper int tileCount = display.calculateDashboardLayouts(30, 8); @@ -160,8 +241,8 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) { _tft.drawRoundRect(x, y, w, h, 8, TFT_WHITE); // Tile label + setBodyFont(); _tft.setTextColor(TFT_WHITE); - _tft.setTextSize(2); int textLen = strlen(tileLabels[i]); int textW = textLen * 12; _tft.setCursor(x + w / 2 - textW / 2, y + h / 2 - 10); @@ -173,6 +254,34 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) { TouchEvent DisplayDriverTFT::readTouch() { TouchEvent evt; + + // Check for test injection via serial + int testX, testY; + bool testPressed; + if(parseTestTouch(&testX, &testY, &testPressed)) { + if(testPressed && !_touchWasPressed) { + evt.pressed = true; + _touchDownX = testX; + _touchDownY = testY; + evt.downX = _touchDownX; + evt.downY = _touchDownY; + } else if(!testPressed && _touchWasPressed) { + evt.released = true; + evt.downX = _touchDownX; + evt.downY = _touchDownY; + } + + if(testPressed) { + evt.x = testX; + evt.y = testY; + evt.downX = _touchDownX; + evt.downY = _touchDownY; + } + + _touchWasPressed = testPressed; + return evt; + } + uint16_t tx, ty; uint8_t touched = _tft.getTouch(&tx, &ty, 100); diff --git a/boards/esp32-32e-4/DisplayDriverTFT.h b/boards/esp32-32e-4/DisplayDriverTFT.h index db0eadf..46468c7 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.h +++ b/boards/esp32-32e-4/DisplayDriverTFT.h @@ -19,6 +19,12 @@ public: // Dashboard - uses transform for touch coordinate correction void transformTouch(int* x, int* y) override; + // Fonts + void setTitleFont() override; + void setBodyFont() override; + void setLabelFont() override; + void setDefaultFont() override; + private: void drawBoot(const ScreenState& st); void drawAlert(const ScreenState& st); @@ -36,4 +42,8 @@ private: bool _touchWasPressed = false; int _touchDownX = -1; int _touchDownY = -1; + + // Test mode for touch injection + bool _testMode = false; + bool parseTestTouch(int* outX, int* outY, bool* outPressed); }; diff --git a/boards/esp32-32e-4/board-config.sh b/boards/esp32-32e-4/board-config.sh index ed7d51e..994d822 100644 --- a/boards/esp32-32e-4/board-config.sh +++ b/boards/esp32-32e-4/board-config.sh @@ -1,4 +1,4 @@ FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" PORT="/dev/ttyUSB0" LIBS="--libraries ./vendor/esp32-32e-4/TFT_eSPI" -OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM" +OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLOAD_GFXFF=1" diff --git a/boards/esp32-32e-4/board_config.h b/boards/esp32-32e-4/board_config.h index 52612cf..2acd33c 100644 --- a/boards/esp32-32e-4/board_config.h +++ b/boards/esp32-32e-4/board_config.h @@ -15,4 +15,23 @@ #define PIN_LCD_BL 27 // Touch — XPT2046 configured in tft_user_setup.h -// Touch CS: GPIO33, Touch IRQ: GPIO36 \ No newline at end of file +// Touch CS: GPIO33, Touch IRQ: GPIO36 + +// ── Style Constants (CSS-like) ──────────────────────────────────────── +// Spacing - scaled for 320x480 +#define STYLE_SPACING_X 6 +#define STYLE_SPACING_Y 6 +#define STYLE_HEADER_HEIGHT 24 +#define STYLE_TILE_GAP 4 +#define STYLE_TILE_PADDING 8 +#define STYLE_TILE_RADIUS 4 + +// Colors +#define STYLE_COLOR_BG TFT_BLACK +#define STYLE_COLOR_HEADER 0x1A1A +#define STYLE_COLOR_FG TFT_WHITE +#define STYLE_COLOR_ALERT TFT_RED +#define STYLE_COLOR_TILE_1 0x0280 +#define STYLE_COLOR_TILE_2 0x0400 +#define STYLE_COLOR_TILE_3 0x0440 +#define STYLE_COLOR_TILE_4 0x0100 \ No newline at end of file diff --git a/boards/esp32-32e-4/esp32-32e-4.ino b/boards/esp32-32e-4/esp32-32e-4.ino index 78971d9..378b9b1 100644 --- a/boards/esp32-32e-4/esp32-32e-4.ino +++ b/boards/esp32-32e-4/esp32-32e-4.ino @@ -4,7 +4,14 @@ #include "DisplayDriverTFT.h" #include "board_config.h" + +// Include local secrets.h if it exists (board-specific credentials), +// otherwise KlubhausCore/src/secrets.h provides defaults. +#ifdef LOCAL_SECRETS #include "secrets.h" +#else +#include +#endif #include diff --git a/boards/esp32-32e/DisplayDriverTFT.cpp b/boards/esp32-32e/DisplayDriverTFT.cpp index 810f08f..91cbced 100644 --- a/boards/esp32-32e/DisplayDriverTFT.cpp +++ b/boards/esp32-32e/DisplayDriverTFT.cpp @@ -1,5 +1,85 @@ #include "DisplayDriverTFT.h" +#include +#include +#include + +extern DisplayManager display; + +// ── Fonts ─────────────────────────────────────────────────── +// TFT_eSPI built-in fonts for 320x240 display (further scaled) +// Using FreeFonts for better readability + +void DisplayDriverTFT::setTitleFont() { _tft.setFreeFont(&FreeSansBold12pt7b); } + +void DisplayDriverTFT::setBodyFont() { _tft.setFreeFont(&FreeSans9pt7b); } + +void DisplayDriverTFT::setLabelFont() { _tft.setFreeFont(&FreeSans9pt7b); } + +void DisplayDriverTFT::setDefaultFont() { _tft.setTextFont(2); } + +// ── Test harness ─────────────────────────────────────────────── + +// Test harness: parse serial commands to inject synthetic touches +// Commands: +// TEST:touch x y press - simulate press at (x, y) +// TEST:touch x y release - simulate release at (x, y) +// TEST:touch clear - clear test mode +bool DisplayDriverTFT::parseTestTouch(int* outX, int* outY, bool* outPressed) { + if(!Serial.available()) + return false; + + if(Serial.peek() != 'T') { + return false; + } + + String cmd = Serial.readStringUntil('\n'); + cmd.trim(); + + if(!cmd.startsWith("TEST:touch")) + return false; + + int firstSpace = cmd.indexOf(' '); + if(firstSpace < 0) + return false; + + String args = cmd.substring(firstSpace + 1); + args.trim(); + + if(args.equals("clear")) { + _testMode = false; + Serial.println("[TEST] Test mode cleared"); + return false; + } + + int secondSpace = args.indexOf(' '); + if(secondSpace < 0) + return false; + + String xStr = args.substring(0, secondSpace); + String yState = args.substring(secondSpace + 1); + yState.trim(); + + int x = xStr.toInt(); + int y = yState.substring(0, yState.indexOf(' ')).toInt(); + String state = yState.substring(yState.indexOf(' ') + 1); + state.trim(); + + bool pressed = state.equals("press"); + + Serial.printf("[TEST] Injecting touch: (%d,%d) %s\n", x, y, pressed ? "press" : "release"); + + if(outX) + *outX = x; + if(outY) + *outY = y; + if(outPressed) + *outPressed = pressed; + + _testMode = true; + return true; +} + void DisplayDriverTFT::begin() { // Backlight pinMode(PIN_LCD_BL, OUTPUT); @@ -145,6 +225,34 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) { TouchEvent DisplayDriverTFT::readTouch() { TouchEvent evt; + + // Check for test injection via serial + int testX, testY; + bool testPressed; + if(parseTestTouch(&testX, &testY, &testPressed)) { + if(testPressed && !_touchWasPressed) { + evt.pressed = true; + _touchDownX = testX; + _touchDownY = testY; + evt.downX = _touchDownX; + evt.downY = _touchDownY; + } else if(!testPressed && _touchWasPressed) { + evt.released = true; + evt.downX = _touchDownX; + evt.downY = _touchDownY; + } + + if(testPressed) { + evt.x = testX; + evt.y = testY; + evt.downX = _touchDownX; + evt.downY = _touchDownY; + } + + _touchWasPressed = testPressed; + return evt; + } + uint16_t tx, ty; uint8_t touched = _tft.getTouch(&tx, &ty, 100); diff --git a/boards/esp32-32e/DisplayDriverTFT.h b/boards/esp32-32e/DisplayDriverTFT.h index 9b8bd37..b8a24ab 100644 --- a/boards/esp32-32e/DisplayDriverTFT.h +++ b/boards/esp32-32e/DisplayDriverTFT.h @@ -11,12 +11,18 @@ public: void setBacklight(bool on) override; void render(const ScreenState& state) override; TouchEvent readTouch() override; - int dashboardTouch(int x, int y) override; + int dashboardTouch(int x, int y); HoldState updateHold(unsigned long holdMs) override; void updateHint(int x, int y, bool active) override; int width() override { return DISPLAY_WIDTH; } int height() override { return DISPLAY_HEIGHT; } + // Fonts + void setTitleFont() override; + void setBodyFont() override; + void setLabelFont() override; + void setDefaultFont() override; + private: void drawBoot(const ScreenState& st); void drawAlert(const ScreenState& st); @@ -34,4 +40,8 @@ private: bool _touchWasPressed = false; int _touchDownX = -1; int _touchDownY = -1; + + // Test mode for touch injection + bool _testMode = false; + bool parseTestTouch(int* outX, int* outY, bool* outPressed); }; diff --git a/boards/esp32-32e/board-config.sh b/boards/esp32-32e/board-config.sh index aa3d5eb..4222609 100644 --- a/boards/esp32-32e/board-config.sh +++ b/boards/esp32-32e/board-config.sh @@ -1,4 +1,4 @@ FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" PORT="/dev/ttyUSB0" LIBS="--libraries ./vendor/esp32-32e/TFT_eSPI" -OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM" +OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLOCAL_SECRETS -DLOAD_GFXFF=1" diff --git a/boards/esp32-32e/board_config.h b/boards/esp32-32e/board_config.h index 06a9ecc..88edfdb 100644 --- a/boards/esp32-32e/board_config.h +++ b/boards/esp32-32e/board_config.h @@ -20,3 +20,22 @@ // If using capacitive touch (e.g. FT6236), configure I2C pins here: // #define TOUCH_SDA 21 // #define TOUCH_SCL 22 + +// ── Style Constants (CSS-like) ──────────────────────────────────────── +// Spacing - scaled for 320x240 +#define STYLE_SPACING_X 4 +#define STYLE_SPACING_Y 4 +#define STYLE_HEADER_HEIGHT 20 +#define STYLE_TILE_GAP 4 +#define STYLE_TILE_PADDING 6 +#define STYLE_TILE_RADIUS 4 + +// Colors +#define STYLE_COLOR_BG TFT_BLACK +#define STYLE_COLOR_HEADER 0x1A1A +#define STYLE_COLOR_FG TFT_WHITE +#define STYLE_COLOR_ALERT TFT_RED +#define STYLE_COLOR_TILE_1 0x0280 +#define STYLE_COLOR_TILE_2 0x0400 +#define STYLE_COLOR_TILE_3 0x0440 +#define STYLE_COLOR_TILE_4 0x0100 diff --git a/boards/esp32-32e/esp32-32e.ino b/boards/esp32-32e/esp32-32e.ino index c1faf50..2e51d38 100644 --- a/boards/esp32-32e/esp32-32e.ino +++ b/boards/esp32-32e/esp32-32e.ino @@ -4,7 +4,14 @@ #include "DisplayDriverTFT.h" #include "board_config.h" + +// Include local secrets.h if it exists (board-specific credentials), +// otherwise KlubhausCore/src/secrets.h provides defaults. +#ifdef LOCAL_SECRETS #include "secrets.h" +#else +#include +#endif #include diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp index c8df1b7..0f6a4e3 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp @@ -58,6 +58,16 @@ int DisplayDriverGFX::width() { return _gfx ? _gfx->width() : DISP_W; } int DisplayDriverGFX::height() { return _gfx ? _gfx->height() : DISP_H; } +// ── Fonts ── +// LovyanGFX built-in fonts for 800x480 display +void DisplayDriverGFX::setTitleFont() { _gfx->setFont(&fonts::FreeSansBold24pt7b); } + +void DisplayDriverGFX::setBodyFont() { _gfx->setFont(&fonts::FreeSans18pt7b); } + +void DisplayDriverGFX::setLabelFont() { _gfx->setFont(&fonts::FreeSans12pt7b); } + +void DisplayDriverGFX::setDefaultFont() { _gfx->setFont(&fonts::Font2); } + // Transform touch coordinates to match display orientation // GT911 touch panel on this board is rotated 180° relative to display void DisplayDriverGFX::transformTouch(int* x, int* y) { @@ -121,9 +131,12 @@ bool DisplayDriverGFX::parseTestTouch(int* outX, int* outY, bool* outPressed) { Serial.printf("[TEST] Injecting touch: (%d,%d) %s\n", x, y, pressed ? "press" : "release"); - if(outX) *outX = x; - if(outY) *outY = y; - if(outPressed) *outPressed = pressed; + if(outX) + *outX = x; + if(outY) + *outY = y; + if(outPressed) + *outPressed = pressed; _testMode = true; return true; @@ -323,18 +336,19 @@ void DisplayDriverGFX::render(const ScreenState& state) { void DisplayDriverGFX::drawBoot(const ScreenState& state) { BootStage stage = state.bootStage; - _gfx->fillScreen(0x000000); - _gfx->setTextColor(0xFFFF); - _gfx->setTextSize(2); - _gfx->setCursor(10, 10); + _gfx->fillScreen(TFT_BLACK); + _gfx->setTextColor(STYLE_COLOR_FG); + setTitleFont(); + _gfx->setCursor(STYLE_SPACING_X, STYLE_SPACING_Y); _gfx->print("KLUBHAUS"); - _gfx->setTextSize(1); - _gfx->setCursor(10, 50); + setBodyFont(); + _gfx->setCursor(STYLE_SPACING_X, STYLE_SPACING_Y + STYLE_HEADER_HEIGHT); _gfx->print(BOARD_NAME); // Show boot stage status - _gfx->setCursor(10, 80); + setLabelFont(); + _gfx->setCursor(STYLE_SPACING_X, STYLE_SPACING_Y + STYLE_HEADER_HEIGHT + 30); switch(stage) { case BootStage::SPLASH: _gfx->print("Initializing..."); @@ -363,30 +377,29 @@ void DisplayDriverGFX::drawAlert(const ScreenState& state) { uint16_t bg = _gfx->color565(pulse, 0, 0); _gfx->fillScreen(bg); - _gfx->setTextColor(0xFFFF, bg); + _gfx->setTextColor(STYLE_COLOR_FG, bg); - _gfx->setTextSize(3); - _gfx->setCursor(10, 20); + setTitleFont(); + _gfx->setCursor(STYLE_SPACING_X, STYLE_HEADER_HEIGHT); _gfx->print(state.alertTitle.length() > 0 ? state.alertTitle.c_str() : "ALERT"); - _gfx->setTextSize(2); - _gfx->setCursor(10, 80); + setBodyFont(); + _gfx->setCursor(STYLE_SPACING_X, STYLE_HEADER_HEIGHT + 50); _gfx->print(state.alertBody); - _gfx->setTextSize(1); - _gfx->setCursor(10, DISP_H - 20); + setLabelFont(); + _gfx->setCursor(STYLE_SPACING_X, DISP_H - STYLE_SPACING_Y); _gfx->print("Hold to silence..."); } void DisplayDriverGFX::drawDashboard(const ScreenState& state) { - _gfx->fillScreen(0x001030); // Dark blue + _gfx->fillScreen(STYLE_COLOR_BG); // Header - _gfx->fillRect(0, 0, DISP_W, 40, 0x1A1A); // Dark gray - _gfx->setFont(&fonts::Font2); - _gfx->setTextColor(0xFFFF); - _gfx->setTextSize(1); - _gfx->setCursor(10, 12); + _gfx->fillRect(0, 0, DISP_W, STYLE_HEADER_HEIGHT, STYLE_COLOR_HEADER); + setBodyFont(); + _gfx->setTextColor(STYLE_COLOR_FG); + _gfx->setCursor(STYLE_SPACING_X, 12); _gfx->printf("KLUBHAUS"); // WiFi status @@ -407,15 +420,15 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) { int h = lay.h; // Tile background - _gfx->fillRoundRect(x, y, w, h, 8, 0x0220); + _gfx->fillRoundRect(x, y, w, h, STYLE_TILE_RADIUS, 0x0220); // Tile border - _gfx->drawRoundRect(x, y, w, h, 8, 0xFFFF); + _gfx->drawRoundRect(x, y, w, h, STYLE_TILE_RADIUS, STYLE_COLOR_FG); // Tile label - _gfx->setTextColor(0xFFFF); - _gfx->setTextSize(2); - _gfx->setCursor(x + w / 2 - 10, y + h / 2 - 10); + _gfx->setTextColor(STYLE_COLOR_FG); + setBodyFont(); + _gfx->setCursor(x + w / 2 - 30, y + h / 2 - 10); _gfx->print(tileLabels[i]); } } diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h index a33466b..9a65942 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h @@ -18,6 +18,12 @@ public: int width() override; int height() override; + // Fonts + void setTitleFont() override; + void setBodyFont() override; + void setLabelFont() override; + void setDefaultFont() override; + // Transform touch coordinates (handles rotated touch panels) void transformTouch(int* x, int* y) override; diff --git a/boards/esp32-s3-lcd-43/board-config.sh b/boards/esp32-s3-lcd-43/board-config.sh index 58eb01c..865a1c7 100644 --- a/boards/esp32-s3-lcd-43/board-config.sh +++ b/boards/esp32-s3-lcd-43/board-config.sh @@ -1,4 +1,4 @@ FQBN="esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=enabled,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB" PORT="/dev/ttyACM0" LIBS="--libraries ~/Arduino/libraries/LovyanGFX" -OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLGFX_USE_V1" +OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLGFX_USE_V1 -DLOCAL_SECRETS" diff --git a/boards/esp32-s3-lcd-43/board_config.h b/boards/esp32-s3-lcd-43/board_config.h index d79b261..f533537 100644 --- a/boards/esp32-s3-lcd-43/board_config.h +++ b/boards/esp32-s3-lcd-43/board_config.h @@ -38,3 +38,22 @@ // ── GT911 Touch ── #define GT911_ADDR 0x5D // #define TOUCH_INT -1 + +// ── Style Constants (CSS-like) ──────────────────────────────────────── +// Spacing +#define STYLE_SPACING_X 10 +#define STYLE_SPACING_Y 10 +#define STYLE_HEADER_HEIGHT 40 +#define STYLE_TILE_GAP 8 +#define STYLE_TILE_PADDING 16 +#define STYLE_TILE_RADIUS 8 + +// Colors +#define STYLE_COLOR_BG 0x001030 // Dark blue +#define STYLE_COLOR_HEADER 0x1A1A // Dark gray +#define STYLE_COLOR_FG TFT_WHITE +#define STYLE_COLOR_ALERT TFT_RED +#define STYLE_COLOR_TILE_1 0x0280 // Green +#define STYLE_COLOR_TILE_2 0x0400 // Dark green +#define STYLE_COLOR_TILE_3 0x0440 // Teal +#define STYLE_COLOR_TILE_4 0x0100 // Dark red diff --git a/libraries/KlubhausCore/src/IDisplayDriver.h b/libraries/KlubhausCore/src/IDisplayDriver.h index ede9ab6..123fdbe 100644 --- a/libraries/KlubhausCore/src/IDisplayDriver.h +++ b/libraries/KlubhausCore/src/IDisplayDriver.h @@ -36,6 +36,12 @@ public: virtual int width() = 0; virtual int height() = 0; + // ── Fonts ── + virtual void setTitleFont() = 0; // Large titles (KLUBHAUS, ALERT) + virtual void setBodyFont() = 0; // Normal text (status, body) + virtual void setLabelFont() = 0; // Small text (hints, captions) + virtual void setDefaultFont() = 0; // Reset to default font + // ── Touch transform (for rotated panels) ── virtual void transformTouch(int* x, int* y) { /* default: no transform */ } }; diff --git a/libraries/KlubhausCore/src/KlubhausCore.h b/libraries/KlubhausCore/src/KlubhausCore.h index 66f769d..6f8dde3 100644 --- a/libraries/KlubhausCore/src/KlubhausCore.h +++ b/libraries/KlubhausCore/src/KlubhausCore.h @@ -7,3 +7,4 @@ #include "IDisplayDriver.h" #include "NetManager.h" #include "ScreenState.h" +#include "Style.h" diff --git a/libraries/KlubhausCore/src/Style.h b/libraries/KlubhausCore/src/Style.h new file mode 100644 index 0000000..9345bad --- /dev/null +++ b/libraries/KlubhausCore/src/Style.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +struct Layout { + uint16_t x, y, w, h; + + Layout() + : x(0) + , y(0) + , w(0) + , h(0) { } + Layout(uint16_t x_, uint16_t y_, uint16_t w_, uint16_t h_) + : x(x_) + , y(y_) + , w(w_) + , h(h_) { } + + static Layout fullScreen(uint16_t w, uint16_t h) { return Layout(0, 0, w, h); } + + static Layout header(uint16_t screenW, uint16_t headerH, uint16_t padding = 10) { + return Layout(0, 0, screenW, headerH); + } + + static Layout content( + uint16_t screenW, uint16_t screenH, uint16_t headerH, uint16_t padding = 10) { + return Layout( + padding, headerH + padding, screenW - 2 * padding, screenH - headerH - 2 * padding); + } + + static Layout tile(uint8_t col, uint8_t row, uint8_t cols, uint8_t rows, uint16_t contentW, + uint16_t contentH, uint8_t gap = 8) { + uint16_t tileW = (contentW - (cols - 1) * gap) / cols; + uint16_t tileH = (contentH - (rows - 1) * gap) / rows; + uint16_t x = col * (tileW + gap); + uint16_t y = row * (tileH + gap); + return Layout(x, y, tileW, tileH); + } +}; + +struct TileMetrics { + uint16_t tileW; + uint16_t tileH; + uint16_t gap; + uint8_t cols; + uint8_t rows; + + static TileMetrics calculate( + uint16_t contentW, uint16_t contentH, uint8_t cols, uint8_t rows, uint8_t gap = 8) { + TileMetrics m; + m.cols = cols; + m.rows = rows; + m.gap = gap; + m.tileW = (contentW - (cols - 1) * gap) / cols; + m.tileH = (contentH - (rows - 1) * gap) / rows; + return m; + } + + Layout get(uint8_t col, uint8_t row) const { + return Layout(col * (tileW + gap), row * (tileH + gap), tileW, tileH); + } +}; diff --git a/mise.toml b/mise.toml index 4ff42d9..9beebfb 100644 --- a/mise.toml +++ b/mise.toml @@ -215,4 +215,4 @@ echo "[OK] Generated .crush.json with FQBN: $FQBN" run = "git add .; lumen draft | git commit -F - " [env] -BOARD = "esp32-s3-lcd-43" +BOARD = "esp32-32e"