UNPKG

blackmagic-atem-nodered

Version:
521 lines (470 loc) 22.9 kB
var udp = require('dgram'); var ping = require("ping"); var commands = require("./commands/commandList.js"); module.exports = function(RED) { //Main node definition function ATEMNetwork(config) { RED.nodes.createNode(this, config); var inProcessingIncoming = false; var node = this; var name = config.name; var ipAddress = config.ipAddress; var port = 9910; var server = null; var pingCheck = null; var sessionId = undefined; var timeoutCount = 0; var localPacketId = 1; var timeoutInterval = undefined; var handshakeInterval = undefined; var messageProcessingInterval = undefined; var processBuffers = true; node.information = { "name": name, "type": node.type, "status": "disconnected", "connectionTimeout": 0, "debug": true } var messageCallbacks = []; var statusCallbacks = []; var sendBuffer = []; var receiveBuffer = []; //var sendInterval = setInterval(function() {processSendBuffer();}, 10); messageProcessingInterval = setInterval(function() { if(processBuffers == true) { processReceiveBuffer(); processSendBuffer(); } }, 10); //Pings the server, returns true if connected function checkConnection(func) { ping.sys.probe(ipAddress, function(status) { func(status); }); } //When the flows are stopped this.on("close", function() { clearInterval(pingCheck); clearInterval(messageProcessingInterval); clearInterval(handshakeInterval); clearInterval(timeoutInterval); server.close(); commands.close(); }); //Process incoming message object (this does no validation so this needs to be done before calling this fn) this.send = function(msg, sender) { if(node.information.status !== "connected") { node.sendStatus("red", "Not Connected!"); } else { var cmd = commands.findCommand(msg.payload.cmd); if(cmd == null) { if(msg.payload.cmd.toUpperCase() == "RAW") { //Build the raw packet to be sent //If the user has defined a name for the command add it to the start of the packet try { var nameBuffer = new Buffer.from(msg.payload.data.name); msg.payload.data.packet = Buffer.concat([nameBuffer, msg.payload.data.packet]); } catch(error){} sendBuffer.push(generatePacket(msg.payload.data.packet, sender)); } else { node.sendStatus("red", "Unknown Command", "Unknown command: " + msg.payload.cmd); return; } } else { var success = cmd.sendData(msg, commands); if(success != null) { switch(success.direction) { //The data was stored and should just be returned case "node": { if(success.command != null) { if(typeof success.command.payload.data === "string" || success.command.payload.data instanceof String) { node.sendStatus("red", "Internal Error", success.command.payload.data); } else { success.command.topic = "command"; messageCallback(success.command); } } else {node.sendStatus("red", "Internal Error", "The returned data was null");} break; } //The data needs to be requested from the server case "server": { //Generate the packet var sendIt = true; var nameBuffer = new Buffer.from(success.name); //Check if the command already exists in the buffer, if so don't add another one! for(var k in sendBuffer) { if(sendBuffer[k].commandPacket.compare(Buffer.concat([nameBuffer, success.command.packet])) == 0) { sendIt = false; } } if(sendIt == true) { sendBuffer.push(generatePacket(Buffer.concat([nameBuffer, success.command.packet]), sender)); } break; } default: { console.log("Internal Error: Unsupported direction"); break; } } } else { node.sendStatus("red", "Internal Error", "The packet was null"); } } } } //Generates a packet. Expects a commandPacket containing the command from the name >> function generatePacket(commandPacket, sender) { var message = { "packet": null, "packetId": localPacketId, "commandPacket": commandPacket, "sender": sender, "attempts": 1, "timeout": 0 } var packet = new Buffer.alloc(16).fill(0); packet[0] = parseInt((16+commandPacket.length)/256 | 0x88); packet[1] = parseInt((16+commandPacket.length)%256); packet[2] = sessionId[0]; packet[3] = sessionId[1]; packet.writeInt16BE(message.packetId, 10); packet[10] = parseInt(message.packetId/256); packet[11] = parseInt(message.packetId%256); packet[12] = parseInt((4+commandPacket.length)/256); packet[13] = parseInt((4+commandPacket.length)%256); message.packet = new Buffer.concat([packet, commandPacket]); return message; } //Send out all commands in the send buffer function processSendBuffer() { //Limit the send buffer to 20 commands if(sendBuffer.length > 20) { sendBuffer.splice(0, 20); } if(sendBuffer.length > 0 && inProcessingIncoming == false) { localPacketId++; try{server.send(sendBuffer[sendBuffer.length - 1].packet, port, ipAddress);} catch(e){node.error("Attempted to send a message but the server was closed: " + e); success = false; sendBuffer = [];} node.sendStatus("yellow", "Sending..."); sendBuffer.splice(sendBuffer.length - 1, 1); } // if(sendBuffer.length > 0 && inProcessingIncoming == false) { // if(sendBuffer[0].timeout <= 0) { // if(sendBuffer[0].attempts == 2) { // //Failed // sendBuffer.splice(0, 1); // // node.sendStatus("red", "Failed to Send: Timeout"); // // timeoutCount++; // // if(timeoutCount > 5) { // // //We have had several timeout issues we must be disconnected // // console.log("TIMEOUT"); // // statusCallback("disconnected", "timeout"); // // } // } // else { // var success = true; // localPacketId++; // try{server.send(sendBuffer[0].packet, port, ipAddress);} // catch(e){node.error("Attempted to send a message but the server was closed: " + e); success = false;} // if(success) { // //Sent // sendBuffer[0].attempts = 2; // sendBuffer[0].timeout = 10; // node.sendStatus("yellow", "Sending..."); // } // } // } // else {sendBuffer[0].timeout -= 1;} //} } //Attempt connection to the HDL controller function connect(ipAddress, port) { //If already open, close before reconnecting try{server.close();}catch(error){} sendBuffer = []; receiveBuffer = []; sessionId = undefined; localPacketId = 1; server = udp.createSocket('udp4'); server.on('error', function(err) { node.error("An Error Occured: " + err); node.sendStatus("red", "Internal Error", err); }); //Attempt handshake server.bind(port); setTimeout(function(){handshake();}, 5000); } //Send a message to the subscribed nodes (appears on the flow) node.sendStatus = function(colour, message, extraInformation = "") { for(var i = 0; i < statusCallbacks.length; i++) { statusCallbacks[i](colour, message, extraInformation); } } //Send a message to the subscribed nodes (appears on the flow) node.sendMessage = function(message) { for(var i = 0; i < messageCallbacks.length; i++) { messageCallbacks[i](message); } } node.addStatusCallback = function(func) {statusCallbacks.push(func);} node.addMessageCallback = function(func) {messageCallbacks.push(func);} //Now lets connect! connect(ipAddress, port); //When a status is received function statusCallback(state, information) { switch(state) { case "connected": { node.sendStatus("green", "Connected!"); node.log("Connected to ATEM @ " + ipAddress); var command = { "topic": "status", "payload": { "type": "status", "connectionStatus": "connected" } } messageCallback(command); //Send out the inital values var cmds = []; for(var key in commands.list) { var cmd = { "topic": "initial", "payload": commands.list[key].afterInit(commands) } if(cmd.payload != false) { cmds.push(cmd); } } messageCallback(cmds); break; } case "got-data": { node.sendStatus("green", "Got Data!"); break; } case "error": { node.sendStatus("orange", "ATEM Error"); var command = { "topic": "status", "payload": { "type": "status", "connectionStatus": "error", "errorInformation": information } } messageCallback(command); break; } case "connecting": { node.sendStatus("orange", "Connecting..."); node.log("Connecting to ATEM @ " + ipAddress); node.information.status = "connecting"; var command = { "topic": "status", "payload": { "type": "status", "connectionStatus": "connecting" } } messageCallback(command); break; } case "disconnected": { if(node.information.status !== "disconnected") { node.sendStatus("red", "Disconnected!"); node.error("Disconnected from ATEM @ " + ipAddress); node.information.status = "disconnected"; clearInterval(pingCheck); node.information.connectionTimeout = 0; commands.close(); var command = { "topic": "status", "payload": { "type": "status", "connectionStatus": "disconnected" } } messageCallback(command); //Close server and attempt reconnection connect(ipAddress, port); } break; } } } //When a message is received and processed send it out the output function messageCallback(command) { node.sendMessage(command); } //Completes a handshake with the atem and returns the session information function handshake() { var id = Math.round(Math.random() * 0x7FF); statusCallback("connecting", ""); handshakeInterval = setInterval(function() { node.sendStatus("yellow", "Attempting Handshake"); node.log("Attempting Handshake"); try{server.send(commands.packets.requestHandshake, port, ipAddress);} catch(e){node.error("Attempted to send a message but the server was closed: " + e); return;} }, 5000); //Check for connection state pingCheck = setInterval(function() { node.information.connectionTimeout++; if(node.information.connectionTimeout > 10) { //Lost connection statusCallback("disconnected", "timeout"); clearInterval(pingCheck); node.information.connectionTimeout = 0; } }, 5000); //On message server.on("message", function(message, rinfo) { processIncomingMessage(message, rinfo); clearInterval(handshakeInterval); }); } function processReceiveBuffer() { for(var k = 0; k < receiveBuffer.length; k++) { var message = receiveBuffer[k]; var flag = message[0] >> 3; var length = ((message[0] & 0x07) << 8) | message[1]; //Split a singular command into its parts var commandMessage = message.slice(12, length); var cmds = []; while(commandMessage.length > 0) { var commandLength = commandMessage.readUInt16BE(0); var thisMessage = commandMessage.slice(0, commandLength); cmds.push(thisMessage); commandMessage = commandMessage.slice(commandLength, length); } //Process the commands for(var i = 0; i < cmds.length; i++) { var command = { "topic": "command", "payload": { "type": undefined, "raw": { "flag": commands.findFlag(flag) }, "data": {} } } var length = cmds[i].readUInt16BE(0); var name = cmds[i].toString("UTF8", 4, 8); command.payload.raw.length = length; command.payload.raw.name = name; command.payload.raw.packet = cmds[i]; // //Answerback flag check for the command that this is a answerback for if(sendBuffer.length > 0) { if(name == commands.findInvertedDirectionName(sendBuffer[0].commandPacket.toString("UTF8", 0, 4)) || commands.findInvertedDirectionName(sendBuffer[0].commandPacket.toString("UTF8", 0, 4)) == "") { //Respose sendBuffer.splice(0, 1); node.sendStatus("green", "Sent!"); timeoutCount = 0; } } //Check for inital conditions and load in the information otherwise sync //Flag 1 >> 5 >> 1 (Done) if(node.information.status !== "connected") { //Check if the command exists in the supported list and pass its inital information var cmd = commands.findCommand(name); if(cmd != null) { cmd.initializeData(cmds[i].slice(8, length), flag, commands, messageCallbacks); } } else { //Check if the command exists in the supported list var cmd = commands.findCommand(name); if(cmd != null) { if(cmd.processData(cmds[i].slice(8, length), flag, command, commands)) { messageCallback(command); statusCallback("got-data", ""); } } else { command.payload.cmd = "raw"; messageCallback(command); statusCallback("got-data", ""); } } } node.information.connectionTimeout = 0; receiveBuffer.splice(k, 1); } } //Process the message sent by the ATEM function processIncomingMessage(message, rinfo) { //Reply if it's our message var length = ((message[0] & 0x07) << 8) | message[1]; if(length == rinfo.size) { var flag = message[0] >> 3; var messageSessionId = [message[2], message[3]]; var remotePacketId = [message[10], message[11]]; //Check for disconnection clearInterval(timeoutInterval); timeoutInterval = setInterval(function() { statusCallback("disconnected", "timeout"); clearInterval(timeoutInterval); }, 2000); //Inital connection if(sessionId === undefined) { if(flag == commands.flags.connect) { //Send handshake answerback try{server.send(commands.packets.handshakeAnswerback, port, ipAddress);} catch(e){node.error("Attempted to send a message but the server was closed: " + e); return;} } else if(flag == commands.flags.sync) { sessionId = messageSessionId; } else { node.error("Unknown connection state: " + flag); statusCallback("disconnected", "Unknown Connection State"); } return; } if(sessionId[0] != messageSessionId[0] || sessionId[1] != messageSessionId[1]) {} else { //Reply to each command var buffer = new Buffer.alloc(12).fill(0); buffer[0] = 0x80; buffer[1] = 0x0C; buffer[2] = sessionId[0]; buffer[3] = sessionId[1]; buffer[4] = remotePacketId[0]; buffer[5] = remotePacketId[1]; buffer[9] = 0x41; setTimeout(function(){ try{server.send(buffer, port, ipAddress);} catch(e) {node.error("Attempted to send a message but the server was closed: " + e); return;} }, 100); //Check if we're connected if(node.information.status != "connected") { if(flag == commands.flags.initializing && node.information.status == "connecting") { node.information.status = "initializing"; statusCallback(node.information.status, ""); } if(message.toString("UTF8", 16, 20) === "Time" && flag == commands.flags.sync && node.information.status == "initializing") { node.information.status = "connected"; setTimeout(function() { statusCallback(node.information.status, ""); }, 2000); } } } receiveBuffer.push(message); } } } //Add the node RED.nodes.registerType("atem-network", ATEMNetwork); }