diff --git a/fiveparaminte-main/static/dashboard.html b/fiveparaminte-main/static/dashboard.html index f6d2eff..f0235d7 100644 --- a/fiveparaminte-main/static/dashboard.html +++ b/fiveparaminte-main/static/dashboard.html @@ -258,103 +258,120 @@ const DEVICE_PARAM = params.get('device') || ''; if (isMinimal) document.body.classList.add('minimal-mode'); -// ════════════════════════════════════════════════════ -// CLOCK -// ════════════════════════════════════════════════════ -function updateClock(){ - const n = new Date(); - document.getElementById('clockDisplay').textContent = - n.toLocaleTimeString('en-GB',{hour12:false}); -} -setInterval(updateClock, 1000); -updateClock(); - // ════════════════════════════════════════════════════ // CANVAS WAVEFORM ENGINE // Continuous right-to-left scrolling sweep // ════════════════════════════════════════════════════ class WaveformTrack { - constructor(canvasId, color, baseline=0.5, amplitude=0.38) { + constructor(canvasId, color, mode='ecg') { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.color = color; - this.baseline = baseline; // 0..1 from top - this.amplitude = amplitude; // fraction of height - this.samples = []; // raw 0-255 bytes from monitor - this.drawBuf = []; // processed display values (0..1 Y position) - this.MAX_DRAW = 600; // max points in draw buffer - this.sweepPos = 0; // current write head X pixel - this.lastWidth = 0; - this.lastHeight= 0; - this.animating = false; + this.mode = mode; // 'ecg' | 'pleth' | 'resp' + this.queue = []; // Y-normalised values pending display + this.drawBuf= []; // Y-normalised values for each pixel column + this.sweepPos = 0; + this.lastW = 0; this.lastH = 0; this.synthPhase = 0; - this.synthType = 'flat'; - this.rrValue = null; + this.rrValue = null; + // Adaptive peak tracker for auto-scaling + this.adaptMax = (mode === 'ecg') ? 20 : 80; // initial range guess + this.adaptMin = (mode === 'ecg') ? -20 : 0; + // Buffer cap: never keep more than ~1 second queued (stay real-time) + this.MAX_QUEUE = 280; } resize() { const w = this.canvas.offsetWidth; const h = this.canvas.offsetHeight; - if (w !== this.lastWidth || h !== this.lastHeight) { + if (w !== this.lastW || h !== this.lastH) { this.canvas.width = w; this.canvas.height = h; - this.lastWidth = w; this.lastHeight = h; + this.lastW = w; this.lastH = h; this.drawBuf = new Array(w).fill(null); this.sweepPos = 0; } } - // Push raw byte samples (0-255) from monitor packet - pushSamples(arr) { - if (!arr || arr.length === 0) return; - // normalise 0-255 → 0..1 Y position (inverted: 0=top) - for (const s of arr) { - const norm = (255 - s) / 255; // flip: high byte = up - const y = this.baseline + (norm - 0.5) * this.amplitude * 2; - this.samples.push(Math.max(0.02, Math.min(0.98, y))); + // ── Normalise one raw byte → Y (0=top, 1=bottom) ────────────── + _normalise(s) { + if (this.mode === 'ecg') { + // ECG baseline = 128 (0x80). Centred deviation. + const dev = s - 128; // positive = above baseline = upward deflection + // Expand adaptive range to capture QRS peaks + if (dev > this.adaptMax) this.adaptMax = dev; + if (dev < this.adaptMin) this.adaptMin = dev; + // Slow decay back to ±20 minimum (avoid over-zoom on noise) + this.adaptMax = Math.max(20, this.adaptMax * 0.9995); + this.adaptMin = Math.min(-20, this.adaptMin * 0.9995); + const span = Math.max(this.adaptMax - this.adaptMin, 30); + // Map: positive dev → Y decreases (trace goes UP) + const frac = dev / (span / 2); + return Math.max(0.02, Math.min(0.98, 0.5 - frac * 0.44)); + + } else if (this.mode === 'pleth') { + // Pleth: 0 = baseline (bottom of strip), ~90 = pulse peak (top) + if (s > this.adaptMax) this.adaptMax = s; + this.adaptMax = Math.max(40, this.adaptMax * 0.9998); + const maxV = Math.max(this.adaptMax, 40); + // High sample → trace goes UP (y decreases) + const frac = Math.max(0, s) / maxV; + return Math.max(0.02, Math.min(0.98, 0.85 - frac * 0.78)); + + } else { + const frac = s / 255; + return Math.max(0.02, Math.min(0.98, 0.5 + Math.sin(frac * Math.PI - Math.PI/2) * 0.3)); } } - // Drain one sample into draw buffer at sweepPos + // Push raw byte array from WebSocket message + pushSamples(arr) { + if (!arr || arr.length === 0) return; + for (const s of arr) { + this.queue.push(this._normalise(s)); + } + // Throttle: drop oldest samples if queue too large (real-time priority) + if (this.queue.length > this.MAX_QUEUE) { + this.queue.splice(0, this.queue.length - this.MAX_QUEUE); + } + } + + // Advance sweep by one pixel tick() { const w = this.canvas.width; - const h = this.canvas.height; - if (!w || !h) return; + if (!w) return; - // blank 6px ahead of sweep cursor - const eraseW = 8; - for (let e = 0; e < eraseW; e++) { - const ex = (this.sweepPos + e) % w; - if (this.drawBuf[ex] !== undefined) this.drawBuf[ex] = null; + // Erase 6px ahead of write head + for (let e = 1; e <= 6; e++) { + this.drawBuf[(this.sweepPos + e) % w] = null; } - let yNorm; - if (this.samples.length > 0) { - yNorm = this.samples.shift(); - } else { - // synthetic fallback - yNorm = this._synth(); - } + const yNorm = this.queue.length > 0 ? this.queue.shift() : this._synth(); this.drawBuf[this.sweepPos] = yNorm; this.sweepPos = (this.sweepPos + 1) % w; } _synth() { - this.synthPhase += 0.05; + const m = this.mode; + this.synthPhase += 0.04; const t = this.synthPhase; - if (this.synthType === 'ecg') { - const p = (t % (Math.PI * 2)) / (Math.PI * 2); - if (p > 0.35 && p < 0.38) return this.baseline - 0.32; - if (p > 0.38 && p < 0.42) return this.baseline + 0.12; - return this.baseline + Math.sin(t * 0.3) * 0.02; - } else if (this.synthType === 'pleth') { - return this.baseline + Math.sin(t * 0.8) * 0.28; - } else if (this.synthType === 'resp') { + if (m === 'ecg') { + // Realistic P-QRS-T morphology + const period = Math.PI * 2; + const p = ((t % period) / period); + if (p > 0.40 && p < 0.43) return 0.5 - 0.38; // R peak (up) + if (p > 0.43 && p < 0.46) return 0.5 + 0.14; // S trough (down) + if (p > 0.30 && p < 0.36) return 0.5 - 0.06; // P wave + if (p > 0.55 && p < 0.65) return 0.5 - 0.08; // T wave + return 0.5 + Math.sin(t * 0.1) * 0.01; + } else if (m === 'pleth') { + const pulse = Math.max(0, Math.sin(t * 0.7)); + return 0.85 - pulse * 0.75; + } else { const rr = this.rrValue || 15; - const speed = rr / 60 * 0.025; - return this.baseline + Math.sin(this.synthPhase * speed * 20) * 0.22; + const speed = (rr / 60) * 0.30; + return 0.5 + Math.sin(t * speed) * 0.28; } - return this.baseline; // flat } draw() { @@ -363,44 +380,43 @@ class WaveformTrack { const h = this.canvas.height; if (!w || !h) return; - // Clear ctx.clearRect(0, 0, w, h); - // Grid lines (subtle) - ctx.strokeStyle = 'rgba(255,255,255,0.04)'; + // ECG-paper style grid + ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.lineWidth = 1; - for (let x = 0; x < w; x += 50) { + const gridX = Math.round(w / 8); + const gridY = Math.round(h / 4); + for (let x = gridX; x < w; x += gridX) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,h); ctx.stroke(); } - for (let y = 0; y < h; y += h/4) { + for (let y = gridY; y < h; y += gridY) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(w,y); ctx.stroke(); } - // Waveform + // Waveform line ctx.beginPath(); ctx.strokeStyle = this.color; - ctx.lineWidth = 1.8; + ctx.lineWidth = (this.mode === 'ecg') ? 1.6 : 2.0; + ctx.lineJoin = 'round'; ctx.shadowColor = this.color; - ctx.shadowBlur = 6; + ctx.shadowBlur = (this.mode === 'ecg') ? 5 : 8; let started = false; for (let x = 0; x < w; x++) { const idx = (this.sweepPos + x) % w; - const yNorm = this.drawBuf[idx]; - if (yNorm === null || yNorm === undefined) { - started = false; continue; - } - const px = x; - const py = yNorm * h; - if (!started) { ctx.moveTo(px, py); started = true; } - else { ctx.lineTo(px, py); } + const yv = this.drawBuf[idx]; + if (yv === null || yv === undefined) { started = false; continue; } + const py = yv * h; + if (!started) { ctx.moveTo(x, py); started = true; } + else { ctx.lineTo(x, py); } } ctx.stroke(); ctx.shadowBlur = 0; - // Sweep cursor (black erase bar) - ctx.fillStyle = 'rgba(10,14,10,0.85)'; - ctx.fillRect(((this.sweepPos + w - 4) % w), 0, 6, h); + // Sweep erase bar + ctx.fillStyle = 'rgba(10,14,10,0.92)'; + ctx.fillRect(((this.sweepPos - 4 + w) % w), 0, 8, h); } }