node-red-contrib-modbus-modpackqt 3.3.0 → 3.3.2
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 +23 -0
- package/nodes/lib/probe-runtime.js +1 -1
- package/nodes/modpackqt-config.html +2 -78
- package/nodes/modpackqt-config.js +7 -11
- package/nodes/modpackqt-master-probe.html +18 -1
- package/nodes/modpackqt-master-probe.js +3 -1
- package/nodes/modpackqt-slave-probe.html +26 -9
- package/nodes/modpackqt-slave-probe.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.2] — 2026-05-10
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Account Key moved to probe nodes.** It is now a field on
|
|
12
|
+
`modpackqt-master-probe` and `modpackqt-slave-probe` — the only
|
|
13
|
+
nodes that talk to modpackqt.com. The runtime config (`modpackqt-config`)
|
|
14
|
+
is now purely local device settings with no cloud fields at all.
|
|
15
|
+
- **My Profiles picker removed from runtime config.** The slave-probe's
|
|
16
|
+
**My Slaves** picker now reads the key from its own node credential
|
|
17
|
+
(`?probe=<nodeId>`) instead of the runtime config.
|
|
18
|
+
- Runtime config dialog is now clean: Name, Device, Transport, and the
|
|
19
|
+
optional embedded slave server section. Nothing cloud-related.
|
|
20
|
+
|
|
21
|
+
## [3.3.1] — 2026-05-10
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Account Key section is now collapsed by default** in the runtime config
|
|
26
|
+
dialog. It's only relevant for probe-node deep-linking and the optional
|
|
27
|
+
cloud profile picker — master-read / write and slave-read / write users
|
|
28
|
+
never need it. Auto-expands if a key is already saved.
|
|
29
|
+
|
|
7
30
|
## [3.3.0] — 2026-05-10
|
|
8
31
|
|
|
9
32
|
### Changed — Runtime config = one device
|
|
@@ -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.2';
|
|
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;
|
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
category: 'config',
|
|
4
4
|
defaults: {
|
|
5
5
|
name: { value: 'ModPackQT Device' },
|
|
6
|
-
// ── Device target (NEW in v3.3.0) — one runtime = one Modbus device.
|
|
7
|
-
// Master/probe nodes inherit these fields from the runtime config.
|
|
8
6
|
targetHost: { value: 'localhost' },
|
|
9
7
|
targetPort: { value: 502, validate: RED.validators.number() },
|
|
10
8
|
unitId: { value: 1, validate: RED.validators.number() },
|
|
11
|
-
// ── Master transport
|
|
12
9
|
masterMode: { value: 'tcp', required: true },
|
|
13
10
|
serialPort: { value: '' },
|
|
14
11
|
baudRate: { value: 9600 },
|
|
@@ -16,14 +13,10 @@
|
|
|
16
13
|
dataBits: { value: 8 },
|
|
17
14
|
stopBits: { value: 1 },
|
|
18
15
|
timeoutMs: { value: 3000, validate: RED.validators.number() },
|
|
19
|
-
// ── Optional embedded Modbus TCP slave server
|
|
20
16
|
slaveEnabled: { value: false },
|
|
21
17
|
slaveHost: { value: '0.0.0.0' },
|
|
22
18
|
slavePort: { value: 1502, validate: RED.validators.number() }
|
|
23
19
|
},
|
|
24
|
-
credentials: {
|
|
25
|
-
apiKey: { type: 'password' }
|
|
26
|
-
},
|
|
27
20
|
label: function () {
|
|
28
21
|
const t = this.masterMode === 'rtu'
|
|
29
22
|
? `RTU ${this.serialPort || '?'} #${this.unitId || 1}`
|
|
@@ -32,8 +25,6 @@
|
|
|
32
25
|
return this.name && this.name !== 'ModPackQT Device' ? `${this.name} (${t})` : `${t}${s}`;
|
|
33
26
|
},
|
|
34
27
|
oneditprepare: function () {
|
|
35
|
-
const node = this;
|
|
36
|
-
|
|
37
28
|
const toggleRtu = () => {
|
|
38
29
|
const isRtu = $('#node-config-input-masterMode').val() === 'rtu';
|
|
39
30
|
$('.modpackqt-rtu-only').toggle(isRtu);
|
|
@@ -48,47 +39,6 @@
|
|
|
48
39
|
toggleRtu();
|
|
49
40
|
toggleSlave();
|
|
50
41
|
|
|
51
|
-
// ── My Profiles picker — auto-fills targetHost/targetPort/unitId
|
|
52
|
-
const $sel = $('#node-config-modpackqt-picker');
|
|
53
|
-
const reload = function () {
|
|
54
|
-
$sel.empty().append('<option value="">— Loading… —</option>');
|
|
55
|
-
const key = $('#node-config-input-apiKey').val();
|
|
56
|
-
// The admin proxy needs the saved config ID. On a brand-new config the
|
|
57
|
-
// user hasn't saved yet — show a friendly message instead.
|
|
58
|
-
if (!node.id) {
|
|
59
|
-
$sel.empty().append('<option value="">— Save the config first, then reopen to load profiles —</option>');
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (!key) {
|
|
63
|
-
$sel.empty().append('<option value="">— Paste your Account Key below to load profiles —</option>');
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
$.getJSON('modpackqt/connections?config=' + encodeURIComponent(node.id))
|
|
67
|
-
.done(function (rows) {
|
|
68
|
-
$sel.empty().append('<option value="">— Manual entry —</option>');
|
|
69
|
-
(rows || [])
|
|
70
|
-
.filter(function (c) { return c && (c.connectionType === 'tcp' || !c.connectionType); })
|
|
71
|
-
.forEach(function (c) {
|
|
72
|
-
const label = (c.name || '(unnamed)') + ' — ' + (c.host || '?') + ':' + (c.port || 502) + ' #' + (c.unitId || 1);
|
|
73
|
-
$('<option>').val(c.id).text(label).data('row', c).appendTo($sel);
|
|
74
|
-
});
|
|
75
|
-
})
|
|
76
|
-
.fail(function (xhr) {
|
|
77
|
-
const msg = (xhr.responseJSON && xhr.responseJSON.error) || ('HTTP ' + xhr.status);
|
|
78
|
-
$sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
$('#modpackqt-config-picker-refresh').on('click', function (e) { e.preventDefault(); reload(); });
|
|
82
|
-
$sel.on('change', function () {
|
|
83
|
-
const row = $(this).find('option:selected').data('row');
|
|
84
|
-
if (!row) return;
|
|
85
|
-
if (!node.name || node.name === 'ModPackQT Device') $('#node-config-input-name').val(row.name || '');
|
|
86
|
-
$('#node-config-input-targetHost').val(row.host || '');
|
|
87
|
-
$('#node-config-input-targetPort').val(row.port || 502);
|
|
88
|
-
$('#node-config-input-unitId').val(row.unitId || 1);
|
|
89
|
-
});
|
|
90
|
-
reload();
|
|
91
|
-
|
|
92
42
|
// ── Embedded slave server section (collapsed by default)
|
|
93
43
|
const $slaveSection = $('.modpackqt-slave-section');
|
|
94
44
|
const $slaveToggle = $('.modpackqt-slave-section-toggle');
|
|
@@ -114,14 +64,6 @@
|
|
|
114
64
|
</div>
|
|
115
65
|
|
|
116
66
|
<h4 style="margin-top:18px">Device</h4>
|
|
117
|
-
<div class="form-row">
|
|
118
|
-
<label for="node-config-modpackqt-picker"><i class="fa fa-cloud-download"></i> My Profiles</label>
|
|
119
|
-
<select id="node-config-modpackqt-picker" style="width:65%"></select>
|
|
120
|
-
<button type="button" id="modpackqt-config-picker-refresh" class="red-ui-button" title="Reload from modpackqt.com" style="margin-left:4px"><i class="fa fa-refresh"></i></button>
|
|
121
|
-
</div>
|
|
122
|
-
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
123
|
-
Picks a saved Modbus profile from your modpackqt.com account — auto-fills the fields below.
|
|
124
|
-
</div>
|
|
125
67
|
<div class="form-row modpackqt-tcp-only">
|
|
126
68
|
<label for="node-config-input-targetHost"><i class="fa fa-plug"></i> Host</label>
|
|
127
69
|
<input type="text" id="node-config-input-targetHost" placeholder="Modbus device IP/hostname">
|
|
@@ -176,16 +118,6 @@
|
|
|
176
118
|
<input type="number" id="node-config-input-timeoutMs" placeholder="3000">
|
|
177
119
|
</div>
|
|
178
120
|
|
|
179
|
-
<h4 style="margin-top:18px">ModPackQT Account Key (optional — for cloud profile sync)</h4>
|
|
180
|
-
<div class="form-row">
|
|
181
|
-
<label for="node-config-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
|
|
182
|
-
<input type="password" id="node-config-input-apiKey" placeholder="Optional — paste your tunnel key for the profile picker">
|
|
183
|
-
</div>
|
|
184
|
-
<div class="form-tips" style="font-size:11px;color:#6b7280;margin-top:-6px">
|
|
185
|
-
Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
186
|
-
All Modbus traffic stays local — the key is only used for read-only profile sync.
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
121
|
<div class="form-row" style="margin:14px 0 4px 0;border-top:1px solid #e5e7eb;padding-top:10px">
|
|
190
122
|
<a href="#" class="modpackqt-slave-section-toggle" style="font-size:12px;color:#6b7280;text-decoration:none;cursor:pointer">▸ Embedded slave server (only for modpackqt-slave-read / -write nodes)</a>
|
|
191
123
|
</div>
|
|
@@ -230,8 +162,8 @@
|
|
|
230
162
|
<h3>One runtime = one device</h3>
|
|
231
163
|
<p>
|
|
232
164
|
For each remote Modbus device you want to read/write, create a separate runtime config.
|
|
233
|
-
Each carries its own Host / Port / Unit ID
|
|
234
|
-
|
|
165
|
+
Each carries its own Host / Port / Unit ID. Read and write nodes inherit those — they
|
|
166
|
+
only need to know the function code, address, and quantity.
|
|
235
167
|
</p>
|
|
236
168
|
|
|
237
169
|
<h3>Master mode</h3>
|
|
@@ -246,12 +178,4 @@
|
|
|
246
178
|
<code>1502</code>). Use the <code>modpackqt-slave-write</code> node to push values into
|
|
247
179
|
its register store; external Modbus masters read whatever you last wrote.
|
|
248
180
|
</p>
|
|
249
|
-
|
|
250
|
-
<h3>ModPackQT Account Key (optional)</h3>
|
|
251
|
-
<p>
|
|
252
|
-
Same tunnel key the Electron gateway uses — generate one at
|
|
253
|
-
<a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
254
|
-
With a key set, the <b>My Profiles</b> picker above auto-fills Host / Port / Unit from
|
|
255
|
-
your saved Modbus profiles.
|
|
256
|
-
</p>
|
|
257
181
|
</script>
|
|
@@ -19,12 +19,12 @@ module.exports = function (RED) {
|
|
|
19
19
|
RED._modpackqtAdminRoutesRegistered = true;
|
|
20
20
|
function makeProxy(fetchFn) {
|
|
21
21
|
return function (req, res) {
|
|
22
|
-
const
|
|
23
|
-
if (!
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
26
|
-
const key =
|
|
27
|
-
if (!key) return res.status(400).json({ error: 'No Account Key set on this
|
|
22
|
+
const probeId = req.query && req.query.probe;
|
|
23
|
+
if (!probeId) return res.status(400).json({ error: 'probe query param required' });
|
|
24
|
+
const probe = RED.nodes.getNode(probeId);
|
|
25
|
+
if (!probe) return res.status(404).json({ error: 'Probe node not found (deploy first?)' });
|
|
26
|
+
const key = probe.credentials && probe.credentials.apiKey;
|
|
27
|
+
if (!key) return res.status(400).json({ error: 'No Account Key set on this probe node.' });
|
|
28
28
|
fetchFn(key, function (err, data) {
|
|
29
29
|
if (err) return res.status(502).json({ error: err.message });
|
|
30
30
|
res.json(data);
|
|
@@ -64,8 +64,6 @@ module.exports = function (RED) {
|
|
|
64
64
|
node.slavePort = parseInt(config.slavePort, 10) || 1502;
|
|
65
65
|
node.slaveHost = config.slaveHost || '0.0.0.0';
|
|
66
66
|
|
|
67
|
-
node.apiKey = (node.credentials && node.credentials.apiKey) || '';
|
|
68
|
-
|
|
69
67
|
const today = () => new Date().toISOString().slice(0, 10);
|
|
70
68
|
node._opsDay = today();
|
|
71
69
|
node._opsCount = 0;
|
|
@@ -337,7 +335,5 @@ module.exports = function (RED) {
|
|
|
337
335
|
});
|
|
338
336
|
}
|
|
339
337
|
|
|
340
|
-
RED.nodes.registerType('modpackqt-config', ModPackQTConfigNode
|
|
341
|
-
credentials: { apiKey: { type: 'password' } }
|
|
342
|
-
});
|
|
338
|
+
RED.nodes.registerType('modpackqt-config', ModPackQTConfigNode);
|
|
343
339
|
};
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
unitId: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } },
|
|
12
12
|
timeoutMs: { value: 0, validate: function (v) { return v === '' || v === 0 || RED.validators.number()(v); } }
|
|
13
13
|
},
|
|
14
|
+
credentials: {
|
|
15
|
+
apiKey: { type: 'password' }
|
|
16
|
+
},
|
|
14
17
|
inputs: 0,
|
|
15
18
|
outputs: 0,
|
|
16
19
|
icon: 'font-awesome/fa-rocket',
|
|
@@ -57,7 +60,16 @@
|
|
|
57
60
|
<input type="text" id="node-input-server">
|
|
58
61
|
</div>
|
|
59
62
|
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
60
|
-
The runtime config carries the device target — the probe deep-links the web tester to it.
|
|
63
|
+
The runtime config carries the device target — the probe deep-links the web tester to it.
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="form-row">
|
|
67
|
+
<label for="node-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
|
|
68
|
+
<input type="password" id="node-input-apiKey" placeholder="Paste your ModPackQT tunnel key">
|
|
69
|
+
</div>
|
|
70
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
71
|
+
Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
72
|
+
Used only by the modpackqt.com web console to sync with this probe.
|
|
61
73
|
</div>
|
|
62
74
|
|
|
63
75
|
<input type="hidden" id="node-input-targetHost">
|
|
@@ -87,6 +99,11 @@
|
|
|
87
99
|
chosen runtime config. Click <b>Open in ModPackQT Console</b> to launch
|
|
88
100
|
the web tester live-attached to that device.</p>
|
|
89
101
|
|
|
102
|
+
<h3>Account Key</h3>
|
|
103
|
+
<p>Paste your ModPackQT tunnel key here — generate one at
|
|
104
|
+
<a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
105
|
+
The key lets the web console authenticate to this probe. All Modbus traffic stays local.</p>
|
|
106
|
+
|
|
90
107
|
<h3>Multiple devices</h3>
|
|
91
108
|
<p>Create one runtime config per device, then drop one master-probe per
|
|
92
109
|
runtime. The web console aggregates all probe nodes from this Node-RED
|
|
@@ -112,5 +112,7 @@ module.exports = function (RED) {
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
RED.nodes.registerType('modpackqt-master-probe', MasterProbeNode
|
|
115
|
+
RED.nodes.registerType('modpackqt-master-probe', MasterProbeNode, {
|
|
116
|
+
credentials: { apiKey: { type: 'password' } }
|
|
117
|
+
});
|
|
116
118
|
};
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
bindPort: { value: 1502, validate: RED.validators.number() },
|
|
10
10
|
unitId: { value: 1, validate: RED.validators.number() }
|
|
11
11
|
},
|
|
12
|
+
credentials: {
|
|
13
|
+
apiKey: { type: 'password' }
|
|
14
|
+
},
|
|
12
15
|
inputs: 0,
|
|
13
16
|
outputs: 0,
|
|
14
17
|
icon: 'font-awesome/fa-server',
|
|
@@ -50,13 +53,12 @@
|
|
|
50
53
|
const $sel = $('#node-input-modpackqt-slave-picker');
|
|
51
54
|
if (!$sel.length) return;
|
|
52
55
|
const reload = function () {
|
|
53
|
-
const cfgId = $('#node-input-server').val();
|
|
54
56
|
$sel.empty().append('<option value="">— Loading… —</option>');
|
|
55
|
-
if (!
|
|
56
|
-
$sel.empty().append('<option value="">—
|
|
57
|
+
if (!node.id) {
|
|
58
|
+
$sel.empty().append('<option value="">— Save the node first, then reopen to load slaves —</option>');
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
59
|
-
$.getJSON('modpackqt/slaves?
|
|
61
|
+
$.getJSON('modpackqt/slaves?probe=' + encodeURIComponent(node.id))
|
|
60
62
|
.done(function (rows) {
|
|
61
63
|
$sel.empty().append('<option value="">— None —</option>');
|
|
62
64
|
(rows || [])
|
|
@@ -71,7 +73,6 @@
|
|
|
71
73
|
$sel.empty().append($('<option>').val('').text('— ' + msg + ' —'));
|
|
72
74
|
});
|
|
73
75
|
};
|
|
74
|
-
$('#node-input-server').on('change', reload);
|
|
75
76
|
$('#modpackqt-slave-picker-refresh').on('click', function (e) { e.preventDefault(); reload(); });
|
|
76
77
|
$sel.on('change', function () {
|
|
77
78
|
const row = $(this).find('option:selected').data('row');
|
|
@@ -94,13 +95,23 @@
|
|
|
94
95
|
<label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
|
|
95
96
|
<input type="text" id="node-input-server">
|
|
96
97
|
</div>
|
|
98
|
+
|
|
99
|
+
<div class="form-row">
|
|
100
|
+
<label for="node-input-apiKey"><i class="fa fa-key"></i> Account Key</label>
|
|
101
|
+
<input type="password" id="node-input-apiKey" placeholder="Paste your ModPackQT tunnel key">
|
|
102
|
+
</div>
|
|
103
|
+
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
104
|
+
Generate at <a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
105
|
+
Used only by the modpackqt.com web console to sync with this probe.
|
|
106
|
+
</div>
|
|
107
|
+
|
|
97
108
|
<div class="form-row">
|
|
98
109
|
<label for="node-input-modpackqt-slave-picker"><i class="fa fa-cloud-download"></i> My Slaves</label>
|
|
99
110
|
<select id="node-input-modpackqt-slave-picker" style="width:65%"></select>
|
|
100
111
|
<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>
|
|
101
112
|
</div>
|
|
102
113
|
<div class="form-tips" style="font-size:11px;color:#6b7280;margin:-8px 0 12px 105px">
|
|
103
|
-
Pick a saved slave config from your modpackqt.com account —
|
|
114
|
+
Pick a saved slave config from your modpackqt.com account — auto-fills the port and unit below.
|
|
104
115
|
</div>
|
|
105
116
|
|
|
106
117
|
<!-- Hidden state — populated by the picker, used to build the probe URL. -->
|
|
@@ -127,9 +138,15 @@
|
|
|
127
138
|
</script>
|
|
128
139
|
|
|
129
140
|
<script type="text/html" data-help-name="modpackqt-slave-probe">
|
|
130
|
-
<p>Live simulator probe for a single Modbus TCP slave.
|
|
131
|
-
config from
|
|
132
|
-
Console</b> to launch the web register editor.</p>
|
|
141
|
+
<p>Live simulator probe for a single Modbus TCP slave. Set your Account Key,
|
|
142
|
+
optionally pick a saved slave config from modpackqt.com, then click
|
|
143
|
+
<b>Open in ModPackQT Console</b> to launch the web register editor.</p>
|
|
144
|
+
|
|
145
|
+
<h3>Account Key</h3>
|
|
146
|
+
<p>Paste your ModPackQT tunnel key here — generate one at
|
|
147
|
+
<a href="https://modpackqt.com/settings" target="_blank" rel="noopener">modpackqt.com → Settings → Cloud Gateways</a>.
|
|
148
|
+
The key lets the web console authenticate to this probe and loads your saved slave configs
|
|
149
|
+
in the <b>My Slaves</b> picker. All Modbus traffic stays local.</p>
|
|
133
150
|
|
|
134
151
|
<h3>How it works</h3>
|
|
135
152
|
<p>Each slave-probe owns a 65 536-register store per type (coils, discrete,
|
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.2",
|
|
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",
|