node-red-contrib-modbus-modpackqt 3.3.1 → 3.3.3

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,45 @@ 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.3] — 2026-05-10
8
+
9
+ ### Added
10
+
11
+ - **`modpackqt-slave-server` config node** — a dedicated Modbus TCP slave
12
+ server with its own register store (65 536 per type). This replaces the
13
+ confusing embedded slave hidden inside the runtime config. Drop one per
14
+ fake device; `slave-read` and `slave-write` nodes point at it directly.
15
+ - Account Key + **My Slaves** picker load saved slave configs from
16
+ modpackqt.com (pre-fills port and unit ID).
17
+ - Register layout and limits are managed from the modpackqt.com web
18
+ console — click **Open in ModPackQT Console** inside the config dialog.
19
+ - Registers with the probe runtime so the web console finds it automatically.
20
+
21
+ ### Changed
22
+
23
+ - **`slave-read` and `slave-write`** now have a **Slave Server** picker
24
+ (type `modpackqt-slave-server`) as their primary config reference. The old
25
+ `server` field (pointing to `modpackqt-config`) is kept as a hidden fallback
26
+ so existing flows with the embedded slave keep working.
27
+ - **Embedded slave section removed from runtime config dialog** — the
28
+ `modpackqt-config` dialog is now purely master/transport settings. The
29
+ embedded slave JS stays in the runtime for backward compat with old flows.
30
+ - `modpackqt-slave-probe` kept as-is for full backward compatibility.
31
+
32
+ ## [3.3.2] — 2026-05-10
33
+
34
+ ### Changed
35
+
36
+ - **Account Key moved to probe nodes.** It is now a field on
37
+ `modpackqt-master-probe` and `modpackqt-slave-probe` — the only
38
+ nodes that talk to modpackqt.com. The runtime config (`modpackqt-config`)
39
+ is now purely local device settings with no cloud fields at all.
40
+ - **My Profiles picker removed from runtime config.** The slave-probe's
41
+ **My Slaves** picker now reads the key from its own node credential
42
+ (`?probe=<nodeId>`) instead of the runtime config.
43
+ - Runtime config dialog is now clean: Name, Device, Transport, and the
44
+ optional embedded slave server section. Nothing cloud-related.
45
+
7
46
  ## [3.3.1] — 2026-05-10
8
47
 
9
48
  ### Changed
@@ -18,7 +18,7 @@
18
18
  const http = require('http');
19
19
  const { URL } = require('url');
20
20
 
21
- const PALETTE_VERSION = '3.3.1';
21
+ const PALETTE_VERSION = '3.3.3';
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;
@@ -3,12 +3,9 @@
3
3
  category: 'config',
4
4
  defaults: {
5
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
6
  targetHost: { value: 'localhost' },
9
7
  targetPort: { value: 502, validate: RED.validators.number() },
10
8
  unitId: { value: 1, validate: RED.validators.number() },
11
- // ── Master transport
12
9
  masterMode: { value: 'tcp', required: true },
13
10
  serialPort: { value: '' },
14
11
  baudRate: { value: 9600 },
@@ -16,14 +13,10 @@
16
13
  dataBits: { value: 8 },
17
14
  stopBits: { value: 1 },
18
15
  timeoutMs: { value: 3000, validate: RED.validators.number() },
19
- // ── Optional embedded Modbus TCP slave server
20
16
  slaveEnabled: { value: false },
21
17
  slaveHost: { value: '0.0.0.0' },
22
18
  slavePort: { value: 1502, validate: RED.validators.number() }
23
19
  },
24
- credentials: {
25
- apiKey: { type: 'password' }
26
- },
27
20
  label: function () {
28
21
  const t = this.masterMode === 'rtu'
29
22
  ? `RTU ${this.serialPort || '?'} #${this.unitId || 1}`
@@ -32,8 +25,6 @@
32
25
  return this.name && this.name !== 'ModPackQT Device' ? `${this.name} (${t})` : `${t}${s}`;
33
26
  },
34
27
  oneditprepare: function () {
35
- const node = this;
36
-
37
28
  const toggleRtu = () => {
38
29
  const isRtu = $('#node-config-input-masterMode').val() === 'rtu';
39
30
  $('.modpackqt-rtu-only').toggle(isRtu);
@@ -48,47 +39,6 @@
48
39
  toggleRtu();
49
40
  toggleSlave();
50
41
 
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
42
  // ── Embedded slave server section (collapsed by default)
93
43
  const $slaveSection = $('.modpackqt-slave-section');
94
44
  const $slaveToggle = $('.modpackqt-slave-section-toggle');
@@ -103,23 +53,6 @@
103
53
  e.preventDefault();
104
54
  setSlaveOpen($slaveSection.is(':hidden'));
105
55
  });
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
- });
123
56
  }
124
57
  });
125
58
  </script>
@@ -131,14 +64,6 @@
131
64
  </div>
132
65
 
133
66
  <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
67
  <div class="form-row modpackqt-tcp-only">
143
68
  <label for="node-config-input-targetHost"><i class="fa fa-plug"></i> Host</label>
144
69
  <input type="text" id="node-config-input-targetHost" placeholder="Modbus device IP/hostname">
@@ -193,20 +118,6 @@
193
118
  <input type="number" id="node-config-input-timeoutMs" placeholder="3000">
194
119
  </div>
195
120
 
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
-
210
121
  <div class="form-row" style="margin:14px 0 4px 0;border-top:1px solid #e5e7eb;padding-top:10px">
211
122
  <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>
212
123
  </div>
@@ -251,8 +162,8 @@
251
162
  <h3>One runtime = one device</h3>
252
163
  <p>
253
164
  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.
165
+ Each carries its own Host / Port / Unit ID. Read and write nodes inherit those they
166
+ only need to know the function code, address, and quantity.
256
167
  </p>
257
168
 
258
169
  <h3>Master mode</h3>
@@ -267,12 +178,4 @@
267
178
  <code>1502</code>). Use the <code>modpackqt-slave-write</code> node to push values into
268
179
  its register store; external Modbus masters read whatever you last wrote.
269
180
  </p>
270
-
271
- <h3>ModPackQT Account Key (optional)</h3>
272
- <p>
273
- Same tunnel key the Electron gateway uses — generate one at
274
- <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
275
- With a key set, the <b>My Profiles</b> picker above auto-fills Host / Port / Unit from
276
- your saved Modbus profiles.
277
- </p>
278
181
  </script>
@@ -19,12 +19,12 @@ module.exports = function (RED) {
19
19
  RED._modpackqtAdminRoutesRegistered = true;
20
20
  function makeProxy(fetchFn) {
21
21
  return function (req, res) {
22
- const configId = req.query && req.query.config;
23
- if (!configId) return res.status(400).json({ error: 'config query param required' });
24
- const cfg = RED.nodes.getNode(configId);
25
- if (!cfg) return res.status(404).json({ error: 'Runtime config not found (deploy first?)' });
26
- const key = cfg.credentials && cfg.credentials.apiKey;
27
- if (!key) return res.status(400).json({ error: 'No Account Key set on this runtime config.' });
22
+ const probeId = req.query && req.query.probe;
23
+ if (!probeId) return res.status(400).json({ error: 'probe query param required' });
24
+ const probe = RED.nodes.getNode(probeId);
25
+ if (!probe) return res.status(404).json({ error: 'Probe node not found (deploy first?)' });
26
+ const key = probe.credentials && probe.credentials.apiKey;
27
+ if (!key) return res.status(400).json({ error: 'No Account Key set on this probe node.' });
28
28
  fetchFn(key, function (err, data) {
29
29
  if (err) return res.status(502).json({ error: err.message });
30
30
  res.json(data);
@@ -64,8 +64,6 @@ module.exports = function (RED) {
64
64
  node.slavePort = parseInt(config.slavePort, 10) || 1502;
65
65
  node.slaveHost = config.slaveHost || '0.0.0.0';
66
66
 
67
- node.apiKey = (node.credentials && node.credentials.apiKey) || '';
68
-
69
67
  const today = () => new Date().toISOString().slice(0, 10);
70
68
  node._opsDay = today();
71
69
  node._opsCount = 0;
@@ -337,7 +335,5 @@ module.exports = function (RED) {
337
335
  });
338
336
  }
339
337
 
340
- RED.nodes.registerType('modpackqt-config', ModPackQTConfigNode, {
341
- credentials: { apiKey: { type: 'password' } }
342
- });
338
+ RED.nodes.registerType('modpackqt-config', ModPackQTConfigNode);
343
339
  };
@@ -11,6 +11,9 @@
11
11
  unitId: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
12
12
  timeoutMs: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } }
13
13
  },
14
+ credentials: {
15
+ apiKey: { type: 'password' }
16
+ },
14
17
  inputs: 0,
15
18
  outputs: 0,
16
19
  icon: 'font-awesome/fa-rocket',
@@ -57,7 +60,16 @@
57
60
  <input type="text" id="node-input-server">
58
61
  </div>
59
62
  <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
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 → +).
63
+ The runtime config carries the device target — the probe deep-links the web tester to it.
64
+ </div>
65
+
66
+ <div class="form-row">
67
+ <label for="node-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
68
+ <input type="password" id="node-input-apiKey" placeholder="Paste your ModPackQT tunnel key">
69
+ </div>
70
+ <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
71
+ Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
72
+ Used only by the modpackqt.com web console to sync with this probe.
61
73
  </div>
62
74
 
63
75
  <input type="hidden" id="node-input-targetHost">
@@ -87,6 +99,11 @@
87
99
  chosen runtime config. Click <b>Open in ModPackQT Console</b> to launch
88
100
  the web tester live-attached to that device.</p>
89
101
 
102
+ <h3>Account Key</h3>
103
+ <p>Paste your ModPackQT tunnel key here — generate one at
104
+ <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
105
+ The key lets the web console authenticate to this probe. All Modbus traffic stays local.</p>
106
+
90
107
  <h3>Multiple devices</h3>
91
108
  <p>Create one runtime config per device, then drop one master-probe per
92
109
  runtime. The web console aggregates all probe nodes from this Node-RED
@@ -112,5 +112,7 @@ module.exports = function (RED) {
112
112
  });
113
113
  }
114
114
 
115
- RED.nodes.registerType('modpackqt-master-probe', MasterProbeNode);
115
+ RED.nodes.registerType('modpackqt-master-probe', MasterProbeNode, {
116
+ credentials: { apiKey: { type: 'password' } }
117
+ });
116
118
  };
@@ -9,6 +9,9 @@
9
9
  bindPort: { value: 1502, validate: RED.validators.number() },
10
10
  unitId: { value: 1, validate: RED.validators.number() }
11
11
  },
12
+ credentials: {
13
+ apiKey: { type: 'password' }
14
+ },
12
15
  inputs: 0,
13
16
  outputs: 0,
14
17
  icon: 'font-awesome/fa-server',
@@ -50,13 +53,12 @@
50
53
  const $sel = $('#node-input-modpackqt-slave-picker');
51
54
  if (!$sel.length) return;
52
55
  const reload = function () {
53
- const cfgId = $('#node-input-server').val();
54
56
  $sel.empty().append('<option value="">— Loading… —</option>');
55
- if (!cfgId || cfgId === '_ADD_') {
56
- $sel.empty().append('<option value="">— Set up runtime &amp; Account Key first —</option>');
57
+ if (!node.id) {
58
+ $sel.empty().append('<option value="">— Save the node first, then reopen to load slaves —</option>');
57
59
  return;
58
60
  }
59
- $.getJSON('modpackqt/slaves?config=' + encodeURIComponent(cfgId))
61
+ $.getJSON('modpackqt/slaves?probe=' + encodeURIComponent(node.id))
60
62
  .done(function (rows) {
61
63
  $sel.empty().append('<option value="">— None —</option>');
62
64
  (rows || [])
@@ -71,7 +73,6 @@
71
73
  $sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
72
74
  });
73
75
  };
74
- $('#node-input-server').on('change', reload);
75
76
  $('#modpackqt-slave-picker-refresh').on('click', function (e) { e.preventDefault(); reload(); });
76
77
  $sel.on('change', function () {
77
78
  const row = $(this).find('option:selected').data('row');
@@ -94,13 +95,23 @@
94
95
  <label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
95
96
  <input type="text" id="node-input-server">
96
97
  </div>
98
+
99
+ <div class="form-row">
100
+ <label for="node-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
101
+ <input type="password" id="node-input-apiKey" placeholder="Paste your ModPackQT tunnel key">
102
+ </div>
103
+ <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
104
+ Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
105
+ Used only by the modpackqt.com web console to sync with this probe.
106
+ </div>
107
+
97
108
  <div class="form-row">
98
109
  <label for="node-input-modpackqt-slave-picker"><i class="fa fa-cloud-download"></i> My Slaves</label>
99
110
  <select id="node-input-modpackqt-slave-picker" style="width:65%"></select>
100
111
  <button type="button" id="modpackqt-slave-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
101
112
  </div>
102
113
  <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
103
- Pick a saved slave config from your modpackqt.com account — the probe deep-links the web register editor to that slave. Set up your Account Key on the runtime config (pencil icon).
114
+ Pick a saved slave config from your modpackqt.com account — auto-fills the port and unit below.
104
115
  </div>
105
116
 
106
117
  <!-- Hidden state — populated by the picker, used to build the probe URL. -->
@@ -127,9 +138,15 @@
127
138
  </script>
128
139
 
129
140
  <script type="text/html" data-help-name="modpackqt-slave-probe">
130
- <p>Live simulator probe for a single Modbus TCP slave. Pick a saved slave
131
- config from your modpackqt.com account, then click <b>Open in ModPackQT
132
- Console</b> to launch the web register editor.</p>
141
+ <p>Live simulator probe for a single Modbus TCP slave. Set your Account Key,
142
+ optionally pick a saved slave config from modpackqt.com, then click
143
+ <b>Open in ModPackQT Console</b> to launch the web register editor.</p>
144
+
145
+ <h3>Account Key</h3>
146
+ <p>Paste your ModPackQT tunnel key here — generate one at
147
+ <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
148
+ The key lets the web console authenticate to this probe and loads your saved slave configs
149
+ in the <b>My Slaves</b> picker. All Modbus traffic stays local.</p>
133
150
 
134
151
  <h3>How it works</h3>
135
152
  <p>Each slave-probe owns a 65 536-register store per type (coils, discrete,
@@ -107,5 +107,7 @@ module.exports = function (RED) {
107
107
  });
108
108
  }
109
109
 
110
- RED.nodes.registerType('modpackqt-slave-probe', SlaveProbeNode);
110
+ RED.nodes.registerType('modpackqt-slave-probe', SlaveProbeNode, {
111
+ credentials: { apiKey: { type: 'password' } }
112
+ });
111
113
  };
@@ -4,7 +4,8 @@
4
4
  color: '#15803d',
5
5
  defaults: {
6
6
  name: { value: '' },
7
- server: { value: '', type: 'modpackqt-config', required: true },
7
+ slaveServer: { value: '', type: 'modpackqt-slave-server' },
8
+ server: { value: '' }, // legacy — modpackqt-config with embedded slave
8
9
  registerType: { value: 'holding', required: true },
9
10
  address: { value: 0, required: true, validate: RED.validators.number() },
10
11
  quantity: { value: 1, required: true, validate: RED.validators.number() },
@@ -26,8 +27,8 @@
26
27
  <input type="text" id="node-input-name" placeholder="Name">
27
28
  </div>
28
29
  <div class="form-row">
29
- <label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
30
- <input type="text" id="node-input-server">
30
+ <label for="node-input-slaveServer"><i class="fa fa-server"></i> Slave Server</label>
31
+ <input type="text" id="node-input-slaveServer">
31
32
  </div>
32
33
  <div class="form-row">
33
34
  <label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
@@ -51,27 +52,29 @@
51
52
  <input type="number" id="node-input-pollInterval" min="0" placeholder="0 = trigger only">
52
53
  </div>
53
54
  <div class="form-tips">
54
- <b>Reads from the local slave register store.</b> Enable the embedded slave server
55
- in the runtime config first. Override at runtime via <code>msg.address</code>,
56
- <code>msg.quantity</code>, or <code>msg.registerType</code>.
55
+ <b>Reads from a slave server's register store.</b> Point at a
56
+ <b>modpackqt-slave-server</b> node. Override at runtime via
57
+ <code>msg.address</code>, <code>msg.quantity</code>, or <code>msg.registerType</code>.
57
58
  </div>
58
59
  <div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
59
- <a href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
60
- style="display:inline-block;padding:8px 20px;background:#2563eb;color:#fff;
61
- text-decoration:none;border-radius:6px;font-weight:600;font-size:13px;
62
- box-shadow:0 1px 2px rgba(0,0,0,0.08);">
63
- Premium Advanced Modbus Tester <i class="fa fa-external-link" style="margin-left:6px;"></i>
64
- </a>
65
- <div style="margin-top:6px;font-size:11px;color:#6b7280;">by ModPackQT &mdash; full-featured web tester, simulator &amp; AI helper</div>
66
- </div>
67
- </script>
60
+ <a href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
61
+ style="display:inline-block;padding:8px 20px;background:#2563eb;color:#fff;
62
+ text-decoration:none;border-radius:6px;font-weight:600;font-size:13px;
63
+ box-shadow:0 1px 2px rgba(0,0,0,0.08);">
64
+ Premium Advanced Modbus Tester <i class="fa fa-external-link" style="margin-left:6px;"></i>
65
+ </a>
66
+ <div style="margin-top:6px;font-size:11px;color:#6b7280;">by ModPackQT &mdash; full-featured web tester, simulator &amp; AI helper</div>
67
+ </div>
68
+ </script>
68
69
 
69
70
  <script type="text/html" data-help-name="modpackqt-slave-read">
70
71
  <p>
71
- Reads from the embedded slave's local register store. Useful for verifying what
72
- external Modbus masters connecting to your slave port will see.
72
+ Reads registers from a <b>modpackqt-slave-server</b> node's local store.
73
+ Useful for seeing exactly what an external Modbus master connecting to that
74
+ slave port will read.
73
75
  </p>
74
- <p>Enable the slave server in the <b>modpackqt-config</b> node first.</p>
76
+ <h3>Setup</h3>
77
+ <p>Set up a <b>Slave Server</b> config node first (pencil icon), then point this node at it.</p>
75
78
  <h3>Outputs</h3>
76
79
  <dl class="message-properties">
77
80
  <dt>payload <span class="property-type">array</span></dt>
@@ -1,29 +1,33 @@
1
1
  /**
2
- * ModPackQT Slave Read — read from the embedded slave's local register store.
3
- * The slave server itself runs in the modpackqt-config node (when enabled).
4
- * Outputs raw register values.
2
+ * ModPackQT Slave Read — read from a slave server's local register store.
3
+ *
4
+ * Points at a modpackqt-slave-server config node (preferred) or falls back
5
+ * to the legacy modpackqt-config with an embedded slave (backward compat).
5
6
  */
6
7
  module.exports = function (RED) {
7
8
  function ModPackQTSlaveReadNode(config) {
8
9
  RED.nodes.createNode(this, config);
9
10
  const node = this;
10
- const server = RED.nodes.getNode(config.server);
11
11
 
12
12
  node.registerType = config.registerType || 'holding';
13
13
  node.address = parseInt(config.address, 10) || 0;
14
14
  node.quantity = parseInt(config.quantity, 10) || 1;
15
15
  node.pollInterval = parseInt(config.pollInterval, 10) || 0;
16
16
 
17
+ // Prefer new slave-server config node; fall back to legacy modpackqt-config.
18
+ const slave = RED.nodes.getNode(config.slaveServer) || RED.nodes.getNode(config.server);
19
+
17
20
  let timer = null;
18
21
 
19
22
  function doRead(msg) {
20
- if (!server) {
21
- node.status({ fill: 'red', shape: 'ring', text: 'no config' });
23
+ if (!slave) {
24
+ node.status({ fill: 'red', shape: 'ring', text: 'no slave server configured' });
25
+ node.error('No Slave Server configured — add a modpackqt-slave-server node.', msg);
22
26
  return;
23
27
  }
24
- if (!server.slaveEnabled) {
25
- node.status({ fill: 'red', shape: 'ring', text: 'slave server disabled in config' });
26
- node.error('Embedded slave is disabledenable it in the modpackqt-config node.', msg);
28
+ if (!slave.slaveEnabled) {
29
+ node.status({ fill: 'red', shape: 'ring', text: 'slave server not running' });
30
+ node.error('Slave server is not running check the slave-server config node.', msg);
27
31
  return;
28
32
  }
29
33
  try {
@@ -31,12 +35,9 @@ module.exports = function (RED) {
31
35
  const address = (msg && msg.address !== undefined) ? parseInt(msg.address, 10) : node.address;
32
36
  const quantity = (msg && msg.quantity !== undefined) ? parseInt(msg.quantity, 10) : node.quantity;
33
37
 
34
- const values = server.slaveGet(registerType, address, quantity);
38
+ const values = slave.slaveGet(registerType, address, quantity);
35
39
  const now = new Date().toLocaleTimeString();
36
- node.status({
37
- fill: 'green', shape: 'dot',
38
- text: server.brandStatus(`slave ${registerType} @${address} [${values.length}] · ${now}`)
39
- });
40
+ node.status({ fill: 'green', shape: 'dot', text: `${registerType} @${address} [${values.length}] · ${now}` });
40
41
  node.send({
41
42
  payload: values,
42
43
  topic: `modpackqt/slave/${registerType}/${address}`,
@@ -0,0 +1,160 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('modpackqt-slave-server', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: '' },
6
+ bindHost: { value: '0.0.0.0' },
7
+ bindPort: { value: 1502, validate: RED.validators.number() },
8
+ unitId: { value: 1, validate: RED.validators.number() }
9
+ },
10
+ credentials: {
11
+ apiKey: { type: 'password' }
12
+ },
13
+ label: function () {
14
+ return this.name || `:${this.bindPort} #${this.unitId}`;
15
+ },
16
+ oneditprepare: function () {
17
+ const node = this;
18
+
19
+ // ── My Slaves picker — loads saved slave configs from modpackqt.com.
20
+ // Pre-fills port and unit ID. Register layout / limits are managed
21
+ // from the web console, not here.
22
+ const $sel = $('#node-config-modpackqt-slave-picker');
23
+ const reload = function () {
24
+ $sel.empty().append('<option value="">— Loading… —</option>');
25
+ if (!node.id) {
26
+ $sel.empty().append('<option value="">— Save this node first, then reopen —</option>');
27
+ return;
28
+ }
29
+ $.getJSON('modpackqt/slaves?probe=' + encodeURIComponent(node.id))
30
+ .done(function (rows) {
31
+ $sel.empty().append('<option value="">— Manual entry —</option>');
32
+ (rows || [])
33
+ .filter(function (s) { return s && (s.protocol === 'tcp' || !s.protocol); })
34
+ .forEach(function (s) {
35
+ const label = (s.name || '(unnamed)') + ' — :' + (s.port || 1502) + ' #' + (s.unitId || 1);
36
+ $('<option>').val(s.id).text(label).data('row', s).appendTo($sel);
37
+ });
38
+ })
39
+ .fail(function (xhr) {
40
+ const msg = (xhr.responseJSON && xhr.responseJSON.error) || ('HTTP ' + xhr.status);
41
+ $sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
42
+ });
43
+ };
44
+ $('#modpackqt-slave-server-picker-refresh').on('click', function (e) {
45
+ e.preventDefault(); reload();
46
+ });
47
+ $sel.on('change', function () {
48
+ const row = $(this).find('option:selected').data('row');
49
+ if (!row) return;
50
+ if (!node.name || node.name === '') $('#node-config-input-name').val(row.name || '');
51
+ $('#node-config-input-bindPort').val(row.port || 1502);
52
+ $('#node-config-input-unitId').val(row.unitId || 1);
53
+ buildLink();
54
+ });
55
+ reload();
56
+
57
+ // ── Probe deep-link
58
+ const buildLink = function () {
59
+ $.getJSON('modpackqt-probe/info')
60
+ .done(function (info) { renderLink(info); })
61
+ .fail(function () { renderLink({}); });
62
+ };
63
+ const renderLink = function (info) {
64
+ const probeHost = (info && info.host) || '127.0.0.1';
65
+ const probePort = (info && info.port) || 8502;
66
+ const bindPort = $('#node-config-input-bindPort').val() || node.bindPort || 1502;
67
+ const unitId = $('#node-config-input-unitId').val() || node.unitId || 1;
68
+ const name = $('#node-config-input-name').val() || node.name || '';
69
+ const url = 'https://modpackqt.com/slave'
70
+ + '?probe=' + encodeURIComponent(node.id || '')
71
+ + '&probeHost=' + encodeURIComponent(probeHost)
72
+ + '&probePort=' + probePort
73
+ + '&port=' + bindPort
74
+ + '&unitId=' + unitId
75
+ + (name ? '&name=' + encodeURIComponent(name) : '');
76
+ $('#modpackqt-slave-server-link').attr('href', url);
77
+ $('#modpackqt-slave-server-runtime').text(probeHost + ':' + probePort);
78
+ $('#modpackqt-slave-server-target').text(':' + bindPort + ' · unit ' + unitId);
79
+ };
80
+ buildLink();
81
+ $('#node-config-input-bindPort, #node-config-input-unitId').on('change input', buildLink);
82
+ }
83
+ });
84
+ </script>
85
+
86
+ <script type="text/html" data-template-name="modpackqt-slave-server">
87
+ <div class="form-row">
88
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
89
+ <input type="text" id="node-config-input-name" placeholder="e.g. Fake Inverter">
90
+ </div>
91
+
92
+ <div class="form-row">
93
+ <label for="node-config-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
94
+ <input type="password" id="node-config-input-apiKey" placeholder="Paste your ModPackQT tunnel key">
95
+ </div>
96
+ <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
97
+ Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
98
+ Loads your saved slave configs in the picker below.
99
+ </div>
100
+
101
+ <div class="form-row">
102
+ <label for="node-config-modpackqt-slave-picker"><i class="fa fa-cloud-download"></i> My Slaves</label>
103
+ <select id="node-config-modpackqt-slave-picker" style="width:65%"></select>
104
+ <button type="button" id="modpackqt-slave-server-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
105
+ </div>
106
+ <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
107
+ Pick a saved slave from modpackqt.com — auto-fills port and unit below.
108
+ Register layout and limits are managed from the web console.
109
+ </div>
110
+
111
+ <div class="form-row">
112
+ <label for="node-config-input-bindPort"><i class="fa fa-hashtag"></i> Port</label>
113
+ <input type="number" id="node-config-input-bindPort" min="1024" max="65535" placeholder="1502">
114
+ </div>
115
+ <div class="form-row">
116
+ <label for="node-config-input-unitId"><i class="fa fa-id-card"></i> Unit ID</label>
117
+ <input type="number" id="node-config-input-unitId" min="1" max="247" placeholder="1">
118
+ </div>
119
+
120
+ <input type="hidden" id="node-config-input-bindHost">
121
+
122
+ <div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
123
+ <a id="modpackqt-slave-server-link" href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
124
+ style="display:inline-block;padding:10px 24px;background:#7c3aed;color:#fff;
125
+ text-decoration:none;border-radius:6px;font-weight:600;font-size:14px;
126
+ box-shadow:0 1px 2px rgba(0,0,0,0.08);">
127
+ Open in ModPackQT Console <i class="fa fa-external-link" style="margin-left:6px;"></i>
128
+ </a>
129
+ <div style="margin-top:6px;font-size:11px;color:#6b7280;">
130
+ Target: <span id="modpackqt-slave-server-target">—</span> · runtime <span id="modpackqt-slave-server-runtime">starting…</span>
131
+ </div>
132
+ </div>
133
+ </script>
134
+
135
+ <script type="text/html" data-help-name="modpackqt-slave-server">
136
+ <p>
137
+ Runs a Modbus TCP slave server and owns the register store. Point
138
+ <b>slave-read</b> and <b>slave-write</b> nodes at it to read and write
139
+ registers from your flow. External Modbus masters can also connect directly
140
+ to the configured port and read whatever your flow has written.
141
+ </p>
142
+
143
+ <h3>How to set it up</h3>
144
+ <ol>
145
+ <li>Drop a <b>slave-read</b> or <b>slave-write</b> node and click its pencil icon next to "Slave Server" to create a new slave server.</li>
146
+ <li>Paste your Account Key and pick a saved slave from <b>My Slaves</b> — it pre-fills the port and unit ID.</li>
147
+ <li>The register layout and coil/register counts are managed from the modpackqt.com web console — click <b>Open in ModPackQT Console</b> to edit them.</li>
148
+ </ol>
149
+
150
+ <h3>Account Key</h3>
151
+ <p>Generate one at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
152
+ Used to load your saved slave configs and to let the web console connect to this server.</p>
153
+
154
+ <h3>Multiple slave servers</h3>
155
+ <p>Create one slave-server config per port/unit combination. Slave-read and
156
+ slave-write nodes each pick which server to talk to.</p>
157
+
158
+ <h3>Port note</h3>
159
+ <p>Use ports above 1024 (e.g. 1502, 1503) to avoid needing root privileges.</p>
160
+ </script>
@@ -0,0 +1,137 @@
1
+ /**
2
+ * ModPackQT Slave Server — Modbus TCP slave with a full register store.
3
+ *
4
+ * This is a config node. Drop one per fake device. Each server:
5
+ * - Binds its own TCP port and listens for external Modbus masters
6
+ * - Owns 65 536 registers per type (coils, discrete, holding, input)
7
+ * - Exposes slaveGet() / slaveSet() so slave-read / slave-write can
8
+ * push and pull values from the flow side
9
+ * - Registers with the probe runtime so the modpackqt.com web console
10
+ * can open a live register editor for it
11
+ *
12
+ * Register limits and layout are managed from the modpackqt.com web
13
+ * console (loaded via the My Slaves picker). Node-RED only needs the
14
+ * port and unit ID to bind the TCP listener.
15
+ */
16
+ module.exports = function (RED) {
17
+ const ModbusRTU = require('modbus-serial');
18
+ const { getRuntime } = require('./lib/probe-runtime');
19
+
20
+ function SlaveServerNode(config) {
21
+ RED.nodes.createNode(this, config);
22
+ const node = this;
23
+
24
+ node.name = config.name || '';
25
+ node.bindHost = config.bindHost || '0.0.0.0';
26
+ node.bindPort = parseInt(config.bindPort, 10) || 1502;
27
+ node.unitId = parseInt(config.unitId, 10) || 1;
28
+
29
+ // ── Register store — full Modbus address space per type.
30
+ // Register limits / layout are defined by the modpackqt.com slave
31
+ // config and are managed via the web console, not here.
32
+ const store = {
33
+ coils: new Array(65536).fill(false),
34
+ discrete: new Array(65536).fill(false),
35
+ holding: new Array(65536).fill(0),
36
+ input: new Array(65536).fill(0)
37
+ };
38
+
39
+ // ── Flow-side API — used by slave-read and slave-write nodes.
40
+ node.slaveEnabled = true;
41
+
42
+ node.slaveGet = function (type, address, quantity) {
43
+ const arr = store[type];
44
+ if (!arr) throw new Error(`Unknown register type: ${type}`);
45
+ return arr.slice(address, address + quantity);
46
+ };
47
+
48
+ node.slaveSet = function (type, address, values) {
49
+ const arr = store[type];
50
+ if (!arr) throw new Error(`Unknown register type: ${type}`);
51
+ const isBool = (type === 'coils' || type === 'discrete');
52
+ const stored = values.map((v) => isBool ? Boolean(v) : (parseInt(v, 10) & 0xFFFF));
53
+ stored.forEach((v, i) => { arr[address + i] = v; });
54
+ };
55
+
56
+ // ── Modbus TCP server
57
+ let server = null;
58
+ try {
59
+ const vector = {
60
+ getCoil: (addr, _u, cb) => cb(null, store.coils[addr]),
61
+ getDiscreteInput: (addr, _u, cb) => cb(null, store.discrete[addr]),
62
+ getHoldingRegister: (addr, _u, cb) => cb(null, store.holding[addr]),
63
+ getInputRegister: (addr, _u, cb) => cb(null, store.input[addr]),
64
+ setCoil: (addr, val, _u, cb) => { store.coils[addr] = !!val; cb(null); },
65
+ setRegister: (addr, val, _u, cb) => { store.holding[addr] = val & 0xFFFF; cb(null); }
66
+ };
67
+ server = new ModbusRTU.ServerTCP(vector, {
68
+ host: node.bindHost,
69
+ port: node.bindPort,
70
+ debug: false,
71
+ unitID: node.unitId
72
+ });
73
+ server.on('socketError', (err) => node.warn(`[slave-server] socket: ${err.message}`));
74
+ server.on('serverError', (err) => node.error(`[slave-server] server: ${err.message}`));
75
+ node.log(`[modpackqt] slave server listening on ${node.bindHost}:${node.bindPort} #${node.unitId}`);
76
+ } catch (err) {
77
+ node.error(`Failed to start slave server: ${err.message}`);
78
+ }
79
+
80
+ // ── Probe runtime registration — lets the web console find this slave.
81
+ const probe = {
82
+ id: node.id,
83
+ kind: 'slave',
84
+ describe(_detailed = false) {
85
+ return {
86
+ id: node.id,
87
+ kind: 'slave',
88
+ name: node.name || `Slave :${node.bindPort} #${node.unitId}`,
89
+ target: {
90
+ mode: 'tcp',
91
+ host: node.bindHost,
92
+ port: node.bindPort,
93
+ unitId: node.unitId
94
+ },
95
+ status: server ? 'listening' : 'error'
96
+ };
97
+ },
98
+ async handleRead() { throw new Error('slave server — use GET /store to inspect registers'); },
99
+ async handleWrite() { throw new Error('slave server — use PUT /store to set registers'); },
100
+ async handleStoreGet(searchParams) {
101
+ const type = searchParams.get('type') || 'holding';
102
+ const addr = parseInt(searchParams.get('address') || '0', 10);
103
+ const qty = parseInt(searchParams.get('quantity') || '10', 10);
104
+ if (!store[type]) throw new Error(`unknown register type: ${type}`);
105
+ return { type, address: addr, values: store[type].slice(addr, addr + qty) };
106
+ },
107
+ async handleStorePut(body) {
108
+ const type = body.type || 'holding';
109
+ const addr = parseInt(body.address, 10) || 0;
110
+ const values = body.values || [];
111
+ if (!store[type]) throw new Error(`unknown register type: ${type}`);
112
+ if (!Array.isArray(values)) throw new Error('values must be an array');
113
+ const isBool = (type === 'coils' || type === 'discrete');
114
+ const stored = values.map((v) => isBool ? Boolean(v) : (parseInt(v, 10) & 0xFFFF));
115
+ stored.forEach((v, i) => { store[type][addr + i] = v; });
116
+ return { ok: true, type, address: addr, written: stored.length };
117
+ }
118
+ };
119
+
120
+ const runtime = getRuntime();
121
+ runtime.ensureAdminRoutes(RED);
122
+ runtime.register(probe);
123
+
124
+ node.on('close', function (done) {
125
+ runtime.unregister(node.id);
126
+ if (server) {
127
+ try { server.close(() => done()); } catch (_) { done(); }
128
+ } else {
129
+ done();
130
+ }
131
+ });
132
+ }
133
+
134
+ RED.nodes.registerType('modpackqt-slave-server', SlaveServerNode, {
135
+ credentials: { apiKey: { type: 'password' } }
136
+ });
137
+ };
@@ -4,7 +4,8 @@
4
4
  color: '#15803d',
5
5
  defaults: {
6
6
  name: { value: '' },
7
- server: { value: '', type: 'modpackqt-config', required: true },
7
+ slaveServer: { value: '', type: 'modpackqt-slave-server' },
8
+ server: { value: '' }, // legacy — modpackqt-config with embedded slave
8
9
  registerType: { value: 'holding', required: true },
9
10
  address: { value: 0, required: true, validate: RED.validators.number() }
10
11
  },
@@ -24,8 +25,8 @@
24
25
  <input type="text" id="node-input-name" placeholder="Name">
25
26
  </div>
26
27
  <div class="form-row">
27
- <label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
28
- <input type="text" id="node-input-server">
28
+ <label for="node-input-slaveServer"><i class="fa fa-server"></i> Slave Server</label>
29
+ <input type="text" id="node-input-slaveServer">
29
30
  </div>
30
31
  <div class="form-row">
31
32
  <label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
@@ -41,26 +42,32 @@
41
42
  <input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
42
43
  </div>
43
44
  <div class="form-tips">
44
- <b>Pushes values into the embedded slave register store.</b> Any Modbus master that
45
- connects to your slave port will read whatever was last written here.
45
+ <b>Pushes values into a slave server's register store.</b> Any external Modbus master
46
+ connecting to that slave port will read whatever you last wrote.
46
47
  Send <code>msg.payload</code> as a number, array, or JSON string.
47
48
  </div>
48
49
  <div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
49
- <a href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
50
- style="display:inline-block;padding:8px 20px;background:#2563eb;color:#fff;
51
- text-decoration:none;border-radius:6px;font-weight:600;font-size:13px;
52
- box-shadow:0 1px 2px rgba(0,0,0,0.08);">
53
- Premium Advanced Modbus Tester <i class="fa fa-external-link" style="margin-left:6px;"></i>
54
- </a>
55
- <div style="margin-top:6px;font-size:11px;color:#6b7280;">by ModPackQT &mdash; full-featured web tester, simulator &amp; AI helper</div>
56
- </div>
57
- </script>
50
+ <a href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
51
+ style="display:inline-block;padding:8px 20px;background:#2563eb;color:#fff;
52
+ text-decoration:none;border-radius:6px;font-weight:600;font-size:13px;
53
+ box-shadow:0 1px 2px rgba(0,0,0,0.08);">
54
+ Premium Advanced Modbus Tester <i class="fa fa-external-link" style="margin-left:6px;"></i>
55
+ </a>
56
+ <div style="margin-top:6px;font-size:11px;color:#6b7280;">by ModPackQT &mdash; full-featured web tester, simulator &amp; AI helper</div>
57
+ </div>
58
+ </script>
58
59
 
59
60
  <script type="text/html" data-help-name="modpackqt-slave-write">
60
61
  <p>
61
- Writes into the embedded slave's local register store. External Modbus masters
62
- that connect to your configured slave port (default <code>1502</code>) will read
63
- these values.
62
+ Writes values into a <b>modpackqt-slave-server</b> node's register store.
63
+ External Modbus masters connecting to that slave's port will read whatever
64
+ was last written here.
64
65
  </p>
65
- <p>Enable the slave server in the <b>modpackqt-config</b> node first.</p>
66
+ <h3>Setup</h3>
67
+ <p>Set up a <b>Slave Server</b> config node first (pencil icon), then point this node at it.</p>
68
+ <h3>Input</h3>
69
+ <dl class="message-properties">
70
+ <dt>payload <span class="property-type">number | array | string</span></dt>
71
+ <dd>Value(s) to write. Arrays write multiple consecutive registers.</dd>
72
+ </dl>
66
73
  </script>
@@ -1,25 +1,31 @@
1
1
  /**
2
- * ModPackQT Slave Write — write into the embedded slave's local register store.
3
- * External Modbus masters connecting to the slave port (configured in
4
- * modpackqt-config) will read whatever was last written here.
2
+ * ModPackQT Slave Write — write into a slave server's local register store.
3
+ *
4
+ * Points at a modpackqt-slave-server config node (preferred) or falls back
5
+ * to the legacy modpackqt-config with an embedded slave (backward compat).
6
+ * External Modbus masters connecting to the slave port will read whatever
7
+ * was last written here.
5
8
  */
6
9
  module.exports = function (RED) {
7
10
  function ModPackQTSlaveWriteNode(config) {
8
11
  RED.nodes.createNode(this, config);
9
12
  const node = this;
10
- const server = RED.nodes.getNode(config.server);
11
13
 
12
14
  node.registerType = config.registerType || 'holding';
13
15
  node.address = parseInt(config.address, 10) || 0;
14
16
 
17
+ // Prefer new slave-server config node; fall back to legacy modpackqt-config.
18
+ const slave = RED.nodes.getNode(config.slaveServer) || RED.nodes.getNode(config.server);
19
+
15
20
  node.on('input', function (msg) {
16
- if (!server) {
17
- node.status({ fill: 'red', shape: 'ring', text: 'no config' });
21
+ if (!slave) {
22
+ node.status({ fill: 'red', shape: 'ring', text: 'no slave server configured' });
23
+ node.error('No Slave Server configured — add a modpackqt-slave-server node.', msg);
18
24
  return;
19
25
  }
20
- if (!server.slaveEnabled) {
21
- node.status({ fill: 'red', shape: 'ring', text: 'slave server disabled in config' });
22
- node.error('Embedded slave is disabledenable it in the modpackqt-config node.', msg);
26
+ if (!slave.slaveEnabled) {
27
+ node.status({ fill: 'red', shape: 'ring', text: 'slave server not running' });
28
+ node.error('Slave server is not running check the slave-server config node.', msg);
23
29
  return;
24
30
  }
25
31
  try {
@@ -37,12 +43,9 @@ module.exports = function (RED) {
37
43
  }
38
44
  const address = msg.address !== undefined ? parseInt(msg.address, 10) : node.address;
39
45
 
40
- server.slaveSet(registerType, address, values);
46
+ slave.slaveSet(registerType, address, values);
41
47
  const now = new Date().toLocaleTimeString();
42
- node.status({
43
- fill: 'green', shape: 'dot',
44
- text: server.brandStatus(`slave wrote ${values.length} @${address} · ${now}`)
45
- });
48
+ node.status({ fill: 'green', shape: 'dot', text: `wrote ${values.length} → ${registerType} @${address} · ${now}` });
46
49
  msg.success = true;
47
50
  msg.valuesWritten = values;
48
51
  node.send(msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-modbus-modpackqt",
3
- "version": "3.3.1",
3
+ "version": "3.3.3",
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",
@@ -51,7 +51,8 @@
51
51
  "modpackqt-slave-write": "nodes/modpackqt-slave-write.js",
52
52
  "modpackqt-traffic": "nodes/modpackqt-traffic.js",
53
53
  "modpackqt-master-probe": "nodes/modpackqt-master-probe.js",
54
- "modpackqt-slave-probe": "nodes/modpackqt-slave-probe.js"
54
+ "modpackqt-slave-probe": "nodes/modpackqt-slave-probe.js",
55
+ "modpackqt-slave-server": "nodes/modpackqt-slave-server.js"
55
56
  }
56
57
  },
57
58
  "dependencies": {