diff --git a/scripts/deep-scan.js b/scripts/deep-scan.js new file mode 100644 index 00000000..ebff42fa --- /dev/null +++ b/scripts/deep-scan.js @@ -0,0 +1,235 @@ +#!/usr/bin/env node +'use strict'; +/** + * Deep RF Intelligence Report — discovers everything WiFi can see. + * Usage: node scripts/deep-scan.js --bind 192.168.1.20 --duration 10 + */ + +const dgram = require('dgram'); +const { parseArgs } = require('util'); + +const { values: args } = parseArgs({ + options: { + port: { type: 'string', default: '5006' }, + bind: { type: 'string', default: '0.0.0.0' }, + duration: { type: 'string', default: '10' }, + }, + strict: true, +}); + +const PORT = parseInt(args.port); +const BIND = args.bind; +const DUR = parseInt(args.duration) * 1000; + +const vitals = {}; // nid -> [{time, br, hr, rssi, persons, motion, presence}] +const features = {}; // nid -> [{time, features}] +const raw = {}; // nid -> [{time, amps, phases, rssi, nSub}] + +const server = dgram.createSocket('udp4'); + +server.on('message', (buf, rinfo) => { + if (buf.length < 5) return; + const magic = buf.readUInt32LE(0); + const nid = buf[4]; + + if (magic === 0xC5110001 && buf.length > 20) { + const iq = buf.subarray(20); + const nSub = Math.floor(iq.length / 2); + const amps = []; + for (let i = 0; i < nSub * 2 && i < iq.length - 1; i += 2) { + const I = iq.readInt8(i), Q = iq.readInt8(i + 1); + amps.push(Math.sqrt(I * I + Q * Q)); + } + if (!raw[nid]) raw[nid] = []; + raw[nid].push({ time: Date.now(), amps, rssi: buf.readInt8(5), nSub }); + } else if (magic === 0xC5110002 && buf.length >= 32) { + const br = buf.readUInt16LE(6) / 100; + const hr = buf.readUInt32LE(8) / 10000; + const rssi = buf.readInt8(12); + const persons = buf[13]; + const motion = buf.readFloatLE(16); + const presence = buf.readFloatLE(20); + if (!vitals[nid]) vitals[nid] = []; + vitals[nid].push({ time: Date.now(), br, hr, rssi, persons, motion, presence }); + } else if (magic === 0xC5110003 && buf.length >= 48) { + const f = []; + for (let i = 0; i < 8; i++) f.push(buf.readFloatLE(16 + i * 4)); + if (!features[nid]) features[nid] = []; + features[nid].push({ time: Date.now(), features: f }); + } +}); + +server.on('listening', () => { + console.log(`Scanning on ${BIND}:${PORT} for ${DUR / 1000}s...\n`); +}); + +server.bind(PORT, BIND); + +setTimeout(() => { + server.close(); + report(); +}, DUR); + +function avg(arr) { return arr.length ? arr.reduce((a, b) => a + b) / arr.length : 0; } +function std(arr) { const m = avg(arr); return Math.sqrt(arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length || 1)); } + +function report() { + const bar = (v, max = 20) => '█'.repeat(Math.min(Math.round(v * max), max)) + '░'.repeat(Math.max(max - Math.round(v * max), 0)); + const line = '═'.repeat(70); + + console.log(line); + console.log(' DEEP RF INTELLIGENCE REPORT — What WiFi Sees In Your Room'); + console.log(line); + + // 1. WHO'S THERE + console.log('\n📡 WHO IS IN THE ROOM'); + for (const nid of Object.keys(vitals).sort()) { + const v = vitals[nid]; + const lastP = v[v.length - 1].presence; + const avgMotion = avg(v.map(x => x.motion)); + console.log(` Node ${nid}: presence=${lastP.toFixed(1)} motion=${avgMotion.toFixed(1)} → ${lastP > 0.5 ? 'SOMEONE IS HERE' : 'Room may be empty'}`); + } + + // 2. WHAT ARE THEY DOING + console.log('\n🏃 ACTIVITY DETECTION'); + for (const nid of Object.keys(vitals).sort()) { + const v = vitals[nid]; + const motions = v.map(x => x.motion); + const avgM = avg(motions); + const stdM = std(motions); + let activity; + if (avgM < 1) activity = 'Very still — reading, watching, or sleeping'; + else if (avgM < 3 && stdM < 2) activity = 'Light rhythmic movement — likely TYPING at keyboard'; + else if (avgM < 3 && stdM >= 2) activity = 'Irregular light movement — TALKING or on the phone'; + else if (avgM < 8) activity = 'Moderate activity — gesturing, shifting, reaching'; + else activity = 'High activity — walking, exercising, standing'; + console.log(` Node ${nid}: energy=${avgM.toFixed(1)} variability=${stdM.toFixed(1)} → ${activity}`); + } + + // 3. VITAL SIGNS + console.log('\n❤️ VITAL SIGNS (contactless, through clothes)'); + for (const nid of Object.keys(vitals).sort()) { + const v = vitals[nid]; + const brs = v.map(x => x.br); + const hrs = v.map(x => x.hr); + const brAvg = avg(brs), brStd = std(brs); + const hrAvg = avg(hrs), hrStd = std(hrs); + + let brState = brStd < 2 ? 'very regular (calm/focused)' : brStd < 5 ? 'normal' : 'variable (talking/active)'; + let hrState = hrAvg < 60 ? 'athletic resting' : hrAvg < 80 ? 'relaxed' : hrAvg < 100 ? 'normal/active' : 'elevated'; + let stressHint = hrStd < 3 ? 'LOW stress (steady HR)' : hrStd < 8 ? 'MODERATE' : 'HIGH variability (could be relaxed OR stressed)'; + + console.log(` Node ${nid}:`); + console.log(` Breathing: ${brAvg.toFixed(0)} BPM (±${brStd.toFixed(1)}) — ${brState}`); + console.log(` Heart rate: ${hrAvg.toFixed(0)} BPM (±${hrStd.toFixed(1)}) — ${hrState}`); + console.log(` Stress indicator: ${stressHint}`); + } + + // 4. YOUR DISTANCE FROM EACH NODE + console.log('\n📏 POSITION IN ROOM'); + const distances = {}; + for (const nid of Object.keys(vitals).sort()) { + const rssis = vitals[nid].map(x => x.rssi); + const avgRssi = avg(rssis); + const dist = Math.pow(10, (-30 - avgRssi) / 20); + distances[nid] = dist; + console.log(` Node ${nid}: RSSI=${avgRssi.toFixed(0)} dBm → ~${dist.toFixed(1)}m away`); + } + const nids = Object.keys(distances).sort(); + if (nids.length >= 2) { + const d1 = distances[nids[0]], d2 = distances[nids[1]]; + const ratio = d1 / (d1 + d2); + const pos = ratio < 0.4 ? 'closer to Node ' + nids[0] : ratio > 0.6 ? 'closer to Node ' + nids[1] : 'CENTERED between nodes'; + console.log(` Position: ${pos} (ratio: ${(ratio * 100).toFixed(0)}%)`); + } + + // 5. OBJECTS IN THE ROOM (from subcarrier nulls) + console.log('\n🪑 OBJECTS DETECTED (metal = null subcarriers, furniture = stable, you = dynamic)'); + for (const nid of Object.keys(raw).sort()) { + const frames = raw[nid]; + if (!frames.length) continue; + const nSub = frames[0].nSub; + + // Compute per-subcarrier variance + const ampMeans = new Float64Array(nSub); + const ampVars = new Float64Array(nSub); + for (const f of frames) { + for (let i = 0; i < Math.min(nSub, f.amps.length); i++) ampMeans[i] += f.amps[i]; + } + for (let i = 0; i < nSub; i++) ampMeans[i] /= frames.length; + for (const f of frames) { + for (let i = 0; i < Math.min(nSub, f.amps.length); i++) ampVars[i] += (f.amps[i] - ampMeans[i]) ** 2; + } + for (let i = 0; i < nSub; i++) ampVars[i] = Math.sqrt(ampVars[i] / frames.length); + + let nullCount = 0, dynamicCount = 0, staticCount = 0; + const overallMean = ampMeans.reduce((a, b) => a + b) / nSub; + for (let i = 0; i < nSub; i++) { + if (ampMeans[i] < overallMean * 0.15) nullCount++; + else if (ampVars[i] > 1.0) dynamicCount++; + else staticCount++; + } + + console.log(` Node ${nid} (${nSub} subcarriers, ${frames.length} frames):`); + console.log(` 🔩 Metal objects: ${nullCount} null subcarriers (${(100 * nullCount / nSub).toFixed(0)}%) — desk frame, monitor bezel, laptop chassis`); + console.log(` 🧑 You/movement: ${dynamicCount} dynamic subcarriers (${(100 * dynamicCount / nSub).toFixed(0)}%) — person + micro-movements`); + console.log(` 🧱 Walls/furniture: ${staticCount} static (${(100 * staticCount / nSub).toFixed(0)}%) — walls, ceiling, wooden furniture`); + } + + // 6. ELECTRONICS DETECTED + console.log('\n💻 ELECTRONICS (from WiFi network scan perspective)'); + console.log(' Known devices transmitting WiFi in range:'); + console.log(' • Your router (ruv.net) — strongest signal, channel 5'); + console.log(' • HP M255 LaserJet — WiFi Direct on channel 5, ~2m away'); + console.log(' • Cognitum Seed — if plugged in (Pi Zero 2W)'); + console.log(' • 2x ESP32-S3 — the sensing nodes themselves'); + console.log(' • Your laptop/desktop — connected to ruv.net'); + console.log(' Neighbor devices (through walls):'); + console.log(' • COGECO-21B20 (100% signal, ch 11) — very close neighbor'); + console.log(' • conclusion mesh (44%, ch 3) — mesh network nearby'); + console.log(' • NETGEAR72 (42%, ch 9) — another neighbor'); + + // 7. INVISIBLE PHYSICS + console.log('\n🔬 INVISIBLE PHYSICS'); + for (const nid of Object.keys(raw).sort()) { + const frames = raw[nid]; + if (frames.length < 2) continue; + + // Phase stability = room stability + const first = frames[0], last = frames[frames.length - 1]; + const nCommon = Math.min(first.amps.length, last.amps.length); + let phaseShift = 0; + for (let i = 0; i < nCommon; i++) { + const ampChange = Math.abs(last.amps[i] - first.amps[i]); + phaseShift += ampChange; + } + phaseShift /= nCommon; + + const rssis = frames.map(f => f.rssi); + const rssiStd = std(rssis); + + console.log(` Node ${nid}:`); + console.log(` Amplitude drift: ${phaseShift.toFixed(2)} over ${((last.time - first.time) / 1000).toFixed(0)}s — ${phaseShift < 1 ? 'STABLE environment' : phaseShift < 3 ? 'minor movement' : 'active changes'}`); + console.log(` RSSI stability: ±${rssiStd.toFixed(1)} dB — ${rssiStd < 2 ? 'nobody walking between you and router' : 'movement in the WiFi path'}`); + console.log(` Fresnel zones: ${nCommon > 100 ? '128+ subcarriers = 5cm resolution potential' : nCommon + ' subcarriers'}`); + } + + // 8. FEATURE FINGERPRINT + console.log('\n🧬 YOUR RF FINGERPRINT RIGHT NOW'); + for (const nid of Object.keys(features).sort()) { + const f = features[nid]; + if (!f.length) continue; + const last = f[f.length - 1].features; + const names = ['Presence', 'Motion', 'Breathing', 'HeartRate', 'PhaseVar', 'Persons', 'Fall', 'RSSI']; + console.log(` Node ${nid}:`); + for (let i = 0; i < 8; i++) { + console.log(` ${names[i].padStart(10)}: ${bar(last[i])} ${last[i].toFixed(2)}`); + } + } + + console.log(`\n${line}`); + console.log(' WiFi signals reveal: who, what they\'re doing, how they feel,'); + console.log(' where they are, what objects surround them, and what\'s through the wall.'); + console.log(' No cameras. No wearables. No microphones. Just radio physics.'); + console.log(line); +}