refactor(doorbell-touch): add build harness with monitor agent and board-specific setup

This commit is contained in:
2026-02-18 03:28:38 -08:00
parent 4ea7165148
commit 3b8e54c511
9 changed files with 353 additions and 84 deletions

7
scripts/install-shared.sh Executable file
View File

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

104
scripts/monitor-agent.py Normal file
View File

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

98
scripts/monitor-agent.sh Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Monitor agent - multiplexed serial monitor for AI interaction
# Usage: ./scripts/monitor-agent.sh <board>
#
# 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