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

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.
@@ -50,7 +50,7 @@
50
50
  defaults: {
51
51
  name: {value: ""},
52
52
  address: {value: "", required: true},
53
- domain: {value: "", required: true},
53
+ domain: {value: ""},
54
54
  clsid: {value: "", required: true},
55
55
  timeout: {value: 5000},
56
56
  },
@@ -62,16 +62,41 @@
62
62
  return this.name||"tier0-opcda-server";
63
63
  },
64
64
  oneditprepare: function () {
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
+ function browseApiUrl() {
72
+ var api = RED.settings && RED.settings.apiRootUrl;
73
+ if (api) {
74
+ var u = String(api);
75
+ if (!/\/$/.test(u)) {
76
+ u += '/';
77
+ }
78
+ return u + 'tier0-opcda/browse';
79
+ }
80
+ var s = RED.settings && RED.settings.httpAdminRoot;
81
+ if (s != null && String(s).trim() !== '' && String(s) !== '/') {
82
+ var root = String(s).replace(/\/+$/, '');
83
+ return (root ? root : '') + '/tier0-opcda/browse';
84
+ }
85
+ var p = window.location.pathname || '';
86
+ p = p.replace(/\/+$/, '');
87
+ if (p && p !== '/') {
88
+ return (p + '/tier0-opcda/browse').replace(/\/+/g, '/');
89
+ }
90
+ return '/tier0-opcda/browse';
91
+ }
65
92
  let browseBtn = $('#node-config-btn-item-browse');
66
93
  let browseBtnIcon = browseBtn.children('i');
67
94
  let exportBtn = $('#node-config-btn-item-export');
68
95
  let exportBtnIcon = exportBtn.children('i');
69
96
  let alertArea = $('#node-config-alert');
70
97
  let itemList = $('#node-config-item-list');
71
-
72
- console.log(this.credentials);
73
-
74
- $("#node-config-input-timeout").spinner().val(5000);
98
+
99
+ $("#node-config-input-timeout").spinner().val(self.timeout != null ? self.timeout : 5000);
75
100
 
76
101
  itemList.hide();
77
102
  alertArea.hide();
@@ -88,6 +113,7 @@
88
113
  exportBtn.addClass('disabled').attr('disabled', 'disabled');
89
114
 
90
115
  var queryData = {
116
+ id: self.id,
91
117
  address: $('#node-config-input-address').val(),
92
118
  domain: $('#node-config-input-domain').val(),
93
119
  username: $('#node-config-input-username').val(),
@@ -95,23 +121,45 @@
95
121
  clsid: $('#node-config-input-clsid').val(),
96
122
  timeout: $('#node-config-input-timeout').val()
97
123
  };
98
-
99
- console.log(queryData);
100
124
 
101
- $.get("opcda/browse", queryData)
102
- .done(function( data, status, jqXHR ) {
103
- alertArea.text(data.items.length + ' items found');
125
+ $.ajax({
126
+ url: browseApiUrl(),
127
+ method: 'GET',
128
+ data: queryData,
129
+ dataType: 'json'
130
+ })
131
+ .done(function( data /*, status, jqXHR */ ) {
132
+ var items = data && data.items;
133
+ if (!Array.isArray(items)) {
134
+ alertArea.text('Browse returned an invalid response (expected JSON { items: [...] }). See Node-RED log.');
135
+ itemList.empty().hide();
136
+ exportBtn.addClass('disabled').attr('disabled', 'disabled');
137
+ return;
138
+ }
139
+ alertArea.text(items.length + ' items found');
104
140
  itemList.empty();
105
- $.each(data.items, function(i, item){
141
+ $.each(items, function(i, item){
106
142
  $('<li/>').text(item).appendTo(itemList);
107
143
  });
108
-
144
+
109
145
  itemList.show();
110
146
  exportBtn.removeClass('disabled').removeAttr('disabled');
111
147
 
112
148
  })
113
- .fail(function( jqXHR, status, errorThrown ) {
114
- alertArea.text(jqXHR.responseJSON.error);
149
+ .fail(function( jqXHR, textStatus, errorThrown ) {
150
+ var errText;
151
+ 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.';
153
+ } 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 || '?') + '.';
156
+ } else {
157
+ errText = (jqXHR.responseJSON && jqXHR.responseJSON.error) ||
158
+ jqXHR.responseText || errorThrown || textStatus || 'Request failed';
159
+ }
160
+ alertArea.text(errText);
161
+ itemList.empty().hide();
162
+ exportBtn.addClass('disabled').attr('disabled', 'disabled');
115
163
  })
116
164
  .always(function(){
117
165
  browseBtnIcon.removeClass('fa-spinner fa-spin fa-fw').addClass('fa-search');
@@ -137,7 +185,7 @@
137
185
  });
138
186
  },
139
187
  oneditsave: function () {
140
-
188
+ this.timeout = parseInt($("#node-config-input-timeout").val(), 10) || 5000;
141
189
  }
142
190
  });
143
191
  </script>
@@ -2,10 +2,12 @@ module.exports = function(RED) {
2
2
  const opcda = require('node-opc-da');
3
3
  const { OPCServer } = opcda;
4
4
  const { ComServer, Session, Clsid } = opcda.dcom;
5
-
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
+
6
8
  const errorCode = {
7
9
  0x80040154 : "Clsid is not found.",
8
- 0x00000005 : "Access denied. Username and/or password might be wrong.",
10
+ 0x00000005 : "Access denied (DCOM)." + ACCESS_DENIED_HINT,
9
11
  0xC0040006 : "The Items AccessRights do not allow the operation.",
10
12
  0xC0040004 : "The server cannot convert the data between the specified format/ requested data type and the canonical data type.",
11
13
  0xC004000C : "Duplicate name not allowed.",
@@ -22,30 +24,101 @@ module.exports = function(RED) {
22
24
  0x0004000E : "A value passed to WRITE was accepted but the output was clamped.",
23
25
  0x0004000F : "The operation cannot be performed because the object is being referenced.",
24
26
  0x0004000D : "The server does not support the requested data rate but will use the closest available rate.",
25
- 0x00000061 : "Clsid syntax is invalid"
27
+ 0x00000061 : "Clsid syntax is invalid",
28
+ 0x80004002 : "No such interface (E_NOINTERFACE)."
26
29
  };
27
30
 
28
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];
38
+ if (errorCode[u] !== undefined) return errorCode[u];
39
+ return "HRESULT 0x" + u.toString(16).toUpperCase() + " (" + e + ")";
40
+ }
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);
45
+ }
46
+ return e.message;
47
+ }
29
48
  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); }
49
+ if (typeof e === "string") return e;
50
+ try {
51
+ return JSON.stringify(e);
52
+ } catch (_) {
53
+ return String(e);
54
+ }
34
55
  }
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{
56
+
57
+ /**
58
+ * Use deployed tier0-opcda-server credentials when the editor password field is empty.
59
+ */
60
+ function resolveBrowseParams(query) {
61
+ 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
+ if (params.password === "__PWRD__" || params.password === "__PASSWORD__") {
64
+ delete params.password;
65
+ }
66
+ const id = params.id;
67
+ if (id) {
68
+ const srv = RED.nodes.getNode(id);
69
+ 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
+ if (srv.domain != null && String(srv.domain).trim() !== "") {
74
+ params.domain = String(srv.domain).trim();
75
+ }
76
+ if (srv.credentials.username) {
77
+ params.username = srv.credentials.username;
78
+ }
79
+ if (srv.credentials.password) {
80
+ params.password = srv.credentials.password;
81
+ }
82
+ } else if (id) {
83
+ RED.log.warn("OPC DA browse: config node id not in runtime (Deploy flows?) — using form/query fields only.");
84
+ }
85
+ }
86
+ params.domain = params.domain != null && String(params.domain).trim() !== "" ?
87
+ String(params.domain).trim() : "";
88
+ params.username = String(params.username || "").trim();
89
+ params.password = String(params.password || "");
90
+ if (params.password === "__PWRD__" || params.password === "__PASSWORD__") {
91
+ params.password = "";
92
+ }
93
+ const t = Number(params.timeout);
94
+ params.timeout = Number.isFinite(t) && t > 0 ? t : 15000;
95
+ return params;
96
+ }
97
+
98
+ RED.httpAdmin.get("/tier0-opcda/browse", RED.auth.needsPermission("node-opc-da.list"), function (req, res) {
99
+ async function browseItems() {
100
+ const params = resolveBrowseParams(req.query);
101
+ try {
102
+ if (!params.address || !params.clsid) {
103
+ res.status(400).send({error: "Missing address or clsid."});
104
+ return;
105
+ }
106
+ if (!params.username || !params.password) {
107
+ 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."
109
+ });
110
+ return;
111
+ }
112
+
40
113
  var session = new Session();
41
114
  session = session.createSession(params.domain, params.username, params.password);
42
115
  session.setGlobalSocketTimeout(params.timeout);
43
116
 
44
117
  var comServer = new ComServer(new Clsid(params.clsid), params.address, session);
45
118
  await comServer.init();
46
-
119
+
47
120
  var comObject = await comServer.createInstance();
48
-
121
+
49
122
  var opcServer = new opcda.OPCServer();
50
123
  await opcServer.init(comObject);
51
124
 
@@ -55,20 +128,19 @@ module.exports = function(RED) {
55
128
  opcBrowser.end()
56
129
  .then(() => opcServer.end())
57
130
  .then(() => comServer.closeStub())
58
- .catch(e => RED.log.error(`Error closing browse session: ${e}`));
131
+ .catch((e) => RED.log.error(`Error closing browse session: ${e}`));
59
132
 
60
- res.status(200).send({items : itemList});
61
- }
62
- catch(e){
133
+ res.status(200).send({items: itemList});
134
+ } catch (e) {
63
135
  var msg = resolveError(e);
64
136
  RED.log.error(`OPC DA browse error: ${msg}`);
65
- RED.log.error(e);
66
- res.status(500).send({error : msg});
137
+ if (e && e.stack) RED.log.error(e.stack);
138
+ res.status(500).send({error: msg});
67
139
  }
68
140
  }
69
-
141
+
70
142
  browseItems();
71
- });
143
+ });
72
144
 
73
145
  function OPCDAServer(config) {
74
146
  RED.nodes.createNode(this,config);
package/opctest.py ADDED
@@ -0,0 +1,214 @@
1
+ # -*- coding: utf-8 -*-
2
+ import OpenOPC
3
+ import paho.mqtt.client as mqtt
4
+ import json
5
+ import time
6
+ import signal
7
+ import sys
8
+ reload(sys)
9
+ sys.setdefaultencoding('utf-8')
10
+
11
+ OPC_SERVER = 'Kepware.KEPServerEX.V6'
12
+ OPC_HOST = '192.168.31.75'
13
+
14
+ MQTT_BROKER = '192.168.31.45'
15
+ MQTT_PORT = 1883
16
+ MQTT_TOPIC_PREFIX = 'opcda'
17
+
18
+ POLL_INTERVAL = 1
19
+
20
+ TAGS = [
21
+ u'通道 1.设备 1.标记 1',
22
+ u'TI2022.PV',
23
+ u'TI2022.SV',
24
+ u'TI2022.MV',
25
+ ]
26
+
27
+ running = True
28
+
29
+ def on_exit(sig, frame):
30
+ global running
31
+ print("\n[*] Shutting down...")
32
+ running = False
33
+
34
+ signal.signal(signal.SIGINT, on_exit)
35
+ signal.signal(signal.SIGTERM, on_exit)
36
+
37
+
38
+ def make_topic(tag):
39
+ parts = tag.split('.')
40
+ if len(parts) >= 2:
41
+ parent = '/'.join(parts[:-1])
42
+ leaf = parts[-1]
43
+ topic = '%s/%s/Metric/%s' % (MQTT_TOPIC_PREFIX, parent, leaf)
44
+ else:
45
+ topic = '%s/Metric/%s' % (MQTT_TOPIC_PREFIX, tag)
46
+ return topic.replace(' ', '_')
47
+
48
+
49
+ UNS_EXPORT_FILE = 'uns_import.json'
50
+
51
+ TOPIC_FIELDS = [
52
+ {"name": "timeStamp", "type": "DATETIME", "unique": True, "systemField": True},
53
+ {"name": "tag", "type": "LONG", "unique": True, "tbValueName": "tag", "systemField": True},
54
+ {"name": "value", "type": "FLOAT", "index": "double_1"},
55
+ {"name": "quality", "type": "LONG", "systemField": True},
56
+ ]
57
+
58
+
59
+ def generate_uns_json(tags):
60
+ def get_or_create(children, name, node_type="path", extra=None):
61
+ for child in children:
62
+ if child["name"] == name:
63
+ return child
64
+ node = {"type": node_type, "name": name}
65
+ if extra:
66
+ node.update(extra)
67
+ if node_type == "path":
68
+ node["children"] = []
69
+ children.append(node)
70
+ return node
71
+
72
+ root_children = []
73
+ for tag in tags:
74
+ parts = tag.replace(' ', '_').split('.')
75
+ if len(parts) >= 2:
76
+ path_parts = parts[:-1]
77
+ leaf = parts[-1]
78
+ else:
79
+ path_parts = []
80
+ leaf = parts[0]
81
+
82
+ current = root_children
83
+ prefix_node = get_or_create(current, MQTT_TOPIC_PREFIX)
84
+ current = prefix_node["children"]
85
+
86
+ for p in path_parts:
87
+ node = get_or_create(current, p)
88
+ current = node["children"]
89
+
90
+ metric_node = get_or_create(current, "Metric", extra={
91
+ "displayName": "Metric",
92
+ "dataType": "METRIC",
93
+ })
94
+ current = metric_node["children"]
95
+
96
+ topic_node = {
97
+ "type": "topic",
98
+ "name": leaf,
99
+ "fields": TOPIC_FIELDS,
100
+ "dataType": "TIME_SEQUENCE_TYPE",
101
+ "generateDashboard": "TRUE",
102
+ "enableHistory": "TRUE",
103
+ "mockData": "FALSE",
104
+ "writeData": "FALSE",
105
+ "topicType": "METRIC",
106
+ }
107
+ exists = False
108
+ for child in current:
109
+ if child["name"] == leaf:
110
+ exists = True
111
+ break
112
+ if not exists:
113
+ current.append(topic_node)
114
+
115
+ result = {"UNS": root_children}
116
+ with open(UNS_EXPORT_FILE, 'w') as f:
117
+ json.dump(result, f, indent=2, ensure_ascii=False)
118
+ print("[+] UNS import JSON written to %s" % UNS_EXPORT_FILE)
119
+
120
+
121
+ def run_bridge():
122
+ global running
123
+
124
+ print("[*] OPC DA -> MQTT Bridge")
125
+ print(" OPC Server: %s @ %s" % (OPC_SERVER, OPC_HOST))
126
+ print(" MQTT Broker: %s:%d" % (MQTT_BROKER, MQTT_PORT))
127
+ print(" Tags: %d" % len(TAGS))
128
+ print("")
129
+
130
+ opc = None
131
+ mqtt_client = None
132
+
133
+ try:
134
+ mqtt_client = mqtt.Client()
135
+ mqtt_client.connect(MQTT_BROKER, MQTT_PORT)
136
+ mqtt_client.loop_start()
137
+ print("[+] MQTT connected")
138
+
139
+ opc = OpenOPC.client()
140
+ opc.connect(OPC_SERVER, OPC_HOST)
141
+ print("[+] OPC DA connected")
142
+
143
+ generate_uns_json(TAGS)
144
+
145
+ print("[*] Publishing data every %ds (Ctrl+C to stop)..." % POLL_INTERVAL)
146
+ print("-" * 50)
147
+
148
+ while running:
149
+ try:
150
+ results = opc.read(TAGS)
151
+ except Exception as e:
152
+ print("[!] OPC read error: %s, reconnecting..." % str(e))
153
+ try:
154
+ opc.close()
155
+ except:
156
+ pass
157
+ opc = OpenOPC.client()
158
+ opc.connect(OPC_SERVER, OPC_HOST)
159
+ continue
160
+
161
+ for tag_name, value, quality, timestamp in results:
162
+ topic = make_topic(tag_name)
163
+
164
+ if isinstance(quality, str):
165
+ quality_code = 0 if quality == 'Good' else 1
166
+ else:
167
+ quality_code = 0 if quality is not None and int(quality) >= 192 else 1
168
+
169
+ if value is None:
170
+ out_value = 0.0
171
+ elif isinstance(value, (int, float)):
172
+ out_value = float(value)
173
+ else:
174
+ try:
175
+ out_value = float(value)
176
+ except (ValueError, TypeError):
177
+ out_value = str(value)
178
+
179
+ payload = json.dumps({
180
+ 'value': out_value,
181
+ 'quality': quality_code,
182
+ 'timestamp': str(timestamp),
183
+ }, ensure_ascii=False)
184
+ mqtt_client.publish(topic, payload, retain=True)
185
+
186
+ ts = time.strftime('%H:%M:%S')
187
+ print("[%s] Published %d tags" % (ts, len(results)))
188
+
189
+ time.sleep(POLL_INTERVAL)
190
+
191
+ except Exception as e:
192
+ print("\n[!] Fatal error: %s" % str(e))
193
+ print(" 1. Is MQTT broker running? ")
194
+ print(" 2. Is OPC server reachable?")
195
+ print(" 3. Run as Administrator if DCOM access is needed.")
196
+ sys.exit(1)
197
+
198
+ finally:
199
+ if opc:
200
+ try:
201
+ opc.close()
202
+ except:
203
+ pass
204
+ print("[-] OPC DA disconnected")
205
+ if mqtt_client:
206
+ mqtt_client.loop_stop()
207
+ mqtt_client.disconnect()
208
+ print("[-] MQTT disconnected")
209
+
210
+ print("[*] Bridge stopped.")
211
+
212
+
213
+ if __name__ == "__main__":
214
+ run_bridge()
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@tier0/node-red-contrib-opcda-client",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Node-RED OPC DA Reading and Writing Node",
5
5
  "node-red": {
6
6
  "nodes": {
7
- "tier0-opc-read": "opcda/opcda-read.js",
8
- "tier0-opc-write": "opcda/opcda-write.js",
9
- "tier0-opc-server": "opcda/opcda-server.js"
7
+ "opc-read": "opcda/opcda-read.js",
8
+ "opc-write": "opcda/opcda-write.js",
9
+ "opc-server": "opcda/opcda-server.js"
10
10
  },
11
11
  "keywords": [
12
12
  "opcda",
@@ -33,13 +33,16 @@
33
33
  "type": "git",
34
34
  "url": "git+https://github.com/FREEZONEX/opcda-client.git"
35
35
  },
36
- "author": "FREEZONEX",
36
+ "author": "FREEZONEX (forked from emrebekar)",
37
37
  "bugs": {
38
38
  "url": "https://github.com/FREEZONEX/opcda-client/issues"
39
39
  },
40
40
  "homepage": "https://github.com/FREEZONEX/opcda-client#readme",
41
41
  "license": "Apache-2.0",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
42
45
  "dependencies": {
43
- "node-opc-da": "^1.0.6"
46
+ "@tier0/node-opc-da": "^1.0.8"
44
47
  }
45
48
  }
package/patch-debug.js ADDED
@@ -0,0 +1,13 @@
1
+ const fs = require('fs');
2
+ const file = '/usr/src/node-red/node_modules/node-dcom/dcom/rpc/security/ntlmauthentication.js';
3
+ let code = fs.readFileSync(file, 'utf8');
4
+ code = code.replace(
5
+ 'let target = null;',
6
+ 'let target = null;\n console.log("[NTLM-DBG] domain=" + info.domain + " user=" + this.credentials.username);'
7
+ );
8
+ code = code.replace(
9
+ "if (target == '') {",
10
+ "console.log('[NTLM-DBG] target=' + target);\n if (target == '') {"
11
+ );
12
+ fs.writeFileSync(file, code);
13
+ console.log('Patched.');
package/patch-ntlm.js ADDED
@@ -0,0 +1,46 @@
1
+ const fs = require('fs');
2
+ const file = '/usr/src/node-red/node_modules/node-dcom/dcom/rpc/security/ntlmauthentication.js';
3
+ let code = fs.readFileSync(file, 'utf8');
4
+
5
+ // Fix 1: Replace all Encdec references with inline uint16le decode
6
+ code = code.replace(
7
+ /new Encdec\(\)\.dec_uint16le\(targetInformation,\s*i\)/g,
8
+ '(targetInformation[i] | (targetInformation[i+1] << 8))'
9
+ );
10
+ code = code.replace(
11
+ /new Encdec\.dec_uint16le\(targetInformation,\s*i\)/g,
12
+ '(targetInformation[i] | (targetInformation[i+1] << 8))'
13
+ );
14
+
15
+ // Fix 2: Add debug logging for NTLM credentials
16
+ code = code.replace(
17
+ 'let target = null;',
18
+ 'let target = null;\n console.log("[NTLM-DBG] domain=" + info.domain + " user=" + this.credentials.username);'
19
+ );
20
+
21
+ // Verify no syntax errors
22
+ try {
23
+ new Function(code);
24
+ console.log('ERROR: wrapped in function - not a module');
25
+ } catch(e) {
26
+ // Expected since it's a class/module, not a function body
27
+ }
28
+
29
+ fs.writeFileSync(file, code);
30
+
31
+ // Verify it loads
32
+ try {
33
+ delete require.cache[require.resolve(file)];
34
+ require(file);
35
+ console.log('Patched and verified OK');
36
+ } catch(e) {
37
+ console.log('WARNING: Load check result:', e.message);
38
+ // May fail due to missing dependencies in standalone context, that's OK
39
+ // as long as it's not a syntax error
40
+ if (e.message.includes('Unexpected token')) {
41
+ console.log('SYNTAX ERROR - reverting!');
42
+ // Don't revert for now, let's see the error
43
+ } else {
44
+ console.log('Non-syntax error (expected in standalone context)');
45
+ }
46
+ }
@@ -0,0 +1,17 @@
1
+ const fs = require('fs');
2
+ const file = '/usr/src/node-red/node_modules/node-dcom/dcom/rpc/security/ntlmauthentication.js';
3
+ let code = fs.readFileSync(file, 'utf8');
4
+
5
+ // Add password length and MD4 check debug
6
+ code = code.replace(
7
+ 'let target = null;',
8
+ `let target = null;
9
+ console.log("[NTLM-DBG] domain=" + info.domain + " user=" + this.credentials.username + " pwdLen=" + (this.credentials.password ? this.credentials.password.length : "NULL"));
10
+ try { const _h = Crypto.createHash('md4'); _h.update(Buffer.from(this.credentials.password || '', 'utf16le')); console.log("[NTLM-DBG] md4-ok ntHash=" + _h.digest('hex')); } catch(e) { console.log("[NTLM-DBG] md4-FAIL: " + e.message); }`
11
+ );
12
+
13
+ fs.writeFileSync(file, code);
14
+
15
+ // Verify loads
16
+ try { require('node-opc-da'); console.log('Patched OK, module loads'); }
17
+ catch(e) { console.log('Load result:', e.message); }
@@ -0,0 +1,69 @@
1
+ const opcda = require('node-opc-da');
2
+ const { OPCServer } = opcda;
3
+ const { ComServer, Session, Clsid } = opcda.dcom;
4
+
5
+ const config = {
6
+ address: '192.168.31.75',
7
+ domain: 'DESKTOP-BBD7VBL',
8
+ username: 'Administrator',
9
+ password: 'Supos@1304',
10
+ clsid: '7BC0CC8E-482C-47CA-ABDC-0FE7F9C6E729',
11
+ timeout: 10000
12
+ };
13
+
14
+ async function test() {
15
+ console.log('=== OPC DA Connection Test ===');
16
+ console.log('Address:', config.address);
17
+ console.log('Domain:', config.domain);
18
+ console.log('Username:', config.username);
19
+ console.log('CLSID:', config.clsid);
20
+ console.log('');
21
+
22
+ try {
23
+ console.log('[1] Creating session...');
24
+ var session = new Session();
25
+ session = session.createSession(config.domain, config.username, config.password);
26
+ session.setGlobalSocketTimeout(config.timeout);
27
+ console.log('[1] Session created OK');
28
+
29
+ console.log('[2] Creating ComServer...');
30
+ var comServer = new ComServer(new Clsid(config.clsid), config.address, session);
31
+ console.log('[2] ComServer created, calling init()...');
32
+
33
+ await comServer.init();
34
+ console.log('[2] ComServer.init() OK!');
35
+
36
+ console.log('[3] Creating COM instance...');
37
+ var comObject = await comServer.createInstance();
38
+ console.log('[3] COM instance created OK!');
39
+
40
+ console.log('[4] Creating OPC Server...');
41
+ var opcServer = new OPCServer();
42
+ await opcServer.init(comObject);
43
+ console.log('[4] OPC Server connected!');
44
+
45
+ console.log('[5] Getting browser...');
46
+ var browser = await opcServer.getBrowser();
47
+ var items = await browser.browseAllFlat();
48
+ console.log('[5] Found', items.length, 'items');
49
+ items.slice(0, 10).forEach(i => console.log(' ', i));
50
+
51
+ await browser.end();
52
+ await opcServer.end();
53
+ await comServer.closeStub();
54
+ console.log('\n=== SUCCESS ===');
55
+ } catch (e) {
56
+ console.log('\n=== FAILED ===');
57
+ console.log('Error type:', typeof e);
58
+ console.log('Error value:', e);
59
+ if (e instanceof Error) {
60
+ console.log('Message:', e.message);
61
+ console.log('Stack:', e.stack);
62
+ }
63
+ if (typeof e === 'number') {
64
+ console.log('Hex:', '0x' + (e >>> 0).toString(16));
65
+ }
66
+ }
67
+ }
68
+
69
+ test();