UNPKG

i6-s7ip

Version:
1,173 lines (996 loc) 96.6 kB
// NodeS7 - A library for communication to Siemens PLCs from node.js. // The MIT License (MIT) // Copyright (c) 2013 Dana Moffit // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // EXTRA WARNING - This is BETA software and as such, be careful, especially when // writing values to programmable controllers. // // Some actions or errors involving programmable controllers can cause injury or death, // and YOU are indicating that you understand the risks, including the // possibility that the wrong address will be overwritten with the wrong value, // when using this library. Test thoroughly in a laboratory environment. var net = require("net"); var util = require("util"); var effectiveDebugLevel = 0; // intentionally global, shared between connections var silentMode = false; module.exports = NodeS7; function NodeS7(opts) { opts = opts || {}; silentMode = opts.silent || false; effectiveDebugLevel = opts.debug ? 99 : 0 var self = this; self.connectReq = new Buffer([0x03, 0x00, 0x00, 0x16, 0x11, 0xe0, 0x00, 0x00, 0x00, 0x02, 0x00, 0xc0, 0x01, 0x0a, 0xc1, 0x02, 0x01, 0x00, 0xc2, 0x02, 0x01, 0x02]); self.negotiatePDU = new Buffer([0x03, 0x00, 0x00, 0x19, 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x08, 0x00, 0x08, 0x03, 0xc0]); self.readReqHeader = new Buffer([0x03, 0x00, 0x00, 0x1f, 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x04, 0x01]); self.readReq = new Buffer(1500); self.writeReqHeader = new Buffer([0x03, 0x00, 0x00, 0x1f, 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x05, 0x01]); self.writeReq = new Buffer(1500); self.resetPending = false; self.resetTimeout = undefined; self.isoclient = undefined; self.isoConnectionState = 0; self.requestMaxPDU = 960; self.maxPDU = 960; self.requestMaxParallel = 8; self.maxParallel = 8; self.parallelJobsNow = 0; self.maxGap = 5; self.doNotOptimize = false; self.connectCallback = undefined; self.readDoneCallback = undefined; self.writeDoneCallback = undefined; self.connectTimeout = undefined; self.PDUTimeout = undefined; self.globalTimeout = 1500; // In many use cases we will want to increase this self.rack = 0; self.slot = 2; self.localTSAP = null; self.remoteTSAP = null; self.readPacketArray = []; self.writePacketArray = []; self.polledReadBlockList = []; self.instantWriteBlockList = []; self.globalReadBlockList = []; self.globalWriteBlockList = []; self.masterSequenceNumber = 1; self.translationCB = doNothing; self.connectionParams = undefined; self.connectionID = 'UNDEF'; self.addRemoveArray = []; self.readPacketValid = false; self.writeInQueue = false; self.connectCBIssued = false; self.dropConnectionCallback = null; self.dropConnectionTimer = null; } NodeS7.prototype.setTranslationCB = function(cb) { var self = this; if (typeof cb === "function") { outputLog('Translation OK'); self.translationCB = cb; } } NodeS7.prototype.initiateConnection = function(cParam, callback) { var self = this; if (cParam === undefined) { cParam = { port: 102, host: '192.168.8.106' }; } outputLog('Initiate Called - Connecting to PLC with address and parameters:'); outputLog(cParam); if (typeof (cParam.rack) !== 'undefined') { self.rack = cParam.rack; } if (typeof (cParam.slot) !== 'undefined') { self.slot = cParam.slot; } if (typeof (cParam.localTSAP) !== 'undefined') { self.localTSAP = cParam.localTSAP; } if (typeof (cParam.remoteTSAP) !== 'undefined') { self.remoteTSAP = cParam.remoteTSAP; } if (typeof (cParam.connection_name) === 'undefined') { self.connectionID = cParam.host + " S" + self.slot; } else { self.connectionID = cParam.connection_name; } self.connectionParams = cParam; self.connectCallback = callback; self.connectCBIssued = false; self.connectNow(self.connectionParams, false); } NodeS7.prototype.dropConnection = function(callback) { var self = this; if (typeof (self.isoclient) !== 'undefined') { // store the callback and request and end to the connection self.dropConnectionCallback = callback; self.isoclient.end(); // now wait for 'on close' event to trigger connection cleanup // but also start a timer to destroy the connection in case we do not receive the close self.dropConnectionTimer = setTimeout(function() { if (self.dropConnectionCallback) { // clean up the connection now the socket has closed self.connectionCleanup(); // initate the callback self.dropConnectionCallback(); // prevent any possiblity of the callback being called twice self.dropConnectionCallback = null; } }, 2500); } else { // if client not active, then callback immediately callback(); } } NodeS7.prototype.connectNow = function(cParam) { var self = this; // Don't re-trigger. if (self.isoConnectionState >= 1) { return; } self.connectionCleanup(); self.isoclient = net.connect(cParam, function() { self.onTCPConnect.apply(self, arguments); }); self.isoConnectionState = 1; // 1 = trying to connect self.isoclient.on('error', function() { self.connectError.apply(self, arguments); }); outputLog('<initiating a new connection ' + Date() + '>', 1, self.connectionID); outputLog('Attempting to connect to host...', 0, self.connectionID); } NodeS7.prototype.connectError = function(e) { var self = this; // Note that a TCP connection timeout error will appear here. An ISO connection timeout error is a packet timeout. outputLog('We Caught a connect error ' + e.code, 0, self.connectionID); if ((!self.connectCBIssued) && (typeof (self.connectCallback) === "function")) { self.connectCBIssued = true; self.connectCallback(e); } self.isoConnectionState = 0; } NodeS7.prototype.readWriteError = function(e) { var self = this; outputLog('We Caught a read/write error ' + e.code + ' - will DISCONNECT and attempt to reconnect.'); self.isoConnectionState = 0; self.connectionReset(); } NodeS7.prototype.packetTimeout = function(packetType, packetSeqNum) { var self = this; outputLog('PacketTimeout called with type ' + packetType + ' and seq ' + packetSeqNum, 1, self.connectionID); if (packetType === "connect") { outputLog("TIMED OUT connecting to the PLC - Disconnecting", 0, self.connectionID); outputLog("Wait for 2 seconds then try again.", 0, self.connectionID); self.connectionReset(); outputLog("Scheduling a reconnect from packetTimeout, connect type", 0, self.connectionID); setTimeout(function() { outputLog("The scheduled reconnect from packetTimeout, connect type, is happening now", 0, self.connectionID); self.connectNow.apply(self, arguments); }, 2000, self.connectionParams); return undefined; } if (packetType === "PDU") { outputLog("TIMED OUT waiting for PDU reply packet from PLC - Disconnecting"); outputLog("Wait for 2 seconds then try again.", 0, self.connectionID); self.connectionReset(); outputLog("Scheduling a reconnect from packetTimeout, connect type", 0, self.connectionID); setTimeout(function() { outputLog("The scheduled reconnect from packetTimeout, PDU type, is happening now", 0, self.connectionID); self.connectNow.apply(self, arguments); }, 2000, self.connectionParams); return undefined; } if (packetType === "read") { outputLog("READ TIMEOUT on sequence number " + packetSeqNum, 0, self.connectionID); self.readResponse(undefined, self.findReadIndexOfSeqNum(packetSeqNum)); return undefined; } if (packetType === "write") { outputLog("WRITE TIMEOUT on sequence number " + packetSeqNum, 0, self.connectionID); self.writeResponse(undefined, self.findWriteIndexOfSeqNum(packetSeqNum)); return undefined; } outputLog("Unknown timeout error. Nothing was done - this shouldn't happen."); } NodeS7.prototype.onTCPConnect = function() { var self = this, connBuf; outputLog('TCP Connection Established to ' + self.isoclient.remoteAddress + ' on port ' + self.isoclient.remotePort, 0, self.connectionID); outputLog('Will attempt ISO-on-TCP connection', 0, self.connectionID); // Track the connection state self.isoConnectionState = 2; // 2 = TCP connected, wait for ISO connection confirmation // Send an ISO-on-TCP connection request. self.connectTimeout = setTimeout(function() { self.packetTimeout.apply(self, arguments); }, self.globalTimeout, "connect"); connBuf = self.connectReq.slice(); if(self.localTSAP !== null && self.remoteTSAP !== null) { outputLog('Using localTSAP [0x' + self.localTSAP.toString(16) + '] and remoteTSAP [0x' + self.remoteTSAP.toString(16) + ']', 0, self.connectionID); connBuf.writeUInt16BE(self.localTSAP, 16) connBuf.writeUInt16BE(self.remoteTSAP, 20) } else { outputLog('Using rack [' + self.rack + '] and slot [' + self.slot + ']', 0, self.connectionID); connBuf[21] = self.rack * 32 + self.slot; } self.isoclient.write(connBuf); // Listen for a reply. self.isoclient.on('data', function() { self.onISOConnectReply.apply(self, arguments); }); // Hook up the event that fires on disconnect self.isoclient.on('end', function() { self.onClientDisconnect.apply(self, arguments); }); // listen for close (caused by us sending an end) self.isoclient.on('close', function() { self.onClientClose.apply(self, arguments); }); } NodeS7.prototype.onISOConnectReply = function(data) { var self = this; self.isoclient.removeAllListeners('data'); //self.onISOConnectReply); self.isoclient.removeAllListeners('error'); clearTimeout(self.connectTimeout); // Track the connection state self.isoConnectionState = 3; // 3 = ISO-ON-TCP connected, Wait for PDU response. // Expected length is from packet sniffing - some applications may be different, especially using routing - not considered yet. if (data.readInt16BE(2) !== data.length || data.length < 22 || data[5] !== 0xd0 || data[4] !== (data.length - 5)) { outputLog('INVALID PACKET or CONNECTION REFUSED - DISCONNECTING'); outputLog(data); outputLog('TPKT Length From Header is ' + data.readInt16BE(2) + ' and RCV buffer length is ' + data.length + ' and COTP length is ' + data.readUInt8(4) + ' and data[5] is ' + data[5]); self.connectionReset(); return null; } outputLog('ISO-on-TCP Connection Confirm Packet Received', 0, self.connectionID); self.negotiatePDU.writeInt16BE(self.requestMaxParallel, 19); self.negotiatePDU.writeInt16BE(self.requestMaxParallel, 21); self.negotiatePDU.writeInt16BE(self.requestMaxPDU, 23); self.PDUTimeout = setTimeout(function() { self.packetTimeout.apply(self, arguments); }, self.globalTimeout, "PDU"); self.isoclient.write(self.negotiatePDU.slice(0, 25)); self.isoclient.on('data', function() { self.onPDUReply.apply(self, arguments); }); self.isoclient.on('error', function() { self.readWriteError.apply(self, arguments); }); } NodeS7.prototype.onPDUReply = function(theData) { var self = this; self.isoclient.removeAllListeners('data'); self.isoclient.removeAllListeners('error'); clearTimeout(self.PDUTimeout); var data=checkRFCData(theData); if(data==="fastACK"){ //Read again and wait for the requested data outputLog('Fast Acknowledge received.', 0, self.connectionID); self.isoclient.removeAllListeners('error'); self.isoclient.removeAllListeners('data'); self.isoclient.on('data', function() { self.onPDUReply.apply(self, arguments); }); self.isoclient.on('error', function() { self.readWriteError.apply(self, arguments); }); }else if((data[4] + 1 + 12 + data.readInt16BE(13) === data.readInt16BE(2) - 4)){//valid the length of FA+S7 package : ISO_Length+ISO_LengthItself+S7Com_Header+S7Com_Header_ParameterLength===TPKT_Length-4 //Everything OK...go on // Track the connection state self.isoConnectionState = 4; // 4 = Received PDU response, good to go var partnerMaxParallel1 = data.readInt16BE(21); var partnerMaxParallel2 = data.readInt16BE(23); var partnerPDU = data.readInt16BE(25); self.maxParallel = self.requestMaxParallel; if (partnerMaxParallel1 < self.requestMaxParallel) { self.maxParallel = partnerMaxParallel1; } if (partnerMaxParallel2 < self.requestMaxParallel) { self.maxParallel = partnerMaxParallel2; } if (partnerPDU < self.requestMaxPDU) { self.maxPDU = partnerPDU; } else { self.maxPDU = self.requestMaxPDU; } outputLog('Received PDU Response - Proceeding with PDU ' + self.maxPDU + ' and ' + self.maxParallel + ' max parallel connections.', 0, self.connectionID); self.isoclient.on('data', function() { self.onResponse.apply(self, arguments); }); // We need to make sure we don't add this event every time if we call it on data. self.isoclient.on('error', function() { self.readWriteError.apply(self, arguments); }); // Might want to remove the self.connecterror listener //self.isoclient.removeAllListeners('error'); if ((!self.connectCBIssued) && (typeof (self.connectCallback) === "function")) { self.connectCBIssued = true; self.connectCallback(); } }else{ outputLog('INVALID Telegram ', 0, self.connectionID); outputLog('Byte 0 From Header is ' + theData[0] + ' it has to be 0x03, Byte 5 From Header is ' + theData[5] + ' and it has to be 0x0F ', 0, self.connectionID); outputLog('INVALID PDU RESPONSE or CONNECTION REFUSED - DISCONNECTING', 0, self.connectionID); outputLog('TPKT Length From Header is ' + theData.readInt16BE(2) + ' and RCV buffer length is ' + theData.length + ' and COTP length is ' + theData.readUInt8(4) + ' and data[6] is ' + theData[6], 0, self.connectionID); outputLog(theData); self.isoclient.end(); setTimeout(function() { self.connectNow.apply(self, arguments); }, 2000, self.connectionParams); return null; } } NodeS7.prototype.writeItems = function(arg, value, cb) { var self = this, i; outputLog("Preparing to WRITE " + arg + " to value " + value, 0, self.connectionID); if (self.isWriting()) { outputLog("You must wait until all previous writes have finished before scheduling another. ", 0, self.connectionID); return; } if (typeof cb === "function") { self.writeDoneCallback = cb; } else { self.writeDoneCallback = doNothing; } self.instantWriteBlockList = []; // Initialize the array. if (typeof arg === "string") { self.instantWriteBlockList.push(stringToS7Addr(self.translationCB(arg), arg)); if (typeof (self.instantWriteBlockList[self.instantWriteBlockList.length - 1]) !== "undefined") { self.instantWriteBlockList[self.instantWriteBlockList.length - 1].writeValue = value; } } else if (Array.isArray(arg) && Array.isArray(value) && (arg.length == value.length)) { for (i = 0; i < arg.length; i++) { if (typeof arg[i] === "string") { self.instantWriteBlockList.push(stringToS7Addr(self.translationCB(arg[i]), arg[i])); if (typeof (self.instantWriteBlockList[self.instantWriteBlockList.length - 1]) !== "undefined") { self.instantWriteBlockList[self.instantWriteBlockList.length - 1].writeValue = value[i]; } } } } // Validity check. for (i = self.instantWriteBlockList.length - 1; i >= 0; i--) { if (self.instantWriteBlockList[i] === undefined) { self.instantWriteBlockList.splice(i, 1); outputLog("Dropping an undefined write item."); } } self.prepareWritePacket(); if (!self.isReading()) { self.sendWritePacket(); } else { self.writeInQueue = true; } } NodeS7.prototype.findItem = function(useraddr) { var self = this, i; var commstate = { value: self.isoConnectionState !== 4, quality: 'OK' }; if (useraddr === '_COMMERR') { return commstate; } for (i = 0; i < self.polledReadBlockList.length; i++) { if (self.polledReadBlockList[i].useraddr === useraddr) { return self.polledReadBlockList[i]; } } return undefined; } NodeS7.prototype.addItems = function(arg) { var self = this; self.addRemoveArray.push({ arg: arg, action: 'add' }); } NodeS7.prototype.addItemsNow = function(arg) { var self = this, i; outputLog("Adding " + arg, 0, self.connectionID); if (typeof (arg) === "string" && arg !== "_COMMERR") { self.polledReadBlockList.push(stringToS7Addr(self.translationCB(arg), arg)); } else if (Array.isArray(arg)) { for (i = 0; i < arg.length; i++) { if (typeof (arg[i]) === "string" && arg[i] !== "_COMMERR") { self.polledReadBlockList.push(stringToS7Addr(self.translationCB(arg[i]), arg[i])); } } } // Validity check. for (i = self.polledReadBlockList.length - 1; i >= 0; i--) { if (self.polledReadBlockList[i] === undefined) { self.polledReadBlockList.splice(i, 1); outputLog("Dropping an undefined request item.", 0, self.connectionID); } } // self.prepareReadPacket(); self.readPacketValid = false; } NodeS7.prototype.removeItems = function(arg) { var self = this; self.addRemoveArray.push({ arg: arg, action: 'remove' }); } NodeS7.prototype.removeItemsNow = function(arg) { var self = this, i; if (typeof arg === "undefined") { self.polledReadBlockList = []; } else if (typeof arg === "string") { for (i = 0; i < self.polledReadBlockList.length; i++) { outputLog('TCBA ' + self.translationCB(arg)); if (self.polledReadBlockList[i].addr === self.translationCB(arg)) { outputLog('Splicing'); self.polledReadBlockList.splice(i, 1); } } } else if (Array.isArray(arg)) { for (i = 0; i < self.polledReadBlockList.length; i++) { for (var j = 0; j < arg.length; j++) { if (self.polledReadBlockList[i].addr === self.translationCB(arg[j])) { self.polledReadBlockList.splice(i, 1); } } } } self.readPacketValid = false; // self.prepareReadPacket(); } NodeS7.prototype.readAllItems = function(arg) { var self = this; outputLog("Reading All Items (readAllItems was called)", 1, self.connectionID); if (typeof arg === "function") { self.readDoneCallback = arg; } else { self.readDoneCallback = doNothing; } if (self.isoConnectionState !== 4) { outputLog("Unable to read when not connected. Return bad values.", 0, self.connectionID); } // For better behaviour when auto-reconnecting - don't return now // Check if ALL are done... You might think we could look at parallel jobs, and for the most part we can, but if one just finished and we end up here before starting another, it's bad. if (self.isWaiting()) { outputLog("Waiting to read for all R/W operations to complete. Will re-trigger readAllItems in 100ms.", 0, self.connectionID); setTimeout(function() { self.readAllItems.apply(self, arguments); }, 100, arg); return; } // Now we check the array of adding and removing things. Only now is it really safe to do this. self.addRemoveArray.forEach(function(element) { outputLog('Adding or Removing ' + util.format(element), 1, self.connectionID); if (element.action === 'remove') { self.removeItemsNow(element.arg); } if (element.action === 'add') { self.addItemsNow(element.arg); } }); self.addRemoveArray = []; // Clear for next time. if (!self.readPacketValid) { self.prepareReadPacket(); } // ideally... incrementSequenceNumbers(); outputLog("Calling SRP from RAI", 1, self.connectionID); self.sendReadPacket(); // Note this sends the first few read packets depending on parallel connection restrictions. } NodeS7.prototype.isWaiting = function() { var self = this; return (self.isReading() || self.isWriting()); } NodeS7.prototype.isReading = function() { var self = this, i; // Walk through the array and if any packets are marked as sent, it means we haven't received our final confirmation. for (i = 0; i < self.readPacketArray.length; i++) { if (self.readPacketArray[i].sent === true) { return true } } return false; } NodeS7.prototype.isWriting = function() { var self = this, i; // Walk through the array and if any packets are marked as sent, it means we haven't received our final confirmation. for (i = 0; i < self.writePacketArray.length; i++) { if (self.writePacketArray[i].sent === true) { return true } } return false; } NodeS7.prototype.clearReadPacketTimeouts = function() { var self = this, i; outputLog('Clearing read PacketTimeouts', 1, self.connectionID); // Before we initialize the self.readPacketArray, we need to loop through all of them and clear timeouts. for (i = 0; i < self.readPacketArray.length; i++) { clearTimeout(self.readPacketArray[i].timeout); self.readPacketArray[i].sent = false; self.readPacketArray[i].rcvd = false; } } NodeS7.prototype.clearWritePacketTimeouts = function() { var self = this, i; outputLog('Clearing write PacketTimeouts', 1, self.connectionID); // Before we initialize the self.readPacketArray, we need to loop through all of them and clear timeouts. for (i = 0; i < self.writePacketArray.length; i++) { clearTimeout(self.writePacketArray[i].timeout); self.writePacketArray[i].sent = false; self.writePacketArray[i].rcvd = false; } } NodeS7.prototype.prepareWritePacket = function() { var self = this, i; var itemList = self.instantWriteBlockList; var requestList = []; // The request list consists of the block list, split into chunks readable by PDU. var requestNumber = 0; // Sort the items using the sort function, by type and offset. itemList.sort(itemListSorter); // Just exit if there are no items. if (itemList.length === 0) { return undefined; } // Reinitialize the WriteBlockList self.globalWriteBlockList = []; // At this time we do not do write optimizations. // The reason for this is it is would cause numerous issues depending how the code was written in the PLC. // If we write M0.1 and M0.2 then to optimize we would have to write MB0, which also writes 0.0, 0.3, 0.4... // // I suppose when working with integers, if we write MW0 and MW2, we could write these as one block. // But if you really, really want the program to do that, write an array yourself. self.globalWriteBlockList[0] = itemList[0]; self.globalWriteBlockList[0].itemReference = []; self.globalWriteBlockList[0].itemReference.push(itemList[0]); var thisBlock = 0; itemList[0].block = thisBlock; var maxByteRequest = 4 * Math.floor((self.maxPDU - 18 - 12) / 4); // Absolutely must not break a real array into two requests. Maybe we can extend by two bytes when not DINT/REAL/INT. // outputLog("Max Write Length is " + maxByteRequest); // Just push the items into blocks and figure out the write buffers for (i = 0; i < itemList.length; i++) { self.globalWriteBlockList[i] = itemList[i]; // Remember - by reference. self.globalWriteBlockList[i].isOptimized = false; self.globalWriteBlockList[i].itemReference = []; self.globalWriteBlockList[i].itemReference.push(itemList[i]); bufferizeS7Item(itemList[i]); } var thisRequest = 0; // Split the blocks into requests, if they're too large. for (i = 0; i < self.globalWriteBlockList.length; i++) { var startByte = self.globalWriteBlockList[i].offset; var remainingLength = self.globalWriteBlockList[i].byteLength; var lengthOffset = 0; // Always create a request for a self.globalReadBlockList. requestList[thisRequest] = self.globalWriteBlockList[i].clone(); // How many parts? self.globalWriteBlockList[i].parts = Math.ceil(self.globalWriteBlockList[i].byteLength / maxByteRequest); // outputLog("self.globalWriteBlockList " + i + " parts is " + self.globalWriteBlockList[i].parts + " offset is " + self.globalWriteBlockList[i].offset + " MBR is " + maxByteRequest); self.globalWriteBlockList[i].requestReference = []; // If we're optimized... for (var j = 0; j < self.globalWriteBlockList[i].parts; j++) { requestList[thisRequest] = self.globalWriteBlockList[i].clone(); self.globalWriteBlockList[i].requestReference.push(requestList[thisRequest]); requestList[thisRequest].offset = startByte; requestList[thisRequest].byteLength = Math.min(maxByteRequest, remainingLength); requestList[thisRequest].byteLengthWithFill = requestList[thisRequest].byteLength; if (requestList[thisRequest].byteLengthWithFill % 2) { requestList[thisRequest].byteLengthWithFill += 1; } // max requestList[thisRequest].writeBuffer = self.globalWriteBlockList[i].writeBuffer.slice(lengthOffset, lengthOffset + requestList[thisRequest].byteLengthWithFill); requestList[thisRequest].writeQualityBuffer = self.globalWriteBlockList[i].writeQualityBuffer.slice(lengthOffset, lengthOffset + requestList[thisRequest].byteLengthWithFill); lengthOffset += self.globalWriteBlockList[i].requestReference[j].byteLength; if (self.globalWriteBlockList[i].parts > 1) { requestList[thisRequest].datatype = 'BYTE'; requestList[thisRequest].dtypelen = 1; requestList[thisRequest].arrayLength = requestList[thisRequest].byteLength;//self.globalReadBlockList[thisBlock].byteLength; (This line shouldn't be needed anymore - shouldn't matter) } remainingLength -= maxByteRequest; thisRequest++; startByte += maxByteRequest; } } self.clearWritePacketTimeouts(); self.writePacketArray = []; // outputLog("GWBL is " + self.globalWriteBlockList.length); // Before we initialize the self.writePacketArray, we need to loop through all of them and clear timeouts. // The packetizer... while (requestNumber < requestList.length) { // Set up the read packet // Yes this is the same master sequence number shared with the read queue self.masterSequenceNumber += 1; if (self.masterSequenceNumber > 32767) { self.masterSequenceNumber = 1; } var numItems = 0; // Maybe this shouldn't really be here? self.writeReqHeader.copy(self.writeReq, 0); // Packet's length var packetWriteLength = 10 + 4; // 10 byte header and 4 byte param header self.writePacketArray.push(new S7Packet()); var thisPacketNumber = self.writePacketArray.length - 1; self.writePacketArray[thisPacketNumber].seqNum = self.masterSequenceNumber; // outputLog("Write Sequence Number is " + self.writePacketArray[thisPacketNumber].seqNum); self.writePacketArray[thisPacketNumber].itemList = []; // Initialize as array. for (i = requestNumber; i < requestList.length; i++) { //outputLog("Number is " + (requestList[i].byteLengthWithFill + 4 + packetReplyLength)); if (requestList[i].byteLengthWithFill + 12 + 4 + packetWriteLength > self.maxPDU) { // 12 byte header for each item and 4 bytes for the data header if (numItems === 0) { outputLog("breaking when we shouldn't, byte length with fill is " + requestList[i].byteLengthWithFill + " max byte request " + maxByteRequest, 0, self.connectionID); throw new Error("Somehow write request didn't split properly - exiting. Report this as a bug."); } break; // We can't fit this packet in here. } requestNumber++; numItems++; packetWriteLength += (requestList[i].byteLengthWithFill + 12 + 4); // Don't forget each request has a 12 byte header as well. //outputLog('I is ' + i + ' Addr Type is ' + requestList[i].addrtype + ' and type is ' + requestList[i].datatype + ' and DBNO is ' + requestList[i].dbNumber + ' and offset is ' + requestList[i].offset + ' bit ' + requestList[i].bitOffset + ' len ' + requestList[i].arrayLength); //S7AddrToBuffer(requestList[i]).copy(self.writeReq, 19 + numItems * 12); // i or numItems? used to be i. //itemBuffer = bufferizeS7Packet(requestList[i]); //itemBuffer.copy(dataBuffer, dataBufferPointer); //dataBufferPointer += itemBuffer.length; self.writePacketArray[thisPacketNumber].itemList.push(requestList[i]); } // dataBuffer.copy(self.writeReq, 19 + (numItems + 1) * 12, 0, dataBufferPointer - 1); } } NodeS7.prototype.prepareReadPacket = function() { var self = this, i; // Note that for a PDU size of 240, the MOST bytes we can request depends on the number of items. // To figure this out, allow for a 247 byte packet. 7 TPKT+COTP header doesn't count for PDU, so 240 bytes of "S7 data". // In the response you ALWAYS have a 12 byte S7 header. // Then you have a 2 byte parameter header. // Then you have a 4 byte "item header" PER ITEM. // So you have overhead of 18 bytes for one item, 22 bytes for two items, 26 bytes for 3 and so on. So for example you can request 240 - 22 = 218 bytes for two items. // We can calculate a max byte length for single request as 4*Math.floor((self.maxPDU - 18)/4) - to ensure we don't cross boundaries. var itemList = self.polledReadBlockList; // The items are the actual items requested by the user var requestList = []; // The request list consists of the block list, split into chunks readable by PDU. // Validity check. for (i = itemList.length - 1; i >= 0; i--) { if (itemList[i] === undefined) { itemList.splice(i, 1); outputLog("Dropping an undefined request item.", 0, self.connectionID); } } // Sort the items using the sort function, by type and offset. itemList.sort(itemListSorter); // Just exit if there are no items. if (itemList.length === 0) { return undefined; } self.globalReadBlockList = []; // ...because you have to start your optimization somewhere. self.globalReadBlockList[0] = itemList[0]; self.globalReadBlockList[0].itemReference = []; self.globalReadBlockList[0].itemReference.push(itemList[0]); var thisBlock = 0; itemList[0].block = thisBlock; var maxByteRequest = 4 * Math.floor((self.maxPDU - 18) / 4); // Absolutely must not break a real array into two requests. Maybe we can extend by two bytes when not DINT/REAL/INT. // Optimize the items into blocks for (i = 1; i < itemList.length; i++) { // Skip T, C, P types if ((itemList[i].areaS7Code !== self.globalReadBlockList[thisBlock].areaS7Code) || // Can't optimize between areas (itemList[i].dbNumber !== self.globalReadBlockList[thisBlock].dbNumber) || // Can't optimize across DBs (!self.isOptimizableArea(itemList[i].areaS7Code)) || // Can't optimize T,C (I don't think) and definitely not P. ((itemList[i].offset - self.globalReadBlockList[thisBlock].offset + itemList[i].byteLength) > maxByteRequest) || // If this request puts us over our max byte length, create a new block for consistency reasons. (itemList[i].offset - (self.globalReadBlockList[thisBlock].offset + self.globalReadBlockList[thisBlock].byteLength) > self.maxGap)) { // If our gap is large, create a new block. outputLog("Skipping optimization of item " + itemList[i].addr, 0, self.connectionID); // At this point we give up and create a new block. thisBlock = thisBlock + 1; self.globalReadBlockList[thisBlock] = itemList[i]; // By reference. // itemList[i].block = thisBlock; // Don't need to do this. self.globalReadBlockList[thisBlock].isOptimized = false; self.globalReadBlockList[thisBlock].itemReference = []; self.globalReadBlockList[thisBlock].itemReference.push(itemList[i]); } else { outputLog("Attempting optimization of item " + itemList[i].addr + " with " + self.globalReadBlockList[thisBlock].addr, 0, self.connectionID); // This next line checks the maximum. // Think of this situation - we have a large request of 40 bytes starting at byte 10. // Then someone else wants one byte starting at byte 12. The block length doesn't change. // // But if we had 40 bytes starting at byte 10 (which gives us byte 10-49) and we want byte 50, our byte length is 50-10 + 1 = 41. self.globalReadBlockList[thisBlock].byteLength = Math.max(self.globalReadBlockList[thisBlock].byteLength, itemList[i].offset - self.globalReadBlockList[thisBlock].offset + itemList[i].byteLength); // Point the buffers (byte and quality) to a sliced version of the optimized block. This is by reference (same area of memory) itemList[i].byteBuffer = self.globalReadBlockList[thisBlock].byteBuffer.slice(itemList[i].offset - self.globalReadBlockList[thisBlock].offset, itemList[i].offset - self.globalReadBlockList[thisBlock].offset + itemList[i].byteLength); itemList[i].qualityBuffer = self.globalReadBlockList[thisBlock].qualityBuffer.slice(itemList[i].offset - self.globalReadBlockList[thisBlock].offset, itemList[i].offset - self.globalReadBlockList[thisBlock].offset + itemList[i].byteLength); // For now, change the request type here, and fill in some other things. // I am not sure we want to do these next two steps. // It seems like things get screwed up when we do this. // Since self.globalReadBlockList[thisBlock] exists already at this point, and our buffer is already set, let's not do this now. // self.globalReadBlockList[thisBlock].datatype = 'BYTE'; // self.globalReadBlockList[thisBlock].dtypelen = 1; self.globalReadBlockList[thisBlock].isOptimized = true; self.globalReadBlockList[thisBlock].itemReference.push(itemList[i]); } } var thisRequest = 0; // outputLog("Preparing the read packet..."); // Split the blocks into requests, if they're too large. for (i = 0; i < self.globalReadBlockList.length; i++) { // Always create a request for a self.globalReadBlockList. requestList[thisRequest] = self.globalReadBlockList[i].clone(); // How many parts? self.globalReadBlockList[i].parts = Math.ceil(self.globalReadBlockList[i].byteLength / maxByteRequest); outputLog("self.globalReadBlockList " + i + " parts is " + self.globalReadBlockList[i].parts + " offset is " + self.globalReadBlockList[i].offset + " MBR is " + maxByteRequest, 1, self.connectionID); var startByte = self.globalReadBlockList[i].offset; var remainingLength = self.globalReadBlockList[i].byteLength; self.globalReadBlockList[i].requestReference = []; // If we're optimized... for (var j = 0; j < self.globalReadBlockList[i].parts; j++) { requestList[thisRequest] = self.globalReadBlockList[i].clone(); self.globalReadBlockList[i].requestReference.push(requestList[thisRequest]); //outputLog(self.globalReadBlockList[i]); //outputLog(self.globalReadBlockList.slice(i,i+1)); requestList[thisRequest].offset = startByte; requestList[thisRequest].byteLength = Math.min(maxByteRequest, remainingLength); requestList[thisRequest].byteLengthWithFill = requestList[thisRequest].byteLength; if (requestList[thisRequest].byteLengthWithFill % 2) { requestList[thisRequest].byteLengthWithFill += 1; } // Just for now... if (self.globalReadBlockList[i].parts > 1) { requestList[thisRequest].datatype = 'BYTE'; requestList[thisRequest].dtypelen = 1; requestList[thisRequest].arrayLength = requestList[thisRequest].byteLength;//self.globalReadBlockList[thisBlock].byteLength; } remainingLength -= maxByteRequest; thisRequest++; startByte += maxByteRequest; } } //requestList[5].offset = 243; // requestList = self.globalReadBlockList; // The packetizer... var requestNumber = 0; self.clearReadPacketTimeouts(); self.readPacketArray = []; while (requestNumber < requestList.length) { // Set up the read packet self.masterSequenceNumber += 1; if (self.masterSequenceNumber > 32767) { self.masterSequenceNumber = 1; } var numItems = 0; self.readReqHeader.copy(self.readReq, 0); // Packet's expected reply length var packetReplyLength = 12 + 2; // var packetRequestLength = 12; //s7 header and parameter header self.readPacketArray.push(new S7Packet()); var thisPacketNumber = self.readPacketArray.length - 1; self.readPacketArray[thisPacketNumber].seqNum = self.masterSequenceNumber; outputLog("Sequence Number is " + self.readPacketArray[thisPacketNumber].seqNum, 1, self.connectionID); self.readPacketArray[thisPacketNumber].itemList = []; // Initialize as array. for (i = requestNumber; i < requestList.length; i++) { //outputLog("Number is " + (requestList[i].byteLengthWithFill + 4 + packetReplyLength)); if (requestList[i].byteLengthWithFill + 4 + packetReplyLength > self.maxPDU || packetRequestLength + 12 > self.maxPDU) { outputLog("Splitting request: " + numItems + " items, requestLength would be " + (packetRequestLength + 12) + ", replyLength would be " + (requestList[i].byteLengthWithFill + 4 + packetReplyLength) + ", PDU is " + self.maxPDU, 1, self.connectionID); if (numItems === 0) { outputLog("breaking when we shouldn't, rlibl " + requestList[i].byteLengthWithFill + " MBR " + maxByteRequest, 0, self.connectionID); throw new Error("Somehow write request didn't split properly - exiting. Report this as a bug."); } break; // We can't fit this packet in here. } requestNumber++; numItems++; packetReplyLength += (requestList[i].byteLengthWithFill + 4); packetRequestLength += 12; //outputLog('I is ' + i + ' Addr Type is ' + requestList[i].addrtype + ' and type is ' + requestList[i].datatype + ' and DBNO is ' + requestList[i].dbNumber + ' and offset is ' + requestList[i].offset + ' bit ' + requestList[i].bitOffset + ' len ' + requestList[i].arrayLength); // skip this for now S7AddrToBuffer(requestList[i]).copy(self.readReq, 19 + numItems * 12); // i or numItems? self.readPacketArray[thisPacketNumber].itemList.push(requestList[i]); } } self.readPacketValid = true; } NodeS7.prototype.sendReadPacket = function() { var self = this, i, j, flagReconnect = false; outputLog("SendReadPacket called", 1, self.connectionID); for (i = 0; i < self.readPacketArray.length; i++) { if (self.readPacketArray[i].sent) { continue; } if (self.parallelJobsNow >= self.maxParallel) { continue; } // From here down is SENDING the packet self.readPacketArray[i].reqTime = process.hrtime(); self.readReq.writeUInt8(self.readPacketArray[i].itemList.length, 18); self.readReq.writeUInt16BE(19 + self.readPacketArray[i].itemList.length * 12, 2); // buffer length self.readReq.writeUInt16BE(self.readPacketArray[i].seqNum, 11); self.readReq.writeUInt16BE(self.readPacketArray[i].itemList.length * 12 + 2, 13); // Parameter length - 14 for one read, 28 for 2. for (j = 0; j < self.readPacketArray[i].itemList.length; j++) { S7AddrToBuffer(self.readPacketArray[i].itemList[j], false).copy(self.readReq, 19 + j * 12); } if (self.isoConnectionState == 4) { self.readPacketArray[i].timeout = setTimeout(function() { self.packetTimeout.apply(self, arguments); }, self.globalTimeout, "read", self.readPacketArray[i].seqNum); self.isoclient.write(self.readReq.slice(0, 19 + self.readPacketArray[i].itemList.length * 12)); // was 31 self.readPacketArray[i].sent = true; self.readPacketArray[i].rcvd = false; self.readPacketArray[i].timeoutError = false; self.parallelJobsNow += 1; } else { // outputLog('Somehow got into read block without proper self.isoConnectionState of 3. Disconnect.'); // self.isoclient.end(); // setTimeout(function(){ // self.connectNow.apply(self, arguments); // }, 2000, self.connectionParams); self.readPacketArray[i].sent = true; self.readPacketArray[i].rcvd = false; self.readPacketArray[i].timeoutError = true; if (!flagReconnect) { // Prevent duplicates outputLog('Not Sending Read Packet because we are not connected - ISO CS is ' + self.isoConnectionState, 0, self.connectionID); } // This is essentially an instantTimeout. if (self.isoConnectionState === 0) { flagReconnect = true; } outputLog('Requesting PacketTimeout Due to ISO CS NOT 4 - READ SN ' + self.readPacketArray[i].seqNum, 1, self.connectionID); self.readPacketArray[i].timeout = setTimeout(function() { self.packetTimeout.apply(self, arguments); }, 0, "read", self.readPacketArray[i].seqNum); } outputLog('Sending Read Packet', 1, self.connectionID); } if (flagReconnect) { // console.log("Asking for callback next tick and my ID is " + self.connectionID); setTimeout(function() { // console.log("Next tick is here and my ID is " + self.connectionID); outputLog("The scheduled reconnect from sendReadPacket is happening now", 1, self.connectionID); self.connectNow(self.connectionParams); // We used to do this NOW - not NextTick() as we need to mark isoConnectionState as 1 right now. Otherwise we queue up LOTS of connects and crash. }, 0); } } NodeS7.prototype.sendWritePacket = function() { var self = this, i, dataBuffer, itemBuffer, dataBufferPointer, flagReconnect; dataBuffer = new Buffer(8192); self.writeInQueue = false; for (i = 0; i < self.writePacketArray.length; i++) { if (self.writePacketArray[i].sent) { continue; } if (self.parallelJobsNow >= self.maxParallel) { continue; } // From here down is SENDING the packet self.writePacketArray[i].reqTime = process.hrtime(); self.writeReq.writeUInt8(self.writePacketArray[i].itemList.length, 18); self.writeReq.writeUInt16BE(self.writePacketArray[i].seqNum, 11); dataBufferPointer = 0; for (var j = 0; j < self.writePacketArray[i].itemList.length; j++) { S7AddrToBuffer(self.writePacketArray[i].itemList[j], true).copy(self.writeReq, 19 + j * 12); itemBuffer = getWriteBuffer(self.writePacketArray[i].itemList[j]); itemBuffer.copy(dataBuffer, dataBufferPointer); dataBufferPointer += itemBuffer.length; // NOTE: It seems that when writing, the data that is sent must have a "fill byte" so that data length is even only for all // but the last request. The last request must have no padding. So we add the padding here. if (j < (self.writePacketArray[i].itemList.length - 1)) { if (itemBuffer.length % 2) { dataBufferPointer += 1; } } } // outputLog('DataBufferPointer is ' + dataBufferPointer); self.writeReq.writeUInt16BE(19 + self.writePacketArray[i].itemList.length * 12 + dataBufferPointer, 2); // buffer length self.writeReq.writeUInt16BE(self.writePacketArray[i].itemList.length * 12 + 2, 13); // Parameter length - 14 for one read, 28 for 2. self.writeReq.writeUInt16BE(dataBufferPointer, 15); // Data length - as appropriate. dataBuffer.copy(self.writeReq, 19 + self.writePacketArray[i].itemList.length * 12, 0, dataBufferPointer); if (self.isoConnectionState === 4) { // outputLog('writing' + (19+dataBufferPointer+self.writePacketArray[i].itemList.length*12)); self.writePacketArray[i].timeout = setTimeout(function() { self.packetTimeout.apply(self, arguments); }, self.globalTimeout, "write", self.writePacketArray[i].seqNum); self.isoclient.write(self.writeReq.slice(0, 19 + dataBufferPointer + self.writePacketArray[i].itemList.length * 12)); // was 31 self.writePacketArray[i].sent = true; self.writePacketArray[i].rcvd = false; self.writePacketArray[i].timeoutError = false; self.parallelJobsNow += 1; outputLog('Sending Write Packet With Sequence Number ' + self.writePacketArray[i].seqNum, 1, self.connectionID); } else { // outputLog('Somehow got into write block without proper isoConnectionState of 4. Disconnect.'); // connectionReset(); // setTimeout(connectNow, 2000, connectionParams); // This is essentially an instantTimeout. self.writePacketArray[i].sent = true; self.writePacketArray[i].rcvd = false; self.writePacketArray[i].timeoutError = true; // Without the scopePlaceholder, this doesn't work. writePacketArray[i] becomes undefined. // The reason is that the value i is part of a closure and when seen "nextTick" has the same value // it would have just after the FOR loop is done. // (The FOR statement will increment it to beyond the array, then exit after the condition fails) // scopePlaceholder works as the array is de-referenced NOW, not "nextTick". var scopePlaceholder = self.writePacketArray[i].seqNum; process.nextTick(function() { self.packetTimeout("write", scopePlaceholder); }); if (self.isoConnectionState === 0) { flagReconnect = true; } } } if (flagReconnect) { // console.log("Asking for callback next tick and my ID is " + self.connectionID); setTimeout(function() { // console.log("Next tick is here and my ID is " + self.connectionID); outputLog("The scheduled reconnect from sendWritePacket is happening now", 1, self.connectionID); self.connectNow(self.connectionParams); // We used to do this NOW - not NextTick() as we need to mark isoConnectionState as 1 right now. Otherwise we queue up LOTS of connects and crash. }, 0); } } NodeS7.prototype.isOptimizableArea = function(area) { var self = this; if (self.doNotOptimize) { return false; } // Are we skipping all optimization due to user request? switch (area) { case 0x84: // db case 0x81: // input bytes case 0x82: // output bytes case 0x83: // memory bytes return true; default: return false; } } NodeS7.prototype.onResponse = function(theData) { var self = this; // Packet Validity Check. Note that this will pass even with a "not available" response received from the server. // For length calculation and verification: // data[4] = COTP header length. Normally 2. This doesn't include the length byte so add 1. // read(13) is parameter length. Normally 4. // read(14) is data length. (Includes item headers) // 12 is length of "S7 header" // Then we need to add 4 for TPKT header. // Decrement our parallel jobs now // NOT SO FAST - can't do this here. If we time out, then later get the reply, we can't decrement this twice. Or the CPU will not like us. Do it if not rcvd. self.parallelJobsNow--; var data=checkRFCData(theData); if(data==="fastACK"){ //read again and wait for the requested data outputLog('Fast Acknowledge received.', 0, self.connectionID); self.isoclient.removeAllListeners('error'); self.isoclient.removeAllListeners('data'); self.isoclient.on('data', function() { self.onResponse.apply(self, arguments); }); self.isoclient.on('error', function() { self.readWriteError.apply(self, arguments); }); }else if( data[7] === 0x32 ){//check the validy of FA+S7 package //********************* VALIDY CHECK *********************************** //TODO: Check S7-Header properly if (data.length > 8 && data[8] != 3) { outputLog('PDU type (byte 8) was returned as ' + data[8] + ' where the response PDU of 3 was expected.'); outputLog('Maybe you are requesting more than 240 bytes of data in a packet?'); outputLog(data); self.connectionReset(); return null; } // The smallest read packet will pass a length check of 25. For a 1-item write response with no data, length will be 22. if (data.length > data.readInt16BE(2)) { outputLog("An oversize packet was detected. Excess length is " + (data.length - data.readInt16BE(2)) + ". "); outputLog("We assume this is because two packets were sent at nearly the same time by the PLC."); outputLog("We are slicing the buffer and scheduling the second half for further processing next loop."); setTimeout(function() { self.onResponse.apply(self, arguments); }, 0, data.slice(data.readInt16BE(2))); // This re-triggers this same function with the sliced-up buffer. // was used as a test setTimeout(process.exit, 2000); } if (data.length < data.readInt16BE(2) || data.readInt16BE(2) < 22 || data[5] !== 0xf0 || data[4] + 1 + 12 + 4 + data.readInt16BE(13) + data.readInt16BE(15) !== data.readInt16BE(2) || !(data[6] >> 7) || (data[7] !== 0x32) || (data[8] !== 3)) { outputLog('INVALID READ RESPONSE - DISCONNECTING'); outputLog('TPKT Length From Header is ' + data.readInt16BE(2) + ' and RCV buffer length is ' + data.length + ' and COTP length is ' + data.readUInt8(4) + ' and data[6] is ' + data[6]); outputLog(data); self.connectionReset(); return null; } //********************** GO ON ************************* // Log the receive outputLog('Received ' + data.readUInt16BE(15) + ' bytes of S7-data from PLC. Sequence number is ' + data.readUInt16BE(11), 1, self.connectionID); // Check the sequence number var foundSeqNum; // self.readPacketArray.length - 1; var isReadResponse, isWriteResponse; // for (packetCount = 0; packetCount < self.readPacketArray.length; packetCount++) { // if (self.readPacketArray[packetCount].seqNum == data.readUInt16BE(11)) { // foundSeqNum = packetCount; // break; // } // } foundSeqNum = self.findReadIndexOfSeqNum(data.readUInt16BE(11)); // if (self.readPacketArray[packetCount] == undefined) { if (foundSeqNum === undefined) { foundSeqNum = self.findWriteIndexOfSeqNum(data.readUInt16BE(11)); if (foundSeqNum !== undefined) { // for (packetCount = 0; packetCount < self.writePacketArray.length; packetCount++) { // if (self.writePacketArray[packetCount].seqNum == data.readUInt16BE(11)) { // foundSeqNum = packetCount; self.writeResponse(data, foundSeqNum); isWriteResponse = true; // break; } } else { isReadResponse = true; self.readResponse(data, foundSeqNum); } if ((!isReadResponse) && (!isWriteResponse)) { outputLog("Sequence number that arrived wasn't a write reply either - dropping"); outputLog(data); // I guess this isn't a showstopper, just ignore it. // self.isoclient.end(); // setTimeout(self.connectNow, 2000, self.connectionParams); return null; } }else{ outputLog('INVALID READ RESPONSE - DISCONNECTING'); outputLog('TPKT Length From Head