mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
2bccdf5065
* 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.
475 lines
19 KiB
Markdown
475 lines
19 KiB
Markdown
# 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](docs/adr/ADR-125-ruview-apple-home-native-hap-bridge.md).
|
||
|
||
---
|
||
|
||
## 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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
ping 192.168.1.20
|
||
```
|
||
|
||
### Step 2: Create a Python venv on the Mac and install HAP-python
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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)
|
||
|
||
```bash
|
||
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)
|
||
|
||
```bash
|
||
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)
|
||
|
||
```bash
|
||
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 1–2 seconds.
|
||
|
||
---
|
||
|
||
## Per-Room Expansion
|
||
|
||
To monitor multiple rooms, run multiple C6 nodes, each with its own `c6-presence-watcher.py` instance:
|
||
|
||
```bash
|
||
# 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 0–1 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](docs/adr/ADR-118-bfld-beamforming-feedback-layer-for-detection.md).
|
||
|
||
---
|
||
|
||
## Siri-by-Room
|
||
|
||
Name each HomeKit accessory after its room. The HAP bridge pulls room names from the state file prefixes:
|
||
|
||
```bash
|
||
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 **Automation** → **Create New Automation** → **When an Accessory is Controlled**.
|
||
2. Select **Living Room** → **Programmable Switch** → **Single 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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
# 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`](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:
|
||
|
||
```bash
|
||
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
|
||
|
||
```python
|
||
# 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](docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md).
|
||
|
||
---
|
||
|
||
## 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:
|
||
```bash
|
||
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 Accessory** → **More 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:
|
||
```bash
|
||
nc -u -l 5005 & # Listen on UDP 5005
|
||
# You should see occasional packets from the C6
|
||
```
|
||
3. Manually query the sensing server:
|
||
```bash
|
||
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
|
||
|
||
- [ADR-125 — RuView ↔ Apple Home native HAP bridge](docs/adr/ADR-125-ruview-apple-home-native-hap-bridge.md) — Design spec, privacy rationale, sequencing
|
||
- [ADR-118 — Beamforming Feedback Layer for Detection](docs/adr/ADR-118-bfld-beamforming-feedback-layer-for-detection.md) — BFLD privacy gate and identity-risk semantics
|
||
- [ADR-124 — rvagent MCP / RuVector npm integration](docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md) — MCP tool specification
|
||
- [Issue #796](https://github.com/ruvnet/RuView/issues/796) — Tier 1+2 sprint tracking (close-out comments have per-iter empirical data)
|
||
- [scripts/macos-shortcuts/README.md](scripts/macos-shortcuts/README.md) — Shortcuts-as-glue setup and troubleshooting
|
||
- [HomeKit Accessory Protocol (Non-Commercial Version)](https://developer.apple.com/apple-home/) — HAP-1.1 spec
|
||
- [HAP-python on GitHub](https://github.com/ikalchev/HAP-python) — Implementation library
|