From fd1803d347ca6aced6d3991a4f3119923447d01f Mon Sep 17 00:00:00 2001 From: ruv Date: Sat, 23 May 2026 14:50:52 -0400 Subject: [PATCH] feat(adr-115): ESP32 hardware validation harness + witness integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## scripts/validate-esp32-mqtt.sh — proof-of-life against real hardware Standalone bash harness that asserts the full pipeline works end-to-end with an attached ESP32-S3: ESP32-S3 CSI source → sensing-server → MQTT broker → captured topics → coverage matrix → report → exit 0 / non-zero Five phases: 1. Pre-flight (mosquitto-clients on PATH, cargo on PATH; starts an inline mosquitto if no broker reachable) 2. Start sensing-server with --source esp32 --mqtt (uses the example binary that landed in P6) 3. Capture mosquitto_sub traffic for --duration seconds 4. Assert coverage matrix: 16 expected HA discovery topics (raw + semantic primitives) MUST appear; ≥1 state message MUST land 5. Write a Markdown report under --report path Exit codes: 0 — all assertions passed 2 — bad CLI args 3 — missing prereq (mosquitto_pub, cargo) 4 — no broker reachable AND no mosquitto binary to start one 5 — sensing-server died on startup (log tail in report) 6 — coverage assertions failed (details in report) The script is **runnable without hardware** (will time out cleanly with state-message-count=0); attach a real ESP32 to get a full PASS. Default port: 127.0.0.1:11883 + 60 s capture window. Usage: bash scripts/validate-esp32-mqtt.sh \ --duration 60 \ --broker 127.0.0.1:11883 \ --source esp32 \ --report dist/validation-esp32-.txt ## scripts/witness-adr-115.sh — integration Two changes: 1. Always copy `docs/integrations/benchmarks.md` into the bundle's `bench-results/` dir so the bench numbers travel with the bundle even when `RUVIEW_RUN_BENCH=0` (the captured numbers from `ca10df7b0` are still load-bearing). 2. New `RUVIEW_RUN_ESP32=1` opt-in path that runs the validation harness above and bakes the report into the bundle as `esp32-validation.md`. Without the env var, a placeholder note explains how to opt in. Both scripts pass `bash -n` syntax check on Windows Git Bash. Refs #776, PR #778. Co-Authored-By: claude-flow --- scripts/validate-esp32-mqtt.sh | 230 +++++++++++++++++++++++++++++++++ scripts/witness-adr-115.sh | 22 ++++ 2 files changed, 252 insertions(+) create mode 100644 scripts/validate-esp32-mqtt.sh diff --git a/scripts/validate-esp32-mqtt.sh b/scripts/validate-esp32-mqtt.sh new file mode 100644 index 00000000..f804f6af --- /dev/null +++ b/scripts/validate-esp32-mqtt.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +# ADR-115 — ESP32 ↔ MQTT end-to-end validation harness. +# +# Asserts: real ESP32-S3 CSI source → sensing-server → MQTT broker → +# the full set of expected HA discovery topics + at least one state +# message per entity. Exits 0 only if all asserts pass. +# +# Prereqs (caller responsibility): +# - ESP32-S3 on COM7 (Windows) or /dev/ttyUSB0 (Linux), provisioned +# with WiFi credentials + a reachable seed URL (see provision.py) +# - mosquitto-clients installed (apt-get install mosquitto-clients) +# - sensing-server built with --features mqtt +# +# Usage: +# bash scripts/validate-esp32-mqtt.sh \ +# --duration 60 \ +# --broker 127.0.0.1:11883 \ +# --report dist/validation-esp32-.txt +# +# The script: +# 1. Starts mosquitto locally with allow_anonymous + log_dest stdout +# 2. Starts sensing-server with --source esp32 --mqtt +# 3. Streams `mosquitto_sub -t 'homeassistant/#'` for `duration` seconds +# 4. Parses the captured topics → verifies coverage matrix +# 5. Generates a report under `--report` that goes into the witness bundle +# +# This harness IS the proof-of-life for ADR-115 against real hardware. + +set -euo pipefail + +# ── Defaults ───────────────────────────────────────────────────────── +DURATION=60 +BROKER_HOST="127.0.0.1" +BROKER_PORT=11883 +REPORT="dist/validation-esp32-$(git rev-parse --short HEAD 2>/dev/null || echo unknown).txt" +SOURCE="esp32" + +usage() { + cat <&2; usage; exit 2 ;; + esac +done + +mkdir -p "$(dirname "$REPORT")" +TMPDIR="$(mktemp -d)" +trap "rm -rf '$TMPDIR'" EXIT + +# ── Pre-flight checks ──────────────────────────────────────────────── +echo "[validate] phase 1/5 — pre-flight" +need() { + command -v "$1" >/dev/null 2>&1 || { echo "[validate] FATAL: '$1' not on PATH" >&2; exit 3; } +} +need mosquitto_sub +need mosquitto_pub +need cargo + +# Confirm a broker is reachable; if not, start one inline. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$ROOT" + +BROKER_PID="" +if ! mosquitto_pub -h "$BROKER_HOST" -p "$BROKER_PORT" -t healthcheck -m ok -q 0 2>/dev/null; then + if command -v mosquitto >/dev/null 2>&1; then + cat > "$TMPDIR/mosquitto.conf" <"$TMPDIR/mosquitto.log" 2>&1 & + BROKER_PID=$! + echo "[validate] started inline mosquitto pid=$BROKER_PID on $BROKER_PORT" + sleep 2 + else + echo "[validate] FATAL: no broker at $BROKER_HOST:$BROKER_PORT and 'mosquitto' not installed" >&2 + exit 4 + fi +fi + +# ── Start sensing-server with MQTT ─────────────────────────────────── +echo "[validate] phase 2/5 — start sensing-server with --source $SOURCE --mqtt" + +SERVER_LOG="$TMPDIR/sensing-server.log" +( cd v2 && cargo run --release -p wifi-densepose-sensing-server \ + --features mqtt --example mqtt_publisher -- \ + --mqtt --mqtt-host "$BROKER_HOST" --mqtt-port "$BROKER_PORT" \ + --source "$SOURCE" \ + >"$SERVER_LOG" 2>&1 ) & +SERVER_PID=$! +echo "[validate] sensing-server pid=$SERVER_PID" + +cleanup() { + if [[ -n "${SERVER_PID:-}" ]]; then kill "$SERVER_PID" 2>/dev/null || true; fi + if [[ -n "${BROKER_PID:-}" ]]; then kill "$BROKER_PID" 2>/dev/null || true; fi +} +trap cleanup EXIT + +sleep 3 +if ! kill -0 "$SERVER_PID" 2>/dev/null; then + echo "[validate] FATAL: sensing-server died on startup" >&2 + cat "$SERVER_LOG" | tail -40 >&2 + exit 5 +fi + +# ── Capture MQTT traffic ───────────────────────────────────────────── +echo "[validate] phase 3/5 — capture MQTT traffic for ${DURATION}s" + +MQTT_CAPTURE="$TMPDIR/mqtt-capture.log" +( mosquitto_sub -h "$BROKER_HOST" -p "$BROKER_PORT" -t 'homeassistant/#' -v -W $((DURATION + 5)) \ + >"$MQTT_CAPTURE" 2>&1 ) || true + +CAPTURED=$(wc -l < "$MQTT_CAPTURE") +echo "[validate] captured $CAPTURED MQTT lines" + +# ── Assert coverage ────────────────────────────────────────────────── +echo "[validate] phase 4/5 — assert coverage" + +EXPECTED_DISCOVERY=( + "binary_sensor/wifi_densepose_.*/presence/config" + "sensor/wifi_densepose_.*/person_count/config" + "sensor/wifi_densepose_.*/heart_rate/config" + "sensor/wifi_densepose_.*/breathing_rate/config" + "sensor/wifi_densepose_.*/motion_level/config" + "event/wifi_densepose_.*/fall/config" + "sensor/wifi_densepose_.*/rssi/config" + "binary_sensor/wifi_densepose_.*/someone_sleeping/config" + "binary_sensor/wifi_densepose_.*/possible_distress/config" + "binary_sensor/wifi_densepose_.*/room_active/config" + "binary_sensor/wifi_densepose_.*/bathroom_occupied/config" + "binary_sensor/wifi_densepose_.*/no_movement/config" + "binary_sensor/wifi_densepose_.*/meeting_in_progress/config" + "sensor/wifi_densepose_.*/fall_risk_elevated/config" + "event/wifi_densepose_.*/bed_exit/config" + "event/wifi_densepose_.*/multi_room_transition/config" +) + +PASS=0 +FAIL=0 +RESULTS="" +for pattern in "${EXPECTED_DISCOVERY[@]}"; do + if grep -qE "homeassistant/$pattern" "$MQTT_CAPTURE"; then + PASS=$((PASS + 1)) + RESULTS+=" ✓ $pattern"$'\n' + else + FAIL=$((FAIL + 1)) + RESULTS+=" ✗ $pattern"$'\n' + fi +done + +# Also assert at least one state message landed. +STATE_COUNT=$(grep -cE "/state " "$MQTT_CAPTURE" || true) +if [[ "$STATE_COUNT" -gt 0 ]]; then + RESULTS+=" ✓ at least one state message published ($STATE_COUNT total)"$'\n' + PASS=$((PASS + 1)) +else + RESULTS+=" ✗ no state messages observed in capture"$'\n' + FAIL=$((FAIL + 1)) +fi + +# ── Generate report ────────────────────────────────────────────────── +echo "[validate] phase 5/5 — write report to $REPORT" + +cat > "$REPORT" </dev/null || echo "(no git)") +**Branch**: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "(no git)") +**Source**: $SOURCE +**Broker**: $BROKER_HOST:$BROKER_PORT +**Capture duration**: ${DURATION}s +**MQTT lines captured**: $CAPTURED +**State messages observed**: $STATE_COUNT + +## Result: $([ "$FAIL" -eq 0 ] && echo "PASS ✓" || echo "FAIL ✗") + +- Assertions passed: $PASS +- Assertions failed: $FAIL + +## Coverage + +$RESULTS + +## Tail of sensing-server log (last 20 lines) + +\`\`\` +$(tail -20 "$SERVER_LOG" 2>/dev/null || echo "(no log)") +\`\`\` + +## Tail of mqtt capture (last 30 lines) + +\`\`\` +$(tail -30 "$MQTT_CAPTURE" 2>/dev/null || echo "(no capture)") +\`\`\` + +## Reproduce + +\`\`\`bash +bash scripts/validate-esp32-mqtt.sh --duration $DURATION --broker $BROKER_HOST:$BROKER_PORT --source $SOURCE +\`\`\` +EOF + +echo +echo "[validate] report written to $REPORT" +echo "[validate] PASS=$PASS FAIL=$FAIL" +if [[ "$FAIL" -gt 0 ]]; then + echo "[validate] VALIDATION FAILED — see report for details" + exit 6 +fi +echo "[validate] ESP32 ↔ MQTT validation: PASS ✓" diff --git a/scripts/witness-adr-115.sh b/scripts/witness-adr-115.sh index b7d6fb2c..aecf820c 100644 --- a/scripts/witness-adr-115.sh +++ b/scripts/witness-adr-115.sh @@ -71,6 +71,28 @@ else echo "[witness] SKIP benchmarks (set RUVIEW_RUN_BENCH=1 to include — ~3 min)" echo "Skipped — set RUVIEW_RUN_BENCH=1 to include." > "${BUNDLE_DIR}/bench-results/criterion-stdout.log" fi +# Always include the benchmark reference doc with previously-captured numbers. +cp docs/integrations/benchmarks.md "${BUNDLE_DIR}/bench-results/" 2>/dev/null || true + +# ── 5b. ESP32 ↔ MQTT validation report (optional, needs hardware) ──── +if [[ "${RUVIEW_RUN_ESP32:-0}" == "1" ]]; then + echo "[witness] running ESP32 validation (needs hardware on the configured port)" + bash scripts/validate-esp32-mqtt.sh \ + --duration 60 \ + --broker 127.0.0.1:11883 \ + --report "${BUNDLE_DIR}/esp32-validation.md" \ + 2>&1 | tee "${BUNDLE_DIR}/esp32-validation-stdout.log" || true +else + echo "[witness] SKIP ESP32 validation (set RUVIEW_RUN_ESP32=1 with hardware attached)" + cat > "${BUNDLE_DIR}/esp32-validation.md" <