node-red-contrib-modbus-modpackqt 3.3.8 → 3.3.10
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 +20 -0
- package/README.md +0 -1
- package/nodes/lib/probe-runtime.js +1 -1
- package/nodes/modpackqt-master-write.html +11 -1
- package/nodes/modpackqt-master-write.js +11 -1
- package/package.json +1 -2
- package/nodes/modpackqt-traffic.html +0 -127
- package/nodes/modpackqt-traffic.js +0 -68
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ 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.10] — 2026-05-10
|
|
8
|
+
|
|
9
|
+
### Removed
|
|
10
|
+
|
|
11
|
+
- **`modpackqt-traffic` node removed.** The matching modpackqt.com web
|
|
12
|
+
console already surfaces a live, decoded view of every Modbus op for
|
|
13
|
+
any device a master or slave node is attached to — the in-flow
|
|
14
|
+
passive sniffer was redundant. Existing flows referencing
|
|
15
|
+
`modpackqt-traffic` will show an "unknown node type" warning and can
|
|
16
|
+
be deleted.
|
|
17
|
+
|
|
18
|
+
## [3.3.9] — 2026-05-10
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Master Write** now has a **Quantity** field — fixes how many registers
|
|
23
|
+
the write covers. Payload arrays longer than this are truncated; shorter
|
|
24
|
+
ones are padded with the last value (or 0 if empty). For FC5 / FC6
|
|
25
|
+
(single-register writes) Quantity is forced to 1.
|
|
26
|
+
|
|
7
27
|
## [3.3.8] — 2026-05-10
|
|
8
28
|
|
|
9
29
|
### Changed
|
package/README.md
CHANGED
|
@@ -15,7 +15,6 @@ By [ModPackQT](https://modpackqt.com).
|
|
|
15
15
|
|
|
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
|
-
- **Passive traffic monitor** — see every Modbus op (timing, values, errors) in real time
|
|
19
18
|
- **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
19
|
- **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
20
|
- **Zero external dependencies** — Modbus runs inside the Node-RED process
|
|
@@ -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.10';
|
|
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;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
targetPort: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
|
|
10
10
|
unitId: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
|
|
11
11
|
functionCode: { value: '6', required: true },
|
|
12
|
-
address: { value: 0, required: true, validate: RED.validators.number() }
|
|
12
|
+
address: { value: 0, required: true, validate: RED.validators.number() },
|
|
13
|
+
quantity: { value: 1, required: true, validate: RED.validators.number() }
|
|
13
14
|
},
|
|
14
15
|
inputs: 1,
|
|
15
16
|
outputs: 1,
|
|
@@ -47,6 +48,15 @@
|
|
|
47
48
|
<label for="node-input-address"><i class="fa fa-map-marker"></i> Start Address</label>
|
|
48
49
|
<input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
|
|
49
50
|
</div>
|
|
51
|
+
<div class="form-row">
|
|
52
|
+
<label for="node-input-quantity"><i class="fa fa-sort-numeric-asc"></i> Quantity</label>
|
|
53
|
+
<input type="number" id="node-input-quantity" min="1" max="125" placeholder="1">
|
|
54
|
+
</div>
|
|
55
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
56
|
+
Number of registers to write. Payload arrays longer than this are truncated;
|
|
57
|
+
shorter ones are padded with the last value (or 0 if empty). For FC5 / FC6
|
|
58
|
+
(single-register writes) Quantity is forced to 1.
|
|
59
|
+
</div>
|
|
50
60
|
|
|
51
61
|
<input type="hidden" id="node-input-targetHost">
|
|
52
62
|
<input type="hidden" id="node-input-targetPort">
|
|
@@ -14,6 +14,7 @@ module.exports = function (RED) {
|
|
|
14
14
|
node.unitId = parseInt(config.unitId, 10) || 0;
|
|
15
15
|
node.functionCode = parseInt(config.functionCode, 10) || 6;
|
|
16
16
|
node.address = parseInt(config.address, 10) || 0;
|
|
17
|
+
node.quantity = parseInt(config.quantity, 10) || 1;
|
|
17
18
|
|
|
18
19
|
node.on('input', async function (msg) {
|
|
19
20
|
if (!server) {
|
|
@@ -26,7 +27,16 @@ module.exports = function (RED) {
|
|
|
26
27
|
try { raw = JSON.parse(raw); } catch (_) { raw = Number(raw); }
|
|
27
28
|
}
|
|
28
29
|
const isCoilWrite = (node.functionCode === 5 || node.functionCode === 15);
|
|
29
|
-
const
|
|
30
|
+
const isSingle = (node.functionCode === 5 || node.functionCode === 6);
|
|
31
|
+
const targetQty = isSingle ? 1 : Math.max(1, node.quantity);
|
|
32
|
+
let arr = Array.isArray(raw) ? raw.slice() : [raw];
|
|
33
|
+
// Pad / truncate to targetQty
|
|
34
|
+
if (arr.length > targetQty) {
|
|
35
|
+
arr = arr.slice(0, targetQty);
|
|
36
|
+
} else if (arr.length < targetQty) {
|
|
37
|
+
const filler = arr.length ? arr[arr.length - 1] : (isCoilWrite ? 0 : 0);
|
|
38
|
+
while (arr.length < targetQty) arr.push(filler);
|
|
39
|
+
}
|
|
30
40
|
const values = arr.map((v) => {
|
|
31
41
|
if (isCoilWrite) return v ? 1 : 0;
|
|
32
42
|
const n = Number(v);
|
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.10",
|
|
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",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"modpackqt-master-write": "nodes/modpackqt-master-write.js",
|
|
50
50
|
"modpackqt-slave-read": "nodes/modpackqt-slave-read.js",
|
|
51
51
|
"modpackqt-slave-write": "nodes/modpackqt-slave-write.js",
|
|
52
|
-
"modpackqt-traffic": "nodes/modpackqt-traffic.js",
|
|
53
52
|
"modpackqt-master-probe": "nodes/modpackqt-master-probe.js",
|
|
54
53
|
"modpackqt-slave-server": "nodes/modpackqt-slave-server.js"
|
|
55
54
|
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
<script type="text/javascript">
|
|
2
|
-
RED.nodes.registerType('modpackqt-traffic', {
|
|
3
|
-
category: 'ModPackQT',
|
|
4
|
-
color: '#22c55e',
|
|
5
|
-
defaults: {
|
|
6
|
-
name: { value: '' },
|
|
7
|
-
server: { value: '', type: 'modpackqt-config', required: true },
|
|
8
|
-
filterDirection: { value: 'any' },
|
|
9
|
-
filterKind: { value: 'any' },
|
|
10
|
-
filterFc: { value: 'any' },
|
|
11
|
-
filterTarget: { value: '' },
|
|
12
|
-
format: { value: 'object' },
|
|
13
|
-
alsoLog: { value: false }
|
|
14
|
-
},
|
|
15
|
-
inputs: 0,
|
|
16
|
-
outputs: 1,
|
|
17
|
-
icon: 'font-awesome/fa-eye',
|
|
18
|
-
label: function () {
|
|
19
|
-
const parts = [];
|
|
20
|
-
if (this.filterKind !== 'any') parts.push(this.filterKind);
|
|
21
|
-
if (this.filterDirection !== 'any') parts.push(this.filterDirection);
|
|
22
|
-
if (this.filterFc !== 'any') parts.push('FC' + this.filterFc);
|
|
23
|
-
if (this.filterTarget) parts.push(this.filterTarget);
|
|
24
|
-
return this.name || ('traffic' + (parts.length ? ' · ' + parts.join(' ') : ''));
|
|
25
|
-
},
|
|
26
|
-
paletteLabel: 'modbus traffic'
|
|
27
|
-
});
|
|
28
|
-
</script>
|
|
29
|
-
|
|
30
|
-
<script type="text/html" data-template-name="modpackqt-traffic">
|
|
31
|
-
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
32
|
-
<input type="text" id="node-input-name" placeholder="Name"></div>
|
|
33
|
-
<div class="form-row"><label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
|
|
34
|
-
<input type="text" id="node-input-server"></div>
|
|
35
|
-
|
|
36
|
-
<h4 style="margin-top:14px">Filters (optional)</h4>
|
|
37
|
-
<div class="form-row"><label for="node-input-filterKind"><i class="fa fa-server"></i> Kind</label>
|
|
38
|
-
<select id="node-input-filterKind">
|
|
39
|
-
<option value="any">Any</option>
|
|
40
|
-
<option value="master">Master only</option>
|
|
41
|
-
<option value="slave">Slave only</option>
|
|
42
|
-
</select>
|
|
43
|
-
</div>
|
|
44
|
-
<div class="form-row"><label for="node-input-filterDirection"><i class="fa fa-exchange"></i> Direction</label>
|
|
45
|
-
<select id="node-input-filterDirection">
|
|
46
|
-
<option value="any">Any</option>
|
|
47
|
-
<option value="read">Read only</option>
|
|
48
|
-
<option value="write">Write only</option>
|
|
49
|
-
</select>
|
|
50
|
-
</div>
|
|
51
|
-
<div class="form-row"><label for="node-input-filterFc"><i class="fa fa-list"></i> Function Code</label>
|
|
52
|
-
<select id="node-input-filterFc">
|
|
53
|
-
<option value="any">Any</option>
|
|
54
|
-
<option value="1">FC1 — Read Coils</option>
|
|
55
|
-
<option value="2">FC2 — Read Discrete Inputs</option>
|
|
56
|
-
<option value="3">FC3 — Read Holding</option>
|
|
57
|
-
<option value="4">FC4 — Read Input</option>
|
|
58
|
-
<option value="5">FC5 — Write Coil</option>
|
|
59
|
-
<option value="6">FC6 — Write Register</option>
|
|
60
|
-
<option value="15">FC15 — Write Coils</option>
|
|
61
|
-
<option value="16">FC16 — Write Registers</option>
|
|
62
|
-
</select>
|
|
63
|
-
</div>
|
|
64
|
-
<div class="form-row"><label for="node-input-filterTarget"><i class="fa fa-filter"></i> Target contains</label>
|
|
65
|
-
<input type="text" id="node-input-filterTarget" placeholder="e.g. 192.168.1.10 or rtu:/dev/ttyUSB0"></div>
|
|
66
|
-
|
|
67
|
-
<h4 style="margin-top:14px">Output</h4>
|
|
68
|
-
<div class="form-row"><label for="node-input-format"><i class="fa fa-code"></i> Payload</label>
|
|
69
|
-
<select id="node-input-format">
|
|
70
|
-
<option value="object">Full event object</option>
|
|
71
|
-
<option value="summary">One-line summary string</option>
|
|
72
|
-
</select>
|
|
73
|
-
</div>
|
|
74
|
-
<div class="form-row"><label for="node-input-alsoLog" style="width:auto">
|
|
75
|
-
<input type="checkbox" id="node-input-alsoLog" style="width:auto;vertical-align:middle">
|
|
76
|
-
Also write to Node-RED log
|
|
77
|
-
</label></div>
|
|
78
|
-
|
|
79
|
-
<div class="form-tips">
|
|
80
|
-
<b>Passive monitor.</b> No input wire — just listens to all Modbus ops on the runtime
|
|
81
|
-
and emits one message each. Free.
|
|
82
|
-
</div>
|
|
83
|
-
<div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
|
|
84
|
-
<a href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
|
|
85
|
-
style="display:inline-block;padding:8px 20px;background:#2563eb;color:#fff;
|
|
86
|
-
text-decoration:none;border-radius:6px;font-weight:600;font-size:13px;
|
|
87
|
-
box-shadow:0 1px 2px rgba(0,0,0,0.08);">
|
|
88
|
-
Premium Advanced Modbus Tester <i class="fa fa-external-link" style="margin-left:6px;"></i>
|
|
89
|
-
</a>
|
|
90
|
-
<div style="margin-top:6px;font-size:11px;color:#6b7280;">by ModPackQT — full-featured web tester, simulator & AI helper</div>
|
|
91
|
-
</div>
|
|
92
|
-
</script>
|
|
93
|
-
|
|
94
|
-
<script type="text/html" data-help-name="modpackqt-traffic">
|
|
95
|
-
<p>
|
|
96
|
-
Passive Modbus traffic monitor. Subscribes to all Modbus operations performed by any
|
|
97
|
-
master or slave node sharing the same runtime config and emits one message per op.
|
|
98
|
-
</p>
|
|
99
|
-
|
|
100
|
-
<h3>Output payload (object format)</h3>
|
|
101
|
-
<pre>{
|
|
102
|
-
ts: "2026-05-09T14:23:01.234Z",
|
|
103
|
-
direction: "read" | "write",
|
|
104
|
-
kind: "master" | "slave",
|
|
105
|
-
target: "192.168.1.10:502" | "local:1502" | "rtu:/dev/ttyUSB0",
|
|
106
|
-
unitId: 1,
|
|
107
|
-
fc: 3, // null for slave events
|
|
108
|
-
registerType: "holding",// only set for slave events
|
|
109
|
-
address: 100,
|
|
110
|
-
quantity: 2,
|
|
111
|
-
values: [16828, 0], // raw register values
|
|
112
|
-
durationMs: 12,
|
|
113
|
-
ok: true,
|
|
114
|
-
error: null // populated on failure
|
|
115
|
-
}</pre>
|
|
116
|
-
|
|
117
|
-
<h3>Use cases</h3>
|
|
118
|
-
<ul>
|
|
119
|
-
<li><b>Debug:</b> wire to a Debug node — see exactly what every Modbus op did.</li>
|
|
120
|
-
<li><b>Alerting:</b> filter <code>ok=false</code> → email/Slack on errors.</li>
|
|
121
|
-
<li><b>Charting:</b> count ops/sec, response-time histograms, per-target latency.</li>
|
|
122
|
-
<li><b>Audit trail:</b> store every op in a database for compliance.</li>
|
|
123
|
-
</ul>
|
|
124
|
-
|
|
125
|
-
<h3>Pricing</h3>
|
|
126
|
-
<p><b>Free</b> — full visibility into every Modbus operation, no usage limits.</p>
|
|
127
|
-
</script>
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ModPackQT Traffic — passive Modbus traffic monitor.
|
|
3
|
-
*
|
|
4
|
-
* Subscribes to the config node's traffic event bus and emits one message
|
|
5
|
-
* per Modbus operation (master read/write or slave read/write).
|
|
6
|
-
*
|
|
7
|
-
* Free for everyone — debug visibility costs nothing.
|
|
8
|
-
*/
|
|
9
|
-
module.exports = function (RED) {
|
|
10
|
-
function ModPackQTTrafficNode(config) {
|
|
11
|
-
RED.nodes.createNode(this, config);
|
|
12
|
-
const node = this;
|
|
13
|
-
const server = RED.nodes.getNode(config.server);
|
|
14
|
-
|
|
15
|
-
node.filterDirection = config.filterDirection || 'any'; // any | read | write
|
|
16
|
-
node.filterKind = config.filterKind || 'any'; // any | master | slave
|
|
17
|
-
node.filterFc = config.filterFc || 'any'; // any | 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16
|
|
18
|
-
node.filterTarget = (config.filterTarget || '').trim(); // substring match
|
|
19
|
-
node.format = config.format || 'object'; // object | summary
|
|
20
|
-
node.alsoLog = config.alsoLog === true || config.alsoLog === 'true';
|
|
21
|
-
|
|
22
|
-
let count = 0;
|
|
23
|
-
|
|
24
|
-
if (!server) {
|
|
25
|
-
node.status({ fill: 'red', shape: 'ring', text: 'no config' });
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function summarize(evt) {
|
|
30
|
-
const tag = evt.kind === 'slave'
|
|
31
|
-
? `slave ${evt.direction} ${evt.registerType}@${evt.address}×${evt.quantity}`
|
|
32
|
-
: `${evt.direction} FC${evt.fc} ${evt.target} u${evt.unitId} @${evt.address}×${evt.quantity}`;
|
|
33
|
-
const status = evt.ok ? `${evt.durationMs}ms` : `ERR ${evt.error || ''}`;
|
|
34
|
-
return `${tag} · ${status}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function onTraffic(evt) {
|
|
38
|
-
// Filters
|
|
39
|
-
if (node.filterDirection !== 'any' && evt.direction !== node.filterDirection) return;
|
|
40
|
-
if (node.filterKind !== 'any' && evt.kind !== node.filterKind) return;
|
|
41
|
-
if (node.filterFc !== 'any' && String(evt.fc) !== String(node.filterFc)) return;
|
|
42
|
-
if (node.filterTarget && !(evt.target || '').includes(node.filterTarget)) return;
|
|
43
|
-
|
|
44
|
-
count += 1;
|
|
45
|
-
const summary = summarize(evt);
|
|
46
|
-
node.status({
|
|
47
|
-
fill: evt.ok ? 'green' : 'red', shape: 'dot',
|
|
48
|
-
text: `modpackqt · ${count} ops · ${summary.slice(0, 50)}`
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (node.alsoLog) node.log(summary);
|
|
52
|
-
|
|
53
|
-
const msg = (node.format === 'summary')
|
|
54
|
-
? { payload: summary, traffic: evt }
|
|
55
|
-
: { payload: evt };
|
|
56
|
-
msg.topic = `modpackqt/traffic/${evt.kind}/${evt.direction}`;
|
|
57
|
-
node.send(msg);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
server.traffic.on('op', onTraffic);
|
|
61
|
-
node.status({ fill: 'grey', shape: 'ring', text: 'modpackqt · waiting…' });
|
|
62
|
-
|
|
63
|
-
node.on('close', function () {
|
|
64
|
-
try { server.traffic.removeListener('op', onTraffic); } catch (_) {}
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
RED.nodes.registerType('modpackqt-traffic', ModPackQTTrafficNode);
|
|
68
|
-
};
|