feat: bluetooth and magnetometer channels — two more real instruments
Both measure genuine physics rather than dressing up a random number. Bluetooth (lib/bluetooth.ts): BLE advertises in the 2.4GHz ISM band, and the human body is mostly water, which absorbs 2.4GHz strongly — the same physics that makes a microwave work and that degrades your wifi when someone stands between you and the router. So RSSI genuinely drops when a body crosses the path. That makes signal-strength variance a real, physically-grounded movement signal. The module reports exactly that and nothing more: it never claims a drop *is* a presence, only that the field changed. Threshold is 6dB — above the 2-4dB of idle multipath wander, and inside the 3-10dB a real body actually causes. Magnetometer (lib/magnetometer.ts): the existing EMF mode infers field disturbance from DeviceMotion/DeviceOrientation, which is a real measurement but measures *movement*, not magnetism — a phone sitting still beside a running motor reads nothing. This reads the actual magnetometer, so the EMF meter measures what an EMF meter is supposed to. Real ghost-hunting EMF meters are just magnetometers, and the spikes they pick up come from mains wiring, motors and moving ferrous mass — all of which this picks up, for the same real reasons. 3uT threshold clears the ~0.5-1uT sensor noise while still catching household sources. Earth's constant 25-65uT background is explicitly what the rolling baseline exists to subtract. Both are additive: neither replaces the existing motion-based EMF, which stays the fallback because it works on iOS where neither of these do (no Web Bluetooth, no Generic Sensor API in any iOS browser). Both reuse the time-aware EMA baseline shape from coldSpot.ts, since advertisement and sensor intervals are irregular and a fixed per-sample alpha would weight a burst and a long gap identically. Web Bluetooth types are declared locally rather than pulling in @types/web-bluetooth for three shapes — same approach lib/emf.ts already takes with its non-standard sensor types. Also renders unprompted 'manifest' utterances as an intrusion: violet edge, full opacity (unlike the faded ambient/fragment murmurs), and a brief blur-in. The unsettling part is that it is perfectly clear and completely unbidden. 355 frontend tests pass (26 new); i18n parity gate passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1059,6 +1059,42 @@
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* Unprompted speech: nobody asked for this. Treated as an intrusion
|
||||
rather than a reply — a violet edge marks it as arriving from outside
|
||||
the conversation, and it sits at full opacity (unlike the deliberately
|
||||
faded ambient/fragment murmurs) because the unsettling part is that it
|
||||
is perfectly clear and completely unbidden. */
|
||||
.tx-utterance.kind-manifest {
|
||||
border-left: 2px solid rgba(178, 107, 255, 0.6);
|
||||
padding-left: 0.6rem;
|
||||
margin-left: -0.2rem;
|
||||
}
|
||||
|
||||
.tx-utterance.kind-manifest .tx-text {
|
||||
color: #ecdcff;
|
||||
text-shadow: 0 0 9px rgba(178, 107, 255, 0.45);
|
||||
animation: manifestArrive 620ms ease-out both;
|
||||
}
|
||||
|
||||
@keyframes manifestArrive {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-5px);
|
||||
filter: blur(2.5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tx-utterance.kind-manifest .tx-text {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tx-utterance.speaking .tx-speaker {
|
||||
animation: flickerAnim 1.2s linear infinite;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user