node-red-contrib-modbus-modpackqt 3.1.0 → 3.2.0
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 +41 -0
- package/README.md +1 -0
- package/nodes/lib/cloud-client.js +58 -0
- package/nodes/lib/probe-runtime.js +1 -1
- package/nodes/modpackqt-config.html +15 -6
- package/nodes/modpackqt-config.js +25 -0
- package/nodes/modpackqt-master-probe.html +30 -9
- package/nodes/modpackqt-master-read.html +54 -1
- package/nodes/modpackqt-master-write.html +10 -1
- package/nodes/modpackqt-slave-probe.html +71 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ 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.2.0] — 2026-05-10
|
|
8
|
+
|
|
9
|
+
### Added — Cloud profile pickers
|
|
10
|
+
|
|
11
|
+
- **"My Profiles" dropdown** on `modpackqt-master-read`,
|
|
12
|
+
`modpackqt-master-write` and `modpackqt-master-probe`. Picks one of
|
|
13
|
+
your saved Modbus TCP profiles from your modpackqt.com account and
|
|
14
|
+
auto-fills `targetHost`, `targetPort` and `unitId`. No more retyping
|
|
15
|
+
IPs across nodes.
|
|
16
|
+
- **"My Slaves" dropdown** on `modpackqt-slave-probe`. Picks one of
|
|
17
|
+
your saved slave configs and auto-fills `bindPort` + `unitId`.
|
|
18
|
+
- **Account Key** field on the runtime config (renamed from "API Key").
|
|
19
|
+
Paste your tunnel key from
|
|
20
|
+
[modpackqt.com → Settings → Cloud Gateways](https://modpackqt.com/settings)
|
|
21
|
+
to enable the dropdowns. Same key the Electron gateway uses — no new
|
|
22
|
+
signup, no new mechanism.
|
|
23
|
+
- **Hidden admin proxy** (`/modpackqt/connections`, `/modpackqt/slaves`)
|
|
24
|
+
on the Node-RED admin port. Calls modpackqt.com server-side from the
|
|
25
|
+
Node-RED process so the Account Key never reaches the browser.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Manual `targetHost` / `targetPort` / `unitId` fields stay as a fallback
|
|
30
|
+
on every node — existing v3.1.x flows keep working untouched.
|
|
31
|
+
- Runtime config `apiKey` credential is unchanged on disk — only the
|
|
32
|
+
label and helper text in the editor were updated.
|
|
33
|
+
|
|
34
|
+
## [3.1.1] — 2026-05-10
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **"Open in ModPackQT Console" buttons now point at the existing
|
|
39
|
+
`/master` and `/slave` pages** (instead of a non-existent `/console`
|
|
40
|
+
route). Master probe → `https://modpackqt.com/master?probe=…&host=…
|
|
41
|
+
&port=…&unitId=…`, slave probe → `https://modpackqt.com/slave?probe=…
|
|
42
|
+
&port=…&unitId=…`. The pages auto-fill the connection panel from the
|
|
43
|
+
query string, so the button is useful immediately — even before the
|
|
44
|
+
web app wires up to the local probe runtime API.
|
|
45
|
+
- Both URLs still carry `probeHost` + `probePort` + `probe` (node id)
|
|
46
|
+
so the web app can attach to the local runtime once that work lands.
|
|
47
|
+
|
|
7
48
|
## [3.1.0] — 2026-05-10
|
|
8
49
|
|
|
9
50
|
### Added
|
package/README.md
CHANGED
|
@@ -16,6 +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
|
+
- **Optional cloud profile pickers** *(v3.2.0)* — paste your ModPackQT Account Key into the runtime config and master / probe nodes show a dropdown of your saved Modbus profiles. No more retyping IPs across nodes.
|
|
19
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
|
|
20
21
|
- **Zero external dependencies** — Modbus runs inside the Node-RED process
|
|
21
22
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModPackQT Cloud Client — minimal HTTPS reader for the user's saved
|
|
3
|
+
* Modbus profiles & slaves at modpackqt.com.
|
|
4
|
+
*
|
|
5
|
+
* Reuses the existing X-Tunnel-Key auth that the Electron gateway already
|
|
6
|
+
* uses. The user's "Account Key" is the same tunnel key they generate at
|
|
7
|
+
* modpackqt.com → Settings → Cloud Gateways. No new auth, no new dep.
|
|
8
|
+
*
|
|
9
|
+
* All requests are read-only GETs. The Node-RED admin endpoint in
|
|
10
|
+
* modpackqt-config.js wraps these so the editor never sees the key
|
|
11
|
+
* (proxied server-side from the Node-RED process).
|
|
12
|
+
*/
|
|
13
|
+
const https = require('https');
|
|
14
|
+
const http = require('http');
|
|
15
|
+
const { URL } = require('url');
|
|
16
|
+
|
|
17
|
+
const ORIGIN = process.env.MODPACKQT_CLOUD_ORIGIN || 'https://modpackqt.com';
|
|
18
|
+
const TIMEOUT_MS = 10000;
|
|
19
|
+
|
|
20
|
+
function fetchJson(path, key, callback) {
|
|
21
|
+
let u;
|
|
22
|
+
try { u = new URL(path, ORIGIN); }
|
|
23
|
+
catch (e) { return callback(new Error('Invalid cloud URL: ' + e.message)); }
|
|
24
|
+
|
|
25
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
26
|
+
const req = lib.request({
|
|
27
|
+
method: 'GET',
|
|
28
|
+
hostname: u.hostname,
|
|
29
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
30
|
+
path: u.pathname + (u.search || ''),
|
|
31
|
+
headers: {
|
|
32
|
+
'X-Tunnel-Key': key,
|
|
33
|
+
'Accept': 'application/json',
|
|
34
|
+
'User-Agent': 'node-red-contrib-modbus-modpackqt'
|
|
35
|
+
},
|
|
36
|
+
timeout: TIMEOUT_MS
|
|
37
|
+
}, (res) => {
|
|
38
|
+
let body = '';
|
|
39
|
+
res.setEncoding('utf8');
|
|
40
|
+
res.on('data', (c) => { body += c; });
|
|
41
|
+
res.on('end', () => {
|
|
42
|
+
if (res.statusCode >= 400) {
|
|
43
|
+
return callback(new Error('Cloud HTTP ' + res.statusCode + ': ' + body.slice(0, 200)));
|
|
44
|
+
}
|
|
45
|
+
try { callback(null, JSON.parse(body)); }
|
|
46
|
+
catch (e) { callback(new Error('Invalid JSON from cloud: ' + e.message)); }
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
req.on('error', (err) => callback(err));
|
|
50
|
+
req.on('timeout', () => { req.destroy(new Error('Cloud request timed out after ' + TIMEOUT_MS + 'ms')); });
|
|
51
|
+
req.end();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
origin: () => ORIGIN,
|
|
56
|
+
fetchConnections: (key, cb) => fetchJson('/api/connections', key, cb),
|
|
57
|
+
fetchSlaves: (key, cb) => fetchJson('/api/slaves', key, cb),
|
|
58
|
+
};
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
const http = require('http');
|
|
19
19
|
const { URL } = require('url');
|
|
20
20
|
|
|
21
|
-
const PALETTE_VERSION = '3.
|
|
21
|
+
const PALETTE_VERSION = '3.2.0';
|
|
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;
|
|
@@ -102,10 +102,15 @@
|
|
|
102
102
|
<input type="number" id="node-config-input-slavePort" placeholder="1502">
|
|
103
103
|
</div>
|
|
104
104
|
|
|
105
|
-
<h4 style="margin-top:18px">
|
|
105
|
+
<h4 style="margin-top:18px">ModPackQT Account Key (optional — for cloud profile sync)</h4>
|
|
106
106
|
<div class="form-row">
|
|
107
|
-
<label for="node-config-input-apiKey"><i class="fa fa-key"></i>
|
|
108
|
-
<input type="password" id="node-config-input-apiKey" placeholder="Optional —
|
|
107
|
+
<label for="node-config-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
|
|
108
|
+
<input type="password" id="node-config-input-apiKey" placeholder="Optional — paste your tunnel key for profile pickers">
|
|
109
|
+
</div>
|
|
110
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin-top:-6px">
|
|
111
|
+
With a key set, master & probe nodes show a dropdown of your saved Modbus profiles
|
|
112
|
+
and slaves from <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
113
|
+
All Modbus runs locally — the key is only used for read-only profile sync.
|
|
109
114
|
</div>
|
|
110
115
|
|
|
111
116
|
<div class="form-tips">
|
|
@@ -143,9 +148,13 @@
|
|
|
143
148
|
last wrote.
|
|
144
149
|
</p>
|
|
145
150
|
|
|
146
|
-
<h3>
|
|
151
|
+
<h3>ModPackQT Account Key (optional)</h3>
|
|
147
152
|
<p>
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
Same tunnel key the Electron gateway uses — generate one at
|
|
154
|
+
<a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
155
|
+
With a key set, the master and probe nodes show a dropdown of your saved Modbus
|
|
156
|
+
profiles & slaves so you don't have to retype IPs and unit IDs. All Modbus traffic
|
|
157
|
+
still runs locally — the key is only used for a read-only HTTPS call from this
|
|
158
|
+
Node-RED process to load your profile list.
|
|
150
159
|
</p>
|
|
151
160
|
</script>
|
|
@@ -8,6 +8,31 @@
|
|
|
8
8
|
module.exports = function (RED) {
|
|
9
9
|
const ModbusRTU = require('modbus-serial');
|
|
10
10
|
const { EventEmitter } = require('events');
|
|
11
|
+
const cloudClient = require('./lib/cloud-client');
|
|
12
|
+
|
|
13
|
+
// ── Admin proxy endpoints — let the editor read the user's saved profiles
|
|
14
|
+
// and slaves from modpackqt.com without ever exposing the Account Key to
|
|
15
|
+
// the browser. Same X-Tunnel-Key the Electron gateway already uses.
|
|
16
|
+
// Registered once per Node-RED process.
|
|
17
|
+
if (!RED._modpackqtAdminRoutesRegistered) {
|
|
18
|
+
RED._modpackqtAdminRoutesRegistered = true;
|
|
19
|
+
function makeProxy(fetchFn) {
|
|
20
|
+
return function (req, res) {
|
|
21
|
+
const configId = req.query && req.query.config;
|
|
22
|
+
if (!configId) return res.status(400).json({ error: 'config query param required' });
|
|
23
|
+
const cfg = RED.nodes.getNode(configId);
|
|
24
|
+
if (!cfg) return res.status(404).json({ error: 'Runtime config not found (deploy first?)' });
|
|
25
|
+
const key = cfg.credentials && cfg.credentials.apiKey;
|
|
26
|
+
if (!key) return res.status(400).json({ error: 'No Account Key set on the runtime config — paste it in the modpackqt-config node first.' });
|
|
27
|
+
fetchFn(key, function (err, data) {
|
|
28
|
+
if (err) return res.status(502).json({ error: err.message });
|
|
29
|
+
res.json(data);
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
RED.httpAdmin.get('/modpackqt/connections', RED.auth.needsPermission('flows.read'), makeProxy(cloudClient.fetchConnections));
|
|
34
|
+
RED.httpAdmin.get('/modpackqt/slaves', RED.auth.needsPermission('flows.read'), makeProxy(cloudClient.fetchSlaves));
|
|
35
|
+
}
|
|
11
36
|
|
|
12
37
|
function ModPackQTConfigNode(config) {
|
|
13
38
|
RED.nodes.createNode(this, config);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
color: '#7c3aed',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: '' },
|
|
7
|
+
server: { value: '', type: 'modpackqt-config' },
|
|
7
8
|
targetHost: { value: '192.168.1.10', required: true },
|
|
8
9
|
targetPort: { value: 502, required: true, validate: RED.validators.number() },
|
|
9
10
|
unitId: { value: 1, required: true, validate: RED.validators.number() },
|
|
@@ -18,17 +19,25 @@
|
|
|
18
19
|
paletteLabel: 'modbus master probe',
|
|
19
20
|
oneditprepare: function () {
|
|
20
21
|
const node = this;
|
|
22
|
+
// Optional cloud profile picker (fills targetHost/targetPort/unitId)
|
|
23
|
+
if (window.ModPackQTMasterPicker) ModPackQTMasterPicker.attach(this);
|
|
21
24
|
const buildLink = (info) => {
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
+ '
|
|
25
|
+
const probeHost = (info && info.host) || '127.0.0.1';
|
|
26
|
+
const probePort = (info && info.port) || 8502;
|
|
27
|
+
const targetHost = $('#node-input-targetHost').val() || node.targetHost || '';
|
|
28
|
+
const targetPort = $('#node-input-targetPort').val() || node.targetPort || 502;
|
|
29
|
+
const unitId = $('#node-input-unitId').val() || node.unitId || 1;
|
|
30
|
+
const name = $('#node-input-name').val() || node.name || '';
|
|
31
|
+
const url = 'https://modpackqt.com/master'
|
|
32
|
+
+ '?probe=' + encodeURIComponent(node.id)
|
|
33
|
+
+ '&probeHost=' + encodeURIComponent(probeHost)
|
|
34
|
+
+ '&probePort=' + probePort
|
|
35
|
+
+ '&host=' + encodeURIComponent(targetHost)
|
|
36
|
+
+ '&port=' + targetPort
|
|
37
|
+
+ '&unitId=' + unitId
|
|
38
|
+
+ (name ? '&name=' + encodeURIComponent(name) : '');
|
|
30
39
|
$('#modpackqt-probe-link').attr('href', url);
|
|
31
|
-
$('#modpackqt-probe-runtime').text(
|
|
40
|
+
$('#modpackqt-probe-runtime').text(probeHost + ':' + probePort);
|
|
32
41
|
};
|
|
33
42
|
$.getJSON('modpackqt-probe/info').done(buildLink).fail(() => buildLink({}));
|
|
34
43
|
}
|
|
@@ -40,6 +49,18 @@
|
|
|
40
49
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
41
50
|
<input type="text" id="node-input-name" placeholder="(optional, e.g. Inverter A)">
|
|
42
51
|
</div>
|
|
52
|
+
<div class="form-row">
|
|
53
|
+
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime <span style="color:#9ca3af;font-weight:normal">(for picker)</span></label>
|
|
54
|
+
<input type="text" id="node-input-server">
|
|
55
|
+
</div>
|
|
56
|
+
<div class="form-row">
|
|
57
|
+
<label for="node-input-modpackqt-picker"><i class="fa fa-cloud-download"></i> My Profiles</label>
|
|
58
|
+
<select id="node-input-modpackqt-picker" style="width:65%"></select>
|
|
59
|
+
<button type="button" id="modpackqt-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 8px 105px">
|
|
62
|
+
Pick a saved Modbus profile from your account — fills host/port/unit below. Requires Account Key on the runtime config.
|
|
63
|
+
</div>
|
|
43
64
|
<div class="form-row">
|
|
44
65
|
<label for="node-input-targetHost"><i class="fa fa-plug"></i> Target Host</label>
|
|
45
66
|
<input type="text" id="node-input-targetHost" placeholder="192.168.1.10">
|
|
@@ -19,10 +19,55 @@
|
|
|
19
19
|
label: function () {
|
|
20
20
|
return this.name || `Master Read FC${this.functionCode} @${this.address}`;
|
|
21
21
|
},
|
|
22
|
-
paletteLabel: 'modbus master read'
|
|
22
|
+
paletteLabel: 'modbus master read',
|
|
23
|
+
oneditprepare: function () { ModPackQTMasterPicker.attach(this); }
|
|
23
24
|
});
|
|
24
25
|
</script>
|
|
25
26
|
|
|
27
|
+
<!-- Shared picker helper — loaded once per editor session -->
|
|
28
|
+
<script type="text/javascript">
|
|
29
|
+
if (!window.ModPackQTMasterPicker) {
|
|
30
|
+
window.ModPackQTMasterPicker = {
|
|
31
|
+
attach: function (node) {
|
|
32
|
+
const $sel = $('#node-input-modpackqt-picker');
|
|
33
|
+
if (!$sel.length) return;
|
|
34
|
+
const reload = function () {
|
|
35
|
+
const cfgId = $('#node-input-server').val();
|
|
36
|
+
$sel.empty().append('<option value="">— Loading… —</option>');
|
|
37
|
+
if (!cfgId || cfgId === '_ADD_') {
|
|
38
|
+
$sel.empty().append('<option value="">— Set runtime config first —</option>');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
$.getJSON('modpackqt/connections?config=' + encodeURIComponent(cfgId))
|
|
42
|
+
.done(function (rows) {
|
|
43
|
+
$sel.empty().append('<option value="">— Manual entry below —</option>');
|
|
44
|
+
(rows || [])
|
|
45
|
+
.filter(function (c) { return c && (c.connectionType === 'tcp' || !c.connectionType); })
|
|
46
|
+
.forEach(function (c) {
|
|
47
|
+
const label = (c.name || '(unnamed)') + ' — ' + (c.host || '?') + ':' + (c.port || 502) + ' #' + (c.unitId || 1);
|
|
48
|
+
$('<option>').val(c.id).text(label).data('row', c).appendTo($sel);
|
|
49
|
+
});
|
|
50
|
+
})
|
|
51
|
+
.fail(function (xhr) {
|
|
52
|
+
const msg = (xhr.responseJSON && xhr.responseJSON.error) || ('HTTP ' + xhr.status);
|
|
53
|
+
$sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
$('#node-input-server').on('change', reload);
|
|
57
|
+
$('#modpackqt-picker-refresh').on('click', function (e) { e.preventDefault(); reload(); });
|
|
58
|
+
$sel.on('change', function () {
|
|
59
|
+
const row = $(this).find('option:selected').data('row');
|
|
60
|
+
if (!row) return;
|
|
61
|
+
$('#node-input-targetHost').val(row.host || '');
|
|
62
|
+
$('#node-input-targetPort').val(row.port || 502);
|
|
63
|
+
$('#node-input-unitId').val(row.unitId || 1);
|
|
64
|
+
});
|
|
65
|
+
reload();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
26
71
|
<script type="text/html" data-template-name="modpackqt-master-read">
|
|
27
72
|
<div class="form-row">
|
|
28
73
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
@@ -32,6 +77,14 @@
|
|
|
32
77
|
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
|
|
33
78
|
<input type="text" id="node-input-server">
|
|
34
79
|
</div>
|
|
80
|
+
<div class="form-row">
|
|
81
|
+
<label for="node-input-modpackqt-picker"><i class="fa fa-cloud-download"></i> My Profiles</label>
|
|
82
|
+
<select id="node-input-modpackqt-picker" style="width:65%"></select>
|
|
83
|
+
<button type="button" id="modpackqt-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 8px 105px">
|
|
86
|
+
Pick a saved Modbus profile from your account — fills host/port/unit below. Requires Account Key on the runtime config.
|
|
87
|
+
</div>
|
|
35
88
|
<div class="form-row">
|
|
36
89
|
<label for="node-input-targetHost"><i class="fa fa-plug"></i> Target Host</label>
|
|
37
90
|
<input type="text" id="node-input-targetHost" placeholder="Modbus device IP/hostname (TCP only)">
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
label: function () {
|
|
18
18
|
return this.name || `Master Write FC${this.functionCode} @${this.address}`;
|
|
19
19
|
},
|
|
20
|
-
paletteLabel: 'modbus master write'
|
|
20
|
+
paletteLabel: 'modbus master write',
|
|
21
|
+
oneditprepare: function () { if (window.ModPackQTMasterPicker) ModPackQTMasterPicker.attach(this); }
|
|
21
22
|
});
|
|
22
23
|
</script>
|
|
23
24
|
|
|
@@ -30,6 +31,14 @@
|
|
|
30
31
|
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
|
|
31
32
|
<input type="text" id="node-input-server">
|
|
32
33
|
</div>
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<label for="node-input-modpackqt-picker"><i class="fa fa-cloud-download"></i> My Profiles</label>
|
|
36
|
+
<select id="node-input-modpackqt-picker" style="width:65%"></select>
|
|
37
|
+
<button type="button" id="modpackqt-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 8px 105px">
|
|
40
|
+
Pick a saved Modbus profile from your account — fills host/port/unit below. Requires Account Key on the runtime config.
|
|
41
|
+
</div>
|
|
33
42
|
<div class="form-row">
|
|
34
43
|
<label for="node-input-targetHost"><i class="fa fa-plug"></i> Target Host</label>
|
|
35
44
|
<input type="text" id="node-input-targetHost" placeholder="Modbus device IP/hostname (TCP only)">
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
color: '#7c3aed',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: '' },
|
|
7
|
+
server: { value: '', type: 'modpackqt-config' },
|
|
7
8
|
bindHost: { value: '0.0.0.0' },
|
|
8
9
|
bindPort: { value: 1502, required: true, validate: RED.validators.number() },
|
|
9
10
|
unitId: { value: 1, required: true, validate: RED.validators.number() }
|
|
@@ -17,28 +18,89 @@
|
|
|
17
18
|
paletteLabel: 'modbus slave probe',
|
|
18
19
|
oneditprepare: function () {
|
|
19
20
|
const node = this;
|
|
21
|
+
// Optional cloud slave-config picker (fills bindPort/unitId)
|
|
22
|
+
if (window.ModPackQTSlavePicker) ModPackQTSlavePicker.attach(this);
|
|
20
23
|
const buildLink = (info) => {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
+ '
|
|
28
|
-
+ '&
|
|
24
|
+
const probeHost = (info && info.host) || '127.0.0.1';
|
|
25
|
+
const probePort = (info && info.port) || 8502;
|
|
26
|
+
const bindPort = $('#node-input-bindPort').val() || node.bindPort || 1502;
|
|
27
|
+
const unitId = $('#node-input-unitId').val() || node.unitId || 1;
|
|
28
|
+
const name = $('#node-input-name').val() || node.name || '';
|
|
29
|
+
const url = 'https://modpackqt.com/slave'
|
|
30
|
+
+ '?probe=' + encodeURIComponent(node.id)
|
|
31
|
+
+ '&probeHost=' + encodeURIComponent(probeHost)
|
|
32
|
+
+ '&probePort=' + probePort
|
|
33
|
+
+ '&port=' + bindPort
|
|
34
|
+
+ '&unitId=' + unitId
|
|
35
|
+
+ (name ? '&name=' + encodeURIComponent(name) : '');
|
|
29
36
|
$('#modpackqt-slave-probe-link').attr('href', url);
|
|
30
|
-
$('#modpackqt-slave-probe-runtime').text(
|
|
37
|
+
$('#modpackqt-slave-probe-runtime').text(probeHost + ':' + probePort);
|
|
31
38
|
};
|
|
32
39
|
$.getJSON('modpackqt-probe/info').done(buildLink).fail(() => buildLink({}));
|
|
33
40
|
}
|
|
34
41
|
});
|
|
35
42
|
</script>
|
|
36
43
|
|
|
44
|
+
<!-- Shared slave-picker helper — loaded once per editor session -->
|
|
45
|
+
<script type="text/javascript">
|
|
46
|
+
if (!window.ModPackQTSlavePicker) {
|
|
47
|
+
window.ModPackQTSlavePicker = {
|
|
48
|
+
attach: function (node) {
|
|
49
|
+
const $sel = $('#node-input-modpackqt-slave-picker');
|
|
50
|
+
if (!$sel.length) return;
|
|
51
|
+
const reload = function () {
|
|
52
|
+
const cfgId = $('#node-input-server').val();
|
|
53
|
+
$sel.empty().append('<option value="">— Loading… —</option>');
|
|
54
|
+
if (!cfgId || cfgId === '_ADD_') {
|
|
55
|
+
$sel.empty().append('<option value="">— Set runtime config first —</option>');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
$.getJSON('modpackqt/slaves?config=' + encodeURIComponent(cfgId))
|
|
59
|
+
.done(function (rows) {
|
|
60
|
+
$sel.empty().append('<option value="">— Manual entry below —</option>');
|
|
61
|
+
(rows || [])
|
|
62
|
+
.filter(function (s) { return s && (s.protocol === 'tcp' || !s.protocol); })
|
|
63
|
+
.forEach(function (s) {
|
|
64
|
+
const label = (s.name || '(unnamed)') + ' — :' + (s.port || 1502) + ' #' + (s.unitId || 1);
|
|
65
|
+
$('<option>').val(s.id).text(label).data('row', s).appendTo($sel);
|
|
66
|
+
});
|
|
67
|
+
})
|
|
68
|
+
.fail(function (xhr) {
|
|
69
|
+
const msg = (xhr.responseJSON && xhr.responseJSON.error) || ('HTTP ' + xhr.status);
|
|
70
|
+
$sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
$('#node-input-server').on('change', reload);
|
|
74
|
+
$('#modpackqt-slave-picker-refresh').on('click', function (e) { e.preventDefault(); reload(); });
|
|
75
|
+
$sel.on('change', function () {
|
|
76
|
+
const row = $(this).find('option:selected').data('row');
|
|
77
|
+
if (!row) return;
|
|
78
|
+
$('#node-input-bindPort').val(row.port || 1502);
|
|
79
|
+
$('#node-input-unitId').val(row.unitId || 1);
|
|
80
|
+
});
|
|
81
|
+
reload();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
37
87
|
<script type="text/html" data-template-name="modpackqt-slave-probe">
|
|
38
88
|
<div class="form-row">
|
|
39
89
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
40
90
|
<input type="text" id="node-input-name" placeholder="(optional, e.g. Fake Inverter)">
|
|
41
91
|
</div>
|
|
92
|
+
<div class="form-row">
|
|
93
|
+
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime <span style="color:#9ca3af;font-weight:normal">(for picker)</span></label>
|
|
94
|
+
<input type="text" id="node-input-server">
|
|
95
|
+
</div>
|
|
96
|
+
<div class="form-row">
|
|
97
|
+
<label for="node-input-modpackqt-slave-picker"><i class="fa fa-cloud-download"></i> My Slaves</label>
|
|
98
|
+
<select id="node-input-modpackqt-slave-picker" style="width:65%"></select>
|
|
99
|
+
<button type="button" id="modpackqt-slave-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 8px 105px">
|
|
102
|
+
Pick a saved slave config from your account — fills port/unit below. Requires Account Key on the runtime config.
|
|
103
|
+
</div>
|
|
42
104
|
<div class="form-row">
|
|
43
105
|
<label for="node-input-bindHost"><i class="fa fa-globe"></i> Bind Host</label>
|
|
44
106
|
<input type="text" id="node-input-bindHost" placeholder="0.0.0.0 (all interfaces)">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-modbus-modpackqt",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
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",
|