Files
klubhaus-doorbell/AGENTS.md

20 KiB
Raw Blame History

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-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:

# 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):

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.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:

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:

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:

    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:

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)

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:

# ❌ 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)

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)

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)

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)

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)

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)

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)

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)

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)

rtk curl <url>          # Compact HTTP responses (70%)
rtk wget <url>          # Compact download output (65%)

Meta Commands

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.