node-red-contrib-modbus-modpackqt 3.3.2 → 3.3.3
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 +25 -0
- package/nodes/lib/probe-runtime.js +1 -1
- package/nodes/modpackqt-slave-read.html +21 -18
- package/nodes/modpackqt-slave-read.js +15 -14
- package/nodes/modpackqt-slave-server.html +160 -0
- package/nodes/modpackqt-slave-server.js +137 -0
- package/nodes/modpackqt-slave-write.html +25 -18
- package/nodes/modpackqt-slave-write.js +17 -14
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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.3] — 2026-05-10
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`modpackqt-slave-server` config node** — a dedicated Modbus TCP slave
|
|
12
|
+
server with its own register store (65 536 per type). This replaces the
|
|
13
|
+
confusing embedded slave hidden inside the runtime config. Drop one per
|
|
14
|
+
fake device; `slave-read` and `slave-write` nodes point at it directly.
|
|
15
|
+
- Account Key + **My Slaves** picker load saved slave configs from
|
|
16
|
+
modpackqt.com (pre-fills port and unit ID).
|
|
17
|
+
- Register layout and limits are managed from the modpackqt.com web
|
|
18
|
+
console — click **Open in ModPackQT Console** inside the config dialog.
|
|
19
|
+
- Registers with the probe runtime so the web console finds it automatically.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **`slave-read` and `slave-write`** now have a **Slave Server** picker
|
|
24
|
+
(type `modpackqt-slave-server`) as their primary config reference. The old
|
|
25
|
+
`server` field (pointing to `modpackqt-config`) is kept as a hidden fallback
|
|
26
|
+
so existing flows with the embedded slave keep working.
|
|
27
|
+
- **Embedded slave section removed from runtime config dialog** — the
|
|
28
|
+
`modpackqt-config` dialog is now purely master/transport settings. The
|
|
29
|
+
embedded slave JS stays in the runtime for backward compat with old flows.
|
|
30
|
+
- `modpackqt-slave-probe` kept as-is for full backward compatibility.
|
|
31
|
+
|
|
7
32
|
## [3.3.2] — 2026-05-10
|
|
8
33
|
|
|
9
34
|
### Changed
|
|
@@ -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.3';
|
|
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;
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
color: '#15803d',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: '' },
|
|
7
|
-
|
|
7
|
+
slaveServer: { value: '', type: 'modpackqt-slave-server' },
|
|
8
|
+
server: { value: '' }, // legacy — modpackqt-config with embedded slave
|
|
8
9
|
registerType: { value: 'holding', required: true },
|
|
9
10
|
address: { value: 0, required: true, validate: RED.validators.number() },
|
|
10
11
|
quantity: { value: 1, required: true, validate: RED.validators.number() },
|
|
@@ -26,8 +27,8 @@
|
|
|
26
27
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
27
28
|
</div>
|
|
28
29
|
<div class="form-row">
|
|
29
|
-
<label for="node-input-
|
|
30
|
-
<input type="text" id="node-input-
|
|
30
|
+
<label for="node-input-slaveServer"><i class="fa fa-server"></i> Slave Server</label>
|
|
31
|
+
<input type="text" id="node-input-slaveServer">
|
|
31
32
|
</div>
|
|
32
33
|
<div class="form-row">
|
|
33
34
|
<label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
|
|
@@ -51,27 +52,29 @@
|
|
|
51
52
|
<input type="number" id="node-input-pollInterval" min="0" placeholder="0 = trigger only">
|
|
52
53
|
</div>
|
|
53
54
|
<div class="form-tips">
|
|
54
|
-
<b>Reads from
|
|
55
|
-
|
|
56
|
-
<code>msg.quantity</code>, or <code>msg.registerType</code>.
|
|
55
|
+
<b>Reads from a slave server's register store.</b> Point at a
|
|
56
|
+
<b>modpackqt-slave-server</b> node. Override at runtime via
|
|
57
|
+
<code>msg.address</code>, <code>msg.quantity</code>, or <code>msg.registerType</code>.
|
|
57
58
|
</div>
|
|
58
59
|
<div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
<a href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
|
|
61
|
+
style="display:inline-block;padding:8px 20px;background:#2563eb;color:#fff;
|
|
62
|
+
text-decoration:none;border-radius:6px;font-weight:600;font-size:13px;
|
|
63
|
+
box-shadow:0 1px 2px rgba(0,0,0,0.08);">
|
|
64
|
+
Premium Advanced Modbus Tester <i class="fa fa-external-link" style="margin-left:6px;"></i>
|
|
65
|
+
</a>
|
|
66
|
+
<div style="margin-top:6px;font-size:11px;color:#6b7280;">by ModPackQT — full-featured web tester, simulator & AI helper</div>
|
|
67
|
+
</div>
|
|
68
|
+
</script>
|
|
68
69
|
|
|
69
70
|
<script type="text/html" data-help-name="modpackqt-slave-read">
|
|
70
71
|
<p>
|
|
71
|
-
Reads from
|
|
72
|
-
external Modbus
|
|
72
|
+
Reads registers from a <b>modpackqt-slave-server</b> node's local store.
|
|
73
|
+
Useful for seeing exactly what an external Modbus master connecting to that
|
|
74
|
+
slave port will read.
|
|
73
75
|
</p>
|
|
74
|
-
<
|
|
76
|
+
<h3>Setup</h3>
|
|
77
|
+
<p>Set up a <b>Slave Server</b> config node first (pencil icon), then point this node at it.</p>
|
|
75
78
|
<h3>Outputs</h3>
|
|
76
79
|
<dl class="message-properties">
|
|
77
80
|
<dt>payload <span class="property-type">array</span></dt>
|
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ModPackQT Slave Read — read from
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* ModPackQT Slave Read — read from a slave server's local register store.
|
|
3
|
+
*
|
|
4
|
+
* Points at a modpackqt-slave-server config node (preferred) or falls back
|
|
5
|
+
* to the legacy modpackqt-config with an embedded slave (backward compat).
|
|
5
6
|
*/
|
|
6
7
|
module.exports = function (RED) {
|
|
7
8
|
function ModPackQTSlaveReadNode(config) {
|
|
8
9
|
RED.nodes.createNode(this, config);
|
|
9
10
|
const node = this;
|
|
10
|
-
const server = RED.nodes.getNode(config.server);
|
|
11
11
|
|
|
12
12
|
node.registerType = config.registerType || 'holding';
|
|
13
13
|
node.address = parseInt(config.address, 10) || 0;
|
|
14
14
|
node.quantity = parseInt(config.quantity, 10) || 1;
|
|
15
15
|
node.pollInterval = parseInt(config.pollInterval, 10) || 0;
|
|
16
16
|
|
|
17
|
+
// Prefer new slave-server config node; fall back to legacy modpackqt-config.
|
|
18
|
+
const slave = RED.nodes.getNode(config.slaveServer) || RED.nodes.getNode(config.server);
|
|
19
|
+
|
|
17
20
|
let timer = null;
|
|
18
21
|
|
|
19
22
|
function doRead(msg) {
|
|
20
|
-
if (!
|
|
21
|
-
node.status({ fill: 'red', shape: 'ring', text: 'no
|
|
23
|
+
if (!slave) {
|
|
24
|
+
node.status({ fill: 'red', shape: 'ring', text: 'no slave server configured' });
|
|
25
|
+
node.error('No Slave Server configured — add a modpackqt-slave-server node.', msg);
|
|
22
26
|
return;
|
|
23
27
|
}
|
|
24
|
-
if (!
|
|
25
|
-
node.status({ fill: 'red', shape: 'ring', text: 'slave server
|
|
26
|
-
node.error('
|
|
28
|
+
if (!slave.slaveEnabled) {
|
|
29
|
+
node.status({ fill: 'red', shape: 'ring', text: 'slave server not running' });
|
|
30
|
+
node.error('Slave server is not running — check the slave-server config node.', msg);
|
|
27
31
|
return;
|
|
28
32
|
}
|
|
29
33
|
try {
|
|
@@ -31,12 +35,9 @@ module.exports = function (RED) {
|
|
|
31
35
|
const address = (msg && msg.address !== undefined) ? parseInt(msg.address, 10) : node.address;
|
|
32
36
|
const quantity = (msg && msg.quantity !== undefined) ? parseInt(msg.quantity, 10) : node.quantity;
|
|
33
37
|
|
|
34
|
-
const values =
|
|
38
|
+
const values = slave.slaveGet(registerType, address, quantity);
|
|
35
39
|
const now = new Date().toLocaleTimeString();
|
|
36
|
-
node.status({
|
|
37
|
-
fill: 'green', shape: 'dot',
|
|
38
|
-
text: server.brandStatus(`slave ${registerType} @${address} [${values.length}] · ${now}`)
|
|
39
|
-
});
|
|
40
|
+
node.status({ fill: 'green', shape: 'dot', text: `${registerType} @${address} [${values.length}] · ${now}` });
|
|
40
41
|
node.send({
|
|
41
42
|
payload: values,
|
|
42
43
|
topic: `modpackqt/slave/${registerType}/${address}`,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('modpackqt-slave-server', {
|
|
3
|
+
category: 'config',
|
|
4
|
+
defaults: {
|
|
5
|
+
name: { value: '' },
|
|
6
|
+
bindHost: { value: '0.0.0.0' },
|
|
7
|
+
bindPort: { value: 1502, validate: RED.validators.number() },
|
|
8
|
+
unitId: { value: 1, validate: RED.validators.number() }
|
|
9
|
+
},
|
|
10
|
+
credentials: {
|
|
11
|
+
apiKey: { type: 'password' }
|
|
12
|
+
},
|
|
13
|
+
label: function () {
|
|
14
|
+
return this.name || `:${this.bindPort} #${this.unitId}`;
|
|
15
|
+
},
|
|
16
|
+
oneditprepare: function () {
|
|
17
|
+
const node = this;
|
|
18
|
+
|
|
19
|
+
// ── My Slaves picker — loads saved slave configs from modpackqt.com.
|
|
20
|
+
// Pre-fills port and unit ID. Register layout / limits are managed
|
|
21
|
+
// from the web console, not here.
|
|
22
|
+
const $sel = $('#node-config-modpackqt-slave-picker');
|
|
23
|
+
const reload = function () {
|
|
24
|
+
$sel.empty().append('<option value="">— Loading… —</option>');
|
|
25
|
+
if (!node.id) {
|
|
26
|
+
$sel.empty().append('<option value="">— Save this node first, then reopen —</option>');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
$.getJSON('modpackqt/slaves?probe=' + encodeURIComponent(node.id))
|
|
30
|
+
.done(function (rows) {
|
|
31
|
+
$sel.empty().append('<option value="">— Manual entry —</option>');
|
|
32
|
+
(rows || [])
|
|
33
|
+
.filter(function (s) { return s && (s.protocol === 'tcp' || !s.protocol); })
|
|
34
|
+
.forEach(function (s) {
|
|
35
|
+
const label = (s.name || '(unnamed)') + ' — :' + (s.port || 1502) + ' #' + (s.unitId || 1);
|
|
36
|
+
$('<option>').val(s.id).text(label).data('row', s).appendTo($sel);
|
|
37
|
+
});
|
|
38
|
+
})
|
|
39
|
+
.fail(function (xhr) {
|
|
40
|
+
const msg = (xhr.responseJSON && xhr.responseJSON.error) || ('HTTP ' + xhr.status);
|
|
41
|
+
$sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
$('#modpackqt-slave-server-picker-refresh').on('click', function (e) {
|
|
45
|
+
e.preventDefault(); reload();
|
|
46
|
+
});
|
|
47
|
+
$sel.on('change', function () {
|
|
48
|
+
const row = $(this).find('option:selected').data('row');
|
|
49
|
+
if (!row) return;
|
|
50
|
+
if (!node.name || node.name === '') $('#node-config-input-name').val(row.name || '');
|
|
51
|
+
$('#node-config-input-bindPort').val(row.port || 1502);
|
|
52
|
+
$('#node-config-input-unitId').val(row.unitId || 1);
|
|
53
|
+
buildLink();
|
|
54
|
+
});
|
|
55
|
+
reload();
|
|
56
|
+
|
|
57
|
+
// ── Probe deep-link
|
|
58
|
+
const buildLink = function () {
|
|
59
|
+
$.getJSON('modpackqt-probe/info')
|
|
60
|
+
.done(function (info) { renderLink(info); })
|
|
61
|
+
.fail(function () { renderLink({}); });
|
|
62
|
+
};
|
|
63
|
+
const renderLink = function (info) {
|
|
64
|
+
const probeHost = (info && info.host) || '127.0.0.1';
|
|
65
|
+
const probePort = (info && info.port) || 8502;
|
|
66
|
+
const bindPort = $('#node-config-input-bindPort').val() || node.bindPort || 1502;
|
|
67
|
+
const unitId = $('#node-config-input-unitId').val() || node.unitId || 1;
|
|
68
|
+
const name = $('#node-config-input-name').val() || node.name || '';
|
|
69
|
+
const url = 'https://modpackqt.com/slave'
|
|
70
|
+
+ '?probe=' + encodeURIComponent(node.id || '')
|
|
71
|
+
+ '&probeHost=' + encodeURIComponent(probeHost)
|
|
72
|
+
+ '&probePort=' + probePort
|
|
73
|
+
+ '&port=' + bindPort
|
|
74
|
+
+ '&unitId=' + unitId
|
|
75
|
+
+ (name ? '&name=' + encodeURIComponent(name) : '');
|
|
76
|
+
$('#modpackqt-slave-server-link').attr('href', url);
|
|
77
|
+
$('#modpackqt-slave-server-runtime').text(probeHost + ':' + probePort);
|
|
78
|
+
$('#modpackqt-slave-server-target').text(':' + bindPort + ' · unit ' + unitId);
|
|
79
|
+
};
|
|
80
|
+
buildLink();
|
|
81
|
+
$('#node-config-input-bindPort, #node-config-input-unitId').on('change input', buildLink);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<script type="text/html" data-template-name="modpackqt-slave-server">
|
|
87
|
+
<div class="form-row">
|
|
88
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
89
|
+
<input type="text" id="node-config-input-name" placeholder="e.g. Fake Inverter">
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="form-row">
|
|
93
|
+
<label for="node-config-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
|
|
94
|
+
<input type="password" id="node-config-input-apiKey" placeholder="Paste your ModPackQT tunnel key">
|
|
95
|
+
</div>
|
|
96
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
97
|
+
Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
98
|
+
Loads your saved slave configs in the picker below.
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="form-row">
|
|
102
|
+
<label for="node-config-modpackqt-slave-picker"><i class="fa fa-cloud-download"></i> My Slaves</label>
|
|
103
|
+
<select id="node-config-modpackqt-slave-picker" style="width:65%"></select>
|
|
104
|
+
<button type="button" id="modpackqt-slave-server-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
107
|
+
Pick a saved slave from modpackqt.com — auto-fills port and unit below.
|
|
108
|
+
Register layout and limits are managed from the web console.
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="form-row">
|
|
112
|
+
<label for="node-config-input-bindPort"><i class="fa fa-hashtag"></i> Port</label>
|
|
113
|
+
<input type="number" id="node-config-input-bindPort" min="1024" max="65535" placeholder="1502">
|
|
114
|
+
</div>
|
|
115
|
+
<div class="form-row">
|
|
116
|
+
<label for="node-config-input-unitId"><i class="fa fa-id-card"></i> Unit ID</label>
|
|
117
|
+
<input type="number" id="node-config-input-unitId" min="1" max="247" placeholder="1">
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<input type="hidden" id="node-config-input-bindHost">
|
|
121
|
+
|
|
122
|
+
<div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
|
|
123
|
+
<a id="modpackqt-slave-server-link" href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
|
|
124
|
+
style="display:inline-block;padding:10px 24px;background:#7c3aed;color:#fff;
|
|
125
|
+
text-decoration:none;border-radius:6px;font-weight:600;font-size:14px;
|
|
126
|
+
box-shadow:0 1px 2px rgba(0,0,0,0.08);">
|
|
127
|
+
Open in ModPackQT Console <i class="fa fa-external-link" style="margin-left:6px;"></i>
|
|
128
|
+
</a>
|
|
129
|
+
<div style="margin-top:6px;font-size:11px;color:#6b7280;">
|
|
130
|
+
Target: <span id="modpackqt-slave-server-target">—</span> · runtime <span id="modpackqt-slave-server-runtime">starting…</span>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<script type="text/html" data-help-name="modpackqt-slave-server">
|
|
136
|
+
<p>
|
|
137
|
+
Runs a Modbus TCP slave server and owns the register store. Point
|
|
138
|
+
<b>slave-read</b> and <b>slave-write</b> nodes at it to read and write
|
|
139
|
+
registers from your flow. External Modbus masters can also connect directly
|
|
140
|
+
to the configured port and read whatever your flow has written.
|
|
141
|
+
</p>
|
|
142
|
+
|
|
143
|
+
<h3>How to set it up</h3>
|
|
144
|
+
<ol>
|
|
145
|
+
<li>Drop a <b>slave-read</b> or <b>slave-write</b> node and click its pencil icon next to "Slave Server" to create a new slave server.</li>
|
|
146
|
+
<li>Paste your Account Key and pick a saved slave from <b>My Slaves</b> — it pre-fills the port and unit ID.</li>
|
|
147
|
+
<li>The register layout and coil/register counts are managed from the modpackqt.com web console — click <b>Open in ModPackQT Console</b> to edit them.</li>
|
|
148
|
+
</ol>
|
|
149
|
+
|
|
150
|
+
<h3>Account Key</h3>
|
|
151
|
+
<p>Generate one at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
152
|
+
Used to load your saved slave configs and to let the web console connect to this server.</p>
|
|
153
|
+
|
|
154
|
+
<h3>Multiple slave servers</h3>
|
|
155
|
+
<p>Create one slave-server config per port/unit combination. Slave-read and
|
|
156
|
+
slave-write nodes each pick which server to talk to.</p>
|
|
157
|
+
|
|
158
|
+
<h3>Port note</h3>
|
|
159
|
+
<p>Use ports above 1024 (e.g. 1502, 1503) to avoid needing root privileges.</p>
|
|
160
|
+
</script>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModPackQT Slave Server — Modbus TCP slave with a full register store.
|
|
3
|
+
*
|
|
4
|
+
* This is a config node. Drop one per fake device. Each server:
|
|
5
|
+
* - Binds its own TCP port and listens for external Modbus masters
|
|
6
|
+
* - Owns 65 536 registers per type (coils, discrete, holding, input)
|
|
7
|
+
* - Exposes slaveGet() / slaveSet() so slave-read / slave-write can
|
|
8
|
+
* push and pull values from the flow side
|
|
9
|
+
* - Registers with the probe runtime so the modpackqt.com web console
|
|
10
|
+
* can open a live register editor for it
|
|
11
|
+
*
|
|
12
|
+
* Register limits and layout are managed from the modpackqt.com web
|
|
13
|
+
* console (loaded via the My Slaves picker). Node-RED only needs the
|
|
14
|
+
* port and unit ID to bind the TCP listener.
|
|
15
|
+
*/
|
|
16
|
+
module.exports = function (RED) {
|
|
17
|
+
const ModbusRTU = require('modbus-serial');
|
|
18
|
+
const { getRuntime } = require('./lib/probe-runtime');
|
|
19
|
+
|
|
20
|
+
function SlaveServerNode(config) {
|
|
21
|
+
RED.nodes.createNode(this, config);
|
|
22
|
+
const node = this;
|
|
23
|
+
|
|
24
|
+
node.name = config.name || '';
|
|
25
|
+
node.bindHost = config.bindHost || '0.0.0.0';
|
|
26
|
+
node.bindPort = parseInt(config.bindPort, 10) || 1502;
|
|
27
|
+
node.unitId = parseInt(config.unitId, 10) || 1;
|
|
28
|
+
|
|
29
|
+
// ── Register store — full Modbus address space per type.
|
|
30
|
+
// Register limits / layout are defined by the modpackqt.com slave
|
|
31
|
+
// config and are managed via the web console, not here.
|
|
32
|
+
const store = {
|
|
33
|
+
coils: new Array(65536).fill(false),
|
|
34
|
+
discrete: new Array(65536).fill(false),
|
|
35
|
+
holding: new Array(65536).fill(0),
|
|
36
|
+
input: new Array(65536).fill(0)
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Flow-side API — used by slave-read and slave-write nodes.
|
|
40
|
+
node.slaveEnabled = true;
|
|
41
|
+
|
|
42
|
+
node.slaveGet = function (type, address, quantity) {
|
|
43
|
+
const arr = store[type];
|
|
44
|
+
if (!arr) throw new Error(`Unknown register type: ${type}`);
|
|
45
|
+
return arr.slice(address, address + quantity);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
node.slaveSet = function (type, address, values) {
|
|
49
|
+
const arr = store[type];
|
|
50
|
+
if (!arr) throw new Error(`Unknown register type: ${type}`);
|
|
51
|
+
const isBool = (type === 'coils' || type === 'discrete');
|
|
52
|
+
const stored = values.map((v) => isBool ? Boolean(v) : (parseInt(v, 10) & 0xFFFF));
|
|
53
|
+
stored.forEach((v, i) => { arr[address + i] = v; });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ── Modbus TCP server
|
|
57
|
+
let server = null;
|
|
58
|
+
try {
|
|
59
|
+
const vector = {
|
|
60
|
+
getCoil: (addr, _u, cb) => cb(null, store.coils[addr]),
|
|
61
|
+
getDiscreteInput: (addr, _u, cb) => cb(null, store.discrete[addr]),
|
|
62
|
+
getHoldingRegister: (addr, _u, cb) => cb(null, store.holding[addr]),
|
|
63
|
+
getInputRegister: (addr, _u, cb) => cb(null, store.input[addr]),
|
|
64
|
+
setCoil: (addr, val, _u, cb) => { store.coils[addr] = !!val; cb(null); },
|
|
65
|
+
setRegister: (addr, val, _u, cb) => { store.holding[addr] = val & 0xFFFF; cb(null); }
|
|
66
|
+
};
|
|
67
|
+
server = new ModbusRTU.ServerTCP(vector, {
|
|
68
|
+
host: node.bindHost,
|
|
69
|
+
port: node.bindPort,
|
|
70
|
+
debug: false,
|
|
71
|
+
unitID: node.unitId
|
|
72
|
+
});
|
|
73
|
+
server.on('socketError', (err) => node.warn(`[slave-server] socket: ${err.message}`));
|
|
74
|
+
server.on('serverError', (err) => node.error(`[slave-server] server: ${err.message}`));
|
|
75
|
+
node.log(`[modpackqt] slave server listening on ${node.bindHost}:${node.bindPort} #${node.unitId}`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
node.error(`Failed to start slave server: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Probe runtime registration — lets the web console find this slave.
|
|
81
|
+
const probe = {
|
|
82
|
+
id: node.id,
|
|
83
|
+
kind: 'slave',
|
|
84
|
+
describe(_detailed = false) {
|
|
85
|
+
return {
|
|
86
|
+
id: node.id,
|
|
87
|
+
kind: 'slave',
|
|
88
|
+
name: node.name || `Slave :${node.bindPort} #${node.unitId}`,
|
|
89
|
+
target: {
|
|
90
|
+
mode: 'tcp',
|
|
91
|
+
host: node.bindHost,
|
|
92
|
+
port: node.bindPort,
|
|
93
|
+
unitId: node.unitId
|
|
94
|
+
},
|
|
95
|
+
status: server ? 'listening' : 'error'
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
async handleRead() { throw new Error('slave server — use GET /store to inspect registers'); },
|
|
99
|
+
async handleWrite() { throw new Error('slave server — use PUT /store to set registers'); },
|
|
100
|
+
async handleStoreGet(searchParams) {
|
|
101
|
+
const type = searchParams.get('type') || 'holding';
|
|
102
|
+
const addr = parseInt(searchParams.get('address') || '0', 10);
|
|
103
|
+
const qty = parseInt(searchParams.get('quantity') || '10', 10);
|
|
104
|
+
if (!store[type]) throw new Error(`unknown register type: ${type}`);
|
|
105
|
+
return { type, address: addr, values: store[type].slice(addr, addr + qty) };
|
|
106
|
+
},
|
|
107
|
+
async handleStorePut(body) {
|
|
108
|
+
const type = body.type || 'holding';
|
|
109
|
+
const addr = parseInt(body.address, 10) || 0;
|
|
110
|
+
const values = body.values || [];
|
|
111
|
+
if (!store[type]) throw new Error(`unknown register type: ${type}`);
|
|
112
|
+
if (!Array.isArray(values)) throw new Error('values must be an array');
|
|
113
|
+
const isBool = (type === 'coils' || type === 'discrete');
|
|
114
|
+
const stored = values.map((v) => isBool ? Boolean(v) : (parseInt(v, 10) & 0xFFFF));
|
|
115
|
+
stored.forEach((v, i) => { store[type][addr + i] = v; });
|
|
116
|
+
return { ok: true, type, address: addr, written: stored.length };
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const runtime = getRuntime();
|
|
121
|
+
runtime.ensureAdminRoutes(RED);
|
|
122
|
+
runtime.register(probe);
|
|
123
|
+
|
|
124
|
+
node.on('close', function (done) {
|
|
125
|
+
runtime.unregister(node.id);
|
|
126
|
+
if (server) {
|
|
127
|
+
try { server.close(() => done()); } catch (_) { done(); }
|
|
128
|
+
} else {
|
|
129
|
+
done();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
RED.nodes.registerType('modpackqt-slave-server', SlaveServerNode, {
|
|
135
|
+
credentials: { apiKey: { type: 'password' } }
|
|
136
|
+
});
|
|
137
|
+
};
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
color: '#15803d',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: '' },
|
|
7
|
-
|
|
7
|
+
slaveServer: { value: '', type: 'modpackqt-slave-server' },
|
|
8
|
+
server: { value: '' }, // legacy — modpackqt-config with embedded slave
|
|
8
9
|
registerType: { value: 'holding', required: true },
|
|
9
10
|
address: { value: 0, required: true, validate: RED.validators.number() }
|
|
10
11
|
},
|
|
@@ -24,8 +25,8 @@
|
|
|
24
25
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
25
26
|
</div>
|
|
26
27
|
<div class="form-row">
|
|
27
|
-
<label for="node-input-
|
|
28
|
-
<input type="text" id="node-input-
|
|
28
|
+
<label for="node-input-slaveServer"><i class="fa fa-server"></i> Slave Server</label>
|
|
29
|
+
<input type="text" id="node-input-slaveServer">
|
|
29
30
|
</div>
|
|
30
31
|
<div class="form-row">
|
|
31
32
|
<label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
|
|
@@ -41,26 +42,32 @@
|
|
|
41
42
|
<input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
|
|
42
43
|
</div>
|
|
43
44
|
<div class="form-tips">
|
|
44
|
-
<b>Pushes values into
|
|
45
|
-
|
|
45
|
+
<b>Pushes values into a slave server's register store.</b> Any external Modbus master
|
|
46
|
+
connecting to that slave port will read whatever you last wrote.
|
|
46
47
|
Send <code>msg.payload</code> as a number, array, or JSON string.
|
|
47
48
|
</div>
|
|
48
49
|
<div class="form-row" style="margin-top:18px;padding-top:14px;border-top:1px solid #e5e7eb;text-align:center;">
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
<a href="https://modpackqt.com" target="_blank" rel="noopener noreferrer"
|
|
51
|
+
style="display:inline-block;padding:8px 20px;background:#2563eb;color:#fff;
|
|
52
|
+
text-decoration:none;border-radius:6px;font-weight:600;font-size:13px;
|
|
53
|
+
box-shadow:0 1px 2px rgba(0,0,0,0.08);">
|
|
54
|
+
Premium Advanced Modbus Tester <i class="fa fa-external-link" style="margin-left:6px;"></i>
|
|
55
|
+
</a>
|
|
56
|
+
<div style="margin-top:6px;font-size:11px;color:#6b7280;">by ModPackQT — full-featured web tester, simulator & AI helper</div>
|
|
57
|
+
</div>
|
|
58
|
+
</script>
|
|
58
59
|
|
|
59
60
|
<script type="text/html" data-help-name="modpackqt-slave-write">
|
|
60
61
|
<p>
|
|
61
|
-
Writes into
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
Writes values into a <b>modpackqt-slave-server</b> node's register store.
|
|
63
|
+
External Modbus masters connecting to that slave's port will read whatever
|
|
64
|
+
was last written here.
|
|
64
65
|
</p>
|
|
65
|
-
<
|
|
66
|
+
<h3>Setup</h3>
|
|
67
|
+
<p>Set up a <b>Slave Server</b> config node first (pencil icon), then point this node at it.</p>
|
|
68
|
+
<h3>Input</h3>
|
|
69
|
+
<dl class="message-properties">
|
|
70
|
+
<dt>payload <span class="property-type">number | array | string</span></dt>
|
|
71
|
+
<dd>Value(s) to write. Arrays write multiple consecutive registers.</dd>
|
|
72
|
+
</dl>
|
|
66
73
|
</script>
|
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ModPackQT Slave Write — write into
|
|
3
|
-
*
|
|
4
|
-
* modpackqt-config
|
|
2
|
+
* ModPackQT Slave Write — write into a slave server's local register store.
|
|
3
|
+
*
|
|
4
|
+
* Points at a modpackqt-slave-server config node (preferred) or falls back
|
|
5
|
+
* to the legacy modpackqt-config with an embedded slave (backward compat).
|
|
6
|
+
* External Modbus masters connecting to the slave port will read whatever
|
|
7
|
+
* was last written here.
|
|
5
8
|
*/
|
|
6
9
|
module.exports = function (RED) {
|
|
7
10
|
function ModPackQTSlaveWriteNode(config) {
|
|
8
11
|
RED.nodes.createNode(this, config);
|
|
9
12
|
const node = this;
|
|
10
|
-
const server = RED.nodes.getNode(config.server);
|
|
11
13
|
|
|
12
14
|
node.registerType = config.registerType || 'holding';
|
|
13
15
|
node.address = parseInt(config.address, 10) || 0;
|
|
14
16
|
|
|
17
|
+
// Prefer new slave-server config node; fall back to legacy modpackqt-config.
|
|
18
|
+
const slave = RED.nodes.getNode(config.slaveServer) || RED.nodes.getNode(config.server);
|
|
19
|
+
|
|
15
20
|
node.on('input', function (msg) {
|
|
16
|
-
if (!
|
|
17
|
-
node.status({ fill: 'red', shape: 'ring', text: 'no
|
|
21
|
+
if (!slave) {
|
|
22
|
+
node.status({ fill: 'red', shape: 'ring', text: 'no slave server configured' });
|
|
23
|
+
node.error('No Slave Server configured — add a modpackqt-slave-server node.', msg);
|
|
18
24
|
return;
|
|
19
25
|
}
|
|
20
|
-
if (!
|
|
21
|
-
node.status({ fill: 'red', shape: 'ring', text: 'slave server
|
|
22
|
-
node.error('
|
|
26
|
+
if (!slave.slaveEnabled) {
|
|
27
|
+
node.status({ fill: 'red', shape: 'ring', text: 'slave server not running' });
|
|
28
|
+
node.error('Slave server is not running — check the slave-server config node.', msg);
|
|
23
29
|
return;
|
|
24
30
|
}
|
|
25
31
|
try {
|
|
@@ -37,12 +43,9 @@ module.exports = function (RED) {
|
|
|
37
43
|
}
|
|
38
44
|
const address = msg.address !== undefined ? parseInt(msg.address, 10) : node.address;
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
slave.slaveSet(registerType, address, values);
|
|
41
47
|
const now = new Date().toLocaleTimeString();
|
|
42
|
-
node.status({
|
|
43
|
-
fill: 'green', shape: 'dot',
|
|
44
|
-
text: server.brandStatus(`slave wrote ${values.length} @${address} · ${now}`)
|
|
45
|
-
});
|
|
48
|
+
node.status({ fill: 'green', shape: 'dot', text: `wrote ${values.length} → ${registerType} @${address} · ${now}` });
|
|
46
49
|
msg.success = true;
|
|
47
50
|
msg.valuesWritten = values;
|
|
48
51
|
node.send(msg);
|
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.3",
|
|
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",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"modpackqt-slave-write": "nodes/modpackqt-slave-write.js",
|
|
52
52
|
"modpackqt-traffic": "nodes/modpackqt-traffic.js",
|
|
53
53
|
"modpackqt-master-probe": "nodes/modpackqt-master-probe.js",
|
|
54
|
-
"modpackqt-slave-probe": "nodes/modpackqt-slave-probe.js"
|
|
54
|
+
"modpackqt-slave-probe": "nodes/modpackqt-slave-probe.js",
|
|
55
|
+
"modpackqt-slave-server": "nodes/modpackqt-slave-server.js"
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|