@tier0/node-red-contrib-opcda-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,350 @@
1
+ module.exports = function(RED) {
2
+ const opcda = require('node-opc-da');
3
+ const { OPCServer } = opcda;
4
+ const { ComServer, Session, Clsid } = opcda.dcom;
5
+
6
+ const errorCode = {
7
+ 0x80040154 : "Clsid is not found.",
8
+ 0x00000005 : "Access denied. Username and/or password might be wrong.",
9
+ 0xC0040006 : "The Items AccessRights do not allow the operation.",
10
+ 0xC0040004 : "The server cannot convert the data between the specified format/ requested data type and the canonical data type.",
11
+ 0xC004000C : "Duplicate name not allowed.",
12
+ 0xC0040010 : "The server's configuration file is an invalid format.",
13
+ 0xC0040009 : "The filter string was not valid",
14
+ 0xC0040001 : "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.",
15
+ 0xC0040008 : "The item ID doesn't conform to the server's syntax.",
16
+ 0xC0040203 : "The passed property ID is not valid for the item.",
17
+ 0xC0040011 : "Requested Object (e.g. a public group) was not found.",
18
+ 0xC0040005 : "The requested operation cannot be done on a public group.",
19
+ 0xC004000B : "The value was out of range.",
20
+ 0xC0040007 : "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).",
21
+ 0xC004000A : "The item's access path is not known to the server.",
22
+ 0x0004000E : "A value passed to WRITE was accepted but the output was clamped.",
23
+ 0x0004000F : "The operation cannot be performed because the object is being referenced.",
24
+ 0x0004000D : "The server does not support the requested data rate but will use the closest available rate.",
25
+ 0x00000061 : "Clsid syntax is invalid"
26
+ };
27
+
28
+ function resolveError(e) {
29
+ if (errorCode[e]) return errorCode[e];
30
+ if (typeof e === 'number') return `DCOM error code: 0x${(e >>> 0).toString(16).toUpperCase()}`;
31
+ if (e instanceof Error) return e.message || e.toString();
32
+ if (typeof e === 'string') return e;
33
+ try { return JSON.stringify(e); } catch (_) { return String(e); }
34
+ }
35
+
36
+ function OPCDARead(config) {
37
+ RED.nodes.createNode(this,config);
38
+ let node = this;
39
+
40
+ let server = RED.nodes.getNode(config.server);
41
+ let serverHandles, clientHandles;
42
+
43
+ node.opcServer = null;
44
+ node.comServer = null;
45
+
46
+ node.opcSyncIO = null;
47
+ node.opcItemMgr = null;
48
+
49
+ node.opcGroup = null;
50
+
51
+ node.isConnected = false;
52
+ node.isReading = false;
53
+
54
+ if(!server){
55
+ node.error("Please select a server.");
56
+ return;
57
+ }
58
+
59
+ if (!server.credentials) {
60
+ node.error("Failed to load credentials!");
61
+ return;
62
+ }
63
+
64
+ node.updateStatus = function(status){
65
+ switch(status){
66
+ case "disconnected":
67
+ node.status({fill:"red",shape:"ring",text:"Disconnected"});
68
+ break;
69
+ case "timeout":
70
+ node.status({fill:"red",shape:"ring",text:"Timeout"});
71
+ break;
72
+ case "connecting":
73
+ node.status({fill:"yellow",shape:"ring",text:"Connecting"});
74
+ break;
75
+ case "error":
76
+ node.status({fill:"red",shape:"ring",text:"Error"});
77
+ break;
78
+ case "noitem":
79
+ node.status({fill:"yellow",shape:"ring",text:"No Item"});
80
+ break;
81
+ case "badquality":
82
+ node.status({fill:"red",shape:"ring",text:"Bad Quality"});
83
+ break;
84
+ case "goodquality":
85
+ node.status({fill:"blue",shape:"ring",text:"Good Quality"});
86
+ break;
87
+ case "ready":
88
+ node.status({fill:"green",shape:"ring",text:"Ready"});
89
+ break;
90
+ case "reading":
91
+ node.status({fill:"blue",shape:"ring",text:"Reading"});
92
+ break;
93
+ case "mismatch":
94
+ node.status({fill:"yellow",shape:"ring",text:"Mismatch Data"});
95
+ break;
96
+ default:
97
+ node.status({fill:"grey",shape:"ring",text:"Unknown"});
98
+ break;
99
+ }
100
+ }
101
+
102
+ node.init = function(){
103
+ return new Promise(async function(resolve, reject){
104
+ if(!node.isConnected){
105
+ try{
106
+ node.updateStatus('connecting');
107
+
108
+ var timeout = parseInt(server.config.timeout);
109
+ var comSession = new Session();
110
+
111
+ comSession = comSession.createSession(server.config.domain, server.credentials.username, server.credentials.password);
112
+ comSession.setGlobalSocketTimeout(timeout);
113
+
114
+ node.tout = setTimeout(function(){
115
+ node.updateStatus("timeout");
116
+ reject("Connection Timeout");
117
+ }, timeout);
118
+
119
+ node.comServer = new ComServer(new Clsid(server.config.clsid), server.config.address, comSession);
120
+ await node.comServer.init();
121
+
122
+ var comObject = await node.comServer.createInstance();
123
+ node.opcServer = new OPCServer();
124
+ await node.opcServer.init(comObject);
125
+
126
+ clearTimeout(node.tout);
127
+
128
+ serverHandles = [];
129
+ clientHandles = [];
130
+
131
+ node.opcGroup = await node.opcServer.addGroup(config.id, null);
132
+ node.opcItemMgr = await node.opcGroup.getItemManager();
133
+ node.opcSyncIO = await node.opcGroup.getSyncIO();
134
+
135
+ let clientHandle = 1;
136
+ var itemsList = config.groupitems.map(e => {
137
+ return { itemID: e, clientHandle: clientHandle++ };
138
+ });
139
+
140
+ var addedItems = await node.opcItemMgr.add(itemsList);
141
+
142
+ for(let i=0; i < addedItems.length; i++ ){
143
+ const addedItem = addedItems[i];
144
+ const item = itemsList[i];
145
+
146
+ if (addedItem[0] !== 0) {
147
+ node.warn(`Error adding item '${item.itemID}'`);
148
+ }
149
+ else {
150
+ serverHandles.push(addedItem[1].serverHandle);
151
+ clientHandles[item.clientHandle] = item.itemID;
152
+ }
153
+ }
154
+
155
+ node.isConnected = true;
156
+ node.updateStatus('ready');
157
+
158
+ resolve();
159
+ }
160
+ catch(e){
161
+ reject(e);
162
+ }
163
+ }
164
+ });
165
+ }
166
+
167
+ node.destroy = function(){
168
+ return new Promise(async function(resolve){
169
+ try{
170
+ node.isConnected = false;
171
+
172
+ if (node.opcSyncIO) {
173
+ await node.opcSyncIO.end();
174
+ node.opcSyncIO = null;
175
+ }
176
+
177
+ if (node.opcItemMgr) {
178
+ await node.opcItemMgr.end();
179
+ node.opcItemMgr = null;
180
+ }
181
+
182
+ if (node.opcGroup) {
183
+ await node.opcGroup.end();
184
+ node.opcGroup = null;
185
+ }
186
+
187
+ if(node.opcServer){
188
+ node.opcServer.end();
189
+ node.opcServer = null;
190
+ }
191
+
192
+ if(node.comServer){
193
+ node.comServer.closeStub();
194
+ node.comServer = null;
195
+ }
196
+
197
+ resolve();
198
+ }
199
+ catch(e){
200
+ reject(e);
201
+ }
202
+ });
203
+ }
204
+
205
+ let oldValues = [];
206
+ node.readGroup = function readGroup(cache){
207
+ var dataSource = cache ? opcda.constants.opc.dataSource.CACHE : opcda.constants.opc.dataSource.DEVICE;
208
+
209
+ let valuesTmp = [];
210
+ node.isReading = true;
211
+ node.opcSyncIO.read(dataSource, serverHandles).then(valueSets => {
212
+
213
+ var datas = [];
214
+
215
+ let changed = false;
216
+ let isGood = true;
217
+
218
+ for(let i in valueSets){
219
+
220
+ if(config.datachange){
221
+ if(!changed){
222
+ if(oldValues.length != valueSets.length || valueSets[i].value != oldValues[i]){
223
+ changed = true;
224
+ }
225
+ }
226
+
227
+ valuesTmp[i] = valueSets[i].value;
228
+ oldValues[i] = valueSets[i].value;
229
+ }
230
+
231
+ var quality;
232
+
233
+ if(valueSets[i].quality >= 0 && valueSets[i].quality < 64){
234
+ quality = "BAD";
235
+ isGood = false;
236
+ }
237
+ else if(valueSets[i].quality >= 64 && valueSets[i].quality < 192){
238
+ quality = "UNCERTAIN";
239
+ isGood = false;
240
+ }
241
+ else if(valueSets[i].quality >= 192 && valueSets[i].quality <= 219){
242
+ quality = "GOOD";
243
+ }
244
+ else{
245
+ quality = "UNKNOWN";
246
+ isGood = false;
247
+ }
248
+
249
+ var data = {
250
+ itemID: clientHandles[valueSets[i].clientHandle],
251
+ errorCode: valueSets[i].errorCode,
252
+ quality: quality,
253
+ timestamp: valueSets[i].timestamp,
254
+ value: valueSets[i].value,
255
+ }
256
+
257
+ datas.push(data);
258
+ }
259
+
260
+ if(isGood){
261
+ if(config.groupitems.length == datas.length){
262
+ node.updateStatus('goodquality');
263
+ }
264
+
265
+ if(config.groupitems.length != datas.length){
266
+ node.updateStatus('mismatch');
267
+ }
268
+
269
+ if(config.groupitems.length < 1){
270
+ node.updateStatus('noitem');
271
+ }
272
+
273
+ if(config.datachange){
274
+ oldValues = valuesTmp;
275
+ if(changed){
276
+ var msg = { payload: datas };
277
+ node.send(msg);
278
+ }
279
+ }
280
+
281
+ else{
282
+ var msg = { payload: datas };
283
+ node.send(msg);
284
+ }
285
+ }
286
+ else{
287
+ node.updateStatus('badquality');
288
+ }
289
+
290
+ node.isReading = false;
291
+ }).catch(e => {
292
+ node.error("opcda-error", e.message);
293
+ node.updateStatus("error");
294
+ node.reconnect();
295
+ });
296
+ }
297
+
298
+ node.isReconnecting = false;
299
+ node.reconnect = async function(){
300
+ try{
301
+ if(!node.isReconnecting){
302
+ node.isReconnecting = true;
303
+ await node.destroy();
304
+ await new Promise(resolve => setTimeout(resolve, 3000));
305
+ await node.init();
306
+ node.isReconnecting = false;
307
+ }
308
+
309
+ node.comServer.on('disconnected',async function(){
310
+ node.isConnected = false;
311
+ node.updateStatus('disconnected');
312
+ await node.reconnect();
313
+ });
314
+ }
315
+ catch(e){
316
+ node.isReconnecting = false;
317
+ var msg = resolveError(e);
318
+ node.updateStatus('error');
319
+ node.error(`OPC DA connection error: ${msg}`);
320
+ switch(e) {
321
+ case 0x00000005:
322
+ case 0xC0040010:
323
+ case 0x80040154:
324
+ case 0x00000061:
325
+ return;
326
+ default:
327
+ await node.reconnect();
328
+ }
329
+ }
330
+ }
331
+
332
+ node.reconnect();
333
+
334
+ node.on('input', function(){
335
+ if(node.isConnected && !node.isReading){
336
+ node.readGroup(config.cache);
337
+ }
338
+ });
339
+
340
+ node.on('close', function(done){
341
+ node.status({});
342
+ node.destroy().then(function(){
343
+ done();
344
+ });
345
+ });
346
+
347
+ }
348
+
349
+ RED.nodes.registerType("tier0-opcda-read",OPCDARead);
350
+ }
@@ -0,0 +1,143 @@
1
+ <script type="text/html" data-template-name="tier0-opcda-server">
2
+ <div class="form-row">
3
+ <label for="node-config-input-name"><i class="fa fa-tasks"></i> Name</label>
4
+ <input type="text" id="node-config-input-name" placeholder="Name">
5
+ </div>
6
+ <div class="form-row">
7
+ <label for="node-config-input-address"><i class="fa fa-rss"></i> Address</label>
8
+ <input type="text" id="node-config-input-address" placeholder="x.x.x.x or Hostname">
9
+ </div>
10
+ <div class="form-row">
11
+ <label for="node-config-input-domain"><i class="fa fa-sitemap"></i> Domain</label>
12
+ <input type="text" id="node-config-input-domain" placeholder="Domain">
13
+ </div>
14
+ <div class="form-row">
15
+ <label for="node-config-input-username"><i class="fa fa-user"></i> User Name</label>
16
+ <input type="text" id="node-config-input-username" placeholder="Name">
17
+ </div>
18
+ <div class="form-row">
19
+ <label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
20
+ <input type="password" id="node-config-input-password" placeholder="Password">
21
+ </div>
22
+ <div class="form-row">
23
+ <label for="node-config-input-clsid"><i class="fa fa-tasks"></i> ClsId</label>
24
+ <input type="text" id="node-config-input-clsid" placeholder="ClsId">
25
+ </div>
26
+ <div class="form-row">
27
+ <div style="display:inline">
28
+ <label for="node-config-input-timeout"><i class="fa fa-refresh"></i> Timeout</label>
29
+ <input type="text" id="node-config-input-timeout" style="width: 60px;" placeholder="5000"> <span>ms</span>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="form-row">
34
+ <button class="editor-button" id="node-config-btn-item-browse"><i class="fa fa-search"></i> Browse</button>
35
+ <button href="#" class="editor-button" id="node-config-btn-item-export" style="margin: 4px"><i class="fa fa-download"></i> Export</button>
36
+ </div>
37
+ <div class="form-row">
38
+ <p class="form-tips" id="node-config-alert"></p>
39
+ <ol id="node-config-item-list" style="list-style-position:inside; margin:0; padding: 5px; height: 300px; border:1px solid #cccccc; overflow:scroll"></ol>
40
+ </div>
41
+ </script>
42
+
43
+ <script type="text/html" data-help-name="tier0-opcda-server">
44
+ <p>Opcda Server Node. For more details please visit https://github.com/FREEZONEX/opcda-client</p>
45
+ </script>
46
+
47
+ <script type="text/javascript">
48
+ RED.nodes.registerType('tier0-opcda-server',{
49
+ category: 'config',
50
+ defaults: {
51
+ name: {value: ""},
52
+ address: {value: "", required: true},
53
+ domain: {value: "", required: true},
54
+ clsid: {value: "", required: true},
55
+ timeout: {value: 5000},
56
+ },
57
+ credentials: {
58
+ username: { type: "text", required: true },
59
+ password: { type: "password", required: true }
60
+ },
61
+ label: function() {
62
+ return this.name||"tier0-opcda-server";
63
+ },
64
+ oneditprepare: function () {
65
+ let browseBtn = $('#node-config-btn-item-browse');
66
+ let browseBtnIcon = browseBtn.children('i');
67
+ let exportBtn = $('#node-config-btn-item-export');
68
+ let exportBtnIcon = exportBtn.children('i');
69
+ let alertArea = $('#node-config-alert');
70
+ let itemList = $('#node-config-item-list');
71
+
72
+ console.log(this.credentials);
73
+
74
+ $("#node-config-input-timeout").spinner().val(5000);
75
+
76
+ itemList.hide();
77
+ alertArea.hide();
78
+ alertArea.empty();
79
+ exportBtn.addClass('disabled').attr('disabled', 'disabled');
80
+
81
+ browseBtn.click(function(){
82
+ itemList.hide();
83
+ alertArea.hide();
84
+ alertArea.empty();
85
+
86
+ browseBtn.addClass('disabled').attr('disabled', 'disabled');
87
+ browseBtnIcon.removeClass('fa-search').addClass('fa-spinner fa-spin fa-fw');
88
+ exportBtn.addClass('disabled').attr('disabled', 'disabled');
89
+
90
+ var queryData = {
91
+ address: $('#node-config-input-address').val(),
92
+ domain: $('#node-config-input-domain').val(),
93
+ username: $('#node-config-input-username').val(),
94
+ password: $('#node-config-input-password').val(),
95
+ clsid: $('#node-config-input-clsid').val(),
96
+ timeout: $('#node-config-input-timeout').val()
97
+ };
98
+
99
+ console.log(queryData);
100
+
101
+ $.get("opcda/browse", queryData)
102
+ .done(function( data, status, jqXHR ) {
103
+ alertArea.text(data.items.length + ' items found');
104
+ itemList.empty();
105
+ $.each(data.items, function(i, item){
106
+ $('<li/>').text(item).appendTo(itemList);
107
+ });
108
+
109
+ itemList.show();
110
+ exportBtn.removeClass('disabled').removeAttr('disabled');
111
+
112
+ })
113
+ .fail(function( jqXHR, status, errorThrown ) {
114
+ alertArea.text(jqXHR.responseJSON.error);
115
+ })
116
+ .always(function(){
117
+ browseBtnIcon.removeClass('fa-spinner fa-spin fa-fw').addClass('fa-search');
118
+ browseBtn.removeClass('disabled').removeAttr('disabled');
119
+ alertArea.show();
120
+ });
121
+ });
122
+
123
+ exportBtn.click(function(){
124
+ var items = itemList.children('li');
125
+ var itemData = [];
126
+
127
+ items.each(function (i) {
128
+ itemData.push($(this).text());
129
+ });
130
+
131
+ var jsonData = "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(itemData, null, 4));
132
+ console.log();
133
+ const a = document.createElement('a');
134
+ a.href = 'data:' + jsonData;
135
+ a.download = 'export.json';
136
+ a.click();
137
+ });
138
+ },
139
+ oneditsave: function () {
140
+
141
+ }
142
+ });
143
+ </script>
@@ -0,0 +1,91 @@
1
+ module.exports = function(RED) {
2
+ const opcda = require('node-opc-da');
3
+ const { OPCServer } = opcda;
4
+ const { ComServer, Session, Clsid } = opcda.dcom;
5
+
6
+ const errorCode = {
7
+ 0x80040154 : "Clsid is not found.",
8
+ 0x00000005 : "Access denied. Username and/or password might be wrong.",
9
+ 0xC0040006 : "The Items AccessRights do not allow the operation.",
10
+ 0xC0040004 : "The server cannot convert the data between the specified format/ requested data type and the canonical data type.",
11
+ 0xC004000C : "Duplicate name not allowed.",
12
+ 0xC0040010 : "The server's configuration file is an invalid format.",
13
+ 0xC0040009 : "The filter string was not valid",
14
+ 0xC0040001 : "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.",
15
+ 0xC0040008 : "The item ID doesn't conform to the server's syntax.",
16
+ 0xC0040203 : "The passed property ID is not valid for the item.",
17
+ 0xC0040011 : "Requested Object (e.g. a public group) was not found.",
18
+ 0xC0040005 : "The requested operation cannot be done on a public group.",
19
+ 0xC004000B : "The value was out of range.",
20
+ 0xC0040007 : "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).",
21
+ 0xC004000A : "The item's access path is not known to the server.",
22
+ 0x0004000E : "A value passed to WRITE was accepted but the output was clamped.",
23
+ 0x0004000F : "The operation cannot be performed because the object is being referenced.",
24
+ 0x0004000D : "The server does not support the requested data rate but will use the closest available rate.",
25
+ 0x00000061 : "Clsid syntax is invalid"
26
+ };
27
+
28
+ function resolveError(e) {
29
+ if (errorCode[e]) return errorCode[e];
30
+ if (typeof e === 'number') return `DCOM error code: 0x${(e >>> 0).toString(16).toUpperCase()}`;
31
+ if (e instanceof Error) return e.message || e.toString();
32
+ if (typeof e === 'string') return e;
33
+ try { return JSON.stringify(e); } catch (_) { return String(e); }
34
+ }
35
+
36
+ RED.httpAdmin.get('/opcda/browse', RED.auth.needsPermission('node-opc-da.list'), function (req, res) {
37
+ let params = req.query
38
+ async function browseItems() {
39
+ try{
40
+ var session = new Session();
41
+ session = session.createSession(params.domain, params.username, params.password);
42
+ session.setGlobalSocketTimeout(params.timeout);
43
+
44
+ var comServer = new ComServer(new Clsid(params.clsid), params.address, session);
45
+ await comServer.init();
46
+
47
+ var comObject = await comServer.createInstance();
48
+
49
+ var opcServer = new opcda.OPCServer();
50
+ await opcServer.init(comObject);
51
+
52
+ var opcBrowser = await opcServer.getBrowser();
53
+ var itemList = await opcBrowser.browseAllFlat();
54
+
55
+ opcBrowser.end()
56
+ .then(() => opcServer.end())
57
+ .then(() => comServer.closeStub())
58
+ .catch(e => RED.log.error(`Error closing browse session: ${e}`));
59
+
60
+ res.status(200).send({items : itemList});
61
+ }
62
+ catch(e){
63
+ var msg = resolveError(e);
64
+ RED.log.error(`OPC DA browse error: ${msg}`);
65
+ RED.log.error(e);
66
+ res.status(500).send({error : msg});
67
+ }
68
+ }
69
+
70
+ browseItems();
71
+ });
72
+
73
+ function OPCDAServer(config) {
74
+ RED.nodes.createNode(this,config);
75
+ const node = this;
76
+
77
+ node.config = config;
78
+
79
+
80
+ node.on('close', function(done){
81
+ done();
82
+ });
83
+ }
84
+
85
+ RED.nodes.registerType("tier0-opcda-server", OPCDAServer, {
86
+ credentials: {
87
+ username: {type:"text"},
88
+ password: {type:"password"}
89
+ }
90
+ });
91
+ }
@@ -0,0 +1,32 @@
1
+ <script type="text/html" data-template-name="tier0-opcda-write">
2
+ <div class="form-row">
3
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-input-name" placeholder="Name">
5
+ </div>
6
+ <div class="form-row">
7
+ <label for="node-input-server"><i class="fa fa-server"></i> Server</label>
8
+ <input id="node-input-server" placeholder="Select OPC DA Server">
9
+ </div>
10
+ </script>
11
+
12
+ <script type="text/html" data-help-name="tier0-opcda-write">
13
+ <p>Opcda Write Node. For more details please visit https://github.com/FREEZONEX/opcda-client</p>
14
+ </script>
15
+
16
+ <script type="text/javascript">
17
+ RED.nodes.registerType('tier0-opcda-write',{
18
+ category: 'opcda',
19
+ color: '#a6bbcf',
20
+ defaults: {
21
+ server: {value: "", type: "tier0-opcda-server", required: true},
22
+ name: {value: ""},
23
+ },
24
+ inputs:1,
25
+ outputs:1,
26
+ icon: "serial.png",
27
+ align: 'right',
28
+ label: function() {
29
+ return this.name||this._("tier0-opcda-write");
30
+ }
31
+ });
32
+ </script>