node-red-contrib-modbus-modpackqt 1.1.5 → 1.1.7

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/README.md CHANGED
@@ -45,11 +45,28 @@ npm install node-red-contrib-modbus-modpackqt
45
45
 
46
46
  ## Prerequisites
47
47
 
48
- 1. **Download and start the ModPackQT Gateway app:**
48
+ 1. **Sign up at [modpackqt.com](https://modpackqt.com)**
49
+ A free account works for all master nodes. Slave nodes require a **paid plan**.
50
+
51
+ 2. **Download and start the ModPackQT Gateway app**
49
52
  Download it from [modpackqt.com/download](https://modpackqt.com/download) and run it.
50
53
  By default it listens on **port 8502** (`localhost:8502`).
51
54
 
52
- 2. For slave nodes, create and start at least one slave in the ModPackQT app first.
55
+ 3. **Create an API key**
56
+ In the ModPackQT web app go to **Settings → API Keys → New Key**. Paste it into the `modpackqt-config` node.
57
+
58
+ 4. **For slave nodes** — create and start at least one slave in the ModPackQT app first (Slave Simulator → New Slave). Note the **Slave ID** shown — you'll need it in Node-RED.
59
+
60
+ > **Plan limits**
61
+ >
62
+ > | Feature | Free | Paid |
63
+ > |---|---|---|
64
+ > | Master Read (`modpackqt-master-read`) | ✓ | ✓ |
65
+ > | Master Write (`modpackqt-master-write`) | ✓ | ✓ |
66
+ > | Slave Write (`modpackqt-slave-write`) | ✗ | ✓ |
67
+ > | Slave Read (`modpackqt-slave-read`) | ✗ | ✓ |
68
+ >
69
+ > [View plans & upgrade →](https://modpackqt.com/#pricing)
53
70
 
54
71
  ---
55
72
 
@@ -60,8 +77,8 @@ npm install node-red-contrib-modbus-modpackqt
60
77
  | `modpackqt-config` | Config | Shared gateway connection (host, port, API key) |
61
78
  | `modpackqt-master-read` | Modbus Master | Read Modbus TCP registers — FC1/FC2/FC3/FC4 |
62
79
  | `modpackqt-master-write` | Modbus Master | Write Modbus TCP registers — FC5/FC6/FC15/FC16 |
63
- | `modpackqt-slave-read` | Modbus Slave | Read registers from a ModPackQT simulated slave |
64
- | `modpackqt-slave-write` | Modbus Slave | Write registers into a ModPackQT simulated slave |
80
+ | `modpackqt-slave-read` | Modbus Slave *(paid)* | Read registers from a ModPackQT slave |
81
+ | `modpackqt-slave-write` | Modbus Slave *(paid)* | Push values into a ModPackQT slave (build a live Modbus device from Node-RED) |
65
82
 
66
83
  ---
67
84
 
@@ -114,21 +131,45 @@ The `modpackqt-master-write` node writes registers to **any Modbus TCP slave dev
114
131
 
115
132
  ---
116
133
 
117
- ## Node-RED Modbus Slave Read & Write Simulated Registers
134
+ ## Building a Modbus Slave from Node-RED *(Paid plan)*
118
135
 
119
- The `modpackqt-slave-read` and `modpackqt-slave-write` nodes interact with ModPackQT's **built-in slave simulator** a virtual PLC register map you control.
136
+ Use `modpackqt-slave-write` to make Node-RED the **data source** for a live Modbus slave device. Any external Modbus master (PLC, SCADA, HMI) that connects to the ModPackQT slave port will read whatever values you last pushed from Node-RED.
137
+
138
+ **Typical flow:**
139
+ ```
140
+ [MQTT / Sensor / Timer] → [Function: scale to integer] → [modpackqt-slave-write addr=0] → [External Modbus master reads]
141
+ ```
120
142
 
121
- **Register types:** Holding Registers, Coils, Input Registers, Discrete Inputs
143
+ **Use cases:**
144
+ - Publish MQTT sensor data as Modbus TCP registers
145
+ - Simulate a PLC register map for HMI/SCADA testing
146
+ - Bridge OPC-UA, REST, or database data to Modbus
147
+
148
+ **Slave write input — all of these work:**
149
+ ```js
150
+ msg.payload = 234; // single integer
151
+ msg.payload = [234, 1013, 65]; // array — writes 3 registers starting at address 0
152
+ msg.payload = "[234, 1013, 65]"; // JSON string also works (inject string type)
153
+ ```
154
+
155
+ **Override at runtime:**
156
+ ```js
157
+ msg.slaveId = "42"; // select a different slave per message
158
+ msg.address = 10; // start at register 10 instead of the configured address
159
+ msg.registerType = "coil"; // override register type
160
+ ```
122
161
 
123
162
  **Slave read output:**
124
163
  ```json
125
164
  {
126
165
  "payload": {
127
- "values": [0, 42, 100],
166
+ "values": [234, 1013, 65],
128
167
  "registerType": "holding",
129
- "address": 0
168
+ "address": 0,
169
+ "quantity": 3,
170
+ "slaveId": "42"
130
171
  },
131
- "topic": "slave/{slaveId}/holding/0"
172
+ "topic": "slave/42/holding/0"
132
173
  }
133
174
  ```
134
175
 
@@ -136,7 +177,7 @@ The `modpackqt-slave-read` and `modpackqt-slave-write` nodes interact with ModPa
136
177
 
137
178
  ## Example Flows
138
179
 
139
- ### Poll Holding Registers Every 5 Seconds (Node-RED Modbus Master)
180
+ ### Poll Holding Registers Every 5 Seconds (Modbus Master)
140
181
 
141
182
  ```
142
183
  [Inject (repeat 5s)] → [modpackqt-master-read FC3 target=192.168.1.10:502 unitId=1 addr=0 qty=10] → [Debug]
@@ -148,13 +189,14 @@ The `modpackqt-slave-read` and `modpackqt-slave-write` nodes interact with ModPa
148
189
  [Inject (payload=1234)] → [modpackqt-master-write FC6 target=192.168.1.10:502 unitId=1 addr=100] → [Debug]
149
190
  ```
150
191
 
151
- ### Inject Test Values into a Modbus Slave Simulator
192
+ ### Build a Slave Publish Sensor Data as Modbus (Paid plan)
152
193
 
153
194
  ```
154
- [Inject ([10,20,30])] → [modpackqt-slave-write Holding addr=0]
155
- [modpackqt-slave-read Holding addr=0 qty=3] → [Debug]
195
+ [MQTT in] → [Function: msg.payload = parseInt(msg.payload * 10)] → [modpackqt-slave-write Holding addr=0] → [Debug]
156
196
  ```
157
197
 
198
+ Any Modbus master on the network now reads live sensor values from the slave.
199
+
158
200
  ### Import the Demo Flow
159
201
 
160
202
  1. Open Node-RED → **Menu → Import**
@@ -191,9 +233,11 @@ Add one `modpackqt-config` node per gateway instance. All other nodes reference
191
233
 
192
234
  | Issue | Solution |
193
235
  |---|---|
194
- | `connect ECONNREFUSED localhost:8502` | Make sure the ModPackQT Gateway app is running. Check host/port in the config node. |
195
- | `404 Slave not found` | Check the Slave IDfind it in the ModPackQT app under your slave's settings. |
196
- | `Slave not running` (slave-write) | Start the slave in the ModPackQT app before writing. |
236
+ | `connect ECONNREFUSED localhost:8502` | The ModPackQT Gateway app is not running. Start it first. |
237
+ | `401 Unauthorized` | Missing or invalid API key check `modpackqt-config`. |
238
+ | `403 Forbidden` on slave nodes | Your plan does not include the Slave Simulator. [Upgrade your plan →](https://modpackqt.com/#pricing) |
239
+ | `slave not found — check Slave ID` | The Slave ID is wrong or that slave was deleted. Check in ModPackQT → Slave Simulator. |
240
+ | `gateway not running` on slave nodes | The Gateway app is stopped. Open ModPackQT and click Start Gateway. |
197
241
  | `Cannot find module 'axios'` | Run `npm install` in `~/.node-red`. |
198
242
  | Nodes not appearing after install | Restart Node-RED completely after `npm install`. |
199
243
  | Node-RED version error | Requires Node-RED ≥ 2.0.0 and Node.js ≥ 14. |
@@ -2,15 +2,15 @@
2
2
  RED.nodes.registerType('modpackqt-config', {
3
3
  category: 'config',
4
4
  defaults: {
5
- name: { value: 'ModPackQT', required: false },
5
+ name: { value: 'ModPackQT Gateway', required: false },
6
6
  host: { value: 'localhost', required: true },
7
- port: { value: 3000, required: true, validate: RED.validators.number() }
7
+ port: { value: 8502, required: true, validate: RED.validators.number() }
8
8
  },
9
9
  credentials: {
10
10
  apiKey: { type: 'password' }
11
11
  },
12
12
  label: function() {
13
- return this.name || `${this.host}:${this.port}`;
13
+ return this.name || (this.host + ':' + this.port);
14
14
  }
15
15
  });
16
16
  </script>
@@ -18,7 +18,7 @@
18
18
  <script type="text/html" data-template-name="modpackqt-config">
19
19
  <div class="form-row">
20
20
  <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
21
- <input type="text" id="node-config-input-name" placeholder="ModPackQT">
21
+ <input type="text" id="node-config-input-name" placeholder="ModPackQT Gateway">
22
22
  </div>
23
23
  <div class="form-row">
24
24
  <label for="node-config-input-host"><i class="fa fa-globe"></i> Host</label>
@@ -26,23 +26,103 @@
26
26
  </div>
27
27
  <div class="form-row">
28
28
  <label for="node-config-input-port"><i class="fa fa-plug"></i> Port</label>
29
- <input type="number" id="node-config-input-port" placeholder="3000" min="1" max="65535">
29
+ <input type="number" id="node-config-input-port" placeholder="8502" min="1" max="65535">
30
30
  </div>
31
31
  <div class="form-row">
32
32
  <label for="node-config-input-apiKey"><i class="fa fa-key"></i> API Key</label>
33
- <input type="password" id="node-config-input-apiKey" placeholder="optional">
33
+ <input type="password" id="node-config-input-apiKey" placeholder="Paste your API key from ModPackQT → Settings">
34
+ </div>
35
+ <div class="form-tips">
36
+ <b>Default setup:</b> Host <code>localhost</code>, Port <code>8502</code>.<br>
37
+ Get your API key from <b>ModPackQT app → Settings → API Keys</b>.
34
38
  </div>
35
39
  </script>
36
40
 
37
41
  <script type="text/html" data-help-name="modpackqt-config">
38
- <p>Connection configuration for a ModPackQT instance.</p>
42
+ <p>
43
+ Shared connection settings for all ModPackQT nodes. Defines which
44
+ <b>ModPackQT Gateway</b> instance to communicate with and how to authenticate.
45
+ </p>
46
+
47
+ <h3>What is the ModPackQT Gateway?</h3>
48
+ <p>
49
+ The Gateway is a small app that runs on your local machine (or a server on your network).
50
+ It acts as a bridge between Node-RED and your Modbus devices, and also hosts the
51
+ Slave Simulator. All ModPackQT nodes talk to this Gateway's REST API.
52
+ </p>
53
+
54
+ <h3>First-time setup</h3>
55
+ <ol>
56
+ <li>
57
+ <b>Sign up</b> at <a href="https://modpackqt.com" target="_blank">modpackqt.com</a>
58
+ — a free account lets you use the master read/write nodes.
59
+ Slave and advanced features require a paid plan.
60
+ </li>
61
+ <li>
62
+ <b>Download the Gateway app</b> — log in to ModPackQT, open
63
+ <em>Settings → Gateway</em> and download the installer for your OS (Windows/Mac/Linux).
64
+ </li>
65
+ <li>
66
+ <b>Run the Gateway</b> — it starts a local REST API on port <code>8502</code> by default.
67
+ You will see a <em>Running</em> status in the app when it is ready.
68
+ </li>
69
+ <li>
70
+ <b>Create an API Key</b> — in the ModPackQT web app, go to
71
+ <em>Settings → API Keys → New Key</em>. Copy the key and paste it into the
72
+ <em>API Key</em> field in this config node.
73
+ </li>
74
+ <li>
75
+ <b>Set Host and Port</b>:
76
+ <ul>
77
+ <li>If Node-RED runs on the same machine as the Gateway: <code>localhost</code> / <code>8502</code></li>
78
+ <li>If the Gateway is on another machine: use that machine's IP address</li>
79
+ </ul>
80
+ </li>
81
+ </ol>
82
+
39
83
  <h3>Properties</h3>
40
84
  <dl class="message-properties">
41
85
  <dt>Host <span class="property-type">string</span></dt>
42
- <dd>Hostname or IP address of the ModPackQT server (default: <code>localhost</code>).</dd>
86
+ <dd>
87
+ Hostname or IP address of the machine running the ModPackQT Gateway.
88
+ Default: <code>localhost</code>.
89
+ </dd>
43
90
  <dt>Port <span class="property-type">number</span></dt>
44
- <dd>TCP port the ModPackQT REST API listens on (default: <code>3000</code>).</dd>
91
+ <dd>
92
+ TCP port the Gateway REST API listens on. Default: <code>8502</code>.
93
+ Change this only if you configured a custom port in the Gateway settings.
94
+ </dd>
45
95
  <dt>API Key <span class="property-type">password</span></dt>
46
- <dd>Optional API key sent as the <code>x-api-key</code> header.</dd>
96
+ <dd>
97
+ Your ModPackQT API key. Required for authenticated operations.
98
+ Get it from <em>ModPackQT → Settings → API Keys</em>.
99
+ </dd>
47
100
  </dl>
101
+
102
+ <h3>Plan limits</h3>
103
+ <table>
104
+ <thead><tr><th>Feature</th><th>Free</th><th>Paid</th></tr></thead>
105
+ <tbody>
106
+ <tr><td>Master Read (modpackqt-master-read)</td><td>✓</td><td>✓</td></tr>
107
+ <tr><td>Master Write (modpackqt-master-write)</td><td>✓</td><td>✓</td></tr>
108
+ <tr><td>Slave Write (modpackqt-slave-write)</td><td>✗</td><td>✓</td></tr>
109
+ <tr><td>Slave Read (modpackqt-slave-read)</td><td>✗</td><td>✓</td></tr>
110
+ <tr><td>Number of Slaves</td><td>0</td><td>Plan dependent</td></tr>
111
+ </tbody>
112
+ </table>
113
+ <p>
114
+ <a href="https://modpackqt.com/#pricing" target="_blank">View plans &amp; upgrade →</a>
115
+ </p>
116
+
117
+ <h3>Troubleshooting</h3>
118
+ <ul>
119
+ <li><b>ECONNREFUSED</b> — the Gateway app is not running. Start it from the ModPackQT app.</li>
120
+ <li><b>401 Unauthorized</b> — missing or invalid API key.</li>
121
+ <li><b>403 Forbidden</b> — your plan does not include this feature, or you have reached a limit.</li>
122
+ <li><b>404 Not Found</b> — the Slave ID or Connection Profile ID does not exist in your account.</li>
123
+ </ul>
124
+
125
+ <p>
126
+ <a href="https://modpackqt.com/node-red" target="_blank">Full Node-RED documentation →</a>
127
+ </p>
48
128
  </script>
@@ -5,7 +5,7 @@
5
5
  defaults: {
6
6
  name: { value: '' },
7
7
  server: { value: '', type: 'modpackqt-config', required: true },
8
- slaveId: { value: '', required: true },
8
+ slaveId: { value: '' },
9
9
  registerType: { value: 'holding', required: true },
10
10
  address: { value: 0, required: true, validate: RED.validators.number() },
11
11
  quantity: { value: 1, required: true, validate: RED.validators.number() },
@@ -15,7 +15,7 @@
15
15
  outputs: 1,
16
16
  icon: 'font-awesome/fa-server',
17
17
  label: function() {
18
- return this.name || `Slave Read ${this.registerType} @${this.address}`;
18
+ return this.name || ('Slave Read ' + (this.registerType || 'holding') + ' @' + this.address);
19
19
  },
20
20
  paletteLabel: 'slave read'
21
21
  });
@@ -32,12 +32,12 @@
32
32
  </div>
33
33
  <div class="form-row">
34
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="Slave ID from ModPackQT app">
35
+ <input type="text" id="node-input-slaveId" placeholder="e.g. 42 (or leave blank, set via msg.slaveId)">
36
36
  </div>
37
37
  <div class="form-row">
38
38
  <label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
39
39
  <select id="node-input-registerType">
40
- <option value="holding" selected>Holding Registers</option>
40
+ <option value="holding">Holding Registers</option>
41
41
  <option value="coil">Coils</option>
42
42
  <option value="input">Input Registers</option>
43
43
  <option value="discrete">Discrete Inputs</option>
@@ -53,25 +53,92 @@
53
53
  </div>
54
54
  <div class="form-row">
55
55
  <label for="node-input-pollInterval"><i class="fa fa-clock-o"></i> Poll Interval (ms)</label>
56
- <input type="number" id="node-input-pollInterval" min="0" placeholder="0 = disabled">
56
+ <input type="number" id="node-input-pollInterval" min="0" placeholder="0 = trigger only">
57
+ </div>
58
+ <div class="form-tips">
59
+ <b>Tip:</b> Override at runtime via <code>msg.slaveId</code>, <code>msg.address</code>,
60
+ <code>msg.quantity</code>, or <code>msg.registerType</code>.
57
61
  </div>
58
62
  </script>
59
63
 
60
64
  <script type="text/html" data-help-name="modpackqt-slave-read">
61
- <p>Reads registers from a ModPackQT simulated slave via the ModPackQT Gateway app.</p>
62
- <p>Calls <code>GET /api/slaves/:id/registers</code> on the gateway.</p>
65
+ <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.
68
+ </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
+
63
102
  <h3>Inputs</h3>
64
103
  <dl class="message-properties">
65
104
  <dt>payload <span class="property-type">any</span></dt>
66
- <dd>Any message triggers a read (when Poll Interval is 0).</dd>
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>
67
114
  </dl>
115
+
68
116
  <h3>Outputs</h3>
69
117
  <dl class="message-properties">
70
- <dt>payload <span class="property-type">object</span></dt>
71
- <dd>Contains <code>values</code> (array), <code>registerType</code>, <code>address</code>, and <code>total</code>.</dd>
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>
72
128
  <dt>topic <span class="property-type">string</span></dt>
73
129
  <dd><code>slave/{slaveId}/{registerType}/{address}</code></dd>
74
130
  </dl>
75
- <h3>Details</h3>
76
- <p><b>Slave ID</b> — the ID of the slave as configured in the ModPackQT app. Find it in the app or via <code>GET /api/slaves</code>.</p>
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>
77
144
  </script>
@@ -6,39 +6,67 @@ module.exports = function(RED) {
6
6
 
7
7
  node.slaveId = config.slaveId || '';
8
8
  node.registerType = config.registerType || 'holding';
9
- node.address = parseInt(config.address) || 0;
10
- node.quantity = parseInt(config.quantity) || 1;
11
- node.pollInterval = parseInt(config.pollInterval) || 0;
9
+ node.address = parseInt(config.address, 10) || 0;
10
+ node.quantity = parseInt(config.quantity, 10) || 1;
11
+ node.pollInterval = parseInt(config.pollInterval, 10) || 0;
12
12
 
13
13
  let timer = null;
14
14
 
15
- async function doRead() {
15
+ async function doRead(msg) {
16
16
  if (!server) {
17
- node.status({ fill: 'red', shape: 'ring', text: 'no config' });
17
+ node.status({ fill: 'red', shape: 'ring', text: 'no config — add a ModPackQT Config node' });
18
18
  return;
19
19
  }
20
- if (!node.slaveId) {
20
+
21
+ const slaveId = (msg && msg.slaveId) || node.slaveId;
22
+ if (!slaveId) {
21
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);
22
25
  return;
23
26
  }
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
+
24
32
  try {
25
33
  const result = await server.request(
26
34
  'GET',
27
- `/api/slaves/${encodeURIComponent(node.slaveId)}/registers?type=${node.registerType}&offset=${node.address}&limit=${node.quantity}`
35
+ `/api/slaves/${encodeURIComponent(slaveId)}/registers?type=${registerType}&offset=${address}&limit=${quantity}`
28
36
  );
29
- node.status({ fill: 'green', shape: 'dot', text: `${node.registerType} @${node.address}` });
30
- node.send({ payload: { values: result.values || [], registerType: node.registerType, address: node.address, total: result.total }, topic: `slave/${node.slaveId}/${node.registerType}/${node.address}` });
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
+
43
+ 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}`
53
+ });
31
54
  } catch (err) {
32
- node.status({ fill: 'red', shape: 'dot', text: err.message });
33
- node.error(err.message);
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 });
60
+ node.error(err.message, msg);
34
61
  }
35
62
  }
36
63
 
37
64
  if (node.pollInterval > 0) {
38
- timer = setInterval(doRead, node.pollInterval);
65
+ node.status({ fill: 'yellow', shape: 'ring', text: `polling every ${node.pollInterval}ms…` });
66
+ timer = setInterval(() => doRead(null), node.pollInterval);
39
67
  }
40
68
 
41
- node.on('input', async function() { await doRead(); });
69
+ node.on('input', async function(msg) { await doRead(msg); });
42
70
  node.on('close', function() { if (timer) clearInterval(timer); });
43
71
  }
44
72
  RED.nodes.registerType('modpackqt-slave-read', ModPackQTSlaveReadNode);
@@ -5,7 +5,7 @@
5
5
  defaults: {
6
6
  name: { value: '' },
7
7
  server: { value: '', type: 'modpackqt-config', required: true },
8
- slaveId: { value: '', required: true },
8
+ slaveId: { value: '' },
9
9
  registerType: { value: 'holding', required: true },
10
10
  address: { value: 0, required: true, validate: RED.validators.number() }
11
11
  },
@@ -13,7 +13,7 @@
13
13
  outputs: 1,
14
14
  icon: 'font-awesome/fa-hdd-o',
15
15
  label: function() {
16
- return this.name || `Slave Write ${this.registerType} @${this.address}`;
16
+ return this.name || ('Slave Write ' + (this.registerType || 'holding') + ' @' + this.address);
17
17
  },
18
18
  paletteLabel: 'slave write'
19
19
  });
@@ -30,13 +30,13 @@
30
30
  </div>
31
31
  <div class="form-row">
32
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="Slave ID from ModPackQT app">
33
+ <input type="text" id="node-input-slaveId" placeholder="e.g. 42 (or leave blank, set via msg.slaveId)">
34
34
  </div>
35
35
  <div class="form-row">
36
36
  <label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
37
37
  <select id="node-input-registerType">
38
- <option value="holding" selected>Holding Registers</option>
39
- <option value="coil">Coils</option>
38
+ <option value="holding">Holding Registers (FC16)</option>
39
+ <option value="coil">Coils (FC15)</option>
40
40
  <option value="input">Input Registers</option>
41
41
  <option value="discrete">Discrete Inputs</option>
42
42
  </select>
@@ -45,21 +45,94 @@
45
45
  <label for="node-input-address"><i class="fa fa-map-marker"></i> Start Address</label>
46
46
  <input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
47
47
  </div>
48
+ <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>
52
+ </div>
48
53
  </script>
49
54
 
50
55
  <script type="text/html" data-help-name="modpackqt-slave-write">
51
- <p>Writes values into a ModPackQT simulated slave register table via the ModPackQT Gateway app.</p>
52
- <p>Calls <code>PATCH /api/slaves/:id/registers</code> on the gateway. The slave must be running.</p>
56
+ <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
+
53
93
  <h3>Inputs</h3>
54
94
  <dl class="message-properties">
55
- <dt>payload <span class="property-type">number | array</span></dt>
56
- <dd>Value(s) to write into the slave register table.</dd>
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>
57
107
  </dl>
108
+
58
109
  <h3>Outputs</h3>
59
110
  <dl class="message-properties">
60
111
  <dt>payload <span class="property-type">any</span></dt>
61
- <dd>Original message passed through with <code>msg.success = true</code>.</dd>
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>
62
117
  </dl>
63
- <h3>Details</h3>
64
- <p><b>Slave ID</b> — the ID of the slave as configured in the ModPackQT app. The slave must be started before writing.</p>
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>
137
+ </p>
65
138
  </script>
@@ -6,29 +6,56 @@ module.exports = function(RED) {
6
6
 
7
7
  node.slaveId = config.slaveId || '';
8
8
  node.registerType = config.registerType || 'holding';
9
- node.address = parseInt(config.address) || 0;
9
+ node.address = parseInt(config.address, 10) || 0;
10
10
 
11
11
  node.on('input', async function(msg) {
12
12
  if (!server) {
13
- node.status({ fill: 'red', shape: 'ring', text: 'no config' });
13
+ node.status({ fill: 'red', shape: 'ring', text: 'no config — add a ModPackQT Config node' });
14
14
  return;
15
15
  }
16
- if (!node.slaveId) {
16
+
17
+ const slaveId = msg.slaveId || node.slaveId;
18
+ if (!slaveId) {
17
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);
18
21
  return;
19
22
  }
23
+
20
24
  try {
21
- const values = Array.isArray(msg.payload) ? msg.payload : [msg.payload];
22
- await server.request('PATCH', `/api/slaves/${encodeURIComponent(node.slaveId)}/registers`, {
23
- registerType: node.registerType,
24
- address: node.address,
25
+ // Accept string JSON ("[100,200]"), plain number string, actual array, or number
26
+ let raw = msg.payload;
27
+ if (typeof raw === 'string') {
28
+ try { raw = JSON.parse(raw); } catch (_) { raw = Number(raw); }
29
+ }
30
+
31
+ // Always convert every value to an integer to avoid encoding corruption
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)}`);
38
+ }
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,
25
45
  values
26
46
  });
27
- node.status({ fill: 'green', shape: 'dot', text: `${node.registerType} @${node.address}` });
47
+
48
+ const now = new Date().toLocaleTimeString();
49
+ node.status({ fill: 'green', shape: 'dot', text: `wrote ${values.length} @ addr ${node.address} · ${now}` });
28
50
  msg.success = true;
51
+ msg.valuesWritten = values;
29
52
  node.send(msg);
30
53
  } catch (err) {
31
- node.status({ fill: 'red', shape: 'dot', text: err.message });
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 });
32
59
  node.error(err.message, msg);
33
60
  }
34
61
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-modbus-modpackqt",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Node-RED nodes for ModPackQT — read/write Modbus TCP registers (master & slave) via the ModPackQT Gateway app. Supports FC1/FC2/FC3/FC4 reads and FC5/FC6/FC15/FC16 writes.",
5
5
  "keywords": [
6
6
  "node-red",