refactor: implement packet parsing heuristics and introduce device-level state caching to throttle database writes and vitals updates

This commit is contained in:
2026-07-11 14:24:14 +05:30
parent a4972b3251
commit 84b8acc904
32 changed files with 3499 additions and 180 deletions
+169 -16
View File
@@ -3,8 +3,8 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Patient Vital Signs Monitor — Contec CMS7000PLUS</title>
<meta name="description" content="Real-time patient vital signs monitoring dashboard for Contec CMS7000PLUS patient monitor">
<title>Patient Vital Signs Monitor — Contec CMS7000PLUS / CMS8500</title>
<meta name="description" content="Real-time patient vital signs monitoring dashboard for Contec CMS7000PLUS / CMS8500 patient monitors">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
@@ -224,6 +224,37 @@
color: var(--text-secondary);
}
/* === Device Selector Styles === */
.device-selector-container {
display: flex;
align-items: center;
gap: 8px;
margin-right: 8px;
}
.device-select-dropdown {
background: rgba(17, 24, 39, 0.8);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
outline: none;
cursor: pointer;
transition: all 0.3s;
}
.device-select-dropdown:hover {
border-color: var(--spo2-color);
background: rgba(25, 34, 56, 0.9);
}
.device-select-dropdown option {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
/* === Setup Guide Panel === */
.setup-guide {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.08), rgba(168, 85, 247, 0.06));
@@ -699,10 +730,16 @@
<div class="header-icon">🏥</div>
<div>
<div class="header-title">Patient Vital Signs Monitor</div>
<div class="header-subtitle">Contec CMS7000PLUS — Real-time Monitoring</div>
<div class="header-subtitle">Contec Monitor — Real-time Monitoring</div>
</div>
</div>
<div class="header-right">
<div class="device-selector-container">
<label for="deviceSelector" style="font-size: 13px; color: var(--text-secondary); font-weight: 500;">Select Monitor:</label>
<select id="deviceSelector" class="device-select-dropdown" onchange="selectDevice(this.value)">
<option value="">Waiting for devices...</option>
</select>
</div>
<div class="audio-toggle" id="audioToggle" onclick="toggleAudioAlert()" title="Toggle audio alerts for critical vitals">
<span id="audioIcon">🔇</span>
<span id="audioText">Alerts Off</span>
@@ -720,7 +757,7 @@
<div class="setup-guide-header" onclick="toggleSetupGuide()">
<div class="setup-guide-title">
<span>📡</span>
<span>CMS7000PLUS Setup Guide — Configure Your Monitor</span>
<span>Contec CMS7000PLUS / CMS8500 Setup Guide — Configure Your Monitor</span>
</div>
<div class="setup-guide-toggle" id="setupToggleText">▾ Collapse</div>
</div>
@@ -728,43 +765,43 @@
<div class="setup-step">
<div class="step-number">1</div>
<div class="step-content">
<h4>Connect via Ethernet</h4>
<p>Connect your CMS7000PLUS to this PC using an Ethernet cable (direct or through a network switch).</p>
<h4>Connect via Wi-Fi or Ethernet</h4>
<p><strong>Wi-Fi:</strong> Connect the monitor (e.g. CMS8500) and your PC to the same Wi-Fi network. <br><strong>Ethernet:</strong> Connect using an Ethernet cable (direct or switch).</p>
</div>
</div>
<div class="setup-step">
<div class="step-number">2</div>
<div class="step-content">
<h4>Set PC IP Address</h4>
<p>Set your PC's Ethernet adapter to a static IP, e.g. <code>192.168.1.50</code>, subnet <code>255.255.255.0</code>.</p>
<h4>Get PC IP Address</h4>
<p>Find your PC's IP address on the network (shown in the box below). If using Wi-Fi, use your PC's Wi-Fi IP address.</p>
</div>
</div>
<div class="setup-step">
<div class="step-number">3</div>
<div class="step-content">
<h4>Configure Monitor CMS Settings</h4>
<p>On your CMS7000PLUS: <code>System Setup → Network → CMS Settings</code>. Set the Server IP to your PC's IP.</p>
<p>On your monitor: Go to <code>System Setup → Network → CMS Settings</code>. Set <strong>Server IP</strong> (or CMS IP) to your PC's IP address.</p>
</div>
</div>
<div class="setup-step">
<div class="step-number">4</div>
<div class="step-content">
<h4>Set Server Port</h4>
<p>Set Server Port to <code>511</code> (default). If running without admin, use a port &gt;1024 like <code>6060</code>.</p>
<p>Set Server Port on your monitor to one of the listening ports: <code id="activePortsList">Detecting...</code> (usually <code>511</code> or <code>518</code>).</p>
</div>
</div>
<div class="setup-step">
<div class="step-number">5</div>
<div class="step-content">
<h4>Enable CMS Connection</h4>
<p>Enable the CMS/Central Monitor connection on your CMS7000PLUS. Set the sending interval (e.g., 5 seconds).</p>
<p>Enable the CMS/Central Monitor connection in the monitor settings. Set the sending interval (e.g. 5 seconds) to start transmitting.</p>
</div>
</div>
<div class="setup-step">
<div class="step-number">6</div>
<div class="step-content">
<h4>Data Will Appear Automatically</h4>
<p>Once connected, vital signs will appear on this dashboard in real-time via WebSocket or polling.</p>
<p>Once connected, the monitor will show up in the "Select Monitor" list, and its vitals will display in real-time.</p>
</div>
</div>
<div class="setup-ip-display">
@@ -970,15 +1007,22 @@
let setupGuideCollapsed = false;
let hasReceivedData = false;
let activeDevices = [];
let selectedDeviceId = localStorage.getItem('selectedDeviceId') || '';
// === Initialize ===
document.addEventListener('DOMContentLoaded', () => {
initWaveforms();
updateClock();
setInterval(updateClock, 1000);
tryWebSocket();
startPolling();
fetchDevices().then(() => {
tryWebSocket();
startPolling();
});
animateWaveforms();
fetchNetworkInfo();
// Periodically refresh the list of active devices
setInterval(fetchDevices, 4000);
});
// === Fetch Network Info ===
@@ -1005,6 +1049,98 @@
}
}
// === Fetch Devices List ===
async function fetchDevices() {
try {
const response = await fetch(`${API_BASE}/devices`);
if (response.ok) {
const devices = await response.json();
activeDevices = devices;
updateDeviceSelector();
}
} catch (e) {
console.error("Error fetching devices:", e);
}
}
// === Update Device Dropdown Selector ===
function updateDeviceSelector() {
const selector = document.getElementById('deviceSelector');
const previousSelection = selector.value || selectedDeviceId;
selector.innerHTML = '';
if (activeDevices.length === 0) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'No monitors connected';
selector.appendChild(opt);
return;
}
// Sort: active devices first, then offline
const sortedDevices = [...activeDevices].sort((a, b) => {
if (a.status === 'active' && b.status !== 'active') return -1;
if (a.status !== 'active' && b.status === 'active') return 1;
return new Date(b.last_seen) - new Date(a.last_seen);
});
sortedDevices.forEach(d => {
const opt = document.createElement('option');
opt.value = d.device_id;
const statusSymbol = d.status === 'active' ? '🟢' : '⚫';
const ipStr = d.ip_address && d.ip_address !== 'unknown' ? ` (${d.ip_address})` : '';
opt.textContent = `${statusSymbol} ${d.device_id}${ipStr}`;
selector.appendChild(opt);
});
if (previousSelection && sortedDevices.some(d => d.device_id === previousSelection)) {
selector.value = previousSelection;
selectedDeviceId = previousSelection;
} else {
const firstActive = sortedDevices.find(d => d.status === 'active');
if (firstActive) {
selector.value = firstActive.device_id;
selectedDeviceId = firstActive.device_id;
} else if (sortedDevices.length > 0) {
selector.value = sortedDevices[0].device_id;
selectedDeviceId = sortedDevices[0].device_id;
}
}
localStorage.setItem('selectedDeviceId', selectedDeviceId);
}
// === Change Selected Device ===
function selectDevice(deviceId) {
selectedDeviceId = deviceId;
localStorage.setItem('selectedDeviceId', deviceId);
clearVitalsDisplay();
fetchLatestReadings();
}
// === Clear Vitals from Screen ===
function clearVitalsDisplay() {
['hr', 'spo2', 'bp', 'rr', 'temp', 'map'].forEach(key => {
const valueEl = document.getElementById(`value-${key}`);
const statusEl = document.getElementById(`status-${key}`);
const cardEl = document.getElementById(`card-${key}`);
valueEl.innerHTML = '---';
valueEl.className = 'vital-value no-data';
statusEl.textContent = 'No Data';
statusEl.className = 'status-badge offline';
cardEl.classList.remove('has-value');
waveformData[key] = [];
});
document.getElementById('deviceId').textContent = 'Waiting...';
document.getElementById('patientId').textContent = 'Waiting...';
lastReadingId = null;
}
// === Setup Guide Toggle ===
function toggleSetupGuide() {
const body = document.getElementById('setupGuideBody');
@@ -1086,7 +1222,15 @@
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
updateVitals(data);
// Filter messages by selected device or auto-select if nothing selected yet
if (!selectedDeviceId && data.device_id) {
selectDevice(data.device_id);
}
if (data.device_id === selectedDeviceId) {
updateVitals(data);
}
// Refresh device list status
fetchDevices();
};
ws.onclose = () => {
useWebSocket = false;
@@ -1109,7 +1253,10 @@
async function fetchLatestReadings() {
try {
const response = await fetch(`${API_BASE}/latest-readings?limit=1`);
const url = selectedDeviceId
? `${API_BASE}/latest-readings?device_id=${encodeURIComponent(selectedDeviceId)}&limit=1`
: `${API_BASE}/latest-readings?limit=1`;
const response = await fetch(url);
if (!response.ok) throw new Error('API error');
const readings = await response.json();
@@ -1138,6 +1285,12 @@
status.pending_transmissions > 0
? `${status.pending_transmissions} pending`
: 'All sent ✓';
// Update active ports list in guide
const portsEl = document.getElementById('activePortsList');
if (portsEl && status.active_ports && status.active_ports.length > 0) {
portsEl.textContent = status.active_ports.join(', ');
}
}
} catch (e) {
if (!useWebSocket) {