node-omron-fins
Overview
This is an implementation of the OMRON FINS protocol using Node.js. This library allows for rapid development of network based services that need to communicate with FINS capable devices. Utilizing the awesome asynchronous abilities of Node.js communication with large numbers of devices is very fast.
Both UDP and TCP are supported however UDP is preferred because of its low overhead and performance advantages. Although UDP is connectionless this library makes use of software based timeouts and transaction identifiers (SID) to ensure request and response are matched up allowing for better reliability.
This adaption of the library utilises a sequence manager to coordinate callbacks with data responses, providing the ability to clearly understand who called for data when the request returns. All of the commands (listed below) can be called not only with a callback
(or an options
object with .callback
), they also except a tag
of any type (typically an object or key) permitting further routing in the users callback. If the options
/callback
is omitted, the appropriate reply/timeout/error is emitted instead.
When an options
object is used in the command function, FINS routing and timeout can also be specified. See the Memory Area Read Command below for an example of using an options object and FINS routing.
Version Update Notes
This release (and possibly future releases up to V1.0.0) may have breaking changes.
Where possible, I make every attempt to keep things compatible, but sometimes, it becomes obvious a better way is worth the trouble - it happens (sorry) :)
Semantic Versioning 2.0.0 will be followed after V1 however for now, where you see V0.x.y
...
x
= major / minor changey
= patch / bugfix
Supported PLCs:
- CP
- CV
- CS
- CJ
- NJ
- NX
NOTE: Not all NX PLCs have FINS support.
Supported Commands:
- Memory area read (Word and Bit)
- Memory area write (Word and Bit)
- Memory area fill
- Multiple Memory area read
- Memory area transfer
- Controller status read
- Run
- Stop
- CPU UNIT Data read
Prerequisites
- Node.js (required) - Server side javascript runtime for Windows, Linux and Mac)
- Wireshark (optional) - This will allow you to see and monitor FINS communication
Install
As an example we will be making a directory for our example code and installing the module there:
mkdir helloFins
cd helloFins
npm init
npm install omron-fins
Usage
Requiring the library:
const fins = require('omron-fins');
Create a FinsClient
object and pass it:
port
- FINS UDP port number as set on the PLCip
- IP address of the PLCoptions
- An object containing necessary parameters protocol
, timeout
, DNA
, DA1
, DA2
, SNA
, SA1
, SA2
const options = {timeout: 5000, SA1: 2, DA1: 1, protocol: "udp"};
const IP = '192.168.0.2';
const PORT = 9600;
const client = fins.FinsClient(PORT, IP, options);
Add a reply listener. The msg
parameters content will vary depending on the command issued.
.sid
- Transaction identifier. Use this to track specific command/ response pairs..request
- An object containing values like the parsed address
object and the command command
.response
- An object containing the parsed values from the FINS command including the remotehost
, endCode
and endCodeDescription
..error
- This will normally be false
. Check msg.response.*
if true
.complete
- true if the command has completed.values
- if the command returns values, they will be here.stats
- performance information.tag
- the tag value you sent (if any) with the command..timeout
- a boolean flag to indicate if a transaction timeout occurred
Example...
client.on('reply', msg){
console.log("Reply from : ", msg.response.remoteHost);
console.log("Sequence ID (SID) : ", msg.sid);
console.log("Requested command : ", msg.request.command);
console.log("Response code : ", msg.response.endCode);
console.log("Response desc : ", msg.response.endCodeDescription);
console.log("Data returned : ", msg.response.values || "");
console.log("Round trip time : ", msg.timeTaken + "ms");
console.log("Your tag : ", msg.tag);
});
Finally, call any of the supported commands!
Memory Area Read Command
.read(address, count, callback, tag)
address
- Memory area and the numerical start address e.g. D100
or CIO50.0
count
- Number of registers to readoptions
- Optional options object or a callback (err, msg) => {}
(If an options object is provided then a callback can be added to the options object as options.callback
)tag
- Optional tag item that is sent back in the callback method
.read('D00000',10);
client.read('D00000',10,function(err, msg) {
console.log("msg: ", msg);
});
const opts = {
timeoutMS: 3000,
DNA: 5,
DA1: 2,
callback: function(err, msg) {
if(err) console.error(err);
console.log("msg: ", msg);
}
}
client.read('D00000',10, opts, myTag);
Memory Area Write Command
.write(address, dataToBeWritten, callback, tag)
address
- Memory area and the numerical start address e.g. D100
or CIO50.0
data
- An array of values or single valueoptions
- Optional options object or a callback (err, msg) => {}
(If an options object is provided then a callback can be added to the options object as options.callback
)tag
- Optional tag item that is sent back in the callback method
.write('D00000',1337)
.write('D00000',[12,34,56]);
.write('D0.4',[true, false, 1, 0]);
.write('D00000',[12,34,56], function(msg){
console.log(msg.response)
});
.write('D00000',[12,34,56],function(err, msg) {
console.log("seq: ", msg);
});
Memory Area Fill Command
.fill(address, value, count, callback, tag)
address
- Memory area and the numerical start address e.g. D100
or CIO50
value
- Value to be filledcount
- Number of registers to writeoptions
- Optional options object or a callback (err, msg) => {}
(If an options object is provided then a callback can be added to the options object as options.callback
)tag
- Optional tag item that is sent back in the callback method
.fill('D00100',1337,10);
.fill('D00100',1337,10,function(err, msg) {
console.log("msg: ", msg);
});
.fill('CIO5.0',true,4);
Multiple Memory Area Read Command
.readMultiple(addresses, callback, tag)
addresses
- Array or CSV of Memory addresses e.g. "D10.15,CIO100,E0_100"
or ["CIO50.0", "D30", "W0.0"]
options
- Optional options object or a callback (err, msg) => {}
(If an options object is provided then a callback can be added to the options object as options.callback
)tag
- Optional tag item that is sent back in the callback method
.readMultiple("D10.15,CIO100,E0_100");
.readMultiple(["CIO50.0","D30", "W0.0"],1337,10,function(err, msg) {
console.log("msg: ", msg);
});
MEMORY AREA TRANSFER Command
.transfer(srcAddress, dstAddress, count, callback, tag)
srcAddress
- Source Memory address e.g. D100
or CIO50
dstAddress
- Destination Memory address e.g. D200
or CI100
count
- Number of registers to copyoptions
- Optional options object or a callback (err, msg) => {}
(If an options object is provided then a callback can be added to the options object as options.callback
)tag
- Optional tag item that is sent back in the callback method
.transfer("D10", "CIO20", 10);
.transfer("D10", "CIO20", 10, function(err, msg) {
console.log("msg: ", msg);
});
Change PLC to MONITOR mode
.run(callback, tag)
options
- Optional options object or a callback (err, msg) => {}
(If an options object is provided then a callback can be added to the options object as options.callback
)tag
- Optional tag item that is sent back in the callback method
.run(function(err, msg) {
console.log(err, msg)
});
Change PLC to PROGRAM mode
.stop(callback, tag)
options
- Optional options object or a callback (err, msg) => {}
(If an options object is provided then a callback can be added to the options object as options.callback
)tag
- Optional tag item that is sent back in the callback method
.stop(function(err, msg) {
console.log(err, msg)
});
.stop();
Get PLC Status
.status(callback, tag)
options
- Optional options object or a callback (err, msg) => {}
(If an options object is provided then a callback can be added to the options object as options.callback
)tag
- Optional tag item that is sent back in the callback method
.status(function(err, msg) {
console.log(err, msg)
}, tag);
.status();
CPU UNIT DATA READ
.cpuUnitDataRead(callback, tag)
options
- Optional options object or a callback (err, msg) => {}
(If an options object is provided then a callback can be added to the options object as options.callback
)tag
- Optional tag item that is sent back in the callback method
.cpuUnitDataRead(function(err, msg) {
console.log(err, msg)
}, tag);
.cpuUnitDataRead();
======
Example applications
Basic Example
A basic example that will demonstrate most of the features for a single client connection.
const fins = require('../lib/index');
const client = fins.FinsClient(9600, '192.168.1.120', { protocol: "udp", SA1: 36, DA1: 120, timeout: 2000 });
client.on('error', function (error, msg) {
console.log("Error: ", error, msg);
});
client.on('timeout', function (host, msg) {
console.log("Timeout: ", host, msg);
});
client.on('open', function (info) {
console.log("open: ", info);
client.on('reply', function (msg) {
console.log("");
console.log("############# client.on('reply'...) #################");
console.log("Reply from : ", msg.response.remoteHost);
console.log("Sequence ID (SID) : ", msg.sid);
console.log("Requested command : ", msg.request.command);
console.log("Response code : ", msg.response.endCode);
console.log("Response desc : ", msg.response.endCodeDescription);
if (msg.request.command.name == 'cpu-unit-data-read') {
console.log("CPU model : ", msg.response.CPUUnitModel || "");
console.log("CPU version : ", msg.response.CPUUnitInternalSystemVersion || "");
} else {
console.log("Data returned : ", msg.response.values || "");
}
console.log("Round trip time : ", msg.timeTaken + "ms");
console.log("Your tag : ", msg.tag);
console.log("#####################################################");
console.log("");
});
console.log("Read CPU Unit Data ")
client.cpuUnitDataRead(null, { "tagdata": "Calling cpuUnitDataRead" });
console.log("Read 10 WD from D0")
client.read('D0', 10, null, { "tagdata": "I asked for 10 registers from D0" });
console.log("Read 32 bits from D0.0")
client.read('D0.0', 32, null, { "tagdata": "I asked for 32 bits from D0.0" });
console.log(`Read multiple addresses "D0,D0.0,D0.1,D0.2,D0.3,W10,D1.15"`)
client.readMultiple('D0,D0.0,D0.1,D0.2,D0.3,W10,D1.15', null, "readMultiple 'D0,D0.0,D0.1,D0.2,D0.3,W10,D1.15'");
console.log(`Read multiple addresses ["D0","D0.0","D0.1","D0.2","W10","D1.15"]`)
client.readMultiple(["D0", "D0.0", "D0.1", "D0.2", "W10", "D1.15"], null, 'readMultiple ["D0","D0.0","D0.1","D0.2","W10","D1.15"]');
var cb = function (err, msg) {
console.log("");
console.log("################ DIRECT CALLBACK ####################");
if (err)
console.error(err);
else
console.log("SID: " + msg.request.sid, msg.request.command.name, msg.request.command.desc, msg.request.command.descExtra, msg.tag || "", msg.response.endCodeDescription);
console.log("#####################################################");
console.log("");
};
let randomInt = parseInt(Math.random() * 1000) + 1;
console.log(`Fill D700~D709 with random number '${randomInt}' - direct callback expected`)
client.fill('D700', randomInt, 10, cb, `set D700~D709 to '${randomInt}'`);
console.log("Transfer D700~D709 to D710~D719 - direct callback expected");
client.transfer('D700', 'D710', 10, cb, "Transfer D700~D709 to D710~D719");
console.log(`Read D700~D719 - expect ${randomInt}`)
client.read('D700', 20, null, `Read D700~D719 - expect all values to be '${randomInt}'`)
console.log(`Read D700~D719 from DNA:2, DA1:11 with individual timeout setting`)
const readRemotePLC_options = {
timeoutMS: 400,
DNA: 2,
DA1: 11,
callback: function (err, msg) {
if (err) {
console.error(err, msg, "Read D700~D719 from DNA: 2, DA1:11")
} else {
console.log(msg, "Read D700~D719 from DNA: 2, DA1:11")
}
}
}
client.read('D700', 20, readRemotePLC_options, `Read D700~D719 from DNA:2, DA1:11`);
client.write('D700.0', [true, false, 1, 0, "true", true, 1, "1", "false", false, 0, "0", 0, 1, 0, 1], null, "write 1010 1111 0000 0101 to D700");
client.read('D700.0', 16, null, "read D700.0 ~ D700.15 - should contain 1010 1111 0000 0101");
const tag = { "source": "system-a", "sendto": "system-b" };
getStatus(tag);
function getStatus(_tag) {
console.log("Get PLC Status...")
client.status(function (err, msg) {
if (err) {
console.error(err, msg);
} else {
console.log("");
console.log("################ STATUS CALLBACK ####################");
console.log(msg.response.status, msg.response.mode, msg.response);
console.log("#####################################################");
console.log("");
}
}, _tag);
}
setTimeout(() => {
console.log("Request PLC change to STOP mode...")
client.stop((err, msg) => {
if (err) {
console.error(err)
} else {
console.log("* PLC should be stopped - check next STATUS CALLBACK")
setTimeout(() => {
getStatus();
}, 150);
}
})
}, 500);
setTimeout(() => {
console.log("Request PLC change to RUN mode...")
client.run((err, msg) => {
if (err) {
console.error(err)
} else {
console.log("* PLC should be running - check next STATUS CALLBACK")
setTimeout(() => {
getStatus();
}, 150);
}
})
}, 2000);
});
Multiple Clients
TODO: Test and update this demo following v0.2.0 breaking changes
Example of instantiating multiple objects to allow for asynchronous communications. Because this code doesn't wait for a response from any client before sending/receiving packets it is incredibly fast. In this example we attempt to read a memory area from a list of remote hosts. Each command will either return with a response or timeout. Every transaction will be recorded to the responses
array with the ip
as a key and the msg.response.values
as the associated value.
If a timeout occurs and you have provided a callback, the msg.timeout
flag will be set.
If a timeout occurs and you have not provided a callback, to can get a response by listening for 'timeout'
being emitted.
Once the size of the responses array is equal to the number of units we tried to communicate with we know we have gotten a response or timeout from every unit
const fins = require('omron-fins');
const debug = true;
const clients = [];
const responses = {};
const remoteHosts = [
{ KEY: "PLC1", IP:'192.168.0.1', OPTS: {DA1:1, SA1:99} },
{ KEY: "PLC2", IP:'192.168.0.2', OPTS: {DA1:2, SA1:99} },
{ KEY: "PLC3", IP:'192.168.0.3', OPTS: {DA1:3, SA1:99} },
];
const finished = function(responses) {
console.log("All responses and or timeouts received");
console.log(responses);
};
const pollUnits = function() {
const numberOfRemoteHosts = remoteHosts.length;
const options = {timeout:2000};
for (let remHost in remoteHosts) {
clients[remHost.KEY] = fins.FinsClient(9600,remHost.IP,remHost.OPTS);
clients[remHost.KEY].on('reply', function(err, msg) {
if(debug) console.log("Got reply from: ", msg.response.remotehost);
responses[msg.response.remotehost] = msg.response.values;
if(Object.keys(responses).length == numberOfRemoteHosts){
finished(responses);
}
});
clients[remHost.KEY].on('timeout',function(host, msg) {
responses[host] = null;
if(Object.keys(responses).length == numberOfRemoteHosts){
finished(responses);
};
if(debug) console.log("Got timeout from: ", host);
});
clients[remHost.KEY].on('error',function(err, msg) {
console.error(err)
});
clients[remHost.KEY].read('D00000',10);
};
};
console.log("Starting.....");
pollUnits();
Logging Data & Troubleshooting
If you have Wireshark installed it is very simple to analyse OMRON FINS/UDP traffic:
Simply select your network interface and then hit "Start"
Once in Wireshark change your filter to "omron"
Now you can examine each FINS packet individually