Files
klubhaus-doorbell/AGENTS.md

344 lines
13 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-32e-4` (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
```
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 WiFi creds (gitignored, copy from .example)
│ ├── 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 WiFi creds (gitignored, copy from .example)
│ ├── 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 WiFi creds (gitignored, copy from .example)
├── 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`)
### 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
**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 |
## 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-32e-4` (set in mise.toml).
8. **Lockfile system**: The build system uses lockfiles (`/tmp/doorbell-$BOARD.lock`) to prevent concurrent upload/monitor operations. Use `mise run kill` to clean up stuck processes.
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. **Lockfile mechanism**: The build system uses `/tmp/doorbell-$BOARD.lock` to prevent concurrent operations. The lockfile script (`scripts/lockfile.sh`) provides `acquire_lock()` and `release_lock()` functions. Use `FORCE=1` to override locks.
## 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) |
## 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 | 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 <command> --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**: 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)