diff --git a/AGENTS.md b/AGENTS.md index 0d04635..5fe5097 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,455 +1,143 @@ # AGENTS.md — Klubhaus Doorbell -Multi-target Arduino/ESP32 doorbell alert system using ntfy.sh. +Multi-target Arduino/ESP32 doorbell alert system using ntfy.sh. Default BOARD: `esp32-s3-lcd-43`. -## Quick Reference - -**Default BOARD**: `esp32-s3-lcd-43` (set in mise.toml). To switch boards: +## Build Commands ```bash -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 -``` +# Set target board +mise set BOARD=esp32-32e-4 # ESP32-32E 4" (320x480 ST7796) +mise set BOARD=esp32-32e # ESP32-32E 3.5" (320x240 ILI9341) +mise set BOARD=esp32-s3-lcd-43 # ESP32-S3-Touch-LCD-4.3 (800x480 RGB) -Then run commands without `BOARD=` prefix: +# Core commands +mise run compile # compile for current BOARD +mise run upload # upload (auto-kills monitor first) +mise run monitor # start JSON monitor daemon +mise run kill # kill monitor/release serial port -```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 +# Formatting & cleanup +mise run format # format code with clang-format +mise run clean # remove build artifacts -# Install libs (run after cloning) -mise run install-libs-shared # shared libs (ArduinoJson, NTPClient) -mise run install # shared + board-specific libs -``` - -## 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 | `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 - -All commands run via **mise**: - -```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) -mise run install-libs-shared -mise run install # install shared + board-specific libs (requires BOARD env) - -# 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 - -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" - -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 -mise run clean # clean build artifacts -mise run kill # kill running monitor/upload for BOARD -mise run log-tail # tail colored logs (requires BOARD) -mise run cmd COMMAND=dashboard # send command to device (requires BOARD) -mise run state # show device state (requires BOARD) - -# LSP / IDE support -mise run gen-compile-commands # generate compile_commands.json for LSP -mise run gen-crush-config # generate .crush.json with BOARD-based FQBN - -# Arduino maintenance -mise run arduino-clean # clear Arduino CLI cache (staging + packages) - -# Raw serial access -mise run monitor-raw # raw serial monitor (arduino-cli) +# Debugging +mise run log-tail # tail colored logs +mise run cmd COMMAND=dashboard # send command to device +mise run state # show device state +mise run monitor-raw # raw serial monitor (115200 baud) mise run monitor-tio # show tio command for terminal UI + +# Install dependencies +mise run install-libs-shared # shared libs (ArduinoJson, NTPClient) +mise run install # shared + board-specific libs + +# LSP / IDE +mise run gen-compile-commands # generate compile_commands.json +mise run gen-crush-config # generate .crush.json for BOARD ``` -# Board Configuration Files +**Serial debug commands** (115200 baud): `alert`, `silence`, `dashboard`, `off`, `status`, `reboot` -Each board directory contains `board-config.sh` which defines: +**No unit tests exist** — verify changes by compiling and deploying to hardware. -| Variable | Description | -|----------|-------------| -| `FQBN` | Fully Qualified Board Name for arduino-cli | -| `PORT` | Serial port for upload/monitoring (default: `/dev/ttyUSB0`) | -| `LIBS` | Vendor library path for `--libraries` flag | -| `OPTS` | Additional compiler flags (e.g., `-DDEBUG_MODE`) | +## Code Style -**Example** (`boards/esp32-32e-4/board-config.sh`): - -```bash -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" -``` - -**Port override**: `mise set PORT=/dev/ttyXXX` before running upload/monitor commands. - -**Prerequisites**: arduino-cli with `esp32:esp32` platform installed, mise. - -## Project Structure - -```text -libraries/KlubhausCore/src/ Shared Arduino library -├── KlubhausCore.h Umbrella include (board sketches use this) -├── Config.h Constants, timing, WiFiCred struct -├── ScreenState.h State enums/structs -├── IDisplayDriver.h Pure virtual display interface -├── DisplayManager.h Thin wrapper delegating to IDisplayDriver -├── NetManager.* WiFi, HTTP, NTP -└── DoorbellLogic.* State machine, ntfy polling - -boards/ -├── esp32-32e/ -│ ├── esp32-32e.ino Main sketch -│ ├── board_config.h Board-specific config -│ ├── secrets.h.example WiFi creds template (copy to secrets.h) -│ ├── tft_user_setup.h TFT_eSPI config -│ └── DisplayDriverTFT.* Concrete IDisplayDriver for TFT -├── esp32-32e-4/ -│ ├── esp32-32e-4.ino Main sketch -│ ├── board_config.h Board-specific config -│ ├── secrets.h.example WiFi creds template (copy to secrets.h) -│ ├── tft_user_setup.h TFT_eSPI config -│ └── DisplayDriverTFT.* Concrete IDisplayDriver for TFT -└── esp32-s3-lcd-43/ - ├── esp32-s3-lcd-43.ino Main sketch - ├── board_config.h Board-specific config - ├── secrets.h.example WiFi creds template (copy to secrets.h) - ├── LovyanPins.h Pin definitions - └── DisplayDriverGFX.* Concrete IDisplayDriver for LovyanGFX - -vendor/ Vendored display libs (recreated by install-libs) -├── esp32-32e/TFT_eSPI/ -├── esp32-32e-4/TFT_eSPI/ -└── esp32-s3-lcd-43/LovyanGFX/ -``` - -## Code Patterns +### Formatting (.clang-format) +- BasedOnStyle: WebKit +- 4-space indentation, no tabs +- Column limit: 100 +- Opening brace on same line (`BreakBeforeBraces: Attach`) +- Run `mise run format` to format code ### Header Guards - Use `#pragma once` (not `#ifndef` guards). -### Formatting - -- 4-space indentation, no tabs -- WebKit-based style (see `.clang-format`) -- Column limit: 100 -- Opening brace stays on same line (`BreakBeforeBraces: Attach`) - ### Naming Conventions - -- Classes: `PascalCase` (e.g., `DisplayManager`, `IDisplayDriver`) -- Constants/enums: `SCREAMING_SNAKE` (e.g., `POLL_INTERVAL_MS`, `ScreenState::DASHBOARD`) -- Variables/functions: `camelCase` (e.g., `currentState`, `updateDisplay`) -- Member variables: prefix with `_` (e.g., `_screenWidth`, `_isConnected`) +| Type | Convention | Example | +|------|------------|---------| +| Classes | PascalCase | `DisplayManager`, `IDisplayDriver` | +| Constants/enums | SCREAMING_SNAKE | `POLL_INTERVAL_MS`, `ScreenState::DASHBOARD` | +| Variables/functions | camelCase | `currentState`, `updateDisplay` | +| Member variables | `_` prefix | `_screenWidth`, `_isConnected` | ### Types - -- Arduino types: `int`, `uint8_t`, `uint16_t`, `size_t` - Use fixed-width types for protocol/serialization (`uint8_t`, not `byte`) -- Avoid `bool` for pin states - use `uint8_t` or `int` - Use `size_t` for sizes and array indices +- Avoid `bool` for pin states — use `uint8_t` or `int` -### Imports Organization - -- Arduino core headers first (`Arduino.h`) -- Standard C/C++ library (``, ``, ``) -- Third-party libraries (e.g., `TFT_eSPI.h`, `ArduinoJson.h`) -- Local project headers (e.g., `"Config.h"`, `"ScreenState.h"`) +### Imports Organization (in order) +1. Arduino core (`Arduino.h`) +2. Standard C/C++ (``, ``, ``) +3. Third-party libs (`TFT_eSPI.h`, `ArduinoJson.h`) +4. Local project (`"Config.h"`, `"ScreenState.h"`) ### Error Handling +- Serial logging: `Serial.println("[ERROR] message")` +- Use `Serial.printf()` for formatted debug +- Return error codes, not exceptions +- Log state: `[STATE] → DASHBOARD` -- Serial logging pattern: `Serial.println("[ERROR] message")` -- Use `Serial.printf()` for formatted debug output -- Return error codes, not exceptions (exceptions not available in Arduino) -- Log state transitions with `[STATE] → STATE` tags +## Architecture Patterns -### Class Design - -- Pure virtual `IDisplayDriver` interface in shared library -- Each board implements a concrete driver (e.g., `DisplayDriverTFT`, `DisplayDriverGFX`) +### Display Driver Interface +- Pure virtual `IDisplayDriver` in shared `KlubhausCore` +- Each board implements concrete driver (`DisplayDriverTFT`, `DisplayDriverGFX`) - `DisplayManager` delegates to `IDisplayDriver` — no display-lib coupling in shared code ### Arduino Patterns - -- `setup()` runs once at boot — call `begin()` on managers -- `loop()` runs continuously — call `update()` on managers +- `setup()` — call `begin()` on managers +- `loop()` — call `update()` on managers - Use `millis()` for timing (not `delay()`) -- Serial console at 115200 baud for debug commands +- Serial baud: 115200 ### Style System +- Style constants in board's `board_config.h`: `STYLE_SPACING_X`, `STYLE_COLOR_BG`, etc. +- Font abstraction via `IDisplayDriver`: `setTitleFont()`, `setBodyFont()`, etc. +- Layout helpers in `KlubhausCore/src/Style.h` -The project uses a CSS-like styling system for consistent UI across different display sizes: +## Key Files -- **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 - -**No unit tests exist** - This is an embedded Arduino sketch. Verify changes by building and deploying to hardware: - -```bash -mise set BOARD=esp32-s3-lcd-43 # compile for esp32-s3-lcd-43 -mise set BOARD=esp32-32e-4 # compile for ESP32-32E-4" ``` +libraries/KlubhausCore/src/ +├── KlubhausCore.h # Umbrella include +├── Config.h # Timing, WiFiCred struct +├── ScreenState.h # State enums/structs +├── IDisplayDriver.h # Pure virtual interface +├── DisplayManager.h # Delegates to IDisplayDriver +├── NetManager.* # WiFi, HTTP, NTP +└── DoorbellLogic.* # State machine, ntfy polling -**Serial commands** (type into serial monitor at 115200 baud): - -| Command | Action | -|---------|--------| -| `alert` | Trigger a test alert | -| `silence` | Silence current alert | -| `dashboard` | Show dashboard screen | -| `off` | Turn off display | -| `status` | Print state + memory info | -| `reboot` | Restart device | - -**Touch Test Commands** (ESP32-S3-LCD-4.3 only): - -| Command | Action | -|---------|--------| -| `TEST:touch X Y press` | Inject synthetic press 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) | - -**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: - -- Display (100,140) → Raw (700, 340) -- Display (700,140) → Raw (100, 340) - -## Monitor Daemon and Logging - -The build system includes a Python-based monitor agent that provides JSON logging and command pipes. - -- **JSON log**: `/tmp/doorbell-$BOARD.jsonl` – each line is `{"ts":123.456,"line":"..."}` -- **State file**: `/tmp/doorbell-$BOARD-state.json` – current device state (screen, alert status, etc.) -- **Command FIFO**: `/tmp/doorbell-$BOARD-cmd.fifo` – send commands via `echo 'dashboard' > /tmp/doorbell-$BOARD-cmd.fifo` - -Commands to interact with the daemon: - -```bash -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 +boards/{BOARD}/ +├── {BOARD}.ino # Main sketch +├── board_config.h # Board-specific config +├── secrets.h # WiFi credentials +├── tft_user_setup.h # TFT_eSPI config (TFT boards) +└── DisplayDriver*.{h,cpp} # Concrete IDisplayDriver ``` -The monitor daemon automatically starts after upload and is killed before upload to avoid serial port conflicts. - ## Gotchas -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. +1. **secrets.h**: Boards with `-DLOCAL_SECRETS` use local `secrets.h`; others use `KlubhausCore/src/secrets.h` +2. **Vendored libs**: Each board links only its display lib — never TFT_eSPI + LovyanGFX together +3. **LSP errors**: Run `mise run gen-compile-commands` then restart LSP; build works regardless +4. **Serial port**: `upload`/`monitor` auto-depend on `kill` to release port +5. **State tags**: Use `[STATE] → DASHBOARD`, `[ADMIN]`, `[TOUCH]`, `[ALERT]` for monitor parsing - ```bash - # For boards without local secrets.h, the defaults will be used - # To use board-specific credentials, add -DLOCAL_SECRETS to OPTS in board-config.sh - ``` - -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. - -3. **No unit tests**: This is an embedded Arduino sketch — no test suite exists. Verify changes by building and deploying to hardware. - -4. **LSP errors are expected**: The LSP may show errors about missing Arduino types until `compile_commands.json` is generated. Run `mise run gen-compile-commands` first, then restart the LSP. The build works correctly via arduino-cli. - -5. **Build artifacts in board dirs**: Build output goes to `boards/[board]/build/` — cleaned by `mise run clean`. - -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. 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. - -9. **Monitor state parsing**: The monitor-agent parses serial output for state updates: - - `[STATE] → DASHBOARD` → updates state file to `"screen":"DASHBOARD"` - - `[STATE] → ALERT` → updates state file to `"screen":"ALERT"` - - `[STATE] → OFF` → updates state file to `"screen":"OFF"` - - `[STATE] → BOOT` → updates state file to `"screen":"BOOT"` - - `[ADMIN]` commands also update state accordingly - -10. **Serial baud rate**: All serial communication uses 115200 baud. - -11. **Serial port contention**: The `kill` task uses `fuser` to release the serial port. Both `upload` and `monitor` tasks depend on `kill` to ensure the port is free. - -## Config Constants - -Key timing and configuration values in `Config.h`: +## Config Constants (Config.h) | Constant | Default | Description | |----------|---------|-------------| -| `FW_VERSION` | "5.1" | Firmware version string | -| `POLL_INTERVAL_MS` | 15000 | How often to poll ntfy.sh for new messages | -| `HEARTBEAT_INTERVAL_MS` | 300000 | NTP sync interval (5 min) | -| `ALERT_TIMEOUT_MS` | 120000 | Auto-clear alert after 2 min | -| `INACTIVITY_TIMEOUT_MS` | 30000 | Turn off display after 30s of inactivity | -| `HOLD_TO_SILENCE_MS` | 3000 | Hold duration to silence alert | -| `LOOP_YIELD_MS` | 10 | Yield to prevent Task Watchdog (configurable) | -| `BOOT_GRACE_MS` | 5000 | Grace period at boot before polling starts | -| `SILENCE_DISPLAY_MS` | 10000 | How long to show silence confirmation | -| `WIFI_CONNECT_TIMEOUT_MS` | 15000 | WiFi connection timeout | +| `FW_VERSION` | "5.1" | Firmware version | +| `POLL_INTERVAL_MS` | 15000 | ntfy.sh poll interval | +| `ALERT_TIMEOUT_MS` | 120000 | Auto-clear alert | +| `INACTIVITY_TIMEOUT_MS` | 30000 | Display off timeout | +| `HOLD_TO_SILENCE_MS` | 3000 | Hold to silence | +| `WIFI_CONNECT_TIMEOUT_MS` | 15000 | WiFi timeout | | `HTTP_TIMEOUT_MS` | 10000 | HTTP request timeout | -| `HINT_ANIMATION_MS` | 2000 | Hint animation duration | -| `HINT_MIN_BRIGHTNESS` | 30 | Minimum brightness for hints | -| `HINT_MAX_BRIGHTNESS` | 60 | Maximum brightness for hints | -| `TOUCH_DEBOUNCE_MS` | 100 | Touch debounce delay | ## Screen States -The device operates in these states (defined in `ScreenState.h`): - - **BOOT** — Initializing -- **DASHBOARD** — Normal operation, showing status -- **ALERT** — Doorbell ring detected, display on -- **OFF** — Display backlight off (but polling continues) - -## Serial Output Tags - -The firmware outputs structured tags for the monitor agent: - -- `[STATE] → DASHBOARD/ALERT/OFF/BOOT` — State transitions -- `[ADMIN]` — Admin commands received (dashboard, off, alert, silence, status, reboot) -- `[TOUCH]` — Touch events (x, y, pressed/released) -- `[ALERT]` — Alert triggered - -## Reverted Changes Log - -Track changes that were reverted to avoid flapping: - -- 2025-02-18: Initially configured LSP via neovim/mason (`.config/nvim/lua/plugins/arduino.lua`) — user clarified they wanted Crush-native LSP config instead - -## Troubleshooting - -| Issue | Solution | -|-------|----------| -| "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 `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 | - -## Known Fixes - -- **dashboard/off admin commands reset inactivity timer** (DoorbellLogic.cpp:234,240) — Admin commands like `dashboard` and `off` now reset the inactivity timer so the display doesn't turn off immediately after switching screens. - -## Documentation Lookup Rule - -When uncertain about CLI tool flags or argument syntax: - -1. Run the tool with `-h` or `--help` first -2. If unclear, search for official documentation matching the tool version -3. Prefer checking the tool's own help/docs over guessing -4. For Arduino CLI, check `arduino-cli --help` or official Arduino CLI docs - -## LSP / IDE Configuration - -The project uses **clangd** for C++ via the `compile_commands.json` generated by arduino-cli: - -- `mise run gen-compile-commands` regenerates the compile database (automatically run as part of `compile`) -- The generated `compile_commands.json` includes all necessary includes, defines, and library paths for your BOARD - -**Generate compile_commands.json** for accurate IDE diagnostics: - -```bash -mise set BOARD=esp32-32e-4 -mise run gen-compile-commands -``` - -**Generate static .crush.json** for a specific board: - -```bash -mise set BOARD=esp32-32e-4 -mise run gen-crush-config -``` - -## Hardware Research Log - -### Hosyond ESP32-32E 4" (320x480) - Planned - -**Source**: - -| Spec | Value | -|------|-------| -| Display Controller | ST7796S | -| Resolution | 320x480 | -| Touch | XPT2046 (resistive) | -| Library | TFT_eSPI V2.5.43 (same as 32E) | - -**GPIO Pinout**: - -| Function | GPIO | -|----------|------| -| LCD CS | 15 | -| LCD DC | 2 | -| LCD MOSI | 13 | -| LCD SCLK | 14 | -| LCD RST | EN | -| LCD BL | 27 | -| Touch CS | 33 | -| Touch IRQ | 36 | - -**Quirks**: - -- SPI pins shared between LCD and touch -- Touch IRQ on IO36 (input-only) triggers LOW on touch -- Backlight on IO27 (HIGH = on) -- Common anode RGB LEDs on IO16, IO17, IO22 (LOW = on) - -# RTK (Rust Token Killer) - -RTK filters command output to reduce tokens. It's optional but recommended for saving tokens. - -## Installation Check - -```bash -which rtk # Check if installed -``` - -If installed, use it before git commands: -```bash -rtk git status -rtk git diff -rtk git add . -rtk git commit -m "message" -rtk git push -``` - -RTK passes through unchanged if no filter exists, so it's always safe to use. -| Package Managers | pnpm, npm, npx | 70-90% | -| Files | ls, read, grep, find | 60-75% | -| Infrastructure | docker, kubectl | 85% | -| Network | curl, wget | 65-70% | - -Overall average: **60-90% token reduction** on common development operations. - +- **DASHBOARD** — Normal operation +- **ALERT** — Doorbell ring detected +- **OFF** — Display backlight off (polling continues) diff --git a/boards/esp32-32e-4/DisplayDriverTFT.cpp b/boards/esp32-32e-4/DisplayDriverTFT.cpp index 35b4cf5..c56a797 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.cpp +++ b/boards/esp32-32e-4/DisplayDriverTFT.cpp @@ -259,6 +259,7 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) { // Get tile layouts from library helper int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP); + display.setHeaderHeight(STYLE_HEADER_HEIGHT); const TileLayout* layouts = display.getTileLayouts(); const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" }; @@ -377,6 +378,14 @@ TouchEvent DisplayDriverTFT::readTouch() { uint16_t tx, ty; uint8_t touched = _tft.getTouch(&tx, &ty, 100); + // Filter out invalid coordinates (touch panel can return garbage on release) + // Check against actual display dimensions (320x480 for this board) + bool validCoords = !(tx > 320 || ty > 480); + if(touched && !validCoords) { + touched = 0; + return evt; + } + // Debug: log touch state changes if(touched != _touchWasPressed) { Serial.printf("[TOUCH] raw touched=%d wasPressed=%d (x=%d,y=%d)\n", touched, diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp index 3a77220..729fe91 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp @@ -194,6 +194,15 @@ TouchEvent DisplayDriverGFX::readTouch() { int32_t x, y; bool pressed = _gfx->getTouch(&x, &y); + // Filter out invalid coordinates (touch panel can return garbage on release) + // Ignore both press and release transitions when coordinates are out of bounds + bool validCoords = !(x < 0 || x > DISP_W || y < 0 || y > DISP_H); + if(!validCoords) { + pressed = false; + // Don't update _lastTouch.pressed - keep previous state to avoid false release + return evt; + } + // Debounce: ignore repeated press events within debounce window after release unsigned long now = millis(); if(pressed && _touchBounced) { @@ -330,6 +339,13 @@ void DisplayDriverGFX::render(const ScreenState& state) { _needsRedraw = false; } break; + + case ScreenID::STATUS: + if(_needsRedraw) { + drawStatus(state); + _needsRedraw = false; + } + break; } } @@ -429,11 +445,13 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) { // Get tile layouts from library helper int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP); + display.setHeaderHeight(STYLE_HEADER_HEIGHT); const TileLayout* layouts = display.getTileLayouts(); - const char* tileLabels[] = { "1", "2", "3", "4", "5", "6", "7", "8" }; + const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" }; + const uint16_t tileColors[] = { 0x0280, 0x0400, 0x0440, 0x0100 }; - for(int i = 0; i < tileCount && i < 8; i++) { + for(int i = 0; i < tileCount && i < 4; i++) { const TileLayout& lay = layouts[i]; int x = lay.x; int y = lay.y; @@ -441,7 +459,7 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) { int h = lay.h; // Tile background - _gfx->fillRoundRect(x, y, w, h, STYLE_TILE_RADIUS, 0x0220); + _gfx->fillRoundRect(x, y, w, h, STYLE_TILE_RADIUS, tileColors[i]); // Tile border _gfx->drawRoundRect(x, y, w, h, STYLE_TILE_RADIUS, STYLE_COLOR_FG); @@ -449,11 +467,114 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) { // Tile label _gfx->setTextColor(STYLE_COLOR_FG); setBodyFont(); - _gfx->setCursor(x + w / 2 - 30, y + h / 2 - 10); + int textLen = strlen(tileLabels[i]); + int textW = textLen * 14; + _gfx->setCursor(x + w / 2 - textW / 2, y + h / 2 - 10); _gfx->print(tileLabels[i]); } } +void DisplayDriverGFX::drawStatus(const ScreenState& st) { + _gfx->fillScreen(STYLE_COLOR_BG); + + // Header with title and back button + _gfx->fillRect(0, 0, DISP_W, STYLE_HEADER_HEIGHT, STYLE_COLOR_HEADER); + Layout header = Layout::header(DISP_W, STYLE_HEADER_HEIGHT); + Layout safeText = header.padded(STYLE_SPACING_X); + + setTitleFont(); + _gfx->setTextColor(STYLE_COLOR_FG); + _gfx->setCursor(safeText.x, safeText.y + 4); + _gfx->print("STATUS"); + + // Back button in lower-right + setLabelFont(); + _gfx->setCursor(DISP_W - 60, DISP_H - 20); + _gfx->print("[BACK]"); + + // Status items in 2-column layout + setBodyFont(); + int colWidth = DISP_W / 2; + int startY = STYLE_HEADER_HEIGHT + 30; + int rowHeight = 35; + int labelX = STYLE_SPACING_X + 10; + int valueX = STYLE_SPACING_X + 120; + + // Column 1 + int y = startY; + + // WiFi SSID + _gfx->setTextColor(0x8888); + _gfx->setCursor(labelX, y); + _gfx->print("WiFi:"); + _gfx->setTextColor(STYLE_COLOR_FG); + _gfx->setCursor(valueX, y); + _gfx->print(st.wifiSsid.length() > 0 ? st.wifiSsid.c_str() : "N/A"); + + // RSSI + y += rowHeight; + _gfx->setTextColor(0x8888); + _gfx->setCursor(labelX, y); + _gfx->print("Signal:"); + _gfx->setTextColor(STYLE_COLOR_FG); + _gfx->setCursor(valueX, y); + _gfx->printf("%d dBm", st.wifiRssi); + + // IP Address + y += rowHeight; + _gfx->setTextColor(0x8888); + _gfx->setCursor(labelX, y); + _gfx->print("IP:"); + _gfx->setTextColor(STYLE_COLOR_FG); + _gfx->setCursor(valueX, y); + _gfx->print(st.ipAddr.length() > 0 ? st.ipAddr.c_str() : "N/A"); + + // Column 2 + y = startY; + + // Uptime + uint32_t upSec = st.uptimeMs / 1000; + uint32_t upMin = upSec / 60; + uint32_t upHr = upMin / 60; + upSec = upSec % 60; + upMin = upMin % 60; + _gfx->setTextColor(0x8888); + _gfx->setCursor(colWidth + labelX, y); + _gfx->print("Uptime:"); + _gfx->setTextColor(STYLE_COLOR_FG); + _gfx->setCursor(colWidth + valueX, y); + _gfx->printf("%02lu:%02lu:%02lu", upHr, upMin, upSec); + + // Heap + y += rowHeight; + _gfx->setTextColor(0x8888); + _gfx->setCursor(colWidth + labelX, y); + _gfx->print("Heap:"); + _gfx->setTextColor(STYLE_COLOR_FG); + _gfx->setCursor(colWidth + valueX, y); + _gfx->printf("%d KB", ESP.getFreeHeap() / 1024); + + // Last Poll + y += rowHeight; + _gfx->setTextColor(0x8888); + _gfx->setCursor(colWidth + labelX, y); + _gfx->print("Last Poll:"); + _gfx->setTextColor(STYLE_COLOR_FG); + _gfx->setCursor(colWidth + valueX, y); + uint32_t pollAgo = (millis() - st.lastPollMs) / 1000; + if(pollAgo < 60) { + _gfx->printf("%lu sec", pollAgo); + } else { + _gfx->printf("%lu min", pollAgo / 60); + } + + // Footer with firmware version + setLabelFont(); + _gfx->setTextColor(0x6666); + _gfx->setCursor(STYLE_SPACING_X, DISP_H - STYLE_SPACING_Y); + _gfx->print("v" FW_VERSION); +} + void DisplayDriverGFX::drawDebugTouch(int x, int y) { if(!_gfx) return; diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h index 45f6320..6829234 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h @@ -42,6 +42,7 @@ private: void drawBoot(const ScreenState& state); void drawAlert(const ScreenState& state); void drawDashboard(const ScreenState& state); + void drawStatus(const ScreenState& state); // Touch handling TouchEvent _lastTouch = { false, false, 0, 0, -1, -1 }; diff --git a/libraries/KlubhausCore/src/DisplayManager.h b/libraries/KlubhausCore/src/DisplayManager.h index 1df0546..2b08308 100644 --- a/libraries/KlubhausCore/src/DisplayManager.h +++ b/libraries/KlubhausCore/src/DisplayManager.h @@ -166,7 +166,7 @@ public: _drv->transformTouch(&dx, &dy); _drv->transformTouch(&cx, &cy); - int headerH = 30; + int headerH = _headerHeight; int cellW = _drv->width() / _gridCols; int cellH = (_drv->height() - headerH) / _gridRows; @@ -184,6 +184,8 @@ public: /// Handle dashboard touch - returns action for tapped tile, or NONE TileAction handleDashboardTouch(int x, int y) const { HitResult hr = hitTest(x, y); + Serial.printf("[HIT] x=%d y=%d type=%d idx=%d _headerHeight=%d\n", x, y, (int)hr.type, + hr.index, _headerHeight); if(hr.type == UIElementType::TILE && hr.index >= 0 && hr.index < DASHBOARD_TILE_COUNT) { return DASHBOARD_TILES[hr.index].action; }