Files
klubhaus-doorbell/AGENTS.md

544 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (`<cstdint>`, `<String>`, `<vector>`)
- 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 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 (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=<board>` or `FORCE=1 mise run <task> BOARD=<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=<board> 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 <command> --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
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**: <https://www.lcdwiki.com/4.0inch_ESP32-32E_Display>
| 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-instructions v2 -->
# 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 <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% |
| 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.
<!-- /rtk-instructions -->