feat(adr-115): P6 + P10 — runnable wiring example + witness bundle (VERIFIED)

## P6 — Wiring example

`v2/crates/wifi-densepose-sensing-server/examples/mqtt_publisher.rs`
— a runnable end-to-end demo that constructs `MqttConfig` from CLI,
runs `mqtt::security::audit`, spawns the publisher, and feeds it
demo `VitalsSnapshot`s. Every line is the production-wiring blueprint
for `main.rs` when `args.mqtt` is true. Keeping it in `examples/`
lets us validate end-to-end without touching the 6,000-line main.rs
that the parallel ADR-110 agent is editing (see
[[feedback-multi-agent-worktree]]).

Run it:

    cargo run --release -p wifi-densepose-sensing-server \
        --features mqtt --example mqtt_publisher -- \
        --mqtt --mqtt-host 127.0.0.1

Compile-checked clean under `--features mqtt`.

## P10 — Witness bundle (VERIFIED)

`scripts/witness-adr-115.sh` — generator that captures everything a
reviewer needs to verify ADR-115 from the receiving end:

- ADR-115 design doc snapshot
- `integration-docs/` — home-assistant.md + semantic-primitives-metrics.md
- `test-results/lib-tests.log` — cargo test --no-default-features --lib
  (372 passed, 0 failed, 1 properly ignored)
- `test-results/lib-tests-mqtt-feature.log` — under --features mqtt
- `test-results/integration-tests.log` — opt-in via RUVIEW_RUN_INTEGRATION=1
- `bench-results/criterion-*.log` — opt-in via RUVIEW_RUN_BENCH=1
- `manifest/source-hashes.txt` — SHA-256 of every ADR-115 source file
- `manifest/git-head.txt` + `git-head-commit.txt` — exact source commit
- `VERIFY.sh` — self-verification script; recipient runs `bash VERIFY.sh`
  and gets exit-0 if the bundle is internally consistent + lib tests
  passed. Local self-test PASSED end-to-end on this commit.
- `WITNESS-LOG-115.md` — per-phase attestation matrix (P1–P10 status)

Bundle dropped at `dist/witness-bundle-ADR115-<sha>-<ts>.tar.gz`.

## Docs

- `docs/user-guide.md` — new "Home Assistant + Matter integration"
  section between Data Sources and Web UI. 30-second Mosquitto-add-on
  flow, --privacy-mode example for healthcare/AAL, Matter pairing
  walk-through. Links back to docs/integrations/home-assistant.md
  for the full reference.
- `CHANGELOG.md` Unreleased Added — single bullet announcing ADR-115
  with the 21 entities, --privacy-mode architectural win, witness
  bundle, deferred P7-P8 status.

## Phase status

| Phase | Status |
|---|---|
| P1 MQTT feature + CLI flags |  |
| P2 HA discovery emitter |  |
| P3 State + publisher |  |
| P4 Mosquitto integration |  (CI-gated) |
| P4.5 Semantic inference (HA-MIND) |  |
| P5 Docs |  |
| P6 Wiring example |  |
| P7-P8 Matter Bridge | ⏸ deferred to v0.7.1+ per §9.10 |
| P9 Security + bench |  |
| P10 Witness bundle |  |

Total lines: ~6000. Total tests: 372 passed. Witness: VERIFIED.

Refs #776.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-05-23 14:26:14 -04:00
parent d25e331bbf
commit a4f56d2f1b
4 changed files with 452 additions and 0 deletions
@@ -0,0 +1,122 @@
//! ADR-115 P6 — minimal runnable example wiring the MQTT publisher
//! against a broadcast channel of `VitalsSnapshot`s.
//!
//! Run with:
//! cargo run --release -p wifi-densepose-sensing-server \
//! --features mqtt --example mqtt_publisher -- \
//! --mqtt --mqtt-host 127.0.0.1
//!
//! Then in another terminal:
//! mosquitto_sub -h 127.0.0.1 -t 'homeassistant/#' -v
//!
//! You should see one HA discovery `config` topic per entity per node
//! land within a second of startup, followed by `state` topics ticking
//! at the configured rates.
//!
//! This example is the production-wiring blueprint for `main.rs`:
//! every line below is what the binary's startup path should do when
//! `args.mqtt` is true. Keeping it in `examples/` lets us validate the
//! wiring end-to-end without touching the 6000-line main.rs (which is
//! the active edit surface of the parallel ADR-110 agent — see
//! [[feedback-multi-agent-worktree]]).
#![cfg(feature = "mqtt")]
use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
use tokio::sync::broadcast;
use tracing::info;
use wifi_densepose_sensing_server::cli::Args;
use wifi_densepose_sensing_server::mqtt::{
config::MqttConfig,
publisher::{spawn, OwnedDiscoveryBuilder},
security::audit,
state::VitalsSnapshot,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let args = Args::parse();
if !args.mqtt {
eprintln!("This example requires --mqtt. Aborting.");
std::process::exit(2);
}
// 1. Build MqttConfig from CLI + run the security audit before any
// network I/O. A failed audit short-circuits with a clear error.
let cfg = Arc::new(MqttConfig::from_args(&args));
match audit(&cfg) {
Ok(()) => {}
Err(e) if !e.is_fatal() => {
tracing::warn!(error = %e, "non-fatal MQTT audit advisory");
}
Err(e) => {
eprintln!("MQTT audit failed: {e}");
std::process::exit(1);
}
}
// 2. The DiscoveryBuilder owns the per-node identity. In a real
// deployment each ESP32 node would get its own builder; here we
// fake one for demonstration.
let builder = OwnedDiscoveryBuilder {
discovery_prefix: cfg.discovery_prefix.clone(),
node_id: "example_node".into(),
node_friendly_name: Some("Example RuView Node".into()),
sw_version: env!("CARGO_PKG_VERSION").into(),
model: "ESP32-S3 CSI node (example)".into(),
via_device: None,
};
// 3. Broadcast channel — `sensing-server` already creates one of
// these in main.rs (the one the WebSocket handler subscribes to).
// We mirror it here.
let (tx, rx) = broadcast::channel::<VitalsSnapshot>(256);
// 4. Spawn the publisher. It returns a JoinHandle the caller can
// await on shutdown.
let publisher = spawn(cfg.clone(), builder, rx);
info!("publisher spawned, sending demo snapshots every 500ms");
// 5. Demo loop — produce a fresh VitalsSnapshot every 500ms with
// alternating presence so HA sees ON/OFF transitions.
let mut tick: u64 = 0;
let mut interval = tokio::time::interval(Duration::from_millis(500));
let stop = tokio::signal::ctrl_c();
tokio::pin!(stop);
loop {
tokio::select! {
_ = interval.tick() => {
tick += 1;
let snap = VitalsSnapshot {
node_id: "example_node".into(),
timestamp_ms: chrono::Utc::now().timestamp_millis(),
presence: tick % 20 < 10,
fall_detected: tick % 60 == 30,
motion: 0.10 + ((tick as f64).sin().abs() * 0.30),
motion_energy: 1000.0 + (tick as f64).cos() * 200.0,
presence_score: 0.85,
breathing_rate_bpm: Some(13.0 + ((tick as f64) * 0.05).sin()),
heartrate_bpm: Some(68.0 + ((tick as f64) * 0.03).sin() * 5.0),
n_persons: if tick % 20 < 10 { 1 } else { 0 },
rssi_dbm: Some(-50.0 + ((tick as f64) * 0.1).sin() * 5.0),
vital_confidence: 0.85,
};
let _ = tx.send(snap);
}
_ = &mut stop => {
info!("ctrl-c received, shutting down");
break;
}
}
}
drop(tx); // close broadcast → publisher publishes `offline` + disconnects.
let _ = tokio::time::timeout(Duration::from_secs(2), publisher).await;
Ok(())
}