mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-19 05:10:13 -05:00
Integrate reviewed source PRs #2201-#2223, #2225-#2231, #2235-#2240, #2242 and #2243 (firmware README hunks remain in #2247). Regenerate one package manifest after integration, validate native Windows test execution, bound MTK listener startup, support official C6 release bundles, and retain stronger release metadata gates.
236 lines
10 KiB
JavaScript
236 lines
10 KiB
JavaScript
#!/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(16), 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);
|
|
}
|