mirror of
https://github.com/ruvnet/RuView
synced 2026-08-09 20:21:43 +00:00
feat: RSSI visualization, RuVector attention WASM, cache-bust fixes
- Add animated RSSI Signal Strength panel with sparkline history - Fix RuVector WasmMultiHeadAttention retptr calling convention - Wire up RuVector Multi-Head + Flash Attention in CNN embedder - Add ambient temporal drift to CSI simulator for visible heatmap animation - Fix embedding space projection (sparse projection replaces cancelling sum) - Add auto-scaling to embedding space renderer - Add cache busters (?v=4) to all ES module imports to prevent stale caches - Add diagnostic logging for module version verification - Add RSSI tracking with quality labels and color-coded dBm display - Includes ruvector-attention-wasm v2.0.5 browser ESM wrapper Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -185,22 +185,63 @@ export class CanvasRenderer {
|
||||
ctx.beginPath(); ctx.moveTo(w / 2, 0); ctx.lineTo(w / 2, h); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(0, h / 2); ctx.lineTo(w, h / 2); ctx.stroke();
|
||||
|
||||
// Auto-scale: find max extent across all point sets
|
||||
let maxExtent = 0.01;
|
||||
for (const pts of [points.video, points.csi, points.fused]) {
|
||||
if (!pts) continue;
|
||||
for (const p of pts) {
|
||||
if (!p) continue;
|
||||
maxExtent = Math.max(maxExtent, Math.abs(p[0]), Math.abs(p[1]));
|
||||
}
|
||||
}
|
||||
const scale = 0.42 / maxExtent; // Fill ~84% of half-width
|
||||
|
||||
const drawPoints = (pts, color, size) => {
|
||||
if (!pts || pts.length === 0) return;
|
||||
const len = pts.length;
|
||||
|
||||
// Draw trail line connecting recent points
|
||||
if (len >= 2) {
|
||||
ctx.beginPath();
|
||||
let started = false;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const p = pts[i];
|
||||
if (!p) continue;
|
||||
const px = w / 2 + p[0] * scale * w;
|
||||
const py = h / 2 + p[1] * scale * h;
|
||||
if (px < -10 || px > w + 10 || py < -10 || py > h + 10) continue;
|
||||
if (!started) { ctx.moveTo(px, py); started = true; }
|
||||
else ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.strokeStyle = color;
|
||||
ctx.globalAlpha = 0.2;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw dots with glow on newest
|
||||
for (let i = 0; i < len; i++) {
|
||||
const p = pts[i];
|
||||
if (!p) continue;
|
||||
const age = 1 - (i / len) * 0.7; // Fade older points
|
||||
const px = w / 2 + p[0] * w * 0.35;
|
||||
const py = h / 2 + p[1] * h * 0.35;
|
||||
const age = 1 - (i / len) * 0.7;
|
||||
const px = w / 2 + p[0] * scale * w;
|
||||
const py = h / 2 + p[1] * scale * h;
|
||||
|
||||
if (px < 0 || px > w || py < 0 || py > h) continue;
|
||||
if (px < -10 || px > w + 10 || py < -10 || py > h + 10) continue;
|
||||
|
||||
// Glow on newest point
|
||||
if (i === len - 1) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, size + 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, size, 0, Math.PI * 2);
|
||||
ctx.arc(px, py, i === len - 1 ? size + 1 : size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.globalAlpha = age * 0.7;
|
||||
ctx.globalAlpha = age * 0.8;
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* CNN Embedder — Lightweight MobileNet-V3-style feature extractor.
|
||||
* CNN Embedder — RuVector Attention-powered feature extractor.
|
||||
*
|
||||
* Architecture mirrors ruvector-cnn: Conv2D → BatchNorm → ReLU → Pool → Project → L2 Normalize
|
||||
* Uses pre-seeded random weights (deterministic). When ruvector-cnn-wasm is available,
|
||||
* transparently delegates to the WASM implementation.
|
||||
* Uses the real ruvector-attention-wasm WASM module for Multi-Head Attention
|
||||
* and Flash Attention on CSI/video data. Falls back to a JS Conv2D pipeline
|
||||
* when WASM is not available.
|
||||
*
|
||||
* Pipeline: Conv2D → BatchNorm → ReLU → Pool → RuVector Attention → Project → L2 Normalize
|
||||
* Two instances are created: one for video frames, one for CSI pseudo-images.
|
||||
*/
|
||||
|
||||
@@ -31,6 +32,10 @@ export class CnnEmbedder {
|
||||
this.embeddingDim = opts.embeddingDim || 128;
|
||||
this.normalize = opts.normalize !== false;
|
||||
this.wasmEmbedder = null;
|
||||
this.rvAttention = null; // RuVector Multi-Head Attention (WASM)
|
||||
this.rvFlash = null; // RuVector Flash Attention (WASM)
|
||||
this.rvModule = null; // RuVector WASM module reference
|
||||
this.useRuVector = false;
|
||||
|
||||
// Initialize weights with deterministic PRNG
|
||||
const rng = mulberry32(opts.seed || 42);
|
||||
@@ -48,18 +53,44 @@ export class CnnEmbedder {
|
||||
this.bnMean = new Float32Array(16).fill(0.0);
|
||||
this.bnVar = new Float32Array(16).fill(1.0);
|
||||
|
||||
// Projection: 16 → embeddingDim
|
||||
// Projection: 16 → embeddingDim (used when RuVector not available)
|
||||
this.projWeights = new Float32Array(16 * this.embeddingDim);
|
||||
for (let i = 0; i < this.projWeights.length; i++) {
|
||||
this.projWeights[i] = randRange(-0.1, 0.1);
|
||||
}
|
||||
|
||||
// Attention projection: attention_dim → embeddingDim
|
||||
this.attnProjWeights = new Float32Array(16 * this.embeddingDim);
|
||||
for (let i = 0; i < this.attnProjWeights.length; i++) {
|
||||
this.attnProjWeights[i] = randRange(-0.08, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to load WASM embedder from ruvector-cnn-wasm package
|
||||
* Try to load RuVector attention WASM, then fall back to ruvector-cnn-wasm
|
||||
* @param {string} wasmPath - Path to the WASM package directory
|
||||
*/
|
||||
async tryLoadWasm(wasmPath) {
|
||||
// First try: RuVector Attention WASM (the real thing — browser ESM build)
|
||||
try {
|
||||
const attnBase = new URL('../pkg/ruvector-attention/ruvector_attention_browser.js', import.meta.url).href;
|
||||
const mod = await import(attnBase);
|
||||
await mod.default(); // async WASM init via fetch
|
||||
mod.init();
|
||||
|
||||
// Create Multi-Head Attention (dim=16 matches conv output channels, 4 heads)
|
||||
this.rvAttention = new mod.WasmMultiHeadAttention(16, 4);
|
||||
// Create Flash Attention for larger sequences
|
||||
this.rvFlash = new mod.WasmFlashAttention(16, 8);
|
||||
this.rvModule = mod;
|
||||
this.useRuVector = true;
|
||||
console.log(`[CNN] RuVector Attention WASM v${mod.version()} loaded — Multi-Head + Flash Attention active`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log('[CNN] RuVector Attention WASM not available:', e.message);
|
||||
}
|
||||
|
||||
// Second try: ruvector-cnn-wasm (legacy path)
|
||||
try {
|
||||
const mod = await import(`${wasmPath}/ruvector_cnn_wasm.js`);
|
||||
await mod.default();
|
||||
@@ -68,10 +99,10 @@ export class CnnEmbedder {
|
||||
config.embedding_dim = this.embeddingDim;
|
||||
config.normalize = this.normalize;
|
||||
this.wasmEmbedder = new mod.WasmCnnEmbedder(config);
|
||||
console.log('[CNN] WASM embedder loaded successfully');
|
||||
console.log('[CNN] WASM CNN embedder loaded successfully');
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log('[CNN] WASM not available, using JS fallback:', e.message);
|
||||
console.log('[CNN] WASM CNN not available, using JS fallback:', e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -125,10 +156,17 @@ export class CnnEmbedder {
|
||||
if (convOut[i] < 0) convOut[i] = 0;
|
||||
}
|
||||
|
||||
// 6. Global average pooling → 16-dim
|
||||
// 6. Global average pooling → spatial tokens (each 16-dim)
|
||||
const outH = sz - 2, outW = sz - 2;
|
||||
const pooled = new Float32Array(16);
|
||||
const spatial = outH * outW;
|
||||
|
||||
// 7. RuVector Attention (if loaded) — apply attention over spatial tokens
|
||||
if (this.useRuVector && this.rvAttention) {
|
||||
return this._extractWithAttention(convOut, spatial, 16);
|
||||
}
|
||||
|
||||
// Fallback: simple global average pool + linear projection
|
||||
const pooled = new Float32Array(16);
|
||||
for (let i = 0; i < spatial; i++) {
|
||||
for (let c = 0; c < 16; c++) {
|
||||
pooled[c] += convOut[i * 16 + c];
|
||||
@@ -136,7 +174,7 @@ export class CnnEmbedder {
|
||||
}
|
||||
for (let c = 0; c < 16; c++) pooled[c] /= spatial;
|
||||
|
||||
// 7. Linear projection → embeddingDim
|
||||
// Linear projection → embeddingDim
|
||||
const emb = new Float32Array(this.embeddingDim);
|
||||
for (let o = 0; o < this.embeddingDim; o++) {
|
||||
let sum = 0;
|
||||
@@ -146,7 +184,7 @@ export class CnnEmbedder {
|
||||
emb[o] = sum;
|
||||
}
|
||||
|
||||
// 8. L2 normalize
|
||||
// L2 normalize
|
||||
if (this.normalize) {
|
||||
let norm = 0;
|
||||
for (let i = 0; i < emb.length; i++) norm += emb[i] * emb[i];
|
||||
@@ -159,6 +197,70 @@ export class CnnEmbedder {
|
||||
return emb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract embedding using RuVector Multi-Head Attention WASM.
|
||||
* Treats conv feature map spatial positions as sequence tokens,
|
||||
* applies self-attention, then projects to embedding dimension.
|
||||
*/
|
||||
_extractWithAttention(convOut, numTokens, channels) {
|
||||
// Subsample spatial tokens for attention (keep it fast: max 64 tokens)
|
||||
const maxTokens = 64;
|
||||
const step = numTokens > maxTokens ? Math.floor(numTokens / maxTokens) : 1;
|
||||
const tokens = [];
|
||||
for (let i = 0; i < numTokens && tokens.length < maxTokens; i += step) {
|
||||
const token = new Float32Array(channels);
|
||||
for (let c = 0; c < channels; c++) {
|
||||
token[c] = convOut[i * channels + c];
|
||||
}
|
||||
tokens.push(token);
|
||||
}
|
||||
|
||||
// Use first token as query, all tokens as keys/values (self-attention)
|
||||
// Average multiple query positions for robust embedding
|
||||
const numQueries = Math.min(4, tokens.length);
|
||||
const queryStride = Math.floor(tokens.length / numQueries);
|
||||
const attended = new Float32Array(channels);
|
||||
|
||||
for (let q = 0; q < numQueries; q++) {
|
||||
const queryToken = tokens[q * queryStride];
|
||||
try {
|
||||
const result = this.rvAttention.compute(queryToken, tokens, tokens);
|
||||
for (let c = 0; c < channels; c++) {
|
||||
attended[c] += result[c] / numQueries;
|
||||
}
|
||||
} catch (_) {
|
||||
// Fallback: just average the tokens
|
||||
for (let c = 0; c < channels; c++) {
|
||||
attended[c] += queryToken[c] / numQueries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Project attended features → embeddingDim
|
||||
const emb = new Float32Array(this.embeddingDim);
|
||||
for (let o = 0; o < this.embeddingDim; o++) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < channels; i++) {
|
||||
sum += attended[i] * this.attnProjWeights[i * this.embeddingDim + o];
|
||||
}
|
||||
emb[o] = sum;
|
||||
}
|
||||
|
||||
// L2 normalize using RuVector WASM
|
||||
if (this.normalize && this.rvModule) {
|
||||
try {
|
||||
this.rvModule.normalize(emb);
|
||||
} catch (_) {
|
||||
let norm = 0;
|
||||
for (let i = 0; i < emb.length; i++) norm += emb[i] * emb[i];
|
||||
norm = Math.sqrt(norm);
|
||||
if (norm > 1e-8) for (let i = 0; i < emb.length; i++) emb[i] /= norm;
|
||||
}
|
||||
}
|
||||
|
||||
return emb;
|
||||
}
|
||||
|
||||
_conv2d3x3(input, H, W, Cin, Cout) {
|
||||
const outH = H - 2, outW = W - 2;
|
||||
const output = new Float32Array(outH * outW * Cout);
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
*/
|
||||
|
||||
export class CsiSimulator {
|
||||
static VERSION = 'v4-drift'; // Cache-bust verification
|
||||
|
||||
constructor(opts = {}) {
|
||||
this.subcarriers = opts.subcarriers || 52; // 802.11n HT20
|
||||
this.timeWindow = opts.timeWindow || 56; // frames in sliding window
|
||||
@@ -32,6 +34,10 @@ export class CsiSimulator {
|
||||
this._basePhase[i] = (i / this.subcarriers) * Math.PI * 2;
|
||||
}
|
||||
|
||||
// RSSI tracking
|
||||
this.rssiDbm = -70; // default mid-range
|
||||
this._rssiTarget = -70;
|
||||
|
||||
// Person influence (updated from video motion)
|
||||
this.personPresence = 0;
|
||||
this.personX = 0.5;
|
||||
@@ -126,6 +132,13 @@ export class CsiSimulator {
|
||||
this.phaseBuffer.shift();
|
||||
}
|
||||
|
||||
// RSSI: smooth toward target (demo mode generates synthetic RSSI)
|
||||
if (this.mode === 'demo') {
|
||||
// Simulate RSSI based on person presence and slow drift
|
||||
this._rssiTarget = -55 - 25 * (1 - this.personPresence) + Math.sin(elapsed * 0.3) * 3;
|
||||
}
|
||||
this.rssiDbm += (this._rssiTarget - this.rssiDbm) * 0.1;
|
||||
|
||||
// SNR estimate
|
||||
let signalPower = 0, noisePower = 0;
|
||||
for (let i = 0; i < this.subcarriers; i++) {
|
||||
@@ -215,6 +228,11 @@ export class CsiSimulator {
|
||||
this._noiseState[i] = 0.95 * this._noiseState[i] + 0.05 * (rng() * 2 - 1) * 0.03;
|
||||
a += this._noiseState[i];
|
||||
|
||||
// Ambient temporal drift (multipath fading even in empty room)
|
||||
a += 0.06 * Math.sin(elapsed * 0.7 + i * 0.25)
|
||||
+ 0.04 * Math.sin(elapsed * 1.3 - i * 0.18)
|
||||
+ 0.03 * Math.cos(elapsed * 2.1 + i * 0.4);
|
||||
|
||||
// Person-induced CSI perturbation
|
||||
if (presence > 0.1) {
|
||||
// Subcarrier-dependent body reflection (Fresnel zone model)
|
||||
@@ -237,6 +255,17 @@ export class CsiSimulator {
|
||||
}
|
||||
|
||||
_handleLiveFrame(data) {
|
||||
// Handle JSON text frames from the sensing server
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
const msg = JSON.parse(data);
|
||||
this._handleJsonFrame(msg);
|
||||
} catch (_) { /* ignore malformed JSON */ }
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle binary ArrayBuffer frames (ADR-018 format)
|
||||
if (!(data instanceof ArrayBuffer)) return;
|
||||
const view = new DataView(data);
|
||||
// Check ADR-018 magic: 0xC5110001
|
||||
if (data.byteLength < 20) return;
|
||||
@@ -256,6 +285,64 @@ export class CsiSimulator {
|
||||
}
|
||||
}
|
||||
|
||||
_handleJsonFrame(msg) {
|
||||
// Sensing server sends: { type: "sensing_update", nodes: [{ amplitude: [...], subcarrier_count }], classification, features }
|
||||
this._liveAmplitude = new Float32Array(this.subcarriers);
|
||||
this._livePhase = new Float32Array(this.subcarriers);
|
||||
|
||||
// Extract amplitude from sensing_update node data
|
||||
const node = (msg.nodes && msg.nodes[0]) || msg;
|
||||
const ampArr = node.amplitude || msg.amplitude;
|
||||
if (ampArr && Array.isArray(ampArr)) {
|
||||
const n = Math.min(ampArr.length, this.subcarriers);
|
||||
// Server sends raw amplitude (already magnitude), normalize to 0-1
|
||||
let maxAmp = 0;
|
||||
for (let i = 0; i < n; i++) maxAmp = Math.max(maxAmp, Math.abs(ampArr[i]));
|
||||
const scale = maxAmp > 0 ? 1.0 / maxAmp : 1.0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
this._liveAmplitude[i] = Math.abs(ampArr[i]) * scale;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase from node (if available)
|
||||
const phaseArr = node.phase || msg.phase;
|
||||
if (phaseArr && Array.isArray(phaseArr)) {
|
||||
const n = Math.min(phaseArr.length, this.subcarriers);
|
||||
for (let i = 0; i < n; i++) this._livePhase[i] = phaseArr[i];
|
||||
} else if (ampArr) {
|
||||
// Synthesize phase from amplitude variation (Hilbert-like estimate)
|
||||
for (let i = 1; i < this.subcarriers; i++) {
|
||||
this._livePhase[i] = this._livePhase[i - 1] + (this._liveAmplitude[i] - this._liveAmplitude[i - 1]) * Math.PI;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle raw I/Q pairs
|
||||
const iq = node.iq || msg.iq;
|
||||
if (iq && Array.isArray(iq)) {
|
||||
const n = Math.min(iq.length / 2, this.subcarriers);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const real = iq[i * 2], imag = iq[i * 2 + 1];
|
||||
this._liveAmplitude[i] = Math.sqrt(real * real + imag * imag) / 2048;
|
||||
this._livePhase[i] = Math.atan2(imag, real);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract RSSI from node data
|
||||
if (typeof node.rssi_dbm === 'number') {
|
||||
this._rssiTarget = node.rssi_dbm;
|
||||
} else if (msg.features && typeof msg.features.mean_rssi === 'number') {
|
||||
this._rssiTarget = msg.features.mean_rssi;
|
||||
}
|
||||
|
||||
// Update presence from server classification
|
||||
const cls = msg.classification;
|
||||
if (cls) {
|
||||
if (typeof cls.confidence === 'number') {
|
||||
this.personPresence = cls.presence ? cls.confidence : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_mulberry32(seed) {
|
||||
return function() {
|
||||
let t = (seed += 0x6D2B79F5);
|
||||
|
||||
@@ -111,18 +111,19 @@ export class FusionEngine {
|
||||
* @returns {{ video: Array, csi: Array, fused: Array }}
|
||||
*/
|
||||
getEmbeddingPoints() {
|
||||
// Simple 2D projection using first two principal components (approximated)
|
||||
// Sparse random projection: pick a few dimensions with fixed coefficients
|
||||
// to get visible 2D spread (avoids cancellation from summing all 128 dims)
|
||||
const project = (emb) => {
|
||||
if (!emb || emb.length < 4) return null;
|
||||
// Use pairs of dimensions as crude 2D projection
|
||||
let x = 0, y = 0;
|
||||
for (let i = 0; i < emb.length; i += 2) {
|
||||
x += emb[i] * (i % 4 < 2 ? 1 : -1);
|
||||
if (i + 1 < emb.length) {
|
||||
y += emb[i + 1] * (i % 4 < 2 ? 1 : -1);
|
||||
}
|
||||
}
|
||||
return [x * 2, y * 2]; // Scale for visibility
|
||||
// Use 8 sparse dimensions with predetermined signs (seeded, not random)
|
||||
const dim = emb.length;
|
||||
const x = emb[0] * 3.2 - emb[3] * 2.8 + emb[7] * 2.1 - emb[12] * 1.9
|
||||
+ (dim > 30 ? emb[29] * 1.5 - emb[31] * 1.3 : 0)
|
||||
+ (dim > 60 ? emb[55] * 1.1 - emb[60] * 0.9 : 0);
|
||||
const y = emb[1] * 3.0 - emb[5] * 2.5 + emb[9] * 2.3 - emb[15] * 1.7
|
||||
+ (dim > 40 ? emb[37] * 1.4 - emb[42] * 1.2 : 0)
|
||||
+ (dim > 80 ? emb[73] * 1.0 - emb[80] * 0.8 : 0);
|
||||
return [x, y];
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+137
-13
@@ -4,12 +4,12 @@
|
||||
* Main orchestration: video capture → CNN embedding → CSI processing → fusion → rendering
|
||||
*/
|
||||
|
||||
import { VideoCapture } from './video-capture.js';
|
||||
import { CsiSimulator } from './csi-simulator.js';
|
||||
import { CnnEmbedder } from './cnn-embedder.js';
|
||||
import { FusionEngine } from './fusion-engine.js';
|
||||
import { PoseDecoder } from './pose-decoder.js';
|
||||
import { CanvasRenderer } from './canvas-renderer.js';
|
||||
import { VideoCapture } from './video-capture.js?v=4';
|
||||
import { CsiSimulator } from './csi-simulator.js?v=4';
|
||||
import { CnnEmbedder } from './cnn-embedder.js?v=4';
|
||||
import { FusionEngine } from './fusion-engine.js?v=4';
|
||||
import { PoseDecoder } from './pose-decoder.js?v=4';
|
||||
import { CanvasRenderer } from './canvas-renderer.js?v=4';
|
||||
|
||||
// === State ===
|
||||
let mode = 'dual'; // 'dual' | 'video' | 'csi'
|
||||
@@ -71,9 +71,20 @@ const latTotalEl = document.getElementById('lat-total');
|
||||
// Cross-modal similarity
|
||||
const crossModalEl = document.getElementById('cross-modal-sim');
|
||||
|
||||
// RSSI elements
|
||||
const rssiBarEl = document.getElementById('rssi-bar');
|
||||
const rssiValueEl = document.getElementById('rssi-value');
|
||||
const rssiQualityEl = document.getElementById('rssi-quality');
|
||||
const rssiSparkCanvas = document.getElementById('rssi-sparkline');
|
||||
const rssiSparkCtx = rssiSparkCanvas ? rssiSparkCanvas.getContext('2d') : null;
|
||||
const rssiHistory = [];
|
||||
const RSSI_HISTORY_MAX = 80;
|
||||
|
||||
// === Initialize ===
|
||||
function init() {
|
||||
console.log(`[PoseFusion] init() v4 — CsiSimulator=${CsiSimulator.VERSION || 'OLD'}, starting...`);
|
||||
resizeCanvases();
|
||||
console.log(`[PoseFusion] canvases: skeleton=${skeletonCanvas.width}x${skeletonCanvas.height}, csi=${csiCanvas.width}x${csiCanvas.height}, emb=${embeddingCanvas.width}x${embeddingCanvas.height}`);
|
||||
window.addEventListener('resize', resizeCanvases);
|
||||
|
||||
// Mode change
|
||||
@@ -110,9 +121,9 @@ function init() {
|
||||
}
|
||||
});
|
||||
|
||||
// Try to load WASM embedders (non-blocking)
|
||||
// Resolve relative to this JS module file (in pose-fusion/js/) → ../pkg/
|
||||
const wasmBase = new URL('../pkg/ruvector_cnn_wasm', import.meta.url).href;
|
||||
// Try to load RuVector Attention WASM embedders (non-blocking)
|
||||
// Loads from ../pkg/ruvector-attention/ (real RuVector Multi-Head + Flash Attention)
|
||||
const wasmBase = new URL('../pkg/ruvector-attention', import.meta.url).href;
|
||||
visualCnn.tryLoadWasm(wasmBase);
|
||||
csiCnn.tryLoadWasm(wasmBase);
|
||||
|
||||
@@ -168,22 +179,24 @@ function resizeCanvases() {
|
||||
skeletonCanvas.height = rect.height;
|
||||
}
|
||||
|
||||
// CSI canvas
|
||||
csiCanvas.width = csiCanvas.parentElement.clientWidth;
|
||||
// CSI canvas (min 200px width)
|
||||
csiCanvas.width = Math.max(200, csiCanvas.parentElement.clientWidth);
|
||||
csiCanvas.height = 120;
|
||||
|
||||
// Embedding canvas
|
||||
embeddingCanvas.width = embeddingCanvas.parentElement.clientWidth;
|
||||
// Embedding canvas (min 200px width)
|
||||
embeddingCanvas.width = Math.max(200, embeddingCanvas.parentElement.clientWidth);
|
||||
embeddingCanvas.height = 140;
|
||||
}
|
||||
|
||||
// === Main Loop ===
|
||||
let _loopErrorShown = false;
|
||||
function mainLoop(timestamp) {
|
||||
if (!isRunning) return;
|
||||
requestAnimationFrame(mainLoop);
|
||||
|
||||
if (isPaused) return;
|
||||
|
||||
try {
|
||||
const elapsed = performance.now() / 1000 - startTime;
|
||||
const totalStart = performance.now();
|
||||
|
||||
@@ -309,6 +322,117 @@ function mainLoop(timestamp) {
|
||||
// Cross-modal similarity
|
||||
const sim = fusionEngine.getCrossModalSimilarity();
|
||||
crossModalEl.textContent = sim.toFixed(3);
|
||||
|
||||
// RSSI update
|
||||
updateRssi(csiSimulator.rssiDbm);
|
||||
|
||||
// One-time diagnostic
|
||||
if (frameCount === 1) {
|
||||
console.log(`[PoseFusion] frame 1 OK — mode=${mode}, csi.bufLen=${csiSimulator.amplitudeBuffer.length}, embPts=${embPoints.fused.length}, rssi=${csiSimulator.rssiDbm.toFixed(1)}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (!_loopErrorShown) {
|
||||
_loopErrorShown = true;
|
||||
console.error('[MainLoop]', err);
|
||||
// Show error visually on page
|
||||
const errDiv = document.createElement('div');
|
||||
errDiv.style.cssText = 'position:fixed;bottom:60px;left:24px;right:24px;background:rgba(255,48,64,0.95);color:#fff;padding:12px 16px;border-radius:8px;font:12px/1.4 "JetBrains Mono",monospace;z-index:9999;max-height:120px;overflow:auto';
|
||||
errDiv.textContent = `[MainLoop Error] ${err.message}\n${err.stack?.split('\n').slice(0,3).join('\n')}`;
|
||||
document.body.appendChild(errDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === RSSI Visualization ===
|
||||
function updateRssi(dbm) {
|
||||
if (!rssiBarEl) return;
|
||||
|
||||
// Clamp to typical WiFi range: -100 (worst) to -30 (best)
|
||||
const clamped = Math.max(-100, Math.min(-30, dbm));
|
||||
const pct = ((clamped + 100) / 70) * 100; // 0-100%
|
||||
|
||||
rssiBarEl.style.width = `${pct}%`;
|
||||
rssiValueEl.textContent = `${Math.round(clamped)} dBm`;
|
||||
|
||||
// Quality label
|
||||
let quality;
|
||||
if (clamped > -50) quality = 'Excellent';
|
||||
else if (clamped > -60) quality = 'Good';
|
||||
else if (clamped > -70) quality = 'Fair';
|
||||
else if (clamped > -80) quality = 'Weak';
|
||||
else quality = 'Poor';
|
||||
rssiQualityEl.textContent = quality;
|
||||
|
||||
// Color the dBm value based on quality
|
||||
if (clamped > -60) rssiValueEl.style.color = 'var(--green-glow)';
|
||||
else if (clamped > -75) rssiValueEl.style.color = 'var(--amber)';
|
||||
else rssiValueEl.style.color = 'var(--red-alert)';
|
||||
|
||||
// Sparkline history
|
||||
rssiHistory.push(clamped);
|
||||
if (rssiHistory.length > RSSI_HISTORY_MAX) rssiHistory.shift();
|
||||
drawRssiSparkline();
|
||||
}
|
||||
|
||||
function drawRssiSparkline() {
|
||||
if (!rssiSparkCtx || rssiHistory.length < 2) return;
|
||||
const w = rssiSparkCanvas.width;
|
||||
const h = rssiSparkCanvas.height;
|
||||
const ctx = rssiSparkCtx;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Draw signal strength line
|
||||
const len = rssiHistory.length;
|
||||
const step = w / (RSSI_HISTORY_MAX - 1);
|
||||
|
||||
// Gradient fill under line
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, h);
|
||||
grad.addColorStop(0, 'rgba(0,210,120,0.3)');
|
||||
grad.addColorStop(1, 'rgba(0,210,120,0)');
|
||||
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < len; i++) {
|
||||
const x = (RSSI_HISTORY_MAX - len + i) * step;
|
||||
const y = h - ((rssiHistory[i] + 100) / 70) * h;
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
// Fill area
|
||||
const lastX = (RSSI_HISTORY_MAX - 1) * step;
|
||||
const firstX = (RSSI_HISTORY_MAX - len) * step;
|
||||
ctx.lineTo(lastX, h);
|
||||
ctx.lineTo(firstX, h);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
|
||||
// Draw line on top
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < len; i++) {
|
||||
const x = (RSSI_HISTORY_MAX - len + i) * step;
|
||||
const y = h - ((rssiHistory[i] + 100) / 70) * h;
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.strokeStyle = '#00d878';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Pulsing dot at latest value
|
||||
const latestX = lastX;
|
||||
const latestY = h - ((rssiHistory[len - 1] + 100) / 70) * h;
|
||||
const pulse = 0.5 + 0.5 * Math.sin(performance.now() / 300);
|
||||
ctx.beginPath();
|
||||
ctx.arc(latestX, latestY, 2 + pulse, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#00d878';
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(latestX, latestY, 4 + pulse * 2, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = `rgba(0,216,120,${0.3 + pulse * 0.3})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Boot
|
||||
|
||||
Reference in New Issue
Block a user