fuzzyworld

haunt / files / docs / wifi-rtt-hue-brief.md

wifi-rtt-hue-brief.md

Blame: the commit that last changed each line · file · history

f4823ace 2026-05-13 Pavel Safronov1# WiFi RTT → Hue Light Activation — Project Brief
2
3## Goal
4An Android app (Kotlin) that uses WiFi Round-Trip Time (RTT) ranging against 3 Google WiFi pucks to fingerprint the phone's position by comparing live distance-triples to calibrated zone readings, then activates the corresponding Philips Hue light via the local Hue Bridge API.
5
6No floor plans, no measuring tape, no coordinate systems required.
7
8---
9
10## Core Concept: Distance-Triple Fingerprinting
11
12Each Google WiFi puck gives a distance reading in meters. Three pucks produce a **distance-triple**:
13```
14reading = (d1, d2, d3) // meters to puck A, B, C respectively
15```
16
17This triple is unique to a physical location — two spots in different rooms will have meaningfully different triples even without knowing where the pucks are. During calibration, you walk to each zone and record its triple. At runtime, you compare the live triple to all stored ones and pick the closest match.
18
19**Match score** (lower = better):
20```
21diff = sqrt((d1_live - d1_cal)² + (d2_live - d2_cal)² + (d3_live - d3_cal)²)
22```
23
24This is the same geometric insight used to reconstruct positions from relative distances alone — no absolute reference frame needed.
25
26---
27
28## Architecture
29
30```
31Android App (Kotlin)
32 ├── RttRangingManager — ranges against 3 Google WiFi APs, returns distance-triple
33 ├── CalibrationStore — saves/loads named zone fingerprints (JSON on device)
34 ├── ZoneMatcher — nearest-neighbor match in distance-triple space
35 ├── HueBridgeClient — calls local Hue Bridge REST API to toggle light
36 └── MainActivity — orchestrates calibration mode and live mode
37```
38
39No trilateration. No coordinate system. No backend server.
40
41---
42
43## Prerequisites
44
45### Hardware
46- 3x Google WiFi pucks (original squat-cylinder model, 2016+)
47 - Natively support IEEE 802.11mc FTM RTT on the 5 GHz band
48- Philips Hue Bridge (v2 square, connected to same LAN)
49- Android phone: Pixel 2 or newer, Android 9+ (Android 10+ recommended)
50
51### Accounts / Tokens
52- **Hue Bridge IP**: discoverable via `https://discovery.meethue.com` or your router's DHCP table
53- **Hue API username**: created by pressing the Bridge button and hitting `POST /api` (see setup steps below)
54
55### Dev Environment
56- Android Studio (Ladybug or later)
57- Kotlin (bundled with Android Studio)
58- minSdk 28 (Android 9 / Pie) — required for WifiRttManager API
59- targetSdk 35
60
61---
62
63## Setup Steps
64
65### 1. Find Puck BSSIDs
66You need the MAC address (BSSID) of each puck's 5 GHz radio so the app knows which RTT responses to use.
67
68Options:
69- Google Home app → device details (may show MAC)
70- `adb shell dumpsys wifi` while connected — look for your network's BSSIDs
71- The app can scan and list all RTT-capable APs on first launch for you to identify and hardcode
72
73Record them:
74```kotlin
75val knownBssids = listOf(
76 "AA:BB:CC:DD:EE:01", // Puck 1 (label with tape so you know which is which)
77 "AA:BB:CC:DD:EE:02", // Puck 2
78 "AA:BB:CC:DD:EE:03", // Puck 3
79)
80```
81Order must stay consistent — d1 always means puck 1, etc.
82
83### 2. Get Hue API Username
84```bash
85# 1. Press the physical button on your Hue Bridge
86# 2. Within 30 seconds, run:
87curl -X POST http://<bridge-ip>/api \
88 -H "Content-Type: application/json" \
89 -d '{"devicetype":"wifi_rtt_hue#android"}'
90# Response contains your username token — save it
91```
92
93### 3. Find Hue Light IDs
94```bash
95curl http://<bridge-ip>/api/<username>/lights
96# Returns JSON listing all lights and their integer IDs
97```
98
99### 4. Calibrate Zones (in-app, no measuring)
1001. Open the app, enter **Calibration Mode**
1012. Stand in a zone (e.g. living room), tap **"Add Zone"**
1023. Name it (e.g. "Living Room"), enter the Hue light ID for that room
1034. App takes N RTT readings, averages them, stores the distance-triple
1045. Repeat for each zone
1056. Done — no rulers involved
106
107---
108
109## Android Project Structure
110
111```
112app/
113└── src/main/
114 ├── AndroidManifest.xml
115 └── java/com/example/wifirtthue/
116 ├── MainActivity.kt — tab/mode switcher: Calibrate vs Live
117 ├── model/
118 │ ├── DistanceTriple.kt — data class: d1, d2, d3 (Doubles, meters)
119 │ └── ZoneFingerprint.kt — data class: name, hueLightId, triple
120 ├── ranging/
121 │ └── RttRangingManager.kt — wraps WifiRttManager, returns DistanceTriple
122 ├── calibration/
123 │ ├── CalibrationStore.kt — persist/load List<ZoneFingerprint> as JSON
124 │ └── CalibrationViewModel.kt — drives calibration UI, averages N readings
125 ├── matching/
126 │ └── ZoneMatcher.kt — finds nearest ZoneFingerprint by diff score
127 └── hue/
128 └── HueBridgeClient.kt — Retrofit calls to Hue Bridge
129```
130
131---
132
133## Key Android Permissions (AndroidManifest.xml)
134
135```xml
136<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
137<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
138<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
139<uses-permission android:name="android.permission.INTERNET" />
140<uses-feature android:name="android.hardware.wifi.rtt" android:required="true" />
141```
142
143---
144
145## Key Dependencies (libs.versions.toml)
146
147```toml
148[versions]
149kotlin = "2.0.0"
150coroutines = "1.8.1"
151retrofit = "2.11.0"
152okhttp = "4.12.0"
153
154[libraries]
155kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
156retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
157retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
158okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
159gson = { module = "com.google.code.gson:gson", version = "2.11.0" }
160```
161
162---
163
164## Core Logic Sketches
165
166### DistanceTriple.kt
167```kotlin
168data class DistanceTriple(val d1: Double, val d2: Double, val d3: Double) {
169 fun distanceTo(other: DistanceTriple): Double = sqrt(
170 (d1 - other.d1).pow(2) + (d2 - other.d2).pow(2) + (d3 - other.d3).pow(2)
171 )
172
173 companion object {
174 fun average(readings: List<DistanceTriple>) = DistanceTriple(
175 readings.map { it.d1 }.average(),
176 readings.map { it.d2 }.average(),
177 readings.map { it.d3 }.average(),
178 )
179 }
180}
181```
182
183### ZoneMatcher.kt
184```kotlin
185fun findNearestZone(live: DistanceTriple, zones: List<ZoneFingerprint>): ZoneFingerprint? =
186 zones.minByOrNull { it.triple.distanceTo(live) }
187```
188
189### Hue toggle (HueBridgeClient.kt)
190```
191PUT http://<bridge-ip>/api/<username>/lights/<id>/state
192Body: {"on": true, "bri": 200}
193```
194
195---
196
197## App Flow
198
199### Calibration Mode
200```
201For each zone:
202 1. User stands in zone, taps "Add Zone"
203 2. App fires N=5 RTT ranging passes (coroutine loop)
204 3. Averages the N DistanceTriples → stored as zone fingerprint
205 4. User names zone + enters Hue light ID
206 5. Saved to CalibrationStore (JSON on device)
207```
208
209### Live Mode
210```
211Loop every 2 seconds:
212 1. Fire RTT ranging → live DistanceTriple
213 2. ZoneMatcher.findNearestZone()
214 3. If same zone detected K=3 consecutive times (debounce):
215 → HueBridgeClient.turnOn(zone.hueLightId)
216 → optionally turn off previous zone's light
217```
218
219---
220
221## Accuracy Expectations
222
223| Condition | Expected zone accuracy |
224|---|---|
225| Same floor, rooms 3m+ apart | Reliable room-level detection |
226| Open plan with subtle zones | May need more calibration points per zone |
227| Different floor from pucks | Unreliable — treat as a separate calibration set |
228
229More calibration points per zone (taken from slightly different spots within the zone) improve robustness significantly.
230
231---
232
233## Development Order
234
2351. **Scaffold** project, add permissions, confirm `WifiRttManager.isAvailable()` returns true on device
2362. **RttRangingManager** — scan for RTT-capable APs, log BSSIDs, return raw DistanceTriple
2373. **CalibrationStore** — save/load fingerprints as JSON; unit test serialization
2384. **ZoneMatcher** — unit test with synthetic triples before wiring to real data
2395. **CalibrationViewModel + UI** — walk-and-tap calibration flow
2406. **HueBridgeClient** — test light toggle independently with a hardcoded light ID
2417. **Wire live mode** in MainActivity with debounce logic
2428. **Tune** — adjust N (readings averaged) and K (debounce count) for your home
243
244---
245
246## Notes & Gotchas
247
248- **5 GHz only**: Google WiFi pucks only support FTM RTT on the 5 GHz radio. Confirm all 3 pucks appear in RTT scan results before calibrating.
249- **Location permission is mandatory** at runtime — RTT ranging returns nothing without it, with no error thrown.
250- **BSSID order must be stable** — always map puck 1 → d1, puck 2 → d2, puck 3 → d3. If a puck is temporarily unreachable, the triple is invalid; skip that reading.
251- **BSSID randomization** — Android randomizes MACs for passive scans. RTT uses the real BSSID of the AP being ranged against, but confirm this works on your specific device/Android version early.
252- **Hue Bridge must be on the same LAN** — won't work over mobile data.
253- **Recalibrate if pucks move** — fingerprints are relative to puck positions; moving a puck invalidates all stored zones.
254- **Multiple calibration points per zone** — instead of one averaged triple per zone, consider storing 3–5 samples from different spots within the zone and matching against all of them (take the best match across all samples). This handles within-room variance better.