UNPKG

node-red-contrib-mcprotocol

Version:

Mode-red nodes to Read from & Write to MITSUBISHI PLC over Ethernet using MC Protocol

1,411 lines (1,203 loc) β€’ 150 kB
// MCPROTOCOL - A library for communication to Mitsubishi PLCs over Ethernet from node.js. // Currently only FX3U CPUs using FX3U-ENET and FX3U-ENET-ADP modules (Ethernet modules) tested. // Please report experiences with others. // The MIT License (MIT) // Copyright (c) 2015 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 dgram = require('dgram'); var EventEmitter = require('events').EventEmitter; var util = require("util"); var inherits = require('util').inherits var effectiveDebugLevel = 0; // intentionally global, shared between connections var monitoringTime = 10; module.exports = MCProtocol; function MCProtocol() { if (!(this instanceof MCProtocol)) return new MCProtocol(); EventEmitter.call(this); var self = this; //self.data = {};//for data access self.readReq = Buffer.alloc(1000);//not calculated self.writeReq;// = new Buffer(1500);//size depends on PLC type! As Q/L can read/write 950 WDs self.queue = []; self.resetPending = false; self.resetTimeout = undefined; self.maxPDU = 255; self.netClient = undefined; self.connectionState = 0; self.requestMaxParallel = 1; self.maxParallel = 1; // MC protocol is read/response. Parallel jobs not supported. self.isAscii = false; self.octalInputOutput; 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 = 4500; self.lastPacketSent = undefined; self.readPacketArray = []; self.writePacketArray = []; self.polledReadBlockList = []; self.globalReadBlockList = []; self.globalWriteBlockList = []; self.masterSequenceNumber = 1; self.translationCB = function (tag) { return tag }; self.connectionParams = undefined; self.connectionID = 'UNDEF'; self.addRemoveArray = []; self.readPacketValid = false; self.connectCBIssued = false; self.queueTimer = undefined; self.queuePollTime = 50; self.queueMaxLength = 50;//avg time on good connection is 20ms. 20 * 50 = 1000ms to process. } inherits(MCProtocol, EventEmitter); MCProtocol.prototype.isConnected = function () { var self = this; return self.connectionState == 4; } MCProtocol.prototype.setDebugLevel = function (level) { var l = (level + "").toUpperCase(); switch (l) { case 'TRACE': effectiveDebugLevel = 4; break; case 'DEBUG': effectiveDebugLevel = 3; break; case 'INFO': effectiveDebugLevel = 2; break; case 'WARN': effectiveDebugLevel = 1; break; case 'ERROR': effectiveDebugLevel = 0; break; case 'NONE': effectiveDebugLevel = -1; break; default: effectiveDebugLevel = level; } } MCProtocol.prototype.nextSequenceNumber = function () { var self = this; self.masterSequenceNumber += 1; if (self.masterSequenceNumber > 32767) { self.masterSequenceNumber = 1; } return self.masterSequenceNumber; } MCProtocol.prototype.setTranslationCB = function (cb) { var self = this; if (typeof cb === "function") { outputLog('Translation OK', "TRACE"); self.translationCB = cb; } } MCProtocol.prototype.initiateConnection = function (cParam, callback) { var self = this; if (cParam === undefined) { cParam = { port: 10000, host: '192.168.8.106', ascii: false }; } outputLog('Initiate Called - Connecting to PLC with address and parameters...', "DEBUG"); outputLog(cParam, "DEBUG"); if (typeof (cParam.name) === 'undefined') { self.connectionID = cParam.host; } else { self.connectionID = cParam.name; } if (typeof (cParam.ascii) === 'undefined') { self.isAscii = false; } else { self.isAscii = cParam.ascii; } if (typeof (cParam.octalInputOutput) === 'undefined') { self.octalInputOutput = false; } else { self.octalInputOutput = cParam.octalInputOutput; } if (typeof (cParam.plcType) === 'undefined') { self.plcType = MCProtocol.prototype.enumPLCTypes.Q.name; self.enumDeviceCodeSpec = MCProtocol.prototype.enumDeviceCodeSpecQ;//default to Q/L series outputLog(`plcType not provided, defaulting to Q series PLC`,"WARN"); } else { self.plcType = cParam.plcType; if(!MCProtocol.prototype.enumPLCTypes[cParam.plcType]){ self.plcType = MCProtocol.prototype.enumPLCTypes.Q.name; outputLog(`plcType '${cParam.plcType}' unknown. Currently supported types are '${MCProtocol.prototype.enumPLCTypes.keys.join("|")}', defaulting to Q series PLC`,"WARN"); } self.plcSeries = MCProtocol.prototype.enumPLCTypes[self.plcType]; //not sure how best to handle A/QnA series - not even sure A series can do 3E/4E frames! //for now, default to Q (will be overwritten below if user choses 1E frames) self.enumDeviceCodeSpec = MCProtocol.prototype['enumDeviceCodeSpec' + self.plcType] || MCProtocol.prototype.enumDeviceCodeSpecQ; outputLog(`'plcType' set is ${self.plcType}`,"INFO"); } if (typeof (cParam.frame) === 'undefined') { outputLog(`'frame' not provided, defaulting '3E'. Valid options are 1E, 3E, 4E.`,"WARN"); self.frame = '3E'; } else { switch (cParam.frame.toUpperCase()) { case '1E': self.frame = '1E'; self.enumDeviceCodeSpec = MCProtocol.prototype.enumDeviceCodeSpec1E; break; case '3E': self.frame = '3E'; break; case '4E': self.frame = '4E'; break; default: self.frame = '3E'; outputLog(`'frame' ${cParam.frame} is unknown. Defaulting to 3E. Valid options are 1E, 3E, 4E.`,"WARN"); break; } self.frame = cParam.frame; outputLog(`'frame' set is ${self.frame}`,"DEBUG"); } if(!self.enumDeviceCodeSpec){ throw new Error("Error determining device code specification. Check combination of PLC Type and Frame Type are valid"); } if (typeof (cParam.PLCStation) !== 'undefined') { self.PLCStation = cParam.PLCStation; } if (typeof (cParam.PCStation) !== 'undefined') { self.PCStation = cParam.PCStation; } if (typeof (cParam.network) !== 'undefined') { self.network = cParam.network; } if (typeof (cParam.PLCModuleNo) !== 'undefined') { self.PLCModuleNo = cParam.PLCModuleNo; } if (typeof (cParam.queuePollTime) !== 'undefined') { self.queuePollTime = cParam.queuePollTime; } if (typeof (cParam.queueMaxAge) !== 'undefined') { self.queueMaxAge = cParam.queueMaxAge; } else { self.queueMaxAge = 2000; } self.plcSeries = MCProtocol.prototype.enumPLCTypes[self.plcType]; self.writeReq = Buffer.alloc(self.plcSeries.requiredWriteBufferSize);//size depends on PLC type! As Q/L can read/write 950 WDs self.connectionParams = cParam; self.connectCallback = callback; self.connectCBIssued = false; if (self.fakeTheConnection)//debug self.connectCallback(); else self.connectNow(self.connectionParams, false); self.startQueueTimer = function (ms) { self.queueTimer = setTimeout(() => { self._processQueue() }, ms); } self.startQueueTimer(self.queuePollTime); self.processQueueASAP = function () { setImmediate(() => { outputLog(`πŸ›’οΈ setImmediate Calling _processQueue() --> `, "TRACE"); self._processQueue(); }); } self._processQueue = function () { clearTimeout(self.queueTimer); try { if (!self.queue.length) { return; } // {arg: , cb: , dt: } var queueItem = self.queue[0]; var itemAge = Date.now() - queueItem.dt; if (itemAge > self.queueMaxAge) { outputLog(`πŸ›’οΈβž‘πŸ—‘οΈ Discarding queued '${queueItem.fn}' item ${queueItem.arg} (item age is ${itemAge}ms, max age is ${self.queueMaxAge}ms)`, "WARN") self.queue.shift(); return;//too old - discard } outputLog(`πŸ›’οΈβž‘βš™οΈ Sending queued '${queueItem.fn}' item ${queueItem.arg}`, "DEBUG"); var result; if (queueItem.fn == "read") { result = _readItems(self, queueItem.arg, queueItem.cb, true); } else if (queueItem.fn == "write") { result = _writeItems(self, queueItem.arg, queueItem.value, queueItem.cb, true); } let allSent = result.every(function (r) { return r.sendStatus == MCProtocol.prototype.enumSendResult.sent; }); if (allSent) { outputLog(`πŸ›’οΈβž‘βš™οΈ Successfully sent queued '${queueItem.fn}' item '${queueItem.arg}'. (queue will be shifted to remove this item)`, "DEBUG"); self.queue.shift();//all sent shift the queued item - its done :) return; //no need to continue } let noneSent = result.every(function (r) { return r.sendStatus == MCProtocol.prototype.enumSendResult.notSent; }); if (noneSent) { outputLog(`πŸ›’οΈβž‘X Queued '${queueItem.fn}' item ${queueItem.arg} NOT sent - will try again soon`, "DEBUG"); return; //no need to continue } //by default, if !allSent and !nonSent, then _some_ were sent! outputLog(`πŸ›’οΈβž‘β˜ οΈ Something failed to send '${queueItem.fn}' item '${queueItem.arg}'. Queue will be shifted to remove this item.`, "WARN"); self.queue.shift();//some sent / some bad - shift the queued item regardless } catch (error) { outputLog(`Something went wrong polling the queue: ${error}. Queue will be shifted to remove this item.`, "ERROR"); self.queue.shift(); } finally { self.startQueueTimer(); } } //_processQueue() } MCProtocol.prototype.dropConnection = function () { var self = this; outputLog(`dropConnection() called`, "TRACE", self.connectionID); if(self.connectionParams.protocol == "UDP"){ //TODO - implement UDP try { if(self.netClient){ self.netClient.close(); } } catch (error) { outputLog(`dropConnection() caused an error error: ${error}`, "ERROR", self.connectionID); } } else { try { if (typeof (self.netClient) !== 'undefined') { self.netClient.end(); } } catch (error) { outputLog(`dropConnection() caused an error error: ${error}`, "ERROR", self.connectionID); } } self.connectionCleanup(); self.connected = false; } MCProtocol.prototype.close = function () { this.dropConnection(); }; MCProtocol.prototype.connectNow = function (cParam, suppressCallback) { // TODO - implement or remove suppressCallback var self = this; if (self.connectionParams.protocol == "UDP") { //TODO - implement UDP // Track the connection state self.connectionState = 1 // 1 = trying to connect if (self.netClient) { self.connectionState = 0 self.netClient.removeAllListeners(); delete self.netClient; } self.netClient = dgram.createSocket('udp4'); self.connected = false; self.requests = {}; function close() { self.connectionState = 0; self.emit('close'); self.connected = false; } // self.netClient.on('listening', function () { // self.onUDPConnect.apply(self, arguments); // }); self.netClient.on('close', close); //self.netClient.connect(); self.netClient.write = function(buffer){ self.netClient.send( buffer, 0, buffer.length, cParam.port, cParam.host, function (err) { if (err) { self.emit('error');//?? } }); } //{ outputLog('UDP Connection Setup to ' + cParam.host + ' on port ' + cParam.port, "DEBUG", self.connectionID); self.netClient.removeAllListeners('data'); self.netClient.removeAllListeners('message'); self.netClient.removeAllListeners('error'); self.netClient.on('message', 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.netClient.on('error', function () { self.readWriteError.apply(self, arguments); }); // Might want to remove the connecterror listener self.emit('open'); if ((!self.connectCBIssued) && (typeof (self.connectCallback) === "function")) { self.connectCBIssued = true; self.connectCallback(); } //} self.connectionState = 4; } else { // Don't re-trigger. if (self.connectionState >= 1) { return; } self.connectionCleanup(); self.netClient = net.connect(cParam, function () { self.netClient.setKeepAlive(true, 2500); // For reliable unplug detection in most cases - although it takes 10 minutes to notify self.onTCPConnect.apply(self, arguments); }); self.connectionState = 1; // 1 = trying to connect self.netClient.on('error', function () { self.connectError.apply(self, arguments); }); self.netClient.on('close', function () { self.onClientDisconnect.apply(self, arguments); }); outputLog('<initiating a new connection>', "INFO", self.connectionID); outputLog('Attempting to connect to host...', "DEBUG", self.connectionID); } } MCProtocol.prototype.connectError = function (e) { var self = this; self.emit('error',e); // Note that a TCP connection timeout error will appear here. An MC connection timeout error is a packet timeout. outputLog('We Caught a connect error ' + e.code, "ERROR", self.connectionID); if ((!self.connectCBIssued) && (typeof (self.connectCallback) === "function")) { self.connectCBIssued = true; self.connectCallback(e); } self.connectionState = 0; } MCProtocol.prototype.readWriteError = function (e) { var self = this; outputLog('We Caught a read/write error ' + e.code + ' - resetting connection', "ERROR", self.connectionID); self.emit('error', e); self.connectionState = 0; self.connectionReset(); } MCProtocol.prototype.packetTimeout = function (packetType, packetSeqNum) { var self = this; outputLog('PacketTimeout called with type ' + packetType + ' and seq ' + packetSeqNum, "WARN", self.connectionID); if (packetType === "read") { outputLog("READ TIMEOUT on sequence number " + packetSeqNum, "WARN", self.connectionID); self.readResponse(undefined); //, self.findReadIndexOfSeqNum(packetSeqNum)); return undefined; } if (packetType === "write") { outputLog("WRITE TIMEOUT on sequence number " + packetSeqNum, "WARN", self.connectionID); self.writeResponse(undefined); //, self.findWriteIndexOfSeqNum(packetSeqNum)); return undefined; } outputLog("Unknown timeout error. Nothing was done - this shouldn't happen.", "ERROR", self.connectionID); } MCProtocol.prototype.onTCPConnect = function () { var self = this; outputLog('TCP Connection Established to ' + self.netClient.remoteAddress + ' on port ' + self.netClient.remotePort, "DEBUG", self.connectionID); // Track the connection state self.connectionState = 4; // 4 = all connected, simple with MC protocol. Other protocols have a negotiation/session packet as well. self.netClient.removeAllListeners('data'); self.netClient.removeAllListeners('message'); self.netClient.removeAllListeners('error'); self.netClient.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.netClient.on('error', function () { self.readWriteError.apply(self, arguments); }); // Might want to remove the connecterror listener self.emit('open'); if ((!self.connectCBIssued) && (typeof (self.connectCallback) === "function")) { self.connectCBIssued = true; self.connectCallback(); } return; } MCProtocol.prototype.onUDPConnect = function () { var self = this; outputLog('UDP Connection Established to ' + self.netClient.remoteAddress + ' on port ' + self.netClient.remotePort, "DEBUG", self.connectionID); // Track the connection state self.connectionState = 4; // 4 = all connected, simple with MC protocol. Other protocols have a negotiation/session packet as well. self.netClient.removeAllListeners('data'); self.netClient.removeAllListeners('message'); self.netClient.removeAllListeners('error'); self.netClient.on('message', 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.netClient.on('error', function () { self.readWriteError.apply(self, arguments); }); // Might want to remove the connecterror listener self.emit('open'); if ((!self.connectCBIssued) && (typeof (self.connectCallback) === "function")) { self.connectCBIssued = true; self.connectCallback(); } return; } MCProtocol.prototype.writeItems = function (arg, value, cb) { return _writeItems(this, arg, value, cb, false); } function _writeItems(self, arg, value, cb, queuedItem) { //var self = this; var i; var reply = []; outputLog("Preparing to WRITE " + arg, "DEBUG", self.connectionID); //ensure arg is an array regardless of count let argArr = arg; let valueArr = value; if (Array.isArray(arg) != true) { argArr = [arg]; valueArr = [value]; } if (self.isWaiting()) { let sendStatus = MCProtocol.prototype.enumSendResult.unknown; if (queuedItem) { sendStatus = MCProtocol.prototype.enumSendResult.notSent; outputLog(`οΈπŸ›’οΈβž‘πŸš§ queued writeItem '${arg}' still not sent (isWaiting)`, "DEBUG") } else if (self.queue.length >= self.queueMaxLength) { outputLog(`οΈπŸ›’οΈβž‘πŸ—‘οΈ writeItem '${arg}' discarded, queue full`, "WARN") sendStatus = MCProtocol.prototype.enumSendResult.queueFull; } else { sendStatus = MCProtocol.prototype.enumSendResult.queued; self.queue.push({ arg: arg, value: value, cb: cb, fn: "write", dt: Date.now() }); outputLog(`οΈβœοΈβž‘πŸ›’οΈ writeItem '${arg}' pushed to queue`, "DEBUG") } reply.push({ TAG: arg, sendStatus: sendStatus });//[item.useraddr] = MCProtocol.prototype.enumSendResult.badRequest; return reply; } let plcitems = []; for (i = 0; i < argArr.length; i++) { if (typeof argArr[i] === "string") { let plcitem = new PLCItem(self); plcitem.init(self.translationCB(argArr[i]), argArr[i], self.octalInputOutput, self.frame, self.plcType, valueArr[i]); plcitem._instance = "original"; if (Array.isArray(cb)) plcitem.cb = cb[i]; else plcitem.cb = cb; plcitems.push(plcitem); } } //do callback for non initialised (bad) items plcitems.map(function (item) { if (item.initialised == false) { if (item.cb) { var cbd = new PLCWriteResult(item.useraddr, item.addr, MCProtocol.prototype.enumOPCQuality.badDeviceFailure.value, 0); cbd.error = item.initError; cbd.problem = true; item.cb(true, cbd); } item.extraInfo = item.initError; reply.problem = true; let r = { TAG: item.useraddr, sendStatus: MCProtocol.prototype.enumSendResult.badRequest }; reply.push(r);//[item.useraddr] = MCProtocol.prototype.enumSendResult.badRequest; } }); //filter OK items var plcitemsInitialised = plcitems.filter(function (item) { return item.initialised; }); var preparedCount = self.prepareWritePacket(plcitemsInitialised); var plcitemsBuffered = plcitemsInitialised.filter(function (item) { return item.bufferized; }); //do callback for items not buffered plcitemsInitialised.map(function (item) { if (!item.bufferized) { if (item.cb) { var cbd = new PLCWriteResult(item.useraddr, item.addr, MCProtocol.prototype.enumOPCQuality.bad.value, 0); cbd.error = item.lastError; cbd.problem = true; item.cb(true, cbd); } item.extraInfo = item.lastError; reply.problem = true; let r = { TAG: item.useraddr, sendStatus: MCProtocol.prototype.enumSendResult.badRequest }; reply.push(r);//[item.useraddr] = MCProtocol.prototype.enumSendResult.badRequest; } }); let sentCount = 0; if (plcitemsBuffered.length) { sentCount = self.sendWritePacket(); } plcitemsBuffered.map(function (item) { let s = sentCount ? MCProtocol.prototype.enumSendResult.sent : MCProtocol.prototype.enumSendResult.notSent; if (s != MCProtocol.prototype.enumSendResult.sent) { reply.problem = true; } let r = { TAG: item.useraddr, sendStatus: s }; reply.push(r); }); return reply; } MCProtocol.prototype.findItem = function (useraddr) { var self = this; var i; var commstate = { value: self.connectionState !== 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; } MCProtocol.prototype.addItems = function (arg, cb) { var self = this; self.addRemoveArray.push({ arg: arg, cb: cb, action: 'poll' }); } MCProtocol.prototype.addItemsNow = function (arg, action, cb) { var self = this; var i; outputLog("Adding " + arg, "DEBUG", self.connectionID); addItemsFlag = false; var addedCount = 0; var expectedCount = Array.isArray(arg) ? arg.length : 1; if (typeof arg === "string" && arg !== "_COMMERR") { //plcitem = stringToMCAddr(self.translationCB(arg), arg, self.octalInputOutput, self.frame, self.plcType); let plcitem = new PLCItem(self); plcitem.init(self.translationCB(arg), arg, self.octalInputOutput, self.frame, self.plcType, undefined /*not writing*/); if (plcitem.initialised) { plcitem.action = action; plcitem.cb = cb; self.polledReadBlockList.push(plcitem); addedCount++; } else { outputLog(`Dropping bad request item '${arg}'`, "WARN"); } } else if (Array.isArray(arg)) { for (i = 0; i < arg.length; i++) { if (typeof arg[i] === "string" && arg[i] !== "_COMMERR") { //plcitem = stringToMCAddr(self.translationCB(arg[i]), arg[i], self.octalInputOutput, self.frame, self.plcType); let plcitem = new PLCItem(self); plcitem.init(self.translationCB(arg[i]), arg[i], self.octalInputOutput, self.frame, self.plcType, undefined /*not writing*/); if (plcitem.initialised) { if (Array.isArray(cb)) plcitem.cb = cb[i]; else plcitem.cb = cb; if (Array.isArray(action)) plcitem.action = action[i]; else plcitem.action = action; self.polledReadBlockList.push(plcitem); addedCount++; } else { outputLog(`Dropping bad request item '${arg[i]}'`, "WARN"); } } } } // 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.", "WARN"); } } // prepareReadPacket(); self.readPacketValid = false; } MCProtocol.prototype.removeItems = function (arg) { var self = this; self.addRemoveArray.push({ arg: arg, action: 'remove' }); } MCProtocol.prototype.removeItemsNow = function (arg) { var self = this; var i; self.removeItemsFlag = false; if (typeof arg === "undefined") { self.polledReadBlockList = []; } else if (typeof arg === "string") { for (i = 0; i < self.polledReadBlockList.length; i++) { outputLog('TCBA ' + self.translationCB(arg), "TRACE"); if (self.polledReadBlockList[i].addr === self.translationCB(arg)) { outputLog('Splicing', "TRACE"); self.polledReadBlockList.splice(i, 1); } } } else if (Array.isArray(arg)) { for (i = 0; i < self.polledReadBlockList.length; i++) { for (j = 0; j < arg.length; j++) { if (self.polledReadBlockList[i].addr === self.translationCB(arg[j])) { self.polledReadBlockList.splice(i, 1); } } } } self.readPacketValid = false; // prepareReadPacket(); } MCProtocol.prototype.readAllItems = function (arg) { var self = this; var i; outputLog("Reading All Items (readAllItems was called)", "TRACE", self.connectionID); if (typeof arg === "function") { self.readDoneCallback = arg; } else { self.readDoneCallback = doNothing; } if (self.connectionState !== 4) { outputLog("Unable to read when not connected. Return bad values.", "WARN", 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.", "DEBUG"); 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), "DEBUG", self.connectionID); if (element.action === 'remove') { self.removeItemsNow(element.arg); } if (element.action === 'poll' || element.action === 'read') { self.addItemsNow(element.arg, element.action, element.cb); } }); self.addRemoveArray = []; // Clear for next time. if (!self.readPacketValid) { self.prepareReadPacket(); } outputLog("Calling SRP from RAI", "TRACE", self.connectionID); self.sendReadPacket(); // Note this sends the first few read packets depending on parallel connection restrictions. } MCProtocol.prototype.readItems = function (arg, cb) { return _readItems(this, arg, cb, false); } function _readItems(self, arg, cb, queuedItem) { //var self = this; var i; var reply = []; outputLog("#readItems() was called)", "TRACE", self.connectionID); //ensure arg is an array regardless of count let argArr = arg; if (Array.isArray(arg) != true) { argArr = [arg]; } if (self.connectionState !== 4) { outputLog("Unable to read when not connected. Return bad values.", "WARN", self.connectionID); //self.queue = [];//empty the queue } // For better behaviour when auto-reconnecting - don't return now if (self.isWaiting()) { let sendStatus = MCProtocol.prototype.enumSendResult.unknown; if (queuedItem) { sendStatus = MCProtocol.prototype.enumSendResult.notSent; outputLog(`οΈπŸ›’οΈβž‘πŸš§ queued readItem '${arg}' still not sent (isWaiting)`, "DEBUG") } else if (self.queue.length >= self.queueMaxLength) { outputLog(`οΈπŸ›’οΈβž‘πŸ—‘οΈ readItem '${arg}' discarded, queue full`, "WARN") sendStatus = MCProtocol.prototype.enumSendResult.queueFull; } else { sendStatus = MCProtocol.prototype.enumSendResult.queued; self.queue.push({ arg: arg, cb: cb, fn: "read", dt: Date.now() }); outputLog(`οΈπŸ“’βž‘πŸ›’οΈ readItem '${arg}' pushed to queue`, "DEBUG") } reply.push({ TAG: arg, sendStatus: sendStatus });//[item.useraddr] = MCProtocol.prototype.enumSendResult.badRequest; return reply; } let plcitems = []; for (i = 0; i < argArr.length; i++) { if (typeof argArr[i] === "string" && argArr[i] !== "_COMMERR") { let plcitem = new PLCItem(self); plcitem.init(self.translationCB(argArr[i]), argArr[i], self.octalInputOutput, self.frame, self.plcType, undefined /*not writing*/); if (Array.isArray(cb)) plcitem.cb = cb[i]; else plcitem.cb = cb; plcitem.action = 'read'; plcitems.push(plcitem); } } //do callback for non initialised (bad) items plcitems.map(function (item) { if (item.initialised == false) { var cbd = new PLCReadResult(item.useraddr, item.addr, MCProtocol.prototype.enumOPCQuality.badConfigErrInServer.value, 0, undefined, undefined); outputLog(`Failed to initialise PLC item. Addr '${item.addr}' may be invalid for this type of PLC and frame setting - item will be dropped`, "ERROR"); item.cb(true, cbd); item.extraInfo = item.initError; reply.problem = true; let r = { TAG: item.useraddr, sendStatus: MCProtocol.prototype.enumSendResult.badRequest }; reply.push(r); } }); //filter OK items var plcitemsInitialised = plcitems.filter(function (item) { return item.initialised; }); //check how many good items - return if none if (!plcitemsInitialised.length) { outputLog("Nothing to send!", "WARN"); return reply; } let pp = self.prepareReadPacket(plcitemsInitialised); outputLog("Calling sendReadPacket()", "TRACE", self.connectionID); var sentCount = self.sendReadPacket(); // Note this sends the first few read packets depending on parallel connection restrictions. plcitemsInitialised.map(function (item) { let s = sentCount ? MCProtocol.prototype.enumSendResult.sent : MCProtocol.prototype.enumSendResult.notSent; if (s != MCProtocol.prototype.enumSendResult.sent) { reply.problem = true; } let r = { TAG: item.useraddr, sendStatus: s }; reply.push(r); }); return reply; } MCProtocol.prototype.isWaiting = function () { var self = this; return (self.isReading() || self.isWriting()); } MCProtocol.prototype.isReading = function () { return this.readPacketArray.some(function(el){ return el.sent; }); } MCProtocol.prototype.isWriting = function () { return this.writePacketArray.some(function(el){ return el.sent; }); } MCProtocol.prototype.clearReadPacketTimeouts = function () { var self = this; clearPacketTimeouts(self.readPacketArray); } MCProtocol.prototype.clearWritePacketTimeouts = function () { var self = this; outputLog('Clearing write PacketTimeouts', "DEBUG", self.connectionID); // Before we initialize the 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; } } MCProtocol.prototype.prepareWritePacket = function (itemList) { outputLog("#################### prepareWritePacket() ####################", "TRACE"); var self = this; var requestList = []; // The request list consists of the block list, split into chunks readable by PDU. var requestNumber = 0, thisBlock = 0, thisRequest = 0; var itemsThisPacket; var numItems; // 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.globalWriteBlockList = []; itemList[0].block = thisBlock; // Just push the items into blocks and figure out the write buffers for (i = 0; i < itemList.length; i++) { if (itemList[i].prepareWriteData()) { itemList[i].writeBuffer._instance = itemList[i]._instance; self.globalWriteBlockList.push(itemList[i]); // Remember - by reference. var bli = self.globalWriteBlockList[self.globalWriteBlockList.length-1]; bli.isOptimized = false; bli.itemReference = []; bli.itemReference.push(itemList[i]); } } // Split the blocks into requests, if they're too large. for (i = 0; i < self.globalWriteBlockList.length; i++) { let block = self.globalWriteBlockList[i]; var startElement = block.offset; var remainingLength = block.byteLengthWrite; var remainingTotalArrayLength = block.totalArrayLength; var maxByteRequest = block.maxWordLength() * 2; var lengthOffset = 0; block.partsBufferized = 0; // How many parts? block.parts = Math.ceil(block.byteLengthWrite / maxByteRequest); outputLog(`globalWriteBlockList[${i}].parts == ${block.parts} for request '${block.useraddr}', .offset (device number) == ${block.offset}, maxByteRequest==${maxByteRequest}`, "DEBUG"); block.requestReference = []; // If we need to spread the sending/receiving over multiple packets... for (j = 0; j < block.parts; j++) { // create a request for a globalWriteBlockList. requestList[thisRequest] = block.clone(); let reqItem = requestList[thisRequest]; reqItem._instance = "block clone (request item)"; reqItem.part = j+1; //reqItem.updateSeqNum(self.nextSequenceNumber()); reqItem.offset = startElement; reqItem.byteLengthWrite = Math.min(maxByteRequest, remainingLength); if (reqItem.bitNative) { reqItem.totalArrayLength = Math.min(maxByteRequest * 2, remainingTotalArrayLength, block.totalArrayLength); } else { // I think we should be dividing by dtypelen here reqItem.totalArrayLength = Math.min(maxByteRequest / block.dtypelen, remainingLength / block.dtypelen, block.totalArrayLength); } remainingTotalArrayLength -= reqItem.totalArrayLength; reqItem.byteLengthWithFill = reqItem.byteLengthWrite; reqItem.writeBuffer = block.writeBuffer.slice(lengthOffset, lengthOffset + reqItem.byteLengthWithFill); reqItem.writeQualityBuffer = block.writeQualityBuffer.slice(lengthOffset, lengthOffset + reqItem.byteLengthWithFill); lengthOffset += reqItem.byteLengthWrite; //if we have split a request into parts, then we need to update the device count before creating the comm buffer if (block.parts > 1) { //reqItem.datatype = 'BYTE';//why??? //reqItem.dtypelen = 1;//why??? if (reqItem.bitNative) { reqItem.arrayLength = reqItem.totalArrayLength;//globalReadBlockList[thisBlock].byteLength; } else { reqItem.arrayLength = reqItem.byteLengthWrite / 2;//globalReadBlockList[thisBlock].byteLength; } } //generate the comm buffer for this part (with new address offset & length etc) reqItem.toBuffer(self.isAscii, self.frame, self.plcType, self.nextSequenceNumber(), self.network, self.PCStation, self.PLCStation, self.PLCModuleNo); block.requestReference.push(reqItem); block.partsBufferized++; outputLog(`Created block part (reqItem) ${reqItem.part} of ${block.parts} for request '${block.useraddr}', .offset (device number) == ${reqItem.offset}, byteLengthWrite==${reqItem.byteLengthWrite}, SeqNum == ${reqItem.seqNum}`, "DEBUG"); remainingLength -= maxByteRequest; if (block.bitNative) { startElement += maxByteRequest * 2; } else { startElement += maxByteRequest / 2; } thisRequest++; } block.bufferized = (block.partsBufferized == block.parts); } self.clearWritePacketTimeouts(); self.writePacketArray = []; // Set up the write packet while (requestNumber < requestList.length) { numItems = 0; self.writePacketArray.push(new PLCPacket()); var thisPacketNumber = self.writePacketArray.length - 1; self.writePacketArray[thisPacketNumber].itemList = []; // Initialize as array. for (var i = requestNumber; i < requestList.length; i++) { if (numItems == 1) { break; // Used to break when packet was full. Now break when we can't fit this packet in here. } requestNumber++; numItems++; self.writePacketArray[thisPacketNumber].seqNum = requestList[i].seqNum; self.writePacketArray[thisPacketNumber].itemList.push(requestList[i]); } } outputLog("writePacketArray Length = " + self.writePacketArray.length, "DEBUG"); return thisRequest; //return count of prepared requests. } MCProtocol.prototype.prepareReadPacket = function (items) { outputLog("#################### prepareReadPacket() ####################", "TRACE"); const fnstarttime = process.hrtime(); var self = this; var itemList = items || 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. var startOfSlice, endOfSlice, oldEndCoil, demandEndCoil; let blocklist = []; //self.globalReadBlockList // Validity check. for (i = itemList.length - 1; i >= 0; i--) { if (itemList[i] === undefined) { itemList.splice(i, 1); outputLog("Dropping an undefined request item.", "WARN", 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; } /* DISABLED for now as I need to match the items[x] with the cacheitem[x] and update cacheitem[x].cb to new items[x].cb callback //in order to optimise, cache read packets - where the items addresses are used to generate a key var packetCacheKey = 'key:'; for (const item in itemList) { if (itemList.hasOwnProperty(item)) { packetCacheKey = packetCacheKey + itemList[item].addr + ','; } } outputLog("packetCacheKey = " + packetCacheKey, 3, self.connectionID); if (!this.readPacketArrayCache) this.readPacketArrayCache = []; var cache = this.readPacketArrayCache.find(obj => obj.key == packetCacheKey) if (cache) { outputLog(`------ ⚑⚑ ------ YEY ------ ⚑⚑ ------- Found cache item packetCacheKey:${packetCacheKey} - returning this (nice speed up) -------- ⚑⚑⚑`, 2, self.connectionID); cache.lastUsed = Date.now(); self.readPacketArray = cache.packetArray; self.globalReadBlockList = cache.blocklist; self.readPacketValid = true; clearPacketTimeouts(self.readPacketArray); // //increment seq number // for (let p in self.readPacketArray) { // if (self.readPacketArray.hasOwnProperty(p)) { // self.readPacketArray[p].seqNum = self.nextSequenceNumber(); // } // } let fntimetaken = process.hrtime(fnstarttime); outputLog(`function '${getFuncName()}()' - time taken ${fntimetaken[0] * 1e9 + fntimetaken[1]} nanoseconds βŒ›`, 3); return cache; } outputLog("Cache item not found :(. packetCacheKey : " + packetCacheKey, 3, self.connectionID); //cache maintenance... //IMPORTANT: each item cached is approx 99kb (well stringified it is anyhow - so keep cache size reasonable) if (this.readPacketArrayCache.length >= 10) { //cleanup - delete oldest this.readPacketArrayCache.sort((a, b) => a.lastUsed < b.lastUsed); let deleteditem = this.readPacketArrayCache.pop(); outputLog("--------BOO!------- Cache item clean up (Cache exhausted) - removing item packetCacheKey : " + deleteditem.key + ' ---------cache item removed--- ✨✨✨ ', 3, self.connectionID); } */ // ...because you have to start your optimization somewhere. blocklist[0] = itemList[0]; blocklist[0].itemReference = []; blocklist[0].itemReference.push(itemList[0]); var maxByteRequest, thisBlock = 0; itemList[0].block = thisBlock; // Optimize the items into blocks for (i = 1; i < itemList.length; i++) { maxByteRequest = itemList[i].maxWordLength() * 2; if ((itemList[i].areaMCCode !== blocklist[thisBlock].areaMCCode) || // Can't optimize between areas (!self.isOptimizableArea(itemList[i].areaMCCode)) || // May as well try to optimize everything. ((itemList[i].offset - blocklist[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 - (blocklist[thisBlock].offset + blocklist[thisBlock].byteLength) > self.maxGap) && !itemList[i].bitNative) || ((itemList[i].offset - (blocklist[thisBlock].offset + blocklist[thisBlock].byteLength) > self.maxGap * 8) && itemList[i].bitNative)) { // If our gap is large, create a new block. // At this point we give up and create a new block. thisBlock = thisBlock + 1; blocklist[thisBlock] = itemList[i]; // By reference. // itemList[i].block = thisBlock; // Don't need to do this. blocklist[thisBlock].isOptimized = false; blocklist[thisBlock].itemReference = []; blocklist[thisBlock].itemReference.push(itemList[i]); // outputLog("Not optimizing."); } else { outputLog("Performing optimization of item " + itemList[i].addr + " with " + blocklist[thisBlock].addr, "DEBUG"); // 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. if (itemList[i].bitNative) { // Coils and inputs must be special-cased blocklist[thisBlock].byteLength = Math.max( blocklist[thisBlock].byteLength, (Math.floor((itemList[i].requestOffset - blocklist[thisBlock].requestOffset) / 8) + itemList[i].byteLength) ); if (blocklist[thisBlock].byteLength % 2) { // shouldn't be necessary blocklist[thisBlock].byteLength += 1; } } else { blocklist[thisBlock].byteLength = Math.max( blocklist[thisBlock].byteLength, ((itemList[i].offset - blocklist[thisBlock].offset) * 2 + Math.ceil(itemList[i].byteLength / itemList[i].multidtypelen)) * itemList[i].multidtypelen ); } outputLog("Optimized byte length is now " + blocklist[thisBlock].byteLength, "DEBUG"); // Point the buffers (byte and quality) to a sliced version of the optimized block. This is by reference (same area of memory) if (itemList[i].bitNative) { // Again a special case. startOfSlice = (itemList[i].requestOffset - blocklist[thisBlock].requestOffset) / 8; // NO, NO, NO - not the dtype length - start of slice varies with register width. itemList[i].multidtypelen; } else { startOfSlice = (itemList[i].requestOffset - blocklist[thisBlock].requestOffset) * 2; // NO, NO, NO - not the dtype length - start of slice varies with register width. itemList[i].multidtypelen; } endOfSlice = startOfSlice + itemList[i].byteLength; itemList[i].byteBuffer = blocklist[thisBlock].byteBuffer.slice(startOfSlice, endOfSlice); itemList[i].qualityBuffer = blocklist[thisBlock].qualityBuffer.slice(startOfSlice, endOfSlice); // 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 globalReadBlockList[thisBlock] exists already at this point, and our buffer is already set, let's not do this now. // globalReadBlockList[thisBlock].datatype = 'BYTE'; // globalReadBlockList[thisBlock].dtypelen = 1; blocklist[thisBlock].isOptimized = true; blocklist[thisBlock].itemReference.push(itemList[i]); } } var thisRequest = 0; // Split the blocks into requests, if they're too large. for (i = 0; i < blocklist.length; i++) { // Always create a request for a globalReadBlockList. let blockListItem = blocklist[i]; // How many parts? maxByteRequest = blockListItem.maxWordLength() * 2; blockListItem.parts = Math.ceil(blockListItem.byteLength / maxByteRequest); var startElement = blockListItem.requestOffset; // try to ignore the offset var remainingLength = blockListItem.byteLength; var remainingTotalArrayLength = blockListItem.totalArrayLength; //initialise the buffers blockListItem.byteBuffer.fill(0)// = new Buffer(blockListItem.byteLength); blockListItem.qualityBuffer.fill(0)// = new Buffer(blockListItem.byteLength); blockListItem.requestReference = []; // If we need to spread the sending/receiving over multiple packets... for (j = 0; j < blockListItem.parts; j++) { requestList[thisRequest] = blockListItem.clone(); let thisReqItem = requestList[thisRequest]; thisReqItem._instance = "block clone (request item)"; blockListItem.requestReference.push(thisReqItem); thisReqItem.requestOffset = startElement; thisReqItem.byteLength = Math.min(maxByteRequest, remainingLength); if (thisReqItem.bitNative) { thisReqItem.totalArrayLength = Math.min(maxByteRequest * 8, remainingLength * 8, blockListItem.totalArrayLength); } else { thisReqItem.totalArrayLength = Math.min(maxByteRequest / blockListItem.dtypelen, remainingLength / blockListItem.dtypelen, blockListItem.totalArrayLength); } thisReqItem.byteLengthWithFill = thisReqItem.byteLength; if (thisReqItem.byteLengthWithFill % 2) { thisReqItem.byteLengthWithFill += 1; }; // Just for now... I am not sure if we really want to do this in this case. if (blockListItem.parts > 1) { thisReqItem.datatype = 'BYTE'; thisReqItem.dtypelen = 1; if (thisReqItem.bitNative) { thisReqItem.arrayLength = thisReqItem.totalArrayLength;//globalReadBlockList[thisBlock].byteLength; } else { thisReqItem.arrayLength = thisReqItem.byteLength / 2;//globalReadBlockList[thisBlock].byteLength; } } outputLog(`Created block part (reqItem) ${j+1} of ${blockListItem.parts} for request '${blockListItem.useraddr}', .offset (device number) == ${thisReqItem.offset}, byteLength==${thisReqItem.byteLength}`, "DEBUG"); remainingLength -= maxByteRequest; if (blockListItem.bitNative) { // startElement += maxByteRequest/thisReqItem.multidtypelen; startElement += maxByteRequest * 8; } else { startElement += maxByteRequest / 2; } thisRequest++; } } // The packetizer... var requestNumber = 0; var itemsThisPacket; self.clearReadPacketTimeouts(); let packetArray = []; packetArray = []; while (requestNumber < requestList.length) { // Set up the read packet var numItems = 0; packetArray.push(new PLCPacket()); var thisPacketNumber = packetArray.length - 1; //packetArray[thisPacketNumber].seqNum = self.nextSequenceNumber(); packetArray[thisPacketNumber].itemList = []; // Initialize as array. for (var i = requestNumber; i < requestList.length; i++) { if (numItems >= 1) { break; // We can't fit this packet in here. For now, this is always the case as we only have one item in MC protocol. } requestNumber++; numItems++; packetArray[thisPacketNumber].itemList.push(requestList[i]); } } self.readPacketArray = packetArray; self.globalReadBlockList = blocklist; self.readPacketValid = true; let rpa = { lastUsed: Date.now(), /* DISABLED cache for now key: packetCacheKey, */ blocklist: blocklist, packetArray: packetArray, } let fntimetaken = process.hrtime(fnstarttime); outputLog(`function '${getFuncName()}()' - time taken ${fntimetaken[0] * 1e9 + fntimetaken[1]} nanoseconds βŒ›`, "TRACE"); /* DISABLED cache for now outputLog("------------------ that took aaaaaggggeeeeessss adding packetCacheKey : " + packetCacheKey + ' to the cache for optimisation --------------- 🏺🏺🏺', 3, self.connectionID); self.readPacketArrayCache.push(rpa); */ return rpa; } function clearPacketTimeouts(packetArray) { outputLog('Clearing read PacketTimeouts', 3); // Before we initialize the readPacketArray, we need to loop through all of them and clear timeouts. for (i = 0; i < packetArray.length; i++) { clearTimeout(packetArray[i].timeout); packetArray[i].sent = false; packetArray[i].rcvd = false; } } function getFuncName() { return getFuncName.caller.name } MCProtocol.prototype.sendReadPacket = function (arg) { var self = this; var i, j, curLength, returnedBfr, routerLength; var flagReconnect = false; var sentCount = 0; outputLog("#################### sendReadPacket() ####################", 3, self.connectionID); for (i = 0; i < self.readPacketArray.length; i++) { let readPacket = self.readPacketArray[i]; if (readPacket.sent) { continue; } if (self.parallelJobsNow >= self.maxParallel) { continue; } // From here down is SENDING the packet readPacket.reqTime = process.hrtime(); curLength = 0; routerLength = 0; // The FOR loop is left in here for now, but really we are only doing one request per packet for now. for (j = 0; j < readPacket.itemList.length; j++) { var item = readPacket.itemList[j]; item.sendTime = Date.now(); readPacket.seqNum = self.nextSequenceNumber();// readPacket.seqNum; item.toBuffer(self.isAscii, self.frame, self.plcType, readPacket.seqNum, self.network, self.PCStation, self.PLCStation, self.PLCModuleNo); returnedBfr = item.buffer.data; returnedBfr.copy(self.readReq, curLength); curLength += returnedBfr.length; outputLog(`The buffer.length of read item '${item.useraddr} [offset:${item.offset}] (seqNum:${item.seqNum})' is '${returnedBfr.length}'...`, "DEBUG"); outputLog(returnedBfr, "DEBUG"); } outputLog("The final send buffer is...", "DEBUG"); var sendBuffer = self.readReq.slice(0, curLength); if (self.isAscii) { outputLog(asciize(sendBuffer), "DEBUG"); outputLog(binarize(asciize(sendBuffer)), "DEBUG"); } else { outputLog(sendBuffer, "DEBUG"); } if (self.connectionState == 4) { readPacket.timeout = setTimeout(function () { self.packetTimeout.apply(self, arguments); }, self.globalTimeout, "read", readPacket.seqNum); if (self.isAscii) { self.netClient.write(asciize(sendBuffer)); } else { self.netClient.write(sendBuffer); // was 31 } self.lastPacketSent = readPacket; self.lastPacketSent.isWritePacket = false; self.lastPacketSent.isReadPacket = true; sentCount++; readPacket.sent = true; readPacket.rcvd = false; readPacket.timeoutError = false; self.parallelJobsNow += 1; outputLog('Sent Read Packet SEQ ' + readPacket.seqNum, "DEBUG"); } else { // outputLog('Somehow got into read block without proper connectionState of 4. Disconnect.'); // connectionReset(); // setTimeout(connectNow, 2000, connectionParams); // Note we aren't incrementing maxParallel so we are actually going to time out on all our packets all at once. readPacket.sent = true; readPacket.rcvd = false; readPacket.timeoutError = true; if (!flagReconnect) { // Prevent duplicates outputLog(`Not Sending Read Packet (seqNum==${readPacket