Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
6001CEM-Cloud-Connector/fusionSniffer.js
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
365 lines (338 sloc)
11.8 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const net = require('net'); | |
let fusionClient = null; | |
const clientData = { | |
"meta": {}, | |
"log": { | |
"fusion": {}, | |
}, | |
"devices": [], | |
"liveevents": [], | |
"eventhistory": [], | |
} | |
const updateClientData = () => { | |
if (process.send) { | |
process.send(clientData); | |
} else { | |
console.log("[SRV] Error: No process to send to") | |
} | |
}; | |
const fusionDeviceTypes = { | |
1: "System Faults", | |
2: "Nurse Call System", | |
3: "Cell Call System", | |
4: "General Alarm System", | |
5: "Fire Alarm System", | |
6: "Medical Gas System", | |
7: "Attack Alarm System", | |
8: "Cell Call IP File Server / System", | |
9: "Cell Misting System", | |
10: "SSG Phone Management Server (Lon)", | |
11: "SSG RTLS Gateway Server", | |
12: "3rd Party Fire Alarm System", | |
14: "Ultima+ Nurse Call System", | |
20: "iButton Registration Point", | |
21: "Popup Client", | |
22: "Smart Client", | |
128: "SSG Data Management Server", | |
129: "SSG Master Display", | |
130: "SSG Messaging Server", | |
131: "SSG Centra Server", | |
132: "SSG BMS Integration Server", | |
133: "SSG Phone Management Server (IP)", | |
134: "SSG Nurse Call Gateway Server", | |
135: "SSG Fusion Data-Link", | |
254: "SSG Backup Fusion-IP Router", | |
} | |
const fusionDeviceIcons = { | |
1: "triangle-exclamation", | |
2: "hospital", | |
3: "handcuffs", | |
4: "siren-on", | |
5: "fire", | |
6: "cloud", | |
7: "user-ninja", | |
8: "server", | |
9: "smog", | |
10: "phone-alt", | |
11: "router", | |
12: "sensor-fire", | |
14: "heart-circle-plus", | |
20: "fingerprint", | |
21: "envelope", | |
22: "mobile-screen", | |
128: "database", | |
129: "map", | |
130: "envelope", | |
131: "wrench", | |
132: "buildings", | |
133: "mobile-signal-out", | |
134: "link-horizontal", | |
135: "tower-cell", | |
254: "network-wired" | |
} | |
for (let i = 192; i < 250; i++) { | |
fusionDeviceTypes[i] = "3rd Party System"; | |
} | |
for (let i = 192; i < 250; i++) { | |
fusionDeviceTypes[i] = "3rd Party System"; | |
} | |
const messageTypes = { | |
0: "System Identification Request", | |
1: "System Identification Response", | |
2: "Health Check Request", | |
3: "Active Events Request", | |
4: "Disconnected Headend", | |
5: "Health Check Reply", | |
6: "Time + Date Sync", | |
7: "Routing ID Table", | |
8: "Extra Data Message", | |
9: "System ID Change", | |
10: "Routing ID Table Request", | |
33: "Event Activated", | |
34: "Event Deactivated", | |
35: "Device Info Request", | |
36: "Device Info Response", | |
37: "Special Message", | |
38: "Event Cleardown", | |
40: "Global Message", | |
41: "Control Message - Event", | |
42: "Acknowledged Message", | |
43: "Not Acknowledged Message", | |
44: "Control Message - Data", | |
50: "BACnet Device Status Request", | |
51: "BACnet Device Status", | |
52: "DS BACnet DB Request", | |
53: "DS BACnet DB Response", | |
54: "DS BACnet Device Status Request", | |
55: "DS BACnet Device Status", | |
56: "BMS Heartbeat", | |
57: "Reset Event", | |
200: "Unified Message", | |
201: "Unified Message Response", | |
}; | |
const fusionMsgQueue = []; | |
let initalBuffer = false; | |
const connectToFusion = () => { | |
console.log("[FUS] Connecting to Fusion protocol..."); | |
fusionClient = new net.Socket(); | |
fusionClient.on('data', function (rawData) { | |
fusionMsgQueue.push(rawData); | |
if (!initalBuffer && rawData[1] == 0) { | |
parseFusionMsg(rawData); | |
} else if (initalBuffer) { | |
newFusionMsg(); | |
} | |
}); | |
console.log("MSG Receiver defined") | |
fusionClient.on('error', function (err) { | |
if (err.code === 'ECONNREFUSED') { | |
console.log('[FUS] Fusion is not running - waiting 60 seconds...'); | |
setTimeout(connectToFusion, 60000); | |
} else { | |
console.log('[FUS] Fusion error: ' + err); | |
setTimeout(connectToFusion, 5000); | |
} | |
}); | |
fusionClient.on('close', function () { | |
fusionMsgQueue.length = 0; | |
}); | |
fusionClient.connect(30000, '127.0.0.1', function () { | |
console.log('[FUS] Connected to Fusion server - handshake should be soon...'); | |
initalBuffer = true; | |
}); | |
} | |
const newFusionMsg = () => { | |
const msgcount = fusionMsgQueue.length; | |
while (fusionMsgQueue.length > 0) { | |
// connectToFusion.pause(); | |
parseFusionMsg(fusionMsgQueue.shift()); | |
} | |
// connectToFusion.resume(); | |
return msgcount; | |
} | |
parseFusionMsg = (rawData) => { | |
// Parse data to readable bytes | |
strData = Buffer.from(rawData).toString('hex'); | |
strData = strData.replace(/(.{2})/g, "$1 ").trim(); | |
// console.log(strData); | |
dataLength = rawData[0]; | |
dataHeader = rawData[1]; | |
dataData = rawData.subarray(2, dataLength + 2); | |
// datetime string as key dd/mm hh:mm:ss | |
// remove year from date | |
dt = new Date(); | |
date = dt.toLocaleString(); | |
epoch = dt.getTime(); | |
dtime = date.split(" ")[1]; | |
date = date.split(" ")[0].slice(0, -6); | |
datetime = date + " " + dtime; | |
if (messageTypes[dataHeader] == undefined) { | |
clientData.log.fusion[epoch] = { "msg": "Unknown Message Type", dt: datetime }; | |
} else if ([2, 5].includes(dataHeader)) { | |
// skip healthchecks from log | |
} else { | |
clientData.log.fusion[epoch] = { "msg": messageTypes[dataHeader], dt: datetime } | |
} | |
updateClientData(); | |
switch (dataHeader) { | |
// HANDSHAKE | |
case 0x00: | |
console.log("[FUS] Handshake started..."); | |
clientID = dataData[0]; | |
clientData.meta.psuedoID = clientID; | |
updateClientData(); | |
fusionClient.write(Buffer.from([0x0B, 0x01, 0x00, clientID, 0x02, 0x31, 0x2E, 0x33, 0x30, 0x00, 0x00, 0x00])); | |
console.log("[FUS] Handshake finished w/ client ID: " + clientID); | |
break; | |
// HEADEND CLIENT | |
case 0x01: // Connect | |
switch (dataData[2]) { | |
case 2: // Ultima or Codemlon | |
clientData.meta.clientType = 0; | |
clientData.meta.clientStr = "Nurse Call"; | |
updateClientData(); | |
case 14: // Acelo | |
clientData.meta.clientType = 1; | |
clientData.meta.clientStr = "Acelo"; | |
updateClientData(); | |
} | |
console.log("[FUS] Device connected: " + fusionDeviceTypes[dataData[2]]); | |
// split remaining data by 0x00 delimiter | |
// first chunk is version, second and third is system specific | |
chunks = dataData.subarray(3).toString().split("\0") | |
// remove last chunk as is blank | |
chunks.pop(); | |
updated = false; | |
for (i = 0; i < clientData.devices.length; i++) { | |
if (clientData.devices[i].sysID == dataData[1]) { | |
clientData.devices[i].connected = true; | |
clientData.devices[i].lastConn = epoch; | |
updated = true; | |
} | |
} | |
if (!updated) { | |
clientData.devices.push({ | |
"sysID": dataData[1], | |
"sysType": dataData[2], | |
"sysTypeStr": fusionDeviceTypes[dataData[2]], | |
"sysVersion": chunks[0], | |
"icon": fusionDeviceIcons[dataData[2]], | |
"data1": chunks[1], | |
"data2": chunks[2], | |
"connected": true, | |
"lastConn": epoch | |
}); | |
} | |
updateClientData(); | |
// fusionClient.write(Buffer.from([3, 10, clientData.meta.psuedoID, dataData[1]])); | |
break; | |
case 0x08: // Extra Data | |
console.log("[FUS] Headend client extra data"); | |
break; | |
case 0x04: // Disconnect | |
console.log("[FUS] Headend client disconnected:"); | |
console.log(dataData[1]); | |
for (i = 0; i < clientData.devices.length; i++) { | |
if (clientData.devices[i].sysID == dataData[1]) { | |
clientData.devices[i].connected = false; | |
updateClientData(); | |
} | |
} | |
break; | |
// HEALTHCHECKS | |
case 0x02: | |
console.log("[FUS] Healthcheck request"); | |
break; | |
case 0x05: | |
console.log("[FUS] Healthcheck response"); | |
break; | |
// CALLS | |
case 0x21: // Event Activated | |
console.log("[FUS] Event Activated"); | |
labels = dataData.subarray(21); | |
// labels split by 0x00 delimiter | |
labels = labels.toString().split("\0"); | |
eventData = { | |
"clientID": dataData[1], | |
"serverID": dataData[2], | |
"eventtime": dataData.subarray(3, 11), | |
"sysType": dataData[11], | |
"sysString": fusionDeviceTypes[dataData[11]], | |
"params": dataData.subarray(12, 20), | |
"label1": labels[0], | |
"label2": labels[1], | |
"label3": labels[2], | |
"label4": labels[3], | |
"label5": labels[4], | |
"active": true, | |
"evtIcon": fusionDeviceIcons[dataData[11]], | |
"time": epoch, | |
// readable time in hh:mm | |
"readtime": dtime.slice(0, -3) | |
} | |
clientData.liveevents.push(eventData); | |
clientData.eventhistory.push(eventData); | |
updateClientData(); | |
break; | |
case 0x22: // Event Deactivated | |
console.log("[FUS] Event Deactivated"); | |
// find event with matching clientID, remove from live, change active on history | |
clientData.liveevents.forEach((event, index) => { | |
if (event.clientID == dataData[1]) { | |
clientData.liveevents.splice(index, 1); | |
clientData.eventhistory.forEach((event, index) => { | |
if (event.clientID == dataData[1]) { | |
clientData.eventhistory[index].active = false; | |
} | |
}); | |
} | |
}); | |
updateClientData(); | |
break; | |
// TIME + DATE | |
case 0x06: | |
const year = parseInt(strData.split(" ")[9] + strData.split(" ")[10], 16); | |
const date = new Date(year, dataData[6] - 1, dataData[5], dataData[2], dataData[3], dataData[4]); | |
clientData.meta.fusionDT = date; | |
updateClientData(); | |
console.log("[FUS] Time and Date Sync: " + date); | |
break; | |
// TODO | |
// IP TABLES | |
case 0x07: | |
console.log(dataData); | |
deviceID = dataData[0]; | |
sysType = dataData[1]; | |
sysTypeStr = fusionDeviceTypes[sysType]; | |
wardNo = dataData[2]; | |
ipaddress = dataData.subarray(3).toString(); | |
clientData.devices.forEach((device, index) => { | |
if (device.sysID == deviceID) { | |
console.log("[FUS] IP Table found for device " + deviceID); | |
clientData.devices[index].ipaddress = ipaddress; | |
clientData.devices[index].wardNo = wardNo; | |
} | |
}); | |
updateClientData(); | |
break; | |
default: | |
// Remaining Codes: | |
// 0x03 - Request Active Events | |
// 0x07 - Routing ID Table - Ignore | |
// 0x09 - System ID Change | |
// 0x0A - Routing ID Table Request - Ignore | |
// 0x(35) - Device Info Request | |
// 0x(36) - Device Info | |
// 0x(37) - Special Message | |
// 0x(38) - Cleardown Message - Removes data station events | |
// 0x(40) - Global Message | |
// 0x(41) - Control (Event) Message | |
// 0x(42) - Ack / Nack Message | |
// 0x(43) - Control (Data) Message | |
// 0x(52) - 0x(56) - BACNet - Can be ignored | |
console.log("[FUS] Unhandled message: " + dataHeader) | |
console.log(strData) | |
break; | |
} | |
} | |
connectToFusion(); |