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 +23 -0
- package/README.md +1 -1
- package/nodes/lib/probe-runtime.js +1 -1
- package/nodes/modpackqt-config.html +8 -5
- package/nodes/modpackqt-master-probe.html +2 -2
- package/nodes/modpackqt-master-read.html +2 -2
- package/nodes/modpackqt-master-write.html +2 -2
- package/nodes/modpackqt-slave-write.html +22 -4
- package/nodes/modpackqt-slave-write.js +57 -15
- package/package.json +1 -1
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
|
-
- **
|
|
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.
|
|
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>
|
|
133
|
-
<input type="text" id="node-config-input-targetHost" placeholder="
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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:
|
|
7
|
-
slaveServerId:
|
|
8
|
-
registerType:
|
|
9
|
-
address:
|
|
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 > 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
|
|
11
|
-
node.address
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
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",
|