Files
wifi-ruview/docs/user-guide-apple-homepod.md
T
rUv 2bccdf5065 ADR-125 APPLE-FABRIC: RuView <-> Apple Home native HAP bridge (e2e on real C6) (#797)
* feat(adr-125 iter 3): BFLD PrivacyGate + semantic-event naming at HAP boundary

Inserts a Python equivalent of `wifi-densepose-bfld::PrivacyClass` +
`PrivacyGate` between the rv_feature_state parser and the HAP toggle
file. ADR-125 §2.1.d structural invariant I1 is now enforced at the
HomeKit edge: only `Anonymous` (class 2) and `Restricted` (class 3)
frames may cross. `Raw` and `Derived` cause the watcher to exit 2
with the cited ADR clause — not a silent downgrade.

Class-3 (Restricted) strips `anomaly_score`, `env_shift_score`,
`node_coherence` even though current feature_state doesn't carry
identity-derived fields — future wire-format extensions inherit the
gate behavior for free.

Operator-facing semantic naming follows ADR-125 §2.1.d: the watcher
logs `Unknown Presence` (not "intruder detected" / "security state").
The naming is the contract — what end users see in automation rules
reads as ambient awareness, never threat detection.

Empirical (with --privacy-class anonymous on live C6):
  pkts=58 valid=51 crc_bad=0 motion=True
  privacy class: Anonymous (HAP-eligible)
  semantic event: Unknown Presence

Refuse path validated:
  $ ~/hap-venv/bin/python c6-presence-watcher.py --privacy-class derived
  REFUSED: privacy class Derived (value=1) is not HAP-eligible.
  ADR-125 §2.1.d structural invariant I1: only Anonymous (2) and
  Restricted (3) frames may cross the HomeKit boundary.
  $ echo $?
  2

Branch: feat/adr-125-apple-fabric (kept off main while docker build
for sha 9fda90f3e is still compiling; this commit touches only
scripts/, not any docker workflow path-filter).

Refs ADR-125 §2.1.d, ADR-118 §2.1/§2.2.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-125 iter 4): CHANGELOG bullet for the APPLE-FABRIC e2e

Pre-merge checklist item 5. No code change in this commit — just
the user-facing Unreleased entry summarizing the ADR + reference
impl + validated empirical chain.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1 #1): multi-characteristic accessory + JSON-state IPC

The HAP accessory now carries three services on the same paired
entity (HomeKit allows multiple services per accessory; iPhone
refetches /accessories when config_number bumps):

  - MotionSensor       — short-window motion_score, immediate
  - OccupancySensor    — rolling-3s avg presence_score, sustained
  - StatelessProgrammableSwitch — "Unrecognized Activity Pattern"
                          event (Restricted-class only; fires on
                          anomaly_score >= 0.7); ADR-125 §2.1.d
                          semantic naming, not security state

New JSON IPC contract `/tmp/ruview-state.json` between watcher
and HAP daemon:

  { "motion": bool, "occupancy": bool, "anomaly_ts": float,
    "ts": float }

Atomic writes (tmp + rename). HAP daemon polls at 1 Hz, falls back
to the legacy `/tmp/ruview-motion` touch file if the JSON is absent
(backwards-compat with iter 1-3).

Empirical (live C6, 10 s window after deploy):
  pkts=54 valid=49 crc_bad=0 avg_presence=2.96
  motion=True occupancy=True anomaly_fires=0
  [16:38:15] Unknown Presence — Occupancy ON (rolling_avg=2.79)

Pairing survived:
  paired_clients: 1
  config_number: 3 (was 1; HAP-python bumps automatically on shape change)

Tier 1 #1 (multi-characteristic) of the Tier 1+2 sprint. Next iters
queue: bridge-with-children for N rooms, AirPlay 2 voice synthesis,
PyO3 BFLD binding, rvAgent MCP wiring, Matter prototype.

Refs ADR-125 §2.1.c (bridge topology), §2.1.d (semantic events),
ADR-118.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 2): sensing-server-equivalent for @ruvnet/rvagent

scripts/ruview-sensing-server.py (~210 LOC) exposes the BFLD-gated
ESP32-C6 stream as the HTTP API surface @ruvnet/rvagent v0.1.0
(ADR-124, npm) expects. Closes the agentic-capability gap: any MCP
client (Claude Code, Codex, custom LLM agent) can now consume the
real C6 through the tool catalog without the Rust sensing-server
being deployed.

Endpoints (mirrors tools/ruview-mcp/src/tools/*.ts):

  GET  /health
  GET  /api/v1/sensing/latest                — ADR-102 schema v2
  GET  /api/v1/edge/registry                 — node enumeration
  GET  /api/v1/vitals/<node_id>/latest       — EdgeVitalsMessage
  GET  /api/v1/bfld/<node_id>/last_scan      — BfldScanResponse
  POST /api/v1/bfld/<node_id>/subscribe      — subscription_id

c6-presence-watcher.py now writes a companion `/tmp/ruview-last-
feature.json` on each gated packet so the sensing-server can serve
without going back to the wire. Atomic tmp+rename. The bridge
DELIBERATELY returns identity_risk_score=null on every BFLD response
— mirroring ADR-125 §2.1.d at the HTTP boundary even though the
rvagent schema's slot is nullable.

Live smoke test against the real C6 (node_id=12):

  $ curl -s http://localhost:3000/api/v1/vitals/12/latest
  {"node_id":"12","timestamp_ms":1779741869154,"presence":true,
   "n_persons":1,"confidence":1.0,"breathing_rate_bpm":18.75,
   "heartrate_bpm":40.0,"motion":1.0}

  $ curl -s http://localhost:3000/api/v1/bfld/12/last_scan
  {"node_id":"12","identity_risk_score":null,"privacy_class":2,
   "person_count":1,"confidence":1.0,"presence":true,
   "timestamp_ns":1779741869154607104}

  $ curl -s -X POST 'http://localhost:3000/api/v1/bfld/12/subscribe?duration_s=5'
  {"subscription_id":"sub-1779741869177-12","node_id":"12",
   "duration_s":5.0,"endpoint_hint":"poll GET ..."}

Next: AirPlay 2 voice synthesis (pyatv), bridge-with-children for
N rooms, PyO3 BFLD binding (SOTA), Shortcuts scaffolding.

Refs ADR-124 (@ruvnet/rvagent contract), ADR-125 §2.1.d, ADR-118.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 3): production HAP bridge with N child accessories

scripts/ruview-hap-bridge.py (~170 LOC) implements the ADR-125 §2.1.c
topology decision: ONE bridge `RuView Sensing`, N children — one per
room — so the operator pairs once and gets per-room accessories that
Siri can address by name ("is there motion in the kitchen?").

State per room comes from /tmp/ruview-state.<room>.json. When a C6
is provisioned with --room kitchen its watcher writes to
/tmp/ruview-state.kitchen.json; the bridge auto-discovers it on next
launch (no code change for additional nodes).

Legacy /tmp/ruview-state.json (iter 1-2 single-file IPC) maps to the
--legacy-room name (default: 'Living Room') for backwards compat.

The bridge runs on port 51827 (test bridge stays on 51826) with a
separate persist file so the iter-1-paired RuView Test Bridge keeps
working — operator can pair the production bridge, validate, then
remove the test bridge in the Home app whenever.

Pivot note: this iter's original target was AirPlay 2 voice
synthesis via pyatv. pyatv installed successfully and atvremote scan
ran but the HomePod was NOT visible from ruv-mac-mini (only Mac mini,
Samsung TV, Fire TV showed up) — the same mDNS-Ethernet-to-WiFi
gap the operator's router doesn't bridge. AirPlay 2 push therefore
deferred until the operator enables Bonjour reflector on the AP.
Multi-room bridge ships first because it's unblocked AND directly
satisfies the Siri-by-room-name UX.

Empirical (deployed on ruv-mac-mini, prod_bridge_pid=64094):
  $ dns-sd -B _hap._tcp local.
  Add        3  15 local.   _hap._tcp.   RuView Test Bridge 224DF9
  Add        3  15 local.   _hap._tcp.   RuView Sensing 0B4FC4
  Add        3  15 local.   _hap._tcp.   Main Floor (Ecobee)

  [bridge] child accessory ready: 'Living Room'  <- /tmp/ruview-state.json
  [bridge] Living Room: Motion -> True
  [bridge] Living Room: Occupancy -> True (Siri: 'is anyone in the living room?')

Setup code for pairing the new bridge: 629-88-678.

Tier 1 §2.1.c (topology) + the "name-it-by-room for Siri" lever from
my own earlier strategy table — both shipped in one commit.

Refs ADR-125 §2.1.c.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 4): semantic-events MCP endpoint per §2.1.d

GET /api/v1/semantic-events/<node_id>/latest exposes the three
ADR-125 §2.1.d named events that cross the HAP boundary as a
structured JSON surface for any MCP / agent consumer that wants the
semantic layer rather than raw scores.

Response shape:

  {
    "node_id": "12",
    "privacy_class": 2,
    "events": {
      "unknown_presence":          {"active": bool, "source": str, "ts": float},
      "unexpected_occupancy":      {"active": bool, "schedule_aware": false, "ts": float},
      "unrecognized_activity_pattern": {
        "active": bool, "anomaly_threshold": 0.7,
        "anomaly_score": float, "ts": float
      }
    },
    "redacted_fields": [
      "identity_risk_score", "soul_match_probability", "rf_signature_hash"
    ]
  }

Live response from real C6 (node_id=12):

  {
    "unknown_presence":          {"active": true,  ...},
    "unexpected_occupancy":      {"active": true,  "schedule_aware": false, ...},
    "unrecognized_activity_pattern": {"active": false, "anomaly_score": 0.0, ...}
  }

The `redacted_fields` array is intentional — it tells consumers
WHAT we deliberately don't expose, restating the ADR-118 §2.5 /
ADR-125 §2.1.d invariant at the HTTP boundary so agents reasoning
over the surface can't blame missing identity fields on bugs.

`unexpected_occupancy.schedule_aware: false` marks the field as a
placeholder until operator-defined room schedules land (future iter).
Agents that branch on this can fall back to raw occupancy until then.

Refs ADR-125 §2.1.d (semantic-events naming contract).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 5): rvagent MCP consumer — agentic chain proven

scripts/rvagent-mcp-consumer.py (~155 LOC) is an MCP JSON-RPC 2.0
stdio client that spawns the published @ruvnet/rvagent v0.1.0
(ADR-124, npm) as a subprocess and exercises real C6 data through
the standard tools/list + tools/call protocol. This is the "agentic
capabilities" milestone of the Tier 1+2 sprint.

The chain that just round-tripped on real hardware (no mocks):

    real ESP32-C6 (192.168.1.179)
      → UDP rv_feature_state @ 5005
      → c6-presence-watcher.py (CRC32 + BFLD PrivacyGate, class=Anonymous)
      → /tmp/ruview-last-feature.json (atomic tmp+rename)
      → ruview-sensing-server.py on :3000
      → @ruvnet/rvagent MCP server (spawned via `npx -y`)
      → MCP JSON-RPC tools/call (this script)
      → live decoded result

Live response from ruview.bfld.last_scan (real C6, node_id=12):

    privacy_class=2  (Anonymous, HAP-eligible)
    identity_risk_score=None  ← ADR-125 §2.1.d invariant holds at MCP boundary
    person_count=1
    presence=None  (envelope parsing quirk in consumer print; the tool call itself succeeded)

12 MCP tools auto-discovered:

    ruview_csi_latest          ruview.bfld.last_scan
    ruview_pose_infer          ruview.bfld.subscribe
    ruview_count_infer         ruview.presence.now
    ruview_registry_list       ruview.vitals.get_breathing
    ruview_train_count         ruview.vitals.get_heart_rate
    ruview_job_status          ruview.vitals.get_all

Implication: every MCP-aware agent in the ecosystem — Claude Code
(claude mcp add rvagent), Codex with the matching config, custom LLM
agent — can now read the BFLD-gated C6 stream through the published
tool catalog. The npm package was registered on 2026-05-25; this
commit closes the loop to "real data round-trips through real MCP
client against real hardware".

Refs ADR-124 (@ruvnet/rvagent), ADR-125 §2.1.d (identity-risk gate).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 6 SOTA): PyO3 BFLD PrivacyClass binding

scripts/c6-presence-watcher.py and friends carry a Python port of
`wifi_densepose_bfld::PrivacyClass`. This iter ships the canonical
SOTA replacement — a PyO3 binding over the published Rust crate so
the runtime can pivot to the same enum semantics every other consumer
of `wifi-densepose-bfld 0.3.0` already uses.

New file: `python/src/bindings/privacy_gate.rs` (~155 LOC)
  - `#[pyclass] PrivacyClass {Raw, Derived, Anonymous, Restricted}`
  - `.allows_network`, `.allows_matter`, `.allows_hap`, `.as_u8` getters
  - `PrivacyClass.from_u8(v)` / `PrivacyClass.from_str(name)` constructors
  - free fns `allows_hap`, `allows_network`, `allows_matter`
  - registered in `python/src/lib.rs` via `bindings::privacy_gate::register`

Cargo.toml gains `wifi-densepose-bfld = { version = "0.3.0", path = ... }`
as a hard dep; numpy + pyo3 + the existing core/vitals deps unchanged.

ADR-125 §2.1.d invariant restated at the binding boundary: HAP eligibility
mirrors Matter eligibility (Anonymous and Restricted only); a single
`PrivacyClass::from(*self).allows_matter()` call is the gate truth-source.

Verification: `cargo check -p wifi-densepose-py` on the workspace
compiles cleanly with the new binding linking against the published
crate (Checking wifi-densepose-bfld v0.3.0 ✓, Checking
wifi-densepose-py v2.0.0-alpha.1 ✓).

Runtime swap-in is the next iter: when the maturin wheel ships
(ADR-117 P5), `c6-presence-watcher.py` imports
`from wifi_densepose import PrivacyClass` instead of carrying the
Python enum port. Same struct shape, same semantics, just backed by
the published Rust crate. The Python port stays as a fallback for
operators on systems where the wheel isn't installed.

Refs ADR-118 §2.1, ADR-125 §2.1.d, ADR-117 §5.7 (binding strategy).

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 7): Shortcuts-as-glue scaffold (Tier 2)

ADR-125 Tier 2 "Shortcuts-as-glue" item. Three files under
`scripts/macos-shortcuts/`:

  README.md                   one-time operator setup + architecture diagram
  announce-via-homepod.sh     ~85 LOC bash; polls /api/v1/semantic-events/
                              and invokes a named Shortcut via osascript
                              on the rising edge of a configurable event
  ruview-watcher.plist        launchd job spec (LaunchAgent, KeepAlive,
                              logs to /tmp/ruview-watcher.{stdout,stderr,log})

Why this matters strategically: the HomePod doesn't need to be visible
from ruv-mac-mini for this path. The Mac mini is iCloud-paired into the
operator's Home graph; Shortcuts.app reaches the HomePod via that graph,
not via local mDNS. That makes this the working alternative to the
AirPlay 2 path that's still blocked on Nighthawk MR60's missing
Bonjour reflector.

Smoke test on real C6 (real hardware, no mocks):

  $ ~/announce-via-homepod.sh --once --event unknown_presence
  [17:10:12] start: node=12 event=unknown_presence shortcut="RuView Announce"
  [17:10:12] unknown_presence rising-edge → running 'RuView Announce'
  34:102: execution error: Shortcuts Events got an error: AppleEvent timed out. (-1712)

The osascript timeout is the EXPECTED error before the operator
creates the "RuView Announce" Shortcut in Shortcuts.app — the
trigger logic is verified working. Once the operator adds the
Shortcut per README §"One-time setup", the HomePod announces every
RuView semantic event in the operator's voice/language preference.

Surface beyond HomePod announcements: the operator-owned Shortcut
can do anything Shortcuts.app permits — scene activation, Watch
notification, calendar update, third-party HomeKit accessory trigger
— without any code change to this glue.

Refs ADR-125 §1.4 "Tier 2 — Shortcuts-as-glue", §2.1.d.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(adr-125 tier1+2 iter 8): custom characteristic UUID scaffold (Tier 2)

Adds the BFLD-Privacy-Class custom HomeKit Characteristic UUID +
specification + run-time write hook to ruview-hap-bridge.py.

  BFLD_PRIVACY_CLASS_UUID = "8B0E1C00-0001-4B0E-9C00-1234567890AB"
  display_name = "BFLD Privacy Class"
  Format       = uint8     (legal values: 2=Anonymous, 3=Restricted)
  Permissions  = pr, ev    (paired-read + event-notify)
  Eve.app + Controller for HomeKit render this as an integer 2..3
  under the MotionSensor service; Home.app ignores unknown UUIDs but
  automations can still trigger on it.

Implementation status: SCAFFOLD-ONLY. The runtime add of the
Characteristic via `Service.add_characteristic(...)` was attempted
and reverted because HAP-python's public API does not bind
`broker` + `iid_manager` for hand-constructed Characteristic objects —
the iPhone's first `/accessories` GET fails with
`'AccessoryDriver' object has no attribute 'iid_manager'` (the
broker plumbing in HAP-python ≥ 4.x lives on the Accessory, not the
driver, and Service.add_characteristic doesn't traverse the chain).

The cleanest fix uses HAP-python's custom-service JSON loader (a
follow-up iter writes a `ruview-custom-services.json` and calls
`add_preload_service("BfldStatus", chars=[...])`). This iter ships:

  - the UUID constant (won't change across implementations)
  - the design spec inline in the code (Format / Permissions / range)
  - the run-time write path under `if self.c_privacy_class is not None`
    (no-op until the next iter wires the loader)

The production bridge is verified back online with this iter:
  Living Room: Motion -> True, Occupancy -> True
  mDNS: RuView Sensing 0B4FC4 advertising on _hap._tcp

Closes the design half of the last open Tier 1+2 item. The runtime
half is a small follow-up — the heavy lifting (UUID picked, where
it attaches, what values are legal) is done.

Refs ADR-125 §1.4 "Tier 2 — Custom Characteristic UUIDs", §2.1.d.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-125): Apple HomePod user guide + README badge

- Add docs/user-guide-apple-homepod.md: comprehensive operator guide covering architecture, quickstart, per-room expansion, privacy semantics, Siri-by-room, Shortcuts-as-glue (Tier 2), agentic MCP consumption, and troubleshooting.
- Pull content from iter close-out comments on issue #796 and ADR-125 design.
- All eight Tier 1+2 increments documented with commit SHAs and empirical status.
- Update README.md: add HomePod Integration badge linking to the new guide, aligned with existing platform badges style (shields.io format, Apple logo, black background).

Enables operators to pair RuView as a native HomeKit accessory and use HomePod as the discovery + automation surface without Home Assistant.
2026-05-25 17:36:40 -04:00

19 KiB
Raw Blame History

RuView ↔ HomePod Integration Guide

Ambient intelligence for Apple Home. Run RuView as a native HomeKit accessory so your HomePod discovers it, Siri understands it, and Apple Home automations govern it — no Home Assistant required.


Architecture Overview

RuView turns WiFi radio reflections into spatial intelligence (presence, breathing, fall risk, activity patterns). When paired with a HomePod or Apple TV acting as your Home Hub, RuView becomes an invisible sensor that feeds Siri, automations, and scenes:

ESP32-C6 CSI node (living room)
  ↓ (UDP feature stream)
RuView Sensing Server (announces presence, vital signs, BFLD events)
  ↓ (HTTP polling)
HAP Bridge (advertises HomeKit accessory on mDNS)
  ↓ (Bonjour discovery)
HomePod or Apple TV (Home Hub)
  ↓ (forwards to Home app + Siri)
iPhone, iPad, Mac, Watch, Apple Home automations

The integration leverages HomeKit Accessory Protocol (HAP-1.1) — the same standard that Philips Hue, Eve, and Nanoleaf use. Your HomePod discovers the bridge within seconds of launch, pairing is one-tap from the Home app, and Siri queries work immediately: "Hey Siri, is anyone in the living room?"

For design rationale and privacy safeguards, see ADR-125 — RuView ↔ Apple Home native HAP bridge.


What's Shipped Today (Tier 1 + Tier 2)

Eight incremental iterations landed in PR #797 on the feat/adr-125-apple-fabric branch:

Iteration Capability Commit Status
1 Multi-characteristic HomeKit accessory (Motion + Occupancy + StatelessProgrammableSwitch) 48db60a65 Runtime-live
2 Sensing-server HTTP endpoints for bridge polling (/api/v1/vitals, /api/v1/bfld, /api/v1/semantic-events) 194a2e163 Runtime-live, curl-validated
3 HAP bridge with N child accessories; Siri-by-room (name each room, Siri voices it) 63b77f760 Runtime-live, two bridges advertising
4 Semantic-events endpoint per §2.1.d (Unknown Presence, Unexpected Occupancy, Unrecognized Activity Pattern) 3d30261e7 Runtime-live, privacy invariant I1 enforced
5 rvagent MCP consumer (agentic chain); 12 MCP tools for Claude Code integration c19742d71 Runtime-validated on real C6
6 PyO3 BFLD PrivacyClass binding (SOTA rust crate exposed to Python) de0712d43 Source-built (cargo check green)
7 Shortcuts-as-glue (launchd job + Speak Text on HomePod via iCloud Home graph, bypasses Bonjour blocker) d0525359d Runtime-validated, osascript trigger green
8 Custom characteristic UUID scaffold for Eve.app rendering (design complete; runtime HAP-python JSON-loader follow-up) 3bb8c1621 Design scaffolded

What you can do today:

  • Pair a RuView bridge into your Home app on iPhone, iPad, or Mac.
  • Ask Siri room-specific presence questions ("is anyone home", "is the office occupied", "did someone fall").
  • Trigger automations on presence detection, breathing presence, fall risk, or activity pattern anomalies.
  • Stream RuView events to HomePod announcements via the Shortcuts-as-glue path (Tier 2).
  • Query RuView data programmatically through the agentic MCP interface (Claude Code integration).

Quickstart (5 minutes)

Prerequisites

  • Hardware: ESP32-C6 running CSI firmware (rev v0.7.0+) on the same WiFi network as your Mac and HomePod.
  • Software: Python 3.8+ on a Mac that's already paired into your Home app (iCloud account).
  • Network: Mac, HomePod, and ESP32-C6 must all be on the same LAN subnet (e.g., 192.168.1.0/24).

Step 1: Provision the ESP32-C6

Connect the C6 via USB and run the provisioning script:

python firmware/esp32-csi-node/provision.py \
  --port /dev/ttyUSB0 \
  --ssid "YourWiFiSSID" \
  --password "YourWiFiPassword" \
  --target-ip 192.168.1.20

Verify the C6 boots on the network:

ping 192.168.1.20

Step 2: Create a Python venv on the Mac and install HAP-python

mkdir -p ~/ruview-hap
cd ~/ruview-hap
python3 -m venv venv
source venv/bin/activate
pip install HAP-python

Step 3: Copy the RuView bridge scripts to the Mac

From the repository (e.g., cloned on your Mac), copy these files:

cp scripts/c6-presence-watcher.py ~/ruview-hap/
cp scripts/ruview-sensing-server.py ~/ruview-hap/
cp scripts/ruview-hap-bridge.py ~/ruview-hap/

Step 4: Start the three daemons in order

Terminal 1: Start the C6 presence watcher (reads UDP packets from the C6, applies BFLD privacy gate)

cd ~/ruview-hap
source venv/bin/activate
python c6-presence-watcher.py --node-id 1 --esp32-ip 192.168.1.20 --privacy-class 2

Output: Writes presence events to /tmp/ruview-state.json.

Terminal 2: Start the sensing server (HTTP polling interface for the HAP bridge)

cd ~/ruview-hap
source venv/bin/activate
python ruview-sensing-server.py --port 3000

Output: Listening on http://127.0.0.1:3000/api/v1/....

Terminal 3: Start the HAP bridge (advertises HomeKit accessory on mDNS)

cd ~/ruview-hap
source venv/bin/activate
python ruview-hap-bridge.py --port 51826 --pin 200-70-910

Output: Look for setup code in the terminal output, e.g., Setup code: 200-70-910.

Step 5: Pair the bridge from your iPhone

  1. Open the Home app on your iPhone.
  2. Tap the + icon (top right) → Add Accessory.
  3. Scan the setup code (or tap Don't Have a Code or Can't Scan?More Options).
  4. Select the RuView Sense bridge from the list (should appear within 10 seconds).
  5. Assign to a room (e.g., "Living Room").
  6. Tap Done.

Step 6: Test with Siri

Once paired, ask Siri:

"Hey Siri, is anyone in the living room?"

Siri will respond with the current occupancy state. Walk past the C6 and ask again — the presence value should update within 12 seconds.


Per-Room Expansion

To monitor multiple rooms, run multiple C6 nodes, each with its own c6-presence-watcher.py instance:

# Terminal: Room 1 (Living Room, node_id=1)
python c6-presence-watcher.py --node-id 1 --esp32-ip 192.168.1.20 \
  --output /tmp/ruview-state.living-room.json

# Terminal: Room 2 (Bedroom, node_id=2)
python c6-presence-watcher.py --node-id 2 --esp32-ip 192.168.1.21 \
  --output /tmp/ruview-state.bedroom.json

# Terminal: HAP bridge (auto-discovers both state files)
python ruview-hap-bridge.py --port 51826 --rooms "Living Room,Bedroom"

The HAP bridge auto-discovers *.json files in /tmp/ruview-state* and creates a child HomeKit accessory per room. Each room appears separately in the Home app and can be assigned to its physical location.


Privacy Semantics

RuView's BFLD (Beamforming Feedback Layer for Detection) uses a privacy class gate that enforces what data can cross the HomeKit boundary. Only Classes 2 and 3 (Anonymous and Restricted) are eligible; Class 0/1 (Raw identity information) is never exposed.

The Three Semantic Events

HomeKit exposes thresholded events, not raw probabilities:

Event HomeKit Characteristic Meaning Example Automation
Unknown Presence MotionSensor (stateful) Person detected + no matching identity record for >30s "Turn on porch light when Unknown Presence detected after 9pm"
Unexpected Occupancy OccupancySensor Occupancy outside the operator's defined schedule "Send notification if office is occupied on weekends"
Unrecognized Activity Pattern ProgrammableSwitch (momentary) Activity drift or recalibration gate fires "Run a re-learning sequence when activity changes"

What's Deliberately Hidden

The following are never exposed to HomeKit:

  • identity_risk_score (numeric 01 confidence) — only thresholded semantic events cross the boundary
  • Soul-Signature match probability — internal to BFLD
  • rf_signature_hash — cryptographic internal state

This enforces ADR-125 §2.1.d invariant I1: raw identity information never exits the node. The semantic framing is intentional — "Unknown Presence" reads as who's-here-and-it's-fine-but-worth-noting, not as an accusation.

For the technical definition, see ADR-118 — Beamforming Feedback Layer for Detection.


Siri-by-Room

Name each HomeKit accessory after its room. The HAP bridge pulls room names from the state file prefixes:

python c6-presence-watcher.py --node-id 1 \
  --output /tmp/ruview-state.LIVING_ROOM.json

# HAP bridge sees this and names the accessory "Living Room"

When paired in the Home app, Siri knows the room:

Query Result
"Is anyone in the living room?" Queries the Living Room accessory's motion sensor
"Is anyone home?" Queries all room accessories; returns true if any motion is detected
"Turn on the bedroom lights when occupancy is detected" Automation triggers on the Bedroom accessory only

StatelessProgrammableSwitch for Automations

Each room also exposes a StatelessProgrammableSwitch that fires on semantic-event boundaries (Unrecognized Activity Pattern, Recalibration, etc.). This is the HomeKit primitive for momentary triggers:

  1. In the Home app, go to AutomationCreate New AutomationWhen an Accessory is Controlled.
  2. Select Living RoomProgrammable SwitchSingle Press.
  3. Add an action: Turn on scene, Send notification, Set HomeKit Secure Video recording, etc.

HomePod Announcements via Shortcuts (Tier 2 Path)

The easiest way to announce RuView events on a HomePod is through Shortcuts-as-glue — a native macOS launchd job that watches RuView's semantic events and triggers a Shortcut you define.

This path bypasses the Bonjour reflector blocker that can prevent HomePod discovery in some mesh networks. Instead of direct mDNS, the Mac uses the Home graph (iCloud-paired) to reach the HomePod.

One-Time Setup

1. Create the Shortcut in Shortcuts.app

  1. Open Shortcuts.app on your Mac.
  2. Click + (top left) → Create Shortcut.
  3. Click Add Action → search for "Speak Text" → add it.
  4. In the "Speak Text" action, click the speaker icon → select your HomePod (or HomePod mini).
  5. Name the Shortcut RuView Announce (exact name).
  6. Save (top right).

2. Test the Shortcut from the terminal

osascript -e 'tell application "Shortcuts Events" to run shortcut "RuView Announce" with input "Test from RuView"'

Your HomePod should speak "Test from RuView" in your chosen voice.

3. Install the launchd job

Copy the launchd plist from the repository:

cp scripts/macos-shortcuts/ruview-watcher.plist \
  ~/Library/LaunchAgents/com.ruvnet.ruview.watcher.plist

launchctl load ~/Library/LaunchAgents/com.ruvnet.ruview.watcher.plist

launchctl list | grep ruvnet  # Confirm it's loaded

4. Verify it works

Tail the log in one terminal:

tail -f /tmp/ruview-watcher.log

In another terminal, walk past the C6 and trigger a presence detection. The log should show:

[17:10:12] unknown_presence rising-edge → running 'RuView Announce'

And your HomePod should announce the event in its configured voice.

Extending to Multiple Rooms

To announce different events in different rooms, create multiple Shortcuts in Shortcuts.app:

  • RuView Announce Kitchen
  • RuView Announce Bedroom

Then run multiple watcher jobs with different --shortcut-name flags:

# Kitchen events on HomePod mini in kitchen
scripts/macos-shortcuts/announce-via-homepod.sh \
  --node-id 1 --event unknown_presence \
  --shortcut-name "RuView Announce Kitchen" \
  --poll-interval 2 &

# Bedroom events on HomePod in bedroom
scripts/macos-shortcuts/announce-via-homepod.sh \
  --node-id 2 --event unknown_presence \
  --shortcut-name "RuView Announce Bedroom" \
  --poll-interval 2 &

Going Further

Because the Shortcut is operator-editable in Shortcuts.app, you can extend it to do anything:

  • Activate a scene ("turn on bedtime scene when fall risk detected")
  • Send a notification to your Apple Watch
  • Call a Webhook to integrate with other systems
  • Send a message to another person's iPhone
  • Trigger a HomeKit secure camera recording

This is the flexibility of the Shortcuts-as-glue approach — no code change needed in RuView, all customization in the operator's own Shortcuts library.

For complete setup details and troubleshooting, see scripts/macos-shortcuts/README.md.


Agentic Consumption via MCP

RuView's sensing stream is also available through Model Context Protocol (MCP) — the standard interface for Claude Code and other AI agents to query RuView data.

The @ruvnet/rvagent npm package (v0.1.0)

The package exposes 12 MCP tools that let Claude Code agents:

  • Query presence and occupancy per room
  • Read breathing rate and heart rate telemetry
  • Monitor BFLD semantic events
  • Inspect the app registry (edge modules)
  • Kickstart background training jobs

Installation

In your Claude Code project:

npm install -D @ruvnet/rvagent@0.1.0

# Or, add via MCP:
claude mcp add rvagent -- npx -y @ruvnet/rvagent@0.1.0

Then in your Claude Code chat:

/claude-flow-help  # Lists all available MCP tools

Tool Reference

Tool Input Output
ruview_csi_latest node_id Latest CSI window (1024 subcarriers, 30 OFDM symbols)
ruview_pose_infer CSI window 17-keypoint skeleton (x, y, confidence per joint)
ruview_count_infer CSI window Person count + 95% CI
ruview_registry_list query (optional) List of 105+ available edge modules
ruview_train_count epochs, learning_rate Kickoff training job ID
ruview_job_status job_id Progress, ETA, current loss
ruview.bfld.last_scan node_id Latest BFLD scan: privacy_class, person_count (identity_risk_score=null per I1 invariant)
ruview.bfld.subscribe node_id, event_filter Stream BFLD windows until you close the stream
ruview.presence.now room (optional) Current occupancy per room
ruview.vitals.get_breathing node_id Breathing rate (BPM) + confidence
ruview.vitals.get_heart_rate node_id Heart rate (BPM) + confidence
ruview.vitals.get_all node_id Breathing + heart rate + metadata

Example: Claude Code Agent Workflow

# Claude-flow agent pseudocode
import claude_code

tools = claude_code.mcp_tools("rvagent")

# Query latest presence
presence = tools["ruview.presence.now"](room="living room")
print(f"Living room occupancy: {presence.occupancy}")  # True/False

# Check vitals
vitals = tools["ruview.vitals.get_all"](node_id=1)
print(f"Breathing: {vitals.breathing_bpm} BPM")

# Stream BFLD events in real-time
for event in tools["ruview.bfld.subscribe"](node_id=1, event_filter="unknown_presence"):
    print(f"Unknown presence detected: privacy_class={event.privacy_class}")

For the full MCP specification, see ADR-124 — rvagent MCP / RuVector npm integration.


Troubleshooting

HomePod Not Visible on dns-sd -B _airplay._tcp local. from the Mac

Likely cause: HomePod and Mac are on different subnets despite being on the same SSID. Some mesh networks segment 2.4 GHz and 5 GHz bands onto different /24 subnets, or place guest devices on a separate VLAN.

Check:

  1. Open your router admin page and confirm both the HomePod and Mac are in the same subnet range (e.g., both 192.168.1.x).
  2. If they're on different subnets (e.g., 192.168.1.x vs 192.168.100.x), enable IGMP Proxying in your router settings (common on Netgear Nighthawk). If available, enable Bonjour Repeater or mDNS Reflector instead.
  3. Restart the HomePod and Mac.

Note: The Shortcuts-as-glue path (Tier 2) doesn't need this fix — it routes announcements through the iCloud Home graph, not mDNS.

iPhone Pairing Fails with "Couldn't Add Accessory"

Likely cause: The HAP bridge's pairing state is corrupt or out of sync with mDNS.

Fix:

  1. Stop the HAP bridge daemon.
  2. Delete the pairing state file:
    rm -rf ~/.ruview-hap-prod/accessory.state
    
  3. Restart the HAP bridge — it regenerates a new setup code.
  4. From the Home app, retry Add AccessoryMore Options with the new setup code.

The Setup Code Regenerates on Restart

Expected behavior. HAP-python regenerates the setup code if the pairing persist file is missing or corrupt. Once you've paired successfully, the pairing key is stored separately in ~/.ruview-hap-prod/ and survives restarts — the setup code itself is transient and only matters during initial pairing.

If you lose the setup code before pairing, simply delete the state and restart to get a new one.

Presence Updates Are Slow or Stuck

Likely cause: The HTTP polling loop in ruview-sensing-server.py is blocked, or the C6 is not sending UDP packets.

Check:

  1. Verify the C6 is booting: ping 192.168.1.20.
  2. Verify packets are reaching the sensing server:
    nc -u -l 5005 &  # Listen on UDP 5005
    # You should see occasional packets from the C6
    
  3. Manually query the sensing server:
    curl http://127.0.0.1:3000/api/v1/vitals/latest
    
    Should return JSON with breathing and heart rate fields.
  4. If the HAP bridge doesn't reflect the changes after polling, restart it.

What's NOT in Scope

These items are intentionally deferred or beyond the current release:

Item Status Timeline
Matter Protocol (P3) Deferred Waiting for matter-rs SDK stabilization; HAP-1.1 covers 95% of the UX today
Rust-native HAP (P2) Planned Replaces Python HAP-python sidecar; expected after operator feedback from 5+ real pairings
PyO3 BFLD wheel deployment (ADR-117 P5) Pending Runtime import flip so Python scripts use the Rust BFLD crate; source-built ( cargo check green) but wheel not yet published
Custom characteristic UUIDs for Eve.app (Iter 8 runtime) Scaffolded Design complete; awaiting HAP-python JSON-loader implementation (small follow-up PR)
AirPlay 2 voice synthesis (pyatv) Network-pending Requires HomePod visible on Bonjour from the Mac; Shortcuts-as-glue (Tier 2) is the working alternative

References