node-red-contrib-modbus-modpackqt 3.2.4 → 3.3.1
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/CHANGELOG.md +33 -0
- package/nodes/lib/probe-runtime.js +1 -1
- package/nodes/modpackqt-config.html +129 -31
- package/nodes/modpackqt-config.js +44 -49
- package/nodes/modpackqt-master-probe.html +22 -41
- package/nodes/modpackqt-master-read.html +14 -100
- package/nodes/modpackqt-master-read.js +15 -7
- package/nodes/modpackqt-master-write.html +11 -34
- package/nodes/modpackqt-master-write.js +13 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ All notable changes to **node-red-contrib-modbus-modpackqt** are documented
|
|
|
4
4
|
here. This project follows [Semantic Versioning](https://semver.org/) — pin a
|
|
5
5
|
major version (`^2.0.0`) in production.
|
|
6
6
|
|
|
7
|
+
## [3.3.1] — 2026-05-10
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Account Key section is now collapsed by default** in the runtime config
|
|
12
|
+
dialog. It's only relevant for probe-node deep-linking and the optional
|
|
13
|
+
cloud profile picker — master-read / write and slave-read / write users
|
|
14
|
+
never need it. Auto-expands if a key is already saved.
|
|
15
|
+
|
|
16
|
+
## [3.3.0] — 2026-05-10
|
|
17
|
+
|
|
18
|
+
### Changed — Runtime config = one device
|
|
19
|
+
|
|
20
|
+
- **Host / Port / Unit ID now live on the `modpackqt-config` runtime, not on
|
|
21
|
+
each master/probe node.** Master-read, master-write, and master-probe
|
|
22
|
+
nodes simply pick which runtime to talk to — they only specify function
|
|
23
|
+
code, address, quantity, and poll interval. To talk to multiple devices,
|
|
24
|
+
create one runtime config per device.
|
|
25
|
+
- The runtime config dialog also gained a **My Profiles** picker (uses your
|
|
26
|
+
Account Key) that auto-fills Host / Port / Unit from a saved
|
|
27
|
+
modpackqt.com profile in one click.
|
|
28
|
+
- Edit dialogs are now **Name → Runtime → fields** — no Advanced toggle on
|
|
29
|
+
master nodes anymore.
|
|
30
|
+
|
|
31
|
+
### Backward compatibility
|
|
32
|
+
|
|
33
|
+
- Existing flows from v3.2.x keep working unchanged. Master nodes still
|
|
34
|
+
read their own `targetHost` / `targetPort` / `unitId` if set, and only
|
|
35
|
+
fall back to the runtime config's values when those fields are empty
|
|
36
|
+
(the new default for nodes created in v3.3.0+).
|
|
37
|
+
- `resolveTarget()` on the runtime config does this merging — exposed for
|
|
38
|
+
any third-party node that wants the same behaviour.
|
|
39
|
+
|
|
7
40
|
## [3.2.4] — 2026-05-10
|
|
8
41
|
|
|
9
42
|
### Changed — Probe nodes are now picker-only
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
const http = require('http');
|
|
19
19
|
const { URL } = require('url');
|
|
20
20
|
|
|
21
|
-
const PALETTE_VERSION = '3.
|
|
21
|
+
const PALETTE_VERSION = '3.3.1';
|
|
22
22
|
const DEFAULT_PORT = parseInt(process.env.MODPACKQT_PROBE_PORT, 10) || 8502;
|
|
23
23
|
const BIND_HOST = process.env.MODPACKQT_PROBE_HOST || '127.0.0.1';
|
|
24
24
|
const PORT_RETRY = 5;
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
RED.nodes.registerType('modpackqt-config', {
|
|
3
3
|
category: 'config',
|
|
4
4
|
defaults: {
|
|
5
|
-
name: { value: 'ModPackQT
|
|
5
|
+
name: { value: 'ModPackQT Device' },
|
|
6
|
+
// ── Device target (NEW in v3.3.0) — one runtime = one Modbus device.
|
|
7
|
+
// Master/probe nodes inherit these fields from the runtime config.
|
|
8
|
+
targetHost: { value: 'localhost' },
|
|
9
|
+
targetPort: { value: 502, validate: RED.validators.number() },
|
|
10
|
+
unitId: { value: 1, validate: RED.validators.number() },
|
|
11
|
+
// ── Master transport
|
|
6
12
|
masterMode: { value: 'tcp', required: true },
|
|
7
13
|
serialPort: { value: '' },
|
|
8
14
|
baudRate: { value: 9600 },
|
|
@@ -10,6 +16,7 @@
|
|
|
10
16
|
dataBits: { value: 8 },
|
|
11
17
|
stopBits: { value: 1 },
|
|
12
18
|
timeoutMs: { value: 3000, validate: RED.validators.number() },
|
|
19
|
+
// ── Optional embedded Modbus TCP slave server
|
|
13
20
|
slaveEnabled: { value: false },
|
|
14
21
|
slaveHost: { value: '0.0.0.0' },
|
|
15
22
|
slavePort: { value: 1502, validate: RED.validators.number() }
|
|
@@ -18,14 +25,19 @@
|
|
|
18
25
|
apiKey: { type: 'password' }
|
|
19
26
|
},
|
|
20
27
|
label: function () {
|
|
21
|
-
const
|
|
28
|
+
const t = this.masterMode === 'rtu'
|
|
29
|
+
? `RTU ${this.serialPort || '?'} #${this.unitId || 1}`
|
|
30
|
+
: `${this.targetHost || 'localhost'}:${this.targetPort || 502} #${this.unitId || 1}`;
|
|
22
31
|
const s = this.slaveEnabled ? ` + slave :${this.slavePort}` : '';
|
|
23
|
-
return this.name
|
|
32
|
+
return this.name && this.name !== 'ModPackQT Device' ? `${this.name} (${t})` : `${t}${s}`;
|
|
24
33
|
},
|
|
25
34
|
oneditprepare: function () {
|
|
35
|
+
const node = this;
|
|
36
|
+
|
|
26
37
|
const toggleRtu = () => {
|
|
27
38
|
const isRtu = $('#node-config-input-masterMode').val() === 'rtu';
|
|
28
39
|
$('.modpackqt-rtu-only').toggle(isRtu);
|
|
40
|
+
$('.modpackqt-tcp-only').toggle(!isRtu);
|
|
29
41
|
};
|
|
30
42
|
const toggleSlave = () => {
|
|
31
43
|
const on = $('#node-config-input-slaveEnabled').is(':checked');
|
|
@@ -35,9 +47,49 @@
|
|
|
35
47
|
$('#node-config-input-slaveEnabled').on('change', toggleSlave);
|
|
36
48
|
toggleRtu();
|
|
37
49
|
toggleSlave();
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
50
|
+
|
|
51
|
+
// ── My Profiles picker — auto-fills targetHost/targetPort/unitId
|
|
52
|
+
const $sel = $('#node-config-modpackqt-picker');
|
|
53
|
+
const reload = function () {
|
|
54
|
+
$sel.empty().append('<option value="">— Loading… —</option>');
|
|
55
|
+
const key = $('#node-config-input-apiKey').val();
|
|
56
|
+
// The admin proxy needs the saved config ID. On a brand-new config the
|
|
57
|
+
// user hasn't saved yet — show a friendly message instead.
|
|
58
|
+
if (!node.id) {
|
|
59
|
+
$sel.empty().append('<option value="">— Save the config first, then reopen to load profiles —</option>');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!key) {
|
|
63
|
+
$sel.empty().append('<option value="">— Paste your Account Key below to load profiles —</option>');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
$.getJSON('modpackqt/connections?config=' + encodeURIComponent(node.id))
|
|
67
|
+
.done(function (rows) {
|
|
68
|
+
$sel.empty().append('<option value="">— Manual entry —</option>');
|
|
69
|
+
(rows || [])
|
|
70
|
+
.filter(function (c) { return c && (c.connectionType === 'tcp' || !c.connectionType); })
|
|
71
|
+
.forEach(function (c) {
|
|
72
|
+
const label = (c.name || '(unnamed)') + ' — ' + (c.host || '?') + ':' + (c.port || 502) + ' #' + (c.unitId || 1);
|
|
73
|
+
$('<option>').val(c.id).text(label).data('row', c).appendTo($sel);
|
|
74
|
+
});
|
|
75
|
+
})
|
|
76
|
+
.fail(function (xhr) {
|
|
77
|
+
const msg = (xhr.responseJSON && xhr.responseJSON.error) || ('HTTP ' + xhr.status);
|
|
78
|
+
$sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
$('#modpackqt-config-picker-refresh').on('click', function (e) { e.preventDefault(); reload(); });
|
|
82
|
+
$sel.on('change', function () {
|
|
83
|
+
const row = $(this).find('option:selected').data('row');
|
|
84
|
+
if (!row) return;
|
|
85
|
+
if (!node.name || node.name === 'ModPackQT Device') $('#node-config-input-name').val(row.name || '');
|
|
86
|
+
$('#node-config-input-targetHost').val(row.host || '');
|
|
87
|
+
$('#node-config-input-targetPort').val(row.port || 502);
|
|
88
|
+
$('#node-config-input-unitId').val(row.unitId || 1);
|
|
89
|
+
});
|
|
90
|
+
reload();
|
|
91
|
+
|
|
92
|
+
// ── Embedded slave server section (collapsed by default)
|
|
41
93
|
const $slaveSection = $('.modpackqt-slave-section');
|
|
42
94
|
const $slaveToggle = $('.modpackqt-slave-section-toggle');
|
|
43
95
|
const setSlaveOpen = (open) => {
|
|
@@ -51,6 +103,23 @@
|
|
|
51
103
|
e.preventDefault();
|
|
52
104
|
setSlaveOpen($slaveSection.is(':hidden'));
|
|
53
105
|
});
|
|
106
|
+
|
|
107
|
+
// ── Account Key section (collapsed by default — only relevant for the
|
|
108
|
+
// My Profiles picker and for probe nodes that deep-link the cloud).
|
|
109
|
+
const $keySection = $('.modpackqt-key-section');
|
|
110
|
+
const $keyToggle = $('.modpackqt-key-section-toggle');
|
|
111
|
+
const setKeyOpen = (open) => {
|
|
112
|
+
$keySection.toggle(open);
|
|
113
|
+
$keyToggle.html(open
|
|
114
|
+
? '▾ Hide ModPackQT Account Key'
|
|
115
|
+
: '▸ ModPackQT Account Key (only for probe nodes & cloud profile sync)');
|
|
116
|
+
};
|
|
117
|
+
const hasKey = !!(this.credentials && this.credentials.has && this.credentials.has.apiKey);
|
|
118
|
+
setKeyOpen(hasKey);
|
|
119
|
+
$keyToggle.off('click.modpackqt').on('click.modpackqt', (e) => {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
setKeyOpen($keySection.is(':hidden'));
|
|
122
|
+
});
|
|
54
123
|
}
|
|
55
124
|
});
|
|
56
125
|
</script>
|
|
@@ -58,10 +127,32 @@
|
|
|
58
127
|
<script type="text/html" data-template-name="modpackqt-config">
|
|
59
128
|
<div class="form-row">
|
|
60
129
|
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
61
|
-
<input type="text" id="node-config-input-name" placeholder="
|
|
130
|
+
<input type="text" id="node-config-input-name" placeholder="e.g. Inverter A">
|
|
62
131
|
</div>
|
|
63
132
|
|
|
64
|
-
<h4 style="margin-top:18px">
|
|
133
|
+
<h4 style="margin-top:18px">Device</h4>
|
|
134
|
+
<div class="form-row">
|
|
135
|
+
<label for="node-config-modpackqt-picker"><i class="fa fa-cloud-download"></i> My Profiles</label>
|
|
136
|
+
<select id="node-config-modpackqt-picker" style="width:65%"></select>
|
|
137
|
+
<button type="button" id="modpackqt-config-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
140
|
+
Picks a saved Modbus profile from your modpackqt.com account — auto-fills the fields below.
|
|
141
|
+
</div>
|
|
142
|
+
<div class="form-row modpackqt-tcp-only">
|
|
143
|
+
<label for="node-config-input-targetHost"><i class="fa fa-plug"></i> Host</label>
|
|
144
|
+
<input type="text" id="node-config-input-targetHost" placeholder="Modbus device IP/hostname">
|
|
145
|
+
</div>
|
|
146
|
+
<div class="form-row modpackqt-tcp-only">
|
|
147
|
+
<label for="node-config-input-targetPort"><i class="fa fa-hashtag"></i> Port</label>
|
|
148
|
+
<input type="number" id="node-config-input-targetPort" min="1" max="65535" placeholder="502">
|
|
149
|
+
</div>
|
|
150
|
+
<div class="form-row">
|
|
151
|
+
<label for="node-config-input-unitId"><i class="fa fa-id-card"></i> Unit ID</label>
|
|
152
|
+
<input type="number" id="node-config-input-unitId" min="1" max="247" placeholder="1">
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<h4 style="margin-top:18px">Transport</h4>
|
|
65
156
|
<div class="form-row">
|
|
66
157
|
<label for="node-config-input-masterMode"><i class="fa fa-cogs"></i> Mode</label>
|
|
67
158
|
<select id="node-config-input-masterMode">
|
|
@@ -102,6 +193,20 @@
|
|
|
102
193
|
<input type="number" id="node-config-input-timeoutMs" placeholder="3000">
|
|
103
194
|
</div>
|
|
104
195
|
|
|
196
|
+
<div class="form-row" style="margin:14px 0 4px 0;border-top:1px solid #e5e7eb;padding-top:10px">
|
|
197
|
+
<a href="#" class="modpackqt-key-section-toggle" style="font-size:12px;color:#6b7280;text-decoration:none;cursor:pointer">▸ ModPackQT Account Key (only for probe nodes & cloud profile sync)</a>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="modpackqt-key-section" style="display:none">
|
|
200
|
+
<div class="form-row">
|
|
201
|
+
<label for="node-config-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
|
|
202
|
+
<input type="password" id="node-config-input-apiKey" placeholder="Optional — paste your tunnel key for the profile picker">
|
|
203
|
+
</div>
|
|
204
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin-top:-6px">
|
|
205
|
+
Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
206
|
+
All Modbus traffic stays local — the key is only used for read-only profile sync.
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
105
210
|
<div class="form-row" style="margin:14px 0 4px 0;border-top:1px solid #e5e7eb;padding-top:10px">
|
|
106
211
|
<a href="#" class="modpackqt-slave-section-toggle" style="font-size:12px;color:#6b7280;text-decoration:none;cursor:pointer">▸ Embedded slave server (only for modpackqt-slave-read / -write nodes)</a>
|
|
107
212
|
</div>
|
|
@@ -122,19 +227,8 @@
|
|
|
122
227
|
</div>
|
|
123
228
|
</div>
|
|
124
229
|
|
|
125
|
-
<h4 style="margin-top:18px">ModPackQT Account Key (optional — for cloud profile sync)</h4>
|
|
126
|
-
<div class="form-row">
|
|
127
|
-
<label for="node-config-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
|
|
128
|
-
<input type="password" id="node-config-input-apiKey" placeholder="Optional — paste your tunnel key for profile pickers">
|
|
129
|
-
</div>
|
|
130
|
-
<div class="form-tips" style="font-size:11px;color:#6b7280;margin-top:-6px">
|
|
131
|
-
With a key set, master & probe nodes show a dropdown of your saved Modbus profiles
|
|
132
|
-
and slaves from <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
133
|
-
All Modbus runs locally — the key is only used for read-only profile sync.
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
230
|
<div class="form-tips">
|
|
137
|
-
<b>
|
|
231
|
+
<b>One runtime = one device.</b> For multiple devices, create one runtime config per device — the read/write nodes then just pick which device to talk to.
|
|
138
232
|
</div>
|
|
139
233
|
<div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
|
|
140
234
|
<a href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
|
|
@@ -149,32 +243,36 @@
|
|
|
149
243
|
|
|
150
244
|
<script type="text/html" data-help-name="modpackqt-config">
|
|
151
245
|
<p>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
246
|
+
Defines <b>one Modbus device</b> plus the embedded runtime that talks to it. Master and
|
|
247
|
+
probe nodes simply pick which runtime config to use — no need to retype host/port/unit
|
|
248
|
+
on every node.
|
|
249
|
+
</p>
|
|
250
|
+
|
|
251
|
+
<h3>One runtime = one device</h3>
|
|
252
|
+
<p>
|
|
253
|
+
For each remote Modbus device you want to read/write, create a separate runtime config.
|
|
254
|
+
Each carries its own Host / Port / Unit ID (and optional Account Key). Read and write
|
|
255
|
+
nodes inherit those — they only need to know the function code, address, and quantity.
|
|
155
256
|
</p>
|
|
156
257
|
|
|
157
258
|
<h3>Master mode</h3>
|
|
158
259
|
<ul>
|
|
159
|
-
<li><b>Modbus TCP</b> — connects to
|
|
160
|
-
<li><b>Modbus RTU</b> — opens the configured serial port
|
|
260
|
+
<li><b>Modbus TCP</b> — connects to the configured Host/Port.</li>
|
|
261
|
+
<li><b>Modbus RTU</b> — opens the configured serial port. Host/Port are ignored; only Unit ID matters.</li>
|
|
161
262
|
</ul>
|
|
162
263
|
|
|
163
264
|
<h3>Embedded slave server</h3>
|
|
164
265
|
<p>
|
|
165
266
|
When enabled, this config opens a Modbus TCP server on the configured port (default
|
|
166
267
|
<code>1502</code>). Use the <code>modpackqt-slave-write</code> node to push values into
|
|
167
|
-
its register store; external Modbus masters
|
|
168
|
-
last wrote.
|
|
268
|
+
its register store; external Modbus masters read whatever you last wrote.
|
|
169
269
|
</p>
|
|
170
270
|
|
|
171
271
|
<h3>ModPackQT Account Key (optional)</h3>
|
|
172
272
|
<p>
|
|
173
273
|
Same tunnel key the Electron gateway uses — generate one at
|
|
174
274
|
<a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
175
|
-
With a key set, the
|
|
176
|
-
|
|
177
|
-
still runs locally — the key is only used for a read-only HTTPS call from this
|
|
178
|
-
Node-RED process to load your profile list.
|
|
275
|
+
With a key set, the <b>My Profiles</b> picker above auto-fills Host / Port / Unit from
|
|
276
|
+
your saved Modbus profiles.
|
|
179
277
|
</p>
|
|
180
278
|
</script>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ModPackQT Config Node — embedded Modbus runtime for Node-RED.
|
|
3
3
|
*
|
|
4
|
-
* Owns:
|
|
5
|
-
* -
|
|
4
|
+
* One config = one Modbus device. Owns:
|
|
5
|
+
* - Device target (host / port / unit) used by master + probe nodes.
|
|
6
|
+
* - A single-target Modbus TCP/RTU master client (pooled per runtime).
|
|
6
7
|
* - An optional embedded Modbus TCP slave server with a local register store.
|
|
7
8
|
*/
|
|
8
9
|
module.exports = function (RED) {
|
|
@@ -23,7 +24,7 @@ module.exports = function (RED) {
|
|
|
23
24
|
const cfg = RED.nodes.getNode(configId);
|
|
24
25
|
if (!cfg) return res.status(404).json({ error: 'Runtime config not found (deploy first?)' });
|
|
25
26
|
const key = cfg.credentials && cfg.credentials.apiKey;
|
|
26
|
-
if (!key) return res.status(400).json({ error: 'No Account Key set on
|
|
27
|
+
if (!key) return res.status(400).json({ error: 'No Account Key set on this runtime config.' });
|
|
27
28
|
fetchFn(key, function (err, data) {
|
|
28
29
|
if (err) return res.status(502).json({ error: err.message });
|
|
29
30
|
res.json(data);
|
|
@@ -38,13 +39,17 @@ module.exports = function (RED) {
|
|
|
38
39
|
RED.nodes.createNode(this, config);
|
|
39
40
|
const node = this;
|
|
40
41
|
|
|
41
|
-
// Traffic event bus — modpackqt-traffic nodes subscribe to this.
|
|
42
42
|
node.traffic = new EventEmitter();
|
|
43
43
|
node.traffic.setMaxListeners(0);
|
|
44
44
|
function emitTraffic(evt) {
|
|
45
45
|
try { node.traffic.emit('op', evt); } catch (_) { /* listener errors must not affect ops */ }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// ---- Device target (NEW in v3.3.0) — single device per runtime ----
|
|
49
|
+
node.targetHost = config.targetHost || 'localhost';
|
|
50
|
+
node.targetPort = parseInt(config.targetPort, 10) || 502;
|
|
51
|
+
node.unitId = parseInt(config.unitId, 10) || 1;
|
|
52
|
+
|
|
48
53
|
// ---- Master settings ----
|
|
49
54
|
node.masterMode = config.masterMode || 'tcp'; // 'tcp' | 'rtu'
|
|
50
55
|
node.serialPort = config.serialPort || '';
|
|
@@ -59,12 +64,8 @@ module.exports = function (RED) {
|
|
|
59
64
|
node.slavePort = parseInt(config.slavePort, 10) || 1502;
|
|
60
65
|
node.slaveHost = config.slaveHost || '0.0.0.0';
|
|
61
66
|
|
|
62
|
-
// ---- Optional API key (reserved for future cloud features) ----
|
|
63
67
|
node.apiKey = (node.credentials && node.credentials.apiKey) || '';
|
|
64
68
|
|
|
65
|
-
// ====================================================================
|
|
66
|
-
// OPS COUNTER (informational only — no limit enforced)
|
|
67
|
-
// ====================================================================
|
|
68
69
|
const today = () => new Date().toISOString().slice(0, 10);
|
|
69
70
|
node._opsDay = today();
|
|
70
71
|
node._opsCount = 0;
|
|
@@ -77,14 +78,25 @@ module.exports = function (RED) {
|
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
node.brandStatus = function (text) { return text; };
|
|
81
|
+
node.opsToday = function () { return node._opsCount; };
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
// Resolve the effective device target. Master/probe nodes call this so
|
|
84
|
+
// legacy nodes that still carry their own targetHost/targetPort/unitId
|
|
85
|
+
// (pre-v3.3.0) keep working — node-level fields override config when set.
|
|
86
|
+
node.resolveTarget = function (override) {
|
|
87
|
+
const o = override || {};
|
|
88
|
+
return {
|
|
89
|
+
host: o.host || node.targetHost,
|
|
90
|
+
port: parseInt(o.port, 10) || node.targetPort,
|
|
91
|
+
unitId: parseInt(o.unitId, 10) || node.unitId
|
|
92
|
+
};
|
|
93
|
+
};
|
|
82
94
|
|
|
83
95
|
// ====================================================================
|
|
84
96
|
// MASTER CLIENT POOL — one client per target (host:port:unit or serial:unit)
|
|
85
97
|
// ====================================================================
|
|
86
98
|
node._masterPool = new Map();
|
|
87
|
-
node._masterQueue = Promise.resolve();
|
|
99
|
+
node._masterQueue = Promise.resolve();
|
|
88
100
|
|
|
89
101
|
function targetKey(opts) {
|
|
90
102
|
if (node.masterMode === 'rtu') return `rtu:${node.serialPort}:${opts.unitId}`;
|
|
@@ -115,26 +127,19 @@ module.exports = function (RED) {
|
|
|
115
127
|
return client;
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
*/
|
|
123
|
-
// Modbus spec maximums (per IEC 61158-6-7) — enforced before hitting the wire
|
|
124
|
-
// so users get a clear error instead of a cryptic device exception.
|
|
125
|
-
const MAX_READ_BITS = 2000; // FC1, FC2
|
|
126
|
-
const MAX_READ_REGS = 125; // FC3, FC4
|
|
127
|
-
const MAX_WRITE_BITS = 1968; // FC15
|
|
128
|
-
const MAX_WRITE_REGS = 123; // FC16
|
|
130
|
+
const MAX_READ_BITS = 2000;
|
|
131
|
+
const MAX_READ_REGS = 125;
|
|
132
|
+
const MAX_WRITE_BITS = 1968;
|
|
133
|
+
const MAX_WRITE_REGS = 123;
|
|
129
134
|
|
|
130
135
|
node.read = function (opts) {
|
|
131
|
-
|
|
136
|
+
const t = node.resolveTarget(opts);
|
|
137
|
+
const merged = Object.assign({}, opts, t);
|
|
132
138
|
node._masterQueue = node._masterQueue.then(async () => {
|
|
133
139
|
node.checkLimit();
|
|
134
|
-
const fc = parseInt(
|
|
135
|
-
const addr = parseInt(
|
|
136
|
-
const qty = parseInt(
|
|
137
|
-
// Pre-validate quantity against Modbus spec
|
|
140
|
+
const fc = parseInt(merged.functionCode, 10);
|
|
141
|
+
const addr = parseInt(merged.address, 10);
|
|
142
|
+
const qty = parseInt(merged.quantity, 10);
|
|
138
143
|
if (qty < 1) throw new Error(`Quantity must be >= 1 (got ${qty})`);
|
|
139
144
|
if ((fc === 1 || fc === 2) && qty > MAX_READ_BITS) {
|
|
140
145
|
throw new Error(`FC${fc} read quantity ${qty} exceeds Modbus spec max of ${MAX_READ_BITS} bits`);
|
|
@@ -143,12 +148,12 @@ module.exports = function (RED) {
|
|
|
143
148
|
throw new Error(`FC${fc} read quantity ${qty} exceeds Modbus spec max of ${MAX_READ_REGS} registers`);
|
|
144
149
|
}
|
|
145
150
|
const target = node.masterMode === 'rtu'
|
|
146
|
-
? `rtu:${node.serialPort}` : `${
|
|
151
|
+
? `rtu:${node.serialPort}` : `${merged.host}:${merged.port}`;
|
|
147
152
|
const start = Date.now();
|
|
148
153
|
let res, err = null;
|
|
149
154
|
try {
|
|
150
|
-
const client = await getClient(
|
|
151
|
-
client.setID(
|
|
155
|
+
const client = await getClient(merged);
|
|
156
|
+
client.setID(merged.unitId);
|
|
152
157
|
switch (fc) {
|
|
153
158
|
case 1: res = await client.readCoils(addr, qty); break;
|
|
154
159
|
case 2: res = await client.readDiscreteInputs(addr, qty); break;
|
|
@@ -162,7 +167,7 @@ module.exports = function (RED) {
|
|
|
162
167
|
emitTraffic({
|
|
163
168
|
ts: new Date().toISOString(),
|
|
164
169
|
direction: 'read', kind: 'master',
|
|
165
|
-
target, unitId:
|
|
170
|
+
target, unitId: merged.unitId, fc, address: addr, quantity: qty,
|
|
166
171
|
values: res ? res.data : null,
|
|
167
172
|
durationMs: Date.now() - start,
|
|
168
173
|
ok: !err, error: err ? err.message : null
|
|
@@ -172,17 +177,14 @@ module.exports = function (RED) {
|
|
|
172
177
|
return node._masterQueue;
|
|
173
178
|
};
|
|
174
179
|
|
|
175
|
-
/**
|
|
176
|
-
* Write raw register values to a remote Modbus device.
|
|
177
|
-
* Caller must pre-encode multi-register values (use the bytes palette).
|
|
178
|
-
*/
|
|
179
180
|
node.write = function (opts) {
|
|
181
|
+
const t = node.resolveTarget(opts);
|
|
182
|
+
const merged = Object.assign({}, opts, t);
|
|
180
183
|
node._masterQueue = node._masterQueue.then(async () => {
|
|
181
184
|
node.checkLimit();
|
|
182
|
-
const fc = parseInt(
|
|
183
|
-
const addr = parseInt(
|
|
184
|
-
const values =
|
|
185
|
-
// Pre-validate write count against Modbus spec
|
|
185
|
+
const fc = parseInt(merged.functionCode, 10);
|
|
186
|
+
const addr = parseInt(merged.address, 10);
|
|
187
|
+
const values = merged.values;
|
|
186
188
|
const count = Array.isArray(values) ? values.length : 1;
|
|
187
189
|
if (fc === 15 && count > MAX_WRITE_BITS) {
|
|
188
190
|
throw new Error(`FC15 write count ${count} exceeds Modbus spec max of ${MAX_WRITE_BITS} bits`);
|
|
@@ -191,12 +193,12 @@ module.exports = function (RED) {
|
|
|
191
193
|
throw new Error(`FC16 write count ${count} exceeds Modbus spec max of ${MAX_WRITE_REGS} registers`);
|
|
192
194
|
}
|
|
193
195
|
const target = node.masterMode === 'rtu'
|
|
194
|
-
? `rtu:${node.serialPort}` : `${
|
|
196
|
+
? `rtu:${node.serialPort}` : `${merged.host}:${merged.port}`;
|
|
195
197
|
const start = Date.now();
|
|
196
198
|
let err = null;
|
|
197
199
|
try {
|
|
198
|
-
const client = await getClient(
|
|
199
|
-
client.setID(
|
|
200
|
+
const client = await getClient(merged);
|
|
201
|
+
client.setID(merged.unitId);
|
|
200
202
|
switch (fc) {
|
|
201
203
|
case 5: return await client.writeCoil(addr, !!values[0]);
|
|
202
204
|
case 6: return await client.writeRegister(addr, values[0]);
|
|
@@ -209,7 +211,7 @@ module.exports = function (RED) {
|
|
|
209
211
|
emitTraffic({
|
|
210
212
|
ts: new Date().toISOString(),
|
|
211
213
|
direction: 'write', kind: 'master',
|
|
212
|
-
target, unitId:
|
|
214
|
+
target, unitId: merged.unitId, fc, address: addr,
|
|
213
215
|
quantity: values.length, values,
|
|
214
216
|
durationMs: Date.now() - start,
|
|
215
217
|
ok: !err, error: err ? err.message : null
|
|
@@ -222,7 +224,6 @@ module.exports = function (RED) {
|
|
|
222
224
|
// ====================================================================
|
|
223
225
|
// EMBEDDED SLAVE SERVER (Modbus TCP)
|
|
224
226
|
// ====================================================================
|
|
225
|
-
// Local register store — single unit, multiple register types
|
|
226
227
|
node._slaveStore = {
|
|
227
228
|
coils: new Array(65536).fill(false),
|
|
228
229
|
discrete: new Array(65536).fill(false),
|
|
@@ -259,9 +260,6 @@ module.exports = function (RED) {
|
|
|
259
260
|
});
|
|
260
261
|
};
|
|
261
262
|
|
|
262
|
-
// Helper: emit a slave traffic event for ops coming from EXTERNAL Modbus masters
|
|
263
|
-
// (i.e. via the embedded TCP server's vector callbacks). Internal slaveGet/slaveSet
|
|
264
|
-
// already emit their own events tagged via='flow'.
|
|
265
263
|
function emitExternalSlave(direction, registerType, address, values) {
|
|
266
264
|
emitTraffic({
|
|
267
265
|
ts: new Date().toISOString(),
|
|
@@ -325,9 +323,6 @@ module.exports = function (RED) {
|
|
|
325
323
|
}
|
|
326
324
|
}
|
|
327
325
|
|
|
328
|
-
// ====================================================================
|
|
329
|
-
// CLEANUP
|
|
330
|
-
// ====================================================================
|
|
331
326
|
node.on('close', function (done) {
|
|
332
327
|
const tasks = [];
|
|
333
328
|
for (const client of node._masterPool.values()) {
|
|
@@ -4,28 +4,30 @@
|
|
|
4
4
|
color: '#7c3aed',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: '' },
|
|
7
|
-
server: { value: '', type: 'modpackqt-config' },
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
server: { value: '', type: 'modpackqt-config', required: true },
|
|
8
|
+
// Legacy per-node fields — kept hidden so old flows keep working.
|
|
9
|
+
targetHost: { value: '' },
|
|
10
|
+
targetPort: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
|
|
11
|
+
unitId: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
|
|
12
|
+
timeoutMs: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } }
|
|
12
13
|
},
|
|
13
14
|
inputs: 0,
|
|
14
15
|
outputs: 0,
|
|
15
16
|
icon: 'font-awesome/fa-rocket',
|
|
16
17
|
label: function () {
|
|
17
|
-
return this.name ||
|
|
18
|
+
return this.name || 'master probe';
|
|
18
19
|
},
|
|
19
20
|
paletteLabel: 'modbus master probe',
|
|
20
21
|
oneditprepare: function () {
|
|
21
22
|
const node = this;
|
|
22
|
-
if (window.ModPackQTMasterPicker) ModPackQTMasterPicker.attach(this);
|
|
23
23
|
const buildLink = (info) => {
|
|
24
24
|
const probeHost = (info && info.host) || '127.0.0.1';
|
|
25
25
|
const probePort = (info && info.port) || 8502;
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
26
|
+
const cfgId = $('#node-input-server').val();
|
|
27
|
+
const cfg = cfgId ? RED.nodes.node(cfgId) : null;
|
|
28
|
+
const targetHost = node.targetHost || (cfg && cfg.targetHost) || '';
|
|
29
|
+
const targetPort = node.targetPort || (cfg && cfg.targetPort) || 502;
|
|
30
|
+
const unitId = node.unitId || (cfg && cfg.unitId) || 1;
|
|
29
31
|
const name = $('#node-input-name').val() || node.name || '';
|
|
30
32
|
const url = 'https://modpackqt.com/master'
|
|
31
33
|
+ '?probe=' + encodeURIComponent(node.id)
|
|
@@ -40,8 +42,7 @@
|
|
|
40
42
|
$('#modpackqt-probe-target').text(targetHost + ':' + targetPort + ' · unit ' + unitId);
|
|
41
43
|
};
|
|
42
44
|
$.getJSON('modpackqt-probe/info').done(buildLink).fail(() => buildLink({}));
|
|
43
|
-
|
|
44
|
-
$('#node-input-modpackqt-picker').on('change', () => setTimeout(() => buildLink({}), 0));
|
|
45
|
+
$('#node-input-server').on('change', () => buildLink({}));
|
|
45
46
|
}
|
|
46
47
|
});
|
|
47
48
|
</script>
|
|
@@ -55,16 +56,10 @@
|
|
|
55
56
|
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
|
|
56
57
|
<input type="text" id="node-input-server">
|
|
57
58
|
</div>
|
|
58
|
-
<div class="form-row">
|
|
59
|
-
<label for="node-input-modpackqt-picker"><i class="fa fa-cloud-download"></i> My Profiles</label>
|
|
60
|
-
<select id="node-input-modpackqt-picker" style="width:65%"></select>
|
|
61
|
-
<button type="button" id="modpackqt-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
62
|
-
</div>
|
|
63
59
|
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
64
|
-
|
|
60
|
+
The runtime config carries the device target — the probe deep-links the web tester to it. To probe a different device, create another runtime config (pencil → +).
|
|
65
61
|
</div>
|
|
66
62
|
|
|
67
|
-
<!-- Hidden state — populated by the picker, used to build the probe URL. -->
|
|
68
63
|
<input type="hidden" id="node-input-targetHost">
|
|
69
64
|
<input type="hidden" id="node-input-targetPort">
|
|
70
65
|
<input type="hidden" id="node-input-unitId">
|
|
@@ -88,31 +83,17 @@
|
|
|
88
83
|
</script>
|
|
89
84
|
|
|
90
85
|
<script type="text/html" data-help-name="modpackqt-master-probe">
|
|
91
|
-
<p>Live tester / commissioning probe for
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
to that device.</p>
|
|
95
|
-
|
|
96
|
-
<h3>How it works</h3>
|
|
97
|
-
<p>The first probe deployed in this Node-RED instance starts a small local
|
|
98
|
-
HTTP server (default <code>127.0.0.1:8502</code>). The
|
|
99
|
-
<a href="https://modpackqt.com" target="_blank">modpackqt.com web console</a>
|
|
100
|
-
calls that local server to read/write registers, scan ranges, decode bytes,
|
|
101
|
-
and save profiles.</p>
|
|
86
|
+
<p>Live tester / commissioning probe for the Modbus device defined by the
|
|
87
|
+
chosen runtime config. Click <b>Open in ModPackQT Console</b> to launch
|
|
88
|
+
the web tester live-attached to that device.</p>
|
|
102
89
|
|
|
103
90
|
<h3>Multiple devices</h3>
|
|
104
|
-
<p>
|
|
105
|
-
probe nodes from this Node-RED
|
|
106
|
-
between devices with one click.</p>
|
|
91
|
+
<p>Create one runtime config per device, then drop one master-probe per
|
|
92
|
+
runtime. The web console aggregates all probe nodes from this Node-RED
|
|
93
|
+
instance into a single sidebar — switch between devices with one click.</p>
|
|
107
94
|
|
|
108
95
|
<h3>Network access</h3>
|
|
109
96
|
<p>The runtime binds to <code>127.0.0.1</code> by default — only browsers on
|
|
110
|
-
the same machine can reach it. To allow remote access
|
|
111
|
-
|
|
112
|
-
before starting Node-RED (and ensure your firewall allows port 8502).</p>
|
|
113
|
-
|
|
114
|
-
<h3>No inputs / outputs</h3>
|
|
115
|
-
<p>Probe nodes are commissioning tools — they don't participate in flows.
|
|
116
|
-
Use the regular <code>modpackqt-master-read</code> / <code>-write</code>
|
|
117
|
-
nodes to wire Modbus into your flow logic.</p>
|
|
97
|
+
the same machine can reach it. To allow remote access, set environment
|
|
98
|
+
variable <code>MODPACKQT_PROBE_HOST=0.0.0.0</code> before starting Node-RED.</p>
|
|
118
99
|
</script>
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: '' },
|
|
7
7
|
server: { value: '', type: 'modpackqt-config', required: true },
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// Legacy per-node target fields (pre-v3.3.0) — kept hidden so old flows
|
|
9
|
+
// keep working. New nodes leave them empty and inherit from the runtime.
|
|
10
|
+
targetHost: { value: '' },
|
|
11
|
+
targetPort: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
|
|
12
|
+
unitId: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
|
|
11
13
|
functionCode: { value: '3', required: true },
|
|
12
14
|
address: { value: 0, required: true, validate: RED.validators.number() },
|
|
13
15
|
quantity: { value: 1, required: true, validate: RED.validators.number() },
|
|
@@ -19,91 +21,21 @@
|
|
|
19
21
|
label: function () {
|
|
20
22
|
return this.name || `Master Read FC${this.functionCode} @${this.address}`;
|
|
21
23
|
},
|
|
22
|
-
paletteLabel: 'modbus master read'
|
|
23
|
-
oneditprepare: function () {
|
|
24
|
-
ModPackQTMasterPicker.attach(this);
|
|
25
|
-
if (window.ModPackQTAdvancedToggle) ModPackQTAdvancedToggle.attach('modpackqt-master-read');
|
|
26
|
-
}
|
|
24
|
+
paletteLabel: 'modbus master read'
|
|
27
25
|
});
|
|
28
26
|
</script>
|
|
29
27
|
|
|
30
|
-
<!-- Shared picker helper — loaded once per editor session -->
|
|
31
|
-
<script type="text/javascript">
|
|
32
|
-
if (!window.ModPackQTMasterPicker) {
|
|
33
|
-
window.ModPackQTMasterPicker = {
|
|
34
|
-
attach: function (node) {
|
|
35
|
-
const $sel = $('#node-input-modpackqt-picker');
|
|
36
|
-
if (!$sel.length) return;
|
|
37
|
-
const reload = function () {
|
|
38
|
-
const cfgId = $('#node-input-server').val();
|
|
39
|
-
$sel.empty().append('<option value="">— Loading… —</option>');
|
|
40
|
-
if (!cfgId || cfgId === '_ADD_') {
|
|
41
|
-
$sel.empty().append('<option value="">— Set up runtime & Account Key under Advanced —</option>');
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
$.getJSON('modpackqt/connections?config=' + encodeURIComponent(cfgId))
|
|
45
|
-
.done(function (rows) {
|
|
46
|
-
$sel.empty().append('<option value="">— Manual entry (see Advanced) —</option>');
|
|
47
|
-
(rows || [])
|
|
48
|
-
.filter(function (c) { return c && (c.connectionType === 'tcp' || !c.connectionType); })
|
|
49
|
-
.forEach(function (c) {
|
|
50
|
-
const label = (c.name || '(unnamed)') + ' — ' + (c.host || '?') + ':' + (c.port || 502) + ' #' + (c.unitId || 1);
|
|
51
|
-
$('<option>').val(c.id).text(label).data('row', c).appendTo($sel);
|
|
52
|
-
});
|
|
53
|
-
})
|
|
54
|
-
.fail(function (xhr) {
|
|
55
|
-
const msg = (xhr.responseJSON && xhr.responseJSON.error) || ('HTTP ' + xhr.status);
|
|
56
|
-
$sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
|
|
57
|
-
});
|
|
58
|
-
};
|
|
59
|
-
$('#node-input-server').on('change', reload);
|
|
60
|
-
$('#modpackqt-picker-refresh').on('click', function (e) { e.preventDefault(); reload(); });
|
|
61
|
-
$sel.on('change', function () {
|
|
62
|
-
const row = $(this).find('option:selected').data('row');
|
|
63
|
-
if (!row) return;
|
|
64
|
-
$('#node-input-targetHost').val(row.host || '');
|
|
65
|
-
$('#node-input-targetPort').val(row.port || 502);
|
|
66
|
-
$('#node-input-unitId').val(row.unitId || 1);
|
|
67
|
-
});
|
|
68
|
-
reload();
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
if (!window.ModPackQTAdvancedToggle) {
|
|
73
|
-
window.ModPackQTAdvancedToggle = {
|
|
74
|
-
attach: function (/* nodeType */) {
|
|
75
|
-
const $adv = $('.modpackqt-advanced');
|
|
76
|
-
const $tog = $('.modpackqt-advanced-toggle');
|
|
77
|
-
if (!$adv.length || !$tog.length) return;
|
|
78
|
-
const setOpen = function (open) {
|
|
79
|
-
$adv.toggle(open);
|
|
80
|
-
$tog.html((open ? '▾ Hide advanced' : '▸ Advanced — runtime config & manual entry'));
|
|
81
|
-
};
|
|
82
|
-
// Auto-expand if runtime not yet configured
|
|
83
|
-
const cfgId = $('#node-input-server').val();
|
|
84
|
-
const needsSetup = !cfgId || cfgId === '' || cfgId === '_ADD_';
|
|
85
|
-
setOpen(needsSetup);
|
|
86
|
-
$tog.off('click.modpackqt').on('click.modpackqt', function (e) {
|
|
87
|
-
e.preventDefault();
|
|
88
|
-
setOpen($adv.is(':hidden'));
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
</script>
|
|
94
|
-
|
|
95
28
|
<script type="text/html" data-template-name="modpackqt-master-read">
|
|
96
29
|
<div class="form-row">
|
|
97
30
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
98
31
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
99
32
|
</div>
|
|
100
33
|
<div class="form-row">
|
|
101
|
-
<label for="node-input-
|
|
102
|
-
<
|
|
103
|
-
<button type="button" id="modpackqt-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
34
|
+
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
|
|
35
|
+
<input type="text" id="node-input-server">
|
|
104
36
|
</div>
|
|
105
37
|
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
106
|
-
|
|
38
|
+
The runtime config carries Host / Port / Unit. To talk to a different device, create another runtime config (pencil → +).
|
|
107
39
|
</div>
|
|
108
40
|
|
|
109
41
|
<div class="form-row">
|
|
@@ -128,27 +60,10 @@
|
|
|
128
60
|
<input type="number" id="node-input-pollInterval" min="0" placeholder="1000 (0 = disabled, input-only)">
|
|
129
61
|
</div>
|
|
130
62
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<
|
|
135
|
-
<div class="form-row">
|
|
136
|
-
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
|
|
137
|
-
<input type="text" id="node-input-server">
|
|
138
|
-
</div>
|
|
139
|
-
<div class="form-row">
|
|
140
|
-
<label for="node-input-targetHost"><i class="fa fa-plug"></i> Target Host</label>
|
|
141
|
-
<input type="text" id="node-input-targetHost" placeholder="Modbus device IP/hostname (TCP only)">
|
|
142
|
-
</div>
|
|
143
|
-
<div class="form-row">
|
|
144
|
-
<label for="node-input-targetPort"><i class="fa fa-hashtag"></i> Target Port</label>
|
|
145
|
-
<input type="number" id="node-input-targetPort" min="1" max="65535" placeholder="502">
|
|
146
|
-
</div>
|
|
147
|
-
<div class="form-row">
|
|
148
|
-
<label for="node-input-unitId"><i class="fa fa-id-card"></i> Unit ID</label>
|
|
149
|
-
<input type="number" id="node-input-unitId" min="1" max="247" placeholder="1">
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
63
|
+
<!-- Legacy per-node overrides — hidden, preserved for back-compat with older flows. -->
|
|
64
|
+
<input type="hidden" id="node-input-targetHost">
|
|
65
|
+
<input type="hidden" id="node-input-targetPort">
|
|
66
|
+
<input type="hidden" id="node-input-unitId">
|
|
152
67
|
|
|
153
68
|
<div class="form-tips" style="margin-top:14px">
|
|
154
69
|
<b>Output:</b> <code>msg.payload</code> = raw register array. To decode int / float / string,
|
|
@@ -166,8 +81,7 @@
|
|
|
166
81
|
</script>
|
|
167
82
|
|
|
168
83
|
<script type="text/html" data-help-name="modpackqt-master-read">
|
|
169
|
-
<p>Reads Modbus registers from
|
|
170
|
-
<code>modpackqt-config</code> node. No external gateway app required.</p>
|
|
84
|
+
<p>Reads Modbus registers from the device defined by the chosen runtime config.</p>
|
|
171
85
|
<h3>Outputs</h3>
|
|
172
86
|
<dl class="message-properties">
|
|
173
87
|
<dt>payload <span class="property-type">array</span></dt>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ModPackQT Master Read — embedded Modbus TCP/RTU read.
|
|
3
|
+
* Inherits Host / Port / Unit from the runtime config. Legacy nodes that
|
|
4
|
+
* still have per-node targetHost/targetPort/unitId set will override.
|
|
3
5
|
* Outputs raw register values (no decoding). Pair with
|
|
4
6
|
* node-red-contrib-bytes-modpackqt to decode int/float/string/bitmask.
|
|
5
7
|
*/
|
|
@@ -9,9 +11,10 @@ module.exports = function (RED) {
|
|
|
9
11
|
const node = this;
|
|
10
12
|
const server = RED.nodes.getNode(config.server);
|
|
11
13
|
|
|
12
|
-
node
|
|
13
|
-
node.
|
|
14
|
-
node.
|
|
14
|
+
// Legacy per-node overrides — empty by default in v3.3.0+.
|
|
15
|
+
node.targetHost = config.targetHost || '';
|
|
16
|
+
node.targetPort = parseInt(config.targetPort, 10) || 0;
|
|
17
|
+
node.unitId = parseInt(config.unitId, 10) || 0;
|
|
15
18
|
node.functionCode = parseInt(config.functionCode, 10) || 3;
|
|
16
19
|
node.address = parseInt(config.address, 10) || 0;
|
|
17
20
|
node.quantity = parseInt(config.quantity, 10) || 1;
|
|
@@ -25,10 +28,15 @@ module.exports = function (RED) {
|
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
27
30
|
try {
|
|
31
|
+
const tgt = server.resolveTarget({
|
|
32
|
+
host: node.targetHost || undefined,
|
|
33
|
+
port: node.targetPort || undefined,
|
|
34
|
+
unitId: node.unitId || undefined
|
|
35
|
+
});
|
|
28
36
|
const values = await server.read({
|
|
29
|
-
host:
|
|
30
|
-
port:
|
|
31
|
-
unitId:
|
|
37
|
+
host: tgt.host,
|
|
38
|
+
port: tgt.port,
|
|
39
|
+
unitId: tgt.unitId,
|
|
32
40
|
functionCode: node.functionCode,
|
|
33
41
|
address: node.address,
|
|
34
42
|
quantity: node.quantity
|
|
@@ -41,7 +49,7 @@ module.exports = function (RED) {
|
|
|
41
49
|
});
|
|
42
50
|
node.send({
|
|
43
51
|
payload: values,
|
|
44
|
-
topic: `modpackqt/read/${
|
|
52
|
+
topic: `modpackqt/read/${tgt.host}:${tgt.port}/fc${node.functionCode}/${node.address}`,
|
|
45
53
|
modpackqt: { fc: node.functionCode, address: node.address, quantity: node.quantity }
|
|
46
54
|
});
|
|
47
55
|
} catch (err) {
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: '' },
|
|
7
7
|
server: { value: '', type: 'modpackqt-config', required: true },
|
|
8
|
-
targetHost: { value: '
|
|
9
|
-
targetPort: { value:
|
|
10
|
-
unitId: { value:
|
|
8
|
+
targetHost: { value: '' },
|
|
9
|
+
targetPort: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
|
|
10
|
+
unitId: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
|
|
11
11
|
functionCode: { value: '6', required: true },
|
|
12
12
|
address: { value: 0, required: true, validate: RED.validators.number() }
|
|
13
13
|
},
|
|
@@ -17,11 +17,7 @@
|
|
|
17
17
|
label: function () {
|
|
18
18
|
return this.name || `Master Write FC${this.functionCode} @${this.address}`;
|
|
19
19
|
},
|
|
20
|
-
paletteLabel: 'modbus master write'
|
|
21
|
-
oneditprepare: function () {
|
|
22
|
-
if (window.ModPackQTMasterPicker) ModPackQTMasterPicker.attach(this);
|
|
23
|
-
if (window.ModPackQTAdvancedToggle) ModPackQTAdvancedToggle.attach('modpackqt-master-write');
|
|
24
|
-
}
|
|
20
|
+
paletteLabel: 'modbus master write'
|
|
25
21
|
});
|
|
26
22
|
</script>
|
|
27
23
|
|
|
@@ -31,12 +27,11 @@
|
|
|
31
27
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
32
28
|
</div>
|
|
33
29
|
<div class="form-row">
|
|
34
|
-
<label for="node-input-
|
|
35
|
-
<
|
|
36
|
-
<button type="button" id="modpackqt-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
30
|
+
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
|
|
31
|
+
<input type="text" id="node-input-server">
|
|
37
32
|
</div>
|
|
38
33
|
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
39
|
-
|
|
34
|
+
The runtime config carries Host / Port / Unit. To talk to a different device, create another runtime config (pencil → +).
|
|
40
35
|
</div>
|
|
41
36
|
|
|
42
37
|
<div class="form-row">
|
|
@@ -53,27 +48,9 @@
|
|
|
53
48
|
<input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
|
|
54
49
|
</div>
|
|
55
50
|
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<div class="modpackqt-advanced" style="display:none">
|
|
60
|
-
<div class="form-row">
|
|
61
|
-
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
|
|
62
|
-
<input type="text" id="node-input-server">
|
|
63
|
-
</div>
|
|
64
|
-
<div class="form-row">
|
|
65
|
-
<label for="node-input-targetHost"><i class="fa fa-plug"></i> Target Host</label>
|
|
66
|
-
<input type="text" id="node-input-targetHost" placeholder="Modbus device IP/hostname (TCP only)">
|
|
67
|
-
</div>
|
|
68
|
-
<div class="form-row">
|
|
69
|
-
<label for="node-input-targetPort"><i class="fa fa-hashtag"></i> Target Port</label>
|
|
70
|
-
<input type="number" id="node-input-targetPort" min="1" max="65535" placeholder="502">
|
|
71
|
-
</div>
|
|
72
|
-
<div class="form-row">
|
|
73
|
-
<label for="node-input-unitId"><i class="fa fa-id-card"></i> Unit ID</label>
|
|
74
|
-
<input type="number" id="node-input-unitId" min="1" max="247" placeholder="1">
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
51
|
+
<input type="hidden" id="node-input-targetHost">
|
|
52
|
+
<input type="hidden" id="node-input-targetPort">
|
|
53
|
+
<input type="hidden" id="node-input-unitId">
|
|
77
54
|
|
|
78
55
|
<div class="form-tips" style="margin-top:14px">
|
|
79
56
|
<b>Input:</b> <code>msg.payload</code> = number (FC5/FC6) or array of numbers (FC15/FC16).
|
|
@@ -92,7 +69,7 @@
|
|
|
92
69
|
</script>
|
|
93
70
|
|
|
94
71
|
<script type="text/html" data-help-name="modpackqt-master-write">
|
|
95
|
-
<p>Writes Modbus registers to
|
|
72
|
+
<p>Writes Modbus registers to the device defined by the chosen runtime config.</p>
|
|
96
73
|
<h3>Inputs</h3>
|
|
97
74
|
<dl class="message-properties">
|
|
98
75
|
<dt>payload <span class="property-type">number | array</span></dt>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ModPackQT Master Write — embedded Modbus TCP/RTU write.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Inherits Host / Port / Unit from the runtime config. Legacy nodes that
|
|
4
|
+
* still have per-node targetHost/targetPort/unitId set will override.
|
|
5
5
|
*/
|
|
6
6
|
module.exports = function (RED) {
|
|
7
7
|
function ModPackQTMasterWriteNode(config) {
|
|
@@ -9,9 +9,9 @@ module.exports = function (RED) {
|
|
|
9
9
|
const node = this;
|
|
10
10
|
const server = RED.nodes.getNode(config.server);
|
|
11
11
|
|
|
12
|
-
node.targetHost = config.targetHost || '
|
|
13
|
-
node.targetPort = parseInt(config.targetPort,
|
|
14
|
-
node.unitId = parseInt(config.unitId,
|
|
12
|
+
node.targetHost = config.targetHost || '';
|
|
13
|
+
node.targetPort = parseInt(config.targetPort, 10) || 0;
|
|
14
|
+
node.unitId = parseInt(config.unitId, 10) || 0;
|
|
15
15
|
node.functionCode = parseInt(config.functionCode, 10) || 6;
|
|
16
16
|
node.address = parseInt(config.address, 10) || 0;
|
|
17
17
|
|
|
@@ -25,7 +25,6 @@ module.exports = function (RED) {
|
|
|
25
25
|
if (typeof raw === 'string') {
|
|
26
26
|
try { raw = JSON.parse(raw); } catch (_) { raw = Number(raw); }
|
|
27
27
|
}
|
|
28
|
-
// Booleans accepted for FC5/FC15 (coils) — pass through; otherwise require integers.
|
|
29
28
|
const isCoilWrite = (node.functionCode === 5 || node.functionCode === 15);
|
|
30
29
|
const arr = Array.isArray(raw) ? raw : [raw];
|
|
31
30
|
const values = arr.map((v) => {
|
|
@@ -50,15 +49,19 @@ module.exports = function (RED) {
|
|
|
50
49
|
`For larger numbers use encode-int32 / encode-uint32 from the bytes palette.`
|
|
51
50
|
);
|
|
52
51
|
}
|
|
53
|
-
// Normalize signed → unsigned 16-bit for the wire
|
|
54
52
|
return n < 0 ? (n + 0x10000) : (n & 0xFFFF);
|
|
55
53
|
});
|
|
56
54
|
|
|
57
55
|
node.status({ fill: 'blue', shape: 'ring', text: `writing ${values.length}…` });
|
|
56
|
+
const tgt = server.resolveTarget({
|
|
57
|
+
host: node.targetHost || undefined,
|
|
58
|
+
port: node.targetPort || undefined,
|
|
59
|
+
unitId: node.unitId || undefined
|
|
60
|
+
});
|
|
58
61
|
await server.write({
|
|
59
|
-
host:
|
|
60
|
-
port:
|
|
61
|
-
unitId:
|
|
62
|
+
host: tgt.host,
|
|
63
|
+
port: tgt.port,
|
|
64
|
+
unitId: tgt.unitId,
|
|
62
65
|
functionCode: node.functionCode,
|
|
63
66
|
address: node.address,
|
|
64
67
|
values
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-modbus-modpackqt",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Modbus commissioning, testing & analysis tools for Node-RED. Embedded Modbus TCP/RTU master + slave server, FC1/FC2/FC3/FC4 reads, FC5/FC6/FC15/FC16 writes, built-in slave register store, and a passive traffic monitor for debugging. 100% free, MIT, no usage limits. By ModPackQT — open the matching web console at modpackqt.com for register decoding, simulation and AI assistance.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|