fix: adjust ECG baseline centering and Pleth peak scaling in dashboard

This commit is contained in:
2026-07-15 14:46:44 +05:30
parent 14f3d45e53
commit fbb9febda8
+91 -75
View File
@@ -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;
// 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
// ── 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));
}
}
// Push raw byte array from WebSocket message
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)));
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);
}
}
// Drain one sample into draw buffer at sweepPos
// 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);
}
}