From 3b8e54c5110ee723eb2ff84a9e92227c14f9dd1a Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Wed, 18 Feb 2026 03:28:38 -0800 Subject: [PATCH] refactor(doorbell-touch): add build harness with monitor agent and board-specific setup --- .crushmemory | 32 +++++ boards/esp32-32e-4/install.sh | 16 +++ boards/esp32-32e/install.sh | 16 +++ boards/esp32-s3-lcd-43/install.sh | 31 +++++ libraries/KlubhausCore/src/DoorbellLogic.cpp | 2 + mise.toml | 131 +++++++------------ scripts/install-shared.sh | 7 + scripts/monitor-agent.py | 104 +++++++++++++++ scripts/monitor-agent.sh | 98 ++++++++++++++ 9 files changed, 353 insertions(+), 84 deletions(-) create mode 100644 .crushmemory create mode 100755 boards/esp32-32e-4/install.sh create mode 100755 boards/esp32-32e/install.sh create mode 100755 boards/esp32-s3-lcd-43/install.sh create mode 100755 scripts/install-shared.sh create mode 100644 scripts/monitor-agent.py create mode 100755 scripts/monitor-agent.sh diff --git a/.crushmemory b/.crushmemory new file mode 100644 index 0000000..c562f81 --- /dev/null +++ b/.crushmemory @@ -0,0 +1,32 @@ +# Doorbell Touch - Build Harness + +## Quick Commands +```bash +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 +BOARD=esp32-32e-4 mise run install # Install libs (shared + board) +``` + +## Files +- `mise.toml` - Tasks (compile, upload, monitor, log-tail, cmd, state, install, clean) +- `scripts/lockfile.sh` - Lockfile functions (acquire_lock, kill_locked) +- `scripts/monitor-agent.py` - Serial monitor with JSON log + command FIFO +- `scripts/install-shared.sh` - Shared Arduino libs (ArduinoJson, NTPClient) +- `boards/{BOARD}/install.sh` - Board-specific vendor libs + +## Monitor Daemon +- JSON log: `/tmp/doorbell-$BOARD.jsonl` - `{"ts":123.456,"line":"..."}` +- State: `/tmp/doorbell-$BOARD-state.json` - `{"screen":"DASHBOARD",...}` +- Cmd: `echo 'dashboard' > /tmp/doorbell-$BOARD-cmd.fifo` + +## Boards +- esp32-32e (original 2.8" ILI9341) +- esp32-32e-4 (4" ST7796) +- esp32-s3-lcd-43 (4.3" S3 display) + +## Known Fixes +- dashboard/off admin commands now reset inactivity timer (DoorbellLogic.cpp:234,240) diff --git a/boards/esp32-32e-4/install.sh b/boards/esp32-32e-4/install.sh new file mode 100755 index 0000000..c34f373 --- /dev/null +++ b/boards/esp32-32e-4/install.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Install TFT_eSPI for esp32-32e-4 +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +VENDOR_DIR="$DIR/../../vendor/esp32-32e-4" + +if [ ! -d "$VENDOR_DIR/TFT_eSPI" ]; then + echo "Cloning TFT_eSPI..." + git clone --depth 1 --branch V2.5.43 \ + https://github.com/Bodmer/TFT_eSPI.git \ + "$VENDOR_DIR/TFT_eSPI" +fi + +cp "$DIR/tft_user_setup.h" "$VENDOR_DIR/TFT_eSPI/User_Setup.h" +echo "[OK] TFT_eSPI vendored for esp32-32e-4" diff --git a/boards/esp32-32e/install.sh b/boards/esp32-32e/install.sh new file mode 100755 index 0000000..125b767 --- /dev/null +++ b/boards/esp32-32e/install.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Install TFT_eSPI for esp32-32e +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +VENDOR_DIR="$DIR/../../vendor/esp32-32e" + +if [ ! -d "$VENDOR_DIR/TFT_eSPI" ]; then + echo "Cloning TFT_eSPI..." + git clone --depth 1 --branch V2.5.43 \ + https://github.com/Bodmer/TFT_eSPI.git \ + "$VENDOR_DIR/TFT_eSPI" +fi + +cp "$DIR/tft_user_setup.h" "$VENDOR_DIR/TFT_eSPI/User_Setup.h" +echo "[OK] TFT_eSPI vendored for esp32-32e" diff --git a/boards/esp32-s3-lcd-43/install.sh b/boards/esp32-s3-lcd-43/install.sh new file mode 100755 index 0000000..dfd7ea4 --- /dev/null +++ b/boards/esp32-s3-lcd-43/install.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Install LovyanGFX for esp32-s3-lcd-43 +set -euo pipefail + +DIR="$(cd "$(dirname "$0")" && pwd)" +VENDOR_DIR="$DIR/../../vendor/esp32-s3-lcd-43" +LOVGFX_DIR="$VENDOR_DIR/LovyanGFX" + +if [ ! -d "$LOVGFX_DIR" ]; then + echo "Cloning LovyanGFX..." + git clone --depth 1 \ + https://github.com/lovyan03/LovyanGFX.git \ + "$LOVGFX_DIR" +fi + +# Create library.properties +cat > "$LOVGFX_DIR/library.properties" << 'EOF' +name=LovyanGFX +version=1.2.0 +author=lovyan03 +maintainer=lovyan03 +sentence=Display and touch driver library for ESP32 +paragraph=Universal graphics library for ESP32 with support for various displays and touch controllers +category=Display +url=https://github.com/lovyan03/LovyanGFX +architectures=esp32 +includes=LovyanGFX.hpp +EOF + +mkdir -p "$LOVGFX_DIR/src" +echo "[OK] LovyanGFX vendored for esp32-s3-lcd-43" diff --git a/libraries/KlubhausCore/src/DoorbellLogic.cpp b/libraries/KlubhausCore/src/DoorbellLogic.cpp index d9a7d0c..c2db148 100644 --- a/libraries/KlubhausCore/src/DoorbellLogic.cpp +++ b/libraries/KlubhausCore/src/DoorbellLogic.cpp @@ -231,11 +231,13 @@ void DoorbellLogic::onAdmin(const String& cmd) { ESP.restart(); } else if(cmd == "dashboard") { Serial.printf("[%lu] [ADMIN] dashboard\n", millis()); + _lastActivityMs = millis(); // Reset inactivity timer _state.screen = ScreenID::DASHBOARD; _display->setBacklight(true); _state.backlightOn = true; } else if(cmd == "off") { Serial.printf("[%lu] [ADMIN] off\n", millis()); + _lastActivityMs = millis(); // Reset inactivity timer _state.screen = ScreenID::OFF; _display->setBacklight(false); _state.backlightOn = false; diff --git a/mise.toml b/mise.toml index 8aad66a..6fec025 100644 --- a/mise.toml +++ b/mise.toml @@ -6,6 +6,7 @@ [tools] hk = "latest" pkl = "latest" +# screen = "latest" # Install via system package manager (brew install screen / apt install screen) # zellij = "latest" # Install manually if needed (brew install zellij) # Usage: @@ -36,8 +37,7 @@ arduino-cli compile --fqbn "$FQBN" --libraries ./libraries $LIBS --build-propert arduino-cli upload --fqbn "$FQBN" --port "$PORT" ./boards/$BOARD """ -[tasks.monitor] -description = "Monitor (uses BOARD env var)" +[tasks.monitor-raw] run = """ source ./boards/$BOARD/board-config.sh source ./scripts/lockfile.sh @@ -67,95 +67,58 @@ source ./scripts/lockfile.sh kill_locked """ +[tasks.monitor] +description = "Monitor agent with JSON log + command pipe (Python-based)" +run = """ +python3 ./scripts/monitor-agent.py "$BOARD" +""" + +[tasks.log-tail] +description = "Tail the logs with human-readable formatting" +run = ''' +tail -f "/tmp/doorbell-$BOARD.jsonl" | while read -r line; do + ts=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin)['ts'])" 2>/dev/null) + txt=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin)['line'])" 2>/dev/null) + if [[ "$txt" == *"[STATE]"* ]]; then + echo -e "\\033[1;35m[$ts]\\033[0m $txt" + elif [[ "$txt" == *"[ADMIN]"* ]]; then + echo -e "\\033[1;36m[$ts]\\033[0m $txt" + elif [[ "$txt" == *"[TOUCH]"* ]]; then + echo -e "\\033[1;33m[$ts]\\033[0m $txt" + elif [[ "$txt" == *"ALERT"* ]]; then + echo -e "\\033[1;31m[$ts]\\033[0m $txt" + else + echo "[$ts] $txt" + fi +done +''' + +[tasks.cmd] +description = "Send command to device (COMMAND=dashboard mise run cmd)" +run = """ +echo -n "$COMMAND" > "/tmp/doorbell-$BOARD-cmd.fifo" +echo "[SENT] $COMMAND" +""" + +[tasks.state] +description = "Show current device state" +run = """ +cat "/tmp/doorbell-$BOARD-state.json" +""" + [tasks.install-libs-shared] -description = "Install shared (platform-independent) libraries" +description = "Install shared Arduino libraries" run = """ -arduino-cli lib install "ArduinoJson@7.4.1" -arduino-cli lib install "NTPClient@3.2.1" -echo "[OK] Shared libraries installed" +./scripts/install-shared.sh """ -[tasks.install-libs-32e] -description = "Vendor TFT_eSPI into vendor/esp32-32e" +[tasks.install] +description = "Install libraries for BOARD (shared + board-specific)" run = """ -#!/usr/bin/env bash -set -euo pipefail -if [ ! -d "vendor/esp32-32e/TFT_eSPI" ]; then - echo "Cloning TFT_eSPI..." - git clone --depth 1 --branch V2.5.43 \ - https://github.com/Bodmer/TFT_eSPI.git \ - vendor/esp32-32e/TFT_eSPI -fi -echo "Copying board-specific User_Setup.h..." -cp boards/esp32-32e/tft_user_setup.h vendor/esp32-32e/TFT_eSPI/User_Setup.h -echo "[OK] TFT_eSPI 2.5.43 vendored + configured" +./scripts/install-shared.sh +./boards/$BOARD/install.sh """ -[tasks.install-libs-32e-4] -description = "Vendor TFT_eSPI into vendor/esp32-32e-4 (ST7796 320x480)" -run = """ -#!/usr/bin/env bash -set -euo pipefail -if [ ! -d "vendor/esp32-32e-4/TFT_eSPI" ]; then - echo "Cloning TFT_eSPI..." - git clone --depth 1 --branch V2.5.43 \ - https://github.com/Bodmer/TFT_eSPI.git \ - vendor/esp32-32e-4/TFT_eSPI -fi -echo "Copying board-specific User_Setup.h..." -cp boards/esp32-32e-4/tft_user_setup.h vendor/esp32-32e-4/TFT_eSPI/User_Setup.h -echo "[OK] TFT_eSPI 2.5.43 vendored + configured for 4 inch" -""" - -[tasks.install-libs-s3-43] -description = "Vendor LovyanGFX into vendor/esp32-s3-lcd-43" -run = """ -#!/usr/bin/env bash -set -euo pipefail - -LOVyanGFX_DIR="vendor/esp32-s3-lcd-43/LovyanGFX" - -# Clone LovyanGFX (latest) -if [ ! -d "$LOVyanGFX_DIR" ]; then - echo "Cloning LovyanGFX..." - git clone --depth 1 \ - https://github.com/lovyan03/LovyanGFX.git \ - "$LOVyanGFX_DIR" -else - echo "LovyanGFX already exists, skipping" -fi - -# Create library.properties that correctly points to source -cat > "$LOVyanGFX_DIR/library.properties" << 'EOF' -name=LovyanGFX -version=1.2.0 -author=lovyan03 -maintainer=lovyan03 -sentence=Display and touch driver library for ESP32 -paragraph=Universal graphics library for ESP32 with support for various displays and touch controllers -category=Display -url=https://github.com/lovyan03/LovyanGFX -architectures=esp32 -includes=LovyanGFX.hpp -# This tells Arduino to build from src/ -# Arduino will look in src/ for .cpp files -EOF - -# Create a empty src to ensure sources are found -mkdir -p "$LOVyanGFX_DIR/src" - -echo "[OK] LovyanGFX vendored" -""" - -[tasks.install-libs] -description = "Install all libraries (shared + vendored)" -depends = [ - "install-libs-shared", - "install-libs-32e", - "install-libs-32e-4", - "install-libs-s3-43", -] - # Convenience [tasks.clean] diff --git a/scripts/install-shared.sh b/scripts/install-shared.sh new file mode 100755 index 0000000..910363b --- /dev/null +++ b/scripts/install-shared.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Install shared Arduino libraries +set -euo pipefail + +arduino-cli lib install "ArduinoJson@7.4.1" +arduino-cli lib install "NTPClient@3.2.1" +echo "[OK] Shared libraries installed" diff --git a/scripts/monitor-agent.py b/scripts/monitor-agent.py new file mode 100644 index 0000000..96d4804 --- /dev/null +++ b/scripts/monitor-agent.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Monitor agent - multiplexed serial monitor for AI interaction""" + +import os +import sys +import json +import time +import threading +import serial +import select + +BOARD = sys.argv[1] if len(sys.argv) > 1 else "esp32-32e-4" + +# Load board config +exec(open(f"./boards/{BOARD}/board-config.sh").read().replace("export ", "")) + +SERIAL_PORT = os.environ.get("PORT", PORT) +LOGFILE = f"/tmp/doorbell-{BOARD}.jsonl" +STATEFILE = f"/tmp/doorbell-{BOARD}-state.json" +CMDFIFO = f"/tmp/doorbell-{BOARD}-cmd.fifo" + +# Initialize state +with open(STATEFILE, "w") as f: + json.dump({"screen": "BOOT", "deviceState": "BOOTED", "backlightOn": False}, f) + +# Open serial port +ser = serial.Serial(SERIAL_PORT, 115200, timeout=1) + +# State parsing +def parse_state(line): + updates = {} + if "[STATE]" in line and "→ OFF" in line: + updates["screen"] = "OFF" + elif "[STATE]" in line and "→ DASHBOARD" in line: + updates["screen"] = "DASHBOARD" + elif "[STATE]" in line and "→ ALERT" in line: + updates["screen"] = "ALERT" + elif "[STATE]" in line and "→ BOOT" in line: + updates["screen"] = "BOOT" + elif "[ADMIN]" in line and "dashboard" in line: + updates["screen"] = "DASHBOARD" + elif "[ADMIN]" in line and "off" in line: + updates["screen"] = "OFF" + return updates + +print(f"Monitor agent started (BOARD={BOARD}):") +print(f" Log: {LOGFILE}") +print(f" State: {STATEFILE}") +print(f" Cmd: echo 'dashboard' > {CMDFIFO}") +print() +print("Press Ctrl+C to exit") + +# Create FIFO for commands if not exists +if not os.path.exists(CMDFIFO): + os.mkfifo(CMDFIFO) + +# Command FIFO reader thread +cmd_running = True +def cmd_reader(): + global cmd_running + while cmd_running: + try: + with open(CMDFIFO, "r") as fifo: + # Use select to avoid blocking forever + ready, _, _ = select.select([fifo], [], [], 1) + if ready: + cmd = fifo.read().strip() + if cmd: + ser.write((cmd + "\r").encode()) + print(f"[SENT] {cmd}") + except Exception as e: + if cmd_running: + print(f"Cmd reader error: {e}") + time.sleep(0.1) + +cmd_thread = threading.Thread(target=cmd_reader, daemon=True) +cmd_thread.start() + +# Main loop: read serial and log +try: + while True: + if ser.in_waiting: + line = ser.readline().decode("utf-8", errors="replace").strip() + if line: + ts = time.time() + # Write JSON log + with open(LOGFILE, "a") as f: + json.dump({"ts": f"{ts:.3f}", "line": line}, f) + f.write("\n") + + # Update state + updates = parse_state(line) + if updates: + with open(STATEFILE, "w") as f: + json.dump(updates, f) + + time.sleep(0.01) + +except KeyboardInterrupt: + pass +finally: + cmd_running = False + ser.close() + print("\nMonitor stopped") diff --git a/scripts/monitor-agent.sh b/scripts/monitor-agent.sh new file mode 100755 index 0000000..1ce944f --- /dev/null +++ b/scripts/monitor-agent.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Monitor agent - multiplexed serial monitor for AI interaction +# Usage: ./scripts/monitor-agent.sh +# +# Outputs: +# - /tmp/doorbell-$BOARD.jsonl - JSON Lines log {"ts":123.456,"line":"..."} +# - /tmp/doorbell-$BOARD-state.json - Current device state +# - /tmp/doorbell-$BOARD-cmd.fifo - Command pipe (echo 'cmd' > fifo) +# +# Usage: +# tail -f /tmp/doorbell-$BOARD.jsonl # Watch logs +# echo 'dashboard' > /tmp/doorbell-$BOARD-cmd.fifo # Send command + +set -euo pipefail + +BOARD="${1:-esp32-32e-4}" +source "./boards/$BOARD/board-config.sh" +source ./scripts/lockfile.sh + +acquire_lock || exit 1 + +SERIAL="$PORT" +LOGFILE="/tmp/doorbell-$BOARD.jsonl" +STATEFILE="/tmp/doorbell-$BOARD-state.json" +CMDFIFO="/tmp/doorbell-$BOARD-cmd.fifo" +SCREENSOCK="doorbell-$BOARD" + +# Cleanup +cleanup() { + screen -S "$SCREENSOCK" -X quit 2>/dev/null || true + rm -f "$CMDFIFO" +} +trap cleanup EXIT + +# Create command FIFO +rm -f "$CMDFIFO" +mkfifo "$CMDFIFO" + +# Initialize state +echo '{"screen":"BOOT","deviceState":"BOOTED","backlightOn":false}' > "$STATEFILE" + +# Start screen in detached mode with logging +# -L: enable logging +# -Logfile: specify log file +# -d -m: create detached +# -S: session name +screen -L -Logfile "$LOGFILE.raw" -d -m -S "$SCREENSOCK" "$SERIAL" 115200 + +# Give screen time to start +sleep 1 + +# Reader: parse raw screen log -> JSON Lines + state +( + exec >>"$LOGFILE" 2>&1 + last_screen_line="" + while IFS= read -r line; do + # Skip duplicate/corrupted lines + [[ "$line" == "$last_screen_line" ]] && continue + last_screen_line="$line" + + # Only process non-empty lines + [[ -z "$line" ]] && continue + + TS=$(date +%s.%3N) + printf '{"ts":%s,"line":"%s"}\n' "$TS" "$line" + + # Parse state changes + case "$line" in + *"[STATE]"*"→ OFF"*) echo '{"screen":"OFF"}' > "$STATEFILE" ;; + *"[STATE]"*"→ DASHBOARD"*) echo '{"screen":"DASHBOARD"}' > "$STATEFILE" ;; + *"[STATE]"*"→ ALERT"*) echo '{"screen":"ALERT"}' > "$STATEFILE" ;; + *"[STATE]"*"→ BOOT"*) echo '{"screen":"BOOT"}' > "$STATEFILE" ;; + *"[ADMIN]"*"dashboard"*) echo '{"screen":"DASHBOARD"}' > "$STATEFILE" ;; + *"[ADMIN]"*"off"*) echo '{"screen":"OFF"}' > "$STATEFILE" ;; + esac + done < "$LOGFILE.raw" +) & +READER_PID=$! + +# Writer: command FIFO -> screen session +( + while IFS= read -r cmd < "$CMDFIFO"; do + # Send command to screen session + screen -S "$SCREENSOCK" -X stuff "$cmd^M" + echo "[SENT] $cmd" + done +) & +WRITER_PID=$! + +echo "Monitor agent started (BOARD=$BOARD):" +echo " Log: $LOGFILE" +echo " State: $STATEFILE" +echo " Cmd: echo 'dashboard' > $CMDFIFO" +echo "" +echo "Press Ctrl+C to exit" + +# Wait for reader +wait "$READER_PID" || true