refactor(Style): Add font abstraction and CSS-like styling constants

This commit is contained in:
2026-02-19 14:48:25 -08:00
parent 59b2bf01b7
commit ec8ec4cd18
20 changed files with 492 additions and 82 deletions

1
.gitignore vendored
View File

@@ -16,3 +16,4 @@ vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino/
*~ *~
.DS_Store .DS_Store
compile_commands.json compile_commands.json
.cache/

View File

@@ -4,31 +4,38 @@ Multi-target Arduino/ESP32 doorbell alert system using ntfy.sh.
## Quick Reference ## Quick Reference
**Default BOARD**: `esp32-s3-lcd-43` (set in mise.toml). To switch boards:
```bash ```bash
# Compile, upload, monitor mise set BOARD=esp32-32e-4 # switch to ESP32-32E-4"
BOARD=esp32-32e-4 mise run compile # compile mise set BOARD=esp32-32e # switch to ESP32-32E
BOARD=esp32-32e-4 mise run upload # upload (auto-kills monitor) mise set BOARD=esp32-s3-lcd-43 # switch to ESP32-S3-LCD-4.3
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 Then run commands without `BOARD=` prefix:
BOARD=esp32-32e-4 mise run state # show device state
```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) # Install libs (run after cloning)
mise run install-libs-shared # shared libs (ArduinoJson, NTPClient) 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 ## Project Overview
Three board targets share business logic via a common library: Three board targets share business logic via a common library:
| Board | Display | Library | Build Command | | Board | Display | Library | Build Command |
|-------|---------|---------|--------------| |-------|---------|---------|--------------|
| ESP32-32E | SPI TFT 320x240 (ILI9341) | TFT_eSPI | `BOARD=esp32-32e 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 | `BOARD=esp32-32e-4 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 | `BOARD=esp32-s3-lcd-43 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 ## Essential Commands
@@ -39,18 +46,21 @@ All commands run via **mise**:
mise run install-libs-shared mise run install-libs-shared
mise run install # install shared + board-specific libs (requires BOARD env) mise run install # install shared + board-specific libs (requires BOARD env)
# Generic commands (set BOARD environment variable) # Generic commands (set BOARD with mise set first)
BOARD=esp32-32e mise run compile # compile for ESP32-32E mise set BOARD=esp32-32e # switch to ESP32-32E
BOARD=esp32-32e mise run upload # upload to ESP32-32E mise run compile # compile for ESP32-32E
BOARD=esp32-32e mise run monitor # monitor 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" mise set BOARD=esp32-32e-4 # switch to ESP32-32E-4"
BOARD=esp32-32e-4 mise run upload # upload to ESP32-32E-4" mise run compile # compile for ESP32-32E-4"
BOARD=esp32-32e-4 mise run monitor # monitor 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 mise set BOARD=esp32-s3-lcd-43 # switch to ESP32-S3-LCD-4.3
BOARD=esp32-s3-lcd-43 mise run upload # upload to ESP32-S3-LCD-4.3 mise run compile # compile for ESP32-S3-LCD-4.3
BOARD=esp32-s3-lcd-43 mise run monitor # monitor 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 # Other useful tasks
mise run format # format code mise run format # format code
@@ -92,7 +102,7 @@ LIBS="--libraries ./vendor/esp32-32e-4/TFT_eSPI"
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM" 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. **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: **No unit tests exist** - This is an embedded Arduino sketch. Verify changes by building and deploying to hardware:
```bash ```bash
BOARD=esp32-s3-lcd-43 mise run compile # compile for default board mise set BOARD=esp32-s3-lcd-43 # compile for esp32-s3-lcd-43
BOARD=esp32-32e-4 mise run compile # compile for ESP32-32E-4" mise set BOARD=esp32-32e-4 # compile for ESP32-32E-4"
``` ```
**Serial commands** (type into serial monitor at 115200 baud): **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: Commands to interact with the daemon:
```bash ```bash
BOARD=esp32-32e mise run monitor # Start monitor daemon (background) mise set BOARD=esp32-32e
BOARD=esp32-32e mise run log-tail # Tail colored logs mise run monitor # Start monitor daemon (background)
BOARD=esp32-32e mise run cmd COMMAND=dashboard # Send command mise run log-tail # Tail colored logs
BOARD=esp32-32e mise run state # Show current device state 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. 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. 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. 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 | | Issue | Solution |
|-------|----------| |-------|----------|
| "Another instance is running" error | Run `mise run kill BOARD=<board>` or `FORCE=1 mise run <task> BOARD=<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 | | 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=<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 | | 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 | | 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 | | 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: **Generate compile_commands.json** for accurate IDE diagnostics:
```bash ```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: **Generate static .crush.json** for a specific board:
```bash ```bash
BOARD=esp32-32e-4 mise run gen-crush-config mise set BOARD=esp32-32e-4
mise run gen-crush-config
``` ```
## Hardware Research Log ## Hardware Research Log

View File

@@ -1,9 +1,86 @@
#include "DisplayDriverTFT.h" #include "DisplayDriverTFT.h"
#include <Arduino.h>
#include <KlubhausCore.h> #include <KlubhausCore.h>
#include <TFT_eSPI.h>
extern DisplayManager display; 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() { void DisplayDriverTFT::begin() {
// Backlight // Backlight
pinMode(PIN_LCD_BL, OUTPUT); pinMode(PIN_LCD_BL, OUTPUT);
@@ -74,14 +151,17 @@ void DisplayDriverTFT::drawBoot(const ScreenState& st) {
_tft.fillScreen(TFT_BLACK); _tft.fillScreen(TFT_BLACK);
_tft.setTextColor(TFT_WHITE, TFT_BLACK); _tft.setTextColor(TFT_WHITE, TFT_BLACK);
_tft.setTextSize(2);
setTitleFont();
_tft.setCursor(10, 10); _tft.setCursor(10, 10);
_tft.printf("KLUBHAUS v%s", FW_VERSION); _tft.print("KLUBHAUS");
_tft.setTextSize(1);
setBodyFont();
_tft.setCursor(10, 40); _tft.setCursor(10, 40);
_tft.print(BOARD_NAME); _tft.print(BOARD_NAME);
// Show boot stage status // Show boot stage status
setLabelFont();
_tft.setCursor(10, 70); _tft.setCursor(10, 70);
switch(stage) { switch(stage) {
case BootStage::SPLASH: case BootStage::SPLASH:
@@ -113,15 +193,15 @@ void DisplayDriverTFT::drawAlert(const ScreenState& st) {
_tft.fillScreen(bg); _tft.fillScreen(bg);
_tft.setTextColor(TFT_WHITE, bg); _tft.setTextColor(TFT_WHITE, bg);
_tft.setTextSize(3); setTitleFont();
_tft.setCursor(10, 20); _tft.setCursor(10, 20);
_tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT"); _tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT");
_tft.setTextSize(2); setBodyFont();
_tft.setCursor(10, 80); _tft.setCursor(10, 80);
_tft.print(st.alertBody); _tft.print(st.alertBody);
_tft.setTextSize(1); setLabelFont();
_tft.setCursor(10, DISPLAY_HEIGHT - 20); _tft.setCursor(10, DISPLAY_HEIGHT - 20);
_tft.print("Hold to silence..."); _tft.print("Hold to silence...");
} }
@@ -130,14 +210,15 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
_tft.fillScreen(TFT_BLACK); _tft.fillScreen(TFT_BLACK);
// Header // Header
_tft.setTextColor(TFT_WHITE, TFT_BLACK); _tft.fillRect(0, 0, DISPLAY_WIDTH, 30, 0x1A1A); // Dark gray header
_tft.setTextSize(1); setBodyFont();
_tft.setCursor(5, 5); _tft.setTextColor(TFT_WHITE);
_tft.printf("KLUBHAUS"); _tft.setCursor(5, 10);
_tft.print("KLUBHAUS");
// WiFi indicator // WiFi indicator
_tft.setCursor(DISPLAY_WIDTH - 50, 5); _tft.setCursor(DISPLAY_WIDTH - 60, 10);
_tft.printf("WiFi:%s", st.wifiSsid.length() > 0 ? "ON" : "OFF"); _tft.print(st.wifiSsid.length() > 0 ? "WiFi:ON" : "WiFi:OFF");
// Get tile layouts from library helper // Get tile layouts from library helper
int tileCount = display.calculateDashboardLayouts(30, 8); 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); _tft.drawRoundRect(x, y, w, h, 8, TFT_WHITE);
// Tile label // Tile label
setBodyFont();
_tft.setTextColor(TFT_WHITE); _tft.setTextColor(TFT_WHITE);
_tft.setTextSize(2);
int textLen = strlen(tileLabels[i]); int textLen = strlen(tileLabels[i]);
int textW = textLen * 12; int textW = textLen * 12;
_tft.setCursor(x + w / 2 - textW / 2, y + h / 2 - 10); _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 DisplayDriverTFT::readTouch() {
TouchEvent evt; 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; uint16_t tx, ty;
uint8_t touched = _tft.getTouch(&tx, &ty, 100); uint8_t touched = _tft.getTouch(&tx, &ty, 100);

View File

@@ -19,6 +19,12 @@ public:
// Dashboard - uses transform for touch coordinate correction // Dashboard - uses transform for touch coordinate correction
void transformTouch(int* x, int* y) override; void transformTouch(int* x, int* y) override;
// Fonts
void setTitleFont() override;
void setBodyFont() override;
void setLabelFont() override;
void setDefaultFont() override;
private: private:
void drawBoot(const ScreenState& st); void drawBoot(const ScreenState& st);
void drawAlert(const ScreenState& st); void drawAlert(const ScreenState& st);
@@ -36,4 +42,8 @@ private:
bool _touchWasPressed = false; bool _touchWasPressed = false;
int _touchDownX = -1; int _touchDownX = -1;
int _touchDownY = -1; int _touchDownY = -1;
// Test mode for touch injection
bool _testMode = false;
bool parseTestTouch(int* outX, int* outY, bool* outPressed);
}; };

View File

@@ -1,4 +1,4 @@
FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default"
PORT="/dev/ttyUSB0" PORT="/dev/ttyUSB0"
LIBS="--libraries ./vendor/esp32-32e-4/TFT_eSPI" LIBS="--libraries ./vendor/esp32-32e-4/TFT_eSPI"
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM" OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLOAD_GFXFF=1"

View File

@@ -15,4 +15,23 @@
#define PIN_LCD_BL 27 #define PIN_LCD_BL 27
// Touch — XPT2046 configured in tft_user_setup.h // Touch — XPT2046 configured in tft_user_setup.h
// Touch CS: GPIO33, Touch IRQ: GPIO36 // 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

View File

@@ -4,7 +4,14 @@
#include "DisplayDriverTFT.h" #include "DisplayDriverTFT.h"
#include "board_config.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" #include "secrets.h"
#else
#include <secrets.h>
#endif
#include <KlubhausCore.h> #include <KlubhausCore.h>

View File

@@ -1,5 +1,85 @@
#include "DisplayDriverTFT.h" #include "DisplayDriverTFT.h"
#include <Arduino.h>
#include <KlubhausCore.h>
#include <TFT_eSPI.h>
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() { void DisplayDriverTFT::begin() {
// Backlight // Backlight
pinMode(PIN_LCD_BL, OUTPUT); pinMode(PIN_LCD_BL, OUTPUT);
@@ -145,6 +225,34 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
TouchEvent DisplayDriverTFT::readTouch() { TouchEvent DisplayDriverTFT::readTouch() {
TouchEvent evt; 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; uint16_t tx, ty;
uint8_t touched = _tft.getTouch(&tx, &ty, 100); uint8_t touched = _tft.getTouch(&tx, &ty, 100);

View File

@@ -11,12 +11,18 @@ public:
void setBacklight(bool on) override; void setBacklight(bool on) override;
void render(const ScreenState& state) override; void render(const ScreenState& state) override;
TouchEvent readTouch() override; TouchEvent readTouch() override;
int dashboardTouch(int x, int y) override; int dashboardTouch(int x, int y);
HoldState updateHold(unsigned long holdMs) override; HoldState updateHold(unsigned long holdMs) override;
void updateHint(int x, int y, bool active) override; void updateHint(int x, int y, bool active) override;
int width() override { return DISPLAY_WIDTH; } int width() override { return DISPLAY_WIDTH; }
int height() override { return DISPLAY_HEIGHT; } int height() override { return DISPLAY_HEIGHT; }
// Fonts
void setTitleFont() override;
void setBodyFont() override;
void setLabelFont() override;
void setDefaultFont() override;
private: private:
void drawBoot(const ScreenState& st); void drawBoot(const ScreenState& st);
void drawAlert(const ScreenState& st); void drawAlert(const ScreenState& st);
@@ -34,4 +40,8 @@ private:
bool _touchWasPressed = false; bool _touchWasPressed = false;
int _touchDownX = -1; int _touchDownX = -1;
int _touchDownY = -1; int _touchDownY = -1;
// Test mode for touch injection
bool _testMode = false;
bool parseTestTouch(int* outX, int* outY, bool* outPressed);
}; };

View File

@@ -1,4 +1,4 @@
FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default"
PORT="/dev/ttyUSB0" PORT="/dev/ttyUSB0"
LIBS="--libraries ./vendor/esp32-32e/TFT_eSPI" LIBS="--libraries ./vendor/esp32-32e/TFT_eSPI"
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM" OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLOCAL_SECRETS -DLOAD_GFXFF=1"

View File

@@ -20,3 +20,22 @@
// If using capacitive touch (e.g. FT6236), configure I2C pins here: // If using capacitive touch (e.g. FT6236), configure I2C pins here:
// #define TOUCH_SDA 21 // #define TOUCH_SDA 21
// #define TOUCH_SCL 22 // #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

View File

@@ -4,7 +4,14 @@
#include "DisplayDriverTFT.h" #include "DisplayDriverTFT.h"
#include "board_config.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" #include "secrets.h"
#else
#include <secrets.h>
#endif
#include <KlubhausCore.h> #include <KlubhausCore.h>

View File

@@ -58,6 +58,16 @@ int DisplayDriverGFX::width() { return _gfx ? _gfx->width() : DISP_W; }
int DisplayDriverGFX::height() { return _gfx ? _gfx->height() : DISP_H; } 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 // Transform touch coordinates to match display orientation
// GT911 touch panel on this board is rotated 180° relative to display // GT911 touch panel on this board is rotated 180° relative to display
void DisplayDriverGFX::transformTouch(int* x, int* y) { 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"); Serial.printf("[TEST] Injecting touch: (%d,%d) %s\n", x, y, pressed ? "press" : "release");
if(outX) *outX = x; if(outX)
if(outY) *outY = y; *outX = x;
if(outPressed) *outPressed = pressed; if(outY)
*outY = y;
if(outPressed)
*outPressed = pressed;
_testMode = true; _testMode = true;
return true; return true;
@@ -323,18 +336,19 @@ void DisplayDriverGFX::render(const ScreenState& state) {
void DisplayDriverGFX::drawBoot(const ScreenState& state) { void DisplayDriverGFX::drawBoot(const ScreenState& state) {
BootStage stage = state.bootStage; BootStage stage = state.bootStage;
_gfx->fillScreen(0x000000); _gfx->fillScreen(TFT_BLACK);
_gfx->setTextColor(0xFFFF); _gfx->setTextColor(STYLE_COLOR_FG);
_gfx->setTextSize(2); setTitleFont();
_gfx->setCursor(10, 10); _gfx->setCursor(STYLE_SPACING_X, STYLE_SPACING_Y);
_gfx->print("KLUBHAUS"); _gfx->print("KLUBHAUS");
_gfx->setTextSize(1); setBodyFont();
_gfx->setCursor(10, 50); _gfx->setCursor(STYLE_SPACING_X, STYLE_SPACING_Y + STYLE_HEADER_HEIGHT);
_gfx->print(BOARD_NAME); _gfx->print(BOARD_NAME);
// Show boot stage status // Show boot stage status
_gfx->setCursor(10, 80); setLabelFont();
_gfx->setCursor(STYLE_SPACING_X, STYLE_SPACING_Y + STYLE_HEADER_HEIGHT + 30);
switch(stage) { switch(stage) {
case BootStage::SPLASH: case BootStage::SPLASH:
_gfx->print("Initializing..."); _gfx->print("Initializing...");
@@ -363,30 +377,29 @@ void DisplayDriverGFX::drawAlert(const ScreenState& state) {
uint16_t bg = _gfx->color565(pulse, 0, 0); uint16_t bg = _gfx->color565(pulse, 0, 0);
_gfx->fillScreen(bg); _gfx->fillScreen(bg);
_gfx->setTextColor(0xFFFF, bg); _gfx->setTextColor(STYLE_COLOR_FG, bg);
_gfx->setTextSize(3); setTitleFont();
_gfx->setCursor(10, 20); _gfx->setCursor(STYLE_SPACING_X, STYLE_HEADER_HEIGHT);
_gfx->print(state.alertTitle.length() > 0 ? state.alertTitle.c_str() : "ALERT"); _gfx->print(state.alertTitle.length() > 0 ? state.alertTitle.c_str() : "ALERT");
_gfx->setTextSize(2); setBodyFont();
_gfx->setCursor(10, 80); _gfx->setCursor(STYLE_SPACING_X, STYLE_HEADER_HEIGHT + 50);
_gfx->print(state.alertBody); _gfx->print(state.alertBody);
_gfx->setTextSize(1); setLabelFont();
_gfx->setCursor(10, DISP_H - 20); _gfx->setCursor(STYLE_SPACING_X, DISP_H - STYLE_SPACING_Y);
_gfx->print("Hold to silence..."); _gfx->print("Hold to silence...");
} }
void DisplayDriverGFX::drawDashboard(const ScreenState& state) { void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
_gfx->fillScreen(0x001030); // Dark blue _gfx->fillScreen(STYLE_COLOR_BG);
// Header // Header
_gfx->fillRect(0, 0, DISP_W, 40, 0x1A1A); // Dark gray _gfx->fillRect(0, 0, DISP_W, STYLE_HEADER_HEIGHT, STYLE_COLOR_HEADER);
_gfx->setFont(&fonts::Font2); setBodyFont();
_gfx->setTextColor(0xFFFF); _gfx->setTextColor(STYLE_COLOR_FG);
_gfx->setTextSize(1); _gfx->setCursor(STYLE_SPACING_X, 12);
_gfx->setCursor(10, 12);
_gfx->printf("KLUBHAUS"); _gfx->printf("KLUBHAUS");
// WiFi status // WiFi status
@@ -407,15 +420,15 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
int h = lay.h; int h = lay.h;
// Tile background // Tile background
_gfx->fillRoundRect(x, y, w, h, 8, 0x0220); _gfx->fillRoundRect(x, y, w, h, STYLE_TILE_RADIUS, 0x0220);
// Tile border // Tile border
_gfx->drawRoundRect(x, y, w, h, 8, 0xFFFF); _gfx->drawRoundRect(x, y, w, h, STYLE_TILE_RADIUS, STYLE_COLOR_FG);
// Tile label // Tile label
_gfx->setTextColor(0xFFFF); _gfx->setTextColor(STYLE_COLOR_FG);
_gfx->setTextSize(2); setBodyFont();
_gfx->setCursor(x + w / 2 - 10, y + h / 2 - 10); _gfx->setCursor(x + w / 2 - 30, y + h / 2 - 10);
_gfx->print(tileLabels[i]); _gfx->print(tileLabels[i]);
} }
} }

View File

@@ -18,6 +18,12 @@ public:
int width() override; int width() override;
int height() override; int height() override;
// Fonts
void setTitleFont() override;
void setBodyFont() override;
void setLabelFont() override;
void setDefaultFont() override;
// Transform touch coordinates (handles rotated touch panels) // Transform touch coordinates (handles rotated touch panels)
void transformTouch(int* x, int* y) override; void transformTouch(int* x, int* y) override;

View File

@@ -1,4 +1,4 @@
FQBN="esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=enabled,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB" FQBN="esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=enabled,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB"
PORT="/dev/ttyACM0" PORT="/dev/ttyACM0"
LIBS="--libraries ~/Arduino/libraries/LovyanGFX" 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"

View File

@@ -38,3 +38,22 @@
// ── GT911 Touch ── // ── GT911 Touch ──
#define GT911_ADDR 0x5D #define GT911_ADDR 0x5D
// #define TOUCH_INT -1 // #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

View File

@@ -36,6 +36,12 @@ public:
virtual int width() = 0; virtual int width() = 0;
virtual int height() = 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) ── // ── Touch transform (for rotated panels) ──
virtual void transformTouch(int* x, int* y) { /* default: no transform */ } virtual void transformTouch(int* x, int* y) { /* default: no transform */ }
}; };

View File

@@ -7,3 +7,4 @@
#include "IDisplayDriver.h" #include "IDisplayDriver.h"
#include "NetManager.h" #include "NetManager.h"
#include "ScreenState.h" #include "ScreenState.h"
#include "Style.h"

View File

@@ -0,0 +1,62 @@
#pragma once
#include <cstdint>
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);
}
};

View File

@@ -215,4 +215,4 @@ echo "[OK] Generated .crush.json with FQBN: $FQBN"
run = "git add .; lumen draft | git commit -F - " run = "git add .; lumen draft | git commit -F - "
[env] [env]
BOARD = "esp32-s3-lcd-43" BOARD = "esp32-32e"