omron-fins
Version:
Node.js implementation of the Omron FINS protocol
1,240 lines (1,147 loc) • 55.2 kB
JavaScript
const dgram = require('dgram');
const net = require('net');
const inherits = require('util').inherits;
const EventEmitter = require('events').EventEmitter;
const constants = require('./FinsConstants');
const SequenceManager = require('./FinsSequenceManager');
const FinsHeader = require('./FinsHeader');
const FinsAddressUtil = require('./FinsAddressUtil');
const {dec2bcd, bcd2dec, boolsToBytes, wordsToBytes, dwordsToBytes, mergeData, getKeyName, isInt } = require('./FinsDataUtils');
const MEMORY_AREA_READ = _getResponseCommandCode(...constants.CommandCodes.MEMORY_AREA_READ);
const MEMORY_AREA_WRITE = _getResponseCommandCode(...constants.CommandCodes.MEMORY_AREA_WRITE);
const MEMORY_AREA_FILL = _getResponseCommandCode(...constants.CommandCodes.MEMORY_AREA_FILL);
const MEMORY_AREA_READ_MULTI = _getResponseCommandCode(...constants.CommandCodes.MEMORY_AREA_READ_MULTI);
const MEMORY_AREA_TRANSFER = _getResponseCommandCode(...constants.CommandCodes.MEMORY_AREA_TRANSFER);
const CPU_UNIT_DATA_READ = _getResponseCommandCode(...constants.CommandCodes.CPU_UNIT_DATA_READ);
const CPU_UNIT_STATUS_READ = _getResponseCommandCode(...constants.CommandCodes.CPU_UNIT_STATUS_READ);
const STOP = _getResponseCommandCode(...constants.CommandCodes.STOP);
const RUN = _getResponseCommandCode(...constants.CommandCodes.RUN);
const CLOCK_READ = _getResponseCommandCode(...constants.CommandCodes.CLOCK_READ);
const CLOCK_WRITE = _getResponseCommandCode(...constants.CommandCodes.CLOCK_WRITE);
//#region Common JSdoc hints
/**
* Optional callback for FINs commands
* @callback CommandCallback
* @param {*} err - Error (if any)
* @param {object} msg - The msg object containing the `request`, `response`, `tag` and more.
*/
/**
* @typedef {Object} CommandOptions
* @property {number} [DNA=null] Destination Network Address
* @property {number} [DA1=null] Destination Node
* @property {number} [DA2=null] Destination Unit: Enter 0 for CPU, 10 to 1F for CPU BUS Unit (10+Unit), E1 for inner board
* @property {CommandCallback} [callback=null] Callback to call upon PLC command response
* @property {number} [timeout=null] Optional timeout for this command
*/
//#endregion
module.exports = FinsClient;
/**
*
* @param {number} port The UDP/TCP port to connect to
* @param {string} host The IP or hostname to connect to
* @param {object} options Additional options including `protocol` `MODE` `timeout` `DNA` `DA1` `DA2` `SNA` `SA1` `SA2`
* @param {boolean} [connect=true] (optional, default=true) Connect to PLC when initialising
* @returns
*/
function FinsClient(port, host, options, connect) {
if (!(this instanceof FinsClient)) return new FinsClient(port, host, options, connect);
EventEmitter.call(this);
this.init(port, host, options);
//default is to connect
if(connect === true || connect == null) {
this.connect();
}
}
inherits(FinsClient, EventEmitter);
//#region FinsClient prototypes
/**
* Initialise the FinsClient - must be called before `FinsClient.connect()`.
* NOTE: `init` is normally called when the FinsClient is created. This function is not normally called by user code.
* @param {number} port The UDP/TCP port to connect to
* @param {string} host The IP or hostname to connect to
* @param {object} options Additional options including `MODE` `protocol` `timeout` `DNA` `DA1` `DA2` `SNA` `SA1` `SA2`
*/
FinsClient.prototype.init = function (port, host, options) {
/** @type {FinsClient}*/ const self = this;
const defaultHost = constants.DefaultHostValues;
const defaultOptions = constants.DefaultOptions;
self.initialised = false;
self.connected = false;
self.requests = {};
self.port = port || defaultHost.port;
self.host = host || defaultHost.host;
self.options = options || {};
self.options.MODE = self.options.MODE || "CS";
self.timeout = isInt(options.timeout, defaultOptions.timeout) || 2000;
self.max_queue = isInt(options.ICF, defaultOptions.max_queue) || 100;
self.protocol = (options && options.protocol) || defaultOptions.protocol || "udp";
/** @type {FinsAddressUtil} */ self.finsAddresses = new FinsAddressUtil(self.options.MODE);
try {
self.options.maxEventListeners = parseInt(self.options.maxEventListeners || 30);
if(isNaN(self.options.maxEventListeners) || self.options.maxEventListeners <= 0) {
self.options.maxEventListeners = 30;
}
} catch (error) {
self.options.maxEventListeners = 30;
}
this.setMaxListeners(self.options.maxEventListeners);
switch (self.protocol) {
case 'udp':
case 'tcp':
break;
default:
throw new Error('invalid protocol option specified', self.protocol, 'protocol must be "udp" or "tcp"');
}
self.header = FinsHeader(self.options);
self.sequenceManager = new SequenceManager({ timeout: this.timeout }, function (err, seq) {
if (err) {
self.emit("error", err, seq);
}
});
self.disconnect();
self.remoteInfo = {
address: self.host,
family: 'IPV4',
port: self.port,
protocol: self.protocol
}
self._socket_handler_receive = socket_receive.bind(self);
self._socket_handler_initialised = socket_initialised.bind(self);
self._socket_handler_listening = socket_listening.bind(self);
self._socket_handler_tcp_init_listening = tcp_socket_init_listening.bind(self);
self._socket_handler_tcp_init_receive = tcp_socket_init_receive.bind(self);
self._socket_handler_close = socket_close.bind(self);
self._socket_handler_error = socket_error.bind(self);
self.processReply = _processReply.bind(self);
socket_initialised.call(self);
};
/**
* Open the connection to the PLC.
* NOTE: if `host`, `port` or `options` are provided, any currently open connection will be closed then re-opened using the new values provided.
* If you simply wish to connect with existing settings, call `connect()` without any parameters. If the connection is already open, the function will simply exit.
* @param {number} port The UDP/TCP port to connect to
* @param {string} host The IP or hostname to connect to
* @param {object} options Additional options including `MODE` `protocol` `timeout` `DNA` `DA1` `DA2` `SNA` `SA1` `SA2`
*/
FinsClient.prototype.connect = function (host, port, options) {
/** @type {FinsClient}*/ const self = this;
if(!self.initialised) {
throw new Error('Cannot connect (not initialised)');
}
if(self.connected) {
return;
}
self.disconnect();//first ensure connection is cleaned up
if(host != null || port != null || options != null) {
const optionOverride = (existingOpts, newOpts, optionName ) => {
if(!optionName || !existingOpts || !newOpts) return;
if( Object.prototype.hasOwnProperty.call(newOpts, optionName) ) {
existingOpts[optionName] = newOpts[optionName];
}
};
if(typeof options == "object") {
optionOverride(self.options, options, "protocol");
optionOverride(self.options, options, "MODE");
optionOverride(self.options, options, "timeout");
optionOverride(self.options, options, "max_queue");
optionOverride(self.options, options, "SNA");
optionOverride(self.options, options, "SA1");
optionOverride(self.options, options, "SA2");
optionOverride(self.options, options, "DNA");
optionOverride(self.options, options, "DA1");
optionOverride(self.options, options, "DA2");
optionOverride(self.options, options, "maxEventListeners");
}
self.init(port || self.port, host || self.host, self.options);
if(!self.initialised) {
throw new Error('Cannot connect (not initialised)');
}
}
// eslint-disable-next-line no-self-assign
/** @type {dgram.Socket} */ self.socket = self.socket;
// eslint-disable-next-line no-self-assign
/** @type {net.Socket} */ self.tcp_socket = self.tcp_socket;
switch (self.protocol) {
case 'udp':
/** @type {dgram.Socket} */ self.socket = dgram.createSocket('udp4');
self.socket.on('message', self._socket_handler_receive);
self.socket.on('listening', self._socket_handler_listening);
self.socket.on('close', self._socket_handler_close);
self.socket.on('error', self._socket_handler_error);
self.socket.connect(self.port, self.host);
break;
case 'tcp':
/** @type {net.Socket} */ self.tcp_socket = net.createConnection(self.port, self.host, self._socket_handler_tcp_init_listening);
self.tcp_socket.on('data', self._socket_handler_tcp_init_receive);
self.tcp_socket.on('close', self._socket_handler_close);
self.tcp_socket.on('error', self._socket_handler_error);
break;
default:
throw new Error('invalid protocol option specified', options.protocol, 'protocol must be "udp" or "tcp"');
}
};
/**
* Disconnect the socket from PLC
*/
FinsClient.prototype.disconnect = function () {
/** @type {FinsClient}*/ const self = this;
const doEmit = self.connected;
try {
if (self.socket) {
self.socket.removeAllListeners();
self.socket.close();
}
} catch (error) {
//do nothing
} finally {
delete self.socket;
}
try {
if (self.tcp_socket) {
self.tcp_socket.removeAllListeners();
self.tcp_socket.destroy();
}
} catch (error) {
//do nothing
} finally {
delete self.tcp_socket;
}
self.connected = false;
doEmit && self.emit('close'); //fire "close" manually since we already called removeAllListeners
};
/**
* Memory Area Read Command.
* FINS command code 0101
* @param {string} address - Memory area and the numerical start address e.g. `D100` or `CIO50.0`
* @param {number} count - Number of registers to read
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns the SID of the request (returns `null` if any of the command parameters are invalid).
*/
FinsClient.prototype.read = function (address, count, opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const memoryAddress = self.stringToFinsAddress(address);
const addressData = memoryAddress && memoryAddress.bytes;
if (!addressData) {
_sendError(self, "invalid address", callback, { tag: tag });
return null;
}
if (!count) {
_sendError(self, "count is empty", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const command = constants.Commands["0101"];
const packet = mergeData(headerBytes, command.command, addressData, wordsToBytes(count));
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
address: memoryAddress,
count: count,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* Memory Area Write Command.
* FINS command code 0102
* @param {string} address - Memory area and the numerical start address e.g. `D100` or `CIO50.0`
* @param {number|number[]} data - Data to write. This can be 1 value or an array values. For WD addresses, data value(s) should be 16 bit integer. For BIT addresses, data value(s) should be boolean or 1/0.
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns the SID of the request (returns `null` if any of the command parameters are invalid).
*/
FinsClient.prototype.write = function (address, data, opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const memoryAddress = self.stringToFinsAddress(address);
const addressData = memoryAddress ? memoryAddress.bytes : null;
if (!addressData || !addressData.length) {
_sendError(self, "invalid address", callback, { tag: tag });
return null;
}
if(!Array.isArray(data)) {
data = [data];
}
if (!data || !data.length) {
_sendError(self, "data is empty", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const regsToWrite = wordsToBytes((data.length || 1));
const command = constants.Commands["0102"];
let dataBytesToWrite;
if (memoryAddress.isBitAddress) {
dataBytesToWrite = boolsToBytes(data);
} else if (memoryAddress.elementLength === 4) {
dataBytesToWrite = dwordsToBytes(data);
} else {
dataBytesToWrite = wordsToBytes(data);
}
const packet = mergeData(headerBytes, command.command, addressData, regsToWrite, dataBytesToWrite);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
address: memoryAddress,
dataBytesToWrite: dataBytesToWrite,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* Memory Area Fill command. Fills 1 or more addresses with the same 16bit value.
* FINS command code 0103
* @param {string} address - Memory area and the numerical start address e.g. `D100` or `CIO50`
* @param {number} value - Value to write
* @param {number} count - Number of registers to write
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns the SID of the request (returns `null` if any of the command parameters are invalid).
*/
FinsClient.prototype.fill = function (address, value, count, opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const memoryAddress = self.stringToFinsAddress(address);
const addressData = memoryAddress && memoryAddress.bytes;
if (!addressData) {
_sendError(self, "invalid address", callback, { tag: tag });
return null;
}
if (typeof value != "number") {
_sendError(self, "value is invalid", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const command = constants.Commands["0103"];
const dataBytesToWrite = wordsToBytes(value);
const packet = mergeData(headerBytes, command.command, addressData, wordsToBytes(count), dataBytesToWrite);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
address: memoryAddress,
count: count,
dataBytesToWrite: dataBytesToWrite,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* Multiple Memory Area Read Command.
* FINS command code 0104
* @param {string|string[]} addresses - Array or CSV of Memory addresses e.g. `"D10.15,CIO100,E0_100"` or `["CIO50.0","D30", "W0.0"]`
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
*/
FinsClient.prototype.readMultiple = function (addresses, opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const command = constants.Commands["0104"];
const commandData = [];
let addressList = [];
const memoryAddresses = [];
if (typeof addresses == "string") {
addressList = addresses.split(",");
} else if (Array.isArray(addresses)) {
addressList.push(...addresses);
} else {
_sendError(self, "invalid address", callback, { tag: tag });
}
for (let i = 0; i < addressList.length; i++) {
let address = addressList[i];
if (typeof address !== "string" || !address.trim().length) {
_sendError(self, "invalid address", callback, { tag: tag });
return null;
}
address = address.trim();
const memoryAddress = self.stringToFinsAddress(address);
const addressData = memoryAddress && memoryAddress.bytes;
if (!addressData) {
_sendError(self, "invalid address", callback, { tag: tag });
return null;
}
commandData.push(addressData);
memoryAddresses.push(memoryAddress);
}
const packet = mergeData(headerBytes, command.command, commandData);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
address: memoryAddresses,
count: addressList.length,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* MEMORY AREA TRANSFER.
* Copies and transfers the contents of the specified number of consecutive memory area words to the specified memory area.
* FINS command code 0105
* @param {string} srcAddress - Source Memory address e.g. `D100` or `CIO50`
* @param {string} dstAddress - Destination Memory address e.g. `D200` or `CI100`
* @param {number} count - Number of registers to copy
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns SID
*/
FinsClient.prototype.transfer = function (srcAddress, dstAddress, count, opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const srcMemoryAddress = self.stringToFinsAddress(srcAddress);
const srcAddressData = srcMemoryAddress ? srcMemoryAddress.bytes : null;
if (!srcAddressData || !srcAddressData.length) {
_sendError(self, "invalid source address", callback, { tag: tag });
return null;
}
const dstMemoryAddress = self.stringToFinsAddress(dstAddress);
const dstAddressData = dstMemoryAddress ? dstMemoryAddress.bytes : null;
if (!dstAddressData || !dstAddressData.length) {
_sendError(self, "invalid destination address", callback, { tag: tag });
return null;
}
const command = constants.Commands["0105"];
const commandData = [srcAddressData, dstAddressData, wordsToBytes(count)];
const packet = mergeData(headerBytes, command.command, commandData);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
srcAddress: srcMemoryAddress,
dstAddress: dstMemoryAddress,
count: count,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* Change PLC to MONITOR mode
* FINS command code 0401
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns the SID of the request (returns `null` if any of the command parameters are invalid).
*/
FinsClient.prototype.run = function (opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const command = constants.Commands["0401"];
const packet = mergeData(headerBytes, command.command);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* Change PLC to PROGRAM mode
* FINS command code 0402
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns the SID of the request (returns `null` if any of the command parameters are invalid).
*/
FinsClient.prototype.stop = function (opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const command = constants.Commands["0402"];
const packet = mergeData(headerBytes, command.command);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* CPU UNIT DATA READ. Reads CPU Unit data
* FINS command code 0501
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns the SID of the request (returns `null` if any of the command parameters are invalid).
*/
FinsClient.prototype.cpuUnitDataRead = function (opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const command = constants.Commands["0501"];
const packet = mergeData(headerBytes, command.command);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* Get PLC status
* FINS command code 0601
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns the SID of the request (returns `null` if any of the command parameters are invalid).
*/
FinsClient.prototype.status = function (opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const command = constants.Commands["0601"];
const packet = mergeData(headerBytes, command.command);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* CLOCK READ. Reads the present year, month, date, minute, second, and day of the week.
* FINS command code 0701
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns the SID of the request (returns `null` if any of the command parameters are invalid).
*/
FinsClient.prototype.clockRead = function (opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const command = constants.Commands["0701"];
const packet = mergeData(headerBytes, command.command);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* CLOCK WRITE. Changes the present year, month, date, minute, second, or day of the week.
* FINS command code 0702
* @param {*} clockData - An object containing `{year,month,day,hour,minute,second,day_of_week}` (second & day_of_week are optional)
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns the SID of the request (returns `null` if any of the command parameters are invalid).
*/
FinsClient.prototype.clockWrite = function ({year,month,day,hour,minute,second,day_of_week}, opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const SID = self.header.incrementSID();
const headerBytes = self.header.bytes(options);
const command = constants.Commands["0702"];
const commandData = [dec2bcd(year),dec2bcd(month),dec2bcd(day),dec2bcd(hour),dec2bcd(minute)];
if(second != null) {
commandData.push(dec2bcd(second));
if(day_of_week != null) {
commandData.push(day_of_week);
}
}
const packet = mergeData(headerBytes, command.command, commandData);
const buffer = Buffer.from(packet);
const request = {
sid: SID,
command: command,
options: options,
callback: callback
};
_transmitCommand(self, SID, buffer, request, tag);
return SID;
};
/**
* Generic command
* @param {string} commandCode 4 digit command code. e.g. 0101 MEMORY AREA READ
* @param {Any[]} params associated command parameters
* @param {CommandOptions|CommandCallback} [opts=null] - Optional. If opts is an object, it can contain `.timeout` and `.DNA` `.DA1` `.DA2` numbers (for routing) and a `.callback` method `(err, msg) => {}` If opts is a callback function, it should have the signature `(err, msg) => {}`
* @param {*} [tag=null] - Optional tag item that is sent back in the callback method
* @returns
*/
FinsClient.prototype.command = function (commandCode, params, opts, tag) {
/** @type {FinsClient}*/ const self = this;
const { options, callback } = _normaliseCommandOptions(opts);
if (self.queueCount() >= self.max_queue) {
_sendFull(self, callback);
return null;
}
if(self.connected !== true) {
_sendError(self, "not connected", callback, { tag: tag });
return null;
}
const cmd = constants.Commands[commandCode];
if(!cmd) {
_sendError(self, `commandCode '${commandCode}' not recognised`, callback, { tag: tag });
return null;
}
//basic parameter check
if(cmd.params && cmd.params.length) {
for (let index = 0; index < cmd.params.length; index++) {
const expectedParam = cmd.params[index];
const providedParam = params[index];
if(!providedParam && expectedParam.required) {
_sendError(self, `Parameter ${index+1} Missing. Expected '${expectedParam.name}'`, callback, { tag: tag });
}
if(expectedParam.type == null || expectedParam.type == "*" || expectedParam.type == "Any") {
//param type ok
} else if(typeof providedParam !== expectedParam.type) {
_sendError(self, `Parameter ${index+1} '${expectedParam.name}' incorrect type. Expected type of '${expectedParam.type}'`, callback, { tag: tag });
}
}
}
if(cmd.name == "read") {
return self.read(params[0], params[1], options, tag);
} else if(cmd.name == "write") {
return self.write(params[0], params[1], options, tag);
} else if(cmd.name == "read-multiple") {
return self.readMultiple(params[0], options, tag);
} else if(cmd.name == "fill") {
return self.fill(params[0], params[1], params[2], options, tag);
} else if(cmd.name == "transfer") {
return self.transfer(params[0], params[1], params[2], options, tag);
} else if(cmd.name == "status") {
return self.status(options, tag);
} else if(cmd.name == "run") {
return self.run(options, tag);
} else if(cmd.name == "stop") {
return self.stop(options, tag);
} else if(cmd.name == "cpu-unit-data-read") {
return self.cpuUnitDataRead(options, tag);
} else if(cmd.name == "clock-read") {
return self.clockRead(options, tag);
} else if(cmd.name == "clock-write") {
return self.clockWrite(params[0], options, tag);
} else {
_sendError(self, `command not recognised`, callback, { tag: tag });
return null;
}
};
FinsClient.prototype.stringToFinsAddress = function (addressString) {
return this.finsAddresses.stringToAddress(addressString);
};
FinsClient.prototype.FinsAddressToString = function (finsAddress, offsetWD, offsetBit) {
return this.finsAddresses.addressToString(finsAddress, offsetWD, offsetBit);
};
FinsClient.prototype.queueCount = function () {
return this.sequenceManager.activeCount();
};
//#endregion
//#region Socket Handlers
function socket_initialised() {
/** @type {FinsClient}*/ const self = this;
self.initialised = true;
self.emit('initialised', self.options);
}
function socket_listening() {
/** @type {FinsClient}*/ const self = this;
self.emit('open', self.remoteInfo);
self.connected = true;
}
// eslint-disable-next-line no-unused-vars
function tcp_socket_init_listening(err, data) {
/** @type {FinsClient}*/ const self = this;
/* SEND FINS/TCP COMMAND*/
/*
* GENERATE FINS NODE NUMBER DATA SEND COMMAND (CLIENT TO SERVER)
*/
let fins_tcp_header = Buffer.alloc(20);
fins_tcp_header[0] = 70;// 'F'; /* Header */
fins_tcp_header[1] = 73;// 'I';
fins_tcp_header[2] = 78;// 'N';
fins_tcp_header[3] = 83;// 'S';
fins_tcp_header[4] = 0x00; /* Length */
fins_tcp_header[5] = 0x00;
fins_tcp_header[6] = 0x00;
fins_tcp_header[7] = 0x0C;
fins_tcp_header[8] = 0x00; /* Command */
fins_tcp_header[9] = 0x00;
fins_tcp_header[10] = 0x00;
fins_tcp_header[11] = 0x00;
fins_tcp_header[12] = 0x00; /* Error Code */
fins_tcp_header[13] = 0x00;
fins_tcp_header[14] = 0x00;
fins_tcp_header[15] = 0x00;
fins_tcp_header[16] = 0x00; /* Client Node Add */
fins_tcp_header[17] = 0x00;
fins_tcp_header[18] = 0x00;
fins_tcp_header[19] = 0x00; /* AUTOMATICALLY GET FINS CLIENT FINS NODE NUMBER */
self.tcp_socket.write(fins_tcp_header, () => { });
}
function tcp_socket_init_receive(data) {
/** @type {FinsClient}*/ const self = this;
if (data.length != 24) {
self._socket_handler_tcp_init_error(new Error("Initial response is invalid - expected 24 bytes"));
return;
}
const magic = data.slice(0, 4).toString();
self.client_node_no = data[19]; //My node no
self.server_node_no = data[23]; //PLC node no
if (magic !== "FINS") {
self._socket_handler_tcp_init_error(new Error("Initial response is invalid - expected for find 'FINS' at the beginning of the packet"));
return;
}
self.tcp_socket.off("data", self._socket_handler_tcp_init_receive);
self.tcp_socket.on("data", self._socket_handler_receive);
self._socket_handler_listening();
}
function socket_close() {
/** @type {FinsClient}*/ const self = this;
self.emit('close');
self.connected = false;
}
function socket_error(err) {
/** @type {FinsClient}*/ const self = this;
self.emit('error', err);
}
function socket_receive(buf, rinfo) {
/** @type {FinsClient}*/ const self = this;
if (!rinfo && self.protocol == "tcp") {
rinfo = self.remoteInfo;
}
try {
if (rinfo.protocol === "tcp") {
let offset = 0;
while (offset < buf.length) {
const magic = buf.slice(0 + offset, 4 + offset).toString();
const len = buf.readUint32BE(4 + offset);
//const cmd = buf.readUint32BE(8 + offset);
const err = buf.readUint32BE(12 + offset);
if (magic != "FINS") {
throw new Error("Expected FINS magic packet");
}
if (err) {
throw new Error(constants.TCPCommandErrorCodes[err] || "Error " + err);
}
const tcpBuf = buf.slice(offset+16, offset + len + 8);
process(tcpBuf);
offset += len + 8;
}
} else {
process(buf);
}
} catch (error) {
self.emit('error', error);
}
function process(buffer) {
const response = self.processReply(buffer, rinfo);
if (typeof response === "object") {
self.sequenceManager.done(response.sid); //1st, cancel the timeout
var seq = self.sequenceManager.get(response.sid); //now get the sequence
if (seq) {
seq.response = response;
var request = seq.request;
if (request && request.callback) {
request.callback(null, seq);
} else {
self.emit('reply', seq);
}
self.sequenceManager.remove(response.sid);
}
} else if(response === -1){
//error already sent
} else {
throw new Error("Unable to process the PLC reply");
}
}
}
//#endregion
//#region Supporting functions
function _normaliseCommandOptions(/** @type {CommandCallback|CommandOptions}*/options) {
/** @type {CommandCallback}*/ let callback;
options = options || {};
if (typeof options == "function") {
callback = options
options = {};
}
if (typeof options.callback == "function") {
callback = options.callback
delete options.callback;
}
return { options, callback };
}
function _getResponseCommandCode(byte10, byte11) {
return [byte10, byte11].map(e => e.toString(16).padStart(2, "0")).join('');
}
/**
* Transmit the command buffer to socket
* @param {FinsClient} fcInstance - the FinsClient instance
* @param {number} SID - Service ID for this transmission
* @param {Buffer} buffer - the buffer to transmit
* @param {Object} request - the request details object
* @param {Any} tag - optional tag object to be sent in the request callback back after response is received
*/
function _transmitCommand(fcInstance, SID, buffer, request, tag) {
setImmediate(function (SID, buffer, _req, tag) {
fcInstance.sequenceManager.add(SID, _req, tag);//add the SID sequence manager for monitoring / timeout / stats etc
const cb = function (err) {
if (err) {
fcInstance.sequenceManager.setError(SID, err);
} else {
fcInstance.sequenceManager.confirmSent(SID);
}
}
if (fcInstance.protocol === "tcp") {
if(!fcInstance.tcp_socket || !fcInstance.connected) {
cb(new Error("not connected"));
} else {
let fins_tcp_header = Buffer.alloc(16);
fins_tcp_header[0] = 70;// 'F'; /* Header */
fins_tcp_header[1] = 73;// 'I';
fins_tcp_header[2] = 78;// 'N';
fins_tcp_header[3] = 83;// 'S';
fins_tcp_header[4] = 0x00; /* Length */
fins_tcp_header[5] = 0x00;
fins_tcp_header[6] = 0x00;
fins_tcp_header[7] = 8 + buffer.length; /*Length of data from Command up to end of FINS frame */
fins_tcp_header[8] = 0x00; /* Command */
fins_tcp_header[9] = 0x00;
fins_tcp_header[10] = 0x00;
fins_tcp_header[11] = 0x02;
fins_tcp_header[12] = 0x00; /* Error Code */
fins_tcp_header[13] = 0x00;
fins_tcp_header[14] = 0x00;
fins_tcp_header[15] = 0x00;
buffer[4] = fcInstance.server_node_no//DA1 dest PLC node no
buffer[7] = fcInstance.client_node_no//SA1 src node no
const packet = Buffer.concat([fins_tcp_header, buffer]);
fcInstance.tcp_socket.write(packet, cb);
}
} else {
if(!fcInstance.socket || !fcInstance.connected) {
cb(new Error("not connected"));
} else {
fcInstance.socket.send(buffer, cb);
}
}
}, SID, buffer, request, tag);
}
function _processEndCode(/** @type {number} */hiByte, /** @type {number} */loByte) {
let MRES = hiByte, SRES = loByte;
const NetworkRelayError = ((MRES & 0x80) > 0);
const NonFatalCPUUnitErr = ((SRES & 0x40) > 0);
const FatalCPUUnitErr = ((SRES & 0x80) > 0);
MRES = (MRES & 0x3f);
SRES = (SRES & 0x2f);
let endCode = ((MRES << 8) + SRES).toString(16) + ""; //.padStart(4,"0"); NodeJS8+
while (endCode.length < 4) {
endCode = "0" + endCode;
}
const endCodeDescription = constants.EndCodeDescriptions[endCode] + "";
return {
MRES: MRES,
SRES: SRES,
NetworkRelayError: NetworkRelayError,
NonFatalCPUUnitErr: NonFatalCPUUnitErr,
FatalCPUUnitErr: FatalCPUUnitErr,
endCode: endCode,
endCodeDescription: endCodeDescription
}
}
function _initialProcessing(buf, /** @type {SequenceManager} */sequenceManager, fnName, expectedCmdCode ) {
const sid = buf[9];
const responseCommandCode = _getResponseCommandCode(buf[10], buf[11]);
const seq = sequenceManager && sequenceManager.get(sid);
if(!seq || sid > sequenceManager.maxSID || sid < sequenceManager.minSID) {
throw new Error(`Unexpected SID '${sid}' received`);
}
expectedCmdCode = expectedCmdCode || constants.Commands[responseCommandCode].commandCode
if (responseCommandCode !== expectedCmdCode) {
throw new Error(`Unexpected command code response. Expected '${expectedCmdCode}' received '${responseCommandCode}'`);
}
fnName = fnName || constants.Commands[responseCommandCode].name;
if (seq.request.command.name !== fnName) {
throw new Error(`Unexpected function type response. Expected '${fnName}' received '${seq.request.command.name}'`);
}
return {
sid,
seq,
command: seq.request.command
}
}
function _processDefault(buf, rinfo, sequenceManager) {
const cmdCode = (buf.slice(10, 12)).toString("hex");
const fnName = constants.Commands[cmdCode].name;
const {sid, command} = _initialProcessing(buf, sequenceManager, fnName, cmdCode);
return { remoteHost: rinfo.address, sid: sid, command: command };
}
function _processCpuUnitDataRead(buf, rinfo, sequenceManager) {
/*
* see https://www.myomron.com/downloads/1.Manuals/PLCs/CPUs/W342-E1-14%20CS_CJ_CP+HostLink%20FINS%20ReferenceManual.pdf
* data starts at byte 14 in buffer
* 20bytes = CPU Unit model,
* 20bytes = CPU Unit internal system version
* 40bytes For system use
* 12bytes Area data
* 64bytes CPU Bus Unit configuration
* 1byte CPU Unit information
* 1byte Remote I/O data
*/
const fnName = "cpu-unit-data-read";
const cmdCode = "0501";
const {sid, command} = _initialProcessing(buf, sequenceManager, fnName, cmdCode);
const data = buf.slice(14);
const CPUUnitModel = data.slice(0,20);
const CPUUnitInternalSystemVersion = data.slice(20,40);
const SystemUse = data.slice(40,80);
const DIPSwitches = SystemUse.readUInt8();
const switches = {
SW1: (DIPSwitches & 0b00000001) == 0b00000001,
SW2: (DIPSwitches & 0b00000010) == 0b00000010,
SW3: (DIPSwitches & 0b00000100) == 0b00000100,
SW4: (DIPSwitches & 0b00001000) == 0b00001000,
SW5: (DIPSwitches & 0b00010000) == 0b00010000,
SW6: (DIPSwitches & 0b00100000) == 0b00100000,
SW7: (DIPSwitches & 0b01000000) == 0b01000000,
SW8: (DIPSwitches & 0b10000000) == 0b10000000,
}
const AreaData = data.slice(80,92);
const MaxProgramSizeKb = AreaData.readUInt16BE(0); //Maximum size of usable program area
const IOMSizeKb = AreaData.readUInt8(2);//The size of the area (CIO, WR, HR, AR, timer/ counter completion flags, TN) in which bit commands can be used (always 23)
const NoOfDMWords = AreaData.readUInt16BE(3);//Total words in the DM area (always 32,768)
const TimerCounterSizeKb = AreaData.readUInt8(5); //Maximum number of timers/counters available (always 8)
const EMBankCount_NonFile = AreaData.readUInt8(6); // Among the banks in the EM area, the number of banks (0 to D) without file memory
const MemoryCardType = AreaData.readUInt8(8);
const MemoryCardSize = AreaData.readUInt16BE(10);
const CPUBusUnitConfiguration = data.slice(92,156);
const CPUUnitInformation = data.slice(156,157);
const RemoteIOData = data.slice(157,158);
const CPUBusUnitConfigurationParser = function(unit, buf) {
let present = (buf[0] & 0x80) == 0x80;
buf[0] = (buf[0] & 0x7F);
return {
unit: unit,
modelID: buf.toString(),
present
}
}
const CPUBusUnitConfigurationItems = [];
for (let index = 0; index < 16; index++) {
const idx = index*2;
const entry = CPUBusUnitConfiguration.slice(idx,idx+2);
CPUBusUnitConfigurationItems.push(CPUBusUnitConfigurationParser(index, entry));
}
return {
remoteHost: rinfo.address,
sid: sid,
command: command,
result: {
CPUUnitModel: CPUUnitModel.toString().trim(),
CPUUnitInternalSystemVersion: CPUUnitInternalSystemVersion.toString().trim(),
SystemUse: {
DIPSwitches: switches,
LargestEMBankNumber: SystemUse.readUInt8(1)
},
AreaData: {
MaxProgramSizeKb,
IOMSizeKb,
NoOfDMWords,
TimerCounterSizeKb,
EMBankCount_NonFile,
MemoryCardType,
MemoryCardSize
},
CPUBusUnitConfiguration: CPUBusUnitConfigurationItems,
SYSMACBUSMastersCount: (RemoteIOData[0] & 0x03),
RackCount: (CPUUnitInformation[0] & 0x0f) ,
}
};
}
function _processStatusRead(buf, rinfo, sequenceManager) {
const fnName = "status";
const cmdCode = "0601"
const {sid, command} = _initialProcessing(buf, sequenceManager, fnName, cmdCode);
const status = (buf[14] & 0x81); //Mask out battery[2] and CF[1] status or a direct lookup could fail.
const mode = buf[15];
const fatalErrorData = {};
const nonFatalErrorData = {};
const fed = buf.readInt16BE(16);
const nfed = buf.readInt16BE(18);
const messageYN = buf.readInt16BE(20);
const plcErrCode = buf.readInt16BE(22);
let plcMessage = "";
if (messageYN) plcMessage = buf.slice(24, -1).toString(); //PLC Message
//any fatal errors?
if (fed) {
for (var i in constants.FatalErrorData) {
if ((fed & constants.FatalErrorData[i]) != 0) {
fatalErrorData[i] = true;
}
}
}
//any non fatal errors?
if (nfed) {
for (var j in constants.NonFatalErrorData) {
if ((nfed & constants.NonFatalErrorData[j]) != 0) {
nonFatalErrorData[j] = true;
}
}
}
const statusCodes = constants.Status;
const runModes = constants.Modes;
return {
remoteHost: rinfo.address,
sid: sid,
command: command,
result: {
status: getKeyName(statusCodes, status),
mode: getKeyName(runModes, mode),
fatalErrors: (fed ? fatalErrorData : null),
nonFatalErrors: (nfed ? nonFatalErrorData : null),
plcErrCode: plcErrCode,
plcMessage: plcMessage
}
};
}
function _processClockRead(buf, rinfo, sequenceManager) {
const fnName = "clock-read";
const cmdCode = "0701"
const {sid, command} = _initialProcessing(buf, sequenceManager, fnName, cmdCode);
const dataStart = 14;
/*
BYTE
0 1 2 3 4 5 6
Year Month Day Hour Minute Second Day of week
*/
const year = buf[dataStart+0];
const month = buf[dataStart+1];
const day = buf[dataStart+2];
const hour = buf[dataStart+3];
const minute = buf[dataStart+4];
const second = buf[dataStart+5];
const day_of_week = buf[dataStart+6];
const clock = {
year: bcd2dec(year),
month: bcd2dec(month),
day: bcd2dec(day),
hour: bcd2dec(hour),
minute: bcd2dec(minute),
second: bcd2dec(second),
day_of_week: day_of_week,
}
return {
remoteHost: rinfo.address,
sid: sid,