feat: ESP32-S3-BOX-3 room satellite — ESPHome config, OTA deploy, placeholder faces

Living room unit fully working: on-device wake word (hey_jarvis), voice pipeline
via HA (Wyoming STT → OpenClaw → Wyoming TTS), static PNG display states, OTA
updates. Includes deploy.sh for quick OTA with custom image support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-13 20:48:03 +00:00
parent 3c0d905e64
commit c4cecbd8dc
13 changed files with 1410 additions and 341 deletions

View File

@@ -1,76 +1,177 @@
#!/usr/bin/env bash
# homeai-esp32/setup.sh — P6: ESPHome firmware for ESP32-S3-BOX-3
#
# Components:
# - ESPHomefirmware build + flash tool
# - base.yaml — shared device config
# - voice.yaml — Wyoming Satellite + microWakeWord
# - display.yaml — LVGL animated face
# - Per-room configs — s3-box-living-room.yaml, etc.
# Usage:
# ./setup.sh check environment + validate config
# ./setup.sh flash — compile + flash via USB (first time)
# ./setup.sh ota — compile + flash via OTA (wireless)
# ./setup.sh logs — stream device logs
# ./setup.sh validate — validate YAML without compiling
#
# Prerequisites:
# - P1 (homeai-infra) — Home Assistant running
# - P3 (homeai-voice) — Wyoming STT/TTS running (ports 10300/10301)
# - Python 3.10+
# - USB-C cable for first flash (subsequent updates via OTA)
# - On Linux: ensure user is in the dialout group for USB access
# - ~/homeai-esphome-env — Python 3.12 venv with ESPHome
# - Home Assistant running on 10.0.0.199
# - Wyoming STT/TTS running on Mac Mini (ports 10300/10301)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
source "${REPO_DIR}/scripts/common.sh"
ESPHOME_VENV="${HOME}/homeai-esphome-env"
ESPHOME="${ESPHOME_VENV}/bin/esphome"
ESPHOME_DIR="${SCRIPT_DIR}/esphome"
DEFAULT_CONFIG="${ESPHOME_DIR}/homeai-living-room.yaml"
log_section "P6: ESP32 Firmware (ESPHome)"
detect_platform
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# ─── Prerequisite check ────────────────────────────────────────────────────────
log_info "Checking prerequisites..."
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
if ! command_exists python3; then
log_warn "python3 not found — required for ESPHome"
fi
# ─── Environment checks ──────────────────────────────────────────────────────
if ! command_exists esphome; then
log_info "ESPHome not installed. To install: pip install esphome"
fi
check_env() {
local ok=true
if [[ "$OS_TYPE" == "linux" ]]; then
if ! groups "$USER" | grep -q dialout; then
log_warn "User '$USER' not in 'dialout' group — USB flashing may fail."
log_warn "Fix: sudo usermod -aG dialout $USER (then log out and back in)"
log_info "Checking environment..."
# ESPHome venv
if [[ -x "${ESPHOME}" ]]; then
local version
version=$("${ESPHOME}" version 2>/dev/null)
log_ok "ESPHome: ${version}"
else
log_error "ESPHome not found at ${ESPHOME}"
echo " Install: /opt/homebrew/opt/python@3.12/bin/python3.12 -m venv ${ESPHOME_VENV}"
echo " ${ESPHOME_VENV}/bin/pip install 'esphome>=2025.5.0'"
ok=false
fi
fi
# Check P3 dependency
if ! curl -sf http://localhost:8123 -o /dev/null 2>/dev/null; then
log_warn "Home Assistant (P1) not reachable — ESP32 units won't auto-discover"
fi
# secrets.yaml
if [[ -f "${ESPHOME_DIR}/secrets.yaml" ]]; then
if grep -q "YOUR_" "${ESPHOME_DIR}/secrets.yaml" 2>/dev/null; then
log_warn "secrets.yaml contains placeholder values — edit before flashing"
ok=false
else
log_ok "secrets.yaml configured"
fi
else
log_error "secrets.yaml not found at ${ESPHOME_DIR}/secrets.yaml"
ok=false
fi
# ─── TODO: Implementation ──────────────────────────────────────────────────────
cat <<'EOF'
# Config file
if [[ -f "${DEFAULT_CONFIG}" ]]; then
log_ok "Config: $(basename "${DEFAULT_CONFIG}")"
else
log_error "Config not found: ${DEFAULT_CONFIG}"
ok=false
fi
┌─────────────────────────────────────────────────────────────────┐
│ P6: homeai-esp32 — NOT YET IMPLEMENTED │
│ │
│ Implementation steps: │
│ 1. pip install esphome │
2. Create esphome/secrets.yaml (gitignored) │
│ 3. Create esphome/base.yaml (WiFi, API, OTA) │
4. Create esphome/voice.yaml (Wyoming Satellite, wakeword) │
│ 5. Create esphome/display.yaml (LVGL face, 5 states) │
│ 6. Create esphome/animations.yaml (face state scripts) │
│ 7. Create per-room configs (s3-box-living-room.yaml, etc.) │
│ 8. First flash via USB: esphome run esphome/<room>.yaml │
│ 9. Subsequent OTA: esphome upload esphome/<room>.yaml │
│ 10. Add to Home Assistant → assign Wyoming voice pipeline │
│ │
│ Quick flash (once esphome/ is ready): │
│ esphome run esphome/s3-box-living-room.yaml │
│ esphome logs esphome/s3-box-living-room.yaml │
└─────────────────────────────────────────────────────────────────┘
# Illustrations
local illust_dir="${ESPHOME_DIR}/illustrations"
local illust_count
illust_count=$(find "${illust_dir}" -name "*.png" 2>/dev/null | wc -l | tr -d ' ')
if [[ "${illust_count}" -ge 7 ]]; then
log_ok "Illustrations: ${illust_count} PNGs in illustrations/"
else
log_warn "Missing illustrations (found ${illust_count}, need 7)"
fi
EOF
# Wyoming services on Mac Mini
if curl -sf "http://localhost:10300" -o /dev/null 2>/dev/null || nc -z localhost 10300 2>/dev/null; then
log_ok "Wyoming STT (port 10300) reachable"
else
log_warn "Wyoming STT (port 10300) not reachable"
fi
log_info "P6 is not yet implemented. See homeai-esp32/PLAN.md for details."
exit 0
if curl -sf "http://localhost:10301" -o /dev/null 2>/dev/null || nc -z localhost 10301 2>/dev/null; then
log_ok "Wyoming TTS (port 10301) reachable"
else
log_warn "Wyoming TTS (port 10301) not reachable"
fi
# Home Assistant
if curl -sk "https://10.0.0.199:8123" -o /dev/null 2>/dev/null; then
log_ok "Home Assistant (10.0.0.199:8123) reachable"
else
log_warn "Home Assistant not reachable — ESP32 won't be able to connect"
fi
if $ok; then
log_ok "Environment ready"
else
log_warn "Some issues found — fix before flashing"
fi
}
# ─── Commands ─────────────────────────────────────────────────────────────────
cmd_flash() {
local config="${1:-${DEFAULT_CONFIG}}"
log_info "Compiling + flashing via USB: $(basename "${config}")"
log_info "First compile downloads ESP-IDF toolchain (~500MB), takes 5-10 min..."
cd "${ESPHOME_DIR}"
"${ESPHOME}" run "$(basename "${config}")"
}
cmd_ota() {
local config="${1:-${DEFAULT_CONFIG}}"
log_info "Compiling + OTA upload: $(basename "${config}")"
cd "${ESPHOME_DIR}"
"${ESPHOME}" run "$(basename "${config}")"
}
cmd_logs() {
local config="${1:-${DEFAULT_CONFIG}}"
log_info "Streaming logs for: $(basename "${config}")"
cd "${ESPHOME_DIR}"
"${ESPHOME}" logs "$(basename "${config}")"
}
cmd_validate() {
local config="${1:-${DEFAULT_CONFIG}}"
log_info "Validating: $(basename "${config}")"
cd "${ESPHOME_DIR}"
"${ESPHOME}" config "$(basename "${config}")"
log_ok "Config valid"
}
# ─── Main ─────────────────────────────────────────────────────────────────────
case "${1:-}" in
flash)
check_env
echo ""
cmd_flash "${2:-}"
;;
ota)
check_env
echo ""
cmd_ota "${2:-}"
;;
logs)
cmd_logs "${2:-}"
;;
validate)
cmd_validate "${2:-}"
;;
*)
check_env
echo ""
echo "Usage: $0 {flash|ota|logs|validate} [config.yaml]"
echo ""
echo " flash Compile + flash via USB (first time)"
echo " ota Compile + flash via OTA (wireless, after first flash)"
echo " logs Stream device logs"
echo " validate Validate YAML config without compiling"
echo ""
echo "Default config: $(basename "${DEFAULT_CONFIG}")"
;;
esac