node-red-contrib-modbus-modpackqt 1.1.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/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # node-red-contrib-modbus-modpackqt
2
+
3
+ **Node-RED Modbus nodes powered by [ModPackQT](https://modpackqt.com)** โ€” drag-and-drop nodes to read and write Modbus TCP registers (master & slave) from your Node-RED flows. No hand-written HTTP calls. No protocol handling. Just connect and go.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/node-red-contrib-modbus-modpackqt.svg)](https://www.npmjs.com/package/node-red-contrib-modbus-modpackqt)
6
+ [![Node-RED](https://img.shields.io/badge/Node--RED-%E2%89%A52.0.0-red)](https://nodered.org)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ---
10
+
11
+ ## What is ModPackQT?
12
+
13
+ [ModPackQT](https://modpackqt.com) is an industrial Modbus Master/Slave platform for engineers and technicians. It provides:
14
+
15
+ - **Modbus TCP and RTU** communication via a local gateway app (`modsim.js`)
16
+ - **Built-in slave simulator** โ€” simulate a PLC register map without hardware
17
+ - **Real-time data visualization**, profile management, and AI-assisted setup
18
+ - A **REST API** that this Node-RED palette talks to
19
+
20
+ ๐Ÿ‘‰ [Download ModPackQT](https://modpackqt.com/download) ยท [Modbus Tutorial](https://modpackqt.com/resources/tutorial) ยท [Resources Hub](https://modpackqt.com/resources)
21
+
22
+ > **Requires the ModPackQT gateway (`modsim.js`) to be running locally.**
23
+ > The gateway is what the nodes talk to โ€” not the cloud server directly.
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ ### Option 1 โ€” Node-RED Palette Manager (recommended)
30
+
31
+ 1. Open Node-RED โ†’ **Menu (โ˜ฐ) โ†’ Manage palette โ†’ Install**
32
+ 2. Search for **`modbus-modpackqt`**
33
+ 3. Click **Install**
34
+ 4. Restart Node-RED โ€” the **ModPackQT** palette group appears in the sidebar
35
+
36
+ ### Option 2 โ€” npm CLI
37
+
38
+ ```bash
39
+ cd ~/.node-red
40
+ npm install node-red-contrib-modbus-modpackqt
41
+ # Restart Node-RED after installation
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Prerequisites
47
+
48
+ 1. **Install and start the ModPackQT gateway:**
49
+ ```bash
50
+ node modsim.js
51
+ ```
52
+ By default it listens on **port 8502** (`localhost:8502`).
53
+ Download `modsim.js` from the ModPackQT app or [modpackqt.com/download](https://modpackqt.com/download).
54
+
55
+ 2. For slave nodes, create and start at least one slave in the ModPackQT app first.
56
+
57
+ ---
58
+
59
+ ## Available Nodes
60
+
61
+ | Node | Category | Purpose |
62
+ |---|---|---|
63
+ | `modpackqt-config` | Config | Shared gateway connection (host, port, API key) |
64
+ | `modpackqt-read` | Modbus Master | Read Modbus TCP registers โ€” FC1/FC2/FC3/FC4 |
65
+ | `modpackqt-write` | Modbus Master | Write Modbus TCP registers โ€” FC5/FC6/FC15/FC16 |
66
+ | `modpackqt-slave-read` | Modbus Slave | Read registers from a ModPackQT simulated slave |
67
+ | `modpackqt-slave-write` | Modbus Slave | Write registers into a ModPackQT simulated slave |
68
+
69
+ ---
70
+
71
+ ## Node-RED Modbus Master โ€” Read Registers (FC1/FC2/FC3/FC4)
72
+
73
+ The `modpackqt-read` node reads registers from **any Modbus TCP slave device** by routing the request through the ModPackQT gateway.
74
+
75
+ **Supported function codes:**
76
+ - **FC1** โ€” Read Coils
77
+ - **FC2** โ€” Read Discrete Inputs
78
+ - **FC3** โ€” Read Holding Registers
79
+ - **FC4** โ€” Read Input Registers
80
+
81
+ **Config options:**
82
+
83
+ | Property | Description |
84
+ |---|---|
85
+ | Gateway | Config node pointing at your running `modsim.js` |
86
+ | Target Host | IP/hostname of the Modbus TCP slave to read from |
87
+ | Target Port | Port of the Modbus TCP slave (default: 502) |
88
+ | Unit ID | Modbus unit ID of the slave (1โ€“247) |
89
+ | Function Code | FC1 / FC2 / FC3 / FC4 |
90
+ | Start Address | First register address (0-based) |
91
+ | Quantity | Number of registers/coils to read (1โ€“125) |
92
+ | Poll Interval | Auto-poll every N ms (0 = trigger on input msg only) |
93
+
94
+ **Output:**
95
+ ```json
96
+ {
97
+ "payload": { "success": true, "values": [100, 200, 300] },
98
+ "topic": "modbus/read/192.168.1.10:502"
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Node-RED Modbus Master โ€” Write Registers (FC5/FC6/FC15/FC16)
105
+
106
+ The `modpackqt-write` node writes registers to **any Modbus TCP slave device**.
107
+
108
+ **Supported function codes:**
109
+ - **FC5** โ€” Write Single Coil
110
+ - **FC6** โ€” Write Single Holding Register
111
+ - **FC15** โ€” Write Multiple Coils
112
+ - **FC16** โ€” Write Multiple Holding Registers
113
+
114
+ **Input:** `msg.payload` โ€” a number (FC5/FC6) or array of numbers (FC15/FC16)
115
+
116
+ **Output:** Original `msg` with `msg.success = true`
117
+
118
+ ---
119
+
120
+ ## Node-RED Modbus Slave โ€” Read & Write Simulated Registers
121
+
122
+ The `modpackqt-slave-read` and `modpackqt-slave-write` nodes interact with ModPackQT's **built-in slave simulator** โ€” a virtual PLC register map you control.
123
+
124
+ **Register types:** Holding Registers, Coils, Input Registers, Discrete Inputs
125
+
126
+ **Slave read output:**
127
+ ```json
128
+ {
129
+ "payload": {
130
+ "values": [0, 42, 100],
131
+ "registerType": "holding",
132
+ "address": 0
133
+ },
134
+ "topic": "slave/{slaveId}/holding/0"
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Example Flows
141
+
142
+ ### Poll Holding Registers Every 5 Seconds (Node-RED Modbus Master)
143
+
144
+ ```
145
+ [Inject (repeat 5s)] โ†’ [modpackqt-read FC3 target=192.168.1.10:502 unitId=1 addr=0 qty=10] โ†’ [Debug]
146
+ ```
147
+
148
+ ### Write a Setpoint Register from a Button
149
+
150
+ ```
151
+ [Inject (payload=1234)] โ†’ [modpackqt-write FC6 target=192.168.1.10:502 unitId=1 addr=100] โ†’ [Debug]
152
+ ```
153
+
154
+ ### Inject Test Values into a Modbus Slave Simulator
155
+
156
+ ```
157
+ [Inject ([10,20,30])] โ†’ [modpackqt-slave-write Holding addr=0]
158
+ [modpackqt-slave-read Holding addr=0 qty=3] โ†’ [Debug]
159
+ ```
160
+
161
+ ### Import the Demo Flow
162
+
163
+ 1. Open Node-RED โ†’ **Menu โ†’ Import**
164
+ 2. Choose `examples/basic-flow.json` from the package directory
165
+ 3. Update the config node with your gateway host/port
166
+ 4. Click **Deploy**
167
+
168
+ ---
169
+
170
+ ## Configuration Node
171
+
172
+ Add one `modpackqt-config` node per gateway instance. All other nodes reference it.
173
+
174
+ | Property | Default | Description |
175
+ |---|---|---|
176
+ | Host | `localhost` | Hostname or IP of the machine running `modsim.js` |
177
+ | Port | `8502` | Port that `modsim.js` listens on |
178
+ | API Key | _(empty)_ | Optional โ€” sent as `x-api-key` header if set |
179
+
180
+ ---
181
+
182
+ ## API Endpoints Used
183
+
184
+ | Node | Gateway Endpoint |
185
+ |---|---|
186
+ | `modpackqt-read` | `POST /api/modbus/tcp/read` |
187
+ | `modpackqt-write` | `POST /api/modbus/tcp/write` |
188
+ | `modpackqt-slave-read` | `GET /api/slaves/:id/registers` |
189
+ | `modpackqt-slave-write` | `PATCH /api/slaves/:id/registers` |
190
+
191
+ ---
192
+
193
+ ## Troubleshooting
194
+
195
+ | Issue | Solution |
196
+ |---|---|
197
+ | `connect ECONNREFUSED localhost:8502` | Make sure `modsim.js` is running. Check host/port in the config node. |
198
+ | `404 Slave not found` | Check the Slave ID โ€” find it in the ModPackQT app under your slave's settings. |
199
+ | `Slave not running` (slave-write) | Start the slave in the ModPackQT app before writing. |
200
+ | `Cannot find module 'axios'` | Run `npm install` in `~/.node-red`. |
201
+ | Nodes not appearing after install | Restart Node-RED completely after `npm install`. |
202
+ | Node-RED version error | Requires Node-RED โ‰ฅ 2.0.0 and Node.js โ‰ฅ 14. |
203
+
204
+ ---
205
+
206
+ ## Links
207
+
208
+ - [ModPackQT Homepage](https://modpackqt.com)
209
+ - [Download ModPackQT Gateway](https://modpackqt.com/download)
210
+ - [Free Modbus Tutorial](https://modpackqt.com/resources/tutorial)
211
+ - [Modbus Resources Hub](https://modpackqt.com/resources)
212
+ - [Node-RED Docs](https://nodered.org/docs/)
213
+ - [Report an Issue](https://github.com/jomer0411/Modsimcontrol/issues)
214
+
215
+ ---
216
+
217
+ ## License
218
+
219
+ MIT โ€” ยฉ ModPackQT
@@ -0,0 +1,194 @@
1
+ [
2
+ {
3
+ "id": "tab-modpackqt-demo",
4
+ "type": "tab",
5
+ "label": "ModPackQT Demo",
6
+ "disabled": false,
7
+ "info": "Demonstrates ModPackQT Node-RED nodes against the modsim.js gateway (default port 8502).\n\nBefore deploying:\n1. Start modsim.js: node modsim.js\n2. Update the config node if gateway runs on a different host/port\n3. For slave-read/slave-write: update slaveId to match a slave ID from your ModPackQT app"
8
+ },
9
+ {
10
+ "id": "config-modpackqt-local",
11
+ "type": "modpackqt-config",
12
+ "name": "ModPackQT Gateway (localhost:8502)",
13
+ "host": "localhost",
14
+ "port": 8502,
15
+ "z": "tab-modpackqt-demo"
16
+ },
17
+ {
18
+ "id": "inject-poll-trigger",
19
+ "type": "inject",
20
+ "name": "Poll every 5s",
21
+ "props": [{ "p": "payload" }, { "p": "topic", "vt": "str" }],
22
+ "repeat": "5",
23
+ "crontab": "",
24
+ "once": true,
25
+ "onceDelay": 0,
26
+ "topic": "",
27
+ "payload": "",
28
+ "payloadType": "date",
29
+ "z": "tab-modpackqt-demo",
30
+ "x": 140,
31
+ "y": 80,
32
+ "wires": [["read-holding-regs"]]
33
+ },
34
+ {
35
+ "id": "read-holding-regs",
36
+ "type": "modpackqt-read",
37
+ "name": "Read FC3 Holding Regs",
38
+ "server": "config-modpackqt-local",
39
+ "targetHost": "192.168.1.10",
40
+ "targetPort": 502,
41
+ "unitId": 1,
42
+ "functionCode": "3",
43
+ "address": 0,
44
+ "quantity": 10,
45
+ "pollInterval": 0,
46
+ "z": "tab-modpackqt-demo",
47
+ "x": 380,
48
+ "y": 80,
49
+ "wires": [["debug-holding-regs"]]
50
+ },
51
+ {
52
+ "id": "debug-holding-regs",
53
+ "type": "debug",
54
+ "name": "Holding Reg Values",
55
+ "active": true,
56
+ "tosidebar": true,
57
+ "console": false,
58
+ "complete": "payload",
59
+ "targetType": "msg",
60
+ "z": "tab-modpackqt-demo",
61
+ "x": 640,
62
+ "y": 80,
63
+ "wires": []
64
+ },
65
+ {
66
+ "id": "inject-write-reg",
67
+ "type": "inject",
68
+ "name": "Write value 1234",
69
+ "props": [{ "p": "payload", "v": "1234", "vt": "num" }],
70
+ "repeat": "",
71
+ "crontab": "",
72
+ "once": false,
73
+ "topic": "",
74
+ "z": "tab-modpackqt-demo",
75
+ "x": 140,
76
+ "y": 180,
77
+ "wires": [["write-single-reg"]]
78
+ },
79
+ {
80
+ "id": "write-single-reg",
81
+ "type": "modpackqt-write",
82
+ "name": "Write FC6 @100",
83
+ "server": "config-modpackqt-local",
84
+ "targetHost": "192.168.1.10",
85
+ "targetPort": 502,
86
+ "unitId": 1,
87
+ "functionCode": "6",
88
+ "address": 100,
89
+ "z": "tab-modpackqt-demo",
90
+ "x": 380,
91
+ "y": 180,
92
+ "wires": [["debug-write-result"]]
93
+ },
94
+ {
95
+ "id": "debug-write-result",
96
+ "type": "debug",
97
+ "name": "Write result",
98
+ "active": true,
99
+ "tosidebar": true,
100
+ "console": false,
101
+ "complete": "success",
102
+ "targetType": "msg",
103
+ "z": "tab-modpackqt-demo",
104
+ "x": 620,
105
+ "y": 180,
106
+ "wires": []
107
+ },
108
+ {
109
+ "id": "inject-slave-read",
110
+ "type": "inject",
111
+ "name": "Read slave registers",
112
+ "props": [{ "p": "payload" }],
113
+ "repeat": "2",
114
+ "crontab": "",
115
+ "once": true,
116
+ "onceDelay": 0,
117
+ "topic": "",
118
+ "payload": "",
119
+ "payloadType": "date",
120
+ "z": "tab-modpackqt-demo",
121
+ "x": 150,
122
+ "y": 280,
123
+ "wires": [["slave-read-node"]]
124
+ },
125
+ {
126
+ "id": "slave-read-node",
127
+ "type": "modpackqt-slave-read",
128
+ "name": "Slave Holding @0",
129
+ "server": "config-modpackqt-local",
130
+ "slaveId": "YOUR_SLAVE_ID",
131
+ "registerType": "holding",
132
+ "address": 0,
133
+ "quantity": 5,
134
+ "pollInterval": 0,
135
+ "z": "tab-modpackqt-demo",
136
+ "x": 400,
137
+ "y": 280,
138
+ "wires": [["debug-slave-read"]]
139
+ },
140
+ {
141
+ "id": "debug-slave-read",
142
+ "type": "debug",
143
+ "name": "Slave register values",
144
+ "active": true,
145
+ "tosidebar": true,
146
+ "console": false,
147
+ "complete": "payload",
148
+ "targetType": "msg",
149
+ "z": "tab-modpackqt-demo",
150
+ "x": 650,
151
+ "y": 280,
152
+ "wires": []
153
+ },
154
+ {
155
+ "id": "inject-slave-write",
156
+ "type": "inject",
157
+ "name": "Set slave regs [10,20,30]",
158
+ "props": [{ "p": "payload", "v": "[10,20,30]", "vt": "json" }],
159
+ "repeat": "",
160
+ "crontab": "",
161
+ "once": false,
162
+ "z": "tab-modpackqt-demo",
163
+ "x": 160,
164
+ "y": 380,
165
+ "wires": [["slave-write-node"]]
166
+ },
167
+ {
168
+ "id": "slave-write-node",
169
+ "type": "modpackqt-slave-write",
170
+ "name": "Slave Write Holding @0",
171
+ "server": "config-modpackqt-local",
172
+ "slaveId": "YOUR_SLAVE_ID",
173
+ "registerType": "holding",
174
+ "address": 0,
175
+ "z": "tab-modpackqt-demo",
176
+ "x": 420,
177
+ "y": 380,
178
+ "wires": [["debug-slave-write"]]
179
+ },
180
+ {
181
+ "id": "debug-slave-write",
182
+ "type": "debug",
183
+ "name": "Write success",
184
+ "active": true,
185
+ "tosidebar": true,
186
+ "console": false,
187
+ "complete": "success",
188
+ "targetType": "msg",
189
+ "z": "tab-modpackqt-demo",
190
+ "x": 650,
191
+ "y": 380,
192
+ "wires": []
193
+ }
194
+ ]
@@ -0,0 +1,48 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('modpackqt-config', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: 'ModPackQT', required: false },
6
+ host: { value: 'localhost', required: true },
7
+ port: { value: 3000, required: true, validate: RED.validators.number() }
8
+ },
9
+ credentials: {
10
+ apiKey: { type: 'password' }
11
+ },
12
+ label: function() {
13
+ return this.name || `${this.host}:${this.port}`;
14
+ }
15
+ });
16
+ </script>
17
+
18
+ <script type="text/html" data-template-name="modpackqt-config">
19
+ <div class="form-row">
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">
22
+ </div>
23
+ <div class="form-row">
24
+ <label for="node-config-input-host"><i class="fa fa-globe"></i> Host</label>
25
+ <input type="text" id="node-config-input-host" placeholder="localhost">
26
+ </div>
27
+ <div class="form-row">
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">
30
+ </div>
31
+ <div class="form-row">
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">
34
+ </div>
35
+ </script>
36
+
37
+ <script type="text/html" data-help-name="modpackqt-config">
38
+ <p>Connection configuration for a ModPackQT instance.</p>
39
+ <h3>Properties</h3>
40
+ <dl class="message-properties">
41
+ <dt>Host <span class="property-type">string</span></dt>
42
+ <dd>Hostname or IP address of the ModPackQT server (default: <code>localhost</code>).</dd>
43
+ <dt>Port <span class="property-type">number</span></dt>
44
+ <dd>TCP port the ModPackQT REST API listens on (default: <code>3000</code>).</dd>
45
+ <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>
47
+ </dl>
48
+ </script>
@@ -0,0 +1,27 @@
1
+ module.exports = function(RED) {
2
+ function ModPackQTConfigNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ this.host = config.host || 'localhost';
5
+ this.port = config.port || 8502;
6
+ this.apiKey = this.credentials.apiKey || '';
7
+
8
+ const axios = require('axios');
9
+ this.baseUrl = `http://${this.host}:${this.port}`;
10
+
11
+ this.request = async function(method, path, data) {
12
+ const headers = {};
13
+ if (this.apiKey) headers['x-api-key'] = this.apiKey;
14
+ const response = await axios({
15
+ method,
16
+ url: `${this.baseUrl}${path}`,
17
+ data,
18
+ headers,
19
+ timeout: 10000
20
+ });
21
+ return response.data;
22
+ };
23
+ }
24
+ RED.nodes.registerType('modpackqt-config', ModPackQTConfigNode, {
25
+ credentials: { apiKey: { type: 'password' } }
26
+ });
27
+ };
@@ -0,0 +1,89 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('modpackqt-read', {
3
+ category: 'ModPackQT',
4
+ color: '#2563eb',
5
+ defaults: {
6
+ name: { value: '' },
7
+ server: { value: '', type: 'modpackqt-config', required: true },
8
+ targetHost: { value: 'localhost', required: true },
9
+ targetPort: { value: 502, required: true, validate: RED.validators.number() },
10
+ unitId: { value: 1, required: true, validate: RED.validators.number() },
11
+ functionCode: { value: '3', required: true },
12
+ address: { value: 0, required: true, validate: RED.validators.number() },
13
+ quantity: { value: 1, required: true, validate: RED.validators.number() },
14
+ pollInterval: { value: 0, validate: RED.validators.number() }
15
+ },
16
+ inputs: 1,
17
+ outputs: 1,
18
+ icon: 'font-awesome/fa-download',
19
+ label: function() {
20
+ return this.name || `Read FC${this.functionCode} @${this.address}`;
21
+ },
22
+ paletteLabel: 'modpackqt read'
23
+ });
24
+ </script>
25
+
26
+ <script type="text/html" data-template-name="modpackqt-read">
27
+ <div class="form-row">
28
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
29
+ <input type="text" id="node-input-name" placeholder="Name">
30
+ </div>
31
+ <div class="form-row">
32
+ <label for="node-input-server"><i class="fa fa-cog"></i> Gateway</label>
33
+ <input type="text" id="node-input-server">
34
+ </div>
35
+ <div class="form-row">
36
+ <label for="node-input-targetHost"><i class="fa fa-plug"></i> Target Host</label>
37
+ <input type="text" id="node-input-targetHost" placeholder="Modbus device IP/hostname">
38
+ </div>
39
+ <div class="form-row">
40
+ <label for="node-input-targetPort"><i class="fa fa-hashtag"></i> Target Port</label>
41
+ <input type="number" id="node-input-targetPort" min="1" max="65535" placeholder="502">
42
+ </div>
43
+ <div class="form-row">
44
+ <label for="node-input-unitId"><i class="fa fa-id-card"></i> Unit ID</label>
45
+ <input type="number" id="node-input-unitId" min="1" max="247" placeholder="1">
46
+ </div>
47
+ <div class="form-row">
48
+ <label for="node-input-functionCode"><i class="fa fa-list"></i> Function Code</label>
49
+ <select id="node-input-functionCode">
50
+ <option value="1">FC1 โ€” Read Coils</option>
51
+ <option value="2">FC2 โ€” Read Discrete Inputs</option>
52
+ <option value="3" selected>FC3 โ€” Read Holding Registers</option>
53
+ <option value="4">FC4 โ€” Read Input Registers</option>
54
+ </select>
55
+ </div>
56
+ <div class="form-row">
57
+ <label for="node-input-address"><i class="fa fa-map-marker"></i> Start Address</label>
58
+ <input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
59
+ </div>
60
+ <div class="form-row">
61
+ <label for="node-input-quantity"><i class="fa fa-sort-numeric-asc"></i> Quantity</label>
62
+ <input type="number" id="node-input-quantity" min="1" max="125" placeholder="1">
63
+ </div>
64
+ <div class="form-row">
65
+ <label for="node-input-pollInterval"><i class="fa fa-clock-o"></i> Poll Interval (ms)</label>
66
+ <input type="number" id="node-input-pollInterval" min="0" placeholder="0 = disabled">
67
+ </div>
68
+ </script>
69
+
70
+ <script type="text/html" data-help-name="modpackqt-read">
71
+ <p>Reads Modbus registers from a remote device via the ModPackQT gateway (<code>modsim.js</code>).</p>
72
+ <p>Calls <code>POST /api/modbus/tcp/read</code> on the gateway, which opens a Modbus TCP connection to the target device.</p>
73
+ <h3>Inputs</h3>
74
+ <dl class="message-properties">
75
+ <dt>payload <span class="property-type">any</span></dt>
76
+ <dd>Any incoming message triggers a read (when Poll Interval is 0).</dd>
77
+ </dl>
78
+ <h3>Outputs</h3>
79
+ <dl class="message-properties">
80
+ <dt>payload <span class="property-type">object</span></dt>
81
+ <dd>Contains <code>values</code> (array) and <code>success</code>.</dd>
82
+ <dt>topic <span class="property-type">string</span></dt>
83
+ <dd><code>modbus/read/{targetHost}:{targetPort}</code></dd>
84
+ </dl>
85
+ <h3>Details</h3>
86
+ <p><b>Gateway</b> โ€” host and port of the running <code>modsim.js</code> process (default: <code>localhost:8502</code>).</p>
87
+ <p><b>Target Host / Target Port</b> โ€” the actual Modbus TCP slave device to read from.</p>
88
+ <p>Set <b>Poll Interval</b> &gt; 0 to continuously poll on a timer.</p>
89
+ </script>
@@ -0,0 +1,50 @@
1
+ module.exports = function(RED) {
2
+ function ModPackQTReadNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+ const server = RED.nodes.getNode(config.server);
6
+
7
+ 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) || 3;
11
+ node.address = parseInt(config.address) || 0;
12
+ node.quantity = parseInt(config.quantity) || 1;
13
+ node.pollInterval = parseInt(config.pollInterval) || 0;
14
+
15
+ const FC_TO_REGISTER_TYPE = { 1: 'coil', 2: 'discrete', 3: 'holding', 4: 'input' };
16
+
17
+ let timer = null;
18
+
19
+ async function doRead() {
20
+ if (!server) {
21
+ node.status({ fill: 'red', shape: 'ring', text: 'no config' });
22
+ return;
23
+ }
24
+ try {
25
+ const registerType = FC_TO_REGISTER_TYPE[node.functionCode] || 'holding';
26
+ const result = await server.request('POST', '/api/modbus/tcp/read', {
27
+ host: node.targetHost,
28
+ port: node.targetPort,
29
+ unitId: node.unitId,
30
+ registerType,
31
+ address: node.address,
32
+ quantity: node.quantity
33
+ });
34
+ node.status({ fill: 'green', shape: 'dot', text: `FC${node.functionCode} @${node.address}` });
35
+ node.send({ payload: result, topic: `modbus/read/${node.targetHost}:${node.targetPort}` });
36
+ } catch (err) {
37
+ node.status({ fill: 'red', shape: 'dot', text: err.message });
38
+ node.error(err.message);
39
+ }
40
+ }
41
+
42
+ if (node.pollInterval > 0) {
43
+ timer = setInterval(doRead, node.pollInterval);
44
+ }
45
+
46
+ node.on('input', async function() { await doRead(); });
47
+ node.on('close', function() { if (timer) clearInterval(timer); });
48
+ }
49
+ RED.nodes.registerType('modpackqt-read', ModPackQTReadNode);
50
+ };
@@ -0,0 +1,77 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('modpackqt-slave-read', {
3
+ category: 'ModPackQT',
4
+ color: '#7c3aed',
5
+ defaults: {
6
+ name: { value: '' },
7
+ server: { value: '', type: 'modpackqt-config', required: true },
8
+ slaveId: { value: '', required: true },
9
+ registerType: { value: 'holding', required: true },
10
+ address: { value: 0, required: true, validate: RED.validators.number() },
11
+ quantity: { value: 1, required: true, validate: RED.validators.number() },
12
+ pollInterval: { value: 0, validate: RED.validators.number() }
13
+ },
14
+ inputs: 1,
15
+ outputs: 1,
16
+ icon: 'font-awesome/fa-server',
17
+ label: function() {
18
+ return this.name || `Slave Read ${this.registerType} @${this.address}`;
19
+ },
20
+ paletteLabel: 'slave read'
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="modpackqt-slave-read">
25
+ <div class="form-row">
26
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
27
+ <input type="text" id="node-input-name" placeholder="Name">
28
+ </div>
29
+ <div class="form-row">
30
+ <label for="node-input-server"><i class="fa fa-cog"></i> Gateway</label>
31
+ <input type="text" id="node-input-server">
32
+ </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="Slave ID from modsim.js">
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
39
+ <select id="node-input-registerType">
40
+ <option value="holding" selected>Holding Registers</option>
41
+ <option value="coil">Coils</option>
42
+ <option value="input">Input Registers</option>
43
+ <option value="discrete">Discrete Inputs</option>
44
+ </select>
45
+ </div>
46
+ <div class="form-row">
47
+ <label for="node-input-address"><i class="fa fa-map-marker"></i> Start Address</label>
48
+ <input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
49
+ </div>
50
+ <div class="form-row">
51
+ <label for="node-input-quantity"><i class="fa fa-sort-numeric-asc"></i> Quantity</label>
52
+ <input type="number" id="node-input-quantity" min="1" max="125" placeholder="1">
53
+ </div>
54
+ <div class="form-row">
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">
57
+ </div>
58
+ </script>
59
+
60
+ <script type="text/html" data-help-name="modpackqt-slave-read">
61
+ <p>Reads registers from a ModPackQT simulated slave via the gateway (<code>modsim.js</code>).</p>
62
+ <p>Calls <code>GET /api/slaves/:id/registers</code> on the gateway.</p>
63
+ <h3>Inputs</h3>
64
+ <dl class="message-properties">
65
+ <dt>payload <span class="property-type">any</span></dt>
66
+ <dd>Any message triggers a read (when Poll Interval is 0).</dd>
67
+ </dl>
68
+ <h3>Outputs</h3>
69
+ <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>
72
+ <dt>topic <span class="property-type">string</span></dt>
73
+ <dd><code>slave/{slaveId}/{registerType}/{address}</code></dd>
74
+ </dl>
75
+ <h3>Details</h3>
76
+ <p><b>Slave ID</b> โ€” the ID of the slave as configured in <code>modsim.js</code>. Find it in the ModPackQT app or via <code>GET /api/slaves</code>.</p>
77
+ </script>
@@ -0,0 +1,45 @@
1
+ module.exports = function(RED) {
2
+ function ModPackQTSlaveReadNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+ const server = RED.nodes.getNode(config.server);
6
+
7
+ node.slaveId = config.slaveId || '';
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;
12
+
13
+ let timer = null;
14
+
15
+ async function doRead() {
16
+ if (!server) {
17
+ node.status({ fill: 'red', shape: 'ring', text: 'no config' });
18
+ return;
19
+ }
20
+ if (!node.slaveId) {
21
+ node.status({ fill: 'red', shape: 'ring', text: 'slave ID required' });
22
+ return;
23
+ }
24
+ try {
25
+ const result = await server.request(
26
+ 'GET',
27
+ `/api/slaves/${encodeURIComponent(node.slaveId)}/registers?type=${node.registerType}&offset=${node.address}&limit=${node.quantity}`
28
+ );
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}` });
31
+ } catch (err) {
32
+ node.status({ fill: 'red', shape: 'dot', text: err.message });
33
+ node.error(err.message);
34
+ }
35
+ }
36
+
37
+ if (node.pollInterval > 0) {
38
+ timer = setInterval(doRead, node.pollInterval);
39
+ }
40
+
41
+ node.on('input', async function() { await doRead(); });
42
+ node.on('close', function() { if (timer) clearInterval(timer); });
43
+ }
44
+ RED.nodes.registerType('modpackqt-slave-read', ModPackQTSlaveReadNode);
45
+ };
@@ -0,0 +1,65 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('modpackqt-slave-write', {
3
+ category: 'ModPackQT',
4
+ color: '#7c3aed',
5
+ defaults: {
6
+ name: { value: '' },
7
+ server: { value: '', type: 'modpackqt-config', required: true },
8
+ slaveId: { value: '', required: true },
9
+ registerType: { value: 'holding', required: true },
10
+ address: { value: 0, required: true, validate: RED.validators.number() }
11
+ },
12
+ inputs: 1,
13
+ outputs: 1,
14
+ icon: 'font-awesome/fa-hdd-o',
15
+ label: function() {
16
+ return this.name || `Slave Write ${this.registerType} @${this.address}`;
17
+ },
18
+ paletteLabel: 'slave write'
19
+ });
20
+ </script>
21
+
22
+ <script type="text/html" data-template-name="modpackqt-slave-write">
23
+ <div class="form-row">
24
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
25
+ <input type="text" id="node-input-name" placeholder="Name">
26
+ </div>
27
+ <div class="form-row">
28
+ <label for="node-input-server"><i class="fa fa-cog"></i> Gateway</label>
29
+ <input type="text" id="node-input-server">
30
+ </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="Slave ID from modsim.js">
34
+ </div>
35
+ <div class="form-row">
36
+ <label for="node-input-registerType"><i class="fa fa-list"></i> Register Type</label>
37
+ <select id="node-input-registerType">
38
+ <option value="holding" selected>Holding Registers</option>
39
+ <option value="coil">Coils</option>
40
+ <option value="input">Input Registers</option>
41
+ <option value="discrete">Discrete Inputs</option>
42
+ </select>
43
+ </div>
44
+ <div class="form-row">
45
+ <label for="node-input-address"><i class="fa fa-map-marker"></i> Start Address</label>
46
+ <input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
47
+ </div>
48
+ </script>
49
+
50
+ <script type="text/html" data-help-name="modpackqt-slave-write">
51
+ <p>Writes values into a ModPackQT simulated slave register table via the gateway (<code>modsim.js</code>).</p>
52
+ <p>Calls <code>PATCH /api/slaves/:id/registers</code> on the gateway. The slave must be running.</p>
53
+ <h3>Inputs</h3>
54
+ <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>
57
+ </dl>
58
+ <h3>Outputs</h3>
59
+ <dl class="message-properties">
60
+ <dt>payload <span class="property-type">any</span></dt>
61
+ <dd>Original message passed through with <code>msg.success = true</code>.</dd>
62
+ </dl>
63
+ <h3>Details</h3>
64
+ <p><b>Slave ID</b> โ€” the ID of the slave as configured in <code>modsim.js</code>. The slave must be started before writing.</p>
65
+ </script>
@@ -0,0 +1,37 @@
1
+ module.exports = function(RED) {
2
+ function ModPackQTSlaveWriteNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+ const server = RED.nodes.getNode(config.server);
6
+
7
+ node.slaveId = config.slaveId || '';
8
+ node.registerType = config.registerType || 'holding';
9
+ node.address = parseInt(config.address) || 0;
10
+
11
+ node.on('input', async function(msg) {
12
+ if (!server) {
13
+ node.status({ fill: 'red', shape: 'ring', text: 'no config' });
14
+ return;
15
+ }
16
+ if (!node.slaveId) {
17
+ node.status({ fill: 'red', shape: 'ring', text: 'slave ID required' });
18
+ return;
19
+ }
20
+ 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
+ values
26
+ });
27
+ node.status({ fill: 'green', shape: 'dot', text: `${node.registerType} @${node.address}` });
28
+ msg.success = true;
29
+ node.send(msg);
30
+ } catch (err) {
31
+ node.status({ fill: 'red', shape: 'dot', text: err.message });
32
+ node.error(err.message, msg);
33
+ }
34
+ });
35
+ }
36
+ RED.nodes.registerType('modpackqt-slave-write', ModPackQTSlaveWriteNode);
37
+ };
@@ -0,0 +1,76 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('modpackqt-write', {
3
+ category: 'ModPackQT',
4
+ color: '#2563eb',
5
+ defaults: {
6
+ name: { value: '' },
7
+ server: { value: '', type: 'modpackqt-config', required: true },
8
+ targetHost: { value: 'localhost', required: true },
9
+ targetPort: { value: 502, required: true, validate: RED.validators.number() },
10
+ unitId: { value: 1, required: true, validate: RED.validators.number() },
11
+ functionCode: { value: '6', required: true },
12
+ address: { value: 0, required: true, validate: RED.validators.number() }
13
+ },
14
+ inputs: 1,
15
+ outputs: 1,
16
+ icon: 'font-awesome/fa-upload',
17
+ label: function() {
18
+ return this.name || `Write FC${this.functionCode} @${this.address}`;
19
+ },
20
+ paletteLabel: 'modpackqt write'
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="modpackqt-write">
25
+ <div class="form-row">
26
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
27
+ <input type="text" id="node-input-name" placeholder="Name">
28
+ </div>
29
+ <div class="form-row">
30
+ <label for="node-input-server"><i class="fa fa-cog"></i> Gateway</label>
31
+ <input type="text" id="node-input-server">
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-targetHost"><i class="fa fa-plug"></i> Target Host</label>
35
+ <input type="text" id="node-input-targetHost" placeholder="Modbus device IP/hostname">
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-input-targetPort"><i class="fa fa-hashtag"></i> Target Port</label>
39
+ <input type="number" id="node-input-targetPort" min="1" max="65535" placeholder="502">
40
+ </div>
41
+ <div class="form-row">
42
+ <label for="node-input-unitId"><i class="fa fa-id-card"></i> Unit ID</label>
43
+ <input type="number" id="node-input-unitId" min="1" max="247" placeholder="1">
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-functionCode"><i class="fa fa-list"></i> Function Code</label>
47
+ <select id="node-input-functionCode">
48
+ <option value="5">FC5 โ€” Write Single Coil</option>
49
+ <option value="6" selected>FC6 โ€” Write Single Register</option>
50
+ <option value="15">FC15 โ€” Write Multiple Coils</option>
51
+ <option value="16">FC16 โ€” Write Multiple Registers</option>
52
+ </select>
53
+ </div>
54
+ <div class="form-row">
55
+ <label for="node-input-address"><i class="fa fa-map-marker"></i> Start Address</label>
56
+ <input type="number" id="node-input-address" min="0" max="65535" placeholder="0">
57
+ </div>
58
+ </script>
59
+
60
+ <script type="text/html" data-help-name="modpackqt-write">
61
+ <p>Writes Modbus registers to a remote device via the ModPackQT gateway (<code>modsim.js</code>).</p>
62
+ <p>Calls <code>POST /api/modbus/tcp/write</code> on the gateway.</p>
63
+ <h3>Inputs</h3>
64
+ <dl class="message-properties">
65
+ <dt>payload <span class="property-type">number | array</span></dt>
66
+ <dd>The value(s) to write. A single number for FC5/FC6, or an array for FC15/FC16.</dd>
67
+ </dl>
68
+ <h3>Outputs</h3>
69
+ <dl class="message-properties">
70
+ <dt>payload <span class="property-type">any</span></dt>
71
+ <dd>Original message is passed through with <code>msg.success = true</code> on success.</dd>
72
+ </dl>
73
+ <h3>Details</h3>
74
+ <p><b>Gateway</b> โ€” host and port of the running <code>modsim.js</code> process (default: <code>localhost:8502</code>).</p>
75
+ <p><b>Target Host / Target Port</b> โ€” the actual Modbus TCP slave device to write to.</p>
76
+ </script>
@@ -0,0 +1,41 @@
1
+ module.exports = function(RED) {
2
+ function ModPackQTWriteNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+ const server = RED.nodes.getNode(config.server);
6
+
7
+ 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;
12
+
13
+ const FC_TO_REGISTER_TYPE = { 5: 'coil', 6: 'holding', 15: 'coil', 16: 'holding' };
14
+
15
+ node.on('input', async function(msg) {
16
+ if (!server) {
17
+ node.status({ fill: 'red', shape: 'ring', text: 'no config' });
18
+ return;
19
+ }
20
+ try {
21
+ const values = Array.isArray(msg.payload) ? msg.payload : [msg.payload];
22
+ const registerType = FC_TO_REGISTER_TYPE[node.functionCode] || 'holding';
23
+ await server.request('POST', '/api/modbus/tcp/write', {
24
+ host: node.targetHost,
25
+ port: node.targetPort,
26
+ unitId: node.unitId,
27
+ registerType,
28
+ address: node.address,
29
+ values
30
+ });
31
+ node.status({ fill: 'green', shape: 'dot', text: `FC${node.functionCode} @${node.address}` });
32
+ msg.success = true;
33
+ node.send(msg);
34
+ } catch (err) {
35
+ node.status({ fill: 'red', shape: 'dot', text: err.message });
36
+ node.error(err.message, msg);
37
+ }
38
+ });
39
+ }
40
+ RED.nodes.registerType('modpackqt-write', ModPackQTWriteNode);
41
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "node-red-contrib-modbus-modpackqt",
3
+ "version": "1.1.0",
4
+ "description": "Node-RED nodes for ModPackQT โ€” read/write Modbus TCP registers (master & slave) via the ModPackQT local gateway. Supports FC1/FC2/FC3/FC4 reads and FC5/FC6/FC15/FC16 writes.",
5
+ "keywords": [
6
+ "node-red",
7
+ "modbus",
8
+ "modpackqt",
9
+ "modbus-tcp",
10
+ "modbus-rtu",
11
+ "modbus-master",
12
+ "modbus-slave",
13
+ "industrial",
14
+ "automation",
15
+ "holding-register",
16
+ "coil",
17
+ "iiot",
18
+ "plc"
19
+ ],
20
+ "license": "MIT",
21
+ "author": "ModPackQT",
22
+ "homepage": "https://modpackqt.com",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/jomer0411/Modsimcontrol.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/jomer0411/Modsimcontrol/issues"
29
+ },
30
+ "engines": {
31
+ "node": ">=14"
32
+ },
33
+ "node-red": {
34
+ "version": ">=2.0.0",
35
+ "nodes": {
36
+ "modpackqt-config": "nodes/modpackqt-config.js",
37
+ "modpackqt-read": "nodes/modpackqt-read.js",
38
+ "modpackqt-write": "nodes/modpackqt-write.js",
39
+ "modpackqt-slave-read": "nodes/modpackqt-slave-read.js",
40
+ "modpackqt-slave-write": "nodes/modpackqt-slave-write.js"
41
+ }
42
+ },
43
+ "dependencies": {
44
+ "axios": "^1.6.0"
45
+ }
46
+ }