node-red-contrib-modbus-modpackqt 3.3.29 → 3.3.30

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.
@@ -3,10 +3,16 @@
3
3
  category: 'ModPackQT',
4
4
  color: '#f97316',
5
5
  defaults: {
6
- name: { value: '' },
7
- bindHost: { value: '0.0.0.0' },
8
- bindPort: { value: 1502, validate: RED.validators.number() },
9
- unitId: { value: 1, validate: RED.validators.number() }
6
+ name: { value: '' },
7
+ protocol: { value: 'tcp' },
8
+ bindHost: { value: '0.0.0.0' },
9
+ bindPort: { value: 1502, validate: RED.validators.number() },
10
+ unitId: { value: 1, validate: RED.validators.number() },
11
+ serialPort: { value: '' },
12
+ baudRate: { value: 9600, validate: RED.validators.number() },
13
+ parity: { value: 'none' },
14
+ dataBits: { value: 8, validate: RED.validators.number() },
15
+ stopBits: { value: 1, validate: RED.validators.number() }
10
16
  },
11
17
  credentials: {
12
18
  apiKey: { type: 'password' }
@@ -17,6 +23,11 @@
17
23
  align: 'right',
18
24
  icon: 'modpackqt.png',
19
25
  label: function () {
26
+ if (this.protocol === 'rtu') {
27
+ const tag = `${this.serialPort || 'COM?'} #${this.unitId || 1}`;
28
+ return this.name ? `${this.name} — Modbus RTU Slave ${tag}`
29
+ : `Modbus RTU Slave Server ${tag}`;
30
+ }
20
31
  const tag = `:${this.bindPort || 1502} #${this.unitId || 1}`;
21
32
  return this.name ? `${this.name} — Modbus TCP Slave ${tag}`
22
33
  : `Modbus TCP Slave Server ${tag}`;
@@ -26,7 +37,19 @@
26
37
  oneditprepare: function () {
27
38
  const node = this;
28
39
 
29
- // ── My Slaves picker (TCP only — this node runs a TCP server, not serial RTU)
40
+ // ── Protocol toggle
41
+ const $proto = $('#node-input-protocol');
42
+ const toggleProtocol = function () {
43
+ const isTcp = $proto.val() === 'tcp';
44
+ $('#modpackqt-slave-row-tcp').toggle(isTcp);
45
+ $('#modpackqt-slave-row-rtu').toggle(!isTcp);
46
+ reload();
47
+ buildLink();
48
+ };
49
+ $proto.on('change', toggleProtocol);
50
+ toggleProtocol();
51
+
52
+ // ── My Slaves picker
30
53
  const $sel = $('#node-input-modpackqt-slave-picker');
31
54
  const reload = function () {
32
55
  $sel.empty().append('<option value="">— Loading… —</option>');
@@ -34,16 +57,22 @@
34
57
  $sel.empty().append('<option value="">— Save this node first, then reopen —</option>');
35
58
  return;
36
59
  }
60
+ const proto = $proto.val();
37
61
  $.getJSON('modpackqt/slaves?probe=' + encodeURIComponent(node.id))
38
62
  .done(function (rows) {
39
63
  $sel.empty().append('<option value="">— Manual entry —</option>');
40
- const tcpSlaves = (rows || []).filter(function (s) { return !!s && s.protocol !== 'rtu'; });
41
- if (tcpSlaves.length === 0) {
42
- $sel.empty().append($('<option>').val('').text('— No TCP slaves saved yet —'));
64
+ const filtered = (rows || []).filter(function (s) {
65
+ return !!s && (proto === 'rtu' ? s.protocol === 'rtu' : s.protocol !== 'rtu');
66
+ });
67
+ if (filtered.length === 0) {
68
+ const msg = proto === 'rtu' ? '— No RTU slaves saved yet —' : '— No TCP slaves saved yet —';
69
+ $sel.empty().append($('<option>').val('').text(msg));
43
70
  } else {
44
- tcpSlaves.forEach(function (s) {
45
- const label = (s.name || '(unnamed)') + ' — :' + (s.port || 1502) + ' #' + (s.unitId || 1);
46
- $('<option>').val(s.id).text(label).data('row', s).appendTo($sel);
71
+ filtered.forEach(function (s) {
72
+ const conn = s.protocol === 'rtu'
73
+ ? ' — unit #' + (s.unitId || 1)
74
+ : ' — :' + (s.port || 1502) + ' #' + (s.unitId || 1);
75
+ $('<option>').val(s.id).text((s.name || '(unnamed)') + conn).data('row', s).appendTo($sel);
47
76
  });
48
77
  }
49
78
  })
@@ -57,7 +86,15 @@
57
86
  const row = $(this).find('option:selected').data('row');
58
87
  if (!row) return;
59
88
  if (!node.name) $('#node-input-name').val(row.name || '');
60
- if (row.port) $('#node-input-bindPort').val(row.port);
89
+ if (row.protocol === 'rtu') {
90
+ if (row.serialPort) $('#node-input-serialPort').val(row.serialPort);
91
+ if (row.baudRate) $('#node-input-baudRate').val(row.baudRate);
92
+ if (row.parity) $('#node-input-parity').val(row.parity);
93
+ if (row.dataBits) $('#node-input-dataBits').val(row.dataBits);
94
+ if (row.stopBits) $('#node-input-stopBits').val(row.stopBits);
95
+ } else {
96
+ if (row.port) $('#node-input-bindPort').val(row.port);
97
+ }
61
98
  $('#node-input-unitId').val(row.unitId || 1);
62
99
  buildLink();
63
100
  });
@@ -71,9 +108,10 @@
71
108
  const renderLink = function (info) {
72
109
  const probeHost = (info && info.host) || '127.0.0.1';
73
110
  const probePort = (info && info.port) || 8502;
111
+ const unitId = $('#node-input-unitId').val() || node.unitId || 1;
112
+ const name = $('#node-input-name').val() || node.name || '';
113
+ const isTcp = $proto.val() === 'tcp';
74
114
  const bindPort = $('#node-input-bindPort').val() || node.bindPort || 1502;
75
- const unitId = $('#node-input-unitId').val() || node.unitId || 1;
76
- const name = $('#node-input-name').val() || node.name || '';
77
115
  const url = 'https://modpackqt.com/slave'
78
116
  + '?probe=' + encodeURIComponent(node.id || '')
79
117
  + '&probeHost=' + encodeURIComponent(probeHost)
@@ -83,10 +121,15 @@
83
121
  + (name ? '&name=' + encodeURIComponent(name) : '');
84
122
  $('#modpackqt-slave-server-link').attr('href', url);
85
123
  $('#modpackqt-slave-server-runtime').text(probeHost + ':' + probePort);
86
- $('#modpackqt-slave-server-target').text(':' + bindPort + ' · unit ' + unitId);
124
+ if (isTcp) {
125
+ $('#modpackqt-slave-server-target').text(':' + bindPort + ' · unit ' + unitId);
126
+ } else {
127
+ const sp = $('#node-input-serialPort').val() || node.serialPort || 'COM?';
128
+ $('#modpackqt-slave-server-target').text(sp + ' · unit ' + unitId);
129
+ }
87
130
  };
88
131
  buildLink();
89
- $('#node-input-bindPort, #node-input-unitId').on('change input', buildLink);
132
+ $('#node-input-bindPort, #node-input-unitId, #node-input-serialPort').on('change input', buildLink);
90
133
  }
91
134
  });
92
135
  </script>
@@ -98,12 +141,78 @@
98
141
  </div>
99
142
 
100
143
  <div class="form-row">
101
- <label for="node-input-apiKey"><i class="fa fa-key"></i> Cloud Gateway Key</label>
144
+ <label for="node-input-protocol"><i class="fa fa-plug"></i> Protocol</label>
145
+ <select id="node-input-protocol" style="width:70%">
146
+ <option value="tcp">Modbus TCP (network)</option>
147
+ <option value="rtu">Modbus RTU (serial port)</option>
148
+ </select>
149
+ </div>
150
+
151
+ <!-- TCP fields -->
152
+ <div id="modpackqt-slave-row-tcp">
153
+ <input type="hidden" id="node-input-bindHost">
154
+ <div class="form-row">
155
+ <label for="node-input-bindPort"><i class="fa fa-ethernet"></i> Bind Port</label>
156
+ <input type="number" id="node-input-bindPort" min="1" max="65535" style="width:120px" placeholder="1502">
157
+ </div>
158
+ </div>
159
+
160
+ <!-- RTU fields -->
161
+ <div id="modpackqt-slave-row-rtu" style="display:none">
162
+ <div class="form-row">
163
+ <label for="node-input-serialPort"><i class="fa fa-usb"></i> Serial Port</label>
164
+ <input type="text" id="node-input-serialPort" placeholder="e.g. COM3 or /dev/ttyUSB0" style="width:60%">
165
+ </div>
166
+ <div class="form-row">
167
+ <label for="node-input-baudRate"><i class="fa fa-tachometer"></i> Baud Rate</label>
168
+ <select id="node-input-baudRate" style="width:140px">
169
+ <option value="1200">1200</option>
170
+ <option value="2400">2400</option>
171
+ <option value="4800">4800</option>
172
+ <option value="9600" selected>9600</option>
173
+ <option value="19200">19200</option>
174
+ <option value="38400">38400</option>
175
+ <option value="57600">57600</option>
176
+ <option value="115200">115200</option>
177
+ </select>
178
+ </div>
179
+ <div class="form-row">
180
+ <label for="node-input-parity"><i class="fa fa-check-circle-o"></i> Parity</label>
181
+ <select id="node-input-parity" style="width:140px">
182
+ <option value="none">None</option>
183
+ <option value="even">Even</option>
184
+ <option value="odd">Odd</option>
185
+ </select>
186
+ </div>
187
+ <div class="form-row">
188
+ <label for="node-input-dataBits"><i class="fa fa-sliders"></i> Data Bits</label>
189
+ <select id="node-input-dataBits" style="width:100px">
190
+ <option value="7">7</option>
191
+ <option value="8" selected>8</option>
192
+ </select>
193
+ &nbsp;&nbsp;
194
+ <label for="node-input-stopBits" style="width:auto;margin:0 8px 0 0">Stop Bits</label>
195
+ <select id="node-input-stopBits" style="width:100px">
196
+ <option value="1" selected>1</option>
197
+ <option value="2">2</option>
198
+ </select>
199
+ </div>
200
+ </div>
201
+
202
+ <div class="form-row">
203
+ <label for="node-input-unitId"><i class="fa fa-hashtag"></i> Unit ID</label>
204
+ <input type="number" id="node-input-unitId" min="1" max="247" style="width:80px" placeholder="1">
205
+ </div>
206
+
207
+ <hr style="margin:14px 0;border-color:#e5e7eb;">
208
+
209
+ <div class="form-row">
210
+ <label for="node-input-apiKey"><i class="fa fa-key"></i> Gateway Key</label>
102
211
  <input type="password" id="node-input-apiKey" placeholder="Paste your ModPackQT tunnel key">
103
212
  </div>
104
213
  <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
105
214
  Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
106
- Loads your saved slave configs in the picker below.
215
+ Loads matching saved slave configs in the picker below.
107
216
  </div>
108
217
 
109
218
  <div class="form-row">
@@ -112,14 +221,9 @@
112
221
  <button type="button" id="modpackqt-slave-server-picker-refresh" class="red-ui-button" title="Reload" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
113
222
  </div>
114
223
  <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
115
- Loads your saved <strong>TCP</strong> slaves. RTU slaves are not shown this node runs a Modbus TCP server only.
116
- Register layout and limits are managed from the web console.
224
+ Pick a saved slave to auto-fill settings. Register layout is managed from the web console.
117
225
  </div>
118
226
 
119
- <input type="hidden" id="node-input-bindHost">
120
- <input type="hidden" id="node-input-bindPort">
121
- <input type="hidden" id="node-input-unitId">
122
-
123
227
  <div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
124
228
  <a id="modpackqt-slave-server-link" href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
125
229
  style="display:inline-block;padding:10px 24px;background:#7c3aed;color:#fff;
@@ -133,7 +237,7 @@
133
237
  <div style="margin-top:10px;font-size:12px;">
134
238
  <a href="https://modpackqt.com/settings#slaves" target="_blank" rel="noopener noreferrer"
135
239
  style="color:#7c3aed;text-decoration:none;font-weight:500;">
136
- <i class="fa fa-pencil" style="margin-right:4px;"></i>Edit port / unit ID / register layout on modpackqt.com →
240
+ <i class="fa fa-pencil" style="margin-right:4px;"></i>Edit settings / register layout on modpackqt.com →
137
241
  </a>
138
242
  </div>
139
243
  </div>
@@ -141,20 +245,24 @@
141
245
 
142
246
  <script type="text/html" data-help-name="modpackqt-slave-server">
143
247
  <p>
144
- Runs a Modbus TCP slave server with its own full register store.
145
- Drop one on the canvas per fake device. <b>Slave-read</b> and
146
- <b>slave-write</b> nodes target it by node ID to read and write
147
- registers from the flow.
248
+ Runs a Modbus slave server — either TCP (network) or RTU (serial port) — with its own full register store.
249
+ Drop one on the canvas per fake device. <b>Slave-read</b> and <b>slave-write</b> nodes target it by node ID
250
+ to read and write registers from the flow.
148
251
  </p>
252
+ <h3>Protocol</h3>
253
+ <ul>
254
+ <li><b>Modbus TCP</b> — listens on a TCP port. Any Modbus master on the network can connect.</li>
255
+ <li><b>Modbus RTU</b> — listens on a serial port (COM3, /dev/ttyUSB0, etc.). The node acts as a serial slave device.</li>
256
+ </ul>
149
257
  <h3>Setup</h3>
150
258
  <ol>
151
- <li>Drop this node, set Port and Unit ID (or pick from <b>My Slaves</b>).</li>
259
+ <li>Choose protocol, set the port/serial settings and Unit ID (or pick from <b>My Slaves</b>).</li>
152
260
  <li>On your slave-read / slave-write nodes, select this node in the <b>Slave Server</b> dropdown.</li>
153
261
  <li>Click <b>Open in ModPackQT Console</b> to manage the register layout from the web.</li>
154
262
  </ol>
155
263
  <h3>Cloud Gateway Key</h3>
156
264
  <p>Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
157
265
  Lets the web console connect and loads your saved slave configs.</p>
158
- <h3>Port note</h3>
159
- <p>Use ports above 1024 (e.g. 1502, 1503) to avoid needing root privileges. Each server must use a unique port.</p>
266
+ <h3>TCP port note</h3>
267
+ <p>Use ports above 1024 (e.g. 1502, 1503) to avoid needing root privileges. Each TCP server must use a unique port.</p>
160
268
  </script>
@@ -1,8 +1,8 @@
1
1
  /**
2
- * ModPackQT Slave Server — Modbus TCP slave with a full register store.
2
+ * ModPackQT Slave Server — Modbus TCP or RTU slave with a full register store.
3
3
  *
4
4
  * Drop one on the canvas per fake device. Each server:
5
- * - Binds its own TCP port and listens for external Modbus masters
5
+ * - Binds either a TCP port (ServerTCP) or a serial port (ServerSerial)
6
6
  * - Owns 65 536 registers per type (coils, discrete, holding, input)
7
7
  * - Exposes slaveGet() / slaveSet() so slave-read / slave-write nodes
8
8
  * can push and pull values from the flow side
@@ -10,7 +10,7 @@
10
10
  * register editor for it
11
11
  *
12
12
  * Register limits and layout are managed from the modpackqt.com web
13
- * console — this node only needs port and unit ID to bind.
13
+ * console — this node only needs transport settings and unit ID to bind.
14
14
  */
15
15
  module.exports = function (RED) {
16
16
  const ModbusRTU = require('modbus-serial');
@@ -20,10 +20,16 @@ module.exports = function (RED) {
20
20
  RED.nodes.createNode(this, config);
21
21
  const node = this;
22
22
 
23
- node.name = config.name || '';
24
- node.bindHost = config.bindHost || '0.0.0.0';
25
- node.bindPort = parseInt(config.bindPort, 10) || 1502;
26
- node.unitId = parseInt(config.unitId, 10) || 1;
23
+ node.name = config.name || '';
24
+ node.protocol = config.protocol || 'tcp';
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
+ node.serialPort = config.serialPort || '';
29
+ node.baudRate = parseInt(config.baudRate, 10) || 9600;
30
+ node.parity = config.parity || 'none';
31
+ node.dataBits = parseInt(config.dataBits, 10) || 8;
32
+ node.stopBits = parseInt(config.stopBits, 10) || 1;
27
33
 
28
34
  // ── Register store
29
35
  const store = {
@@ -50,39 +56,58 @@ module.exports = function (RED) {
50
56
  stored.forEach((v, i) => { arr[address + i] = v; });
51
57
  };
52
58
 
53
- // ── Modbus TCP server
59
+ // ── Modbus server (TCP or RTU)
54
60
  let server = null;
61
+ const emit = (direction, type, address, values) => {
62
+ try {
63
+ node.send({
64
+ payload: {
65
+ ts: new Date().toISOString(),
66
+ direction, type, address,
67
+ values: Array.isArray(values) ? values : [values],
68
+ protocol: node.protocol,
69
+ unitId: node.unitId
70
+ }
71
+ });
72
+ } catch (_) { /* never let listener errors break the slave */ }
73
+ };
74
+
75
+ const vector = {
76
+ getCoil: (addr, _u, cb) => { const v = store.coils[addr]; emit('read', 'coils', addr, v); cb(null, v); },
77
+ getDiscreteInput: (addr, _u, cb) => { const v = store.discrete[addr]; emit('read', 'discrete', addr, v); cb(null, v); },
78
+ getHoldingRegister: (addr, _u, cb) => { const v = store.holding[addr]; emit('read', 'holding', addr, v); cb(null, v); },
79
+ getInputRegister: (addr, _u, cb) => { const v = store.input[addr]; emit('read', 'input', addr, v); cb(null, v); },
80
+ setCoil: (addr, val, _u, cb) => { store.coils[addr] = !!val; emit('write', 'coils', addr, !!val); cb(null); },
81
+ setRegister: (addr, val, _u, cb) => { const v = val & 0xFFFF; store.holding[addr] = v; emit('write', 'holding', addr, v); cb(null); }
82
+ };
83
+
55
84
  try {
56
- const emit = (direction, type, address, values) => {
57
- try {
58
- node.send({
59
- payload: {
60
- ts: new Date().toISOString(),
61
- direction, type, address,
62
- values: Array.isArray(values) ? values : [values],
63
- port: node.bindPort, unitId: node.unitId
64
- }
65
- });
66
- } catch (_) { /* never let listener errors break the slave */ }
67
- };
68
- const vector = {
69
- getCoil: (addr, _u, cb) => { const v = store.coils[addr]; emit('read', 'coils', addr, v); cb(null, v); },
70
- getDiscreteInput: (addr, _u, cb) => { const v = store.discrete[addr]; emit('read', 'discrete', addr, v); cb(null, v); },
71
- getHoldingRegister: (addr, _u, cb) => { const v = store.holding[addr]; emit('read', 'holding', addr, v); cb(null, v); },
72
- getInputRegister: (addr, _u, cb) => { const v = store.input[addr]; emit('read', 'input', addr, v); cb(null, v); },
73
- setCoil: (addr, val, _u, cb) => { store.coils[addr] = !!val; emit('write', 'coils', addr, !!val); cb(null); },
74
- setRegister: (addr, val, _u, cb) => { const v = val & 0xFFFF; store.holding[addr] = v; emit('write', 'holding', addr, v); cb(null); }
75
- };
76
- server = new ModbusRTU.ServerTCP(vector, {
77
- host: node.bindHost,
78
- port: node.bindPort,
79
- debug: false,
80
- unitID: node.unitId
81
- });
82
- server.on('socketError', (err) => node.warn(`[slave-server] socket: ${err.message}`));
83
- server.on('serverError', (err) => node.error(`[slave-server] server: ${err.message}`));
84
- node.log(`[modpackqt] slave server listening on ${node.bindHost}:${node.bindPort} #${node.unitId}`);
85
- node.status({ fill: 'green', shape: 'dot', text: `listening :${node.bindPort} #${node.unitId}` });
85
+ if (node.protocol === 'rtu') {
86
+ if (!node.serialPort) throw new Error('Serial port path is required for RTU mode');
87
+ server = new ModbusRTU.ServerSerial(vector, {
88
+ port: node.serialPort,
89
+ baudRate: node.baudRate,
90
+ parity: node.parity,
91
+ dataBits: node.dataBits,
92
+ stopBits: node.stopBits,
93
+ unitID: node.unitId
94
+ });
95
+ server.on('socketError', (err) => node.warn(`[slave-server] serial: ${err.message}`));
96
+ server.on('serverError', (err) => node.error(`[slave-server] serial: ${err.message}`));
97
+ node.log(`[modpackqt] RTU slave listening on ${node.serialPort} #${node.unitId}`);
98
+ node.status({ fill: 'green', shape: 'dot', text: `RTU ${node.serialPort} #${node.unitId}` });
99
+ } else {
100
+ server = new ModbusRTU.ServerTCP(vector, {
101
+ host: node.bindHost,
102
+ port: node.bindPort,
103
+ debug: false,
104
+ unitID: node.unitId
105
+ });
106
+ server.on('socketError', (err) => node.warn(`[slave-server] socket: ${err.message}`));
107
+ server.on('serverError', (err) => node.error(`[slave-server] server: ${err.message}`));
108
+ node.log(`[modpackqt] TCP slave listening on ${node.bindHost}:${node.bindPort} #${node.unitId}`);
109
+ node.status({ fill: 'green', shape: 'dot', text: `TCP :${node.bindPort} #${node.unitId}` });
110
+ }
86
111
  } catch (err) {
87
112
  node.error(`Failed to start slave server: ${err.message}`);
88
113
  node.status({ fill: 'red', shape: 'ring', text: 'failed to start' });
@@ -93,16 +118,14 @@ module.exports = function (RED) {
93
118
  id: node.id,
94
119
  kind: 'slave',
95
120
  describe(_detailed = false) {
121
+ const target = node.protocol === 'rtu'
122
+ ? { mode: 'rtu', serialPort: node.serialPort, baudRate: node.baudRate, parity: node.parity, dataBits: node.dataBits, stopBits: node.stopBits, unitId: node.unitId }
123
+ : { mode: 'tcp', host: node.bindHost, port: node.bindPort, unitId: node.unitId };
96
124
  return {
97
125
  id: node.id,
98
126
  kind: 'slave',
99
- name: node.name || `Slave :${node.bindPort} #${node.unitId}`,
100
- target: {
101
- mode: 'tcp',
102
- host: node.bindHost,
103
- port: node.bindPort,
104
- unitId: node.unitId
105
- },
127
+ name: node.name || (node.protocol === 'rtu' ? `RTU Slave ${node.serialPort} #${node.unitId}` : `Slave :${node.bindPort} #${node.unitId}`),
128
+ target,
106
129
  status: server ? 'listening' : 'error'
107
130
  };
108
131
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-modbus-modpackqt",
3
- "version": "3.3.29",
3
+ "version": "3.3.30",
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",