refactor(doorbell-touch): add build harness with monitor agent and board-specific setup
This commit is contained in:
32
.crushmemory
Normal file
32
.crushmemory
Normal file
@@ -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)
|
||||
16
boards/esp32-32e-4/install.sh
Executable file
16
boards/esp32-32e-4/install.sh
Executable file
@@ -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"
|
||||
16
boards/esp32-32e/install.sh
Executable file
16
boards/esp32-32e/install.sh
Executable file
@@ -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"
|
||||
31
boards/esp32-s3-lcd-43/install.sh
Executable file
31
boards/esp32-s3-lcd-43/install.sh
Executable file
@@ -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"
|
||||
@@ -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;
|
||||
|
||||
127
mise.toml
127
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,94 +67,57 @@ source ./scripts/lockfile.sh
|
||||
kill_locked
|
||||
"""
|
||||
|
||||
[tasks.install-libs-shared]
|
||||
description = "Install shared (platform-independent) libraries"
|
||||
[tasks.monitor]
|
||||
description = "Monitor agent with JSON log + command pipe (Python-based)"
|
||||
run = """
|
||||
arduino-cli lib install "ArduinoJson@7.4.1"
|
||||
arduino-cli lib install "NTPClient@3.2.1"
|
||||
echo "[OK] Shared libraries installed"
|
||||
python3 ./scripts/monitor-agent.py "$BOARD"
|
||||
"""
|
||||
|
||||
[tasks.install-libs-32e]
|
||||
description = "Vendor TFT_eSPI into vendor/esp32-32e"
|
||||
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"
|
||||
"""
|
||||
|
||||
[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"
|
||||
[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 "LovyanGFX already exists, skipping"
|
||||
echo "[$ts] $txt"
|
||||
fi
|
||||
done
|
||||
'''
|
||||
|
||||
# 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.cmd]
|
||||
description = "Send command to device (COMMAND=dashboard mise run cmd)"
|
||||
run = """
|
||||
echo -n "$COMMAND" > "/tmp/doorbell-$BOARD-cmd.fifo"
|
||||
echo "[SENT] $COMMAND"
|
||||
"""
|
||||
|
||||
[tasks.install-libs]
|
||||
description = "Install all libraries (shared + vendored)"
|
||||
depends = [
|
||||
"install-libs-shared",
|
||||
"install-libs-32e",
|
||||
"install-libs-32e-4",
|
||||
"install-libs-s3-43",
|
||||
]
|
||||
[tasks.state]
|
||||
description = "Show current device state"
|
||||
run = """
|
||||
cat "/tmp/doorbell-$BOARD-state.json"
|
||||
"""
|
||||
|
||||
[tasks.install-libs-shared]
|
||||
description = "Install shared Arduino libraries"
|
||||
run = """
|
||||
./scripts/install-shared.sh
|
||||
"""
|
||||
|
||||
[tasks.install]
|
||||
description = "Install libraries for BOARD (shared + board-specific)"
|
||||
run = """
|
||||
./scripts/install-shared.sh
|
||||
./boards/$BOARD/install.sh
|
||||
"""
|
||||
|
||||
# Convenience
|
||||
|
||||
|
||||
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