@tier0/node-red-contrib-opcda-client 1.0.3 → 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.
- package/opcda/opcda-read.html +1 -1
- package/opcda/opcda-read.js +21 -50
- package/opcda/opcda-server.html +11 -20
- package/opcda/opcda-server.js +28 -42
- package/opcda/opcda-write.html +1 -1
- package/opcda/opcda-write.js +17 -20
- package/package.json +4 -4
- package/OPCDA_Bridge_Setup_Guide.md +0 -132
- package/docker-compose.yml +0 -460
- package/opctest.py +0 -214
- package/patch-debug.js +0 -13
- package/patch-ntlm.js +0 -46
- package/patch-pwdcheck.js +0 -17
- package/test-connect.js +0 -69
- package/test-connect2.js +0 -30
- package/test-direct.js +0 -53
- package/test-ntlm-vectors.js +0 -100
- package/test-ntlm-verify.js +0 -77
- package/uns_import.json +0 -204
package/opcda/opcda-read.html
CHANGED
|
@@ -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/
|
|
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">
|
package/opcda/opcda-read.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
package/opcda/opcda-server.html
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
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 : '') + '/
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
155
|
-
'.
|
|
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';
|
package/opcda/opcda-server.js
CHANGED
|
@@ -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
|
|
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
|
|
32
|
-
if (typeof
|
|
33
|
-
const u =
|
|
34
|
-
if (
|
|
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)
|
|
35
|
+
return "HRESULT 0x" + u.toString(16) + " (" + err + ")";
|
|
40
36
|
}
|
|
41
|
-
if (
|
|
42
|
-
const asNum = Number(
|
|
43
|
-
if (!Number.isNaN(asNum) && String(asNum) === String(
|
|
44
|
-
return
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (srv.credentials.
|
|
80
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
136
|
-
RED.log.error(`OPC DA browse
|
|
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
|
}
|
package/opcda/opcda-write.html
CHANGED
|
@@ -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/
|
|
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">
|
package/opcda/opcda-write.js
CHANGED
|
@@ -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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tier0/node-red-contrib-opcda-client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Node-RED OPC DA Reading and Writing Node",
|
|
5
5
|
"node-red": {
|
|
6
6
|
"nodes": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
7
|
+
"tier0-opcda-read": "opcda/opcda-read.js",
|
|
8
|
+
"tier0-opcda-write": "opcda/opcda-write.js",
|
|
9
|
+
"tier0-opcda-server": "opcda/opcda-server.js"
|
|
10
10
|
},
|
|
11
11
|
"keywords": [
|
|
12
12
|
"opcda",
|
|
@@ -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
|