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>
245 lines
8.6 KiB
Bash
Executable File
245 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# homeai-esp32/deploy.sh — Quick OTA deploy for ESP32-S3-BOX-3 satellites
|
|
#
|
|
# Usage:
|
|
# ./deploy.sh — deploy config + images to living room (default)
|
|
# ./deploy.sh bedroom — deploy to bedroom unit
|
|
# ./deploy.sh --images-only — deploy existing PNGs from illustrations/ (no regen)
|
|
# ./deploy.sh --regen-images — regenerate placeholder PNGs then deploy
|
|
# ./deploy.sh --validate — validate config without deploying
|
|
# ./deploy.sh --all — deploy to all configured units
|
|
#
|
|
# Images are compiled into firmware, so any PNG changes require a reflash.
|
|
# To use custom images: drop 320x240 PNGs into esphome/illustrations/ then ./deploy.sh
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ESPHOME_DIR="${SCRIPT_DIR}/esphome"
|
|
ESPHOME_VENV="${HOME}/homeai-esphome-env"
|
|
ESPHOME="${ESPHOME_VENV}/bin/esphome"
|
|
PYTHON="${ESPHOME_VENV}/bin/python3"
|
|
ILLUSTRATIONS_DIR="${ESPHOME_DIR}/illustrations"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
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} $*"; exit 1; }
|
|
log_step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
|
|
|
# ─── Available units ──────────────────────────────────────────────────────────
|
|
|
|
UNIT_NAMES=(living-room bedroom kitchen)
|
|
DEFAULT_UNIT="living-room"
|
|
|
|
unit_config() {
|
|
case "$1" in
|
|
living-room) echo "homeai-living-room.yaml" ;;
|
|
bedroom) echo "homeai-bedroom.yaml" ;;
|
|
kitchen) echo "homeai-kitchen.yaml" ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
unit_list() {
|
|
echo "${UNIT_NAMES[*]}"
|
|
}
|
|
|
|
# ─── Face image generator ────────────────────────────────────────────────────
|
|
|
|
generate_faces() {
|
|
log_step "Generating face illustrations (320x240 PNG)..."
|
|
"${PYTHON}" << 'PYEOF'
|
|
from PIL import Image, ImageDraw
|
|
import os
|
|
|
|
WIDTH, HEIGHT = 320, 240
|
|
OUT = os.environ.get("ILLUSTRATIONS_DIR", "esphome/illustrations")
|
|
|
|
def draw_face(draw, eye_color, mouth_color, eye_height=40, eye_y=80, mouth_style="smile"):
|
|
ex1, ey1 = 95, eye_y
|
|
draw.ellipse([ex1-25, ey1-eye_height//2, ex1+25, ey1+eye_height//2], fill=eye_color)
|
|
ex2, ey2 = 225, eye_y
|
|
draw.ellipse([ex2-25, ey2-eye_height//2, ex2+25, ey2+eye_height//2], fill=eye_color)
|
|
if mouth_style == "smile":
|
|
draw.arc([110, 140, 210, 200], start=0, end=180, fill=mouth_color, width=3)
|
|
elif mouth_style == "open":
|
|
draw.ellipse([135, 150, 185, 190], fill=mouth_color)
|
|
elif mouth_style == "flat":
|
|
draw.line([120, 170, 200, 170], fill=mouth_color, width=3)
|
|
elif mouth_style == "frown":
|
|
draw.arc([110, 160, 210, 220], start=180, end=360, fill=mouth_color, width=3)
|
|
|
|
states = {
|
|
"idle": {"eye_color": "#FFFFFF", "mouth_color": "#FFFFFF", "eye_height": 40, "mouth_style": "smile"},
|
|
"loading": {"eye_color": "#6366F1", "mouth_color": "#6366F1", "eye_height": 30, "mouth_style": "flat"},
|
|
"listening": {"eye_color": "#00BFFF", "mouth_color": "#00BFFF", "eye_height": 50, "mouth_style": "open"},
|
|
"thinking": {"eye_color": "#A78BFA", "mouth_color": "#A78BFA", "eye_height": 20, "mouth_style": "flat"},
|
|
"replying": {"eye_color": "#10B981", "mouth_color": "#10B981", "eye_height": 40, "mouth_style": "open"},
|
|
"error": {"eye_color": "#EF4444", "mouth_color": "#EF4444", "eye_height": 40, "mouth_style": "frown"},
|
|
"timer_finished": {"eye_color": "#F59E0B", "mouth_color": "#F59E0B", "eye_height": 50, "mouth_style": "smile"},
|
|
}
|
|
|
|
os.makedirs(OUT, exist_ok=True)
|
|
for name, p in states.items():
|
|
img = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 255))
|
|
draw = ImageDraw.Draw(img)
|
|
draw_face(draw, p["eye_color"], p["mouth_color"], p["eye_height"], mouth_style=p["mouth_style"])
|
|
img.save(f"{OUT}/{name}.png")
|
|
print(f" {name}.png")
|
|
PYEOF
|
|
log_ok "Generated 7 face illustrations"
|
|
}
|
|
|
|
# ─── Check existing images ───────────────────────────────────────────────────
|
|
|
|
REQUIRED_IMAGES=(idle loading listening thinking replying error timer_finished)
|
|
|
|
check_images() {
|
|
local missing=()
|
|
for name in "${REQUIRED_IMAGES[@]}"; do
|
|
if [[ ! -f "${ILLUSTRATIONS_DIR}/${name}.png" ]]; then
|
|
missing+=("${name}.png")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
log_error "Missing illustrations: ${missing[*]}
|
|
Place 320x240 PNGs in ${ILLUSTRATIONS_DIR}/ or use --regen-images to generate placeholders."
|
|
fi
|
|
|
|
log_ok "All ${#REQUIRED_IMAGES[@]} illustrations present in illustrations/"
|
|
for name in "${REQUIRED_IMAGES[@]}"; do
|
|
local size
|
|
size=$(wc -c < "${ILLUSTRATIONS_DIR}/${name}.png" | tr -d ' ')
|
|
echo -e " ${name}.png (${size} bytes)"
|
|
done
|
|
}
|
|
|
|
# ─── Deploy to a single unit ─────────────────────────────────────────────────
|
|
|
|
deploy_unit() {
|
|
local unit_name="$1"
|
|
local config
|
|
config="$(unit_config "$unit_name")"
|
|
|
|
if [[ -z "$config" ]]; then
|
|
log_error "Unknown unit: ${unit_name}. Available: $(unit_list)"
|
|
fi
|
|
|
|
local config_path="${ESPHOME_DIR}/${config}"
|
|
if [[ ! -f "$config_path" ]]; then
|
|
log_error "Config not found: ${config_path}"
|
|
fi
|
|
|
|
log_step "Validating ${config}..."
|
|
cd "${ESPHOME_DIR}"
|
|
"${ESPHOME}" config "${config}" > /dev/null
|
|
log_ok "Config valid"
|
|
|
|
log_step "Compiling + OTA deploying ${config}..."
|
|
"${ESPHOME}" run "${config}" --device OTA 2>&1
|
|
log_ok "Deployed to ${unit_name}"
|
|
}
|
|
|
|
# ─── Main ─────────────────────────────────────────────────────────────────────
|
|
|
|
IMAGES_ONLY=false
|
|
REGEN_IMAGES=false
|
|
VALIDATE_ONLY=false
|
|
DEPLOY_ALL=false
|
|
TARGET="${DEFAULT_UNIT}"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--images-only) IMAGES_ONLY=true; shift ;;
|
|
--regen-images) REGEN_IMAGES=true; shift ;;
|
|
--validate) VALIDATE_ONLY=true; shift ;;
|
|
--all) DEPLOY_ALL=true; shift ;;
|
|
--help|-h)
|
|
echo "Usage: $0 [unit-name] [--images-only] [--regen-images] [--validate] [--all]"
|
|
echo ""
|
|
echo "Units: $(unit_list)"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --images-only Deploy existing PNGs from illustrations/ (for custom images)"
|
|
echo " --regen-images Regenerate placeholder face PNGs then deploy"
|
|
echo " --validate Validate config without deploying"
|
|
echo " --all Deploy to all configured units"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 # deploy config to living-room"
|
|
echo " $0 bedroom # deploy to bedroom"
|
|
echo " $0 --images-only # deploy with current images (custom or generated)"
|
|
echo " $0 --regen-images # regenerate placeholder faces + deploy"
|
|
echo " $0 --all # deploy to all units"
|
|
echo ""
|
|
echo "Custom images: drop 320x240 PNGs into esphome/illustrations/"
|
|
echo "Required files: ${REQUIRED_IMAGES[*]}"
|
|
exit 0
|
|
;;
|
|
*)
|
|
if [[ -n "$(unit_config "$1")" ]]; then
|
|
TARGET="$1"
|
|
else
|
|
log_error "Unknown option or unit: $1. Use --help for usage."
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Check ESPHome
|
|
if [[ ! -x "${ESPHOME}" ]]; then
|
|
log_error "ESPHome not found at ${ESPHOME}. Run setup.sh first."
|
|
fi
|
|
|
|
# Regenerate placeholder images if requested
|
|
if $REGEN_IMAGES; then
|
|
export ILLUSTRATIONS_DIR
|
|
generate_faces
|
|
fi
|
|
|
|
# Check existing images if deploying with --images-only (or always before deploy)
|
|
if $IMAGES_ONLY; then
|
|
check_images
|
|
fi
|
|
|
|
# Validate only
|
|
if $VALIDATE_ONLY; then
|
|
cd "${ESPHOME_DIR}"
|
|
for unit_name in "${UNIT_NAMES[@]}"; do
|
|
config="$(unit_config "$unit_name")"
|
|
if [[ -f "${config}" ]]; then
|
|
log_step "Validating ${config}..."
|
|
"${ESPHOME}" config "${config}" > /dev/null && log_ok "${config} valid" || log_warn "${config} invalid"
|
|
fi
|
|
done
|
|
exit 0
|
|
fi
|
|
|
|
# Deploy
|
|
if $DEPLOY_ALL; then
|
|
for unit_name in "${UNIT_NAMES[@]}"; do
|
|
config="$(unit_config "$unit_name")"
|
|
if [[ -f "${ESPHOME_DIR}/${config}" ]]; then
|
|
deploy_unit "$unit_name"
|
|
else
|
|
log_warn "Skipping ${unit_name} — ${config} not found"
|
|
fi
|
|
done
|
|
else
|
|
deploy_unit "$TARGET"
|
|
fi
|
|
|
|
echo ""
|
|
log_ok "Deploy complete!"
|