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 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.2.4';
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 Gateway' },
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 m = this.masterMode === 'rtu' ? `RTU ${this.serialPort || '?'}` : 'TCP master';
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 || `${m}${s}`;
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
- // Embedded slave server section is hidden by default — only relevant
39
- // for users of the modpackqt-slave-read / -write nodes. Auto-expand
40
- // if the slave server is already enabled on this config.
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 &amp; 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="ModPackQT Gateway">
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">Master (outbound to remote devices)</h4>
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 &amp; 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 &amp; 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>No extra app needed.</b> Modbus runs inside Node-RED. 100% free, no usage limits.
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
- Shared runtime for all <b>ModPackQT</b> nodes. The Modbus master client and the
153
- optional slave server both run inside this Node-RED process<b>no separate gateway
154
- app required</b>.
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 remote devices via TCP. Each master node specifies its target host/port.</li>
160
- <li><b>Modbus RTU</b> — opens the configured serial port and talks to RTU slaves on the bus. All RTU master nodes share this serial port.</li>
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 connecting to that port read whatever you
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 master and probe nodes show a dropdown of your saved Modbus
176
- profiles &amp; slaves so you don't have to retype IPs and unit IDs. All Modbus traffic
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
- * - A connection pool of Modbus TCP/RTU master clients (one per target).
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 the runtime config — paste it in the modpackqt-config node first.' });
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
- node.opsToday = function () { return node._opsCount; };
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(); // serialize all master ops
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
- * Read raw register values from a remote Modbus device.
120
- * Returns plain array — no decoding, no objects. Use the
121
- * node-red-contrib-bytes-modpackqt palette to decode.
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
- // Queue this op behind any in-flight master ops to avoid serial-port collision
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(opts.functionCode, 10);
135
- const addr = parseInt(opts.address, 10);
136
- const qty = parseInt(opts.quantity, 10);
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}` : `${opts.host}:${opts.port}`;
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(opts);
151
- client.setID(opts.unitId);
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: opts.unitId, fc, address: addr, quantity: qty,
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(opts.functionCode, 10);
183
- const addr = parseInt(opts.address, 10);
184
- const values = opts.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}` : `${opts.host}:${opts.port}`;
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(opts);
199
- client.setID(opts.unitId);
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: opts.unitId, fc, address: addr,
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
- targetHost: { value: '192.168.1.10' },
9
- targetPort: { value: 502, validate: RED.validators.number() },
10
- unitId: { value: 1, validate: RED.validators.number() },
11
- timeoutMs: { value: 3000, validate: RED.validators.number() }
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 || `master probe ${this.targetHost}:${this.targetPort} #${this.unitId}`;
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 targetHost = $('#node-input-targetHost').val() || node.targetHost || '';
27
- const targetPort = $('#node-input-targetPort').val() || node.targetPort || 502;
28
- const unitId = $('#node-input-unitId').val() || node.unitId || 1;
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
- // Refresh the link summary whenever the picker fills the hidden fields
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
- Pick a saved Modbus profile from your modpackqt.com account — the probe deep-links the web tester to that device. Set up your Account Key on the runtime config (pencil icon).
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 a single Modbus TCP device.
92
- Pick a saved profile from your modpackqt.com account, then click
93
- <b>Open in ModPackQT Console</b> to launch the web tester live-attached
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>Drop one master-probe per device. The web console auto-aggregates all
105
- probe nodes from this Node-RED instance into a single sidebar — switch
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 from another machine's
111
- browser, set environment variable <code>MODPACKQT_PROBE_HOST=0.0.0.0</code>
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
- targetHost: { value: 'localhost', required: true },
9
- targetPort: { value: 502, required: true, validate: RED.validators.number() },
10
- unitId: { value: 1, required: true, validate: RED.validators.number() },
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 &amp; 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 &amp; 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-modpackqt-picker"><i class="fa fa-cloud-download"></i> My Profiles</label>
102
- <select id="node-input-modpackqt-picker" style="width:65%"></select>
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
- Pick a saved Modbus profile from your modpackqt.com account auto-fills host/port/unit. Set up your Account Key under <b>Advanced</b> below.
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
- <div class="form-row" style="margin:14px 0 4px 0;border-top:1px solid #e5e7eb;padding-top:10px">
132
- <a href="#" class="modpackqt-advanced-toggle" style="font-size:12px;color:#6b7280;text-decoration:none;cursor:pointer">▸ Advanced — runtime config &amp; manual entry</a>
133
- </div>
134
- <div class="modpackqt-advanced" style="display:none">
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 a remote device using the embedded Modbus runtime in the
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.targetHost = config.targetHost || 'localhost';
13
- node.targetPort = parseInt(config.targetPort, 10) || 502;
14
- node.unitId = parseInt(config.unitId, 10) || 1;
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: node.targetHost,
30
- port: node.targetPort,
31
- unitId: node.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/${node.targetHost}:${node.targetPort}/fc${node.functionCode}/${node.address}`,
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: 'localhost', required: true },
9
- targetPort: { value: 502, required: true, validate: RED.validators.number() },
10
- unitId: { value: 1, required: true, validate: RED.validators.number() },
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-modpackqt-picker"><i class="fa fa-cloud-download"></i> My Profiles</label>
35
- <select id="node-input-modpackqt-picker" style="width:65%"></select>
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
- Pick a saved Modbus profile from your modpackqt.com account auto-fills host/port/unit. Set up your Account Key under <b>Advanced</b> below.
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
- <div class="form-row" style="margin:14px 0 4px 0;border-top:1px solid #e5e7eb;padding-top:10px">
57
- <a href="#" class="modpackqt-advanced-toggle" style="font-size:12px;color:#6b7280;text-decoration:none;cursor:pointer">▸ Advanced — runtime config &amp; manual entry</a>
58
- </div>
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 a remote device using the embedded Modbus runtime.</p>
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
- * Accepts raw register values. Encode multi-register values upstream
4
- * with node-red-contrib-bytes-modpackqt if needed.
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 || 'localhost';
13
- node.targetPort = parseInt(config.targetPort, 10) || 502;
14
- node.unitId = parseInt(config.unitId, 10) || 1;
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: node.targetHost,
60
- port: node.targetPort,
61
- unitId: node.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.2.4",
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",