├── 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 | ![image](https://user-images.githubusercontent.com/44235289/85577974-9c4a7700-b631-11ea-8320-99992892b39d.png) 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 | ![image](https://user-images.githubusercontent.com/44235289/85562713-a619ad80-b624-11ea-971b-dc22754d7cf1.png) 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 | --------------------------------------------------------------------------------