node-red-contrib-modbus-modpackqt 3.3.6 → 3.3.8

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,29 @@ 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.8] — 2026-05-10
8
+
9
+ ### Changed
10
+
11
+ - **Plain-language labels in master nodes.** The dropdown that selects a
12
+ config is now labelled **Target Device** (was "Runtime") in master-read,
13
+ master-write, and master-probe, with clearer helper text.
14
+ - **Config dialog: "Device" section renamed to "Target Device".**
15
+ - **Config dialog: Host field renamed to "IP Address"** with an example
16
+ placeholder (`e.g. 192.168.1.10`) and an inline hint explaining what to
17
+ enter. Port and Unit ID placeholders also clarified.
18
+
19
+ ## [3.3.7] — 2026-05-10
20
+
21
+ ### Added
22
+
23
+ - **Slave Write** now has a **Quantity** field — fixes how many registers
24
+ the node writes. Payload arrays longer than this are truncated; shorter
25
+ ones are padded with the last value (or 0 if empty).
26
+ - **Slave Write** now has an **Update Interval (ms)** field — when set,
27
+ the node re-writes the most recently received `msg.payload` at this
28
+ interval. Useful for keeping a value fresh against external readers.
29
+
7
30
  ## [3.3.6] — 2026-05-10
8
31
 
9
32
  ### Added
package/README.md CHANGED
@@ -16,7 +16,7 @@ By [ModPackQT](https://modpackqt.com).
16
16
  - **Modbus master** — read (FC1–FC4) and write (FC5/FC6/FC15/FC16) over **TCP** or **RTU (serial)**
17
17
  - **Embedded Modbus TCP slave server** — push values from any flow, let PLCs / SCADA / HMIs read them
18
18
  - **Passive traffic monitor** — see every Modbus op (timing, values, errors) in real time
19
- - **Optional cloud profile pickers** *(v3.2.0)* — paste your ModPackQT Account Key into the runtime config and master / probe nodes show a dropdown of your saved Modbus profiles. No more retyping IPs across nodes.
19
+ - **Cloud profile pickers** — paste your ModPackQT Account Key into the runtime config and a **My Connections** dropdown loads your saved devices, auto-filling host / port / unit. The slave server node has a matching **My Slaves** picker. No more retyping IPs across nodes.
20
20
  - **Outputs raw register values** — pair with [`node-red-contrib-bytes-modpackqt`](https://www.npmjs.com/package/node-red-contrib-bytes-modpackqt) to decode int / float / string / bitmask
21
21
  - **Zero external dependencies** — Modbus runs inside the Node-RED process
22
22
 
@@ -18,7 +18,7 @@
18
18
  const http = require('http');
19
19
  const { URL } = require('url');
20
20
 
21
- const PALETTE_VERSION = '3.3.6';
21
+ const PALETTE_VERSION = '3.3.8';
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;
@@ -127,18 +127,21 @@
127
127
  Picks a saved connection from modpackqt.com — auto-fills Host, Port, and Unit ID below.
128
128
  </div>
129
129
 
130
- <h4 style="margin-top:18px">Device</h4>
130
+ <h4 style="margin-top:18px">Target Device</h4>
131
131
  <div class="form-row modpackqt-tcp-only">
132
- <label for="node-config-input-targetHost"><i class="fa fa-plug"></i> Host</label>
133
- <input type="text" id="node-config-input-targetHost" placeholder="Modbus device IP/hostname">
132
+ <label for="node-config-input-targetHost"><i class="fa fa-plug"></i> IP Address</label>
133
+ <input type="text" id="node-config-input-targetHost" placeholder="e.g. 192.168.1.10 IP/hostname of the Modbus device">
134
+ </div>
135
+ <div class="form-tips modpackqt-tcp-only" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
136
+ The IP address (or hostname) of the target Modbus device on your network.
134
137
  </div>
135
138
  <div class="form-row modpackqt-tcp-only">
136
139
  <label for="node-config-input-targetPort"><i class="fa fa-hashtag"></i> Port</label>
137
- <input type="number" id="node-config-input-targetPort" min="1" max="65535" placeholder="502">
140
+ <input type="number" id="node-config-input-targetPort" min="1" max="65535" placeholder="502 (Modbus TCP default)">
138
141
  </div>
139
142
  <div class="form-row">
140
143
  <label for="node-config-input-unitId"><i class="fa fa-id-card"></i> Unit ID</label>
141
- <input type="number" id="node-config-input-unitId" min="1" max="247" placeholder="1">
144
+ <input type="number" id="node-config-input-unitId" min="1" max="247" placeholder="1 (slave address on the bus)">
142
145
  </div>
143
146
 
144
147
  <h4 style="margin-top:18px">Transport</h4>
@@ -56,11 +56,11 @@
56
56
  <input type="text" id="node-input-name" placeholder="(optional, e.g. Inverter A)">
57
57
  </div>
58
58
  <div class="form-row">
59
- <label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
59
+ <label for="node-input-server"><i class="fa fa-plug"></i> Target Device</label>
60
60
  <input type="text" id="node-input-server">
61
61
  </div>
62
62
  <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
63
- The runtime config carries the device target — the probe deep-links the web tester to it.
63
+ Pick the Modbus device this probe should attach to — the web tester deep-links to it.
64
64
  </div>
65
65
 
66
66
  <div class="form-row">
@@ -31,11 +31,11 @@
31
31
  <input type="text" id="node-input-name" placeholder="Name">
32
32
  </div>
33
33
  <div class="form-row">
34
- <label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
34
+ <label for="node-input-server"><i class="fa fa-plug"></i> Target Device</label>
35
35
  <input type="text" id="node-input-server">
36
36
  </div>
37
37
  <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
38
- The runtime config carries Host / Port / Unit. To talk to a different device, create another runtime config (pencil → +).
38
+ Pick the Modbus device this node should read from. Each target device carries its own Host / Port / Unit ID — to talk to a different device, click the pencil → + to add one.
39
39
  </div>
40
40
 
41
41
  <div class="form-row">
@@ -27,11 +27,11 @@
27
27
  <input type="text" id="node-input-name" placeholder="Name">
28
28
  </div>
29
29
  <div class="form-row">
30
- <label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
30
+ <label for="node-input-server"><i class="fa fa-plug"></i> Target Device</label>
31
31
  <input type="text" id="node-input-server">
32
32
  </div>
33
33
  <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
34
- The runtime config carries Host / Port / Unit. To talk to a different device, create another runtime config (pencil → +).
34
+ Pick the Modbus device this node should write to. Each target device carries its own Host / Port / Unit ID — to talk to a different device, click the pencil → + to add one.
35
35
  </div>
36
36
 
37
37
  <div class="form-row">
@@ -3,10 +3,12 @@
3
3
  category: 'ModPackQT',
4
4
  color: '#15803d',
5
5
  defaults: {
6
- name: { value: '' },
7
- slaveServerId: { value: '' }, // ID of a modpackqt-slave-server node on the canvas
8
- registerType: { value: 'holding', required: true },
9
- address: { value: 0, required: true, validate: RED.validators.number() }
6
+ name: { value: '' },
7
+ slaveServerId: { value: '' }, // ID of a modpackqt-slave-server node on the canvas
8
+ registerType: { value: 'holding', required: true },
9
+ address: { value: 0, required: true, validate: RED.validators.number() },
10
+ quantity: { value: 1, required: true, validate: RED.validators.number() },
11
+ updateInterval: { value: 0, validate: RED.validators.number() }
10
12
  },
11
13
  inputs: 1,
12
14
  outputs: 1,
@@ -53,6 +55,22 @@
53
55
  <label for="node-input-address"><i class="fa fa-map-marker"></i> Start Address</label>
54
56
  <input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
55
57
  </div>
58
+ <div class="form-row">
59
+ <label for="node-input-quantity"><i class="fa fa-sort-numeric-asc"></i> Quantity</label>
60
+ <input type="number" id="node-input-quantity" min="1" max="125" placeholder="1">
61
+ </div>
62
+ <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
63
+ Number of registers to write. Payload arrays longer than this are truncated;
64
+ shorter ones are padded with the last value (or 0 if empty).
65
+ </div>
66
+ <div class="form-row">
67
+ <label for="node-input-updateInterval"><i class="fa fa-clock-o"></i> Update Interval (ms)</label>
68
+ <input type="number" id="node-input-updateInterval" min="0" placeholder="0 = trigger only">
69
+ </div>
70
+ <div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
71
+ If &gt; 0, re-writes the most recently received <code>msg.payload</code> at this
72
+ interval — useful for keeping a value fresh against external readers.
73
+ </div>
56
74
  <div class="form-tips">
57
75
  Pushes values into the slave server's register store. External Modbus masters
58
76
  connecting to that port will read whatever you last wrote.
@@ -7,37 +7,67 @@ module.exports = function (RED) {
7
7
  RED.nodes.createNode(this, config);
8
8
  const node = this;
9
9
 
10
- node.registerType = config.registerType || 'holding';
11
- node.address = parseInt(config.address, 10) || 0;
10
+ node.registerType = config.registerType || 'holding';
11
+ node.address = parseInt(config.address, 10) || 0;
12
+ node.quantity = parseInt(config.quantity, 10) || 1;
13
+ node.updateInterval = parseInt(config.updateInterval, 10) || 0;
12
14
 
13
15
  function getSlave() {
14
16
  return RED.nodes.getNode(config.slaveServerId);
15
17
  }
16
18
 
17
- node.on('input', function (msg) {
19
+ let lastValues = null; // last array we wrote, used by the update timer
20
+ let lastAddress = node.address;
21
+ let lastRegisterType = node.registerType;
22
+ let timer = null;
23
+
24
+ function shapePayload(raw, registerType) {
25
+ const isBool = (registerType === 'coils' || registerType === 'discrete');
26
+ let arr;
27
+ if (Array.isArray(raw)) {
28
+ arr = raw.map((v) => isBool ? Boolean(v) : parseInt(v, 10));
29
+ } else {
30
+ arr = [isBool ? Boolean(raw) : parseInt(raw, 10)];
31
+ }
32
+ if (!isBool && arr.some((v) => isNaN(v))) {
33
+ throw new Error(`Invalid payload — expected number or array, got: ${JSON.stringify(raw)}`);
34
+ }
35
+ // Pad / truncate to node.quantity
36
+ if (arr.length > node.quantity) {
37
+ arr = arr.slice(0, node.quantity);
38
+ } else if (arr.length < node.quantity) {
39
+ const filler = arr.length ? arr[arr.length - 1] : (isBool ? false : 0);
40
+ while (arr.length < node.quantity) arr.push(filler);
41
+ }
42
+ return arr;
43
+ }
44
+
45
+ function writeOnce(values, registerType, address, msg) {
18
46
  const slave = getSlave();
19
47
  if (!slave || !slave.slaveEnabled) {
20
48
  node.status({ fill: 'red', shape: 'ring', text: 'no slave server' });
21
- node.error('No slave server selected or it is not running.', msg);
22
- return;
49
+ node.error('No slave server selected or it is not running.', msg || {});
50
+ return false;
23
51
  }
52
+ slave.slaveSet(registerType, address, values);
53
+ const now = new Date().toLocaleTimeString();
54
+ node.status({ fill: 'green', shape: 'dot', text: `wrote ${values.length} → ${registerType} @${address} · ${now}` });
55
+ return true;
56
+ }
57
+
58
+ node.on('input', function (msg) {
24
59
  try {
25
60
  let raw = msg.payload;
26
61
  if (typeof raw === 'string') {
27
62
  try { raw = JSON.parse(raw); } catch (_) { raw = Number(raw); }
28
63
  }
29
64
  const registerType = msg.registerType || node.registerType;
30
- const isBool = (registerType === 'coils' || registerType === 'discrete');
31
- const values = Array.isArray(raw)
32
- ? raw.map((v) => isBool ? Boolean(v) : parseInt(v, 10))
33
- : [isBool ? Boolean(raw) : parseInt(raw, 10)];
34
- if (!isBool && values.some((v) => isNaN(v))) {
35
- throw new Error(`Invalid payload — expected number or array, got: ${JSON.stringify(msg.payload)}`);
36
- }
37
65
  const address = msg.address !== undefined ? parseInt(msg.address, 10) : node.address;
38
- slave.slaveSet(registerType, address, values);
39
- const now = new Date().toLocaleTimeString();
40
- node.status({ fill: 'green', shape: 'dot', text: `wrote ${values.length} → ${registerType} @${address} · ${now}` });
66
+ const values = shapePayload(raw, registerType);
67
+ if (!writeOnce(values, registerType, address, msg)) return;
68
+ lastValues = values;
69
+ lastAddress = address;
70
+ lastRegisterType = registerType;
41
71
  msg.success = true;
42
72
  msg.valuesWritten = values;
43
73
  node.send(msg);
@@ -46,6 +76,18 @@ module.exports = function (RED) {
46
76
  node.error(err.message, msg);
47
77
  }
48
78
  });
79
+
80
+ if (node.updateInterval > 0) {
81
+ timer = setInterval(() => {
82
+ if (!lastValues) return; // nothing received yet
83
+ try { writeOnce(lastValues, lastRegisterType, lastAddress, null); }
84
+ catch (err) { node.status({ fill: 'red', shape: 'dot', text: err.message.slice(0, 60) }); }
85
+ }, node.updateInterval);
86
+ }
87
+
88
+ node.on('close', function () {
89
+ if (timer) clearInterval(timer);
90
+ });
49
91
  }
50
92
  RED.nodes.registerType('modpackqt-slave-write', ModPackQTSlaveWriteNode);
51
93
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-modbus-modpackqt",
3
- "version": "3.3.6",
3
+ "version": "3.3.8",
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",