refactor: implement packet parsing heuristics and introduce device-level state caching to throttle database writes and vitals updates
This commit is contained in:
@@ -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 >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) {
|
||||
|
||||
Reference in New Issue
Block a user