refactor(doorbell-touch): add build harness with monitor agent and board-specific setup
This commit is contained in:
7
scripts/install-shared.sh
Executable file
7
scripts/install-shared.sh
Executable 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
104
scripts/monitor-agent.py
Normal 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
98
scripts/monitor-agent.sh
Executable 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
|
||||
Reference in New Issue
Block a user