node-red-contrib-modbus-modpackqt 3.1.1 → 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 CHANGED
@@ -4,6 +4,33 @@ 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
+
7
34
  ## [3.1.1] — 2026-05-10
8
35
 
9
36
  ### Fixed
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.1.1';
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">API Key (optional — reserved for future cloud features)</h4>
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> API Key</label>
108
- <input type="password" id="node-config-input-apiKey" placeholder="Optional — leave blank for local-only use">
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 &amp; 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>API key (optional)</h3>
151
+ <h3>ModPackQT Account Key (optional)</h3>
147
152
  <p>
148
- Reserved for future cloud features (profile sync, remote console). Leave blank
149
- for local-only use all Modbus functionality works without a key.
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 &amp; 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,6 +19,8 @@
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
25
  const probeHost = (info && info.host) || '127.0.0.1';
23
26
  const probePort = (info && info.port) || 8502;
@@ -46,6 +49,18 @@
46
49
  <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
47
50
  <input type="text" id="node-input-name" placeholder="(optional, e.g. Inverter A)">
48
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>
49
64
  <div class="form-row">
50
65
  <label for="node-input-targetHost"><i class="fa fa-plug"></i> Target Host</label>
51
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,6 +18,8 @@
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
24
  const probeHost = (info && info.host) || '127.0.0.1';
22
25
  const probePort = (info && info.port) || 8502;
@@ -38,11 +41,66 @@
38
41
  });
39
42
  </script>
40
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
+
41
87
  <script type="text/html" data-template-name="modpackqt-slave-probe">
42
88
  <div class="form-row">
43
89
  <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
44
90
  <input type="text" id="node-input-name" placeholder="(optional, e.g. Fake Inverter)">
45
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>
46
104
  <div class="form-row">
47
105
  <label for="node-input-bindHost"><i class="fa fa-globe"></i> Bind Host</label>
48
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.1.1",
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",