node-red-contrib-modbus-modpackqt 3.3.6 → 3.3.7
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 +11 -0
- package/README.md +1 -1
- package/nodes/lib/probe-runtime.js +1 -1
- 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,17 @@ 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.7] — 2026-05-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Slave Write** now has a **Quantity** field — fixes how many registers
|
|
12
|
+
the node writes. Payload arrays longer than this are truncated; shorter
|
|
13
|
+
ones are padded with the last value (or 0 if empty).
|
|
14
|
+
- **Slave Write** now has an **Update Interval (ms)** field — when set,
|
|
15
|
+
the node re-writes the most recently received `msg.payload` at this
|
|
16
|
+
interval. Useful for keeping a value fresh against external readers.
|
|
17
|
+
|
|
7
18
|
## [3.3.6] — 2026-05-10
|
|
8
19
|
|
|
9
20
|
### 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.7';
|
|
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,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.7",
|
|
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",
|