Files
klubhaus-doorbell/AGENTS.md

456 lines
17 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
**Default BOARD**: `esp32-s3-lcd-43` (set in mise.toml). To switch boards:
```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
```
Then run commands without `BOARD=` prefix:
```bash
mise run compile # compile
mise run upload # upload (auto-kills monitor)
mise run monitor # start JSON monitor daemon
mise run log-tail # watch colored logs
mise run cmd COMMAND=dashboard # send command
mise run state # show device state
# Install libs (run after cloning)
mise run install-libs-shared # shared libs (ArduinoJson, NTPClient)
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)
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**: `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
### 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
### 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
**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"
```
**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
```
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.
```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`:
| 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` 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 <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
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**: <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)
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.
<!-- /rtk-instructions -->