Files
wifi-densepose/ui/components/signal-viz.js
Claude dd382824fe feat: Add hardware requirement notice to README, additional Three.js viz components
Add prominent hardware requirements table at top of README documenting
the three paths to real CSI data (ESP32, research NIC, commodity WiFi).
Include remaining Three.js visualization components for dashboard.

https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
2026-02-28 06:26:10 +00:00

468 lines
15 KiB
JavaScript

// Real-time CSI Signal Visualization - WiFi DensePose
// Amplitude heatmap, Phase plot, Doppler spectrum, Motion energy
export class SignalVisualization {
constructor(scene) {
this.scene = scene;
this.group = new THREE.Group();
this.group.name = 'signal-visualization';
this.group.position.set(-5.5, 0, -3);
// Configuration
this.config = {
subcarriers: 30,
timeSlots: 40,
heatmapWidth: 3.0,
heatmapHeight: 1.5,
phaseWidth: 3.0,
phaseHeight: 1.0,
dopplerBars: 16,
dopplerWidth: 2.0,
dopplerHeight: 1.0
};
// Data buffers
this.amplitudeHistory = [];
this.phaseData = new Float32Array(this.config.subcarriers);
this.dopplerData = new Float32Array(this.config.dopplerBars);
this.motionEnergy = 0;
this.targetMotionEnergy = 0;
// Initialize for timeSlots rows of subcarrier data
for (let i = 0; i < this.config.timeSlots; i++) {
this.amplitudeHistory.push(new Float32Array(this.config.subcarriers));
}
// Build visualizations
this._buildAmplitudeHeatmap();
this._buildPhasePlot();
this._buildDopplerSpectrum();
this._buildMotionIndicator();
this._buildLabels();
this.scene.add(this.group);
}
_buildAmplitudeHeatmap() {
// Create a grid of colored cells for CSI amplitude across subcarriers over time
const { subcarriers, timeSlots, heatmapWidth, heatmapHeight } = this.config;
const cellW = heatmapWidth / subcarriers;
const cellH = heatmapHeight / timeSlots;
this._heatmapCells = [];
this._heatmapGroup = new THREE.Group();
this._heatmapGroup.position.set(0, 3.5, 0);
const cellGeom = new THREE.PlaneGeometry(cellW * 0.9, cellH * 0.9);
for (let t = 0; t < timeSlots; t++) {
const row = [];
for (let s = 0; s < subcarriers; s++) {
const mat = new THREE.MeshBasicMaterial({
color: 0x000022,
transparent: true,
opacity: 0.85,
side: THREE.DoubleSide
});
const cell = new THREE.Mesh(cellGeom, mat);
cell.position.set(
s * cellW - heatmapWidth / 2 + cellW / 2,
t * cellH,
0
);
this._heatmapGroup.add(cell);
row.push(cell);
}
this._heatmapCells.push(row);
}
// Border frame
const frameGeom = new THREE.EdgesGeometry(
new THREE.PlaneGeometry(heatmapWidth + 0.1, heatmapHeight + 0.1)
);
const frameMat = new THREE.LineBasicMaterial({ color: 0x335577, opacity: 0.5, transparent: true });
const frame = new THREE.LineSegments(frameGeom, frameMat);
frame.position.set(0, heatmapHeight / 2, -0.01);
this._heatmapGroup.add(frame);
this.group.add(this._heatmapGroup);
}
_buildPhasePlot() {
// Line chart showing phase across subcarriers in 3D space
const { subcarriers, phaseWidth, phaseHeight } = this.config;
this._phaseGroup = new THREE.Group();
this._phaseGroup.position.set(0, 2.0, 0);
// Create the phase line
const positions = new Float32Array(subcarriers * 3);
for (let i = 0; i < subcarriers; i++) {
positions[i * 3] = (i / (subcarriers - 1)) * phaseWidth - phaseWidth / 2;
positions[i * 3 + 1] = 0;
positions[i * 3 + 2] = 0;
}
const phaseGeom = new THREE.BufferGeometry();
phaseGeom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const phaseMat = new THREE.LineBasicMaterial({
color: 0x00ff88,
transparent: true,
opacity: 0.8,
linewidth: 2
});
this._phaseLine = new THREE.Line(phaseGeom, phaseMat);
this._phaseGroup.add(this._phaseLine);
// Phase reference line (zero line)
const refPositions = new Float32Array(6);
refPositions[0] = -phaseWidth / 2; refPositions[1] = 0; refPositions[2] = 0;
refPositions[3] = phaseWidth / 2; refPositions[4] = 0; refPositions[5] = 0;
const refGeom = new THREE.BufferGeometry();
refGeom.setAttribute('position', new THREE.BufferAttribute(refPositions, 3));
const refMat = new THREE.LineBasicMaterial({ color: 0x224433, opacity: 0.3, transparent: true });
this._phaseGroup.add(new THREE.LineSegments(refGeom, refMat));
// Vertical axis lines
const axisPositions = new Float32Array(12);
// Left axis
axisPositions[0] = -phaseWidth / 2; axisPositions[1] = -phaseHeight / 2; axisPositions[2] = 0;
axisPositions[3] = -phaseWidth / 2; axisPositions[4] = phaseHeight / 2; axisPositions[5] = 0;
// Right axis
axisPositions[6] = phaseWidth / 2; axisPositions[7] = -phaseHeight / 2; axisPositions[8] = 0;
axisPositions[9] = phaseWidth / 2; axisPositions[10] = phaseHeight / 2; axisPositions[11] = 0;
const axisGeom = new THREE.BufferGeometry();
axisGeom.setAttribute('position', new THREE.BufferAttribute(axisPositions, 3));
this._phaseGroup.add(new THREE.LineSegments(axisGeom, refMat));
this.group.add(this._phaseGroup);
}
_buildDopplerSpectrum() {
// Bar chart for Doppler frequency spectrum
const { dopplerBars, dopplerWidth, dopplerHeight } = this.config;
const barWidth = (dopplerWidth / dopplerBars) * 0.8;
const gap = (dopplerWidth / dopplerBars) * 0.2;
this._dopplerGroup = new THREE.Group();
this._dopplerGroup.position.set(0, 0.8, 0);
this._dopplerBars = [];
const barGeom = new THREE.BoxGeometry(barWidth, 1, 0.05);
for (let i = 0; i < dopplerBars; i++) {
const mat = new THREE.MeshBasicMaterial({
color: 0x0044aa,
transparent: true,
opacity: 0.75
});
const bar = new THREE.Mesh(barGeom, mat);
const x = (i / (dopplerBars - 1)) * dopplerWidth - dopplerWidth / 2;
bar.position.set(x, 0, 0);
bar.scale.y = 0.01; // Start flat
this._dopplerGroup.add(bar);
this._dopplerBars.push(bar);
}
// Base line
const basePositions = new Float32Array(6);
basePositions[0] = -dopplerWidth / 2 - 0.1; basePositions[1] = 0; basePositions[2] = 0;
basePositions[3] = dopplerWidth / 2 + 0.1; basePositions[4] = 0; basePositions[5] = 0;
const baseGeom = new THREE.BufferGeometry();
baseGeom.setAttribute('position', new THREE.BufferAttribute(basePositions, 3));
const baseMat = new THREE.LineBasicMaterial({ color: 0x335577, opacity: 0.5, transparent: true });
this._dopplerGroup.add(new THREE.LineSegments(baseGeom, baseMat));
this.group.add(this._dopplerGroup);
}
_buildMotionIndicator() {
// Pulsating sphere that grows/brightens with motion energy
this._motionGroup = new THREE.Group();
this._motionGroup.position.set(2.0, 1.5, 0);
// Outer glow ring
const ringGeom = new THREE.RingGeometry(0.25, 0.3, 32);
const ringMat = new THREE.MeshBasicMaterial({
color: 0x00ff44,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide
});
this._motionRing = new THREE.Mesh(ringGeom, ringMat);
this._motionGroup.add(this._motionRing);
// Inner core
const coreGeom = new THREE.SphereGeometry(0.15, 16, 16);
const coreMat = new THREE.MeshBasicMaterial({
color: 0x004422,
transparent: true,
opacity: 0.6
});
this._motionCore = new THREE.Mesh(coreGeom, coreMat);
this._motionGroup.add(this._motionCore);
// Surrounding pulse rings
this._pulseRings = [];
for (let i = 0; i < 3; i++) {
const pulseGeom = new THREE.RingGeometry(0.3, 0.32, 32);
const pulseMat = new THREE.MeshBasicMaterial({
color: 0x00ff88,
transparent: true,
opacity: 0,
side: THREE.DoubleSide
});
const ring = new THREE.Mesh(pulseGeom, pulseMat);
ring.userData.phase = (i / 3) * Math.PI * 2;
this._motionGroup.add(ring);
this._pulseRings.push(ring);
}
this.group.add(this._motionGroup);
}
_buildLabels() {
// Create text labels using canvas textures
const labels = [
{ text: 'CSI AMPLITUDE', pos: [0, 5.2, 0], parent: this._heatmapGroup },
{ text: 'PHASE', pos: [0, 0.7, 0], parent: this._phaseGroup },
{ text: 'DOPPLER SPECTRUM', pos: [0, 0.8, 0], parent: this._dopplerGroup },
{ text: 'MOTION', pos: [0, 0.55, 0], parent: this._motionGroup }
];
for (const label of labels) {
const sprite = this._createTextSprite(label.text, {
fontSize: 14,
color: '#5588aa',
bgColor: 'transparent'
});
sprite.position.set(...label.pos);
sprite.scale.set(1.2, 0.3, 1);
if (label.parent) {
label.parent.add(sprite);
} else {
this.group.add(sprite);
}
}
}
_createTextSprite(text, opts = {}) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 256;
canvas.height = 64;
if (opts.bgColor && opts.bgColor !== 'transparent') {
ctx.fillStyle = opts.bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.font = `${opts.fontSize || 14}px monospace`;
ctx.fillStyle = opts.color || '#88aacc';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
const mat = new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: false
});
return new THREE.Sprite(mat);
}
// Feed new CSI data
// data: { amplitude: Float32Array(30), phase: Float32Array(30), doppler: Float32Array(16), motionEnergy: number }
updateSignalData(data) {
if (!data) return;
// Amplitude: shift history and add new row
if (data.amplitude) {
this.amplitudeHistory.shift();
this.amplitudeHistory.push(new Float32Array(data.amplitude));
}
// Phase
if (data.phase) {
this.phaseData = new Float32Array(data.phase);
}
// Doppler
if (data.doppler) {
for (let i = 0; i < Math.min(data.doppler.length, this.config.dopplerBars); i++) {
this.dopplerData[i] = data.doppler[i];
}
}
// Motion energy
if (data.motionEnergy !== undefined) {
this.targetMotionEnergy = Math.max(0, Math.min(1, data.motionEnergy));
}
}
// Call each frame
update(delta, elapsed) {
this._updateHeatmap();
this._updatePhasePlot();
this._updateDoppler(delta);
this._updateMotionIndicator(delta, elapsed);
}
_updateHeatmap() {
const { subcarriers, timeSlots } = this.config;
for (let t = 0; t < timeSlots; t++) {
const row = this.amplitudeHistory[t];
for (let s = 0; s < subcarriers; s++) {
const cell = this._heatmapCells[t][s];
const val = row[s] || 0;
// Color: dark blue (0) -> cyan (0.5) -> yellow (0.8) -> red (1.0)
cell.material.color.setHSL(
0.6 - val * 0.6, // hue: 0.6 (blue) -> 0 (red)
0.9, // saturation
0.1 + val * 0.5 // lightness: dim to bright
);
}
}
}
_updatePhasePlot() {
const posAttr = this._phaseLine.geometry.getAttribute('position');
const arr = posAttr.array;
const { subcarriers, phaseWidth, phaseHeight } = this.config;
for (let i = 0; i < subcarriers; i++) {
const x = (i / (subcarriers - 1)) * phaseWidth - phaseWidth / 2;
// Phase is in radians, normalize to [-1, 1] range then scale to height
const phase = this.phaseData[i] || 0;
const y = (phase / Math.PI) * (phaseHeight / 2);
arr[i * 3] = x;
arr[i * 3 + 1] = y;
arr[i * 3 + 2] = 0;
}
posAttr.needsUpdate = true;
// Color based on phase variance (more variance = more activity = greener/brighter)
let variance = 0;
let mean = 0;
for (let i = 0; i < subcarriers; i++) mean += this.phaseData[i] || 0;
mean /= subcarriers;
for (let i = 0; i < subcarriers; i++) {
const diff = (this.phaseData[i] || 0) - mean;
variance += diff * diff;
}
variance /= subcarriers;
const activity = Math.min(1, variance / 2);
this._phaseLine.material.color.setHSL(0.3 - activity * 0.15, 1.0, 0.35 + activity * 0.3);
}
_updateDoppler(delta) {
for (let i = 0; i < this._dopplerBars.length; i++) {
const bar = this._dopplerBars[i];
const target = this.dopplerData[i] || 0;
// Smooth bar height
const currentH = bar.scale.y;
bar.scale.y += (target * this.config.dopplerHeight - currentH) * Math.min(1, delta * 8);
bar.scale.y = Math.max(0.01, bar.scale.y);
// Position bar bottom at y=0
bar.position.y = bar.scale.y / 2;
// Color: blue (low) -> purple (mid) -> magenta (high)
const val = target;
bar.material.color.setHSL(
0.7 - val * 0.3, // blue to magenta
0.8,
0.25 + val * 0.35
);
}
}
_updateMotionIndicator(delta, elapsed) {
// Smooth motion energy
this.motionEnergy += (this.targetMotionEnergy - this.motionEnergy) * Math.min(1, delta * 5);
const energy = this.motionEnergy;
// Core: grows and brightens with motion
const coreScale = 0.8 + energy * 0.7;
this._motionCore.scale.setScalar(coreScale);
this._motionCore.material.color.setHSL(
0.3 - energy * 0.2, // green -> yellow-green
1.0,
0.15 + energy * 0.4
);
this._motionCore.material.opacity = 0.4 + energy * 0.5;
// Ring
this._motionRing.material.opacity = 0.15 + energy * 0.5;
this._motionRing.material.color.setHSL(0.3 - energy * 0.15, 1.0, 0.4 + energy * 0.3);
// Pulse rings
for (const ring of this._pulseRings) {
const phase = ring.userData.phase + elapsed * (1 + energy * 3);
const t = (Math.sin(phase) + 1) / 2;
const scale = 1 + t * energy * 2;
ring.scale.setScalar(scale);
ring.material.opacity = (1 - t) * energy * 0.4;
}
}
// Generate synthetic demo signal data
static generateDemoData(elapsed) {
const subcarriers = 30;
const dopplerBars = 16;
// Amplitude: sinusoidal pattern with noise simulating human movement
const amplitude = new Float32Array(subcarriers);
for (let i = 0; i < subcarriers; i++) {
const baseFreq = Math.sin(elapsed * 2 + i * 0.3) * 0.3;
const bodyEffect = Math.sin(elapsed * 0.8 + i * 0.15) * 0.25;
const noise = (Math.random() - 0.5) * 0.1;
amplitude[i] = Math.max(0, Math.min(1, 0.4 + baseFreq + bodyEffect + noise));
}
// Phase: linear with perturbations from movement
const phase = new Float32Array(subcarriers);
for (let i = 0; i < subcarriers; i++) {
const linearPhase = (i / subcarriers) * Math.PI * 2;
const bodyPhase = Math.sin(elapsed * 1.5 + i * 0.2) * 0.8;
phase[i] = linearPhase + bodyPhase;
}
// Doppler: spectral peaks from movement velocity
const doppler = new Float32Array(dopplerBars);
const centerBin = dopplerBars / 2 + Math.sin(elapsed * 0.7) * 3;
for (let i = 0; i < dopplerBars; i++) {
const dist = Math.abs(i - centerBin);
doppler[i] = Math.max(0, Math.exp(-dist * dist * 0.15) * (0.6 + Math.sin(elapsed * 1.2) * 0.3));
doppler[i] += (Math.random() - 0.5) * 0.05;
doppler[i] = Math.max(0, Math.min(1, doppler[i]));
}
// Motion energy: pulsating
const motionEnergy = (Math.sin(elapsed * 0.5) + 1) / 2 * 0.7 + 0.15;
return { amplitude, phase, doppler, motionEnergy };
}
getGroup() {
return this.group;
}
dispose() {
this.group.traverse((child) => {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (child.material.map) child.material.map.dispose();
child.material.dispose();
}
});
this.scene.remove(this.group);
}
}