10 KiB
AGENTS.md — Klubhaus Doorbell
Multi-target Arduino/ESP32 doorbell alert system using ntfy.sh.
Quick Reference
# 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:
# 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
Upload 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
IDisplayDriverinterface in shared library - Each board implements a concrete driver (e.g.,
DisplayDriverTFT,DisplayDriverGFX) DisplayManagerdelegates toIDisplayDriver— no display-lib coupling in shared code
Arduino Patterns
setup()runs once at boot — callbegin()on managersloop()runs continuously — callupdate()on managers- Use
millis()for timing (notdelay()) - 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 viaecho 'dashboard' > /tmp/doorbell-$BOARD-cmd.fifo
Commands to interact with the daemon:
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
-
secrets.h is gitignored: Copy from
.examplebefore building: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 -
Display libs are vendored: Each board uses a different display library. The build system uses
--librariesto link only the board's vendored lib — never link both TFT_eSPI and LovyanGFX in the same build. -
No unit tests: This is an embedded Arduino sketch — no test suite exists. Verify changes by building and deploying to hardware.
-
LSP errors are expected: The LSP cannot find
Arduino.hbecause it requires arduino-cli to resolve. Ignore clangd/IntelliSense errors about missing Arduino types; builds work correctly via arduino-cli. -
Build artifacts in board dirs: Build output goes to
boards/[board]/build/— cleaned bymise run clean. -
WiFi credentials are per-board: Each board directory has its own
secrets.hbecause boards may be on different networks. -
Use BOARD environment variable: All build commands require the
BOARDenvironment variable (e.g.,BOARD=esp32-32e mise run compile). The default BOARD isesp32-32e-4(set in mise.toml). -
Lockfile system: The build system uses lockfiles (
/tmp/doorbell-$BOARD.lock) to prevent concurrent upload/monitor operations. Usemise run killto clean up stuck processes.
Known Fixes
- dashboard/off admin commands reset inactivity timer (DoorbellLogic.cpp:234,240) — Admin commands like
dashboardandoffnow 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:
- Run the tool with
-hor--helpfirst - If unclear, search for official documentation matching the tool version
- Prefer checking the tool's own help/docs over guessing
- For Arduino CLI, check
arduino-cli <command> --helpor 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:
{
"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:
BOARD=esp32-32e-4 mise run gen-compile-commands
Generate static .crush.json for a specific board:
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)