├── .gitignore ├── CHANGELOG.md ├── images ├── node.png ├── additem01.png ├── additem02.png ├── creategroups.png └── createserver.png ├── package.json ├── README.md ├── red ├── locales │ └── en-US │ │ └── opc-da.json ├── opc-da.html └── opc-da.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Version: 0.0.1 2 | ------------ 3 | - Initial development release 4 | -------------------------------------------------------------------------------- /images/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st-one-io/node-red-contrib-opc-da/HEAD/images/node.png -------------------------------------------------------------------------------- /images/additem01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st-one-io/node-red-contrib-opc-da/HEAD/images/additem01.png -------------------------------------------------------------------------------- /images/additem02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st-one-io/node-red-contrib-opc-da/HEAD/images/additem02.png -------------------------------------------------------------------------------- /images/creategroups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st-one-io/node-red-contrib-opc-da/HEAD/images/creategroups.png -------------------------------------------------------------------------------- /images/createserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st-one-io/node-red-contrib-opc-da/HEAD/images/createserver.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-opc-da", 3 | "version": "1.0.3", 4 | "description": "A Node-RED node to talk to automation devices using the OPC-DA protocol", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/netsmarttech/node-red-contrib-opc-da.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/netsmarttech/node-red-contrib-opc-da/issues" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "keywords": [ 17 | "hardware", 18 | "automation", 19 | "opc", 20 | "opc-da", 21 | "data-access", 22 | "dcom", 23 | "dce-rpc", 24 | "plc", 25 | "node-red" 26 | ], 27 | "node-red": { 28 | "nodes": { 29 | "opc-da": "red/opc-da.js" 30 | } 31 | }, 32 | "author": "Smart-Tech Controle e Automação Ltda.", 33 | "license": "Apache-2.0", 34 | "dependencies": { 35 | "node-opc-da": "^1.0.7" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-red-contrib-opc-da 2 | 3 | node-red-contrib-opc-da is an OPC-DA compatible node for Node-RED that allow interaction with remote OPC-DA servers. Currently only reading and browsing operations are supported. 4 | 5 | This node was created by [Smart-Tech](https://netsmarttech.com/) as part of the [ST-One](https://netsmarttech.com/page/st-one) project. 6 | 7 | ## Table of Contents 8 | 9 | - [Install](#install) 10 | - [Usage]() 11 | - [Creating a Server](#creating-a-server) 12 | - [Creating a Group](#creating-a-group) 13 | - [Adding Items to a Group](#adding-items-to-a-group) 14 | - [Contributing](#contributing) 15 | 16 | ## Install 17 | 18 | Using npm: 19 | 20 | ```bash 21 | npm install node-red-contrib-opc-da 22 | ``` 23 | 24 | ## Creating a Server 25 | 26 | To create a server you will need a few information about your target server: the IP address, the domain name, a username with enough privilege to remotely interact with the OPC Server, this users's password, and a [CLSID](https://docs.microsoft.com/en-us/windows/win32/com/clsid). We ship this node with a few known [ProgIds](https://docs.microsoft.com/en-us/windows/win32/com/-progid--key), which will fill the CLSID field with the correct string. If you have one or more applications that you think could be included on the default options feel free to open an issue with your suggestion. In case your server ProgId is not listed, you can choose the ```Custom``` options and type it by hand. 27 | 28 | ![](/images/createserver.png) 29 | 30 | You should also pay attention to the timeout value to make sure it is compatible with the characteristics of your network. If this value is too low, the server might not even be created, and if does other problems related to timeouts might arise. Finally, if you want to test your configuration click the ```Test and get Items``` button. This button will connect to the server, authenticate, and will browse for a full list of available items. 31 | 32 | ## Creating a Group 33 | 34 | Once your server was created you'll have to create a group. For a group to be created you must first select a server you previously created. The update rate defines how frequent the server will be queried for the items added to this group. You can also use the ```Active``` option to activate or deactivate your groups. For now, the ```Deadband``` feature is not fully implemented so you don't need to bother with it. 35 | 36 | ![](/images/creategroups.png) 37 | 38 | ## Adding Items to a Group 39 | 40 | To add Items to a group, you can type the item name as it is stored at the server and click the ```+``` button. In case you are not sure which items are available on your server, return to the OPC Server configuration tab and click the ```Test and get Items``` button since it will browse and return a list of full available items, allowing you to add from a list here. 41 | 42 | | ![](/images/additem01.png) | ![](/images/additem02.png) | 43 | | :------------------------: | -------------------------- | 44 | | | | 45 | ## Contributing 46 | 47 | This is a partial implementation and there are lots that could be done to improve what is already supported or to add support for more OPC-DA features. Feel free to dive in! Open an issue or submit PRs. 48 | -------------------------------------------------------------------------------- /red/locales/en-US/opc-da.json: -------------------------------------------------------------------------------- 1 | { 2 | "opc-da": { 3 | "in": { 4 | "label": { 5 | "group": "OPC Group", 6 | "mode": "Mode", 7 | "item": "Item", 8 | "diff": "Emit only when value changes (diff)", 9 | "item-select": "Select an item", 10 | "item-novar": "No items" 11 | }, 12 | "mode": { 13 | "single": "Single item", 14 | "all-split": "All items, one per message", 15 | "all": "All items" 16 | } 17 | }, 18 | "out": { 19 | "label": { 20 | "group": "OPC Group", 21 | "item": "Item", 22 | "item-select": "Select an item" 23 | }, 24 | "disclaimer": "Caution when writing data to production systems!" 25 | }, 26 | "group": { 27 | "label": { 28 | "server": "OPC Server", 29 | "updaterate": "Update rate", 30 | "deadband": "Deadband", 31 | "active": "Active", 32 | "validate": "Validate", 33 | "items": "Items", 34 | "timeout": "Timeout" 35 | }, 36 | "placeholder": { 37 | "itemadd": "Add a new item" 38 | } 39 | }, 40 | "server": { 41 | "label": { 42 | "address": "IP Address", 43 | "domain": "Domain", 44 | "username": "Username", 45 | "password": "Password", 46 | "progid": "ProgId", 47 | "os": "OS", 48 | "clsid": "ClsId", 49 | "custom": "Custom...", 50 | "verbose": "Debug", 51 | "test": "Test and get items", 52 | "items": "Server items" 53 | }, 54 | "placeholder": { 55 | "address": "IP or FQDN (e.g. dev.example.com)", 56 | "clsid": "8A885D04-1CEB-11C9-9FE8-08002B104860" 57 | }, 58 | "verbose": { 59 | "default": "Default (command line)", 60 | "on": "On", 61 | "off": "Off" 62 | }, 63 | "options": { 64 | "old" : "Windows XP SP3 or older", 65 | "new": "Windows Vista or newer" 66 | } 67 | }, 68 | "label": { 69 | "name": "Name" 70 | }, 71 | "error": { 72 | "missingconfig": "Missing configuration", 73 | "noresponse": "No response from device, restarting communication", 74 | "disconnected": "Disconnected from the server", 75 | "accessdenied": "Access denied. Username and/or password might be wrong", 76 | "classnotreg": "The given Clsid is not registered on the server" 77 | }, 78 | "warn": { 79 | "dupgroupname": "Duplicate group name found, skipping group", 80 | "noitems": "No items configured on this group", 81 | "minupdaterate": "Update rate too low, enforcing minimum of __value__" 82 | }, 83 | "info": { 84 | }, 85 | "status": { 86 | "online": "online", 87 | "badvalues": "failure", 88 | "offline": "offline", 89 | "unknown": "unknown", 90 | "connecting": "connecting" 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /red/opc-da.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 84 | 85 | 110 | 111 | 272 | 273 | 274 | 275 | 306 | 307 | 336 | 337 | 453 | 454 | 455 | 456 | 484 | 485 | 530 | 531 | 612 | 613 | 614 | 615 | 631 | 632 | 657 | 658 | 727 | -------------------------------------------------------------------------------- /red/opc-da.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | /* 3 | Copyright 2019 Smart-Tech Controle e Automação 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | /** 19 | * Compares values for equality, includes special handling for arrays. Fixes #33 20 | * @param {number|string|Array} a 21 | * @param {number|string|Array} b 22 | */ 23 | function equals(a, b) { 24 | if (a === b) return true; 25 | if (a == null || b == null) return false; 26 | if (Array.isArray(a) && Array.isArray(b)) { 27 | if (a.length != b.length) return false; 28 | 29 | for (var i = 0; i < a.length; ++i) { 30 | if (a[i] !== b[i]) return false; 31 | } 32 | return true; 33 | } 34 | return false; 35 | } 36 | 37 | /** 38 | * A very simple function that simply returns the apropriate 39 | * object containing the major and minor version numbers 40 | * @param {String} version 41 | */ 42 | function parseComVersion (version) { 43 | console.log("DEBUG", version); 44 | if (version == "5.4") { 45 | return {major: 5, minor: 4}; 46 | } else if (version == "5.7"){ 47 | return {major: 5, minor: 7}; 48 | } 49 | } 50 | 51 | const MIN_UPDATE_RATE = 100; 52 | 53 | module.exports = function (RED) { 54 | 55 | const EventEmitter = require('events').EventEmitter; 56 | const opcda = require('node-opc-da'); 57 | const { OPCGroupStateManager, OPCItemManager, OPCSyncIO } = opcda; 58 | const { ComServer, Session, Clsid } = opcda.dcom; 59 | 60 | function generateStatus(status, val) { 61 | let obj; 62 | 63 | if (typeof val != 'string' && typeof val != 'number' && typeof val != 'boolean') { 64 | val = RED._("opc-da.status.online"); 65 | } 66 | 67 | switch (status) { 68 | case 'online': 69 | obj = { fill: 'green', shape: 'dot', text: val.toString() }; 70 | break; 71 | case 'badvalues': 72 | obj = { fill: 'yellow', shape: 'dot', text: RED._("opc-da.status.badvalues") }; 73 | break; 74 | case 'offline': 75 | obj = { fill: 'red', shape: 'dot', text: RED._("opc-da.status.offline") }; 76 | break; 77 | case 'connecting': 78 | obj = { fill: 'yellow', shape: 'dot', text: RED._("opc-da.status.connecting") }; 79 | break; 80 | default: 81 | obj = { fill: 'grey', shape: 'dot', text: RED._("opc-da.status.unknown") }; 82 | } 83 | return obj; 84 | } 85 | 86 | RED.httpAdmin.get('/opc-da/browseItems', RED.auth.needsPermission('node-opc-da.list'), function (req, res) { 87 | let params = req.query 88 | function onBrowseError(e) { 89 | RED.log.error(errorMessage(e)); 90 | res.json({err: errorMessage(e)}) 91 | } 92 | 93 | async function browseItems() { 94 | let self = this; 95 | let session = new Session(); 96 | session = session.createSession(params.domain, params.username, params.password); 97 | session.setGlobalSocketTimeout(params.timeout); 98 | 99 | let comServer = new ComServer(new Clsid(params.clsid), params.address, session, parseComVersion(params.comversion)); 100 | 101 | comServer.on("disconnected", function(){ 102 | onBrowseError(RED._("opc-da.error.disconnected")); 103 | }); 104 | 105 | await comServer.init(); 106 | 107 | let comObject = await comServer.createInstance(); 108 | 109 | let opcServer = new opcda.OPCServer(); 110 | await opcServer.init(comObject); 111 | 112 | let opcBrowser = await opcServer.getBrowser(); 113 | let items = await opcBrowser.browseAllFlat(); 114 | 115 | // don't need to await it, so we can return immediately 116 | opcBrowser.end() 117 | .then(() => opcServer.end()) 118 | .then(() => comServer.closeStub()) 119 | .catch(e => RED.log.error(`Error closing browse session: ${e}`)); 120 | 121 | return items; 122 | } 123 | 124 | browseItems().then(items => { 125 | res.json({ items }); 126 | }).catch(onBrowseError); 127 | }); 128 | 129 | /** 130 | * 131 | * @param {object} config 132 | */ 133 | function OPCDAServer(config) { 134 | EventEmitter.call(this); 135 | const node = this; 136 | let isOnCleanUp = false; 137 | let reconnecting = false; 138 | 139 | RED.nodes.createNode(this, config); 140 | 141 | if (!this.credentials) { 142 | return node.error(RED._("opc-da.error.missingconfig")); 143 | } 144 | 145 | //init variables 146 | let status = 'unknown'; 147 | let isVerbose = (config.verbose == 'on' || config.verbose == 'off') ? (config.verbose == 'on') : RED.settings.get('verbose'); 148 | let connOpts = { 149 | address: config.address, 150 | domain: config.domain, 151 | username: this.credentials.username, 152 | password: this.credentials.password, 153 | clsid: config.clsid, 154 | timeout: config.timeout, 155 | comversion: parseComVersion(config.os) 156 | }; 157 | 158 | let groups = new Map(); 159 | let comSession, comServer, comObject, opcServer; 160 | 161 | async function onComServerError(e) { 162 | node.error(errorMessage(e)); 163 | switch(e) { 164 | case 0x00000005: 165 | return; 166 | case 0xC0040010: 167 | return; 168 | case 0x80040154: 169 | return; 170 | case 0x00000061: 171 | return; 172 | default: 173 | node.warn("Trying to reconnect..."); 174 | await new Promise(resolve => setTimeout(resolve, 5000)); 175 | await setup().catch(onComServerError); 176 | } 177 | } 178 | 179 | function updateStatus(newStatus) { 180 | if (status == newStatus) return; 181 | 182 | status = newStatus; 183 | groups.forEach(group => group.onServerStatus(status)); 184 | } 185 | 186 | async function setup() { 187 | let comSession = new Session(); 188 | comSession = comSession.createSession(connOpts.domain, connOpts.username, connOpts.password); 189 | comSession.setGlobalSocketTimeout(connOpts.timeout); 190 | 191 | comServer = new ComServer(new Clsid(connOpts.clsid), connOpts.address, comSession, connOpts.comversion); 192 | 193 | let self = this; 194 | comServer.on('e_classnotreg', function(){ 195 | node.error(RED._("opc-da.error.classnotreg")); 196 | }); 197 | 198 | comServer.on("disconnected", function(){ 199 | onComServerError(RED._("opc-da.error.disconnected")); 200 | }) 201 | 202 | comServer.on("e_accessdenied", function() { 203 | node.error(RED._("opc-da.error.accessdenied")); 204 | }); 205 | 206 | await comServer.init(); 207 | 208 | comObject = await comServer.createInstance(); 209 | 210 | opcServer = new opcda.OPCServer(); 211 | await opcServer.init(comObject); 212 | for (const entry of groups.entries()) { 213 | const name = entry[0]; 214 | const group = entry[1]; 215 | let opcGroup = await opcServer.addGroup(name, group.opcConfig); 216 | console.log("setup for group: " + name); 217 | await group.updateInstance(opcGroup); 218 | } 219 | 220 | updateStatus('online'); 221 | } 222 | 223 | async function cleanup() { 224 | try { 225 | if (isOnCleanUp) return; 226 | console.log("Cleaning Up"); 227 | isOnCleanUp = true; 228 | //cleanup groups first 229 | console.log("Cleaning groups..."); 230 | for (const group of groups.values()) { 231 | //await group.cleanUp(); 232 | } 233 | console.log("Cleaned Groups"); 234 | if (opcServer) { 235 | //await opcServer.end(); 236 | opcServer = null; 237 | } 238 | console.log("Cleaned opcServer"); 239 | if (comSession) { 240 | await comSession.destroySession(); 241 | comServer = null; 242 | } 243 | console.log("Cleaned session. Finished."); 244 | isOnCleanUp = false; 245 | } catch (e) { 246 | //TODO I18N 247 | isOnCleanUp = false; 248 | let err = e && e.stack || e; 249 | console.log(e); 250 | node.error("Error cleaning up server: " + err, { error: err }); 251 | } 252 | 253 | updateStatus('unknown'); 254 | } 255 | 256 | node.reConnect = async function reConnect() { 257 | /* if reconnect was already called, do nothing 258 | if reconnect was never called, try to restart the session */ 259 | if (!reconnecting) { 260 | console.log("cleaning up"); 261 | reconnecting = true; 262 | await cleanup(); 263 | await setup().catch(onComServerError); 264 | reconnecting = false 265 | } 266 | } 267 | 268 | node.createGroup = async function createGroup(group) { 269 | let opcGroup = await opcServer.addGroup(group.opcConfig.name, group.opcConfig); 270 | console.log("setup for group: " + group.config.name); 271 | await group.updateInstance(opcGroup); 272 | group.onServerStatus(status); 273 | } 274 | 275 | node.getStatus = function getStatus() { 276 | return status; 277 | }; 278 | 279 | node.registerGroup = function registerGroup(group) { 280 | if (groups.has(group.config.name)) { 281 | return RED._("opc-da.warn.dupgroupname"); 282 | } 283 | 284 | groups.set(group.config.name, group); 285 | } 286 | 287 | node.unregisterGroup = function unregisterGroup(group) { 288 | groups.delete(group.config.name); 289 | } 290 | 291 | setup().catch(onComServerError); 292 | } 293 | RED.nodes.registerType("opc-da server", OPCDAServer, { 294 | credentials: { 295 | username: { type: "text" }, 296 | password: { type: "password" } 297 | } 298 | }); 299 | 300 | 301 | // ---------- OPC-DA Group ---------- 302 | /** 303 | * @param {object} config 304 | * @param {string} config.server 305 | * @param {string} config.updaterate 306 | * @param {string} config.deadband 307 | * @param {boolean} config.active 308 | * @param {boolean} config.validate 309 | * @param {object[]} config.vartable 310 | */ 311 | function OPCDAGroup(config) { 312 | EventEmitter.call(this); 313 | const node = this; 314 | RED.nodes.createNode(this, config); 315 | 316 | node.server = RED.nodes.getNode(config.server); 317 | if (!node.server || !node.server.registerGroup) { 318 | return node.error(RED._("opc-da.error.missingconfig")); 319 | } 320 | /** @type {OPCGroupStateManager} */ 321 | let opcGroupMgr; 322 | /** @type {OPCItemManager} */ 323 | let opcItemMgr; 324 | /** @type {OPCSyncIO} */ 325 | let opcSyncIo; 326 | let clientHandlePtr; 327 | let serverHandles = [], clientHandles = []; 328 | let status, timer; 329 | 330 | let readInProgress = false; 331 | let connected = false; 332 | let readDeferred = 0; 333 | let oldItems = {}; 334 | let updateRate = parseInt(config.updaterate); 335 | let deadband = parseInt(config.deadband); 336 | let validate = config.validate; 337 | let onCleanUp = false; 338 | 339 | if (isNaN(updateRate)) { 340 | updateRate = 1000; 341 | } 342 | if (isNaN(deadband)) { 343 | deadband = 0; 344 | } 345 | 346 | node.config = config; 347 | node.opcConfig = { 348 | active: config.active, 349 | updateRate: updateRate, 350 | timeBias: 0, 351 | deadband: deadband || 0 352 | } 353 | 354 | if (node.server.getStatus() == 'online') { 355 | node.server.createGroup(this); 356 | } 357 | 358 | /** 359 | * @private 360 | * @param {OPCGroupStateManager} newGroup 361 | */ 362 | async function setup(newGroup) { 363 | clearInterval(timer); 364 | try { 365 | opcGroupMgr = newGroup; 366 | opcItemMgr = await opcGroupMgr.getItemManager(); 367 | opcSyncIo = await opcGroupMgr.getSyncIO(); 368 | 369 | clientHandlePtr = 1; 370 | clientHandles.length = 0; 371 | serverHandles = []; 372 | connected = true; 373 | readInProgress = false; 374 | readDeferred = 0; 375 | 376 | let items = config.vartable || []; 377 | if (items.length < 1) { 378 | node.warn("opc-da.warn.noitems"); 379 | } 380 | 381 | let itemsList = items.map(e => { 382 | return { itemID: e.item, clientHandle: clientHandlePtr++ } 383 | }); 384 | 385 | let resAddItems = await opcItemMgr.add(itemsList); 386 | 387 | for (let i = 0; i < resAddItems.length; i++) { 388 | const resItem = resAddItems[i]; 389 | const item = itemsList[i]; 390 | 391 | if (resItem[0] !== 0) { 392 | node.error(`Error adding item '${itemsList[i].itemID}': ${errorMessage(resItem[0])}`); 393 | } else { 394 | serverHandles.push(resItem[1].serverHandle); 395 | clientHandles[item.clientHandle] = item.itemID; 396 | } 397 | } 398 | } catch (e) { 399 | let err = e && e.stack || e; 400 | console.log(e); 401 | node.error("Error on setting up group: " + err); 402 | } 403 | 404 | // we set up the timer regardless the result of setting up items 405 | // we may support adding items at a later time 406 | if (updateRate < MIN_UPDATE_RATE) { 407 | updateRate = MIN_UPDATE_RATE; 408 | node.warn(RED._('opc-da.warn.minupdaterate', { value: updateRate + 'ms' })) 409 | } 410 | 411 | if (config.active) { 412 | timer = setInterval(doCycle, updateRate); 413 | doCycle(); 414 | } 415 | } 416 | 417 | async function cleanup() { 418 | if (onCleanUp) return; 419 | onCleanUp = true; 420 | 421 | clearInterval(timer); 422 | clientHandlePtr = 1; 423 | clientHandles.length = 0; 424 | serverHandles = []; 425 | 426 | try { 427 | if (opcSyncIo) { 428 | await opcSyncIo.end(); 429 | console.log("GroupCLeanup - opcSync"); 430 | opcSyncIo = null; 431 | } 432 | 433 | if (opcItemMgr) { 434 | await opcItemMgr.end(); 435 | console.log("GroupCLeanup - opcItemMgr"); 436 | opcItemMgr = null; 437 | } 438 | 439 | if (opcGroupMgr) { 440 | await opcGroupMgr.end(); 441 | console.log("GroupCLeanup - opcGroupMgr"); 442 | opcGroupMgr = null; 443 | } 444 | } catch (e) { 445 | onCleanUp = false; 446 | let err = e && e.stack || e; 447 | console.log(e); 448 | node.error("Error on cleaning up group: " + err); 449 | } 450 | onCleanUp = false; 451 | } 452 | 453 | async function doCycle() { 454 | if (connected && !readInProgress) { 455 | if (!serverHandles.length) return; 456 | 457 | readInProgress = true; 458 | readDeferred = 0; 459 | await opcSyncIo.read(opcda.constants.opc.dataSource.DEVICE, serverHandles) 460 | .then(cycleCallback).catch(cycleError); 461 | } else { 462 | readDeferred++; 463 | if (readDeferred > 15) { 464 | node.warn(RED._("opc-da.error.noresponse"), {}); 465 | clearInterval(timer); 466 | // since we have no good way to know if there is a network problem 467 | // or if something else happened, restart the whole thing 468 | node.server.reConnect(); 469 | } 470 | } 471 | } 472 | 473 | function cycleCallback(values) { 474 | readInProgress = false; 475 | 476 | if (readDeferred && connected) { 477 | doCycle(); 478 | readDeferred = 0; 479 | } 480 | //sanitizeValues(values); 481 | let changed = false; 482 | for (const item of values) { 483 | const itemID = clientHandles[item.clientHandle]; 484 | 485 | if (!itemID) { 486 | //TODO - what is the right to do here? 487 | node.warn("Server replied with an unknown client handle"); 488 | continue; 489 | } 490 | 491 | let oldItem = oldItems[itemID]; 492 | 493 | if (!oldItem || oldItem.quality !== item.quality || !equals(oldItem.value, item.value)) { 494 | changed = true; 495 | node.emit(itemID, item); 496 | node.emit('__CHANGED__', { itemID, item }); 497 | } 498 | oldItems[itemID] = item; 499 | } 500 | node.emit('__ALL__', oldItems); 501 | if (changed) node.emit('__ALL_CHANGED__', oldItems); 502 | } 503 | 504 | function cycleError(err) { 505 | readInProgress = false; 506 | node.error('Error reading items: ' + err && err.stack || err); 507 | } 508 | 509 | node.onServerStatus = function onServerStatus(s) { 510 | status = s; 511 | node.emit('__STATUS__', s); 512 | } 513 | 514 | node.getStatus = function getStatus() { 515 | return status; 516 | }; 517 | 518 | node.cleanUp = async function cleanUp() { 519 | await cleanup(); 520 | } 521 | 522 | /** 523 | * @private 524 | * @param {OPCGroupStateManager} newOpcGroup 525 | */ 526 | node.updateInstance = async function updateInstance(newOpcGroup) { 527 | //await cleanup(); 528 | await setup(newOpcGroup); 529 | } 530 | 531 | node.on('close', async function (done) { 532 | node.server.unregisterGroup(this); 533 | await cleanup(); 534 | console.log("group cleaned"); 535 | done(); 536 | }); 537 | let err = node.server.registerGroup(this); 538 | if (err) { 539 | node.error(err, { error: err }); 540 | } 541 | 542 | } 543 | RED.nodes.registerType("opc-da group", OPCDAGroup); 544 | 545 | 546 | // ---------- OPC-DA In ---------- 547 | /** 548 | * @param {object} config 549 | * @param {string} config.group 550 | * @param {string} config.item 551 | * @param {string} config.mode 552 | * @param {boolean} config.diff 553 | */ 554 | function OPCDAIn(config) { 555 | const node = this; 556 | RED.nodes.createNode(this, config); 557 | 558 | node.group = RED.nodes.getNode(config.group); 559 | if (!node.group || !node.group.getStatus) { 560 | return node.error(RED._("opc-da.error.missingconfig")); 561 | } 562 | 563 | let statusVal; 564 | 565 | function sendMsg(data, key, status) { 566 | // if there is no data to be sent 567 | if (!data) return; 568 | if (key === undefined) key = ''; 569 | 570 | let msg; 571 | if (key === '') { //should be the case when mode == 'all' 572 | let newData = new Array(); 573 | for (let key in data) { 574 | newData.push({ 575 | errorCode: data[key].errorCode, 576 | value: data[key].value, 577 | quality: data[key].quality, 578 | timestamp: data[key].timestamp, 579 | topic: key 580 | }); 581 | } 582 | 583 | msg = { 584 | topic: "all", 585 | payload: newData 586 | }; 587 | } else { 588 | if (data.errorCode !== 0) { 589 | //TODO i18n and node status handling 590 | msg = { 591 | errorCode: data.errorCode, 592 | payload: data.value, 593 | quality: data.quality, 594 | timestamp: data.timestamp, 595 | topic: key 596 | } 597 | node.error(`Read of item '${key}' returned error: ${data.errorCode}`, msg); 598 | return; 599 | } 600 | 601 | msg = { 602 | payload: data.value, 603 | quality: data.quality, 604 | timestamp: data.timestamp, 605 | topic: key 606 | }; 607 | } 608 | statusVal = status !== undefined ? status : data; 609 | node.send(msg); 610 | node.status(generateStatus(node.group.getStatus(), statusVal)); 611 | } 612 | 613 | function onChanged(elm) { 614 | sendMsg(elm.item, elm.itemID, null); 615 | } 616 | 617 | function onDataSplit(data) { 618 | Object.keys(data).forEach(function (key) { 619 | sendMsg(data[key], key, null); 620 | }); 621 | } 622 | 623 | function onData(data) { 624 | sendMsg(data, config.mode == 'single' ? config.item : ''); 625 | } 626 | 627 | function onDataSelect(data) { 628 | onData(data[config.item]); 629 | } 630 | 631 | function onGroupStatus(s) { 632 | node.status(generateStatus(s.status, statusVal)); 633 | } 634 | 635 | node.group.on('__STATUS__', onGroupStatus); 636 | node.status(generateStatus(node.group.getStatus(), statusVal)); 637 | 638 | if (config.diff) { 639 | switch (config.mode) { 640 | case 'all-split': 641 | node.group.on('__CHANGED__', onChanged); 642 | break; 643 | case 'single': 644 | node.group.on(config.item, onData); 645 | break; 646 | case 'all': 647 | default: 648 | node.group.on('__ALL_CHANGED__', onData); 649 | } 650 | } else { 651 | switch (config.mode) { 652 | case 'all-split': 653 | node.group.on('__ALL__', onDataSplit); 654 | break; 655 | case 'single': 656 | node.group.on('__ALL__', onDataSelect); 657 | break; 658 | case 'all': 659 | default: 660 | node.group.on('__ALL__', onData); 661 | } 662 | } 663 | 664 | node.on('close', function (done) { 665 | node.group.removeListener('__ALL__', onDataSelect); 666 | node.group.removeListener('__ALL__', onDataSplit); 667 | node.group.removeListener('__ALL__', onData); 668 | node.group.removeListener('__ALL_CHANGED__', onData); 669 | node.group.removeListener('__CHANGED__', onChanged); 670 | node.group.removeListener('__STATUS__', onGroupStatus); 671 | node.group.removeListener(config.item, onData); 672 | done(); 673 | }); 674 | } 675 | RED.nodes.registerType("opc-da in", OPCDAIn); 676 | 677 | 678 | // ---------- OPC-DA Out ---------- 679 | /** 680 | * 681 | * @param {object} config 682 | * @param {string} config.group 683 | * @param {string} config.item 684 | */ 685 | function OPCDAOut(config) { 686 | const node = this; 687 | RED.nodes.createNode(this, config); 688 | 689 | node.group = RED.nodes.getNode(config.group); 690 | if (!node.group) { 691 | return node.error(RED._("opc-da.error.missingconfig")); 692 | } 693 | 694 | let statusVal; 695 | 696 | function onGroupStatus(s) { 697 | node.status(generateStatus(s.status, statusVal)); 698 | } 699 | 700 | function onNewMsg(msg) { 701 | var writeObj = { 702 | name: config.item || msg.item, 703 | val: msg.payload 704 | }; 705 | 706 | if (!writeObj.name) return; 707 | 708 | statusVal = writeObj.val; 709 | node.group.writeVar(writeObj); 710 | node.status(generateStatus(node.group.getStatus(), statusVal)); 711 | } 712 | 713 | node.status(generateStatus(node.group.getStatus(), statusVal)); 714 | 715 | node.on('input', onNewMsg); 716 | node.group.on('__STATUS__', onGroupStatus); 717 | 718 | node.on('close', function (done) { 719 | node.group.removeListener('__STATUS__', onGroupStatus); 720 | done(); 721 | }); 722 | 723 | } 724 | 725 | /** 726 | * @private 727 | * @param {Number} errorCode 728 | */ 729 | function errorMessage(errorCode) { 730 | let msgText; 731 | 732 | switch(errorCode){ 733 | case 0x80040154: 734 | msgText = RED._('opc-da.error.classnotreg'); 735 | break; 736 | case 0x00000005: 737 | msgText = "Access denied. Username and/or password might be wrong." 738 | break; 739 | case 0xC0040006: 740 | msgText = "The Items AccessRights do not allow the operation."; 741 | break; 742 | case 0xC0040004: 743 | msgText = "The server cannot convert the data between the specified format/ requested data type and the canonical data type."; 744 | break; 745 | case 0xC004000C: 746 | msgText = "Duplicate name not allowed."; 747 | break; 748 | case 0xC0040010: 749 | msgText = "The server's configuration file is an invalid format."; 750 | break; 751 | case 0xC0040009: 752 | msgText = "The filter string was not valid"; 753 | break; 754 | case 0xC0040001: 755 | msgText = "The value of the handle is invalid. Note: a client should never pass an invalid handle to a server. If this error occurs, it is due to a programming error in the client or possibly in the server."; 756 | break; 757 | case 0xC0040008: 758 | msgText = "The item ID doesn't conform to the server's syntax."; 759 | break; 760 | case 0xC0040203: 761 | msgText = "The passed property ID is not valid for the item."; 762 | break; 763 | case 0xC0040011: 764 | msgText = "Requested Object (e.g. a public group) was not found."; 765 | break; 766 | case 0xC0040005: 767 | msgText = "The requested operation cannot be done on a public group."; 768 | break; 769 | case 0xC004000B: 770 | msgText = "The value was out of range."; 771 | break; 772 | case 0xC0040007: 773 | msgText = "The item ID is not defined in the server address space (on add or validate) or no longer exists in the server address space (for read or write)."; 774 | break; 775 | case 0xC004000A: 776 | msgText = "The item's access path is not known to the server."; 777 | break; 778 | case 0x0004000E: 779 | msgText = "A value passed to WRITE was accepted but the output was clamped."; 780 | break; 781 | case 0x0004000F: 782 | msgText = "The operation cannot be performed because the object is being referenced."; 783 | break; 784 | case 0x0004000D: 785 | msgText = "The server does not support the requested data rate but will use the closest available rate."; 786 | break; 787 | case 0x00000061: 788 | msgText = "Clsid syntax is invalid"; 789 | break; 790 | default: 791 | msgText = "Unknown error!"; 792 | } 793 | return errorCode.toString(16) + " - " + msgText; 794 | } 795 | RED.nodes.registerType("opc-da out", OPCDAOut); 796 | }; 797 | --------------------------------------------------------------------------------