@tier0/node-red-contrib-opcda-client 1.0.4 → 1.0.5

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.
@@ -32,7 +32,7 @@
32
32
  </script>
33
33
 
34
34
  <script type="text/html" data-help-name="tier0-opcda-read">
35
- <p>Opcda Read Node. For more details please visit https://github.com/FREEZONEX/opcda-client</p>
35
+ <p>Opcda Read Node. For more details please visit https://github.com/emrebekar/node-red-contrib-opcda-client</p>
36
36
  </script>
37
37
 
38
38
  <script type="text/javascript">
@@ -1,5 +1,5 @@
1
1
  module.exports = function(RED) {
2
- const opcda = require('node-opc-da');
2
+ const opcda = require('@tier0/node-opc-da');
3
3
  const { OPCServer } = opcda;
4
4
  const { ComServer, Session, Clsid } = opcda.dcom;
5
5
 
@@ -24,38 +24,6 @@ module.exports = function(RED) {
24
24
  0x0004000D : "The server does not support the requested data rate but will use the closest available rate.",
25
25
  0x00000061 : "Clsid syntax is invalid"
26
26
  };
27
-
28
- function resolveError(e) {
29
- if (typeof e === "number") {
30
- const u = e >>> 0;
31
- if (errorCode[e] !== undefined) return errorCode[e];
32
- if (errorCode[u] !== undefined) return errorCode[u];
33
- return "HRESULT 0x" + u.toString(16).toUpperCase() + " (" + e + ")";
34
- }
35
- if (e instanceof Error && e.message) {
36
- const asNum = Number(e.message);
37
- if (!Number.isNaN(asNum) && String(asNum) === String(e.message).trim()) {
38
- return resolveError(asNum);
39
- }
40
- return e.message;
41
- }
42
- if (errorCode[e]) return errorCode[e];
43
- if (typeof e === "string") return e;
44
- try {
45
- return JSON.stringify(e);
46
- } catch (_) {
47
- return String(e);
48
- }
49
- }
50
-
51
- /** IOPCItemMgt::Add per-item result (may be signed negative in JS). */
52
- function describeOpcItemResult(code) {
53
- if (code === 0) return "OK";
54
- const unsigned = code >>> 0;
55
- const fromOpc = opcda.constants.opc.errorDesc[String(unsigned)];
56
- if (fromOpc) return fromOpc + " (0x" + unsigned.toString(16).toUpperCase() + ")";
57
- return "OPC/DCOM result 0x" + unsigned.toString(16).toUpperCase() + " (" + code + ")";
58
- }
59
27
 
60
28
  function OPCDARead(config) {
61
29
  RED.nodes.createNode(this,config);
@@ -129,12 +97,9 @@ module.exports = function(RED) {
129
97
  try{
130
98
  node.updateStatus('connecting');
131
99
 
132
- var timeout = parseInt(server.config.timeout, 10);
133
- if (!Number.isFinite(timeout) || timeout <= 0) {
134
- timeout = 15000;
135
- }
100
+ var timeout = parseInt(server.config.timeout);
136
101
  var comSession = new Session();
137
-
102
+
138
103
  comSession = comSession.createSession(server.config.domain, server.credentials.username, server.credentials.password);
139
104
  comSession.setGlobalSocketTimeout(timeout);
140
105
 
@@ -171,7 +136,7 @@ module.exports = function(RED) {
171
136
  const item = itemsList[i];
172
137
 
173
138
  if (addedItem[0] !== 0) {
174
- node.warn("Error adding item '" + item.itemID + "': " + describeOpcItemResult(addedItem[0]));
139
+ node.warn(`Error adding item '${item.itemID}'`);
175
140
  }
176
141
  else {
177
142
  serverHandles.push(addedItem[1].serverHandle);
@@ -341,18 +306,24 @@ module.exports = function(RED) {
341
306
  }
342
307
  catch(e){
343
308
  node.isReconnecting = false;
344
- var msg = resolveError(e);
345
- node.updateStatus('error');
346
- node.error(`OPC DA connection error: ${msg}`);
347
- switch(e) {
348
- case 0x00000005:
349
- case 0xC0040010:
350
- case 0x80040154:
351
- case 0x00000061:
352
- return;
353
- default:
354
- await node.reconnect();
309
+ if(errorCode[e]){
310
+ node.updateStatus('error');
311
+ switch(e) {
312
+ case 0x00000005:
313
+ case 0xC0040010:
314
+ case 0x80040154:
315
+ case 0x00000061:
316
+ node.error(errorCode[e]);
317
+ return;
318
+ default:
319
+ node.error(errorCode[e]);
320
+ await node.reconnect();
321
+ }
355
322
  }
323
+ else{
324
+ node.error(e);
325
+ await node.reconnect();
326
+ }
356
327
  }
357
328
  }
358
329
 
@@ -41,7 +41,7 @@
41
41
  </script>
42
42
 
43
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>
44
+ <p>Opcda Server Node. For more details please visit https://github.com/emrebekar/node-red-contrib-opcda-client</p>
45
45
  </script>
46
46
 
47
47
  <script type="text/javascript">
@@ -50,7 +50,7 @@
50
50
  defaults: {
51
51
  name: {value: ""},
52
52
  address: {value: "", required: true},
53
- domain: {value: ""},
53
+ domain: {value: "", required: true},
54
54
  clsid: {value: "", required: true},
55
55
  timeout: {value: 5000},
56
56
  },
@@ -63,31 +63,22 @@
63
63
  },
64
64
  oneditprepare: function () {
65
65
  let self = this;
66
- /**
67
- * Admin route RED.httpAdmin.get('/opcda/browse').
68
- * Tier0 + Kong: public path is /nodered/home/... — must NOT request bare /opcda/browse (hits wrong service → HTML 200).
69
- * Node-RED sets RED.settings.apiRootUrl for proxy subpaths; use it first.
70
- */
71
66
  function browseApiUrl() {
72
67
  var api = RED.settings && RED.settings.apiRootUrl;
73
68
  if (api) {
74
69
  var u = String(api);
75
- if (!/\/$/.test(u)) {
76
- u += '/';
77
- }
78
- return u + 'tier0-opcda/browse';
70
+ if (!/\/$/.test(u)) u += '/';
71
+ return u + 'opcda/browse';
79
72
  }
80
73
  var s = RED.settings && RED.settings.httpAdminRoot;
81
74
  if (s != null && String(s).trim() !== '' && String(s) !== '/') {
82
75
  var root = String(s).replace(/\/+$/, '');
83
- return (root ? root : '') + '/tier0-opcda/browse';
76
+ return (root ? root : '') + '/opcda/browse';
84
77
  }
85
78
  var p = window.location.pathname || '';
86
79
  p = p.replace(/\/+$/, '');
87
- if (p && p !== '/') {
88
- return (p + '/tier0-opcda/browse').replace(/\/+/g, '/');
89
- }
90
- return '/tier0-opcda/browse';
80
+ if (p && p !== '/') return (p + '/opcda/browse').replace(/\/+/g, '/');
81
+ return '/opcda/browse';
91
82
  }
92
83
  let browseBtn = $('#node-config-btn-item-browse');
93
84
  let browseBtnIcon = browseBtn.children('i');
@@ -128,7 +119,7 @@
128
119
  data: queryData,
129
120
  dataType: 'json'
130
121
  })
131
- .done(function( data /*, status, jqXHR */ ) {
122
+ .done(function( data ) {
132
123
  var items = data && data.items;
133
124
  if (!Array.isArray(items)) {
134
125
  alertArea.text('Browse returned an invalid response (expected JSON { items: [...] }). See Node-RED log.');
@@ -149,10 +140,10 @@
149
140
  .fail(function( jqXHR, textStatus, errorThrown ) {
150
141
  var errText;
151
142
  if (jqXHR.status === 403) {
152
- errText = 'Permission denied. Node-RED 4+: grant role permission "OPC DA: browse address space" (node-opc-da.list), then reload the editor.';
143
+ errText = 'Permission denied. Grant node-opc-da.list to your role (Node-RED 4+).';
153
144
  } else if (textStatus === 'parsererror') {
154
- errText = 'Browse got HTML instead of JSON (wrong path). Tried: ' + browseApiUrl() +
155
- '. Open DevTools → Network and confirm this URL returns JSON. Status ' + (jqXHR.status || '?') + '.';
145
+ errText = 'Browse got HTML instead of JSON. Tried: ' + browseApiUrl() +
146
+ '. Check Network tab. Status ' + (jqXHR.status || '?') + '.';
156
147
  } else {
157
148
  errText = (jqXHR.responseJSON && jqXHR.responseJSON.error) ||
158
149
  jqXHR.responseText || errorThrown || textStatus || 'Request failed';
@@ -1,13 +1,11 @@
1
1
  module.exports = function(RED) {
2
- const opcda = require('node-opc-da');
2
+ const opcda = require('@tier0/node-opc-da');
3
3
  const { OPCServer } = opcda;
4
4
  const { ComServer, Session, Clsid } = opcda.dcom;
5
-
6
- const ACCESS_DENIED_HINT = " Check DOMAIN (Windows PC name for local accounts, e.g. DESKTOP-XXX), password, DCOM rights on the OPC host, and Deploy the server config so Browse uses saved credentials.";
7
-
5
+
8
6
  const errorCode = {
9
7
  0x80040154 : "Clsid is not found.",
10
- 0x00000005 : "Access denied (DCOM)." + ACCESS_DENIED_HINT,
8
+ 0x00000005 : "Access denied. Username and/or password might be wrong.",
11
9
  0xC0040006 : "The Items AccessRights do not allow the operation.",
12
10
  0xC0040004 : "The server cannot convert the data between the specified format/ requested data type and the canonical data type.",
13
11
  0xC004000C : "Duplicate name not allowed.",
@@ -25,41 +23,33 @@ module.exports = function(RED) {
25
23
  0x0004000F : "The operation cannot be performed because the object is being referenced.",
26
24
  0x0004000D : "The server does not support the requested data rate but will use the closest available rate.",
27
25
  0x00000061 : "Clsid syntax is invalid",
28
- 0x80004002 : "No such interface (E_NOINTERFACE)."
26
+ 0x80004002 : "No such interface (E_NOINTERFACE).",
27
+ 2147500034 : "No such interface (E_NOINTERFACE)."
29
28
  };
30
29
 
31
- function resolveError(e) {
32
- if (typeof e === "number") {
33
- const u = e >>> 0;
34
- if (u === 0x80070005 || e === 5 || e === -2147024891) {
35
- return "Access denied (DCOM 0x80070005 / 0x5)." + ACCESS_DENIED_HINT;
36
- }
37
- if (errorCode[e] !== undefined) return errorCode[e];
30
+ function formatBrowseError(err) {
31
+ if (typeof err === "number") {
32
+ const u = err >>> 0;
33
+ if (errorCode[err] !== undefined) return errorCode[err];
38
34
  if (errorCode[u] !== undefined) return errorCode[u];
39
- return "HRESULT 0x" + u.toString(16).toUpperCase() + " (" + e + ")";
35
+ return "HRESULT 0x" + u.toString(16) + " (" + err + ")";
40
36
  }
41
- if (e instanceof Error && e.message) {
42
- const asNum = Number(e.message);
43
- if (!Number.isNaN(asNum) && String(asNum) === String(e.message).trim()) {
44
- return resolveError(asNum);
37
+ if (err && err.message) {
38
+ const asNum = Number(err.message);
39
+ if (!Number.isNaN(asNum) && String(asNum) === String(err.message).trim()) {
40
+ return formatBrowseError(asNum);
45
41
  }
46
- return e.message;
47
- }
48
- if (errorCode[e]) return errorCode[e];
49
- if (typeof e === "string") return e;
50
- try {
51
- return JSON.stringify(e);
52
- } catch (_) {
53
- return String(e);
42
+ return err.message;
54
43
  }
44
+ return String(err || "Unknown error.");
55
45
  }
56
46
 
57
47
  /**
58
- * Use deployed tier0-opcda-server credentials when the editor password field is empty.
48
+ * Merge query params with deployed opcda-server config node so Browse works when
49
+ * the password field is empty in the editor (Node-RED never fills stored passwords).
59
50
  */
60
51
  function resolveBrowseParams(query) {
61
52
  const params = Object.assign({}, query);
62
- // Node-RED puts this in the password field when creds exist but user did not re-type the password.
63
53
  if (params.password === "__PWRD__" || params.password === "__PASSWORD__") {
64
54
  delete params.password;
65
55
  }
@@ -67,18 +57,14 @@ module.exports = function(RED) {
67
57
  if (id) {
68
58
  const srv = RED.nodes.getNode(id);
69
59
  if (srv && srv.credentials) {
70
- if (srv.address) params.address = srv.address;
71
- if (srv.clsid) params.clsid = srv.clsid;
72
- if (srv.timeout != null && srv.timeout !== "") params.timeout = srv.timeout;
73
60
  if (srv.domain != null && String(srv.domain).trim() !== "") {
74
61
  params.domain = String(srv.domain).trim();
75
62
  }
76
- if (srv.credentials.username) {
77
- params.username = srv.credentials.username;
78
- }
79
- if (srv.credentials.password) {
80
- params.password = srv.credentials.password;
81
- }
63
+ if (srv.address) params.address = srv.address;
64
+ if (srv.clsid) params.clsid = srv.clsid;
65
+ if (srv.timeout != null && srv.timeout !== "") params.timeout = srv.timeout;
66
+ if (srv.credentials.username) params.username = srv.credentials.username;
67
+ if (srv.credentials.password) params.password = srv.credentials.password;
82
68
  } else if (id) {
83
69
  RED.log.warn("OPC DA browse: config node id not in runtime (Deploy flows?) — using form/query fields only.");
84
70
  }
@@ -95,7 +81,7 @@ module.exports = function(RED) {
95
81
  return params;
96
82
  }
97
83
 
98
- RED.httpAdmin.get("/tier0-opcda/browse", RED.auth.needsPermission("node-opc-da.list"), function (req, res) {
84
+ RED.httpAdmin.get('/opcda/browse', RED.auth.needsPermission('node-opc-da.list'), function (req, res) {
99
85
  async function browseItems() {
100
86
  const params = resolveBrowseParams(req.query);
101
87
  try {
@@ -105,7 +91,7 @@ module.exports = function(RED) {
105
91
  }
106
92
  if (!params.username || !params.password) {
107
93
  res.status(400).send({
108
- error: "Missing username or password. Deploy flows first (Browse uses stored credentials from the tier0-opcda-server node). If the browser showed password=__PWRD__, that is not a real password — Node-RED only sends the real one after Deploy, or re-type the password in the server config and deploy."
94
+ error: "Missing username or password. Deploy flows first (Browse uses stored credentials from the opcda-server node). If the URL showed password=__PWRD__, that is not a real password — deploy or re-type the password in the server config."
109
95
  });
110
96
  return;
111
97
  }
@@ -128,12 +114,12 @@ module.exports = function(RED) {
128
114
  opcBrowser.end()
129
115
  .then(() => opcServer.end())
130
116
  .then(() => comServer.closeStub())
131
- .catch((e) => RED.log.error(`Error closing browse session: ${e}`));
117
+ .catch(e => RED.log.error(`Error closing browse session: ${e}`));
132
118
 
133
119
  res.status(200).send({items: itemList});
134
120
  } catch (e) {
135
- var msg = resolveError(e);
136
- RED.log.error(`OPC DA browse error: ${msg}`);
121
+ const msg = formatBrowseError(e);
122
+ RED.log.error(`OPC DA browse: ${msg}`);
137
123
  if (e && e.stack) RED.log.error(e.stack);
138
124
  res.status(500).send({error: msg});
139
125
  }
@@ -10,7 +10,7 @@
10
10
  </script>
11
11
 
12
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>
13
+ <p>Opcda Write Node. For more details please visit https://github.com/emrebekar/node-red-contrib-opcda-client</p>
14
14
  </script>
15
15
 
16
16
  <script type="text/javascript">
@@ -1,5 +1,5 @@
1
1
  module.exports = function(RED) {
2
- const opcda = require('node-opc-da');
2
+ const opcda = require('@tier0/node-opc-da');
3
3
  const { OPCServer } = opcda;
4
4
  const { ComServer, Session, Clsid, ComString} = opcda.dcom;
5
5
 
@@ -24,14 +24,6 @@ module.exports = function(RED) {
24
24
  0x0004000D : "The server does not support the requested data rate but will use the closest available rate.",
25
25
  0x00000061 : "Clsid syntax is invalid"
26
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
27
 
36
28
  const itemTypes = {
37
29
  "double" : opcda.dcom.Types.DOUBLE,
@@ -255,18 +247,23 @@ module.exports = function(RED) {
255
247
  }
256
248
  catch(e){
257
249
  node.isReconnecting = false;
258
- var msg = resolveError(e);
259
- node.updateStatus('error');
260
- node.error(`OPC DA connection error: ${msg}`);
261
- switch(e) {
262
- case 0x00000005:
263
- case 0xC0040010:
264
- case 0x80040154:
265
- case 0x00000061:
266
- return;
267
- default:
268
- await node.reconnect();
250
+ if(errorCode[e]){
251
+ switch(e) {
252
+ case 0x00000005:
253
+ case 0xC0040010:
254
+ case 0x80040154:
255
+ case 0x00000061:
256
+ node.error(errorCode[e]);
257
+ return;
258
+ default:
259
+ node.error(errorCode[e]);
260
+ await node.reconnect();
261
+ }
269
262
  }
263
+ else{
264
+ node.error(e);
265
+ await node.reconnect();
266
+ }
270
267
  }
271
268
  }
272
269
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tier0/node-red-contrib-opcda-client",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Node-RED OPC DA Reading and Writing Node",
5
5
  "node-red": {
6
6
  "nodes": {
@@ -1,132 +0,0 @@
1
- # OPC DA to MQTT Bridge - Deployment Guide
2
-
3
- ## 1. Prerequisites (Offline Package Contents)
4
-
5
- Before you begin, ensure you have the following files from the provided offline package:
6
-
7
- | # | File | Purpose |
8
- |---|------|---------|
9
- | 1 | `python-2.7.amd64.msi` | Python 2.7 runtime |
10
- | 2 | `pywin32-221.win-amd64-py2.7.exe` | Windows COM/DCOM support for Python |
11
- | 3 | `OpenOPC-1.3.1.win-amd64-py2.7.exe` | OPC DA client library |
12
- | 4 | `paho-mqtt-1.6.1.tar.gz` | MQTT client library (offline) |
13
- | 5 | `opctest.py` | Bridge script |
14
-
15
- > **Note:** If the script fails with a DCOM/OPC error on a machine that does NOT have KEPServerEX or any OPC software installed, you may also need to install `OPC Core Components Redistributable (x64).msi` to register `opcdaauto.dll`.
16
-
17
- ## 2. Installation Steps
18
-
19
- ### Step 1: Install Python 2.7
20
-
21
- 1. Run `python-2.7.amd64.msi`
22
- 2. **Important:** In the installation options, check **"Add python.exe to Path"**
23
- 3. Keep the default installation path: `C:\Python27\`
24
- 4. After installation, open CMD and verify:
25
- ```
26
- C:\Python27\python.exe --version
27
- ```
28
- Expected output: `Python 2.7.x`
29
-
30
- ### Step 2: Install pywin32 (DCOM Support)
31
-
32
- 1. Run `pywin32-221.win-amd64-py2.7.exe`
33
- 2. The installer will automatically detect the Python 2.7 path
34
- 3. Click "Next" through the installation
35
-
36
- ### Step 3: Install OpenOPC
37
-
38
- 1. Run `OpenOPC-1.3.1.win-amd64-py2.7.exe`
39
- 2. The installer will automatically detect the Python 2.7 path
40
- 3. Click "Next" through the installation
41
-
42
- ### Step 4: Install paho-mqtt (Offline)
43
-
44
- 1. Extract `paho-mqtt-1.6.1.tar.gz` to a folder (e.g. `C:\temp\paho-mqtt-1.6.1\`)
45
- 2. Open **Command Prompt (CMD)** and run:
46
- ```
47
- cd C:\yourpath\paho-mqtt-1.6.1
48
- C:\Python27\python.exe setup.py install
49
- ```
50
-
51
- ## 3. Configuration
52
-
53
- Open `opctest.py` in a text editor (e.g. Notepad) and update the following settings at the top of the file:
54
-
55
- ```
56
- OPC_SERVER = 'Kepware.KEPServerEX.V6' # OPC DA server ProgID
57
- OPC_HOST = '192.168.31.75' # IP of the OPC DA server machine
58
- MQTT_BROKER = '192.168.31.45' # IP of the MQTT broker (Node-RED)
59
- MQTT_PORT = 1883 # MQTT port
60
- POLL_INTERVAL = 1 # Read interval in seconds
61
- ```
62
-
63
- Update the `TAGS` list with the actual OPC DA tag names:
64
-
65
- ```
66
- TAGS = [
67
- u'TI2022.PV',
68
- u'TI2022.SV',
69
- u'TI2022.MV',
70
- # Add more tags as needed...
71
- ]
72
- ```
73
-
74
- ## 4. Run the Bridge
75
-
76
- 1. Open **Command Prompt as Administrator** (right-click CMD > "Run as administrator")
77
- 2. Navigate to the folder containing `opctest.py`:
78
- ```
79
- cd C:\path\to\opctest
80
- ```
81
- 3. Run the script:
82
- ```
83
- C:\Python27\python.exe opctest.py
84
- ```
85
- 4. You should see output like:
86
- ```
87
- [*] OPC DA -> MQTT Bridge
88
- OPC Server: Kepware.KEPServerEX.V6 @ 192.168.31.75
89
- MQTT Broker: 192.168.31.45:1883
90
- Tags: 3
91
-
92
- [+] UNS import JSON written to uns_import.json
93
- [+] MQTT connected
94
- [+] OPC DA connected
95
- [*] Publishing data every 1s (Ctrl+C to stop)...
96
- --------------------------------------------------
97
- [18:30:01] Published 3 tags
98
- [18:30:02] Published 3 tags
99
- ```
100
- 5. Press `Ctrl+C` to stop the bridge gracefully
101
-
102
- ## 5. Import UNS Structure into Tier 0
103
-
104
- 1. After the script runs, a file named `uns_import.json` is generated in the same folder
105
- 2. Open the Tier 0 platform
106
- 3. Navigate to the UNS import function
107
- 4. Upload `uns_import.json`
108
- 5. The topic tree will be created automatically (e.g. `opcda/TI2022/Metric/PV`)
109
-
110
- ## 6. Troubleshooting
111
-
112
- | Error | Solution |
113
- |-------|----------|
114
- | `'python' is not recognized` | Python was not added to PATH. Use the full path: `C:\Python27\python.exe` |
115
- | `No module named OpenOPC` | OpenOPC not installed. Re-run Step 4 |
116
- | `No module named paho` | paho-mqtt not installed. Re-run Step 5 |
117
- | `Access denied` / DCOM error | Run CMD as **Administrator** |
118
- | `OPC server not found` | Verify `OPC_SERVER` name and that the OPC server is running on the target machine |
119
- | `MQTT connection refused` | Verify `MQTT_BROKER` IP and that the MQTT broker is running on port 1883 |
120
- | Script stops unexpectedly | Check network connectivity to both OPC and MQTT servers |
121
-
122
- ## 7. Run as a Windows Service (Optional)
123
-
124
- To keep the bridge running in the background after logoff, you can use **NSSM** (Non-Sucking Service Manager):
125
-
126
- 1. Download `nssm.exe` and place it in the script folder
127
- 2. Open CMD as Administrator and run:
128
- ```
129
- nssm install OPCDABridge "C:\Python27\python.exe" "C:\path\to\opctest.py"
130
- nssm start OPCDABridge
131
- ```
132
- 3. The bridge will now run automatically on system startup