refactor(Style): Add font abstraction and CSS-like styling constants
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,3 +16,4 @@ vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino/
|
||||
*~
|
||||
.DS_Store
|
||||
compile_commands.json
|
||||
.cache/
|
||||
|
||||
83
AGENTS.md
83
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=<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 |
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -16,3 +16,22 @@
|
||||
|
||||
// Touch — XPT2046 configured in tft_user_setup.h
|
||||
// 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
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */ }
|
||||
};
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
#include "IDisplayDriver.h"
|
||||
#include "NetManager.h"
|
||||
#include "ScreenState.h"
|
||||
#include "Style.h"
|
||||
|
||||
62
libraries/KlubhausCore/src/Style.h
Normal file
62
libraries/KlubhausCore/src/Style.h
Normal 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user