├── images
├── example.png
├── example0.png
├── example1.png
├── example2.png
└── example3.png
├── nodes
├── icons
│ ├── read.png
│ ├── read2.png
│ ├── write.png
│ └── connection.png
├── _parser.js
├── connection.js
├── fill.html
├── transfer.html
├── write.html
├── write.js
├── readMultiple.html
├── fill.js
├── transfer.js
├── read.html
├── connection.html
├── readMultiple.js
├── control.js
├── control.html
└── read.js
├── omron-fins.js
├── .eslintrc.js
├── package.json
├── LICENCE
├── .gitignore
├── supported-fins-commands.md
├── README.md
├── examples
├── read and write demo.json
├── control node demo.json
└── read and write typed values (requires buffer parser).json
└── connection_pool.js
/images/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steve-Mcl/node-red-contrib-omron-fins/HEAD/images/example.png
--------------------------------------------------------------------------------
/images/example0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steve-Mcl/node-red-contrib-omron-fins/HEAD/images/example0.png
--------------------------------------------------------------------------------
/images/example1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steve-Mcl/node-red-contrib-omron-fins/HEAD/images/example1.png
--------------------------------------------------------------------------------
/images/example2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steve-Mcl/node-red-contrib-omron-fins/HEAD/images/example2.png
--------------------------------------------------------------------------------
/images/example3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steve-Mcl/node-red-contrib-omron-fins/HEAD/images/example3.png
--------------------------------------------------------------------------------
/nodes/icons/read.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steve-Mcl/node-red-contrib-omron-fins/HEAD/nodes/icons/read.png
--------------------------------------------------------------------------------
/omron-fins.js:
--------------------------------------------------------------------------------
1 | var FinsClient = require('omron-fins').FinsClient;
2 |
3 | exports.FinsClient = FinsClient;
4 |
--------------------------------------------------------------------------------
/nodes/icons/read2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steve-Mcl/node-red-contrib-omron-fins/HEAD/nodes/icons/read2.png
--------------------------------------------------------------------------------
/nodes/icons/write.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steve-Mcl/node-red-contrib-omron-fins/HEAD/nodes/icons/write.png
--------------------------------------------------------------------------------
/nodes/icons/connection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Steve-Mcl/node-red-contrib-omron-fins/HEAD/nodes/icons/connection.png
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | commonjs: true,
5 | es2021: true,
6 | node: true,
7 | },
8 | extends: 'eslint:recommended',
9 | parserOptions: {
10 | ecmaVersion: 12,
11 | },
12 | rules: {
13 | indent: ['error', 4],
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-red-contrib-omron-fins",
3 | "version": "0.5.0",
4 | "author": {
5 | "name": "Steve-Mcl",
6 | "email": "44235289+Steve-Mcl@users.noreply.github.com"
7 | },
8 | "description": "OMRON FINS Ethernet protocol functions 'Read' and 'write' for communicating with OMRON PLCs from node-red",
9 | "keywords": [
10 | "node-red",
11 | "OMRON",
12 | "FINS",
13 | "PLC"
14 | ],
15 | "node-red": {
16 | "nodes": {
17 | "omronRead": "nodes/read.js",
18 | "omronReadMultiple": "nodes/readMultiple.js",
19 | "omronWrite": "nodes/write.js",
20 | "omronFile": "nodes/fill.js",
21 | "omronTransfer": "nodes/transfer.js",
22 | "omronConnection": "nodes/connection.js",
23 | "omronControl": "nodes/control.js"
24 | }
25 | },
26 | "license": "MIT",
27 | "dependencies": {
28 | "omron-fins": "0.5.0"
29 | },
30 | "devDependencies": {
31 | "eslint": "^7.29.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019, 2020, 2021 Steve-Mcl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/launch.json
2 | # Logs
3 | logs
4 | *.log
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 | *.pid.lock
14 |
15 | # Directory for instrumented libs generated by jscoverage/JSCover
16 | lib-cov
17 |
18 | # Coverage directory used by tools like istanbul
19 | coverage
20 |
21 | # nyc test coverage
22 | .nyc_output
23 |
24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
25 | .grunt
26 |
27 | # Bower dependency directory (https://bower.io/)
28 | bower_components
29 |
30 | # node-waf configuration
31 | .lock-wscript
32 |
33 | # Compiled binary addons (https://nodejs.org/api/addons.html)
34 | build/Release
35 |
36 | # Dependency directories
37 | node_modules/
38 | jspm_packages/
39 |
40 | # TypeScript v1 declaration files
41 | typings/
42 |
43 | # Optional npm cache directory
44 | .npm
45 |
46 | # Optional eslint cache
47 | .eslintcache
48 |
49 | # Optional REPL history
50 | .node_repl_history
51 |
52 | # Output of 'npm pack'
53 | *.tgz
54 |
55 | # Yarn Integrity file
56 | .yarn-integrity
57 |
58 | # dotenv environment variables file
59 | .env
60 |
61 | # next.js build output
62 | .next
63 | package-lock.json
64 |
--------------------------------------------------------------------------------
/nodes/_parser.js:
--------------------------------------------------------------------------------
1 | function kvMaker(fnAddressToString, address, values) {
2 | let kvs = {};
3 | if (values) {
4 | let iWD = 0;
5 | for (let x in values) {
6 | let item_addr = fnAddressToString(address, iWD, 0);
7 | kvs[item_addr] = values[x];
8 | iWD++;
9 | }
10 | }
11 | return kvs;
12 | }
13 |
14 | function kvMakerBits(fnAddressToString, address, values, asBool) {
15 | let kvs = {};
16 | let tc = false;
17 | if(address.MemoryArea == "C" || address.MemoryArea == "T") {
18 | tc = true;
19 | }
20 | if (values) {
21 | let iWD = 0;
22 | let iBit = 0;
23 | for (let x in values) {
24 | let item_addr = fnAddressToString(address, iWD, iBit);
25 | kvs[item_addr] = asBool ? !!values[x] : values[x];
26 | if(!tc) {
27 | iBit++;
28 | if (address.Bit + iBit > 15) {
29 | iBit = -address.Bit;
30 | iWD++;
31 | }
32 | } else {
33 | iWD++;
34 | }
35 | }
36 | }
37 | return kvs;
38 | }
39 |
40 | module.exports.keyValueMaker = kvMaker;
41 | module.exports.keyValueMakerBits = kvMakerBits;
--------------------------------------------------------------------------------
/supported-fins-commands.md:
--------------------------------------------------------------------------------
1 | # Supported Commands
2 |
3 | ## I/O memory area access
4 |
5 | [01,01] MEMORY AREA READ. Reads the contents of consecutive I/O memory area words.
6 |
7 | [01,02] MEMORY AREA WRITE. Writes the contents of consecutive I/O memory area words.
8 |
9 | [01,03] MEMORY AREA FILL. Writes the same data to the specified range of I/O memory area words.
10 |
11 | [01,04] MULTIPLE MEMORY AREA READ. Reads the contents of specified non-consecutive I/O memory area words.
12 |
13 | [01,05] MEMORY AREA TRANSFER. Copies the contents of consecutive I/O memory area words to another I/O memory area.
14 |
15 | ## Parameter area access
16 | TODO ~~[02,01] PARAMETER AREA READ. Reads the contents of the specified number of consecutive CPU Unit parameter area words starting from the specified word.~~
17 |
18 | TODO ~~[02,02] PARAMETER AREA WRITE. Writes data to the specified number of consecutive CPU Unit parameter area words starting from the specified word.~~
19 |
20 | TODO ~~[02,03] PARAMETER AREA FILL (CLEAR). Writes the same data to the specified range of parameter area words.~~
21 |
22 | ## Program area access
23 | TODO ~~[03,06] PROGRAM AREA READ. Reads the UM (User Memory) area.~~
24 |
25 | TODO ~~[03,07] PROGRAM AREA WRITE. Writes to the UM (User Memory) area.~~
26 |
27 | TODO ~~[03,08] PROGRAM AREA CLEAR. Clears the UM (User Memory) area.~~
28 |
29 |
30 | ## Operating mode changes
31 | [04,01] Set PLC Mode to RUN. Changes the CPU Unit’s operating mode to RUN or MONITOR.
32 |
33 | [04,02] Set PLC Mode to STOP. Changes the CPU Unit’s operating mode to PROGRAM.
34 |
35 | ## Machine configuration reading
36 | [05,01] CPU UNIT DATA READ. Reads CPU Unit data.
37 |
38 | TODO: ~~[05,02] CONNECTION DATA READ. Reads the model numbers of the device corresponding to addresses.~~
39 |
40 |
41 | ## Status reading
42 | [06,01] CPU UNIT STATUS READ. Reads the status of the CPU Unit.
43 |
44 | TODO: ~~[06,20] CYCLE TIME READ. Reads the maximum, minimum, and average cycle time.~~
45 |
46 |
47 | ## Time data access
48 | [07,01] CLOCK READ. Reads the present year, month, date, minute, second, and day of the week.
49 |
50 | [07,02] CLOCK WRITE. Changes the present year, month, date, minute, second, or day of the week.
51 |
52 |
--------------------------------------------------------------------------------
/nodes/connection.js:
--------------------------------------------------------------------------------
1 | /*
2 | MIT License
3 |
4 | Copyright (c) 2019, 2020, 2021 Steve-Mcl
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 | const {FinsConstants: constants, FinsDataUtils: {isInt}} = require('omron-fins');
26 | const connection_pool = require('../connection_pool.js');
27 |
28 | module.exports = function (RED) {
29 |
30 | /*!
31 | * Get value of environment variable.
32 | * @param {RED} _RED - accessing RED object
33 | * @param {String} name - name of variable
34 | * @return {String} value of env var / setting
35 | */
36 | function getSetting(name) {
37 | let result = RED.util.getObjectProperty(RED.settings, name);
38 | return result || process.env[name];
39 | }
40 |
41 | /**
42 | * Checks if a String contains any Environment Variable specifiers and returns
43 | * it with their values substituted in place.
44 | *
45 | * For example, if the env var `WHO` is set to `Joe`, the string `Hello ${WHO}!`
46 | * will return `Hello Joe!`.
47 | * @param {String} value - the string to parse
48 | * @param {Node} node - the node evaluating the property
49 | * @return {String} The parsed string
50 | */
51 | function resolveSetting(value) {
52 | try {
53 | if (!value) return value;
54 | if (typeof value != "string") return value;
55 | let result;
56 | if (/^\${[^}]+}$/.test(value)) {
57 | // ${ENV_VAR}
58 | let name = value.substring(2, value.length - 1);
59 | result = getSetting(name);
60 | } else {
61 | // FOO${ENV_VAR}BAR
62 | result = value.replace(/\${([^}]+)}/g, function (match, name) {
63 | return getSetting(name);
64 | });
65 | }
66 | return (result == null) ? value : result;
67 | } catch (error) {
68 | return value;
69 | }
70 | }
71 |
72 | function omronConnection(config) {
73 | RED.nodes.createNode(this, config);
74 | this.name = config.name;
75 | this.host = resolveSetting(config.host);
76 | this.port = resolveSetting(config.port);
77 | this.options = {};
78 | this.options.MODE = 'CJ';
79 | this.options.protocol = 'udp';
80 | if (config.protocolType == "env") {
81 | this.options.protocol = '';
82 | if(config.protocol) this.options.protocol = getSetting(config.protocol);
83 | } else {
84 | this.options.protocol = config.protocolType || "udp";
85 | }
86 | if(!config.MODEType && (config.MODE == 'CSCJ' || config.MODE == 'NJNX' || config.MODE == 'CV')) {
87 | config.MODEType = config.MODE.substr(0,2);
88 | }
89 | if (config.MODEType == 'env') {
90 | if(config.MODE) this.options.MODE = getSetting(config.MODE);
91 | } else {
92 | this.options.MODE = config.MODEType || 'CJ';
93 | }
94 | if(this.options.MODE) this.options.MODE = this.options.MODE.substr(0,2);
95 | this.options.ICF = isInt(resolveSetting(config.ICF), constants.DefaultFinsHeader.ICF);
96 | this.options.DNA = isInt(resolveSetting(config.DNA), constants.DefaultFinsHeader.DNA);
97 | this.options.DA1 = isInt(resolveSetting(config.DA1), constants.DefaultFinsHeader.DA1);
98 | this.options.DA2 = isInt(resolveSetting(config.DA2), constants.DefaultFinsHeader.DA2);
99 | this.options.SNA = isInt(resolveSetting(config.SNA), constants.DefaultFinsHeader.SNA);
100 | this.options.SA1 = isInt(resolveSetting(config.SA1), constants.DefaultFinsHeader.SA1);
101 | this.options.SA2 = isInt(resolveSetting(config.SA2), constants.DefaultFinsHeader.SA2);
102 | this.autoConnect = config.autoConnect == null ? true : config.autoConnect;
103 |
104 | // eslint-disable-next-line no-unused-vars
105 | this.on('close', function (done) {
106 | try {
107 | connection_pool.close(this, this);
108 | done && done();
109 | // eslint-disable-next-line no-empty
110 | } catch (error) { }
111 | });
112 | }
113 | RED.nodes.registerType("FINS Connection", omronConnection);
114 | };
115 |
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | node-red-contrib-omron-fins
2 | ===========================
3 |
4 | ## Overview
5 | This is a Node-RED node module to directly interface with OMRON PLCs over FINS Ethernet protocol.
6 | Tested on CV, CP, CS, CJ, NJ and NX PLCs (the ones with FINS support)
7 |
8 | Example flows have been included to help you get started.
9 | In the node-red editor, click the hamburger menu, select import then examples (or press ctrl+i)
10 |
11 | ### NODES
12 | * read - read 1 or more WORDs or bits
13 | * write - write 1 or more WORDs or bits
14 | * fill - fill 1 or more consecutive addresses with a value
15 | * read-multiple - read several disparate WORD or BIT address values
16 | * transfer - copy 1 or more data values from one memory area to another
17 | * control - this has the following functions...
18 | * Connect PLC
19 | * Disconnect PLC
20 | * Get PLC Status
21 | * Get PLC Unit Data
22 | * Set PLC STOP/PROGRAM mode
23 | * Set PLC RUN/MONITOR mode
24 | * Get Clock
25 | * Set Clock
26 |
27 | ## Version Update Notes
28 | This release (and possibly future releases up to V1.0.0) has breaking changes.
29 | Where possible, I make every attempt to keep things compatible, but as node-red improves (typedInput widgets for example) I too improve this node to make things easier or better. And sometimes, it becomes plain obvious a wrong decision was made that needs to be rectified before it becomes too late to change - it happens :)
30 | Semantic Versioning 2.0.0 will be followed after V1 however for now, where you see `V0.x.y`...
31 | * `x` = major / minor change
32 | * `y` = patch / bugfix
33 |
34 | ## Tips
35 | * On a reasonable VM, I have managed to achieve polling speeds less than 10ms (100+ reads per second) HOWEVER this really taxes NODE and Node-red. Through usage and testing, this node works very well polling 10 times per second (100ms poll time). This is often more than enough for UI type applications
36 | * Where possible, group your items together in the PLC and do 1 large read as opposed to multiple small reads. Reading 20 WDs from 1 location is much faster than reading 20 single items. An additional benefit of reading multiple items in one poll is the data is consistent (i.e. all values were read at on the same PLC poll)
37 |
38 | ## Prerequisites
39 |
40 | * node.js
41 | * Node-RED
42 | * git (optional) (Used for repository cloning/downloads)
43 |
44 | ## Credits
45 | Credit to [Patrick Servello (patrick--)](https://github.com/patrick--) for his original implementation of FINS
46 |
47 | ## Install
48 |
49 | ### Pallet Manager...
50 |
51 | The simplest method is to install via the pallet manager in node red. Simply search for **node-red-contrib-omron-fins** then click install
52 |
53 | ### Terminal...
54 |
55 | Run the following command in the root directory of your Node-RED install (usually `~/.node-red` or `%userprofile%\.node-red`)
56 |
57 | npm install node-red-contrib-omron-fins
58 |
59 | Or, install direct from github
60 |
61 | npm install steve-mcl/node-red-contrib-omron-fins
62 |
63 | Or clone to a local folder and install using NPM
64 |
65 | git clone https://github.com/Steve-Mcl/node-red-contrib-omron-fins.git
66 | npm install /source-path/node-red-contrib-omron-fins
67 |
68 |
69 |
70 | ## A working example...
71 |
72 | ### PLC Setup
73 | | Setting | Value |
74 | |----|------|
75 | | IP | 192.168.4.88 |
76 | | MASK | 255.255.255.0 |
77 | | Node | 88 |
78 | | UDP | 9600 |
79 |
80 |
81 | ### Node-red Setup
82 | | Setting | Value |
83 | |----|------|
84 | | IP | 192.168.4.179 |
85 | | MASK | 255.255.255.0 |
86 |
87 |
88 | ### FINS Connection Node Settings
89 | | Option | Value |
90 | |----|------|
91 | Host | 192.168.4.88
92 | Port | 9600
93 | MODE | NJ/NX
94 | ICF | 0x80
95 | DNA | 0
96 | DA1 | 88
97 | DA2 | 0
98 | SNA | 0
99 | SA1 | 179
100 | SA2 | 0
101 |
102 |
103 | 
104 |
105 | #### Other notes:
106 | * If the subnet mask is bigger than /24 (e.g. is bigger than 255.255.255.0) you might need to enter the IP and NODE number (of the node-red server) into the FINS **IP address table** so that the PLC understands which IP to respond to when responding to the SA1 NODE number
107 | * FINS works with PLC Addresses. NJ and NX PLCs do NOT have direct addresses to addresses like DM or E0_, E1_.
108 | * In order to use C Series addresses in an N Series PLC, you will need to create a variable in the PLC and set its `AT` property. E.G. If you want to read and write 40 WDs from E0_9000 ~ E0_9039 then you need to add a TAG like this `TAG_NAME ARRAY[0..39] Of WORD %E0_9000`
109 | 
110 |
111 |
112 | ## Data formats and conversion
113 |
114 | As I use multiple PLCs and didn't want to write boolean / 32bit / float / double functionality into each node (it's best to keep things atomic and good at what they do) so I wrote a separate second node for handling data conversions.
115 |
116 | This node "node-red-contrib-buffer-parser" https://flows.nodered.org/node/node-red-contrib-buffer-parser is capable of pretty much anything you will need for this or any PLC that returns 16bit data or a NodeJS Buffer.
117 |
118 | In essence, you pull a bunch of data from the plc in one go & convert it all in the buffer-parser node to almost any format you could wish for (bits, floats, 32bit signed / unsigned, byteswapping etc etc). It can do 1 or many conversions all at once. It can send a [grouped item](https://github.com/Steve-Mcl/node-red-contrib-buffer-parser#example-2---array-of-data-to-an-named-objects) (object) or individual items [with a `topic`](https://github.com/Steve-Mcl/node-red-contrib-buffer-parser#example-1---array-of-data-to-mqtt-multiple-topics--payloads) ready for pushing your data directly from the PLC to MQTT.
119 |
--------------------------------------------------------------------------------
/examples/read and write demo.json:
--------------------------------------------------------------------------------
1 | [{"id":"b1fd5e4f4cca365a","type":"FINS Read Multiple","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"T0,T0.x,C0,C0.x,D100,D100.0,CIO0,CIO0.0,A600,A600.0,A600.1,A600.2,A600.3,W0,W0.0,H0,H0.0","msgPropertyType":"msg","msgProperty":"payload","outputFormatType":"unsignedkv","outputFormat":"","x":360,"y":1800,"wires":[["7092f815dc1a456d"]]},{"id":"1eed7915c8209ac7","type":"FINS Fill","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"A600","valueType":"num","value":"573","countType":"num","count":"10","msgPropertyType":"msg","msgProperty":"payload","x":350,"y":1680,"wires":[[]]},{"id":"283ba0fc982f3ca4","type":"FINS Transfer","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"A600","address2Type":"str","address2":"A610","countType":"num","count":"10","msgPropertyType":"msg","msgProperty":"payload","x":370,"y":1740,"wires":[[]]},{"id":"7092f815dc1a456d","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":610,"y":1800,"wires":[]},{"id":"81d8ad6d6aab26aa","type":"FINS Write","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"A600","dataType":"msg","data":"payload","msgPropertyType":"msg","msgProperty":"payload","x":450,"y":1420,"wires":[[]]},{"id":"77ae72b79c524f5e","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[1,2,3,4,5]","payloadType":"json","x":180,"y":1400,"wires":[["81d8ad6d6aab26aa"]]},{"id":"cba8f8b0c0d2deff","type":"comment","z":"bec69dbd.8d622","name":"Demo of READ, WRITE, FILL, TRANSFER and READ MULTIPLE","info":"","x":330,"y":1200,"wires":[]},{"id":"e53bd28780415061","type":"FINS Read","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"A600","countType":"num","count":"20","msgPropertyType":"msg","msgProperty":"payload","outputFormatType":"unsignedkv","outputFormat":"","x":340,"y":1620,"wires":[["3e274f356f5fb5c1"]]},{"id":"3e274f356f5fb5c1","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":610,"y":1620,"wires":[]},{"id":"7e430be613738235","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[6,7,8,9,10]","payloadType":"json","x":180,"y":1440,"wires":[["81d8ad6d6aab26aa"]]},{"id":"d114f9437d86a527","type":"FINS Read","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"A600.00","countType":"num","count":"32","msgPropertyType":"msg","msgProperty":"payload","outputFormatType":"unsignedkv","outputFormat":"","x":350,"y":1560,"wires":[["d0874f97aad2b013"]]},{"id":"d0874f97aad2b013","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":610,"y":1560,"wires":[]},{"id":"01e3e155bf34a315","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":1560,"wires":[["d114f9437d86a527"]],"l":false},{"id":"d92df41854f0bd1d","type":"FINS Write","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"A600.00","dataType":"msg","data":"payload","msgPropertyType":"msg","msgProperty":"payload","x":440,"y":1500,"wires":[[]]},{"id":"08aeecde7b1c94ad","type":"inject","z":"bec69dbd.8d622","name":"[true,false, true,false]","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[true,false, true,false]","payloadType":"json","x":220,"y":1480,"wires":[["d92df41854f0bd1d"]]},{"id":"3034dc89f9062ca0","type":"inject","z":"bec69dbd.8d622","name":"[0,0,1,1]","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[0,0,1,1]","payloadType":"json","x":180,"y":1520,"wires":[["d92df41854f0bd1d"]]},{"id":"4b72548e180e33a3","type":"inject","z":"bec69dbd.8d622","name":"Connect with options (UDP)","props":[{"p":"topic","vt":"str"},{"p":"options","v":"{\"host\":\"192.168.1.120\",\"port\":9600,\"protocol\":\"udp\",\"DNA\":0,\"DA1\":120,\"DA2\":0,\"SNA\":0,\"SA1\":36,\"SA2\":0}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"connect","x":240,"y":1240,"wires":[["6c5b4c20c9310906"]]},{"id":"6c5b4c20c9310906","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"json","clock":"","connectOptionsType":"msg","connectOptions":"options","msgPropertyType":"msg","msgProperty":"payload","commandType":"msg","command":"topic","x":470,"y":1260,"wires":[[]]},{"id":"74713e2c39275ad3","type":"inject","z":"bec69dbd.8d622","name":"Disconnect from PLC","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"disconnect","x":220,"y":1280,"wires":[["6c5b4c20c9310906"]]},{"id":"4607061e380aea44","type":"catch","z":"bec69dbd.8d622","name":"","scope":["b1fd5e4f4cca365a","1eed7915c8209ac7","283ba0fc982f3ca4","81d8ad6d6aab26aa","e53bd28780415061","d114f9437d86a527","d92df41854f0bd1d","6c5b4c20c9310906"],"uncaught":false,"x":450,"y":1340,"wires":[["6d828b9d0ef44fbe"]]},{"id":"6d828b9d0ef44fbe","type":"debug","z":"bec69dbd.8d622","name":"Errors","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":610,"y":1340,"wires":[]},{"id":"e34dcce21dae26c4","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":1620,"wires":[["e53bd28780415061"]],"l":false},{"id":"6fd981ea6c965037","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":1680,"wires":[["1eed7915c8209ac7"]],"l":false},{"id":"062ceedf2453f42f","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":1740,"wires":[["283ba0fc982f3ca4"]],"l":false},{"id":"2ccc1757724d49ab","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":1800,"wires":[["b1fd5e4f4cca365a"]],"l":false},{"id":"97172f6a093bad4f","type":"FINS Connection","name":"PLC1","host":"127.0.0.1","port":"9600","MODE":"CSCJ","protocol":"","protocolType":"env","ICF":"128","DNA":"","DA1":"","DA2":"","SNA":"","SA1":"","SA2":"","autoConnect":false}]
--------------------------------------------------------------------------------
/nodes/fill.html:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
91 |
92 |
122 |
123 |
162 |
--------------------------------------------------------------------------------
/nodes/transfer.html:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
90 |
91 |
121 |
122 |
172 |
--------------------------------------------------------------------------------
/nodes/write.html:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
79 |
80 |
105 |
106 |
168 |
--------------------------------------------------------------------------------
/examples/control node demo.json:
--------------------------------------------------------------------------------
1 | [{"id":"beb6e99a0b8ad072","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"json","clock":"","connectOptionsType":"msg","connectOptions":"options","msgPropertyType":"msg","msgProperty":"payload","commandType":"msg","command":"topic","x":490,"y":840,"wires":[["dc0a0d6f2be841b6"]]},{"id":"ee37e473dbf2cf33","type":"inject","z":"bec69dbd.8d622","name":"Set PLC mode STOP","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"stop","x":220,"y":940,"wires":[["beb6e99a0b8ad072"]]},{"id":"9beef66a64ef3f68","type":"inject","z":"bec69dbd.8d622","name":"Set PLC mode RUN","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"run","x":210,"y":980,"wires":[["beb6e99a0b8ad072"]]},{"id":"dc0a0d6f2be841b6","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":630,"y":840,"wires":[]},{"id":"7638b445906d0e4f","type":"inject","z":"bec69dbd.8d622","name":"Get PLC STATUS","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"status","x":210,"y":800,"wires":[["beb6e99a0b8ad072"]]},{"id":"201d6f19ee51c7c7","type":"inject","z":"bec69dbd.8d622","name":"Read PLC clock","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"clock-read","x":200,"y":840,"wires":[["beb6e99a0b8ad072"]]},{"id":"7253be4a4afd792f","type":"inject","z":"bec69dbd.8d622","name":"Read CPU Unit data","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"cpu-unit-data-read","x":210,"y":880,"wires":[["beb6e99a0b8ad072"]]},{"id":"ae9e858706b45d7b","type":"inject","z":"bec69dbd.8d622","name":"Connect with options (UDP)","props":[{"p":"topic","vt":"str"},{"p":"options","v":"{\"host\":\"192.168.1.120\",\"port\":9600,\"protocol\":\"udp\",\"DNA\":0,\"DA1\":120,\"DA2\":0,\"SNA\":0,\"SA1\":36,\"SA2\":0}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"connect","x":240,"y":660,"wires":[["beb6e99a0b8ad072"]]},{"id":"817cb22e32480395","type":"inject","z":"bec69dbd.8d622","name":"Connect with options (TCP)","props":[{"p":"topic","vt":"str"},{"p":"options","v":"{\"host\":\"192.168.1.120\",\"port\":9700,\"protocol\":\"tcp\",\"DNA\":0,\"DA1\":120,\"DA2\":0,\"SNA\":0,\"SA1\":38,\"SA2\":0}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"connect","x":240,"y":700,"wires":[["beb6e99a0b8ad072"]]},{"id":"4cdc105ce9e8ddf8","type":"comment","z":"bec69dbd.8d622","name":"Demo of FINS Control node (adjust the connection options in the INJECT node)","info":"","x":380,"y":300,"wires":[]},{"id":"be70389b5d1029b8","type":"inject","z":"bec69dbd.8d622","name":"Disconnect from PLC","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"disconnect","x":220,"y":740,"wires":[["beb6e99a0b8ad072"]]},{"id":"2cb907c650d49fba","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"json","clock":"","connectOptionsType":"msg","connectOptions":"options","msgPropertyType":"msg","msgProperty":"payload","commandType":"status","command":"","x":430,"y":420,"wires":[["79101fd5b363a15a"]]},{"id":"e940482e8fed563b","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"json","clock":"","connectOptionsType":"msg","connectOptions":"options","msgPropertyType":"msg","msgProperty":"payload","commandType":"cpu-unit-data-read","command":"","x":470,"y":480,"wires":[["1716c96fededbe07"]]},{"id":"a795bf58525b7d23","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"json","clock":"","connectOptionsType":"msg","connectOptions":"options","msgPropertyType":"msg","msgProperty":"payload","commandType":"stop","command":"","x":430,"y":540,"wires":[["7b1541ec93480dde"]]},{"id":"0a17c8bc4d46b3a3","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"json","clock":"","connectOptionsType":"msg","connectOptions":"options","msgPropertyType":"msg","msgProperty":"payload","commandType":"run","command":"","x":430,"y":600,"wires":[["259f167c94ea437d"]]},{"id":"636a24ac588c7ab3","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"msg","clock":"","connectOptionsType":"none","connectOptions":"","msgPropertyType":"msg","msgProperty":"payload","commandType":"clock-read","command":"","x":250,"y":1080,"wires":[["45b788d342ca30e9"]]},{"id":"03474ecfb0699167","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":1080,"wires":[["636a24ac588c7ab3"]],"l":false},{"id":"f769a488db5bb484","type":"debug","z":"bec69dbd.8d622","name":"","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":630,"y":1080,"wires":[]},{"id":"29565e6f316ac72a","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"msg","clock":"clock","connectOptionsType":"none","connectOptions":"","msgPropertyType":"msg","msgProperty":"payload","commandType":"clock-write","command":"","x":430,"y":1140,"wires":[[]]},{"id":"725d5cc9721ef0c5","type":"function","z":"bec69dbd.8d622","name":"get time","func":"\nvar now = new Date();\nmsg.clock = { \n \"year\": now.getFullYear() - 2000,\n \"month\": now.getMonth()+1,\n \"day\": now.getDate(),\n \"hour\": now.getHours(),\n \"minute\": now.getMinutes(),\n \"second\": now.getSeconds()\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":240,"y":1140,"wires":[["29565e6f316ac72a"]]},{"id":"45b788d342ca30e9","type":"function","z":"bec69dbd.8d622","name":"make time","func":"msg.payload = new Date(msg.payload.year+2000, msg.payload.month, msg.payload.day, msg.payload.hour, msg.payload.minute, msg.payload.second);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":1080,"wires":[["f769a488db5bb484"]]},{"id":"3c15eed2c14bf843","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":1140,"wires":[["725d5cc9721ef0c5"]],"l":false},{"id":"76ce2f278c7584f6","type":"comment","z":"bec69dbd.8d622","name":"Demo of CLOCK READ and CLOCK WRITE","info":"","x":270,"y":1040,"wires":[]},{"id":"b7840778807316da","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":420,"wires":[["2cb907c650d49fba"]],"l":false},{"id":"79101fd5b363a15a","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":630,"y":420,"wires":[]},{"id":"1716c96fededbe07","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":626.8958129882812,"y":479.8888854980469,"wires":[]},{"id":"7b1541ec93480dde","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":630,"y":540,"wires":[]},{"id":"259f167c94ea437d","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":630,"y":600,"wires":[]},{"id":"915f418e2ec2581d","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":480,"wires":[["e940482e8fed563b"]],"l":false},{"id":"f3237b3b8b2417d8","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":540,"wires":[["a795bf58525b7d23"]],"l":false},{"id":"2fd9f36935c44632","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":135,"y":600,"wires":[["0a17c8bc4d46b3a3"]],"l":false},{"id":"a6b1e72906e48a9b","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"json","clock":"","connectOptionsType":"msg","connectOptions":"options","msgPropertyType":"msg","msgProperty":"payload","commandType":"connect","command":"","x":440,"y":360,"wires":[["4c8eab76aaf43383"]]},{"id":"4c8eab76aaf43383","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"msg","x":630,"y":360,"wires":[]},{"id":"0bc45df8169c3268","type":"inject","z":"bec69dbd.8d622","name":"Connect with options (UDP)","props":[{"p":"topic","vt":"str"},{"p":"options","v":"{\"host\":\"192.168.1.120\",\"port\":9600,\"protocol\":\"udp\",\"DNA\":0,\"DA1\":120,\"DA2\":0,\"SNA\":0,\"SA1\":36,\"SA2\":0}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"connect","x":240,"y":360,"wires":[["a6b1e72906e48a9b"]]},{"id":"97172f6a093bad4f","type":"FINS Connection","name":"PLC1","host":"127.0.0.1","port":"9600","MODE":"CSCJ","protocol":"","protocolType":"env","ICF":"128","DNA":"","DA1":"","DA2":"","SNA":"","SA1":"","SA2":"","autoConnect":false}]
--------------------------------------------------------------------------------
/examples/read and write typed values (requires buffer parser).json:
--------------------------------------------------------------------------------
1 | [{"id":"42d5a6e274f7c8ba","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"D0","payload":"hello","payloadType":"str","x":1120,"y":560,"wires":[["d5dfe198732f152b"]]},{"id":"7faaead01abfaae2","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"D0","payload":"bye","payloadType":"str","x":1110,"y":600,"wires":[["d5dfe198732f152b"]]},{"id":"4abffcb031ca4f97","type":"buffer-parser","z":"bec69dbd.8d622","name":"buffer to uint16","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint16be","name":"item1","offset":0,"length":-1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"value","resultTypeType":"output","multipleResult":true,"fanOutMultipleResult":false,"setTopic":false,"outputs":1,"x":1500,"y":580,"wires":[["be859f782df301ff"]]},{"id":"be859f782df301ff","type":"FINS Write","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"D0","dataType":"msg","data":"payload","msgPropertyType":"msg","msgProperty":"payload","x":1820,"y":580,"wires":[[]]},{"id":"1753308c62c1cfc5","type":"FINS Read","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"D0","countType":"num","count":"10","msgPropertyType":"msg","msgProperty":"payload","outputFormatType":"buffer","outputFormat":"","x":1290,"y":660,"wires":[["b64d1794f9792e42"]]},{"id":"bd2c109e2a291c34","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1075,"y":660,"wires":[["1753308c62c1cfc5"]],"l":false},{"id":"b64d1794f9792e42","type":"buffer-parser","z":"bec69dbd.8d622","name":"buffer to string","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"string","name":"item1","offset":0,"length":10,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"value","resultTypeType":"output","multipleResult":true,"fanOutMultipleResult":false,"setTopic":false,"outputs":1,"x":1500,"y":660,"wires":[["2ea0dc6b03fe1c1d"]]},{"id":"2ea0dc6b03fe1c1d","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1830,"y":660,"wires":[]},{"id":"627592b28dc1622f","type":"comment","z":"bec69dbd.8d622","name":"Demo of writing and reading strings using buffer parser and buffer maker","info":"","x":1290,"y":520,"wires":[]},{"id":"4da45c846c8989c4","type":"inject","z":"bec69dbd.8d622","name":"data1","props":[{"p":"payload"},{"p":"payload.RPM","v":"2401.7","vt":"num"},{"p":"payload.setRPM","v":"2400.0","vt":"num"},{"p":"payload.faultCount","v":"3","vt":"num"},{"p":"payload.lastFault","v":"stalled","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{}","payloadType":"json","x":1110,"y":780,"wires":[["b83076ff41e56d08"]]},{"id":"9127577564dd4e8c","type":"buffer-parser","z":"bec69dbd.8d622","name":"buffer to uint16","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint16be","name":"item1","offset":0,"length":-1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"value","resultTypeType":"output","multipleResult":true,"fanOutMultipleResult":false,"setTopic":false,"outputs":1,"x":1500,"y":800,"wires":[["05c7ef380f32f35f","c5badcd32b5bb57c"]]},{"id":"05c7ef380f32f35f","type":"FINS Write","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"D0","dataType":"msg","data":"payload","msgPropertyType":"msg","msgProperty":"payload","x":1820,"y":800,"wires":[[]]},{"id":"06e75451601b738f","type":"FINS Read","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","addressType":"str","address":"D0","countType":"num","count":"12","msgPropertyType":"msg","msgProperty":"payload","outputFormatType":"buffer","outputFormat":"","x":1290,"y":920,"wires":[["68e5475aa6c6ef6f","621d4244f706def0"]]},{"id":"013ddc4defef660a","type":"inject","z":"bec69dbd.8d622","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1075,"y":920,"wires":[["06e75451601b738f"]],"l":false},{"id":"5298d17f14f7dd5d","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1830,"y":920,"wires":[]},{"id":"96b21d23f204661b","type":"comment","z":"bec69dbd.8d622","name":"Demo of writing floats, ints, bytes from an object then reading them back from PLC into correct types and back into named properties","info":"","x":1480,"y":740,"wires":[]},{"id":"b83076ff41e56d08","type":"buffer-maker","z":"bec69dbd.8d622","name":"","specification":"spec","specificationType":"ui","items":[{"name":"RPM","type":"floatbe","length":1,"dataType":"msg","data":"payload.RPM"},{"name":"setRPM","type":"floatbe","length":1,"dataType":"msg","data":"payload.setRPM"},{"name":"faultCount","type":"uint16be","length":1,"dataType":"msg","data":"payload.faultCount"},{"name":"status","type":"byte","length":1,"dataType":"msg","data":"payload.status"},{"name":"lastFault","type":"string","length":10,"dataType":"msg","data":"payload.lastFault"}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","x":1290,"y":800,"wires":[["9127577564dd4e8c"]]},{"id":"c5badcd32b5bb57c","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1830,"y":860,"wires":[]},{"id":"68e5475aa6c6ef6f","type":"buffer-parser","z":"bec69dbd.8d622","name":"buffer to key/value object","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"floatbe","name":"RPM","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"floatbe","name":"setRPM","offset":4,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint16be","name":"faultCount","offset":8,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"byte","name":"status","offset":10,"length":1,"offsetbit":0,"scale":"1","mask":"0x7"},{"type":"string","name":"lastFault","offset":11,"length":10,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"output","multipleResult":false,"fanOutMultipleResult":false,"setTopic":false,"outputs":1,"x":1530,"y":920,"wires":[["5298d17f14f7dd5d"]]},{"id":"3401b038b01c6511","type":"inject","z":"bec69dbd.8d622","name":"data2","props":[{"p":"payload"},{"p":"payload.RPM","v":"97.193","vt":"num"},{"p":"payload.setRPM","v":"100","vt":"num"},{"p":"payload.faultCount","v":"19","vt":"num"},{"p":"payload.status","v":"1","vt":"num"},{"p":"payload.lastFault","v":"over speed","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{}","payloadType":"json","x":1110,"y":820,"wires":[["b83076ff41e56d08"]]},{"id":"732501c0d62d1e23","type":"catch","z":"bec69dbd.8d622","name":"","scope":["4abffcb031ca4f97","be859f782df301ff","1753308c62c1cfc5","b64d1794f9792e42","9127577564dd4e8c","05c7ef380f32f35f","06e75451601b738f","b83076ff41e56d08","68e5475aa6c6ef6f","93ea5183011f2602","d5dfe198732f152b"],"uncaught":false,"x":1620,"y":420,"wires":[["10abc5163d72e940"]]},{"id":"10abc5163d72e940","type":"debug","z":"bec69dbd.8d622","name":"","active":false,"tosidebar":true,"console":true,"tostatus":true,"complete":"true","targetType":"full","statusVal":"error.message","statusType":"msg","x":1810,"y":420,"wires":[]},{"id":"621d4244f706def0","type":"debug","z":"bec69dbd.8d622","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1350,"y":980,"wires":[]},{"id":"9104ed3b25bfce59","type":"comment","z":"bec69dbd.8d622","name":"NOTE: This demo requires \"node-red-contrib-buffer-parser\" to be installed","info":"","x":1300,"y":360,"wires":[]},{"id":"cd53189b83479520","type":"inject","z":"bec69dbd.8d622","name":"Connect with options (UDP)","props":[{"p":"topic","vt":"str"},{"p":"options","v":"{\"host\":\"192.168.1.120\",\"port\":9600,\"protocol\":\"udp\",\"DNA\":0,\"DA1\":120,\"DA2\":0,\"SNA\":0,\"SA1\":36,\"SA2\":0}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"connect","x":1180,"y":400,"wires":[["93ea5183011f2602"]]},{"id":"93ea5183011f2602","type":"FINS Control","z":"bec69dbd.8d622","name":"","connection":"97172f6a093bad4f","clockType":"json","clock":"","connectOptionsType":"msg","connectOptions":"options","msgPropertyType":"msg","msgProperty":"payload","commandType":"msg","command":"topic","x":1410,"y":420,"wires":[[]]},{"id":"d5dfe198732f152b","type":"buffer-maker","z":"bec69dbd.8d622","name":"","specification":"spec","specificationType":"ui","items":[{"name":"RPM","type":"string","length":10,"dataType":"msg","data":"payload"}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","x":1310,"y":580,"wires":[["4abffcb031ca4f97"]]},{"id":"1c20391714e22f7a","type":"inject","z":"bec69dbd.8d622","name":"Disconnect from PLC","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"disconnect","x":1200,"y":440,"wires":[["93ea5183011f2602"]]},{"id":"97172f6a093bad4f","type":"FINS Connection","name":"PLC1","host":"127.0.0.1","port":"9600","MODE":"CSCJ","protocol":"","protocolType":"env","ICF":"128","DNA":"","DA1":"","DA2":"","SNA":"","SA1":"","SA2":"","autoConnect":false}]
--------------------------------------------------------------------------------
/nodes/write.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-inner-declarations */
2 | /*
3 | MIT License
4 |
5 | Copyright (c) 2019, 2020, 2021 Steve-Mcl
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | */
25 |
26 | module.exports = function (RED) {
27 | const connection_pool = require('../connection_pool.js');
28 | function omronWrite(config) {
29 | RED.nodes.createNode(this, config);
30 | const node = this;
31 | const cmdExpected = '0102';
32 | node.name = config.name;
33 | node.connection = config.connection;
34 | node.address = config.address || 'topic';
35 | node.addressType = config.addressType || 'msg';
36 | node.data = config.data || 'payload';
37 | node.dataType = config.dataType || 'msg';
38 | node.msgProperty = config.msgProperty || 'payload';
39 | node.msgPropertyType = config.msgPropertyType || 'str';
40 | node.connectionConfig = RED.nodes.getNode(node.connection);
41 |
42 | /* **************** Listeners **************** */
43 | function onClientError(error, seq) {
44 | node.status({ fill: 'red', shape: 'ring', text: 'error' });
45 | node.error(error, (seq && seq.tag ? seq.tag : seq));
46 | }
47 | function onClientFull() {
48 | node.throttleUntil = Date.now() + 1000;
49 | node.warn('Client buffer is saturated. Requests for the next 1000ms will be ignored. Consider reducing poll rate of operations to this connection.');
50 | node.status({ fill: 'red', shape: 'dot', text: 'queue full' });
51 | }
52 | // eslint-disable-next-line no-unused-vars
53 | function onClientOpen(remoteInfo) {
54 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
55 | }
56 | function onClientClose() {
57 | node.status({ fill: 'yellow', shape: 'dot', text: 'not connected' });
58 | }
59 | // eslint-disable-next-line no-unused-vars
60 | function onClientInit(options) {
61 | node.status({ fill: 'grey', shape: 'dot', text: 'initialised' });
62 | }
63 |
64 | function removeAllListeners() {
65 | if(node.client) {
66 | node.client.off('error', onClientError);
67 | node.client.off('full', onClientFull);
68 | node.client.off('open', onClientOpen);
69 | node.client.off('close', onClientClose);
70 | node.client.off('initialised', onClientInit);
71 | }
72 | }
73 |
74 | /* **************** Node status **************** */
75 | function nodeStatusError(err, msg, statusText) {
76 | if (err) {
77 | node.error(err, msg);
78 | } else {
79 | node.error(statusText, msg);
80 | }
81 | node.status({ fill: 'red', shape: 'dot', text: statusText });
82 | }
83 |
84 | if (this.connectionConfig) {
85 | node.status({ fill: 'yellow', shape: 'ring', text: 'initialising' });
86 | if(node.client) {
87 | node.client.removeAllListeners();
88 | }
89 | node.client = connection_pool.get(node, node.connectionConfig);
90 | this.client.on('error', onClientError);
91 | this.client.on('full', onClientFull);
92 | this.client.on('open', onClientOpen);
93 | this.client.on('close', onClientClose);
94 | this.client.on('initialised', onClientInit);
95 |
96 | function finsReply(err, sequence) {
97 | if (!err && !sequence) {
98 | return;
99 | }
100 | const origInputMsg = (sequence && sequence.tag) || {};
101 | try {
102 | if(sequence) {
103 | if (err || sequence.error) {
104 | nodeStatusError(((err && err.message) || "error"), origInputMsg, ((err && err.message) || "error") );
105 | return;
106 | }
107 | if (sequence.timeout) {
108 | nodeStatusError('timeout', origInputMsg, 'timeout');
109 | return;
110 | }
111 | if (sequence.response && sequence.sid != sequence.response.sid) {
112 | nodeStatusError(`SID does not match! My SID: ${sequence.sid}, reply SID:${sequence.response.sid}`, origInputMsg, 'Incorrect SID');
113 | return;
114 | }
115 | }
116 | if (!sequence || !sequence.response || sequence.response.endCode !== '0000' || sequence.response.command.commandCode !== cmdExpected) {
117 | let ecd = 'bad response';
118 | if (sequence.response && sequence.response.command.commandCode !== cmdExpected)
119 | ecd = `Unexpected response. Expected command '${cmdExpected}' but received '${sequence.response.command}'`;
120 | else if (sequence.response && sequence.response.endCodeDescription)
121 | ecd = sequence.response.endCodeDescription;
122 | nodeStatusError(`Response is NG! endCode: ${sequence.response ? sequence.response.endCode : '????'}, endCodeDescription:${sequence.response ? sequence.response.endCodeDescription : ''}`, origInputMsg, ecd);
123 | return;
124 | }
125 |
126 | //set the output property
127 | RED.util.setObjectProperty(origInputMsg, node.msgProperty, sequence.sid || 0, true);
128 |
129 | //include additional detail in msg.fins
130 | origInputMsg.fins = {};
131 | origInputMsg.fins.name = node.name; //node name for user logging / routing
132 | origInputMsg.fins.request = {
133 | command: sequence.request.command,
134 | options: sequence.request.options,
135 | address: sequence.request.address,
136 | dataToBeWritten: sequence.request.dataToBeWritten,
137 | sid: sequence.request.sid,
138 | };
139 | origInputMsg.fins.response = sequence.response;
140 | origInputMsg.fins.stats = sequence.stats;
141 | origInputMsg.fins.createTime = sequence.createTime;
142 | origInputMsg.fins.replyTime = sequence.replyTime;
143 | origInputMsg.fins.timeTaken = sequence.timeTaken;
144 |
145 | node.status({ fill: 'green', shape: 'dot', text: 'done' });
146 | node.send(origInputMsg);
147 | } catch (error) {
148 | nodeStatusError(error, origInputMsg, 'error');
149 |
150 | }
151 | }
152 |
153 | this.on('close', function (done) {
154 | removeAllListeners();
155 | if (done) done();
156 | });
157 |
158 | this.on('input', function (msg) {
159 | if (node.throttleUntil) {
160 | if (node.throttleUntil > Date.now()) return; //throttled
161 | node.throttleUntil = null; //throttle time over
162 | }
163 | node.status({});//clear status
164 |
165 | if (msg.disconnect === true || msg.topic === 'disconnect') {
166 | node.client.disconnect();
167 | return;
168 | } else if (msg.connect === true || msg.topic === 'connect') {
169 | node.client.connect();
170 | return;
171 | }
172 |
173 | /* **************** Get address Parameter **************** */
174 | const address = RED.util.evaluateNodeProperty(node.address, node.addressType, node, msg);
175 |
176 | /* **************** Get data Parameter **************** */
177 | let data;
178 | RED.util.evaluateNodeProperty(node.data, node.dataType, node, msg, function (err, value) {
179 | if (err) {
180 | nodeStatusError(err, msg, 'invalid data');
181 | return;//halt flow!
182 | } else {
183 | data = value;
184 | }
185 | });
186 |
187 | if (!address || typeof address != 'string') {
188 | nodeStatusError(null, msg, 'address is not valid');
189 | return;
190 | }
191 | if (data == null) {
192 | nodeStatusError(null, msg, 'data is not valid');
193 | return;
194 | }
195 |
196 | const opts = msg.finsOptions || {};
197 | let sid;
198 | try {
199 | opts.callback = finsReply;
200 | sid = node.client.write(address, data, opts, msg);
201 | if (sid > 0) node.status({ fill: 'yellow', shape: 'ring', text: 'write' });
202 | } catch (error) {
203 | if(error.message == "not connected") {
204 | node.status({ fill: 'yellow', shape: 'dot', text: error.message });
205 | } else {
206 | nodeStatusError(error, msg, 'error');
207 | const debugMsg = {
208 | info: "write.js-->on 'input' - try this.client.write(address, data, finsReply)",
209 | connection: `host: ${node.connectionConfig.host}, port: ${node.connectionConfig.port}`,
210 | sid: sid,
211 | address: address,
212 | data: data,
213 | error: error
214 | };
215 | node.debug(debugMsg);
216 | }
217 | return;
218 | }
219 | });
220 | if(node.client && node.client.connected) {
221 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
222 | } else {
223 | node.status({ fill: 'grey', shape: 'ring', text: 'initialised' });
224 | }
225 |
226 | } else {
227 | node.status({ fill: 'red', shape: 'dot', text: 'Connection config missing' });
228 | }
229 |
230 | }
231 | RED.nodes.registerType('FINS Write', omronWrite);
232 | };
233 |
234 |
--------------------------------------------------------------------------------
/nodes/readMultiple.html:
--------------------------------------------------------------------------------
1 |
24 |
25 |
37 |
38 |
137 |
165 |
166 |
--------------------------------------------------------------------------------
/nodes/fill.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-inner-declarations */
2 | /*
3 | MIT License
4 |
5 | Copyright (c) 2019, 2020, 2021 Steve-Mcl
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | */
25 |
26 | module.exports = function (RED) {
27 | const connection_pool = require('../connection_pool.js');
28 | function omronFill(config) {
29 | RED.nodes.createNode(this, config);
30 | const node = this;
31 | const cmdExpected = '0103';
32 | node.name = config.name;
33 | node.connection = config.connection;
34 | node.address = config.address || '';
35 | node.addressType = config.addressType || 'str';
36 | node.count = config.count || '1';
37 | node.countType = config.countType || 'num';
38 | node.value = config.value || '0';
39 | node.valueType = config.valueType || 'num';
40 | node.msgProperty = config.msgProperty || 'payload';
41 | node.msgPropertyType = config.msgPropertyType || 'str';
42 | node.connectionConfig = RED.nodes.getNode(node.connection);
43 |
44 | /* **************** Listeners **************** */
45 | function onClientError(error, seq) {
46 | node.status({ fill: 'red', shape: 'ring', text: 'error' });
47 | node.error(error, (seq && seq.tag ? seq.tag : seq));
48 | }
49 | function onClientFull() {
50 | node.throttleUntil = Date.now() + 1000;
51 | node.warn('Client buffer is saturated. Requests for the next 1000ms will be ignored. Consider reducing poll rate of operations to this connection.');
52 | node.status({ fill: 'red', shape: 'dot', text: 'queue full' });
53 | }
54 | // eslint-disable-next-line no-unused-vars
55 | function onClientOpen(remoteInfo) {
56 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
57 | }
58 | function onClientClose() {
59 | node.status({ fill: 'yellow', shape: 'dot', text: 'not connected' });
60 | }
61 | // eslint-disable-next-line no-unused-vars
62 | function onClientInit(options) {
63 | node.status({ fill: 'grey', shape: 'dot', text: 'initialised' });
64 | }
65 |
66 | function removeAllListeners() {
67 | if(node.client) {
68 | node.client.off('error', onClientError);
69 | node.client.off('full', onClientFull);
70 | node.client.off('open', onClientOpen);
71 | node.client.off('close', onClientClose);
72 | node.client.off('initialised', onClientInit);
73 | }
74 | }
75 |
76 | /* **************** Node status **************** */
77 | function nodeStatusError(err, msg, statusText) {
78 | if (err) {
79 | node.error(err, msg);
80 | } else {
81 | node.error(statusText, msg);
82 | }
83 | node.status({ fill: 'red', shape: 'dot', text: statusText });
84 | }
85 |
86 | if (this.connectionConfig) {
87 | node.status({ fill: 'yellow', shape: 'ring', text: 'initialising' });
88 | if(node.client) {
89 | node.client.removeAllListeners();
90 | }
91 | node.client = connection_pool.get(node, node.connectionConfig);
92 | this.client.on('error', onClientError);
93 | this.client.on('full', onClientFull);
94 | this.client.on('open', onClientOpen);
95 | this.client.on('close', onClientClose);
96 | this.client.on('initialised', onClientInit);
97 |
98 | function finsReply(err, sequence) {
99 | if (!err && !sequence) {
100 | return;
101 | }
102 | const origInputMsg = (sequence && sequence.tag) || {};
103 | try {
104 | if(sequence) {
105 | if (err || sequence.error) {
106 | nodeStatusError(((err && err.message) || "error"), origInputMsg, ((err && err.message) || "error") );
107 | return;
108 | }
109 | if (sequence.timeout) {
110 | nodeStatusError('timeout', origInputMsg, 'timeout');
111 | return;
112 | }
113 | if (sequence.response && sequence.sid != sequence.response.sid) {
114 | nodeStatusError(`SID does not match! My SID: ${sequence.sid}, reply SID:${sequence.response.sid}`, origInputMsg, 'Incorrect SID');
115 | return;
116 | }
117 | }
118 | if (!sequence || !sequence.response || sequence.response.endCode !== '0000' || sequence.response.command.commandCode !== cmdExpected) {
119 | let ecd = 'bad response';
120 | if (sequence.response && sequence.response.command.commandCode !== cmdExpected)
121 | ecd = `Unexpected response. Expected command '${cmdExpected}' but received '${sequence.response.command}'`;
122 | else if (sequence.response && sequence.response.endCodeDescription)
123 | ecd = sequence.response.endCodeDescription;
124 | nodeStatusError(`Response is NG! endCode: ${sequence.response ? sequence.response.endCode : '????'}, endCodeDescription:${sequence.response ? sequence.response.endCodeDescription : ''}`, origInputMsg, ecd);
125 | return;
126 | }
127 |
128 | //set the output property
129 | RED.util.setObjectProperty(origInputMsg, node.msgProperty, sequence.sid || 0, true);
130 |
131 | //include additional detail in msg.fins
132 | origInputMsg.fins = {};
133 | origInputMsg.fins.name = node.name; //node name for user logging / routing
134 | origInputMsg.fins.request = {
135 | command: sequence.request.command,
136 | options: sequence.request.options,
137 | address: sequence.request.address,
138 | count: sequence.request.count,
139 | sid: sequence.request.sid,
140 | };
141 | origInputMsg.fins.response = sequence.response;
142 | origInputMsg.fins.stats = sequence.stats;
143 | origInputMsg.fins.createTime = sequence.createTime;
144 | origInputMsg.fins.replyTime = sequence.replyTime;
145 | origInputMsg.fins.timeTaken = sequence.timeTaken;
146 |
147 | node.status({ fill: 'green', shape: 'dot', text: 'done' });
148 | node.send(origInputMsg);
149 | } catch (error) {
150 | nodeStatusError(error, origInputMsg, 'error');
151 |
152 | }
153 | }
154 |
155 | this.on('close', function (done) {
156 | removeAllListeners();
157 | if (done) done();
158 | });
159 |
160 | this.on('input', function (msg) {
161 | if (node.throttleUntil) {
162 | if (node.throttleUntil > Date.now()) return; //throttled
163 | node.throttleUntil = null; //throttle time over
164 | }
165 | node.status({});//clear status
166 |
167 | if (msg.disconnect === true || msg.topic === 'disconnect') {
168 | node.client.disconnect();
169 | return;
170 | } else if (msg.connect === true || msg.topic === 'connect') {
171 | node.client.connect();
172 | return;
173 | }
174 |
175 | /* **************** Get address Parameter **************** */
176 | const address = RED.util.evaluateNodeProperty(node.address, node.addressType, node, msg);
177 | if (!address || typeof address != 'string') {
178 | nodeStatusError(null, msg, 'address is not valid');
179 | return;
180 | }
181 |
182 | /* **************** Get fill count Parameter **************** */
183 | const count = RED.util.evaluateNodeProperty(node.count, node.countType, node, msg);
184 | const fillCount = parseInt(count);
185 | if (count == null || isNaN(fillCount) || fillCount <= 0) {
186 | nodeStatusError(`fill count '${count} is invalid'`, msg, `fill count '${count} is invalid'`);
187 | return;
188 | }
189 |
190 | /* **************** Get fill value Parameter **************** */
191 | const value = RED.util.evaluateNodeProperty(node.value, node.valueType, node, msg);
192 | const fillValue = parseInt(value);
193 | if (value == null || isNaN(fillValue)) {
194 | nodeStatusError(`fill value '${value} is invalid'`, msg, `fill value '${value} is invalid'`);
195 | return;
196 | }
197 |
198 | const opts = msg.finsOptions || {};
199 | let sid;
200 | try {
201 | opts.callback = finsReply;
202 | //fill(address, value, count, opts, tag)
203 | sid = node.client.fill(address, fillValue, fillCount, opts, msg);
204 | if (sid > 0) node.status({ fill: 'yellow', shape: 'ring', text: 'fill' });
205 | } catch (error) {
206 | if(error.message == "not connected") {
207 | node.status({ fill: 'yellow', shape: 'dot', text: error.message });
208 | } else {
209 | nodeStatusError(error, msg, 'error');
210 | const debugMsg = {
211 | info: "fill.js-->on 'input' - try this.client.fill(address, fillValue, fillCount, opts, msg)",
212 | connection: `host: ${node.connectionConfig.host}, port: ${node.connectionConfig.port}`,
213 | sid: sid,
214 | address: address,
215 | fillValue: fillValue,
216 | fillCount: fillCount,
217 | opts: opts,
218 | error: error
219 | };
220 | node.debug(debugMsg);
221 | }
222 | return;
223 | }
224 |
225 | });
226 | if(node.client && node.client.connected) {
227 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
228 | } else {
229 | node.status({ fill: 'grey', shape: 'ring', text: 'initialised' });
230 | }
231 |
232 | } else {
233 | node.status({ fill: 'red', shape: 'dot', text: 'Connection config missing' });
234 | }
235 |
236 | }
237 | RED.nodes.registerType('FINS Fill', omronFill);
238 |
239 | };
240 |
241 |
--------------------------------------------------------------------------------
/nodes/transfer.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-inner-declarations */
2 | /*
3 | MIT License
4 |
5 | Copyright (c) 2019, 2020, 2021 Steve-Mcl
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | */
25 |
26 | module.exports = function (RED) {
27 | const connection_pool = require('../connection_pool.js');
28 | function omronTransfer(config) {
29 | RED.nodes.createNode(this, config);
30 | const node = this;
31 | const cmdExpected = '0105';
32 | node.name = config.name;
33 | node.connection = config.connection;
34 | node.address = config.address || '';
35 | node.addressType = config.addressType || 'str';
36 | node.address2 = config.address2 || '';
37 | node.address2Type = config.addressType || 'str';
38 | node.count = config.count || '1';
39 | node.countType = config.countType || 'num';
40 | node.msgProperty = config.msgProperty || 'payload';
41 | node.msgPropertyType = config.msgPropertyType || 'str';
42 | node.connectionConfig = RED.nodes.getNode(node.connection);
43 |
44 | /* **************** Listeners **************** */
45 | function onClientError(error, seq) {
46 | node.status({ fill: 'red', shape: 'ring', text: 'error' });
47 | node.error(error, (seq && seq.tag ? seq.tag : seq));
48 | }
49 | function onClientFull() {
50 | node.throttleUntil = Date.now() + 1000;
51 | node.warn('Client buffer is saturated. Requests for the next 1000ms will be ignored. Consider reducing poll rate of operations to this connection.');
52 | node.status({ fill: 'red', shape: 'dot', text: 'queue full' });
53 | }
54 | // eslint-disable-next-line no-unused-vars
55 | function onClientOpen(remoteInfo) {
56 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
57 | }
58 | function onClientClose() {
59 | node.status({ fill: 'yellow', shape: 'dot', text: 'not connected' });
60 | }
61 | // eslint-disable-next-line no-unused-vars
62 | function onClientInit(options) {
63 | node.status({ fill: 'grey', shape: 'dot', text: 'initialised' });
64 | }
65 |
66 | function removeAllListeners() {
67 | if(node.client) {
68 | node.client.off('error', onClientError);
69 | node.client.off('full', onClientFull);
70 | node.client.off('open', onClientOpen);
71 | node.client.off('close', onClientClose);
72 | node.client.off('initialised', onClientInit);
73 | }
74 | }
75 |
76 | /* **************** Node status **************** */
77 | function nodeStatusError(err, msg, statusText) {
78 | if (err) {
79 | node.error(err, msg);
80 | } else {
81 | node.error(statusText, msg);
82 | }
83 | node.status({ fill: 'red', shape: 'dot', text: statusText });
84 | }
85 |
86 | if (this.connectionConfig) {
87 | node.status({ fill: 'yellow', shape: 'ring', text: 'initialising' });
88 | if(node.client) {
89 | node.client.removeAllListeners();
90 | }
91 | node.client = connection_pool.get(node, node.connectionConfig);
92 | this.client.on('error', onClientError);
93 | this.client.on('full', onClientFull);
94 | this.client.on('open', onClientOpen);
95 | this.client.on('close', onClientClose);
96 | this.client.on('initialised', onClientInit);
97 |
98 | function finsReply(err, sequence) {
99 | if (!err && !sequence) {
100 | return;
101 | }
102 | const origInputMsg = (sequence && sequence.tag) || {};
103 | try {
104 | if(sequence) {
105 | if (err || sequence.error) {
106 | nodeStatusError(((err && err.message) || "error"), origInputMsg, ((err && err.message) || "error") );
107 | return;
108 | }
109 | if (sequence.timeout) {
110 | nodeStatusError('timeout', origInputMsg, 'timeout');
111 | return;
112 | }
113 | if (sequence.response && sequence.sid != sequence.response.sid) {
114 | nodeStatusError(`SID does not match! My SID: ${sequence.sid}, reply SID:${sequence.response.sid}`, origInputMsg, 'Incorrect SID');
115 | return;
116 | }
117 | }
118 | if (!sequence || !sequence.response || sequence.response.endCode !== '0000' || sequence.response.command.commandCode !== cmdExpected) {
119 | let ecd = 'bad response';
120 | if (sequence.response && sequence.response.command.commandCode !== cmdExpected)
121 | ecd = `Unexpected response. Expected command '${cmdExpected}' but received '${sequence.response.command}'`;
122 | else if (sequence.response && sequence.response.endCodeDescription)
123 | ecd = sequence.response.endCodeDescription;
124 | nodeStatusError(`Response is NG! endCode: ${sequence.response ? sequence.response.endCode : '????'}, endCodeDescription:${sequence.response ? sequence.response.endCodeDescription : ''}`, origInputMsg, ecd);
125 | return;
126 | }
127 |
128 | //set the output property
129 | RED.util.setObjectProperty(origInputMsg, node.msgProperty, sequence.sid || 0, true);
130 |
131 | //include additional detail in msg.fins
132 | origInputMsg.fins = {};
133 | origInputMsg.fins.name = node.name; //node name for user logging / routing
134 | origInputMsg.fins.request = {
135 | command: sequence.request.command,
136 | options: sequence.request.options,
137 | srcAddress: sequence.request.srcAddress,
138 | dstAddress: sequence.request.dstAddress,
139 | count: sequence.request.count,
140 | sid: sequence.request.sid,
141 | };
142 | origInputMsg.fins.response = sequence.response;
143 | origInputMsg.fins.stats = sequence.stats;
144 | origInputMsg.fins.createTime = sequence.createTime;
145 | origInputMsg.fins.replyTime = sequence.replyTime;
146 | origInputMsg.fins.timeTaken = sequence.timeTaken;
147 |
148 | node.status({ fill: 'green', shape: 'dot', text: 'done' });
149 | node.send(origInputMsg);
150 | } catch (error) {
151 | nodeStatusError(error, origInputMsg, 'error');
152 |
153 | }
154 | }
155 |
156 | this.on('close', function (done) {
157 | removeAllListeners();
158 | if (done) done();
159 | });
160 |
161 | this.on('input', function (msg) {
162 | if (node.throttleUntil) {
163 | if (node.throttleUntil > Date.now()) return; //throttled
164 | node.throttleUntil = null; //throttle time over
165 | }
166 | node.status({});//clear status
167 |
168 | if (msg.disconnect === true || msg.topic === 'disconnect') {
169 | node.client.disconnect();
170 | return;
171 | } else if (msg.connect === true || msg.topic === 'connect') {
172 | node.client.connect();
173 | return;
174 | }
175 |
176 | /* **************** Get address (source) Parameter **************** */
177 | const address = RED.util.evaluateNodeProperty(node.address, node.addressType, node, msg);
178 | if (!address || typeof address != 'string') {
179 | nodeStatusError(null, msg, 'Source address is not valid');
180 | return;
181 | }
182 | /* **************** Get address2 (destination) Parameter **************** */
183 | const address2 = RED.util.evaluateNodeProperty(node.address2, node.address2Type, node, msg);
184 | if (!address2 || typeof address2 != 'string') {
185 | nodeStatusError(null, msg, 'Destination address is not valid');
186 | return;
187 | }
188 |
189 | /* **************** Get fill count Parameter **************** */
190 | const count = RED.util.evaluateNodeProperty(node.count, node.countType, node, msg);
191 | const transferCount = parseInt(count);
192 | if (count == null || isNaN(transferCount) || transferCount <= 0) {
193 | nodeStatusError(`Transfer count '${count} is invalid'`, msg, `Transfer count '${count} is invalid'`);
194 | return;
195 | }
196 |
197 | const opts = msg.finsOptions || {};
198 | let sid;
199 | try {
200 | opts.callback = finsReply;
201 | //srcAddress, dstAddress, count, opts, tag
202 | sid = node.client.transfer(address, address2, count, opts, msg);
203 | if (sid > 0) node.status({ fill: 'yellow', shape: 'ring', text: 'transfer' });
204 | } catch (error) {
205 | if(error.message == "not connected") {
206 | node.status({ fill: 'yellow', shape: 'dot', text: error.message });
207 | } else {
208 | nodeStatusError(error, msg, 'error');
209 | const debugMsg = {
210 | info: "transfer.js-->on 'input' - try this.client.transfer(address, address2, count, opts, msg)",
211 | connection: `host: ${node.connectionConfig.host}, port: ${node.connectionConfig.port}`,
212 | sid: sid,
213 | address: address,
214 | address2: address2,
215 | count: count,
216 | opts: opts,
217 | error: error
218 | };
219 | node.debug(debugMsg);
220 | }
221 | return;
222 | }
223 | });
224 | if(node.client && node.client.connected) {
225 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
226 | } else {
227 | node.status({ fill: 'grey', shape: 'ring', text: 'initialised' });
228 | }
229 |
230 | } else {
231 | node.status({ fill: 'red', shape: 'dot', text: 'Connection config missing' });
232 | }
233 |
234 | }
235 | RED.nodes.registerType('FINS Transfer', omronTransfer);
236 | };
237 |
238 |
--------------------------------------------------------------------------------
/nodes/read.html:
--------------------------------------------------------------------------------
1 |
24 |
25 |
37 |
38 |
135 |
168 |
169 |
--------------------------------------------------------------------------------
/nodes/connection.html:
--------------------------------------------------------------------------------
1 |
24 |
25 |
46 |
47 |
131 |
132 |
183 |
184 |
--------------------------------------------------------------------------------
/connection_pool.js:
--------------------------------------------------------------------------------
1 | /*
2 | MIT License
3 |
4 | Copyright (c) 2019, 2020, 2021 Steve-Mcl
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 |
26 | function convertPayloadToDataArray(payload) {
27 | let array = [];
28 | let str = '';
29 |
30 | if (Array.isArray(payload)) {
31 | return payload;
32 | } if (typeof payload === 'string') {
33 | str = `${payload}`;
34 | } else if (typeof payload === 'number') {
35 | return [payload];
36 | } else if (typeof payload === 'boolean') {
37 | return [payload];
38 | }
39 |
40 | if (str.length === 0) {
41 | return null;
42 | }
43 | array = str.split(/\s*,\s*/);
44 |
45 | return array;
46 | }
47 |
48 | function describe(host, port, options) {
49 | options = options || {};
50 | return `{host:'${host || ''}', port:'${port || ''}', protocol:'${options.protocol || 'udp'}', MODE:'${options.MODE}', ICF:'${options.ICF}', DNA:'${options.DNA}', DA1:'${options.DA1}', DA2:'${options.DA2}', SNA:'${options.SNA}', SA1:'${options.SA1}', SA2:'${options.SA2}'}`;
51 | }
52 |
53 | const clients = {};
54 | const fins = require('./omron-fins');
55 |
56 | module.exports = {
57 |
58 | get(node, connectionConfig) {
59 | const id = connectionConfig.id;
60 | let _options = { ...(connectionConfig.options || {}) };
61 | let _port = parseInt(connectionConfig.port || _options.port);
62 | let _host = connectionConfig.host || _options.host;
63 | let _connect = connectionConfig.autoConnect == null ? true : connectionConfig.autoConnect;
64 |
65 | if (!clients[id]) {
66 | clients[id] = (function (port, host, options, connect) {
67 |
68 | node.log(`Create new FinsClient. id:${id}, config: ${describe(host, port, options)}`);
69 | let fins_client = fins.FinsClient(port, host, options, false);
70 | let connecting = false;
71 | let inhibitAutoReconnect = true;
72 |
73 | const finsClientWrapper = {
74 | write(address, data, opts, tag) {
75 | checkConnection();
76 | const _data = convertPayloadToDataArray(data);
77 | if (!Array.isArray(_data)) {
78 | throw new Error('data is not valid');
79 | }
80 | return fins_client.write(address, _data, opts, tag);
81 | },
82 | read(address, len, opts, tag) {
83 | checkConnection();
84 | return fins_client.read(address, parseInt(len), opts, tag);
85 | },
86 | readMultiple(addresses, opts, tag) {
87 | checkConnection();
88 | return fins_client.readMultiple(addresses, opts, tag);
89 | },
90 | fill(address, value, count, opts, tag) {
91 | checkConnection();
92 | return fins_client.fill(address, value, parseInt(count), opts, tag);
93 | },
94 | transfer(srcAddress, dstAddress, count, opts, tag) {
95 | checkConnection();
96 | return fins_client.transfer(srcAddress, dstAddress, parseInt(count), opts, tag);
97 | },
98 | status(opts, tag) {
99 | checkConnection();
100 | return fins_client.status(opts, tag);
101 | },
102 | run(opts, tag) {
103 | checkConnection();
104 | return fins_client.run(opts, tag);
105 | },
106 | stop(opts, tag) {
107 | checkConnection();
108 | return fins_client.stop(opts, tag);
109 | },
110 | cpuUnitDataRead(opts, tag) {
111 | checkConnection();
112 | return fins_client.cpuUnitDataRead(opts, tag);
113 | },
114 | clockRead(opts, tag) {
115 | checkConnection();
116 | return fins_client.clockRead(opts, tag);
117 | },
118 | clockWrite(clock, opts, tag) {
119 | checkConnection();
120 | return fins_client.clockWrite(clock, opts, tag);
121 | },
122 | on(a, b) {
123 | try {
124 | fins_client.on(a, b);
125 | // eslint-disable-next-line no-empty
126 | } catch (error) { }
127 | },
128 | off(a, b) {
129 | try {
130 | fins_client.off(a, b);
131 | // eslint-disable-next-line no-empty
132 | } catch (error) { }
133 | },
134 | removeAllListeners() {
135 | try {
136 | fins_client.removeAllListeners();
137 | // eslint-disable-next-line no-empty
138 | } catch (error) { }
139 | },
140 | connect(host, port, opts) {
141 | inhibitAutoReconnect = false; //as `connect` is being called, assume the user wants the connection to auto recover.
142 | finsClientWrapper.reconnect(host, port, opts);
143 | },
144 | reconnect(host, port, opts) {
145 | if (!fins_client.connected && !connecting) {
146 | try {
147 | node.log(`Connecting id:${id}, config: ${describe(finsClientWrapper.connectionInfo.host, finsClientWrapper.connectionInfo.port, finsClientWrapper.connectionInfo.options)}`);
148 | // eslint-disable-next-line no-empty
149 | } catch (error) { }
150 |
151 | try {
152 | fins_client.connect(host, port, opts);
153 | connecting = true;
154 | finsClientWrapper.reconnectTimeOver = setTimeout(() => {
155 | connecting = false;
156 | finsClientWrapper.reconnectTimeOver = null;
157 | }, 8000);
158 |
159 | // eslint-disable-next-line no-empty
160 | } catch (error) {
161 | node.error(error)
162 | }
163 | }
164 | },
165 | disconnect() {
166 | inhibitAutoReconnect = true; //as `disconnect` is being called, assume the user wants to stay disconnected.
167 | if (fins_client) {
168 | fins_client.disconnect();
169 | }
170 | clearTimeout(finsClientWrapper.reconnectTimeOver);
171 | finsClientWrapper.reconnectTimeOver = null;
172 | connecting = false;
173 | },
174 | stringToFinsAddress(addressString) {
175 | return fins_client.stringToFinsAddress(addressString);
176 | },
177 |
178 | FinsAddressToString(decodedAddress, offsetWD, offsetBit) {
179 | return fins_client.FinsAddressToString(decodedAddress, offsetWD, offsetBit);
180 | },
181 |
182 | close() {
183 | connecting = false;
184 | if (fins_client && fins_client.connected) {
185 | node.log(`closing connection ~ ${id}`);
186 | fins_client.disconnect();
187 | }
188 | },
189 |
190 | get connected() {
191 | return fins_client && fins_client.connected;
192 | },
193 |
194 | get connectionInfo() {
195 | if(fins_client) {
196 | const info = {
197 | port: fins_client.port,
198 | host: fins_client.host,
199 | options: {...fins_client.options},
200 | }
201 | if(fins_client.protocol == "tcp") {
202 | info.options.tcp_server_node_no = fins_client.server_node_no;//DA1
203 | info.options.tcp_client_node_no = fins_client.client_node_no;//SA1
204 | }
205 | return info;
206 | }
207 | return {}
208 | }
209 | };
210 |
211 | fins_client.on('open', () => {
212 | try {
213 | clearTimeout(finsClientWrapper.reconnectTimeOver);
214 | finsClientWrapper.reconnectTimeOver = null;
215 | clearTimeout(finsClientWrapper.reconnectTimer);
216 | connecting = false;
217 | finsClientWrapper.reconnectTimer = null;
218 | node.log(`connected ~ ${id}`);
219 | // eslint-disable-next-line no-empty
220 | } catch (error) { }
221 | });
222 |
223 | // eslint-disable-next-line no-unused-vars
224 | fins_client.on('close', (err) => {
225 | try {
226 | clearTimeout(finsClientWrapper.reconnectTimeOver);
227 | finsClientWrapper.reconnectTimeOver = null;
228 | connecting = false;
229 | node.log(`connection closed ~ ${id}`);
230 | scheduleReconnect();
231 | // eslint-disable-next-line no-empty
232 | } catch (error) { }
233 | });
234 |
235 | function checkConnection() {
236 | if (!finsClientWrapper.connected) {
237 | if (!inhibitAutoReconnect && !finsClientWrapper.reconnectTimer) {
238 | scheduleReconnect();
239 | }
240 | throw new Error('not connected');
241 | }
242 | }
243 |
244 | function scheduleReconnect() {
245 | if(!connecting) {
246 | if (!finsClientWrapper.reconnectTimer && !inhibitAutoReconnect) {
247 | finsClientWrapper.reconnectTimer = setTimeout(() => {
248 | if (finsClientWrapper.reconnectTimer && !inhibitAutoReconnect) {
249 | finsClientWrapper.reconnectTimer = null;
250 | node.log(`Scheduled reconnect ~ ${id}`);
251 | finsClientWrapper.reconnect();
252 | }
253 | }, 2000); // TODO: Parametrise
254 | }
255 | }
256 | }
257 |
258 | if(connect) {
259 | finsClientWrapper.connect();
260 | }
261 | return finsClientWrapper;
262 |
263 | }(_port, _host, _options, _connect));
264 | }
265 | return clients[id];
266 | },
267 | close(connectionConfig) {
268 | const cli = this.get(null, connectionConfig);
269 | if(cli) {
270 | clearTimeout(cli.reconnectTimer);
271 | cli.reconnectTimer = null;
272 | cli.removeAllListeners();
273 | if(cli.connected) {
274 | cli.close();
275 | }
276 | }
277 | const id = connectionConfig.id;
278 | delete clients[id];
279 | }
280 | };
281 |
--------------------------------------------------------------------------------
/nodes/readMultiple.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-inner-declarations */
2 | /*
3 | MIT License
4 |
5 | Copyright (c) 2019, 2020, 2021 Steve-Mcl
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | */
25 |
26 | module.exports = function (RED) {
27 | const connection_pool = require('../connection_pool.js');
28 | // const dataParser = require("./_parser");
29 |
30 | function omronReadMultiple(config) {
31 | RED.nodes.createNode(this, config);
32 | const node = this;
33 | const cmdExpected = '0104';
34 | node.name = config.name;
35 | node.topic = config.topic;
36 | node.connection = config.connection;
37 | node.address = config.address || '';
38 | node.addressType = config.addressType || 'str';
39 | node.outputFormat = config.outputFormat || 'buffer';
40 | node.outputFormatType = config.outputFormatType || 'list';
41 | node.msgProperty = config.msgProperty || 'payload';
42 | node.msgPropertyType = config.msgPropertyType || 'str';
43 | node.connectionConfig = RED.nodes.getNode(node.connection);
44 |
45 | /* **************** Listeners **************** */
46 | function onClientError(error, seq) {
47 | node.status({ fill: 'red', shape: 'ring', text: 'error' });
48 | node.error(error, (seq && seq.tag ? seq.tag : seq));
49 | }
50 | function onClientFull() {
51 | node.throttleUntil = Date.now() + 1000;
52 | node.warn('Client buffer is saturated. Requests for the next 1000ms will be ignored. Consider reducing poll rate of operations to this connection.');
53 | node.status({ fill: 'red', shape: 'dot', text: 'queue full' });
54 | }
55 | // eslint-disable-next-line no-unused-vars
56 | function onClientOpen(remoteInfo) {
57 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
58 | }
59 | function onClientClose() {
60 | node.status({ fill: 'yellow', shape: 'dot', text: 'not connected' });
61 | }
62 | // eslint-disable-next-line no-unused-vars
63 | function onClientInit(options) {
64 | node.status({ fill: 'grey', shape: 'dot', text: 'initialised' });
65 | }
66 |
67 | function removeAllListeners() {
68 | if(node.client) {
69 | node.client.off('error', onClientError);
70 | node.client.off('full', onClientFull);
71 | node.client.off('open', onClientOpen);
72 | node.client.off('close', onClientClose);
73 | node.client.off('initialised', onClientInit);
74 | }
75 | }
76 |
77 | /* **************** Node status **************** */
78 | function nodeStatusError(err, msg, statusText) {
79 | if (err) {
80 | node.error(err, msg);
81 | } else {
82 | node.error(statusText, msg);
83 | }
84 | node.status({ fill: 'red', shape: 'dot', text: statusText });
85 | }
86 |
87 | if (this.connectionConfig) {
88 | node.status({ fill: 'yellow', shape: 'ring', text: 'initialising' });
89 | if(node.client) {
90 | node.client.removeAllListeners();
91 | }
92 | node.client = connection_pool.get(node, node.connectionConfig);
93 | this.client.on('error', onClientError);
94 | this.client.on('full', onClientFull);
95 | this.client.on('open', onClientOpen);
96 | this.client.on('close', onClientClose);
97 | this.client.on('initialised', onClientInit);
98 |
99 | function finsReply(err, sequence) {
100 | if (!err && !sequence) {
101 | return;
102 | }
103 | const origInputMsg = (sequence && sequence.tag) || {};
104 | try {
105 | if(sequence) {
106 | if (err || sequence.error) {
107 | nodeStatusError(((err && err.message) || "error"), origInputMsg, ((err && err.message) || "error") );
108 | return;
109 | }
110 | if (sequence.timeout) {
111 | nodeStatusError('timeout', origInputMsg, 'timeout');
112 | return;
113 | }
114 | if (sequence.response && sequence.sid != sequence.response.sid) {
115 | nodeStatusError(`SID does not match! My SID: ${sequence.sid}, reply SID:${sequence.response.sid}`, origInputMsg, 'Incorrect SID')
116 | return;
117 | }
118 | }
119 | if (!sequence || !sequence.response || sequence.response.endCode !== '0000' || sequence.response.command.commandCode !== cmdExpected) {
120 | let ecd = 'bad response';
121 | if (sequence.response && sequence.response.command.commandCode !== cmdExpected)
122 | ecd = `Unexpected response. Expected command '${cmdExpected}' but received '${sequence.response.command}'`;
123 | else if (sequence.response && sequence.response.endCodeDescription)
124 | ecd = sequence.response.endCodeDescription;
125 | nodeStatusError(`Response is NG! endCode: ${sequence.response ? sequence.response.endCode : '????'}, endCodeDescription:${sequence.response ? sequence.response.endCodeDescription : ''}`, origInputMsg, ecd);
126 | return;
127 | }
128 |
129 | let outputFormat = 'unsignedkv';
130 | const builtInReturnTypes = ['signed', 'unsigned', 'signedkv', 'unsignedkv'];
131 | if (builtInReturnTypes.indexOf(node.outputFormatType + '') >= 0) {
132 | outputFormat = node.outputFormatType;
133 | } else {
134 | outputFormat = RED.util.evaluateNodeProperty(node.outputFormat, node.outputFormatType, node, origInputMsg);
135 | }
136 |
137 | if (sequence.response.values.length != sequence.request.address.length) {
138 | nodeStatusError(`Requested count '${sequence.request.address.length}' different to returned count '${sequence.response.values.length}`, origInputMsg, 'error');
139 | return;
140 | }
141 |
142 | let objValues = {};
143 | let arrValues = [];
144 |
145 | for (let index = 0; index < sequence.request.address.length; index++) {
146 | const addr = sequence.request.address[index];
147 | const val = sequence.response.values[index];
148 | const addrString = node.client.FinsAddressToString(addr);
149 |
150 | switch (outputFormat) {
151 | case 'signed':
152 | if (addr.isBitAddress) {
153 | arrValues.push(!!val);
154 | } else {
155 | arrValues.push(val);
156 | }
157 | break;
158 | case 'unsigned':
159 | if (addr.isBitAddress) {
160 | arrValues.push(val);
161 | } else {
162 | arrValues.push(Uint16Array.from([val])[0]);
163 | }
164 | break;
165 | case 'signedkv':
166 | if (addr.isBitAddress) {
167 | objValues[addrString] = (val == 1 || val == true) ? true : false;
168 | } else {
169 | objValues[addrString] = val;
170 | }
171 | break;
172 | default: //case "unsignedkv": //default
173 | if (addr.isBitAddress) {
174 | objValues[addrString] = (val == 1 || val == true) ? 1 : 0;
175 | } else {
176 | objValues[addrString] = Uint16Array.from([val])[0];
177 | }
178 | break;
179 | }
180 | }
181 |
182 | //set the output property
183 | RED.util.setObjectProperty(origInputMsg, node.msgProperty, arrValues.length ? arrValues : objValues, true);
184 |
185 | //include additional detail in msg.fins
186 | origInputMsg.fins = {};
187 | origInputMsg.fins.name = node.name; //node name for user logging / routing
188 | origInputMsg.fins.request = {
189 | address: sequence.request.address,
190 | count: sequence.request.count,
191 | sid: sequence.request.sid,
192 | };
193 |
194 | origInputMsg.fins.response = sequence.response;
195 | origInputMsg.fins.stats = sequence.stats;
196 | origInputMsg.fins.createTime = sequence.createTime;
197 | origInputMsg.fins.replyTime = sequence.replyTime;
198 | origInputMsg.fins.timeTaken = sequence.timeTaken;
199 |
200 | node.status({ fill: 'green', shape: 'dot', text: 'done' });
201 | node.send(origInputMsg);
202 | } catch (error) {
203 | nodeStatusError(error, origInputMsg, 'error');
204 | }
205 | }
206 |
207 | this.on('close', function (done) {
208 | removeAllListeners();
209 | if (done) done();
210 | });
211 |
212 | this.on('input', function (msg) {
213 | if (node.throttleUntil) {
214 | if (node.throttleUntil > Date.now()) return; //throttled
215 | node.throttleUntil = null; //throttle time over
216 | }
217 | node.status({});//clear status
218 |
219 | if (msg.disconnect === true || msg.topic === 'disconnect') {
220 | node.client.disconnect();
221 | return;
222 | } else if (msg.connect === true || msg.topic === 'connect') {
223 | node.client.connect();
224 | return;
225 | }
226 |
227 |
228 | /* **************** Get address Parameter **************** */
229 | const address = RED.util.evaluateNodeProperty(node.address, node.addressType, node, msg);
230 | if (!address || typeof address != 'string') {
231 | nodeStatusError(null, msg, 'Address is not valid');
232 | return;
233 | }
234 |
235 | const opts = msg.finsOptions || {};
236 | let sid;
237 | try {
238 | opts.callback = finsReply;
239 | sid = node.client.readMultiple(address, opts, msg);
240 | if (sid > 0) {
241 | node.status({ fill: 'yellow', shape: 'ring', text: 'reading' });
242 | }
243 | } catch (error) {
244 | if(error.message == "not connected") {
245 | node.status({ fill: 'yellow', shape: 'dot', text: error.message });
246 | } else {
247 | nodeStatusError(error, msg, 'error');
248 | const debugMsg = {
249 | info: "readMultiple.js-->on 'input'",
250 | connection: `host: ${node.connectionConfig.host}, port: ${node.connectionConfig.port}`,
251 | sid: sid,
252 | addresses: address,
253 | opts: opts,
254 | };
255 | node.debug(debugMsg);
256 | }
257 | return;
258 | }
259 |
260 | });
261 | if(node.client && node.client.connected) {
262 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
263 | } else {
264 | node.status({ fill: 'grey', shape: 'ring', text: 'initialised' });
265 | }
266 |
267 | } else {
268 | node.status({ fill: 'red', shape: 'dot', text: 'Connection config missing' });
269 | }
270 | }
271 | RED.nodes.registerType('FINS Read Multiple', omronReadMultiple);
272 | };
273 |
--------------------------------------------------------------------------------
/nodes/control.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-inner-declarations */
2 | /*
3 | MIT License
4 |
5 | Copyright (c) 2019, 2020, 2021 Steve-Mcl
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | */
25 |
26 | module.exports = function (RED) {
27 | const connection_pool = require('../connection_pool.js');
28 | const commandTypes = ['connect', 'disconnect', 'status', 'cpu-unit-data-read', 'stop', 'run', 'clock-read', 'clock-write'];
29 | function omronControl(config) {
30 | RED.nodes.createNode(this, config);
31 | const node = this;
32 | node.name = config.name;
33 | node.topic = config.topic;
34 | node.connection = config.connection;
35 | node.clock = config.clock || 'clock';
36 | node.clockType = config.clockType || 'msg';
37 | node.connectOptions = config.connectOptions || 'connectOptions';
38 | node.connectOptionsType = config.connectOptionsType || 'msg';
39 | node.command = config.command || 'status';
40 | node.commandType = config.commandType || 'status';
41 | node.msgProperty = config.msgProperty || 'payload';
42 | node.msgPropertyType = config.msgPropertyType || 'str';
43 | node.connectionConfig = RED.nodes.getNode(node.connection);
44 |
45 | /* **************** Listeners **************** */
46 | function onClientError(error, seq) {
47 | node.status({ fill: 'red', shape: 'ring', text: 'error' });
48 | node.error(error, (seq && seq.tag ? seq.tag : seq));
49 | }
50 | function onClientFull() {
51 | node.throttleUntil = Date.now() + 1000;
52 | node.warn('Client buffer is saturated. Requests for the next 1000ms will be ignored. Consider reducing poll rate of operations to this connection.');
53 | node.status({ fill: 'red', shape: 'dot', text: 'queue full' });
54 | }
55 | // eslint-disable-next-line no-unused-vars
56 | function onClientOpen(remoteInfo) {
57 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
58 | }
59 | function onClientClose() {
60 | node.status({ fill: 'yellow', shape: 'dot', text: 'not connected' });
61 | }
62 | // eslint-disable-next-line no-unused-vars
63 | function onClientInit(options) {
64 | node.status({ fill: 'grey', shape: 'dot', text: 'initialised' });
65 | }
66 |
67 | function removeAllListeners() {
68 | if(node.client) {
69 | node.client.off('error', onClientError);
70 | node.client.off('full', onClientFull);
71 | node.client.off('open', onClientOpen);
72 | node.client.off('close', onClientClose);
73 | node.client.off('initialised', onClientInit);
74 | }
75 | }
76 |
77 | /* **************** Node status **************** */
78 | function nodeStatusError(err, msg, statusText) {
79 | if (err) {
80 | node.error(err, msg);
81 | } else {
82 | node.error(statusText, msg);
83 | }
84 | node.status({ fill: 'red', shape: 'dot', text: statusText });
85 | }
86 |
87 | if (this.connectionConfig) {
88 | node.status({ fill: 'yellow', shape: 'ring', text: 'initialising' });
89 | if(node.client) {
90 | node.client.removeAllListeners();
91 | }
92 | node.client = connection_pool.get(node, node.connectionConfig);
93 | this.client.on('error', onClientError);
94 | this.client.on('full', onClientFull);
95 | this.client.on('open', onClientOpen);
96 | this.client.on('close', onClientClose);
97 | this.client.on('initialised', onClientInit);
98 |
99 | function finsReply(err, sequence) {
100 | if (!err && !sequence) {
101 | return;
102 | }
103 | const origInputMsg = (sequence && sequence.tag) || {};
104 | try {
105 | if(sequence) {
106 | if (err || sequence.error) {
107 | nodeStatusError(((err && err.message) || "error"), origInputMsg, ((err && err.message) || "error") );
108 | return;
109 | }
110 | if (sequence.timeout) {
111 | nodeStatusError('timeout', origInputMsg, 'timeout');
112 | return;
113 | }
114 | if (sequence.response && sequence.sid != sequence.response.sid) {
115 | nodeStatusError(`SID does not match! My SID: ${sequence.sid}, reply SID:${sequence.response.sid}`, origInputMsg, 'Incorrect SID')
116 | return;
117 | }
118 | }
119 | if (!sequence || !sequence.response || sequence.response.endCode !== '0000' || sequence.response.command.commandCode !== sequence.request.command.commandCode) {
120 | var ecd = 'bad response';
121 | if (sequence.response.command.commandCode !== sequence.request.command.commandCode)
122 | ecd = `Unexpected response. Expected command '${sequence.request.command.commandCode}' but received '${sequence.request.command.commandCode}'`;
123 | else if (sequence.response && sequence.response.endCodeDescription)
124 | ecd = sequence.response.endCodeDescription;
125 | nodeStatusError(`Response is NG! endCode: ${sequence.response ? sequence.response.endCode : '????'}, endCodeDescription:${sequence.response ? sequence.response.endCodeDescription : ''}`, origInputMsg, ecd);
126 | return;
127 | }
128 | let payload;
129 | switch (sequence.request.command.name) {
130 | case 'connect':
131 | case 'disconnect':
132 | break;
133 | case 'status':
134 | case 'cpu-unit-data-read':
135 | case 'clock-read':
136 | payload = sequence.response.result;
137 | break;
138 | default:
139 | payload = sequence.response.sid
140 | break;
141 | }
142 |
143 | //set the output property
144 | RED.util.setObjectProperty(origInputMsg, node.msgProperty, payload, true);
145 |
146 | //include additional detail in msg.fins
147 | origInputMsg.fins = {};
148 | origInputMsg.fins.name = node.name; //node name for user logging / routing
149 | origInputMsg.fins.request = {
150 | command: sequence.request.command,
151 | options: sequence.request.options,
152 | sid: sequence.request.sid,
153 | };
154 | origInputMsg.fins.connectionInfo = node.client.connectionInfo;
155 | origInputMsg.fins.response = sequence.response;
156 | origInputMsg.fins.stats = sequence.stats;
157 | origInputMsg.fins.createTime = sequence.createTime;
158 | origInputMsg.fins.replyTime = sequence.replyTime;
159 | origInputMsg.fins.timeTaken = sequence.timeTaken;
160 |
161 | node.status({ fill: 'green', shape: 'dot', text: 'done' });
162 | node.send(origInputMsg);
163 | } catch (error) {
164 | nodeStatusError(error, origInputMsg, 'error');
165 | }
166 | }
167 |
168 | this.on('close', function (done) {
169 | removeAllListeners();
170 | if (done) done();
171 | });
172 |
173 | this.on('input', function (msg) {
174 | if (node.throttleUntil) {
175 | if (node.throttleUntil > Date.now()) return; //throttled
176 | node.throttleUntil = null; //throttle time over
177 | }
178 | node.status({});//clear status
179 |
180 | let command = '';
181 | if (commandTypes.indexOf(node.commandType + '') >= 0) {
182 | command = node.commandType;
183 | } else {
184 | command = RED.util.evaluateNodeProperty(node.command, node.commandType, node, msg);
185 | }
186 |
187 | if (commandTypes.indexOf(command+'') < 0) {
188 | nodeStatusError(`command '${command?command:''}' is not valid`, msg, `command '${command?command:''}' is not valid`);
189 | return;
190 | }
191 |
192 | let clientFn;
193 | let clockWriteData;
194 | let connectOptions;
195 | const params = [];
196 | switch (command) {
197 | case 'connect':
198 | connectOptions = RED.util.evaluateNodeProperty(node.connectOptions, node.connectOptionsType, node, msg);
199 | if(connectOptions) {
200 | if( typeof connectOptions != "object") {
201 | nodeStatusError("Connect Options must be an object", msg, "error");
202 | return;
203 | } else {
204 | params.push(connectOptions.host);
205 | params.push(connectOptions.port);
206 | params.push(connectOptions);
207 | }
208 | }
209 | clientFn = node.client.connect;
210 | break;
211 | case 'disconnect':
212 | case 'status':
213 | case 'stop':
214 | case 'run':
215 | clientFn = node.client[command];
216 | break;
217 | case 'cpu-unit-data-read':
218 | clientFn = node.client.cpuUnitDataRead;
219 | break;
220 | case 'clock-read':
221 | clientFn = node.client.clockRead;
222 | break;
223 | case 'clock-write':
224 | try {
225 | clockWriteData = RED.util.evaluateNodeProperty(node.clock, node.clockType, node, msg);
226 | if(!clockWriteData || typeof clockWriteData != "object") {
227 | throw new Error();
228 | }
229 | clientFn = node.client.clockWrite;
230 | params.push(clockWriteData);
231 | } catch (error) {
232 | nodeStatusError("Cannot set clock. Clock Value is missing or invalid.", msg, "Clock Value is missing or invalid");
233 | return;
234 | }
235 | break;
236 | }
237 |
238 | const opts = {};
239 | let sid;
240 | try {
241 | opts.callback = finsReply;
242 | if(params.length) {
243 | sid = clientFn(...params, opts, msg);
244 | } else {
245 | sid = clientFn(opts, msg);
246 | }
247 | if (sid > 0) {
248 | node.status({ fill: 'yellow', shape: 'ring', text: 'reading' });
249 | }
250 | } catch (error) {
251 | node.sid = null;
252 | if(error.message == "not connected") {
253 | node.status({ fill: 'yellow', shape: 'dot', text: error.message });
254 | } else {
255 | nodeStatusError(error, msg, 'error');
256 | const debugMsg = {
257 | info: "control.js-->on 'input'",
258 | connection: `host: ${node.connectionConfig.host}, port: ${node.connectionConfig.port}`,
259 | sid: sid,
260 | opts: opts,
261 | };
262 | node.debug(debugMsg);
263 | }
264 | return;
265 | }
266 |
267 | });
268 | if(node.client && node.client.connected) {
269 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
270 | } else {
271 | node.status({ fill: 'grey', shape: 'ring', text: 'initialised' });
272 | }
273 |
274 | } else {
275 | node.status({ fill: 'red', shape: 'dot', text: 'Connection config missing' });
276 | }
277 | }
278 | RED.nodes.registerType('FINS Control', omronControl);
279 |
280 | };
281 |
282 |
--------------------------------------------------------------------------------
/nodes/control.html:
--------------------------------------------------------------------------------
1 |
24 |
25 |
37 |
38 |
141 |
174 |
175 |
--------------------------------------------------------------------------------
/nodes/read.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-inner-declarations */
2 | /*
3 | MIT License
4 |
5 | Copyright (c) 2019, 2020, 2021 Steve-Mcl
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 | */
25 |
26 | module.exports = function (RED) {
27 | const connection_pool = require('../connection_pool.js');
28 | const dataParser = require('./_parser');
29 |
30 | function omronRead(config) {
31 | RED.nodes.createNode(this, config);
32 | const node = this;
33 | const cmdExpected = '0101';
34 | node.name = config.name;
35 | node.topic = config.topic;
36 | node.connection = config.connection;
37 | node.address = config.address || 'topic';
38 | node.addressType = config.addressType || 'msg';
39 | node.count = config.count || 1;
40 | node.countType = config.countType || 'num';
41 | node.outputFormat = config.outputFormat || 'buffer';
42 | node.outputFormatType = config.outputFormatType || 'list';
43 | node.msgProperty = config.msgProperty || 'payload';
44 | node.msgPropertyType = config.msgPropertyType || 'str';
45 | node.connectionConfig = RED.nodes.getNode(node.connection);
46 |
47 | /* **************** Listeners **************** */
48 | function onClientError(error, seq) {
49 | node.status({ fill: 'red', shape: 'ring', text: 'error' });
50 | node.error(error, (seq && seq.tag ? seq.tag : seq));
51 | }
52 | function onClientFull() {
53 | node.throttleUntil = Date.now() + 1000;
54 | node.warn('Client buffer is saturated. Requests for the next 1000ms will be ignored. Consider reducing poll rate of operations to this connection.');
55 | node.status({ fill: 'red', shape: 'dot', text: 'queue full' });
56 | }
57 | // eslint-disable-next-line no-unused-vars
58 | function onClientOpen(remoteInfo) {
59 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
60 | }
61 | function onClientClose() {
62 | node.status({ fill: 'yellow', shape: 'dot', text: 'not connected' });
63 | }
64 | // eslint-disable-next-line no-unused-vars
65 | function onClientInit(options) {
66 | node.status({ fill: 'grey', shape: 'dot', text: 'initialised' });
67 | }
68 |
69 | function removeAllListeners() {
70 | if(node.client) {
71 | node.client.off('error', onClientError);
72 | node.client.off('full', onClientFull);
73 | node.client.off('open', onClientOpen);
74 | node.client.off('close', onClientClose);
75 | node.client.off('initialised', onClientInit);
76 | }
77 | }
78 |
79 | /* **************** Node status **************** */
80 | function nodeStatusError(err, msg, statusText) {
81 | if (err) {
82 | node.error(err, msg);
83 | } else {
84 | node.error(statusText, msg);
85 | }
86 | node.status({ fill: 'red', shape: 'dot', text: statusText });
87 | }
88 |
89 | if (this.connectionConfig) {
90 | node.status({ fill: 'yellow', shape: 'ring', text: 'initialising' });
91 | if(node.client) {
92 | node.client.removeAllListeners();
93 | }
94 | node.client = connection_pool.get(node, node.connectionConfig);
95 | this.client.on('error', onClientError);
96 | this.client.on('full', onClientFull);
97 | this.client.on('open', onClientOpen);
98 | this.client.on('close', onClientClose);
99 | this.client.on('initialised', onClientInit);
100 |
101 | function finsReply(err, sequence) {
102 | if (!err && !sequence) {
103 | return;
104 | }
105 | const origInputMsg = (sequence && sequence.tag) || {};
106 | try {
107 | if(sequence) {
108 | if (err || sequence.error) {
109 | nodeStatusError(((err && err.message) || "error"), origInputMsg, ((err && err.message) || "error") );
110 | return;
111 | }
112 | if (sequence.timeout) {
113 | nodeStatusError('timeout', origInputMsg, 'timeout');
114 | return;
115 | }
116 | if (sequence.response && sequence.sid != sequence.response.sid) {
117 | nodeStatusError(`SID does not match! My SID: ${sequence.sid}, reply SID:${sequence.response.sid}`, origInputMsg, 'Incorrect SID')
118 | return;
119 | }
120 | }
121 | if (!sequence || !sequence.response || sequence.response.endCode !== '0000' || sequence.response.command.commandCode !== cmdExpected) {
122 | let ecd = 'bad response';
123 | if (sequence.response && sequence.response.command.commandCode !== cmdExpected)
124 | ecd = `Unexpected response. Expected command '${cmdExpected}' but received '${sequence.response.command}'`;
125 | else if (sequence.response && sequence.response.endCodeDescription)
126 | ecd = sequence.response.endCodeDescription;
127 | nodeStatusError(`Response is NG! endCode: ${sequence.response ? sequence.response.endCode : '????'}, endCodeDescription:${sequence.response ? sequence.response.endCodeDescription : ''}`, origInputMsg, ecd);
128 | return;
129 | }
130 |
131 | //backwards compatibility, try to upgrade users current setting
132 | //the output type was originally a sub option 'list'
133 | let outputFormat = 'buffer';
134 | const builtInReturnTypes = ['buffer', 'signed', 'unsigned', 'signedkv', 'unsignedkv'];
135 | if (node.outputFormatType == 'list' && builtInReturnTypes.indexOf(node.outputFormat + '') >= 0) {
136 | outputFormat = node.outputFormat;
137 | } else if (builtInReturnTypes.indexOf(node.outputFormatType + '') >= 0) {
138 | outputFormat = node.outputFormatType;
139 | } else {
140 | outputFormat = RED.util.evaluateNodeProperty(node.outputFormat, node.outputFormatType, node, origInputMsg);
141 | }
142 |
143 | let value;
144 | switch (outputFormat) {
145 | case 'signed':
146 | if (sequence.request.address.isBitAddress) {
147 | value = sequence.response.values.map(e => !!e);
148 | } else {
149 | value = sequence.response.values;
150 | }
151 | break;
152 | case 'unsigned':
153 | if (sequence.request.address.isBitAddress) {
154 | value = sequence.response.values;
155 | } else {
156 | sequence.response.values = Uint16Array.from(sequence.response.values);
157 | value = sequence.response.values;
158 | }
159 | break;
160 | case 'signedkv':
161 | value = dataParser.keyValueMaker(node.client.FinsAddressToString, sequence.request.address, sequence.response.values);
162 | if (sequence.request.address.isBitAddress) {
163 | value = dataParser.keyValueMakerBits(node.client.FinsAddressToString, sequence.request.address, sequence.response.values, true);
164 | } else {
165 | value = dataParser.keyValueMaker(node.client.FinsAddressToString, sequence.request.address, sequence.response.values);
166 | }
167 | break;
168 | case 'unsignedkv':
169 | if (sequence.request.address.isBitAddress) {
170 | value = dataParser.keyValueMakerBits(node.client.FinsAddressToString, sequence.request.address, sequence.response.values, false);
171 | } else {
172 | sequence.response.values = Uint16Array.from(sequence.response.values);
173 | value = dataParser.keyValueMaker(node.client.FinsAddressToString, sequence.request.address, sequence.response.values);
174 | }
175 | break;
176 | default: //buffer
177 | value = sequence.response.buffer;
178 | break;
179 | }
180 |
181 | //set the output property
182 | RED.util.setObjectProperty(origInputMsg, node.msgProperty, value, true);
183 |
184 | //include additional detail in msg.fins
185 | origInputMsg.fins = {};
186 | origInputMsg.fins.name = node.name; //node name for user logging / routing
187 | origInputMsg.fins.request = {
188 | command: sequence.request.command,
189 | options: sequence.request.options,
190 | address: sequence.request.address,
191 | count: sequence.request.count,
192 | sid: sequence.request.sid,
193 | };
194 |
195 | origInputMsg.fins.response = sequence.response;
196 | origInputMsg.fins.stats = sequence.stats;
197 | origInputMsg.fins.createTime = sequence.createTime;
198 | origInputMsg.fins.replyTime = sequence.replyTime;
199 | origInputMsg.fins.timeTaken = sequence.timeTaken;
200 |
201 | node.status({ fill: 'green', shape: 'dot', text: 'done' });
202 | node.send(origInputMsg);
203 | } catch (error) {
204 | nodeStatusError(error, origInputMsg, 'error');
205 | }
206 | }
207 |
208 | this.on('close', function (done) {
209 | removeAllListeners();
210 | if (done) done();
211 | });
212 |
213 | this.on('input', function (msg) {
214 | if (node.throttleUntil) {
215 | if (node.throttleUntil > Date.now()) return; //throttled
216 | node.throttleUntil = null; //throttle time over
217 | }
218 | node.status({});//clear status
219 |
220 | if (msg.disconnect === true || msg.topic === 'disconnect') {
221 | node.client.disconnect();
222 | return;
223 | } else if (msg.connect === true || msg.topic === 'connect') {
224 | node.client.connect();
225 | return;
226 | }
227 |
228 |
229 | /* **************** Get address Parameter **************** */
230 | const address = RED.util.evaluateNodeProperty(node.address, node.addressType, node, msg);
231 |
232 | /* **************** Get count Parameter **************** */
233 | let count = RED.util.evaluateNodeProperty(node.count, node.countType, node, msg);
234 |
235 | if (!address || typeof address != 'string') {
236 | nodeStatusError(null, msg, 'address is not valid');
237 | return;
238 | }
239 | count = parseInt(count);
240 | if (Number.isNaN(count) || count <= 0) {
241 | nodeStatusError(null, msg, 'count is not valid');
242 | return;
243 | }
244 |
245 | const opts = msg.finsOptions || {};
246 | let sid;
247 | try {
248 | opts.callback = finsReply;
249 | sid = node.client.read(address, count, opts, msg);
250 | if (sid > 0) {
251 | node.status({ fill: 'yellow', shape: 'ring', text: 'reading' });
252 | }
253 | } catch (error) {
254 | if(error.message == "not connected") {
255 | node.status({ fill: 'yellow', shape: 'dot', text: error.message });
256 | } else {
257 | nodeStatusError(error, msg, 'error');
258 | const debugMsg = {
259 | info: "read.js-->on 'input'",
260 | connection: `host: ${node.connectionConfig.host}, port: ${node.connectionConfig.port}`,
261 | sid: sid,
262 | address: address,
263 | count: count,
264 | opts: opts,
265 | };
266 | node.debug(debugMsg);
267 | }
268 | return;
269 | }
270 |
271 | });
272 | if(node.client && node.client.connected) {
273 | node.status({ fill: 'green', shape: 'dot', text: 'connected' });
274 | } else {
275 | node.status({ fill: 'grey', shape: 'ring', text: 'initialised' });
276 | }
277 |
278 | } else {
279 | node.status({ fill: 'red', shape: 'dot', text: 'Connection config missing' });
280 | }
281 | }
282 | RED.nodes.registerType('FINS Read', omronRead);
283 |
284 | };
285 |
286 |
--------------------------------------------------------------------------------