# AGENTS.md — Klubhaus Doorbell Multi-target Arduino/ESP32 doorbell alert system using ntfy.sh. ## Quick Reference ```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 # 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 ``` **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` | ## Essential Commands All commands run via **mise**: ```bash # 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 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 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" 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 # 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) mise run monitor-tio # show tio command for terminal UI ``` # Board Configuration Files Each board directory contains `board-config.sh` which defines: | 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`) | **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**: `PORT=/dev/ttyXXX BOARD=esp32-32e mise run upload` **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 ### 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`) ### 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 ### 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"`) ### Error Handling - 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 ### Class Design - Pure virtual `IDisplayDriver` interface in shared library - Each board implements a concrete driver (e.g., `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 - Use `millis()` for timing (not `delay()`) - Serial console at 115200 baud for debug commands ## Testing/Debugging **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" ``` **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) | 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 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 ``` The monitor daemon automatically starts after upload and is killed before upload to avoid serial port conflicts. ## Gotchas 1. **secrets.h is gitignored**: Copy from `.example` before building: ```bash cp boards/esp32-32e/secrets.h.example boards/esp32-32e/secrets.h cp boards/esp32-32e-4/secrets.h.example boards/esp32-32e-4/secrets.h 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. 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 cannot find `Arduino.h` because it requires arduino-cli to resolve. Ignore clangd/IntelliSense errors about missing Arduino types; builds work 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 (e.g., `BOARD=esp32-32e mise run compile`). The default BOARD is `esp32-s3-lcd-43` (set in mise.toml). 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`: | 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 | | `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 BOARD=` or `FORCE=1 mise run BOARD=` | | 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= mise run install` | | LSP shows errors but build works | LSP cannot resolve Arduino types without arduino-cli; ignore clangd errors | | 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++ and arduino-language-server for Arduino. The `.crush.json` contains dynamic FQBN resolution: ```json { "lsp": { "arduino": { "command": "arduino-language-server", "args": ["-fqbn", "$(cat boards/${BOARD:-esp32-32e-4}/board-config.sh | grep '^FQBN=' | cut -d'\"' -f2)"] }, "cpp": { "command": "clangd" } } } ``` **Generate compile_commands.json** for accurate IDE diagnostics: ```bash 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 ``` ## 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) - Token-Optimized Commands ## Golden Rule **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. **Important**: Even in command chains with `&&`, use `rtk`: ```bash # ❌ Wrong git add . && git commit -m "msg" && git push # ✅ Correct rtk git add . && rtk git commit -m "msg" && rtk git push ``` ## RTK Commands by Workflow ### Build & Compile (80-90% savings) ```bash rtk cargo build # Cargo build output rtk cargo check # Cargo check output rtk cargo clippy # Clippy warnings grouped by file (80%) rtk tsc # TypeScript errors grouped by file/code (83%) rtk lint # ESLint/Biome violations grouped (84%) rtk prettier --check # Files needing format only (70%) rtk next build # Next.js build with route metrics (87%) ``` ### Test (90-99% savings) ```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 # 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 # 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