node-red-contrib-modbus-modpackqt 1.1.84 → 2.0.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.
@@ -1,57 +1,80 @@
1
- module.exports = function(RED) {
1
+ /**
2
+ * ModPackQT Master Write — embedded Modbus TCP/RTU write.
3
+ * Accepts raw register values. Encode multi-register values upstream
4
+ * with node-red-contrib-bytes-modpackqt if needed.
5
+ */
6
+ module.exports = function (RED) {
2
7
  function ModPackQTMasterWriteNode(config) {
3
8
  RED.nodes.createNode(this, config);
4
9
  const node = this;
5
10
  const server = RED.nodes.getNode(config.server);
6
11
 
7
12
  node.targetHost = config.targetHost || 'localhost';
8
- node.targetPort = parseInt(config.targetPort) || 502;
9
- node.unitId = parseInt(config.unitId) || 1;
10
- node.functionCode = parseInt(config.functionCode) || 6;
11
- node.address = parseInt(config.address) || 0;
13
+ node.targetPort = parseInt(config.targetPort, 10) || 502;
14
+ node.unitId = parseInt(config.unitId, 10) || 1;
15
+ node.functionCode = parseInt(config.functionCode, 10) || 6;
16
+ node.address = parseInt(config.address, 10) || 0;
12
17
 
13
- const FC_TO_REGISTER_TYPE = { 5: 'coil', 6: 'holding', 15: 'coil', 16: 'holding' };
14
-
15
- node.on('input', async function(msg) {
18
+ node.on('input', async function (msg) {
16
19
  if (!server) {
17
20
  node.status({ fill: 'red', shape: 'ring', text: 'no config' });
18
21
  return;
19
22
  }
20
23
  try {
21
- // Accept string JSON (e.g. "[100,200,300]" from inject string type),
22
- // plain number, or actual array/number from inject JSON type.
23
24
  let raw = msg.payload;
24
25
  if (typeof raw === 'string') {
25
26
  try { raw = JSON.parse(raw); } catch (_) { raw = Number(raw); }
26
27
  }
28
+ // Booleans accepted for FC5/FC15 (coils) — pass through; otherwise require integers.
29
+ const isCoilWrite = (node.functionCode === 5 || node.functionCode === 15);
30
+ const arr = Array.isArray(raw) ? raw : [raw];
31
+ const values = arr.map((v) => {
32
+ if (isCoilWrite) return v ? 1 : 0;
33
+ const n = Number(v);
34
+ if (!Number.isFinite(n)) {
35
+ throw new Error(
36
+ `Invalid value: "${v}". Modbus registers must be integers in range 0..65535. ` +
37
+ `If you're trying to write a float or 32-bit value, encode it first with the bytes palette ` +
38
+ `(e.g. encode-float32 → modbus-master-write FC16).`
39
+ );
40
+ }
41
+ if (!Number.isInteger(n)) {
42
+ throw new Error(
43
+ `Cannot write non-integer ${n} as a Modbus register. ` +
44
+ `Use encode-float32 / encode-int32 from the bytes palette to convert it to register values first.`
45
+ );
46
+ }
47
+ if (n < -32768 || n > 65535) {
48
+ throw new Error(
49
+ `Value ${n} out of 16-bit register range (-32768..65535). ` +
50
+ `For larger numbers use encode-int32 / encode-uint32 from the bytes palette.`
51
+ );
52
+ }
53
+ // Normalize signed → unsigned 16-bit for the wire
54
+ return n < 0 ? (n + 0x10000) : (n & 0xFFFF);
55
+ });
27
56
 
28
- // Always convert every value to an integer to avoid byte-encoding corruption.
29
- const values = Array.isArray(raw)
30
- ? raw.map(v => parseInt(v, 10))
31
- : [parseInt(raw, 10)];
32
-
33
- if (values.some(v => isNaN(v))) {
34
- throw new Error(`Invalid payload — expected number or array of numbers, got: ${msg.payload}`);
35
- }
36
-
37
- const registerType = FC_TO_REGISTER_TYPE[node.functionCode] || 'holding';
38
- node.status({ fill: 'blue', shape: 'ring', text: `writing ${values.length} value(s)…` });
39
-
40
- await server.request('POST', '/api/modbus/tcp/write', {
57
+ node.status({ fill: 'blue', shape: 'ring', text: `writing ${values.length}…` });
58
+ await server.write({
41
59
  host: node.targetHost,
42
60
  port: node.targetPort,
43
61
  unitId: node.unitId,
44
- registerType,
62
+ functionCode: node.functionCode,
45
63
  address: node.address,
46
64
  values
47
65
  });
48
66
 
49
- node.status({ fill: 'green', shape: 'dot', text: `wrote ${values.length} @ addr ${node.address}` });
67
+ const ts = new Date().toLocaleTimeString();
68
+ const ops = server.opsToday();
69
+ node.status({
70
+ fill: 'green', shape: 'dot',
71
+ text: server.brandStatus(`wrote ${values.length} @${node.address} · ${ts} · ${ops} ops`)
72
+ });
50
73
  msg.success = true;
51
74
  msg.valuesWritten = values;
52
75
  node.send(msg);
53
76
  } catch (err) {
54
- node.status({ fill: 'red', shape: 'dot', text: err.message });
77
+ node.status({ fill: 'red', shape: 'dot', text: err.message.slice(0, 60) });
55
78
  node.error(err.message, msg);
56
79
  }
57
80
  });
@@ -1,11 +1,10 @@
1
1
  <script type="text/javascript">
2
2
  RED.nodes.registerType('modpackqt-slave-read', {
3
3
  category: 'ModPackQT',
4
- color: '#7c3aed',
4
+ color: '#15803d',
5
5
  defaults: {
6
6
  name: { value: '' },
7
7
  server: { value: '', type: 'modpackqt-config', required: true },
8
- slaveId: { value: '' },
9
8
  registerType: { value: 'holding', required: true },
10
9
  address: { value: 0, required: true, validate: RED.validators.number() },
11
10
  quantity: { value: 1, required: true, validate: RED.validators.number() },
@@ -14,10 +13,10 @@
14
13
  inputs: 1,
15
14
  outputs: 1,
16
15
  icon: 'font-awesome/fa-server',
17
- label: function() {
16
+ label: function () {
18
17
  return this.name || ('Slave Read ' + (this.registerType || 'holding') + ' @' + this.address);
19
18
  },
20
- paletteLabel: 'slave read'
19
+ paletteLabel: 'modbus slave read'
21
20
  });
22
21
  </script>
23
22
 
@@ -27,18 +26,14 @@
27
26
  <input type="text" id="node-input-name" placeholder="Name">
28
27
  </div>
29
28
  <div class="form-row">
30
- <label for="node-input-server"><i class="fa fa-cog"></i> Gateway</label>
29
+ <label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
31
30
  <input type="text" id="node-input-server">
32
31
  </div>
33
- <div class="form-row">
34
- <label for="node-input-slaveId"><i class="fa fa-id-card"></i> Slave ID</label>
35
- <input type="text" id="node-input-slaveId" placeholder="e.g. 42 (or leave blank, set via msg.slaveId)">
36
- </div>
37
32
  <div class="form-row">
38
33
  <label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
39
34
  <select id="node-input-registerType">
40
35
  <option value="holding">Holding Registers</option>
41
- <option value="coil">Coils</option>
36
+ <option value="coils">Coils</option>
42
37
  <option value="input">Input Registers</option>
43
38
  <option value="discrete">Discrete Inputs</option>
44
39
  </select>
@@ -56,89 +51,21 @@
56
51
  <input type="number" id="node-input-pollInterval" min="0" placeholder="0 = trigger only">
57
52
  </div>
58
53
  <div class="form-tips">
59
- <b>Tip:</b> Override at runtime via <code>msg.slaveId</code>, <code>msg.address</code>,
54
+ <b>Reads from the local slave register store.</b> Enable the embedded slave server
55
+ in the runtime config first. Override at runtime via <code>msg.address</code>,
60
56
  <code>msg.quantity</code>, or <code>msg.registerType</code>.
61
57
  </div>
62
58
  </script>
63
59
 
64
60
  <script type="text/html" data-help-name="modpackqt-slave-read">
65
61
  <p>
66
- Reads the current register values from a <b>ModPackQT Slave</b>. Use this to monitor or
67
- verify what your slave is currently serving to external Modbus masters.
62
+ Reads from the embedded slave's local register store. Useful for verifying what
63
+ external Modbus masters connecting to your slave port will see.
68
64
  </p>
69
-
70
- <h3>Quick Setup — 4 steps</h3>
71
- <ol>
72
- <li>
73
- <b>Create a free account</b> at
74
- <a href="https://modpackqt.com" target="_blank">modpackqt.com</a>
75
- (slave feature requires a paid plan — see below).
76
- </li>
77
- <li>
78
- <b>Download and run the Gateway app.</b>
79
- The gateway exposes a local REST API (default port <code>8502</code>).
80
- Open the ModPackQT app and click <em>Download Gateway</em>.
81
- </li>
82
- <li>
83
- <b>Create a Slave</b> in the ModPackQT app:
84
- go to <em>Slave Simulator</em> → <em>New Slave</em>, set a Modbus port (e.g. <code>502</code>),
85
- then click <em>Start</em>. Copy the <b>Slave ID</b> shown in the app.
86
- </li>
87
- <li>
88
- <b>Add a ModPackQT Config node</b> (click the pencil next to Gateway above),
89
- set Host to <code>localhost</code> and Port to <code>8502</code>.
90
- Paste your API key from <em>ModPackQT → Settings → API Keys</em>.
91
- </li>
92
- </ol>
93
-
94
- <h3>Plan requirement</h3>
95
- <p>
96
- The Slave Simulator is a <b>paid-plan feature</b>.
97
- Free accounts cannot use slave nodes. If you receive a <code>403</code> error,
98
- upgrade your plan first.
99
- <a href="https://modpackqt.com/#pricing" target="_blank">View plans &amp; upgrade →</a>
100
- </p>
101
-
102
- <h3>Inputs</h3>
103
- <dl class="message-properties">
104
- <dt>payload <span class="property-type">any</span></dt>
105
- <dd>Any message triggers a read (when Poll Interval is 0 or disabled).</dd>
106
- <dt class="optional">slaveId <span class="property-type">string | number</span></dt>
107
- <dd>Overrides the Slave ID configured in the node.</dd>
108
- <dt class="optional">address <span class="property-type">number</span></dt>
109
- <dd>Overrides the start address.</dd>
110
- <dt class="optional">quantity <span class="property-type">number</span></dt>
111
- <dd>Overrides how many registers to read.</dd>
112
- <dt class="optional">registerType <span class="property-type">string</span></dt>
113
- <dd>Overrides the register type: <code>holding</code>, <code>coil</code>, <code>input</code>, or <code>discrete</code>.</dd>
114
- </dl>
115
-
65
+ <p>Enable the slave server in the <b>modpackqt-config</b> node first.</p>
116
66
  <h3>Outputs</h3>
117
67
  <dl class="message-properties">
118
- <dt>payload.values <span class="property-type">array</span></dt>
119
- <dd>Array of register values (integers for holding/input, booleans for coil/discrete).</dd>
120
- <dt>payload.registerType <span class="property-type">string</span></dt>
121
- <dd>Register type that was read.</dd>
122
- <dt>payload.address <span class="property-type">number</span></dt>
123
- <dd>Start address that was read.</dd>
124
- <dt>payload.quantity <span class="property-type">number</span></dt>
125
- <dd>Number of values returned.</dd>
126
- <dt>payload.slaveId <span class="property-type">string</span></dt>
127
- <dd>The Slave ID that was queried.</dd>
128
- <dt>topic <span class="property-type">string</span></dt>
129
- <dd><code>slave/{slaveId}/{registerType}/{address}</code></dd>
68
+ <dt>payload <span class="property-type">array</span></dt>
69
+ <dd>Raw register values (integers for holding/input, booleans for coils/discrete).</dd>
130
70
  </dl>
131
-
132
- <h3>Status messages</h3>
133
- <ul>
134
- <li><b>yellow ring</b> — polling active, waiting for first response</li>
135
- <li><b>green dot</b> — last read succeeded, shows register info + timestamp</li>
136
- <li><b>red dot</b> — error (check API key / plan / gateway)</li>
137
- <li><i>slave not found</i> — Slave ID is wrong or slave was deleted</li>
138
- <li><i>gateway not running</i> — Gateway app is not started</li>
139
- </ul>
140
-
141
- <p>
142
- <a href="https://modpackqt.com/node-red" target="_blank">Full documentation &amp; example flows →</a>
143
- </p>
144
71
  </script>
@@ -1,10 +1,14 @@
1
- module.exports = function(RED) {
1
+ /**
2
+ * ModPackQT Slave Read — read from the embedded slave's local register store.
3
+ * The slave server itself runs in the modpackqt-config node (when enabled).
4
+ * Outputs raw register values.
5
+ */
6
+ module.exports = function (RED) {
2
7
  function ModPackQTSlaveReadNode(config) {
3
8
  RED.nodes.createNode(this, config);
4
9
  const node = this;
5
10
  const server = RED.nodes.getNode(config.server);
6
11
 
7
- node.slaveId = config.slaveId || '';
8
12
  node.registerType = config.registerType || 'holding';
9
13
  node.address = parseInt(config.address, 10) || 0;
10
14
  node.quantity = parseInt(config.quantity, 10) || 1;
@@ -12,51 +16,34 @@ module.exports = function(RED) {
12
16
 
13
17
  let timer = null;
14
18
 
15
- async function doRead(msg) {
19
+ function doRead(msg) {
16
20
  if (!server) {
17
- node.status({ fill: 'red', shape: 'ring', text: 'no config — add a ModPackQT Config node' });
21
+ node.status({ fill: 'red', shape: 'ring', text: 'no config' });
18
22
  return;
19
23
  }
20
-
21
- const slaveId = (msg && msg.slaveId) || node.slaveId;
22
- if (!slaveId) {
23
- node.status({ fill: 'red', shape: 'ring', text: 'slave ID required' });
24
- node.error('No Slave ID set. Configure it in the node or send msg.slaveId.', msg);
24
+ if (!server.slaveEnabled) {
25
+ node.status({ fill: 'red', shape: 'ring', text: 'slave server disabled in config' });
26
+ node.error('Embedded slave is disabled — enable it in the modpackqt-config node.', msg);
25
27
  return;
26
28
  }
27
-
28
- const registerType = (msg && msg.registerType) || node.registerType;
29
- const address = (msg && msg.address !== undefined) ? parseInt(msg.address, 10) : node.address;
30
- const quantity = (msg && msg.quantity !== undefined) ? parseInt(msg.quantity, 10) : node.quantity;
31
-
32
29
  try {
33
- const result = await server.request(
34
- 'GET',
35
- `/api/slaves/${encodeURIComponent(slaveId)}/registers?type=${registerType}&offset=${address}&limit=${quantity}`
36
- );
37
-
38
- const values = result.values || [];
39
- const now = new Date().toLocaleTimeString();
40
- const pollTxt = node.pollInterval > 0 ? ` (poll ${node.pollInterval}ms)` : '';
41
- node.status({ fill: 'green', shape: 'dot', text: `${registerType} @${address} [${values.length}] · ${now}${pollTxt}` });
42
-
30
+ const registerType = (msg && msg.registerType) || node.registerType;
31
+ const address = (msg && msg.address !== undefined) ? parseInt(msg.address, 10) : node.address;
32
+ const quantity = (msg && msg.quantity !== undefined) ? parseInt(msg.quantity, 10) : node.quantity;
33
+
34
+ const values = server.slaveGet(registerType, address, quantity);
35
+ const now = new Date().toLocaleTimeString();
36
+ node.status({
37
+ fill: 'green', shape: 'dot',
38
+ text: server.brandStatus(`slave ${registerType} @${address} [${values.length}] · ${now}`)
39
+ });
43
40
  node.send({
44
- payload: {
45
- values,
46
- registerType,
47
- address,
48
- quantity: values.length,
49
- total: result.total,
50
- slaveId
51
- },
52
- topic: `slave/${slaveId}/${registerType}/${address}`
41
+ payload: values,
42
+ topic: `modpackqt/slave/${registerType}/${address}`,
43
+ modpackqt: { registerType, address, quantity: values.length }
53
44
  });
54
45
  } catch (err) {
55
- const hint = err.message.includes('403') ? 'check API key / plan limit'
56
- : err.message.includes('404') ? 'slave not found — check Slave ID'
57
- : err.message.includes('ECONNREFUSED') ? 'gateway not running'
58
- : err.message;
59
- node.status({ fill: 'red', shape: 'dot', text: hint });
46
+ node.status({ fill: 'red', shape: 'dot', text: err.message.slice(0, 60) });
60
47
  node.error(err.message, msg);
61
48
  }
62
49
  }
@@ -66,8 +53,8 @@ module.exports = function(RED) {
66
53
  timer = setInterval(() => doRead(null), node.pollInterval);
67
54
  }
68
55
 
69
- node.on('input', async function(msg) { await doRead(msg); });
70
- node.on('close', function() { if (timer) clearInterval(timer); });
56
+ node.on('input', function (msg) { doRead(msg); });
57
+ node.on('close', function () { if (timer) clearInterval(timer); });
71
58
  }
72
59
  RED.nodes.registerType('modpackqt-slave-read', ModPackQTSlaveReadNode);
73
60
  };
@@ -1,21 +1,20 @@
1
1
  <script type="text/javascript">
2
2
  RED.nodes.registerType('modpackqt-slave-write', {
3
3
  category: 'ModPackQT',
4
- color: '#7c3aed',
4
+ color: '#15803d',
5
5
  defaults: {
6
6
  name: { value: '' },
7
7
  server: { value: '', type: 'modpackqt-config', required: true },
8
- slaveId: { value: '' },
9
8
  registerType: { value: 'holding', required: true },
10
9
  address: { value: 0, required: true, validate: RED.validators.number() }
11
10
  },
12
11
  inputs: 1,
13
12
  outputs: 1,
14
13
  icon: 'font-awesome/fa-hdd-o',
15
- label: function() {
14
+ label: function () {
16
15
  return this.name || ('Slave Write ' + (this.registerType || 'holding') + ' @' + this.address);
17
16
  },
18
- paletteLabel: 'slave write'
17
+ paletteLabel: 'modbus slave write'
19
18
  });
20
19
  </script>
21
20
 
@@ -25,18 +24,14 @@
25
24
  <input type="text" id="node-input-name" placeholder="Name">
26
25
  </div>
27
26
  <div class="form-row">
28
- <label for="node-input-server"><i class="fa fa-cog"></i> Gateway</label>
27
+ <label for="node-input-server"><i class="fa fa-cog"></i> Runtime</label>
29
28
  <input type="text" id="node-input-server">
30
29
  </div>
31
- <div class="form-row">
32
- <label for="node-input-slaveId"><i class="fa fa-id-card"></i> Slave ID</label>
33
- <input type="text" id="node-input-slaveId" placeholder="e.g. 42 (or leave blank, set via msg.slaveId)">
34
- </div>
35
30
  <div class="form-row">
36
31
  <label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
37
32
  <select id="node-input-registerType">
38
- <option value="holding">Holding Registers (FC16)</option>
39
- <option value="coil">Coils (FC15)</option>
33
+ <option value="holding">Holding Registers</option>
34
+ <option value="coils">Coils</option>
40
35
  <option value="input">Input Registers</option>
41
36
  <option value="discrete">Discrete Inputs</option>
42
37
  </select>
@@ -46,93 +41,17 @@
46
41
  <input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
47
42
  </div>
48
43
  <div class="form-tips">
49
- <b>Tip:</b> Send <code>msg.slaveId</code> to override the Slave ID at runtime.<br>
50
- Send <code>msg.address</code> to override the start address.<br>
51
- Payload: number, array, or JSON string <code>"[100,200,300]"</code>
44
+ <b>Pushes values into the embedded slave register store.</b> Any Modbus master that
45
+ connects to your slave port will read whatever was last written here.
46
+ Send <code>msg.payload</code> as a number, array, or JSON string.
52
47
  </div>
53
48
  </script>
54
49
 
55
50
  <script type="text/html" data-help-name="modpackqt-slave-write">
56
51
  <p>
57
- Push values into a <b>ModPackQT Slave</b> register table. Any external Modbus master that
58
- connects to that slave will read whatever values you last wrote here.
59
- </p>
60
-
61
- <h3>Quick Setup — 4 steps</h3>
62
- <ol>
63
- <li>
64
- <b>Create a free account</b> at
65
- <a href="https://modpackqt.com" target="_blank">modpackqt.com</a>
66
- (slave feature requires a paid plan — see below).
67
- </li>
68
- <li>
69
- <b>Download and run the Gateway app.</b>
70
- The gateway exposes a local REST API (default port <code>8502</code>) that this
71
- node talks to. Open the ModPackQT app and click <em>Download Gateway</em>.
72
- </li>
73
- <li>
74
- <b>Create a Slave</b> in the ModPackQT app:
75
- go to <em>Slave Simulator</em> → <em>New Slave</em>, set a Modbus port (e.g. <code>502</code>),
76
- then click <em>Start</em>. Copy the <b>Slave ID</b> shown in the app.
77
- </li>
78
- <li>
79
- <b>Add a ModPackQT Config node</b> (click the pencil next to Gateway above),
80
- set Host to <code>localhost</code> and Port to <code>8502</code>.
81
- Paste your API key from <em>ModPackQT → Settings → API Keys</em>.
82
- </li>
83
- </ol>
84
-
85
- <h3>Plan requirement</h3>
86
- <p>
87
- The Slave Simulator is a <b>paid-plan feature</b>.
88
- Free accounts cannot create slaves. If you receive a <code>403</code> error,
89
- your plan does not include slaves or you have reached your slave limit.
90
- <a href="https://modpackqt.com/#pricing" target="_blank">View plans &amp; upgrade →</a>
91
- </p>
92
-
93
- <h3>Inputs</h3>
94
- <dl class="message-properties">
95
- <dt>payload <span class="property-type">number | array | string</span></dt>
96
- <dd>
97
- Value(s) to write. Accepts: a single number (<code>42</code>),
98
- an array (<code>[10, 20, 30]</code>), or a JSON string (<code>"[10,20,30]"</code>).
99
- All values are converted to integers automatically.
100
- </dd>
101
- <dt class="optional">slaveId <span class="property-type">string | number</span></dt>
102
- <dd>Overrides the Slave ID configured in the node editor.</dd>
103
- <dt class="optional">address <span class="property-type">number</span></dt>
104
- <dd>Overrides the start address configured in the node editor.</dd>
105
- <dt class="optional">registerType <span class="property-type">string</span></dt>
106
- <dd>Overrides the register type: <code>holding</code>, <code>coil</code>, <code>input</code>, or <code>discrete</code>.</dd>
107
- </dl>
108
-
109
- <h3>Outputs</h3>
110
- <dl class="message-properties">
111
- <dt>payload <span class="property-type">any</span></dt>
112
- <dd>Original message passed through unchanged.</dd>
113
- <dt>success <span class="property-type">boolean</span></dt>
114
- <dd><code>true</code> when the write succeeded.</dd>
115
- <dt>valuesWritten <span class="property-type">array</span></dt>
116
- <dd>The integer values that were written.</dd>
117
- </dl>
118
-
119
- <h3>Status messages</h3>
120
- <ul>
121
- <li><b>blue ring</b> — write in progress</li>
122
- <li><b>green dot</b> — write succeeded, shows count + timestamp</li>
123
- <li><b>red dot</b> — error (check API key / plan / gateway)</li>
124
- <li><i>slave not found</i> — Slave ID is wrong or slave was deleted</li>
125
- <li><i>gateway not running</i> — Gateway app is not started</li>
126
- </ul>
127
-
128
- <h3>Example use case</h3>
129
- <p>
130
- Read temperature from an MQTT topic → convert to integer → send to this node.
131
- Any PLC or SCADA system on the network can then read that temperature via
132
- standard Modbus TCP from the ModPackQT slave port.
133
- </p>
134
-
135
- <p>
136
- <a href="https://modpackqt.com/node-red" target="_blank">Full documentation &amp; example flows →</a>
52
+ Writes into the embedded slave's local register store. External Modbus masters
53
+ that connect to your configured slave port (default <code>1502</code>) will read
54
+ these values.
137
55
  </p>
56
+ <p>Enable the slave server in the <b>modpackqt-config</b> node first.</p>
138
57
  </script>
@@ -1,61 +1,53 @@
1
- module.exports = function(RED) {
1
+ /**
2
+ * ModPackQT Slave Write — write into the embedded slave's local register store.
3
+ * External Modbus masters connecting to the slave port (configured in
4
+ * modpackqt-config) will read whatever was last written here.
5
+ */
6
+ module.exports = function (RED) {
2
7
  function ModPackQTSlaveWriteNode(config) {
3
8
  RED.nodes.createNode(this, config);
4
9
  const node = this;
5
10
  const server = RED.nodes.getNode(config.server);
6
11
 
7
- node.slaveId = config.slaveId || '';
8
12
  node.registerType = config.registerType || 'holding';
9
13
  node.address = parseInt(config.address, 10) || 0;
10
14
 
11
- node.on('input', async function(msg) {
15
+ node.on('input', function (msg) {
12
16
  if (!server) {
13
- node.status({ fill: 'red', shape: 'ring', text: 'no config — add a ModPackQT Config node' });
17
+ node.status({ fill: 'red', shape: 'ring', text: 'no config' });
14
18
  return;
15
19
  }
16
-
17
- const slaveId = msg.slaveId || node.slaveId;
18
- if (!slaveId) {
19
- node.status({ fill: 'red', shape: 'ring', text: 'slave ID required' });
20
- node.error('No Slave ID set. Configure it in the node or send msg.slaveId.', msg);
20
+ if (!server.slaveEnabled) {
21
+ node.status({ fill: 'red', shape: 'ring', text: 'slave server disabled in config' });
22
+ node.error('Embedded slave is disabled — enable it in the modpackqt-config node.', msg);
21
23
  return;
22
24
  }
23
-
24
25
  try {
25
- // Accept string JSON ("[100,200]"), plain number string, actual array, or number
26
26
  let raw = msg.payload;
27
27
  if (typeof raw === 'string') {
28
28
  try { raw = JSON.parse(raw); } catch (_) { raw = Number(raw); }
29
29
  }
30
-
31
- // Always convert every value to an integer to avoid encoding corruption
30
+ const registerType = msg.registerType || node.registerType;
31
+ const isBool = (registerType === 'coils' || registerType === 'discrete');
32
32
  const values = Array.isArray(raw)
33
- ? raw.map(v => parseInt(v, 10))
34
- : [parseInt(raw, 10)];
35
-
36
- if (values.some(v => isNaN(v))) {
37
- throw new Error(`Invalid payload — expected number or array of numbers, got: ${JSON.stringify(msg.payload)}`);
33
+ ? raw.map((v) => isBool ? Boolean(v) : parseInt(v, 10))
34
+ : [isBool ? Boolean(raw) : parseInt(raw, 10)];
35
+ if (!isBool && values.some((v) => isNaN(v))) {
36
+ throw new Error(`Invalid payload — expected number or array, got: ${JSON.stringify(msg.payload)}`);
38
37
  }
38
+ const address = msg.address !== undefined ? parseInt(msg.address, 10) : node.address;
39
39
 
40
- node.status({ fill: 'blue', shape: 'ring', text: `writing ${values.length} value(s)…` });
41
-
42
- await server.request('PATCH', `/api/slaves/${encodeURIComponent(slaveId)}/registers`, {
43
- registerType: msg.registerType || node.registerType,
44
- address: msg.address !== undefined ? parseInt(msg.address, 10) : node.address,
45
- values
46
- });
47
-
40
+ server.slaveSet(registerType, address, values);
48
41
  const now = new Date().toLocaleTimeString();
49
- node.status({ fill: 'green', shape: 'dot', text: `wrote ${values.length} @ addr ${node.address} · ${now}` });
42
+ node.status({
43
+ fill: 'green', shape: 'dot',
44
+ text: server.brandStatus(`slave wrote ${values.length} @${address} · ${now}`)
45
+ });
50
46
  msg.success = true;
51
47
  msg.valuesWritten = values;
52
48
  node.send(msg);
53
49
  } catch (err) {
54
- const hint = err.message.includes('403') ? 'check API key / plan limit'
55
- : err.message.includes('404') ? 'slave not found — check Slave ID'
56
- : err.message.includes('ECONNREFUSED') ? 'gateway not running'
57
- : err.message;
58
- node.status({ fill: 'red', shape: 'dot', text: hint });
50
+ node.status({ fill: 'red', shape: 'dot', text: err.message.slice(0, 60) });
59
51
  node.error(err.message, msg);
60
52
  }
61
53
  });