Files
klubhaus-doorbell/AGENTS.md

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

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

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

{
  "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)