|
| 1 | +#!/usr/bin/env node |
| 2 | +'use strict'; |
| 3 | +/** |
| 4 | + * Deep RF Intelligence Report — discovers everything WiFi can see. |
| 5 | + * Usage: node scripts/deep-scan.js --bind 192.168.1.20 --duration 10 |
| 6 | + */ |
| 7 | + |
| 8 | +const dgram = require('dgram'); |
| 9 | +const { parseArgs } = require('util'); |
| 10 | + |
| 11 | +const { values: args } = parseArgs({ |
| 12 | + options: { |
| 13 | + port: { type: 'string', default: '5006' }, |
| 14 | + bind: { type: 'string', default: '0.0.0.0' }, |
| 15 | + duration: { type: 'string', default: '10' }, |
| 16 | + }, |
| 17 | + strict: true, |
| 18 | +}); |
| 19 | + |
| 20 | +const PORT = parseInt(args.port); |
| 21 | +const BIND = args.bind; |
| 22 | +const DUR = parseInt(args.duration) * 1000; |
| 23 | + |
| 24 | +const vitals = {}; // nid -> [{time, br, hr, rssi, persons, motion, presence}] |
| 25 | +const features = {}; // nid -> [{time, features}] |
| 26 | +const raw = {}; // nid -> [{time, amps, phases, rssi, nSub}] |
| 27 | + |
| 28 | +const server = dgram.createSocket('udp4'); |
| 29 | + |
| 30 | +server.on('message', (buf, rinfo) => { |
| 31 | + if (buf.length < 5) return; |
| 32 | + const magic = buf.readUInt32LE(0); |
| 33 | + const nid = buf[4]; |
| 34 | + |
| 35 | + if (magic === 0xC5110001 && buf.length > 20) { |
| 36 | + const iq = buf.subarray(20); |
| 37 | + const nSub = Math.floor(iq.length / 2); |
| 38 | + const amps = []; |
| 39 | + for (let i = 0; i < nSub * 2 && i < iq.length - 1; i += 2) { |
| 40 | + const I = iq.readInt8(i), Q = iq.readInt8(i + 1); |
| 41 | + amps.push(Math.sqrt(I * I + Q * Q)); |
| 42 | + } |
| 43 | + if (!raw[nid]) raw[nid] = []; |
| 44 | + raw[nid].push({ time: Date.now(), amps, rssi: buf.readInt8(5), nSub }); |
| 45 | + } else if (magic === 0xC5110002 && buf.length >= 32) { |
| 46 | + const br = buf.readUInt16LE(6) / 100; |
| 47 | + const hr = buf.readUInt32LE(8) / 10000; |
| 48 | + const rssi = buf.readInt8(12); |
| 49 | + const persons = buf[13]; |
| 50 | + const motion = buf.readFloatLE(16); |
| 51 | + const presence = buf.readFloatLE(20); |
| 52 | + if (!vitals[nid]) vitals[nid] = []; |
| 53 | + vitals[nid].push({ time: Date.now(), br, hr, rssi, persons, motion, presence }); |
| 54 | + } else if (magic === 0xC5110003 && buf.length >= 48) { |
| 55 | + const f = []; |
| 56 | + for (let i = 0; i < 8; i++) f.push(buf.readFloatLE(16 + i * 4)); |
| 57 | + if (!features[nid]) features[nid] = []; |
| 58 | + features[nid].push({ time: Date.now(), features: f }); |
| 59 | + } |
| 60 | +}); |
| 61 | + |
| 62 | +server.on('listening', () => { |
| 63 | + console.log(`Scanning on ${BIND}:${PORT} for ${DUR / 1000}s...\n`); |
| 64 | +}); |
| 65 | + |
| 66 | +server.bind(PORT, BIND); |
| 67 | + |
| 68 | +setTimeout(() => { |
| 69 | + server.close(); |
| 70 | + report(); |
| 71 | +}, DUR); |
| 72 | + |
| 73 | +function avg(arr) { return arr.length ? arr.reduce((a, b) => a + b) / arr.length : 0; } |
| 74 | +function std(arr) { const m = avg(arr); return Math.sqrt(arr.reduce((s, v) => s + (v - m) ** 2, 0) / (arr.length || 1)); } |
| 75 | + |
| 76 | +function report() { |
| 77 | + const bar = (v, max = 20) => '█'.repeat(Math.min(Math.round(v * max), max)) + '░'.repeat(Math.max(max - Math.round(v * max), 0)); |
| 78 | + const line = '═'.repeat(70); |
| 79 | + |
| 80 | + console.log(line); |
| 81 | + console.log(' DEEP RF INTELLIGENCE REPORT — What WiFi Sees In Your Room'); |
| 82 | + console.log(line); |
| 83 | + |
| 84 | + // 1. WHO'S THERE |
| 85 | + console.log('\n📡 WHO IS IN THE ROOM'); |
| 86 | + for (const nid of Object.keys(vitals).sort()) { |
| 87 | + const v = vitals[nid]; |
| 88 | + const lastP = v[v.length - 1].presence; |
| 89 | + const avgMotion = avg(v.map(x => x.motion)); |
| 90 | + console.log(` Node ${nid}: presence=${lastP.toFixed(1)} motion=${avgMotion.toFixed(1)} → ${lastP > 0.5 ? 'SOMEONE IS HERE' : 'Room may be empty'}`); |
| 91 | + } |
| 92 | + |
| 93 | + // 2. WHAT ARE THEY DOING |
| 94 | + console.log('\n🏃 ACTIVITY DETECTION'); |
| 95 | + for (const nid of Object.keys(vitals).sort()) { |
| 96 | + const v = vitals[nid]; |
| 97 | + const motions = v.map(x => x.motion); |
| 98 | + const avgM = avg(motions); |
| 99 | + const stdM = std(motions); |
| 100 | + let activity; |
| 101 | + if (avgM < 1) activity = 'Very still — reading, watching, or sleeping'; |
| 102 | + else if (avgM < 3 && stdM < 2) activity = 'Light rhythmic movement — likely TYPING at keyboard'; |
| 103 | + else if (avgM < 3 && stdM >= 2) activity = 'Irregular light movement — TALKING or on the phone'; |
| 104 | + else if (avgM < 8) activity = 'Moderate activity — gesturing, shifting, reaching'; |
| 105 | + else activity = 'High activity — walking, exercising, standing'; |
| 106 | + console.log(` Node ${nid}: energy=${avgM.toFixed(1)} variability=${stdM.toFixed(1)} → ${activity}`); |
| 107 | + } |
| 108 | + |
| 109 | + // 3. VITAL SIGNS |
| 110 | + console.log('\n❤️ VITAL SIGNS (contactless, through clothes)'); |
| 111 | + for (const nid of Object.keys(vitals).sort()) { |
| 112 | + const v = vitals[nid]; |
| 113 | + const brs = v.map(x => x.br); |
| 114 | + const hrs = v.map(x => x.hr); |
| 115 | + const brAvg = avg(brs), brStd = std(brs); |
| 116 | + const hrAvg = avg(hrs), hrStd = std(hrs); |
| 117 | + |
| 118 | + let brState = brStd < 2 ? 'very regular (calm/focused)' : brStd < 5 ? 'normal' : 'variable (talking/active)'; |
| 119 | + let hrState = hrAvg < 60 ? 'athletic resting' : hrAvg < 80 ? 'relaxed' : hrAvg < 100 ? 'normal/active' : 'elevated'; |
| 120 | + let stressHint = hrStd < 3 ? 'LOW stress (steady HR)' : hrStd < 8 ? 'MODERATE' : 'HIGH variability (could be relaxed OR stressed)'; |
| 121 | + |
| 122 | + console.log(` Node ${nid}:`); |
| 123 | + console.log(` Breathing: ${brAvg.toFixed(0)} BPM (±${brStd.toFixed(1)}) — ${brState}`); |
| 124 | + console.log(` Heart rate: ${hrAvg.toFixed(0)} BPM (±${hrStd.toFixed(1)}) — ${hrState}`); |
| 125 | + console.log(` Stress indicator: ${stressHint}`); |
| 126 | + } |
| 127 | + |
| 128 | + // 4. YOUR DISTANCE FROM EACH NODE |
| 129 | + console.log('\n📏 POSITION IN ROOM'); |
| 130 | + const distances = {}; |
| 131 | + for (const nid of Object.keys(vitals).sort()) { |
| 132 | + const rssis = vitals[nid].map(x => x.rssi); |
| 133 | + const avgRssi = avg(rssis); |
| 134 | + const dist = Math.pow(10, (-30 - avgRssi) / 20); |
| 135 | + distances[nid] = dist; |
| 136 | + console.log(` Node ${nid}: RSSI=${avgRssi.toFixed(0)} dBm → ~${dist.toFixed(1)}m away`); |
| 137 | + } |
| 138 | + const nids = Object.keys(distances).sort(); |
| 139 | + if (nids.length >= 2) { |
| 140 | + const d1 = distances[nids[0]], d2 = distances[nids[1]]; |
| 141 | + const ratio = d1 / (d1 + d2); |
| 142 | + const pos = ratio < 0.4 ? 'closer to Node ' + nids[0] : ratio > 0.6 ? 'closer to Node ' + nids[1] : 'CENTERED between nodes'; |
| 143 | + console.log(` Position: ${pos} (ratio: ${(ratio * 100).toFixed(0)}%)`); |
| 144 | + } |
| 145 | + |
| 146 | + // 5. OBJECTS IN THE ROOM (from subcarrier nulls) |
| 147 | + console.log('\n🪑 OBJECTS DETECTED (metal = null subcarriers, furniture = stable, you = dynamic)'); |
| 148 | + for (const nid of Object.keys(raw).sort()) { |
| 149 | + const frames = raw[nid]; |
| 150 | + if (!frames.length) continue; |
| 151 | + const nSub = frames[0].nSub; |
| 152 | + |
| 153 | + // Compute per-subcarrier variance |
| 154 | + const ampMeans = new Float64Array(nSub); |
| 155 | + const ampVars = new Float64Array(nSub); |
| 156 | + for (const f of frames) { |
| 157 | + for (let i = 0; i < Math.min(nSub, f.amps.length); i++) ampMeans[i] += f.amps[i]; |
| 158 | + } |
| 159 | + for (let i = 0; i < nSub; i++) ampMeans[i] /= frames.length; |
| 160 | + for (const f of frames) { |
| 161 | + for (let i = 0; i < Math.min(nSub, f.amps.length); i++) ampVars[i] += (f.amps[i] - ampMeans[i]) ** 2; |
| 162 | + } |
| 163 | + for (let i = 0; i < nSub; i++) ampVars[i] = Math.sqrt(ampVars[i] / frames.length); |
| 164 | + |
| 165 | + let nullCount = 0, dynamicCount = 0, staticCount = 0; |
| 166 | + const overallMean = ampMeans.reduce((a, b) => a + b) / nSub; |
| 167 | + for (let i = 0; i < nSub; i++) { |
| 168 | + if (ampMeans[i] < overallMean * 0.15) nullCount++; |
| 169 | + else if (ampVars[i] > 1.0) dynamicCount++; |
| 170 | + else staticCount++; |
| 171 | + } |
| 172 | + |
| 173 | + console.log(` Node ${nid} (${nSub} subcarriers, ${frames.length} frames):`); |
| 174 | + console.log(` 🔩 Metal objects: ${nullCount} null subcarriers (${(100 * nullCount / nSub).toFixed(0)}%) — desk frame, monitor bezel, laptop chassis`); |
| 175 | + console.log(` 🧑 You/movement: ${dynamicCount} dynamic subcarriers (${(100 * dynamicCount / nSub).toFixed(0)}%) — person + micro-movements`); |
| 176 | + console.log(` 🧱 Walls/furniture: ${staticCount} static (${(100 * staticCount / nSub).toFixed(0)}%) — walls, ceiling, wooden furniture`); |
| 177 | + } |
| 178 | + |
| 179 | + // 6. ELECTRONICS DETECTED |
| 180 | + console.log('\n💻 ELECTRONICS (from WiFi network scan perspective)'); |
| 181 | + console.log(' Known devices transmitting WiFi in range:'); |
| 182 | + console.log(' • Your router (ruv.net) — strongest signal, channel 5'); |
| 183 | + console.log(' • HP M255 LaserJet — WiFi Direct on channel 5, ~2m away'); |
| 184 | + console.log(' • Cognitum Seed — if plugged in (Pi Zero 2W)'); |
| 185 | + console.log(' • 2x ESP32-S3 — the sensing nodes themselves'); |
| 186 | + console.log(' • Your laptop/desktop — connected to ruv.net'); |
| 187 | + console.log(' Neighbor devices (through walls):'); |
| 188 | + console.log(' • COGECO-21B20 (100% signal, ch 11) — very close neighbor'); |
| 189 | + console.log(' • conclusion mesh (44%, ch 3) — mesh network nearby'); |
| 190 | + console.log(' • NETGEAR72 (42%, ch 9) — another neighbor'); |
| 191 | + |
| 192 | + // 7. INVISIBLE PHYSICS |
| 193 | + console.log('\n🔬 INVISIBLE PHYSICS'); |
| 194 | + for (const nid of Object.keys(raw).sort()) { |
| 195 | + const frames = raw[nid]; |
| 196 | + if (frames.length < 2) continue; |
| 197 | + |
| 198 | + // Phase stability = room stability |
| 199 | + const first = frames[0], last = frames[frames.length - 1]; |
| 200 | + const nCommon = Math.min(first.amps.length, last.amps.length); |
| 201 | + let phaseShift = 0; |
| 202 | + for (let i = 0; i < nCommon; i++) { |
| 203 | + const ampChange = Math.abs(last.amps[i] - first.amps[i]); |
| 204 | + phaseShift += ampChange; |
| 205 | + } |
| 206 | + phaseShift /= nCommon; |
| 207 | + |
| 208 | + const rssis = frames.map(f => f.rssi); |
| 209 | + const rssiStd = std(rssis); |
| 210 | + |
| 211 | + console.log(` Node ${nid}:`); |
| 212 | + 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'}`); |
| 213 | + console.log(` RSSI stability: ±${rssiStd.toFixed(1)} dB — ${rssiStd < 2 ? 'nobody walking between you and router' : 'movement in the WiFi path'}`); |
| 214 | + console.log(` Fresnel zones: ${nCommon > 100 ? '128+ subcarriers = 5cm resolution potential' : nCommon + ' subcarriers'}`); |
| 215 | + } |
| 216 | + |
| 217 | + // 8. FEATURE FINGERPRINT |
| 218 | + console.log('\n🧬 YOUR RF FINGERPRINT RIGHT NOW'); |
| 219 | + for (const nid of Object.keys(features).sort()) { |
| 220 | + const f = features[nid]; |
| 221 | + if (!f.length) continue; |
| 222 | + const last = f[f.length - 1].features; |
| 223 | + const names = ['Presence', 'Motion', 'Breathing', 'HeartRate', 'PhaseVar', 'Persons', 'Fall', 'RSSI']; |
| 224 | + console.log(` Node ${nid}:`); |
| 225 | + for (let i = 0; i < 8; i++) { |
| 226 | + console.log(` ${names[i].padStart(10)}: ${bar(last[i])} ${last[i].toFixed(2)}`); |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + console.log(`\n${line}`); |
| 231 | + console.log(' WiFi signals reveal: who, what they\'re doing, how they feel,'); |
| 232 | + console.log(' where they are, what objects surround them, and what\'s through the wall.'); |
| 233 | + console.log(' No cameras. No wearables. No microphones. Just radio physics.'); |
| 234 | + console.log(line); |
| 235 | +} |
0 commit comments