feat(adr-115): ESP32 hardware validation harness + witness integration

## 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-<sha>.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 <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-23 14:50:52 -04:00
parent 409e8e2831
commit fd1803d347
2 changed files with 252 additions and 0 deletions

View File

@ -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-<sha>.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 <<EOF
Usage: $0 [options]
Options:
--duration N Seconds to capture MQTT traffic (default 60)
--broker HOST:PORT MQTT broker (default 127.0.0.1:11883)
--source SRC sensing-server --source flag (default esp32)
--report FILE Write validation report here
-h, --help This help
EOF
}
# ── Argument parsing ─────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--duration) DURATION="$2"; shift 2 ;;
--broker) BROKER_HOST="${2%%:*}"; BROKER_PORT="${2##*:}"; shift 2 ;;
--source) SOURCE="$2"; shift 2 ;;
--report) REPORT="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "[validate] unknown arg: $1" >&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" <<EOF
listener $BROKER_PORT
allow_anonymous true
persistence false
log_dest stdout
EOF
mosquitto -c "$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" <<EOF
# ADR-115 ESP32 ↔ MQTT validation report
**Date**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
**Commit**: $(git rev-parse HEAD 2>/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 ✓"

View File

@ -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" <<EOF
ESP32 ↔ MQTT validation was not run for this witness bundle.
To include it, set RUVIEW_RUN_ESP32=1 and re-run the witness generator
with a provisioned ESP32-S3 on COM7 (Windows) or /dev/ttyUSB0 (Linux).
The harness in \`scripts/validate-esp32-mqtt.sh\` will write a real
validation report into this slot.
EOF
fi
# ── 6. Source manifest with SHA-256 of every ADR-115 file ────────────
echo "[witness] computing source SHA-256 manifest"