Compare commits

...

5 Commits

23 changed files with 628 additions and 308 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/

246
AGENTS.md
View File

@@ -4,53 +4,66 @@ 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
All commands run via **mise**: All commands run via **mise**:
```bash ```bash
# Running multiple tasks - use && to chain them
mise run compile && mise run upload && mise run monitor
# Install all dependencies (shared libs + vendored display libs) # Install all dependencies (shared libs + vendored display libs)
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 +105,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.
@@ -188,13 +201,21 @@ Use `#pragma once` (not `#ifndef` guards).
- Use `millis()` for timing (not `delay()`) - Use `millis()` for timing (not `delay()`)
- Serial console at 115200 baud for debug commands - Serial console at 115200 baud for debug commands
### Style System
The project uses a CSS-like styling system for consistent UI across different display sizes:
- **Style constants** in each board's `board_config.h`: `STYLE_SPACING_X`, `STYLE_HEADER_HEIGHT`, `STYLE_COLOR_BG`, etc.
- **Font abstraction** via `IDisplayDriver` methods: `setTitleFont()`, `setBodyFont()`, `setLabelFont()`, `setDefaultFont()`
- **Layout helpers** in `KlubhausCore/src/Style.h`: `Layout` and `TileMetrics` structs
## Testing/Debugging ## Testing/Debugging
**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):
@@ -216,6 +237,12 @@ BOARD=esp32-32e-4 mise run compile # compile for ESP32-32E-4"
| `TEST:touch X Y release` | Inject synthetic release at raw panel coords (X,Y) | | `TEST:touch X Y release` | Inject synthetic release at raw panel coords (X,Y) |
| `TEST:touch clear` | Clear test mode (required between touch sequences) | | `TEST:touch clear` | Clear test mode (required between touch sequences) |
**Debug Features** (only when `DEBUG_MODE` is enabled in board-config.sh):
| Feature | Description |
|---------|-------------|
| Red crosshair | Draws a red crosshair at exact touch coordinates on tap (DEBUG_MODE only) |
Note: The S3 touch panel is rotated 180° relative to the display. Use raw panel coordinates: Note: The S3 touch panel is rotated 180° relative to the display. Use raw panel coordinates:
- Display (100,140) → Raw (700, 340) - Display (100,140) → Raw (700, 340)
@@ -232,22 +259,22 @@ 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.
## Gotchas ## Gotchas
1. **secrets.h is gitignored**: Copy from `.example` before building: 1. **secrets.h can be shared**: KlubhausCore/src/secrets.h provides default credentials. Boards with `LOCAL_SECRETS` defined in board-config.sh will use their local `secrets.h` instead.
```bash ```bash
cp boards/esp32-32e/secrets.h.example boards/esp32-32e/secrets.h # For boards without local secrets.h, the defaults will be used
cp boards/esp32-32e-4/secrets.h.example boards/esp32-32e-4/secrets.h # To use board-specific credentials, add -DLOCAL_SECRETS to OPTS in board-config.sh
cp boards/esp32-s3-lcd-43/secrets.h.example boards/esp32-s3-lcd-43/secrets.h
``` ```
2. **Display libs are vendored**: Each board uses a different display library. The build system uses `--libraries` to link only the board's vendored lib — never link both TFT_eSPI and LovyanGFX in the same build. 2. **Display libs are vendored**: Each board uses a different display library. The build system uses `--libraries` to link only the board's vendored lib — never link both TFT_eSPI and LovyanGFX in the same build.
@@ -260,7 +287,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 +352,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 +382,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
@@ -397,143 +426,26 @@ BOARD=esp32-32e-4 mise run gen-crush-config
- Backlight on IO27 (HIGH = on) - Backlight on IO27 (HIGH = on)
- Common anode RGB LEDs on IO16, IO17, IO22 (LOW = on) - Common anode RGB LEDs on IO16, IO17, IO22 (LOW = on)
<!-- rtk-instructions v2 --> <!-- rtk-instructions v2 -->
# RTK (Rust Token Killer) - Token-Optimized Commands # RTK (Rust Token Killer)
## Golden Rule RTK filters command output to reduce tokens. It's optional but recommended for saving tokens.
**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use. ## Installation Check
**Important**: Even in command chains with `&&`, use `rtk`:
```bash ```bash
# ❌ Wrong which rtk # Check if installed
git add . && git commit -m "msg" && git push
# ✅ Correct
rtk git add . && rtk git commit -m "msg" && rtk git push
``` ```
## RTK Commands by Workflow If installed, use it before git commands:
### Build & Compile (80-90% savings)
```bash ```bash
rtk cargo build # Cargo build output rtk git status
rtk cargo check # Cargo check output rtk git diff
rtk cargo clippy # Clippy warnings grouped by file (80%) rtk git add .
rtk tsc # TypeScript errors grouped by file/code (83%) rtk git commit -m "message"
rtk lint # ESLint/Biome violations grouped (84%) rtk git push
rtk prettier --check # Files needing format only (70%)
rtk next build # Next.js build with route metrics (87%)
``` ```
### Test (90-99% savings) RTK passes through unchanged if no filter exists, so it's always safe to use.
```bash
rtk cargo test # Cargo test failures only (90%)
rtk vitest run # Vitest failures only (99.5%)
rtk playwright test # Playwright failures only (94%)
rtk test <cmd> # Generic test wrapper - failures only
```
### Git (59-80% savings)
```bash
rtk git status # Compact status
rtk git log # Compact log (works with all git flags)
rtk git diff # Compact diff (80%)
rtk git show # Compact show (80%)
rtk git add # Ultra-compact confirmations (59%)
rtk git commit # Ultra-compact confirmations (59%)
rtk git push # Ultra-compact confirmations
rtk git pull # Ultra-compact confirmations
rtk git branch # Compact branch list
rtk git fetch # Compact fetch
rtk git stash # Compact stash
rtk git worktree # Compact worktree
```
Note: Git passthrough works for ALL subcommands, even those not explicitly listed.
### GitHub (26-87% savings)
```bash
rtk gh pr view <num> # Compact PR view (87%)
rtk gh pr checks # Compact PR checks (79%)
rtk gh run list # Compact workflow runs (82%)
rtk gh issue list # Compact issue list (80%)
rtk gh api # Compact API responses (26%)
```
### JavaScript/TypeScript Tooling (70-90% savings)
```bash
rtk pnpm list # Compact dependency tree (70%)
rtk pnpm outdated # Compact outdated packages (80%)
rtk pnpm install # Compact install output (90%)
rtk npm run <script> # Compact npm script output
rtk npx <cmd> # Compact npx command output
rtk prisma # Prisma without ASCII art (88%)
```
### Files & Search (60-75% savings)
```bash
rtk ls <path> # Tree format, compact (65%)
rtk read <file> # Code reading with filtering (60%)
rtk grep <pattern> # Search grouped by file (75%)
rtk find <pattern> # Find grouped by directory (70%)
```
### Analysis & Debug (70-90% savings)
```bash
rtk err <cmd> # Filter errors only from any command
rtk log <file> # Deduplicated logs with counts
rtk json <file> # JSON structure without values
rtk deps # Dependency overview
rtk env # Environment variables compact
rtk summary <cmd> # Smart summary of command output
rtk diff # Ultra-compact diffs
```
### Infrastructure (85% savings)
```bash
rtk docker ps # Compact container list
rtk docker images # Compact image list
rtk docker logs <c> # Deduplicated logs
rtk kubectl get # Compact resource list
rtk kubectl logs # Deduplicated pod logs
```
### Network (65-70% savings)
```bash
rtk curl <url> # Compact HTTP responses (70%)
rtk wget <url> # Compact download output (65%)
```
### Meta Commands
```bash
rtk gain # View token savings statistics
rtk gain --history # View command history with savings
rtk discover # Analyze Claude Code sessions for missed RTK usage
rtk proxy <cmd> # Run command without filtering (for debugging)
rtk init --global # Add RTK to ~/.claude/CLAUDE.md
```
rtk init # Add RTK instructions to CLAUDE.md
## Token Savings Overview
| Category | Commands | Typical Savings |
|----------|----------|-----------------|
| Tests | vitest, playwright, cargo test | 90-99% |
| Build | next, tsc, lint, prettier | 70-87% |
| Git | status, log, diff, add, commit | 59-80% |
| GitHub | gh pr, gh run, gh issue | 26-87% |
| Package Managers | pnpm, npm, npx | 70-90% | | Package Managers | pnpm, npm, npx | 70-90% |
| Files | ls, read, grep, find | 60-75% | | Files | ls, read, grep, find | 60-75% |
| Infrastructure | docker, kubectl | 85% | | Infrastructure | docker, kubectl | 85% |

77
SESSION_NOTES.md Normal file
View File

@@ -0,0 +1,77 @@
# Session Notes - 2026-02-19
## What's Been Done
### Build System
- Fixed mise.toml compile task (single quotes, proper shell escaping)
- Added upload task back
- All 3 boards now compile successfully
### Code Changes
- Added font abstraction (`setTitleFont`, `setBodyFont`, `setLabelFont`, `setDefaultFont`)
- Added CSS-like styling constants (`STYLE_*` in board_config.h)
- Added Layout helpers in `KlubhausCore/src/Style.h`
- Added `drawDebugTouch()` for red crosshair debug feature
- Added test harness for touch injection (`TEST:touch` commands)
- Fixed dashboard tile layout to use STYLE constants
### Neovim Integration
- Added keymaps in `~/.config/nvim/lua/config/keymaps.lua`
- Board auto-detection from current file path
- Notifications via `vim.notify()` on task completion/failure
- Keybindings: `<leader>mc`, `<leader>mu`, `<leader>mm`, `<leader>ma`, `<leader>mk`
### Documentation
- Updated AGENTS.md with Style System section
- Updated RTK section (slimmed down)
- Added note about multiple mise tasks with `&&`
## Current Issues / TODO
### High Priority
- [ ] Test touch coordinates on esp32-s3-lcd-43 - crosshair should show where user taps
- [ ] Debug why tiles overlap header (should be fixed with STYLE_HEADER_HEIGHT)
### Medium Priority
- [ ] Add theme support (dark/light mode)
- [ ] Fix esp32-32e and esp32-32e-4 dashboard rendering
### Low Priority
- [ ] Add more tile labels for esp32-s3-lcd-43 (currently just "1", "2", "3"...)
## Board Status
| Board | Display | Build | Notes |
|-------|---------|-------|-------|
| esp32-s3-lcd-43 | 800x480 | ✅ 35% | Main development board |
| esp32-32e-4 | 320x480 | ✅ 83% | |
| esp32-32e | 320x240 | ✅ 82% | |
## Style Constants Reference
### Spacing (in board_config.h)
- `STYLE_SPACING_X` - Base horizontal margin
- `STYLE_SPACING_Y` - Base vertical margin
- `STYLE_HEADER_HEIGHT` - Header bar height
- `STYLE_TILE_GAP` - Gap between tiles
- `STYLE_TILE_PADDING` - Tile internal padding
- `STYLE_TILE_RADIUS` - Tile border radius
### Colors
- `STYLE_COLOR_BG` - Screen background
- `STYLE_COLOR_HEADER` - Header background
- `STYLE_COLOR_FG` - Primary text color
- `STYLE_COLOR_ALERT` - Alert screen
- `STYLE_COLOR_TILE_1` through `STYLE_COLOR_TILE_4` - Tile colors
## Useful Commands
```bash
# Compile and upload
mise run compile && mise run upload && mise run monitor
# Debug touch (in serial monitor)
TEST:touch X Y press
TEST:touch X Y release
TEST:touch clear
```

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,17 +210,18 @@ 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(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP);
const TileLayout* layouts = display.getTileLayouts(); const TileLayout* layouts = display.getTileLayouts();
const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" }; const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" };
@@ -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);
@@ -239,11 +348,3 @@ HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) {
} }
return h; return h;
} }
void DisplayDriverTFT::updateHint(int x, int y, bool active) {
float period = active ? 500.0f : 2000.0f;
float t = fmodf(millis(), period) / period;
uint8_t v = static_cast<uint8_t>(30.0f + 30.0f * sinf(t * 2.0f * PI));
uint16_t col = _tft.color565(v, v, v);
_tft.drawRect(x - 40, y - 20, 80, 40, col);
}

View File

@@ -12,13 +12,18 @@ public:
void render(const ScreenState& state) override; void render(const ScreenState& state) override;
TouchEvent readTouch() override; TouchEvent readTouch() override;
HoldState updateHold(unsigned long holdMs) override; HoldState updateHold(unsigned long holdMs) override;
void updateHint(int x, int y, bool active) override;
int width() override { return _tft.width(); } int width() override { return _tft.width(); }
int height() override { return _tft.height(); } int height() override { return _tft.height(); }
// 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 +41,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);
@@ -219,10 +327,8 @@ HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) {
return h; return h;
} }
void DisplayDriverTFT::updateHint(int x, int y, bool active) { void DisplayDriverTFT::drawDebugTouch(int x, int y) {
float period = active ? 500.0f : 2000.0f; const int size = 20;
float t = fmodf(millis(), period) / period; _tft.drawLine(x - size, y, x + size, y, TFT_RED);
uint8_t v = static_cast<uint8_t>(30.0f + 30.0f * sinf(t * 2.0f * PI)); _tft.drawLine(x, y - size, x, y + size, TFT_RED);
uint16_t col = _tft.color565(v, v, v);
_tft.drawRect(x - 40, y - 20, 80, 40, col);
} }

View File

@@ -11,12 +11,20 @@ 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;
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;
// Debug
void drawDebugTouch(int x, int y) 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 +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/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
@@ -394,7 +407,7 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
_gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF"); _gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF");
// Get tile layouts from library helper // Get tile layouts from library helper
int tileCount = display.calculateDashboardLayouts(30, 8); int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP);
const TileLayout* layouts = display.getTileLayouts(); const TileLayout* layouts = display.getTileLayouts();
const char* tileLabels[] = { "1", "2", "3", "4", "5", "6", "7", "8" }; const char* tileLabels[] = { "1", "2", "3", "4", "5", "6", "7", "8" };
@@ -407,34 +420,24 @@ 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]);
} }
} }
void DisplayDriverGFX::updateHint(int x, int y, bool active) { void DisplayDriverGFX::drawDebugTouch(int x, int y) {
if(!_gfx) if(!_gfx)
return; return;
static uint32_t lastTime = 0; const int size = 20;
uint32_t now = millis(); _gfx->drawLine(x - size, y, x + size, y, TFT_RED);
if(now - lastTime < 100) _gfx->drawLine(x, y - size, x, y + size, TFT_RED);
return;
lastTime = now;
// active=true: faster pulse (500ms), active=false: slower pulse (2000ms)
float period = active ? 500.0f : 2000.0f;
float t = fmodf(now, period) / period;
uint8_t v = static_cast<uint8_t>(30.0f + 30.0f * sinf(t * 2.0f * 3.14159f));
uint16_t col = _gfx->color565(v, v, v);
_gfx->drawCircle(x, y, 50, col);
} }

View File

@@ -13,11 +13,17 @@ public:
TouchEvent readTouch() override; TouchEvent readTouch() override;
HoldState updateHold(unsigned long holdMs) override; HoldState updateHold(unsigned long holdMs) override;
void updateHint(int x, int y, bool active) override; void drawDebugTouch(int x, int y) override;
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 45
#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

@@ -139,51 +139,9 @@ public:
HoldState updateHold(unsigned long ms) { return _drv ? _drv->updateHold(ms) : HoldState {}; } HoldState updateHold(unsigned long ms) { return _drv ? _drv->updateHold(ms) : HoldState {}; }
void updateHint(int x, int y, bool active) { void drawDebugTouch(int x, int y) {
if(_drv) if(_drv)
_drv->updateHint(x, y, active); _drv->drawDebugTouch(x, y);
}
/// Show touch feedback - highlights the tile at given coordinates
/// Returns true if a valid tile is being touched
bool showTouchFeedback(int x, int y) {
if(!_drv || _gridCols <= 0)
return false;
// Transform touch coordinates
_drv->transformTouch(&x, &y);
int headerH = 30;
if(y < headerH)
return false;
// Calculate which cell
int cellW = _drv->width() / _gridCols;
int cellH = (_drv->height() - headerH) / _gridRows;
int col = x / cellW;
int row = (y - headerH) / cellH;
if(col < 0 || col >= _gridCols || row < 0 || row >= _gridRows)
return false;
// Find which tile is at this position
for(int i = 0; i < _tileCount; i++) {
const TileLayout& lay = _layouts[i];
if(lay.col <= col && col < lay.col + lay.cols && lay.row <= row
&& lay.row + lay.rows > row) {
// Found the tile - draw highlight via driver
_drv->updateHint(lay.x, lay.y, true); // active=true means show feedback
return true;
}
}
return false;
}
/// Clear touch feedback
void clearTouchFeedback() {
if(_drv)
_drv->updateHint(0, 0, false); // active=false means clear
} }
/// Check if current position is still in same tile as touch-down /// Check if current position is still in same tile as touch-down

View File

@@ -330,18 +330,17 @@ int DoorbellLogic::handleTouch(const TouchEvent& evt) {
return -1; return -1;
} }
// Show touch feedback on press // Draw debug crosshair at touch point
#ifdef DEBUG_MODE
if(_state.screen == ScreenID::DASHBOARD) { if(_state.screen == ScreenID::DASHBOARD) {
_display->showTouchFeedback(evt.x, evt.y); _display->drawDebugTouch(evt.x, evt.y);
} }
#endif
return -1; return -1;
} }
// Handle release - fire action if same tile // Handle release - fire action if same tile
if(evt.released) { if(evt.released) {
// Clear feedback
_display->clearTouchFeedback();
if(_state.screen == ScreenID::DASHBOARD) { if(_state.screen == ScreenID::DASHBOARD) {
// Only fire action if finger stayed on same tile // Only fire action if finger stayed on same tile
if(evt.downX >= 0 && _display->isSameTile(evt.downX, evt.downY, evt.x, evt.y)) { if(evt.downX >= 0 && _display->isSameTile(evt.downX, evt.downY, evt.x, evt.y)) {
@@ -405,10 +404,6 @@ bool DoorbellLogic::updateHold(const TouchEvent& evt) {
holdStartY = evt.y; holdStartY = evt.y;
} }
if(holdStartX >= 0) {
_display->updateHint(holdStartX, holdStartY, h.active);
}
return false; return false;
} }

View File

@@ -32,10 +32,16 @@ public:
// ── Touch ── // ── Touch ──
virtual TouchEvent readTouch() = 0; virtual TouchEvent readTouch() = 0;
virtual HoldState updateHold(unsigned long holdMs) = 0; virtual HoldState updateHold(unsigned long holdMs) = 0;
virtual void updateHint(int x, int y, bool active) = 0; virtual void drawDebugTouch(int x, int y) { /* default: no-op */ }
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

@@ -21,18 +21,18 @@ pkl = "latest"
[tasks.compile] [tasks.compile]
description = "Compile (uses BOARD env var)" description = "Compile (uses BOARD env var)"
depends = ["gen-compile-commands"] depends = ["gen-compile-commands"]
run = """ run = '''
source ./boards/$BOARD/board-config.sh source ./boards/$BOARD/board-config.sh
arduino-cli compile --fqbn "$FQBN" --libraries ./libraries $LIBS --build-property "compiler.cpp.extra_flags=$OPTS" --warnings default ./boards/$BOARD arduino-cli compile --fqbn "$FQBN" --libraries ./libraries $LIBS --build-property "compiler.cpp.extra_flags=$OPTS" --warnings default ./boards/$BOARD
""" '''
[tasks.upload] [tasks.upload]
description = "Upload (uses BOARD env var)" description = "Upload (uses BOARD env var)"
depends = ["compile", "kill"] depends = ["kill"]
run = """ run = '''
source ./boards/$BOARD/board-config.sh source ./boards/$BOARD/board-config.sh
arduino-cli upload --fqbn $FQBN --port $PORT ./boards/$BOARD arduino-cli upload --fqbn "$FQBN" --port "$PORT" ./boards/$BOARD
""" '''
[tasks.monitor-raw] [tasks.monitor-raw]
depends = ["kill"] depends = ["kill"]
@@ -86,7 +86,7 @@ exit 0
description = "Monitor agent with JSON log + command pipe (Python-based)" description = "Monitor agent with JSON log + command pipe (Python-based)"
depends = ["kill"] depends = ["kill"]
run = """ run = """
python3 ./scripts/monitor-agent.py "$BOARD" python3 ./scripts/monitor-agent.py "$BOARD" &
""" """
[tasks.log-tail] [tasks.log-tail]
@@ -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-4"