├── .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 | 
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 | 
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 | |  |  |
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 |
--------------------------------------------------------------------------------