diff --git a/v2/Cargo.lock b/v2/Cargo.lock index 66455ec4..15c3cbc4 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -9189,6 +9189,7 @@ dependencies = [ "tracing", "tracing-subscriber", "ureq 2.12.1", + "wifi-densepose-hardware", "wifi-densepose-signal", "wifi-densepose-wifiscan", ] diff --git a/v2/crates/wifi-densepose-sensing-server/Cargo.toml b/v2/crates/wifi-densepose-sensing-server/Cargo.toml index fed20336..46483456 100644 --- a/v2/crates/wifi-densepose-sensing-server/Cargo.toml +++ b/v2/crates/wifi-densepose-sensing-server/Cargo.toml @@ -68,26 +68,6 @@ ureq = { version = "2", default-features = false, features = ["tls", "json" sha2 = "0.10" thiserror = "1" -# ADR-115 §3.8 — MQTT publisher (HA-DISCO). -# Gated behind the `mqtt` feature so the default binary stays small for users -# who don't need Home Assistant integration. `rumqttc` is the chosen Rust MQTT -# client (ADR-115 §10 references). `rustls` is preferred over openssl on -# Windows to keep parity with the rest of the workspace (`ureq` above also -# uses rustls). -rumqttc = { version = "0.24", default-features = false, features = ["use-rustls"], optional = true } - -[features] -default = [] -# Enables the ADR-115 §2 MQTT auto-discovery publisher. Without this feature -# all `--mqtt-*` CLI flags still parse (cli.rs declares them unconditionally), -# but enabling `--mqtt` at runtime logs a `WARN` and the publisher is a no-op. -mqtt = ["dep:rumqttc"] -# ADR-115 §3.11 — Matter Bridge (HA-FABRIC). Same gating principle: flags -# parse unconditionally; the bridge is a no-op without this feature. -# matter-rs is added in P7; intentionally absent in P1 to keep the dep -# surface small until the SDK choice is validated. -matter = [] - [dev-dependencies] tempfile = "3.10" # `tower::ServiceExt::oneshot` for in-process Router tests (bearer_auth). diff --git a/v2/crates/wifi-densepose-sensing-server/src/cli.rs b/v2/crates/wifi-densepose-sensing-server/src/cli.rs index 4fe6f5f6..c857f35c 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/cli.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/cli.rs @@ -102,234 +102,4 @@ pub struct Args { /// Start field model calibration on boot (empty room required) #[arg(long)] pub calibrate: bool, - - // ─── ADR-115 §3.8 — MQTT publisher (HA-DISCO) ────────────────────────── - // - // All `--mqtt-*` flags are no-ops unless `--mqtt` is set. The - // wifi-densepose-sensing-server crate must be built with `--features mqtt` - // for the publisher to do anything; without that feature the flags parse - // but produce a startup WARN. This split lets the binary stay small for - // users who don't need HA integration, while keeping the CLI surface - // stable. - /// Enable MQTT publisher with HA auto-discovery - #[arg(long, env = "RUVIEW_MQTT")] - pub mqtt: bool, - - /// MQTT broker host - #[arg(long, env = "RUVIEW_MQTT_HOST", default_value = "localhost")] - pub mqtt_host: String, - - /// MQTT broker port (defaults: 1883 plain / 8883 with TLS) - #[arg(long, env = "RUVIEW_MQTT_PORT")] - pub mqtt_port: Option, - - /// MQTT username - #[arg(long, env = "RUVIEW_MQTT_USERNAME")] - pub mqtt_username: Option, - - /// Environment variable holding the MQTT password - #[arg(long, default_value = "MQTT_PASSWORD")] - pub mqtt_password_env: String, - - /// MQTT client ID (default: wifi-densepose-) - #[arg(long, env = "RUVIEW_MQTT_CLIENT_ID")] - pub mqtt_client_id: Option, - - /// Discovery topic prefix (ADR-115 §9.2 — accepted: `homeassistant`) - #[arg(long, env = "RUVIEW_MQTT_PREFIX", default_value = "homeassistant")] - pub mqtt_prefix: String, - - /// Enable TLS to the broker - #[arg(long, env = "RUVIEW_MQTT_TLS")] - pub mqtt_tls: bool, - - /// CA bundle for TLS - #[arg(long, value_name = "PATH")] - pub mqtt_ca_file: Option, - - /// Client certificate for mTLS - #[arg(long, value_name = "PATH")] - pub mqtt_client_cert: Option, - - /// Client key for mTLS - #[arg(long, value_name = "PATH")] - pub mqtt_client_key: Option, - - /// Discovery refresh interval (seconds) - #[arg(long, default_value = "600")] - pub mqtt_refresh_secs: u64, - - /// Vitals publish rate (Hz) — HR/BR - #[arg(long, default_value = "0.2")] - pub mqtt_rate_vitals: f64, - - /// Motion publish rate (Hz) - #[arg(long, default_value = "1.0")] - pub mqtt_rate_motion: f64, - - /// Person count publish rate (Hz) - #[arg(long, default_value = "1.0")] - pub mqtt_rate_count: f64, - - /// RSSI publish rate (Hz) - #[arg(long, default_value = "0.1")] - pub mqtt_rate_rssi: f64, - - /// Publish pose keypoints over MQTT (off by default for bandwidth) - #[arg(long)] - pub mqtt_publish_pose: bool, - - /// Pose publish rate (Hz) when --mqtt-publish-pose is set - #[arg(long, default_value = "1.0")] - pub mqtt_rate_pose: f64, - - // ─── ADR-115 §3.10 — Privacy mode ────────────────────────────────────── - /// Strip biometrics (HR/BR/pose) before any MQTT or Matter publish. - /// Discovery for those entities is suppressed entirely — the controller - /// never sees them exist. Implements the ADR-106 primitive-isolation - /// contract at the integration boundary. - #[arg(long, env = "RUVIEW_PRIVACY_MODE")] - pub privacy_mode: bool, - - // ─── ADR-115 §3.11 — Matter Bridge (HA-FABRIC) ───────────────────────── - /// Enable Matter Bridge - #[arg(long, env = "RUVIEW_MATTER")] - pub matter: bool, - - /// Write Matter setup code + QR string to this file on first start - #[arg(long, value_name = "PATH")] - pub matter_setup_file: Option, - - /// Wipe stored Matter fabric credentials before starting - #[arg(long)] - pub matter_reset: bool, - - /// Matter vendor ID (default: dev VID 0xFFF1 per ADR-115 §9.9) - #[arg(long, default_value = "0xFFF1")] - pub matter_vendor_id: String, - - /// Matter product ID (default: 0x8001) - #[arg(long, default_value = "0x8001")] - pub matter_product_id: String, - - // ─── ADR-115 §3.12 — Semantic Inference (HA-MIND) ───────────────────── - /// Enable semantic inference layer (sleeping/distress/room-active/etc). - /// Default ON — primitives are the primary product surface. - #[arg(long, default_value_t = true)] - pub semantic: bool, - - /// Per-primitive thresholds file - #[arg(long, value_name = "PATH")] - pub semantic_thresholds_file: Option, - - /// Zone-tag map (e.g. {"bathroom": ["zone_3"]}) - #[arg(long, value_name = "PATH")] - pub semantic_zones_file: Option, - - /// Days of history for personalised baselines - #[arg(long, default_value = "14")] - pub semantic_baseline_window_days: u32, - - /// Disable a specific semantic primitive (e.g. `sleeping`); repeatable. - /// Valid names: sleeping, distress, room_active, elderly_anomaly, - /// meeting, bathroom, fall_risk, bed_exit, no_movement, multi_room. - #[arg(long = "no-semantic", value_name = "PRIMITIVE")] - pub no_semantic: Vec, -} - -#[cfg(test)] -mod tests { - use super::*; - use clap::Parser; - - /// MQTT flags default safely (disabled). - #[test] - fn mqtt_defaults_disabled() { - let args = Args::parse_from(["sensing-server"]); - assert!(!args.mqtt, "--mqtt must default to false"); - assert_eq!(args.mqtt_host, "localhost"); - assert_eq!(args.mqtt_prefix, "homeassistant"); // ADR-115 §9.2 - assert_eq!(args.mqtt_refresh_secs, 600); - assert_eq!(args.mqtt_rate_vitals, 0.2); - assert_eq!(args.mqtt_rate_motion, 1.0); - assert_eq!(args.mqtt_rate_count, 1.0); - assert_eq!(args.mqtt_rate_rssi, 0.1); - assert!(!args.mqtt_publish_pose, "pose publish off by default for bandwidth"); - assert_eq!(args.mqtt_rate_pose, 1.0); - assert!(!args.mqtt_tls); - assert!(args.mqtt_username.is_none()); - assert!(args.mqtt_port.is_none()); - } - - /// Privacy mode defaults off so existing deployments don't break; - /// production deployments must opt in. - #[test] - fn privacy_mode_defaults_off() { - let args = Args::parse_from(["sensing-server"]); - assert!(!args.privacy_mode); - } - - /// Matter defaults off; VID is the dev VID per ADR-115 §9.9. - #[test] - fn matter_defaults_off_dev_vid() { - let args = Args::parse_from(["sensing-server"]); - assert!(!args.matter); - assert_eq!(args.matter_vendor_id, "0xFFF1"); - assert_eq!(args.matter_product_id, "0x8001"); - } - - /// Semantic primitives default ON per ADR-115 §3.12. - #[test] - fn semantic_defaults_on() { - let args = Args::parse_from(["sensing-server"]); - assert!(args.semantic); - assert!(args.no_semantic.is_empty()); - assert_eq!(args.semantic_baseline_window_days, 14); - } - - /// All MQTT flags can be set together without conflicts. - #[test] - fn mqtt_all_flags_compose() { - let args = Args::parse_from([ - "sensing-server", - "--mqtt", - "--mqtt-host", "broker.example.com", - "--mqtt-port", "8883", - "--mqtt-username", "ruview", - "--mqtt-prefix", "homeassistant", - "--mqtt-tls", - "--mqtt-refresh-secs", "300", - "--mqtt-rate-vitals", "0.5", - "--mqtt-publish-pose", - "--mqtt-rate-pose", "2.0", - "--privacy-mode", - ]); - assert!(args.mqtt); - assert_eq!(args.mqtt_host, "broker.example.com"); - assert_eq!(args.mqtt_port, Some(8883)); - assert_eq!(args.mqtt_username.as_deref(), Some("ruview")); - assert!(args.mqtt_tls); - assert_eq!(args.mqtt_refresh_secs, 300); - assert_eq!(args.mqtt_rate_vitals, 0.5); - assert!(args.mqtt_publish_pose); - assert_eq!(args.mqtt_rate_pose, 2.0); - assert!(args.privacy_mode); - } - - /// `--no-semantic` is repeatable and accumulates. - #[test] - fn no_semantic_repeatable() { - let args = Args::parse_from([ - "sensing-server", - "--no-semantic", "sleeping", - "--no-semantic", "meeting", - "--no-semantic", "fall_risk", - ]); - assert_eq!(args.no_semantic, vec!["sleeping", "meeting", "fall_risk"]); - } - - // Env-var resolution is covered by clap's own test suite; we don't - // re-test it here because cargo's default parallel runner would race on - // the shared process env. Run `cargo test --test-threads=1` if you need - // local env-var coverage during development. }