# Channels to the dead — what is real, and how real Every channel below measures something physically real. None of them simulate, interpolate, or invent a signal. This document exists so that claim stays honest as the app grows: if you add a channel, add a row, and be specific about what it actually measures and what has actually been verified. **The rule:** the app may *interpret* a measurement however the fiction likes ("the air is thick tonight"), but it must never *fabricate* the measurement itself. A reading shown to a seeker is a reading something took. --- ## Channels | Channel | Measures | Platform | Verified? | |---|---|---|---| | **Microphone (EVP)** | Live FFT of room audio via `AnalyserNode` | Everything incl. iOS | Yes — live | | **Motion (EMF)** | Accelerometer + gyroscope via DeviceMotion | Everything incl. iOS | Yes — live | | **Magnetometer** | True magnetic field, µT, via Generic Sensor API | Chromium/Android | Code + unit tests; **no hardware test** | | **Bluetooth** | BLE advertisement RSSI | Chromium desktop/Android | Code + unit tests; **no hardware test** | | **RTL-SDR** | RF spectrum 88–108 MHz via WebUSB | Chromium desktop/Android | Code + unit tests; **unverified against a real dongle** | | **Network (wire)** | Real throughput/latency/jitter counters | Server-side | Yes — live | | **ESP32 node** | Temperature, pressure, mic level, radar presence | LAN → HTTP | Firmware written; **never flashed to hardware** | | **Astronomy** | Moon phase, true solar midnight | Computed, offline | Yes — validated against published ephemerides | | **Geomagnetic** | NOAA planetary K-index | WAN → NOAA SWPC | Yes — verified against the live endpoint | ### Platform walls that cannot be coded around - **iOS has no WebUSB** — the RTL-SDR cannot work on iPhone. Not a bug, not a polyfill away. Apple does not ship the API and every iOS browser is WebKit underneath. - **iOS has no Web Bluetooth** and **no Generic Sensor API** — same reason. - **No browser can scan WiFi.** There is no `navigator.wifi` on any platform. Browsers deliberately withhold AP lists and RSSI because it is a location-inference vector. WiFi spectrum is only available from the ESP32. On iOS the working channels are the microphone, motion, network, astronomy, geomagnetic, and anything the ESP32 reports over the LAN. --- ## Randomness: the room decides `app/entropy.py` + `frontend/src/lib/entropy.ts` Contact used to be a database lookup — `signature_from_anomalies()` hashed the anomaly pattern, so identical conditions always produced an identical spirit. Now the client harvests real physical noise (microphone and RF noise floors — thermal noise in the ADC, room acoustics, atmospheric RF), Von Neumann debiases it, conditions it with SHA-256, and contributes it to every summon. **The client is untrusted by construction.** A contribution is never used as a seed. Every draw is: ``` HMAC-SHA256(fresh server secret, client bytes || context) ``` Because fresh CSPRNG server bytes are present on every single call, the output is unpredictable and uniformly distributed *no matter what the client sends* — all-zeros, a replayed value, or one chosen adversarially. The room can only ever **add** unpredictability; it can never steer a result. `tests/test_entropy.py` asserts this directly rather than assuming it: 400 replays of one contribution stay uniformly distributed. If you add a new draw, use `veil_random(contribution, context)` with a distinct `context` string. Domain separation is why learning an entity's visible rarity tells you nothing about its hidden alignment. --- ## Generation from nothing `SpiritService.manifest()` Unprompted speech is deliberately **not** `chat_stream` with an empty question. Two things make it generation *from* something rather than a reply *to* something: 1. **No seeker input in the prompt at all.** The model's only stimulus is measured room state, rendered as measurements (`deviation above the floor: 31.4`) rather than interpretations (`terrifying spike`). Putting the conclusion in the prompt would mean the horror came from us instead of from the entity. 2. **The sampling seed is physical.** Ollama's `seed` option fixes the token-sampling path, and it is derived from entropy harvested in that room. The room genuinely selects the words. Change the noise, get different speech; two rooms cannot produce the same utterance. --- ## Tuning knobs These are taste calls, not derived constants. They are the first things to adjust if the feel is wrong: | Constant | File | Meaning | |---|---|---| | `RETURN_CHANCE` | `app/ws.py` | Odds a channel's familiar spirit answers (0.72) | | `VEIL_THINNESS_PULL` | `app/ws.py` | How much a thin veil favours strangers (0.45) | | `MANIFEST_CHANCE_ON_ANOMALY` | `app/ws.py` | Odds a room-shift pulls unprompted speech (0.28) | | `VOICE_ARCHETYPES` | `app/entities.py` | The eight throats a spirit can be drawn with | --- ## If you add a channel 1. Measure something real. If you cannot, do not add it. 2. Put the pure maths in its own module with a rolling-baseline core, and unit-test that core without any hardware object. See `lib/coldSpot.ts`, `lib/bluetooth.ts`, `lib/magnetometer.ts` — they share one shape deliberately, because sensor intervals are irregular and a fixed per-sample alpha would weight a burst and a long gap identically. 3. Feed its frames into the entropy pool. More real noise is strictly better. 4. Add a row to the table above, and be honest in the "Verified?" column. "Written carefully" is not "tested against hardware."