From d6ffcd04128cfaa92456c3449692d31126d7bdc1 Mon Sep 17 00:00:00 2001 From: bwedding Date: Mon, 20 Jan 2025 00:56:12 +0100 Subject: [PATCH 1/2] No status heartrate --- SampleJSON.txt | 58 ++++---- dist/index.html | 4 +- package.json | 2 +- server/server.js | 222 ++++++++++++++++++++---------- src/components/Components.jsx | 28 ++-- src/components/MessageLog.jsx | 26 +++- src/pages/dashboard/Dashboard.jsx | 91 +++++++----- 7 files changed, 283 insertions(+), 148 deletions(-) diff --git a/SampleJSON.txt b/SampleJSON.txt index d459e35..fc2e3c8 100644 --- a/SampleJSON.txt +++ b/SampleJSON.txt @@ -1,6 +1,6 @@ { - "Timestamp": "2025-01-16T10:55:58.9084242Z", - "SystemId": "Maureen Jonas", + "Timestamp": "2025-01-17T09:54:24.8502369Z", + "SystemId": "Michael DeBakey", "StatusData": { "ExtLeft": { "Text": "OK", @@ -15,15 +15,15 @@ "Color": "badge-success" }, "BytesSent": { - "Text": "52", + "Text": "8", "Color": "badge-info" }, "BytesRecd": { - "Text": "0.08", + "Text": "4.87", "Color": "badge-info" }, "Strokes": { - "Text": "2,338", + "Text": "155,794", "Color": "badge-info" }, "IntLeft": { @@ -35,7 +35,7 @@ "Color": "#FF2EAE00" }, "BusLoad": { - "Text": "5%", + "Text": "0%", "Color": "#FF2EAE00" } }, @@ -47,7 +47,7 @@ "BackColor": "Default" }, "IntPressure": { - "PrimaryValue": "2.5", + "PrimaryValue": "3", "SecondaryValue": null, "BackColor": "Yellow" }, @@ -56,28 +56,28 @@ "SecondaryValue": null, "BackColor": "Default" }, - "IntPressureMin": 2, - "IntPressureMax": 3, + "IntPressureMin": 2.79999995, + "IntPressureMax": 4.19999981, "CardiacOutput": { - "PrimaryValue": "5", + "PrimaryValue": "4.9", "SecondaryValue": null, - "BackColor": "Default" + "BackColor": "Yellow" }, - "ActualStrokeLen": "20.4", + "ActualStrokeLen": "19", "TargetStrokeLen": "21.8", - "SensorTemperature": "21.99", - "ThermistorTemperature": "21.99", + "SensorTemperature": "24.9", + "ThermistorTemperature": "24.9", "CpuLoad": "73" }, "RightHeart": { - "StrokeVolume": "40", + "StrokeVolume": "39", "PowerConsumption": { - "PrimaryValue": "0.2", + "PrimaryValue": "0.1", "SecondaryValue": null, "BackColor": "Default" }, "IntPressure": { - "PrimaryValue": "9.5", + "PrimaryValue": "10", "SecondaryValue": null, "BackColor": "Default" }, @@ -86,26 +86,26 @@ "SecondaryValue": null, "BackColor": "Default" }, - "IntPressureMin": 7.5999999, - "IntPressureMax": 11.3999996, + "IntPressureMin": 8.80000019, + "IntPressureMax": 13.1990004, "CardiacOutput": { "PrimaryValue": "5.3", "SecondaryValue": null, "BackColor": "Default" }, - "ActualStrokeLen": "16.4", - "TargetStrokeLen": "18.1", - "SensorTemperature": "21.99", - "ThermistorTemperature": "28.57", + "ActualStrokeLen": "15.8", + "TargetStrokeLen": "17.7", + "SensorTemperature": "24.9", + "ThermistorTemperature": "24.02", "CpuLoad": "73" }, - "HeartRate": "144", - "OperationState": "Auto", + "HeartRate": "148", + "OperationState": "Manual", "HeartStatus": "Both Running", - "FlowLimitState": "", - "FlowLimit": "9.193", + "FlowLimitState": "Decrease", + "FlowLimit": "9.199", "AtmosPressure": "0", - "UseMedicalSensor": true, + "UseMedicalSensor": false, "AoPSensor": { "PrimaryValue": "-", "SecondaryValue": null, @@ -127,6 +127,6 @@ "BackColor": "Default" }, "IVCSensorVal": "-", - "LocalClock": "11:55:58", + "LocalClock": "10:54:24", "Messages": [] } \ No newline at end of file diff --git a/dist/index.html b/dist/index.html index 94defee..0bc048e 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,8 +5,8 @@ SRH Remote Monitor 1.0.0 - - + +
diff --git a/package.json b/package.json index ed90fb5..cc8da0f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "start": "node server/server.js", "dev": "concurrently \"node server/server.js\" \"vite\"", "build": "vite build", - "deploy": "npm run build && scp -r dist package.json index.html root@realheartremote.live:/var/www/realheartremote.live/ && ssh root@realheartremote.live \"cd /var/www/realheartremote.live && npm install && pm2 restart all\"", + "deploy": "npm run build && scp -r dist package.json index.html server root@realheartremote.live:/var/www/realheartremote.live/ && ssh root@realheartremote.live \"cd /var/www/realheartremote.live && npm install && pm2 restart all\"", "preview": "vite preview", "build:css": "tailwindcss -i ./src/input.css -o ./public/styles.css --minify", "watch:css": "tailwindcss -i ./src/input.css -o ./public/styles.css --watch" diff --git a/server/server.js b/server/server.js index 0001d73..29682d1 100644 --- a/server/server.js +++ b/server/server.js @@ -1,6 +1,9 @@ const express = require('express'); -const { WebSocketServer } = require('ws'); +const { WebSocketServer, WebSocket } = require('ws'); const http = require('http'); +const fs = require('fs').promises; +const fsSync = require('fs'); +const path = require('path'); const app = express(); const server = http.createServer(app); @@ -217,36 +220,44 @@ wss.on('connection', (ws, req) => // Broadcast connection message const connectionMessage = { - type: 'deviceMessage', + type: 'chatMessage', timestamp: new Date().toISOString(), username: displayName, message: `${displayName} just connected` }; wss.clients.forEach((client) => { - if (client.readyState === ws.OPEN) { + if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(connectionMessage)); } }); - ws.on('message', (data) => { + ws.on('message', async (data) => { try { const rawData = data.toString(); const message = JSON.parse(rawData); + //console.log('Received message:', message); + + // Log the message + await appendToLog(message).catch(err => console.error('Failed to log message:', err)); + // Handle array of system messages if (Array.isArray(message) && message.length > 0 && message[0].Type === 'systemMessage') { + //console.log('Received system messages:', message); if (ws.systemId) { const watchingClients = connectedSystems.get(ws.systemId); if (watchingClients) { message.forEach(msg => { + console.log('Processing message:', msg); const systemMessage = { type: 'systemMessage', message: msg.Message, messageType: msg.MessageType, - source: msg.Source, + source: msg.Source || wsClients.get(ws) || 'Unknown User', timestamp: msg.Timestamp }; + //console.log('Sending formatted message:', systemMessage); watchingClients.forEach(client => { if (client !== ws && client.readyState === WebSocket.OPEN) { @@ -255,79 +266,80 @@ wss.on('connection', (ws, req) => }); }); } + return; } - return; - } - // Track system if SystemId is present in message - if (message.SystemId) { - const systemId = message.SystemId; + // Track system if SystemId is present in message + if (message.SystemId) { + const systemId = message.SystemId; - // Clean up any duplicates before adding new system - cleanupDuplicateSystems(systemId); + // Clean up any duplicates before adding new system + cleanupDuplicateSystems(systemId); - // Add or update system connection if not already tracked - if (!connectedSystems.has(systemId)) { - connectedSystems.set(systemId, new Set()); - } - - // Mark this connection as the system if not already marked - if (!ws.isSystem) { - ws.isSystem = true; - ws.systemId = systemId; - // Broadcast updated systems list after marking as system - broadcastSystemsList(); - } + // Add or update system connection if not already tracked + if (!connectedSystems.has(systemId)) { + connectedSystems.set(systemId, new Set()); + } + + // Mark this connection as the system if not already marked + if (!ws.isSystem) { + ws.isSystem = true; + ws.systemId = systemId; + // Broadcast updated systems list after marking as system + broadcastSystemsList(); + } - // Forward status update to watching clients - const watchingClients = connectedSystems.get(systemId); - if (watchingClients) { - // Forward the status update - watchingClients.forEach(client => { - if (client !== ws && client.readyState === WebSocket.OPEN) { - client.send(rawData); - } - }); + // Forward status update to watching clients + const watchingClients = connectedSystems.get(systemId); + if (watchingClients) { + // Forward the status update + watchingClients.forEach(client => { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(rawData); + } + }); - // Process any messages in the status update - if (message.Messages && Array.isArray(message.Messages)) { - message.Messages.forEach(msg => { - const systemMessage = { - type: 'systemMessage', - message: msg.Message, - messageType: msg.MessageType, - source: msg.Source, - timestamp: msg.Timestamp - }; - - watchingClients.forEach(client => { - if (client !== ws && client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(systemMessage)); - } + // Process any messages in the status update + if (message.Messages && Array.isArray(message.Messages)) { + message.Messages.forEach(msg => { + const systemMessage = { + type: 'systemMessage', + message: msg.Message, + messageType: msg.MessageType, + source: msg.Source, + timestamp: msg.Timestamp, + username: wsClients.get(ws) || 'Unknown User' + }; + + watchingClients.forEach(client => { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(systemMessage)); + } + }); }); - }); + } } } - } - // Handle device messages - else if (message?.type === 'deviceMessage') { - // Format the display name from the email - const displayName = formatDisplayName(message.email); - - // Send to all clients, marking the message as "self" for the sender - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - const broadcastMessage = { - type: 'deviceMessage', - timestamp: new Date().toISOString(), - username: displayName, - message: message.message, - self: client === ws - }; - client.send(JSON.stringify(broadcastMessage)); - } - }); + // Handle device messages + else if (message?.type === 'deviceMessage' || message?.type === 'chatMessage') { + // Get the display name from message or stored client info + const displayName = message.username || wsClients.get(ws) || 'Unknown User'; + + // Send to all clients, marking the message as "self" for the sender + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + const broadcastMessage = { + type: 'chatMessage', + timestamp: message.timestamp || new Date().toISOString(), + username: displayName, + message: message.message, + self: client === ws + }; + client.send(JSON.stringify(broadcastMessage)); + } + }); + } } } catch (error) { console.error('Error processing message:', error); @@ -335,7 +347,7 @@ wss.on('connection', (ws, req) => try { const email = wsClients.get(ws) || 'Unknown User'; const broadcastMessage = { - type: 'deviceMessage', + type: 'chatMessage', timestamp: new Date().toISOString(), username: formatDisplayName(email), message: data.toString() @@ -369,7 +381,7 @@ wss.on('connection', (ws, req) => if (connectedSystems.size === 0) { const clients = connectedSystems.get(ws.systemId) || new Set(); clients.forEach(client => { - if (client.readyState === ws.OPEN) { + if (client.readyState === WebSocket.OPEN) { waitingClients.add(client); client.systemToWatch = null; } @@ -383,6 +395,76 @@ wss.on('connection', (ws, req) => }); }); +const LOG_FILE = path.join(__dirname, 'logdata.json'); +/** @type {Array<{timestamp: string, type: string, data: any}>} */ +let messageLog = []; +/** @type {fs.WriteStream} */ +let logStream; + +/** + * Initialize the log file and create write stream + * @returns {Promise} + */ +async function initializeLogFile() { + try { + await fs.access(LOG_FILE); + const data = await fs.readFile(LOG_FILE, 'utf8'); + messageLog = JSON.parse(data); + } catch { + await fs.writeFile(LOG_FILE, JSON.stringify([], null, 2)); + } + + // Create write stream in append mode + logStream = fsSync.createWriteStream(LOG_FILE, { + flags: 'a', + encoding: 'utf8' + }); + + // Handle stream errors + logStream.on('error', (error) => { + console.error('Error writing to log file:', error); + }); + + console.log('Log file initialized'); +} + +/** + * Append message to log file using write stream + * @param {any} message - The message to log + * @returns {Promise} + */ +async function appendToLog(message) { + const logEntry = { + timestamp: new Date().toISOString(), + type: Array.isArray(message) ? 'systemMessages' : 'message', + data: message + }; + messageLog.push(logEntry); + + // Write to stream with newline for easier reading + return new Promise((resolve, reject) => { + logStream.write(JSON.stringify(logEntry) + '\n', (error) => { + if (error) reject(error); + else resolve(); + }); + }); +} + +// Clean up write stream when server exits +process.on('SIGINT', () => { + if (logStream) { + logStream.end(() => { + console.log('Log file stream closed'); + process.exit(0); + }); + } +}); + +// Initialize log file when server starts +console.log('Opening Log file...'); + +initializeLogFile().catch(console.error); + server.listen(3000, () => { console.log('Server is running on port 3000'); }); diff --git a/src/components/Components.jsx b/src/components/Components.jsx index 2f046c8..81bd2f4 100644 --- a/src/components/Components.jsx +++ b/src/components/Components.jsx @@ -32,6 +32,14 @@ export function createDetailCard(label, value, iconFile = 'heart.png', color = ' stateValue = "-"; stateDesc = 'Flow State'; } + if(units === '°C') + { + detailedData.LeftHeart.formattedTemp = Number(detailedData.LeftHeart.formattedTemp).toFixed(1); + } + if(units === '%') + { + detailedData.LeftHeart.CpuLoad = Number(detailedData.LeftHeart.CpuLoad ).toFixed(1); + } return React.createElement('div', { className: 'stat bg-base-300 shadow-xl rounded-xl p-4' }, React.createElement('div', { className: 'flex justify-between items-start' }, @@ -209,7 +217,7 @@ export function createStrokeCard(label, targetStroke, actualStroke, iconFile = ' return React.createElement('div', { className: `stat ${cardBgColor} shadow-xl rounded-xl p-4` }, React.createElement('div', { className: 'flex justify-between items-start' }, React.createElement('div', { className: 'flex-1 min-w-0 pr-4' }, - React.createElement('div', { className: 'stat-title opacity-70' }, label), + React.createElement('div', { className: 'stat-title opacity-70 pt-1' }, label), React.createElement('div', { className: 'stat-value text-base-content text-2xl' }, displayValue ), @@ -352,8 +360,8 @@ export function createHeader(status, lastUpdate, isDetailedView, onToggleView, t return timestamp; }; - return React.createElement('div', { className: 'flex flex-wrap justify-between items-center mb-4 gap-2' }, - React.createElement('div', null, + return React.createElement('div', { className: 'flex flex-col sm:flex-row justify-between items-center mb-4 gap-2 w-full' }, + React.createElement('div', { className: 'w-full sm:w-auto order-1' }, React.createElement('h2', { className: 'text-lg font-bold items-center gap-2 w-44' }, 'Status', React.createElement('div', { @@ -369,7 +377,14 @@ export function createHeader(status, lastUpdate, isDetailedView, onToggleView, t `Remote: ${systemId}` ) ), - React.createElement('div', { className: 'flex gap-2' }, + + React.createElement('img', { + src: theme === 'light' ? '/logo-light-mode.png' : '/logo.png', + alt: 'Scandinavian Real Heart AB', + className: 'h-8 order-2 sm:order-3 my-2 sm:my-0' + }), + + React.createElement('div', { className: 'flex gap-2 order-3 sm:order-2 w-full sm:w-auto justify-center' }, React.createElement('button', { className: 'btn btn-primary w-[120px] sm:w-[200px] px-2 sm:px-4 text-sm sm:text-base shadow-lg', onClick: () => { @@ -383,11 +398,6 @@ export function createHeader(status, lastUpdate, isDetailedView, onToggleView, t onClick: onOpenChat }, 'Send Message') ), - React.createElement('img', { - src: theme === 'light' ? '/logo-light-mode.png' : '/logo.png', - alt: 'Scandinavian Real Heart AB', - className: 'h-8 ml-4 mr-4' - }) ); } diff --git a/src/components/MessageLog.jsx b/src/components/MessageLog.jsx index 1fd5656..02b003b 100644 --- a/src/components/MessageLog.jsx +++ b/src/components/MessageLog.jsx @@ -28,9 +28,33 @@ const MessageLog = ({ messages = [] }) => { } }; + const formatTime = (timestamp) => { + const date = new Date(timestamp); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const dd = String(date.getDate()).padStart(2, '0'); + const yy = String(date.getFullYear()).slice(-2); + const hh = String(date.getHours()).padStart(2, '0'); + const min = String(date.getMinutes()).padStart(2, '0'); + const ss = String(date.getSeconds()).padStart(2, '0'); + return `${mm}/${dd}/${yy} ${hh}:${min}:${ss}`; + }; + + const formatMessage = (msg) => { + // All messages now use source field consistently + return msg.source ? `${msg.source}: ${msg.message}` : msg.message; + }; + // Create a copy and reverse to show newest first const sortedMessages = [...messages].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + // Debug log to check message contents + console.log('Messages:', sortedMessages.map(msg => ({ + source: msg?.source, + message: msg?.message, + messageType: msg?.messageType, + timestamp: msg?.timestamp + }))); + return (
@@ -49,7 +73,7 @@ const MessageLog = ({ messages = [] }) => {
- {msg?.timestamp || '-'} + {formatTime(msg?.timestamp)} {msg?.source || '-'} {msg?.message || '-'} diff --git a/src/pages/dashboard/Dashboard.jsx b/src/pages/dashboard/Dashboard.jsx index 6f3806d..e5f4597 100644 --- a/src/pages/dashboard/Dashboard.jsx +++ b/src/pages/dashboard/Dashboard.jsx @@ -52,6 +52,9 @@ function CombinedDashboard() { CardiacOutput: '0', TargetStrokeLen: '0', ActualStrokeLen: '0', + SensorTemperature: '0', + ThermistorTemperature: '0', + CpuLoad: '0', MedicalPressure: { Name: '', PrimaryValue: '-', @@ -70,6 +73,9 @@ function CombinedDashboard() { CardiacOutput: '0', TargetStrokeLen: '0', ActualStrokeLen: '0', + SensorTemperature: '0', + ThermistorTemperature: '0', + CpuLoad: '0', MedicalPressure: { Name: '', PrimaryValue: '-', @@ -200,17 +206,23 @@ function CombinedDashboard() { const handleWebSocketMessage = React.useCallback((event) => { try { const data = JSON.parse(event.data); - if (data.type === 'deviceMessage') { - addMessage(data); + if (data.type === 'deviceMessage' || data.type === 'chatMessage') { + addMessage({ + type: 'chatMessage', + source: data.username || data.source || 'Unknown User', + message: data.message, + timestamp: data.timestamp || new Date().toISOString() + }); return; } if (data.type === 'systemMessage') { addMessage({ - username: data.source || 'System', + type: 'systemMessage', + source: data.source || 'System', message: data.message, timestamp: data.timestamp, - type: data.messageType + messageType: data.messageType }); return; } @@ -238,10 +250,11 @@ function CombinedDashboard() { if (data.Messages && Array.isArray(data.Messages)) { data.Messages.forEach(msg => { addMessage({ - username: msg.Source || 'System', + type: 'systemMessage', + source: msg.Source || 'System', message: msg.Message, timestamp: msg.Timestamp, - type: msg.MessageType + messageType: msg.MessageType }); }); } @@ -270,7 +283,10 @@ function CombinedDashboard() { CardiacOutput: data.LeftHeart?.CardiacOutput || prevData.LeftHeart.CardiacOutput, MedicalPressure: data.LeftHeart?.MedicalPressure || prevData.LeftHeart.MedicalPressure, TargetStrokeLen: data.LeftHeart?.TargetStrokeLen || prevData.LeftHeart.TargetStrokeLen, - ActualStrokeLen: data.LeftHeart?.ActualStrokeLen || prevData.LeftHeart.ActualStrokeLen + ActualStrokeLen: data.LeftHeart?.ActualStrokeLen || prevData.LeftHeart.ActualStrokeLen, + SensorTemperature: Math.round((data.LeftHeart?.SensorTemperature || prevData.LeftHeart.SensorTemperature + Number.EPSILON) * 10) / 10, + ThermistorTemperature: Math.round((data.LeftHeart?.ThermistorTemperature || prevData.LeftHeart.ThermistorTemperature + Number.EPSILON) * 10) / 10, + CpuLoad: Math.round((data.LeftHeart?.CpuLoad || prevData.LeftHeart.CpuLoad + Number.EPSILON) * 10) / 10 }, RightHeart: { StrokeVolume: data.RightHeart?.StrokeVolume || prevData.RightHeart.StrokeVolume, @@ -282,7 +298,10 @@ function CombinedDashboard() { CardiacOutput: data.RightHeart?.CardiacOutput || prevData.RightHeart.CardiacOutput, MedicalPressure: data.RightHeart?.MedicalPressure || prevData.RightHeart.MedicalPressure, TargetStrokeLen: data.RightHeart?.TargetStrokeLen || prevData.RightHeart.TargetStrokeLen, - ActualStrokeLen: data.RightHeart?.ActualStrokeLen || prevData.RightHeart.ActualStrokeLen + ActualStrokeLen: data.RightHeart?.ActualStrokeLen || prevData.RightHeart.ActualStrokeLen, + SensorTemperature: Math.round((data.RightHeart?.SensorTemperature || prevData.RightHeart.SensorTemperature + Number.EPSILON) * 10) / 10, + ThermistorTemperature: Math.round((data.RightHeart?.ThermistorTemperature || prevData.RightHeart.ThermistorTemperature + Number.EPSILON) * 10) / 10, + CpuLoad: Math.round((data.RightHeart?.CpuLoad || prevData.RightHeart.CpuLoad + Number.EPSILON) * 10) / 10 }, HeartRate: data.HeartRate || prevData.HeartRate, CVPSensor: data.CVPSensor || prevData.CVPSensor, @@ -356,10 +375,13 @@ function CombinedDashboard() { const handleSendMessage = React.useCallback((message) => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { const email = user.emailAddresses[0].emailAddress; + const username = user.fullName || email.split('@')[0]; const messageData = { - type: 'deviceMessage', + type: 'chatMessage', message: message, - email: user.emailAddresses[0].emailAddress + email: email, + username: username, + timestamp: new Date().toISOString() }; wsRef.current.send(JSON.stringify(messageData)); @@ -428,17 +450,8 @@ function CombinedDashboard() { React.createElement('div', { className: 'card-body px-1.5 py-0.5' }, React.createElement('h2', { className: 'card-title opacity-80 mt-0 py-0' }, 'System Status'), - React.createElement('div', { className: 'grid grid-cols-2 sm:grid-cols-4 gap-4' }, - createCard('Heart Rate', `${detailedData.HeartRate} BPM`, 'red'), - createCard('Operation State', detailedData.OperationState, 'blue'), - createCard('Heart Status', detailedData.HeartStatus, 'green'), - React.createElement('div', { className: 'grid grid-rows-2 gap-2 py-1' }, - createSensorStatusCard('Medical Sensors', detailedData.UseMedicalSensor), - createSensorStatusCard('Internal Sensors', !detailedData.UseMedicalSensor) - ) - ), - React.createElement('div', { className: 'flex justify-between items-start mt-2 mb-2 flex-wrap gap-2' }, - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'grid grid-cols-5 sm:flex sm:justify-between sm:items-start mt-2 mb-2 gap-x-1 gap-y-2 sm:flex-wrap sm:gap-2' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -448,7 +461,7 @@ function CombinedDashboard() { }, detailedData.StatusData.ExtLeft.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Ext L') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -458,7 +471,7 @@ function CombinedDashboard() { }, detailedData.StatusData.ExtRight.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Ext R') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -468,7 +481,7 @@ function CombinedDashboard() { }, detailedData.StatusData.IntLeft.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Int Lt') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -478,23 +491,23 @@ function CombinedDashboard() { }, detailedData.StatusData.IntRight.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Int Rt') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: `badge ${detailedData.StatusData.Strokes.Color} w-full text-center` }, detailedData.StatusData.Strokes.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Strokes') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: `badge ${detailedData.StatusData.BytesSent.Color} w-full text-center` }, detailedData.StatusData.BytesSent.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'Sent') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: `badge ${detailedData.StatusData.BytesRecd.Color} w-full text-center` }, detailedData.StatusData.BytesRecd.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'MB Rec') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: `badge ${detailedData.StatusData.CANStatus.Color} w-full text-center` }, detailedData.StatusData.CANStatus.Text), React.createElement('span', { className: 'text-xs mt-1' }, 'CAN') ), - React.createElement('div', { className: 'flex-1 flex flex-col items-center' }, + React.createElement('div', { className: 'flex flex-col items-center sm:flex-1' }, React.createElement('span', { className: 'badge w-full text-center', style: { @@ -517,7 +530,7 @@ function CombinedDashboard() { React.createElement('div', { className: 'card-body py-1 px-1.5' }, React.createElement('h2', { className: 'card-title opacity-80' }, 'Left Heart'), - React.createElement('div', { className: 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4' }, + React.createElement('div', { className: 'grid grid-cols-2 md:grid-cols-4 sm:grid-cols-4 lg:grid-cols-4 gap-4' }, //createDetailCard('Stroke Vol', detailedData.LeftHeart.StrokeVolume, 'stroke.png', 'base-content', detailedData, 'mL'), createStrokeCard('Stroke Len', detailedData.LeftHeart.TargetStrokeLen, detailedData.LeftHeart.ActualStrokeLen, 'piston.png'), @@ -526,11 +539,14 @@ function CombinedDashboard() { //createDetailCard('Atrial Press', detailedData.LeftHeart.AtrialPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), - createDetailCard('Medical Press', detailedData.LeftHeart.MedicalPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), + createDetailCard('Med Sensor', detailedData.LeftHeart.MedicalPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), createDetailCard('Cardiac Out', detailedData.LeftHeart.CardiacOutput, 'cardiacout.png', 'base-content', detailedData, 'L/min'), - createDetailCard('Power', detailedData.LeftHeart.PowerConsumption, 'watts.png', 'base-content', detailedData, 'W') + createDetailCard('Power', detailedData.LeftHeart.PowerConsumption, 'watts.png', 'base-content', detailedData, 'W'), + createDetailCard('Sensor Tmp', detailedData.LeftHeart.SensorTemperature, 'temperature.png', 'base-content', detailedData, '°C'), + createDetailCard('Therm Tmp', detailedData.LeftHeart.ThermistorTemperature, 'temperature.png', 'base-content', detailedData, '°C'), + createDetailCard('CPU Load', detailedData.LeftHeart.CpuLoad, 'cpu.png', 'base-content', detailedData, '%') ) @@ -544,7 +560,7 @@ function CombinedDashboard() { React.createElement('div', { className: 'card-body px-1.5 py-1' }, React.createElement('h2', { className: 'card-title opacity-80' }, 'Right Heart'), - React.createElement('div', { className: 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4' }, + React.createElement('div', { className: 'grid grid-cols-2 md:grid-cols-4 sm:grid-cols-4 lg:grid-cols-4 gap-4' }, //createDetailCard('Stroke Vol', detailedData.RightHeart.StrokeVolume, 'stroke.png', 'base-content', detailedData, 'mL'), createStrokeCard('Stroke Len', detailedData.RightHeart.TargetStrokeLen, detailedData.RightHeart.ActualStrokeLen, 'piston.png'), @@ -553,11 +569,14 @@ function CombinedDashboard() { //createDetailCard('Atrial Press', detailedData.RightHeart.AtrialPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), - createDetailCard('Medical Press', detailedData.RightHeart.MedicalPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), + createDetailCard('Med Sensor', detailedData.RightHeart.MedicalPressure, 'pressure.png', 'base-content', detailedData, 'mmHg'), createDetailCard('Cardiac Out', detailedData.RightHeart.CardiacOutput, 'cardiacout.png', 'base-content', detailedData, 'L/min'), - createDetailCard('Power', detailedData.RightHeart.PowerConsumption, 'watts.png', 'base-content', detailedData, 'W') + createDetailCard('Power', detailedData.RightHeart.PowerConsumption, 'watts.png', 'base-content', detailedData, 'W'), + createDetailCard('Sensor Tmp', detailedData.RightHeart.SensorTemperature, 'temperature.png', 'base-content', detailedData, '°C'), + createDetailCard('Therm Tmp', detailedData.RightHeart.ThermistorTemperature, 'temperature.png', 'base-content', detailedData, '°C'), + createDetailCard('CPU Load', detailedData.RightHeart.CpuLoad, 'cpu.png', 'base-content', detailedData, '%') ) @@ -571,7 +590,7 @@ function CombinedDashboard() { React.createElement('div', { className: 'card-body px-1.5 py-1' }, React.createElement('h2', { className: 'card-title opacity-80' }, 'System Pressures'), - React.createElement('div', { className: 'grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5 gap-4' }, + React.createElement('div', { className: 'grid grid-cols-2 md:grid-cols-5 sm:grid-cols-5 lg:grid-cols-5 gap-4' }, createDetailCard('CVP', detailedData.CVPSensor, 'pressure.png', 'base-content', detailedData, 'mmHg'), createDetailCard('PAP', detailedData.PAPSensor, 'pressure.png', 'base-content', detailedData, 'mmHg'), From c89d2a51e600801921044b80616a1120f36e30ad Mon Sep 17 00:00:00 2001 From: bwedding Date: Mon, 20 Jan 2025 00:57:28 +0100 Subject: [PATCH 2/2] res of files --- public/cpu.png | Bin 0 -> 6326 bytes public/temperature.png | Bin 0 -> 8381 bytes server/server2.js | 323 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 public/cpu.png create mode 100644 public/temperature.png create mode 100644 server/server2.js diff --git a/public/cpu.png b/public/cpu.png new file mode 100644 index 0000000000000000000000000000000000000000..f01b9ce9880a385a6adc6c51dd857e717a682ab8 GIT binary patch literal 6326 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 literal 0 HcmV?d00001 diff --git a/public/temperature.png b/public/temperature.png new file mode 100644 index 0000000000000000000000000000000000000000..54570282eeaf469e3d9935757963a3fcf4f54fbb GIT binary patch literal 8381 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 */ +let messageLog = []; +/** @type {fs.WriteStream} */ +let logStream; + +/** + * Initialize the log file and create write stream + * @returns {Promise} + */ +async function initializeLogFile() { + try { + await fs.access(LOG_FILE); + const data = await fs.readFile(LOG_FILE, 'utf8'); + messageLog = JSON.parse(data); + } catch { + await fs.writeFile(LOG_FILE, JSON.stringify([], null, 2)); + } + + // Create write stream in append mode + logStream = fsSync.createWriteStream(LOG_FILE, { + flags: 'a', + encoding: 'utf8' + }); + + // Handle stream errors + logStream.on('error', (error) => { + console.error('Error writing to log file:', error); + }); + + console.log('Log file initialized'); +} + +/** + * Append message to log file using write stream + * @param {any} message - The message to log + * @returns {Promise} + */ +async function appendToLog(message) { + const logEntry = { + timestamp: new Date().toISOString(), + type: Array.isArray(message) ? 'systemMessages' : 'message', + data: message + }; + messageLog.push(logEntry); + + // Write to stream with newline for easier reading + return new Promise((resolve, reject) => { + logStream.write(JSON.stringify(logEntry) + '\n', (error) => { + if (error) reject(error); + else resolve(); + }); + }); +} + +// Clean up write stream when server exits +process.on('SIGINT', () => { + if (logStream) { + logStream.end(() => { + console.log('Log file stream closed'); + process.exit(0); + }); + } +}); + +// Format display name from email +const formatDisplayName = (email) => { + if (!email) return 'Someone'; + + if (email.endsWith('@realheart.se')) { + const [name] = email.split('@'); + const [firstName, lastName] = name.split('.'); + if (firstName && lastName) { + return `${firstName.charAt(0).toUpperCase() + firstName.slice(1)} ${lastName.charAt(0).toUpperCase() + lastName.slice(1)}`; + } + } + return email; +}; + +// Handle uncaught exceptions +process.on('uncaughtException', (err) => +{ + console.error('Uncaught Exception:', err); + process.exit(1); // This will trigger PM2 to restart the process + }); + +// Handle unhandled promise rejections +process.on('unhandledRejection', (reason, promise) => +{ + console.error('Unhandled Rejection at:', promise, 'reason:', reason); + process.exit(1); +}); + +// Optional: Handle SIGTERM signal +process.on('SIGTERM', () => +{ + console.info('SIGTERM signal received'); + + // Close WebSocket server gracefully + wss.close(() => { + console.log('WebSocket server closed'); + + // Close HTTP server gracefully + server.close(() => { + console.log('HTTP server closed'); + process.exit(0); + }); + }); + + // Force exit if graceful shutdown fails + setTimeout(() => + { + console.error('Forced shutdown'); + process.exit(1); + }, 10000); // Force shutdown after 10 seconds +}); + +function isCriticalError(error) +{ + const criticalErrors = [ + 'ECONNRESET', + 'EPIPE', + 'ERR_STREAM_DESTROYED' + ]; + + return criticalErrors.includes(error.code); + } + +// WebSocket connection handling +wss.on('connection', (ws, req) => +{ + ws.on('error', (error) => { + console.error('WebSocket error:', error); + if (isCriticalError(error)) + { + process.exit(1); + } + }); + + // Get email from URL parameters + const url = new URL(req.url, `http://${req.headers.host}`); + const connectionType = url.searchParams.get('type'); + const displayName = connectionType === 'device' + ? url.searchParams.get('device-name') || 'Unknown Device' + : formatDisplayName(url.searchParams.get('email')); + + // Broadcast connection message + const connectionMessage = { + type: 'deviceMessage', + timestamp: new Date().toISOString(), + username: displayName, + message: `${displayName} just connected` + }; + + wss.clients.forEach((client) => { + if (client.readyState === ws.OPEN) { + client.send(JSON.stringify(connectionMessage)); + } + }); + + ws.on('message', async (data) => { + try { + const message = JSON.parse(data.toString()); + let dataString = data.toString(); + + // Log the message + await appendToLog(message).catch(err => console.error('Failed to log message:', err)); + + // Handle device messages differently from data updates + if (message?.type === 'deviceMessage') + { + // Format the display name from the email + const displayName = formatDisplayName(message.email); + + // Send to all clients, marking the message as "self" for the sender + wss.clients.forEach((client) => { + if (client.readyState === ws.OPEN) { + const broadcastMessage = { + type: 'deviceMessage', + timestamp: new Date().toISOString(), + username: displayName, + message: message.message, + self: client === ws + }; + client.send(JSON.stringify(broadcastMessage)); + } + }); + } + else if (message?.type === 'deviceData') + { + // Forward the original data to other clients + wss.clients.forEach((client) => { + if (client !== ws && client.readyState === ws.OPEN) { + // Send the original data first + client.send(dataString); + + // If there are messages in the data, send them separately + if (message.Messages && Array.isArray(message.Messages)) { + message.Messages.forEach(msg => { + const systemMessage = { + type: 'systemMessage', + message: msg.Message, + messageType: msg.MessageType, + source: msg.Source, + timestamp: msg.Timestamp + }; + client.send(JSON.stringify(systemMessage)); + }); + } + } + }); + } + else + { + // Handle other data updates (broadcast to other clients only) + wss.clients.forEach((client) => { + if (client !== ws && client.readyState === ws.OPEN) { + // Ensure UseMedicalSensor is boolean before sending + let dataToSend = dataString; + if (message.UseMedicalSensor !== undefined) { + const modifiedMessage = { ...message }; + modifiedMessage.UseMedicalSensor = modifiedMessage.UseMedicalSensor === true || modifiedMessage.UseMedicalSensor === 'true'; + dataToSend = JSON.stringify(modifiedMessage); + } + // Send the data + client.send(dataToSend); + + // If there are messages in the data, send them separately + if (message.Messages && Array.isArray(message.Messages)) { + message.Messages.forEach(msg => { + const systemMessage = { + type: 'systemMessage', + message: msg.Message, + messageType: msg.MessageType, + source: msg.Source, + timestamp: msg.Timestamp + }; + client.send(JSON.stringify(systemMessage)); + }); + } + } + }); + } + } + catch (error) + { + console.error('Error processing message:', error); + // Don't forward raw messages on parse error + // Instead, try to handle it as a device message if possible + try { + const email = wsClients.get(ws) || 'Unknown User'; + const broadcastMessage = { + type: 'deviceMessage', + timestamp: new Date().toISOString(), + username: formatDisplayName(email), + message: data.toString() + }; + + const broadcastString = JSON.stringify(broadcastMessage); + + // Send to ALL clients INCLUDING the sender + wss.clients.forEach((client) => { + if (client.readyState === ws.OPEN) { + client.send(broadcastString); + } + }); + } + catch (innerError) + { + console.error('Error broadcasting message:', innerError); + } + } + }); + + ws.on('close', () => { + console.log('Client disconnected'); + wsClients.delete(ws); + }); +}); + +server.listen(3000, () => { + console.log('Server is running on port 3000'); +}); + +function checkCriticalConditions() { + // Example: Check memory usage + const usedMemory = process.memoryUsage().heapUsed / 1024 / 1024; + if (usedMemory > 400) { // More than 400MB since server only has 512MB RAM + console.error('Memory threshold exceeded'); + process.exit(1); + } + + // Example: Check WebSocket connections + const clientCount = wss.clients.size; + if (clientCount > 100) { // Too many connections + console.error('Too many WebSocket connections'); + process.exit(1); + } + } + + // Run checks periodically + setInterval(checkCriticalConditions, 60000); + +// Initialize log file when server starts +console.log('Opening Log file...'); +initializeLogFile().catch(console.error); \ No newline at end of file