diff --git a/fiveparaminte-main/static/dashboard.html b/fiveparaminte-main/static/dashboard.html index 46b6f4d..c039dfe 100644 --- a/fiveparaminte-main/static/dashboard.html +++ b/fiveparaminte-main/static/dashboard.html @@ -1270,6 +1270,17 @@ currentHR = null; currentSpO2 = null; currentRR = null; + hasReceivedData = false; + + // Reset wave buffers so sweep starts fresh on next data + scanIndex = 0; + ecgPhase = 0; + plethPhase = 0; + respPhase = 0; + waveBuffers.ecg2.fill(0); + waveBuffers.ecg1.fill(0); + waveBuffers.pleth.fill(0); + waveBuffers.resp.fill(0); document.getElementById('value-hr').textContent = '---'; document.getElementById('value-hr').classList.add('no-data'); @@ -1289,47 +1300,85 @@ // === Animation Loop (Real-time Scanning Sweep Bar) === function animateWaveforms() { - // Speed of the sweep bar (in virtual coordinates per frame) - const speed = 4; - - // Advance scan position - const prevScanIndex = scanIndex; - scanIndex = (scanIndex + speed) % WAVE_BUFFER_SIZE; - - // Increment procedural wave phases - // Scale phase speed depending on live vitals rate (beats/breaths per minute) - const hr = currentHR || 75; // fallback to 75 bpm - const rr = currentRR || 16; // fallback to 16 breaths - - // Increment phase - // For ECG/Pleth, 1 cycle is 1 heartbeat. - // For Resp, 1 cycle is 1 breath. - const ecgPhaseInc = (hr / 60) * (speed / WAVE_BUFFER_SIZE) * 4.0; // 4 seconds sweep - const respPhaseInc = (rr / 60) * (speed / WAVE_BUFFER_SIZE) * 4.0; - - // Compute values and populate circular buffers for the newly scanned segment - for (let i = 0; i < speed; i++) { - const targetIdx = (prevScanIndex + i) % WAVE_BUFFER_SIZE; - - ecgPhase += ecgPhaseInc / speed; - plethPhase += ecgPhaseInc / speed; // Sync SpO2 with ECG - respPhase += respPhaseInc / speed; - - waveBuffers.ecg2[targetIdx] = getECGValue(ecgPhase); - waveBuffers.ecg1[targetIdx] = getECG1Value(ecgPhase + 0.15); // Offset phase slightly for Lead I - waveBuffers.pleth[targetIdx] = getPlethValue(plethPhase - 0.2); // Pulse wave lag - waveBuffers.resp[targetIdx] = getRespValue(respPhase); + const speed = 4; + + // Only generate and draw sweep waveforms when live data is flowing + const hasEcgData = hasReceivedData && currentHR !== null; + const hasSpo2Data = hasReceivedData && currentSpO2 !== null; + const hasRespData = hasReceivedData && currentRR !== null; + + if (hasEcgData || hasSpo2Data || hasRespData) { + // Advance scan position + const prevScanIndex = scanIndex; + scanIndex = (scanIndex + speed) % WAVE_BUFFER_SIZE; + + const hr = currentHR || 75; + const rr = currentRR || 16; + + const ecgPhaseInc = (hr / 60) * (speed / WAVE_BUFFER_SIZE) * 4.0; + const respPhaseInc = (rr / 60) * (speed / WAVE_BUFFER_SIZE) * 4.0; + + for (let i = 0; i < speed; i++) { + const targetIdx = (prevScanIndex + i) % WAVE_BUFFER_SIZE; + + ecgPhase += ecgPhaseInc / speed; + plethPhase += ecgPhaseInc / speed; + respPhase += respPhaseInc / speed; + + // Only fill buffers for channels with live data + if (hasEcgData) { + waveBuffers.ecg2[targetIdx] = getECGValue(ecgPhase); + waveBuffers.ecg1[targetIdx] = getECG1Value(ecgPhase + 0.15); + } else { + waveBuffers.ecg2[targetIdx] = 0; + waveBuffers.ecg1[targetIdx] = 0; + } + + if (hasSpo2Data) { + waveBuffers.pleth[targetIdx] = getPlethValue(plethPhase - 0.2); + } else { + waveBuffers.pleth[targetIdx] = 0; + } + + if (hasRespData) { + waveBuffers.resp[targetIdx] = getRespValue(respPhase); + } else { + waveBuffers.resp[targetIdx] = 0; + } + } + + // Draw active channels with sweep, idle channels with flat baseline + drawChannel('ecg2', waveBuffers.ecg2, varColor('--ecg-color'), 1.8, false, hasEcgData); + drawChannel('ecg1', waveBuffers.ecg1, varColor('--ecg-color'), 1.8, false, hasEcgData); + drawChannel('pleth', waveBuffers.pleth, varColor('--spo2-color'), 2.2, false, hasSpo2Data); + drawChannel('resp', waveBuffers.resp, varColor('--resp-color'), 1.8, true, hasRespData); + } else { + // No live data at all — draw dim flat baselines on every channel + drawFlatBaseline('ecg2', varColor('--ecg-color')); + drawFlatBaseline('ecg1', varColor('--ecg-color')); + drawFlatBaseline('pleth', varColor('--spo2-color')); + drawFlatBaseline('resp', varColor('--resp-color')); } - - // Draw all wave channels - drawChannel('ecg2', waveBuffers.ecg2, varColor('--ecg-color'), 1.8, false); - drawChannel('ecg1', waveBuffers.ecg1, varColor('--ecg-color'), 1.8, false); - drawChannel('pleth', waveBuffers.pleth, varColor('--spo2-color'), 2.2, false); - drawChannel('resp', waveBuffers.resp, varColor('--resp-color'), 1.8, true); // Respiration is dotted/dashed - + requestAnimationFrame(animateWaveforms); } + function drawFlatBaseline(key, color) { + const wf = canvases[key]; + if (!wf) return; + const { ctx, width, height } = wf; + ctx.clearRect(0, 0, width, height); + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.globalAlpha = 0.15; + ctx.lineWidth = 1; + ctx.setLineDash([]); + ctx.moveTo(0, height / 2); + ctx.lineTo(width, height / 2); + ctx.stroke(); + ctx.globalAlpha = 1.0; + } + function varColor(varName) { // Fallback colors if styling var fails if (varName.includes('ecg-color')) return '#00ff66';