node-red-contrib-modbus-modpackqt 3.3.4 → 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 +33 -0
- package/README.md +1 -1
- package/nodes/lib/probe-runtime.js +1 -1
- package/nodes/modpackqt-config.html +64 -0
- package/nodes/modpackqt-config.js +3 -1
- package/nodes/modpackqt-slave-server.html +3 -10
- 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,39 @@ 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
|
+
|
|
18
|
+
## [3.3.6] — 2026-05-10
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Account Key + My Connections picker on the runtime config node.**
|
|
23
|
+
Paste your ModPackQT tunnel key into the config dialog — the
|
|
24
|
+
**My Connections** dropdown loads your saved devices from
|
|
25
|
+
modpackqt.com and auto-fills Host, Port, and Unit ID. Manual entry
|
|
26
|
+
still works as a fallback.
|
|
27
|
+
- Config node credentials are now registered so the admin proxy
|
|
28
|
+
(`/modpackqt/connections`) can look up the key without exposing it
|
|
29
|
+
to the browser.
|
|
30
|
+
|
|
31
|
+
## [3.3.5] — 2026-05-10
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Port and Unit ID fields removed from slave server dialog.** They are now
|
|
36
|
+
hidden — the only way to set them is by picking from the **My Slaves**
|
|
37
|
+
profile picker. This enforces the cloud-driven config workflow and removes
|
|
38
|
+
manual entry that could conflict with the saved profile.
|
|
39
|
+
|
|
7
40
|
## [3.3.4] — 2026-05-10
|
|
8
41
|
|
|
9
42
|
### Changed
|
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;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType('modpackqt-config', {
|
|
3
3
|
category: 'config',
|
|
4
|
+
credentials: {
|
|
5
|
+
apiKey: { type: 'password' }
|
|
6
|
+
},
|
|
4
7
|
defaults: {
|
|
5
8
|
name: { value: 'ModPackQT Device' },
|
|
6
9
|
targetHost: { value: 'localhost' },
|
|
@@ -39,6 +42,49 @@
|
|
|
39
42
|
toggleRtu();
|
|
40
43
|
toggleSlave();
|
|
41
44
|
|
|
45
|
+
// ── My Connections picker
|
|
46
|
+
const nodeId = this.id;
|
|
47
|
+
const $sel = $('#node-config-input-modpackqt-conn-picker');
|
|
48
|
+
const loadConnections = function () {
|
|
49
|
+
$sel.empty().append('<option value="">— Loading… —</option>');
|
|
50
|
+
if (!nodeId) {
|
|
51
|
+
$sel.empty().append('<option value="">— Save this config first, then reopen —</option>');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
$.getJSON('modpackqt/connections?probe=' + encodeURIComponent(nodeId))
|
|
55
|
+
.done(function (rows) {
|
|
56
|
+
$sel.empty().append('<option value="">— Manual entry —</option>');
|
|
57
|
+
(rows || []).forEach(function (c) {
|
|
58
|
+
const label = (c.name || '(unnamed)') + (c.connectionType === 'rtu'
|
|
59
|
+
? ' — RTU'
|
|
60
|
+
: ' — ' + (c.host || '?') + ':' + (c.port || 502) + ' #' + (c.unitId || 1));
|
|
61
|
+
$('<option>').val(c.id).text(label).data('conn', c).appendTo($sel);
|
|
62
|
+
});
|
|
63
|
+
})
|
|
64
|
+
.fail(function (xhr) {
|
|
65
|
+
const msg = (xhr.responseJSON && xhr.responseJSON.error) || ('HTTP ' + xhr.status);
|
|
66
|
+
$sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
$('#modpackqt-conn-picker-refresh').on('click', function (e) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
loadConnections();
|
|
72
|
+
});
|
|
73
|
+
$sel.on('change', function () {
|
|
74
|
+
const c = $(this).find('option:selected').data('conn');
|
|
75
|
+
if (!c) return;
|
|
76
|
+
if (!$('#node-config-input-name').val() || $('#node-config-input-name').val() === 'ModPackQT Device') {
|
|
77
|
+
$('#node-config-input-name').val(c.name || '');
|
|
78
|
+
}
|
|
79
|
+
if (c.host) $('#node-config-input-targetHost').val(c.host);
|
|
80
|
+
if (c.port) $('#node-config-input-targetPort').val(c.port);
|
|
81
|
+
if (c.unitId) $('#node-config-input-unitId').val(c.unitId);
|
|
82
|
+
if (c.connectionType === 'rtu') {
|
|
83
|
+
$('#node-config-input-masterMode').val('rtu').trigger('change');
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
loadConnections();
|
|
87
|
+
|
|
42
88
|
// ── Embedded slave server section (collapsed by default)
|
|
43
89
|
const $slaveSection = $('.modpackqt-slave-section');
|
|
44
90
|
const $slaveToggle = $('.modpackqt-slave-section-toggle');
|
|
@@ -63,6 +109,24 @@
|
|
|
63
109
|
<input type="text" id="node-config-input-name" placeholder="e.g. Inverter A">
|
|
64
110
|
</div>
|
|
65
111
|
|
|
112
|
+
<div class="form-row">
|
|
113
|
+
<label for="node-config-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
|
|
114
|
+
<input type="password" id="node-config-input-apiKey" placeholder="Paste your ModPackQT tunnel key">
|
|
115
|
+
</div>
|
|
116
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 4px 105px">
|
|
117
|
+
Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
118
|
+
Loads your saved connections in the picker below.
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div class="form-row">
|
|
122
|
+
<label for="node-config-input-modpackqt-conn-picker"><i class="fa fa-cloud-download"></i> My Connections</label>
|
|
123
|
+
<select id="node-config-input-modpackqt-conn-picker" style="width:65%"></select>
|
|
124
|
+
<button type="button" id="modpackqt-conn-picker-refresh" class="red-ui-button" title="Reload" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
127
|
+
Picks a saved connection from modpackqt.com — auto-fills Host, Port, and Unit ID below.
|
|
128
|
+
</div>
|
|
129
|
+
|
|
66
130
|
<h4 style="margin-top:18px">Device</h4>
|
|
67
131
|
<div class="form-row modpackqt-tcp-only">
|
|
68
132
|
<label for="node-config-input-targetHost"><i class="fa fa-plug"></i> Host</label>
|
|
@@ -104,20 +104,13 @@
|
|
|
104
104
|
<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>
|
|
105
105
|
</div>
|
|
106
106
|
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
107
|
-
|
|
107
|
+
Required — pick a saved slave to set the port and unit ID.
|
|
108
108
|
Register layout and limits are managed from the web console.
|
|
109
109
|
</div>
|
|
110
110
|
|
|
111
|
-
<div class="form-row">
|
|
112
|
-
<label for="node-input-bindPort"><i class="fa fa-hashtag"></i> Port</label>
|
|
113
|
-
<input type="number" id="node-input-bindPort" min="1024" max="65535" placeholder="1502">
|
|
114
|
-
</div>
|
|
115
|
-
<div class="form-row">
|
|
116
|
-
<label for="node-input-unitId"><i class="fa fa-id-card"></i> Unit ID</label>
|
|
117
|
-
<input type="number" id="node-input-unitId" min="1" max="247" placeholder="1">
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
111
|
<input type="hidden" id="node-input-bindHost">
|
|
112
|
+
<input type="hidden" id="node-input-bindPort">
|
|
113
|
+
<input type="hidden" id="node-input-unitId">
|
|
121
114
|
|
|
122
115
|
<div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
|
|
123
116
|
<a id="modpackqt-slave-server-link" href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
|
|
@@ -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",
|