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

View File

@@ -1,9 +1,86 @@
#include "DisplayDriverTFT.h"
#include <Arduino.h>
#include <KlubhausCore.h>
#include <TFT_eSPI.h>
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);

View File

@@ -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);
};

View File

@@ -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"

View File

@@ -15,4 +15,23 @@
#define PIN_LCD_BL 27
// 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 "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 <secrets.h>
#endif
#include <KlubhausCore.h>

View File

@@ -1,5 +1,85 @@
#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() {
// 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);

View File

@@ -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);
};

View File

@@ -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"

View File

@@ -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

View File

@@ -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 <secrets.h>
#endif
#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; }
// ── 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]);
}
}

View File

@@ -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;

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"
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"

View File

@@ -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