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
+95 -79
View File
@@ -258,103 +258,120 @@ const DEVICE_PARAM = params.get('device') || '';
if (isMinimal) document.body.classList.add('minimal-mode'); 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 // CANVAS WAVEFORM ENGINE
// Continuous right-to-left scrolling sweep // Continuous right-to-left scrolling sweep
// ════════════════════════════════════════════════════ // ════════════════════════════════════════════════════
class WaveformTrack { class WaveformTrack {
constructor(canvasId, color, baseline=0.5, amplitude=0.38) { constructor(canvasId, color, mode='ecg') {
this.canvas = document.getElementById(canvasId); this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d'); this.ctx = this.canvas.getContext('2d');
this.color = color; this.color = color;
this.baseline = baseline; // 0..1 from top this.mode = mode; // 'ecg' | 'pleth' | 'resp'
this.amplitude = amplitude; // fraction of height this.queue = []; // Y-normalised values pending display
this.samples = []; // raw 0-255 bytes from monitor this.drawBuf= []; // Y-normalised values for each pixel column
this.drawBuf = []; // processed display values (0..1 Y position) this.sweepPos = 0;
this.MAX_DRAW = 600; // max points in draw buffer this.lastW = 0; this.lastH = 0;
this.sweepPos = 0; // current write head X pixel
this.lastWidth = 0;
this.lastHeight= 0;
this.animating = false;
this.synthPhase = 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() { resize() {
const w = this.canvas.offsetWidth; const w = this.canvas.offsetWidth;
const h = this.canvas.offsetHeight; 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.width = w;
this.canvas.height = h; this.canvas.height = h;
this.lastWidth = w; this.lastHeight = h; this.lastW = w; this.lastH = h;
this.drawBuf = new Array(w).fill(null); this.drawBuf = new Array(w).fill(null);
this.sweepPos = 0; this.sweepPos = 0;
} }
} }
// Push raw byte samples (0-255) from monitor packet // ── Normalise one raw byte → Y (0=top, 1=bottom) ──────────────
pushSamples(arr) { _normalise(s) {
if (!arr || arr.length === 0) return; if (this.mode === 'ecg') {
// normalise 0-255 → 0..1 Y position (inverted: 0=top) // ECG baseline = 128 (0x80). Centred deviation.
for (const s of arr) { const dev = s - 128; // positive = above baseline = upward deflection
const norm = (255 - s) / 255; // flip: high byte = up // Expand adaptive range to capture QRS peaks
const y = this.baseline + (norm - 0.5) * this.amplitude * 2; if (dev > this.adaptMax) this.adaptMax = dev;
this.samples.push(Math.max(0.02, Math.min(0.98, y))); 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() { tick() {
const w = this.canvas.width; const w = this.canvas.width;
const h = this.canvas.height; if (!w) return;
if (!w || !h) return;
// blank 6px ahead of sweep cursor // Erase 6px ahead of write head
const eraseW = 8; for (let e = 1; e <= 6; e++) {
for (let e = 0; e < eraseW; e++) { this.drawBuf[(this.sweepPos + e) % w] = null;
const ex = (this.sweepPos + e) % w;
if (this.drawBuf[ex] !== undefined) this.drawBuf[ex] = null;
} }
let yNorm; const yNorm = this.queue.length > 0 ? this.queue.shift() : this._synth();
if (this.samples.length > 0) {
yNorm = this.samples.shift();
} else {
// synthetic fallback
yNorm = this._synth();
}
this.drawBuf[this.sweepPos] = yNorm; this.drawBuf[this.sweepPos] = yNorm;
this.sweepPos = (this.sweepPos + 1) % w; this.sweepPos = (this.sweepPos + 1) % w;
} }
_synth() { _synth() {
this.synthPhase += 0.05; const m = this.mode;
this.synthPhase += 0.04;
const t = this.synthPhase; const t = this.synthPhase;
if (this.synthType === 'ecg') { if (m === 'ecg') {
const p = (t % (Math.PI * 2)) / (Math.PI * 2); // Realistic P-QRS-T morphology
if (p > 0.35 && p < 0.38) return this.baseline - 0.32; const period = Math.PI * 2;
if (p > 0.38 && p < 0.42) return this.baseline + 0.12; const p = ((t % period) / period);
return this.baseline + Math.sin(t * 0.3) * 0.02; if (p > 0.40 && p < 0.43) return 0.5 - 0.38; // R peak (up)
} else if (this.synthType === 'pleth') { if (p > 0.43 && p < 0.46) return 0.5 + 0.14; // S trough (down)
return this.baseline + Math.sin(t * 0.8) * 0.28; if (p > 0.30 && p < 0.36) return 0.5 - 0.06; // P wave
} else if (this.synthType === 'resp') { 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 rr = this.rrValue || 15;
const speed = rr / 60 * 0.025; const speed = (rr / 60) * 0.30;
return this.baseline + Math.sin(this.synthPhase * speed * 20) * 0.22; return 0.5 + Math.sin(t * speed) * 0.28;
} }
return this.baseline; // flat
} }
draw() { draw() {
@@ -363,44 +380,43 @@ class WaveformTrack {
const h = this.canvas.height; const h = this.canvas.height;
if (!w || !h) return; if (!w || !h) return;
// Clear
ctx.clearRect(0, 0, w, h); ctx.clearRect(0, 0, w, h);
// Grid lines (subtle) // ECG-paper style grid
ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.lineWidth = 1; 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(); 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(); ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(w,y); ctx.stroke();
} }
// Waveform // Waveform line
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = this.color; 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.shadowColor = this.color;
ctx.shadowBlur = 6; ctx.shadowBlur = (this.mode === 'ecg') ? 5 : 8;
let started = false; let started = false;
for (let x = 0; x < w; x++) { for (let x = 0; x < w; x++) {
const idx = (this.sweepPos + x) % w; const idx = (this.sweepPos + x) % w;
const yNorm = this.drawBuf[idx]; const yv = this.drawBuf[idx];
if (yNorm === null || yNorm === undefined) { if (yv === null || yv === undefined) { started = false; continue; }
started = false; continue; const py = yv * h;
} if (!started) { ctx.moveTo(x, py); started = true; }
const px = x; else { ctx.lineTo(x, py); }
const py = yNorm * h;
if (!started) { ctx.moveTo(px, py); started = true; }
else { ctx.lineTo(px, py); }
} }
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
// Sweep cursor (black erase bar) // Sweep erase bar
ctx.fillStyle = 'rgba(10,14,10,0.85)'; ctx.fillStyle = 'rgba(10,14,10,0.92)';
ctx.fillRect(((this.sweepPos + w - 4) % w), 0, 6, h); ctx.fillRect(((this.sweepPos - 4 + w) % w), 0, 8, h);
} }
} }