node-red-modbus-dynamic-server 0.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/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - **modbus-source-router** node — routes incoming Modbus requests to different
12
+ flow outputs based on the client's source IPv4 address. Rules are expressed
13
+ as CIDR blocks (`192.168.1.0/24`, `10.0.0.0/8`, `0.0.0.0/0`, …) and
14
+ evaluated top-to-bottom; the first matching rule wins. When no rule matches,
15
+ the node sends a configurable Modbus exception response directly back to the
16
+ client via the server config node and emits nothing on any output.
17
+ IPv6-mapped IPv4 addresses (e.g. `::ffff:192.168.1.1`) are automatically
18
+ normalised before evaluation.
19
+
20
+ ## [0.1.0] — 2025-01-01
21
+
22
+ ### Added
23
+ - Initial release.
24
+ - **modbus-dynamic-server-config** — Modbus TCP server configuration node with
25
+ per-connection in-order response delivery and pending-request timeout.
26
+ - **modbus-dynamic-server** — emits incoming Modbus requests into a Node-RED
27
+ flow as structured message objects.
28
+ - **modbus-dynamic-server-response** — sends a dynamic payload back to a
29
+ waiting Modbus client.
30
+ - **modbus-registers-config** — in-memory register map supporting holding,
31
+ input, coil, and discrete register types with multi-word encoding (int16,
32
+ uint16, int32, uint32, float32) and configurable word-order modes
33
+ (ABCD/CDAB/BADC/DCBA).
34
+ - **modbus-registers-write** — writes a single value into a register map.
35
+ - **modbus-registers-respond** — reads register values and responds to a
36
+ pending Modbus request in one step.
37
+ - **modbus-proxy-target** — configuration node that maintains a persistent
38
+ Modbus TCP client connection used by proxy flows.
39
+ - **modbus-dynamic-proxy** — forwards a pending Modbus request to a proxy
40
+ target and returns the response to the originating client.
41
+
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Christopher Piggott
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the “Software”), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # node-red-modbus-dynamic-server
2
+
3
+ ### Advanced Modbus server nodes for Node-RED
4
+
5
+ This package provides a set of Node-RED nodes for building flexible, general-purpose Modbus servers.
6
+
7
+ Unlike the server functionality in `node-red-contrib-modbus`, these nodes allow incoming Modbus requests to be intercepted before a response is sent. This makes it possible to examine, modify, filter, forward, or fulfill requests dynamically in a flow.
8
+
9
+ That enables use cases such as Modbus proxies, gateways, protocol conversion, request filtering, and other advanced server-side behaviors that are difficult or impossible with a conventional static register map.
10
+
11
+ ---
12
+
13
+ ## Included Nodes
14
+
15
+ This package contains the following nodes:
16
+
17
+ ### Runtime nodes
18
+
19
+ #### Core request/response
20
+
21
+ - **modbus-dynamic-server**
22
+ Receives incoming Modbus requests from the server and emits them into a flow for dynamic handling.
23
+
24
+ - **modbus-dynamic-server-response**
25
+ Sends a dynamic response back to the waiting Modbus client request.
26
+
27
+ #### Routing and control
28
+
29
+ - **modbus-source-router**
30
+ Routes incoming Modbus requests to different flow outputs based on the client's source IP address.
31
+ Rules are expressed as IPv4 CIDR blocks and evaluated top-to-bottom; the first matching rule wins.
32
+ If no rule matches, a configurable Modbus exception response is sent and the message is not emitted.
33
+
34
+ - **modbus-fc-filter**
35
+ Allows or blocks requests by Modbus function code (`payload.fc`).
36
+ Allowed requests continue unchanged; blocked requests are terminated with a configurable Modbus exception.
37
+
38
+ - **modbus-server-exception**
39
+ Convenience terminator node for custom reject paths.
40
+ Sends a Modbus exception response for the current pending request and emits no output.
41
+
42
+ #### Data handling
43
+
44
+ - **modbus-registers-read**
45
+ Reads and decodes values from the shared register map and emits them as a normal flow message.
46
+
47
+ - **modbus-registers-write**
48
+ Writes values into a local register store.
49
+
50
+ - **modbus-registers-respond**
51
+ Responds to Modbus requests using values from the local register store.
52
+
53
+ #### Integration / proxy
54
+
55
+ - **modbus-dynamic-proxy**
56
+ Forwards requests dynamically to another Modbus target and returns the result.
57
+
58
+ ### Configuration nodes
59
+
60
+ - **modbus-dynamic-server-config**
61
+ Configures and owns the Modbus TCP server instance.
62
+
63
+ - **modbus-registers-config**
64
+ Defines local register storage used by register-based nodes.
65
+
66
+ - **modbus-proxy-target**
67
+ Defines a downstream Modbus target used for proxying or forwarding requests.
68
+
69
+ ---
70
+
71
+ ## How the pieces fit together
72
+
73
+ A typical dynamic flow looks like this:
74
+
75
+ 1. A Modbus client sends a request to the configured server.
76
+ 2. `modbus-dynamic-server` emits a friendly request object into the flow.
77
+ 3. *(Optional)* `modbus-source-router` filters or routes the request based on the client's IP address.
78
+ 4. The flow decides what to do with the request:
79
+ - answer from local registers
80
+ - generate a value dynamically
81
+ - forward to another Modbus device
82
+ - reject or filter the request
83
+ 5. `modbus-dynamic-server-response` sends the final response back to the client.
84
+
85
+ This separation makes it possible to implement dynamic server-side logic entirely in Node-RED flows.
86
+
87
+ ---
88
+
89
+ ## Message Contract
90
+
91
+ Messages emitted by `modbus-dynamic-server` include internal request context in `msg._modbus`.
92
+
93
+ This context is required by downstream nodes such as:
94
+
95
+ - `modbus-dynamic-server-response`
96
+ - `modbus-source-router`
97
+ - `modbus-fc-filter`
98
+ - `modbus-server-exception`
99
+ - `modbus-dynamic-proxy`
100
+ - `modbus-registers-respond`
101
+
102
+ These nodes use `msg._modbus` to send a response back to the original Modbus client request.
103
+
104
+ Flows must preserve this property. This is especially important when you use other nodes, including
105
+ `function` nodes or custom nodes, to inspect requests, apply custom logic, transform payloads,
106
+ or generate custom responses.
107
+
108
+
109
+ Preferred:
110
+
111
+ ```js
112
+ msg.payload = newValue;
113
+ return msg;
114
+ ```
115
+
116
+ Also valid (safe alternative):
117
+
118
+ ```js
119
+ msg = { ...msg, payload: newValue }
120
+ return msg
121
+ ```
122
+
123
+ Not allowed:
124
+
125
+ ```js
126
+ msg = { payload: newValue };
127
+ return msg;
128
+ ```
129
+
130
+ If msg._modbus is missing, downstream response/termination nodes will be unable to complete the original Modbus request and will raise an error.
131
+
132
+ In general, modify the existing msg object rather than replacing it entirely.
133
+
134
+
135
+
136
+ ---
137
+
138
+ ## Use Cases
139
+
140
+ ### Protocol Conversion
141
+
142
+ Requests can be intercepted, decoded, and translated into requests for another protocol. This is useful for protocol mediation and conversion involving systems such as OPC UA, BACnet, MQTT, Sparkplug, or custom APIs.
143
+
144
+ ### Modbus Gateway
145
+
146
+ These nodes can be used to build a Modbus server that answers some requests from a local register cache while forwarding other requests to external devices, including Modbus TCP or Modbus serial targets.
147
+
148
+ This makes it possible to build gateways, aggregators, and hybrid local/remote data services.
149
+
150
+ ### Security, Data Diode, and Request Filtering
151
+
152
+ These nodes can also be used to build a Modbus TCP gateway that inspects requests and applies filtering rules before allowing them to proceed.
153
+
154
+ For example, requests can be filtered based on client IP address, function code, unit ID, register address, or other request attributes. This makes it possible to implement read-only gateways, request whitelisting, and "data diode"-style patterns that prevent write operations or other side effects.
155
+
156
+ ---
157
+
158
+ ## Getting Started
159
+
160
+ A minimal dynamic server setup usually includes:
161
+
162
+ - one **modbus-dynamic-server-config** node
163
+ - one **modbus-dynamic-server** node
164
+ - one or more flow nodes such as `function`, `switch`, or `change`
165
+ - one **modbus-dynamic-server-response** node
166
+
167
+ More advanced flows may also use:
168
+
169
+ - **modbus-source-router** to split traffic by client IP before any processing logic
170
+ - **modbus-fc-filter** to allow only selected function codes (for example, read-only gateways)
171
+ - **modbus-server-exception** to terminate requests from custom `switch`/`function` logic
172
+ - **modbus-registers-read** to expose cached register values to other flows or dashboards
173
+ - **modbus-registers-config** with **modbus-registers-write** and **modbus-registers-respond**
174
+ - **modbus-proxy-target** with **modbus-dynamic-proxy**
175
+
176
+
177
+ ## Examples
178
+
179
+ This package includes example flows in the `examples/` directory. After installation, they should be available from the Node-RED import dialog under the examples list for this module.
180
+
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "node-red-modbus-dynamic-server",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "A Modbus TCP server for Node-RED that lets you write your own functions to handle requests.",
6
+ "keywords": [
7
+ "node-red",
8
+ "modbus",
9
+ "modbus-tcp",
10
+ "modbus-server",
11
+ "modbus-tcp-server",
12
+ "modbus-gateway",
13
+ "modbus-proxy",
14
+ "industrial",
15
+ "iiot",
16
+ "protocol-translation",
17
+ "protocol-mediation",
18
+ "opcua",
19
+ "mqtt",
20
+ "sparkplug"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18.5"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/wz2b/node-red-modbus-dynamic-server.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/wz2b/node-red-modbus-dynamic-server/issues"
31
+ },
32
+ "homepage": "https://github.com/wz2b/node-red-modbus-dynamic-server#readme",
33
+ "author": "Christopher Piggott",
34
+ "license": "MIT",
35
+ "node-red": {
36
+ "version": ">=3",
37
+ "nodes": {
38
+ "modbus-dynamic-server": "src/modbus-dynamic-server.js",
39
+ "modbus-dynamic-server-response": "src/modbus-dynamic-server-response.js",
40
+ "modbus-dynamic-server-config": "src/modbus-dynamic-server-config.js",
41
+ "modbus-registers-config": "src/modbus-registers-config.js",
42
+ "modbus-registers-write": "src/modbus-registers-write.js",
43
+ "modbus-registers-read": "src/modbus-registers-read.js",
44
+ "modbus-registers-respond": "src/modbus-registers-respond.js",
45
+ "modbus-proxy-target": "src/modbus-proxy-target.js",
46
+ "modbus-dynamic-proxy": "src/modbus-dynamic-proxy.js",
47
+ "modbus-source-router": "src/modbus-source-router.js",
48
+ "modbus-fc-filter": "src/modbus-fc-filter.js",
49
+ "modbus-server-exception": "src/modbus-server-exception.js"
50
+ }
51
+ },
52
+
53
+ "files": [
54
+ "src",
55
+ "examples",
56
+ "README.md",
57
+ "LICENSE",
58
+ "img",
59
+ "CHANGELOG.md"
60
+ ],
61
+ "scripts": {
62
+ "test": "mocha ./test --recursive --reporter dot --timeout 6000",
63
+ "lint": "eslint modbus/**/*.js"
64
+ },
65
+ "dependencies": {
66
+ "jsmodbus": "~4.0.10"
67
+ },
68
+ "devDependencies": {
69
+ "@node-red/nodes": "^3.1.9",
70
+ "eslint": "^8.0.0",
71
+ "mocha": "^10.4.0",
72
+ "node-red": "^4.0.2",
73
+ "node-red-node-test-helper": "^0.3.4"
74
+ }
75
+ }
@@ -0,0 +1,108 @@
1
+ <script type="text/html" data-template-name="modbus-dynamic-proxy">
2
+ <div class="form-row">
3
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-input-name" placeholder="Name">
5
+ </div>
6
+
7
+ <div class="form-row">
8
+ <label for="node-input-proxyTarget"><i class="fa fa-exchange"></i> Proxy Target</label>
9
+ <input type="text" id="node-input-proxyTarget">
10
+ </div>
11
+ </script>
12
+
13
+ <script type="text/javascript">
14
+ RED.nodes.registerType('modbus-dynamic-proxy', {
15
+ category: 'modbus',
16
+ color: '#E9967A',
17
+ defaults: {
18
+ name: { value: '' },
19
+ proxyTarget: { value: '', type: 'modbus-proxy-target', required: true }
20
+ },
21
+ inputs: 1,
22
+ outputs: 1,
23
+ icon: 'arrow-out.svg',
24
+ label: function () {
25
+ return this.name || 'Modbus Dynamic Proxy'
26
+ }
27
+ })
28
+ </script>
29
+
30
+ <script type="text/html" data-help-name="modbus-dynamic-proxy">
31
+ <p>Forwards an incoming Modbus request from a <code>modbus-dynamic-server</code> node to a
32
+ remote Modbus device (via TCP or serial) using a <code>modbus-proxy-target</code> config node.
33
+ The response is automatically forwarded back to the server and also emitted on the node's
34
+ output for inspection or logging.</p>
35
+
36
+ <h3>Inputs</h3>
37
+ <dl class="message-properties">
38
+ <dt>_modbus.requestId <span class="property-type">string</span></dt>
39
+ <dd>Required internal request context field emitted by
40
+ <code>modbus-dynamic-server</code>.</dd>
41
+
42
+ <dt>_modbus.eventName <span class="property-type">string</span></dt>
43
+ <dd>Required internal Modbus function-code event name, e.g.
44
+ <code>readHoldingRegisters</code>.</dd>
45
+
46
+ <dt>_modbus.configNodeId <span class="property-type">string</span></dt>
47
+ <dd>Required internal config-node identifier used to submit response payloads.</dd>
48
+
49
+ <dt>payload <span class="property-type">object</span></dt>
50
+ <dd>The Modbus request object from the server, containing <code>address</code>,
51
+ <code>quantity</code>, and <code>body</code>.</dd>
52
+
53
+ <dt class="optional">requestBuffer <span class="property-type">Buffer</span></dt>
54
+ <dd>The raw Modbus TCP request bytes to send to the target. If not provided, the proxy will
55
+ attempt to construct it from the request object (basic support only).</dd>
56
+ </dl>
57
+
58
+ <h3>Outputs</h3>
59
+ <ol class="node-ports">
60
+ <li>Proxied response
61
+ <dl class="message-properties">
62
+ <dt>payload <span class="property-type">Buffer</span></dt>
63
+ <dd>The raw response bytes received from the remote Modbus device. Useful for caching,
64
+ inspection, or logging. On error, <code>null</code>.</dd>
65
+
66
+ <dt>modbusProxy <span class="property-type">object</span></dt>
67
+ <dd>Metadata about the proxied request:
68
+ <ul>
69
+ <li><code>requestId</code> — the UUID of the original server request.</li>
70
+ <li><code>eventName</code> — the Modbus FC event name (e.g., <code>readHoldingRegisters</code>).</li>
71
+ <li><code>fc</code> — function code number (1–16).</li>
72
+ <li><code>address</code> — register/coil starting address.</li>
73
+ <li><code>quantity</code> — number of registers/coils requested.</li>
74
+ <li><code>proxied: true</code> — indicates a successful proxy.</li>
75
+ <li><code>proxyError</code> — (on failure) description of the error (timeout, connection, etc).</li>
76
+ <li><code>exception</code> — (on failure) Modbus exception code sent to client.</li>
77
+ <li><code>parseError: true</code> — (if set) response could not be parsed.</li>
78
+ <li><code>targetType</code> — <code>tcp</code> or <code>serial</code>.</li>
79
+ </ul>
80
+ </dd>
81
+ </dl>
82
+ </li>
83
+ </ol>
84
+
85
+ <h3>Details</h3>
86
+ <p>The proxy node uses internal request context (<code>msg._modbus</code>) to complete
87
+ the original client request. Both success and failure paths (including timeouts) result in an
88
+ appropriate Modbus response being sent to the client:</p>
89
+ <ul>
90
+ <li><b>Success:</b> response from target is parsed and relayed back to client; <code>msg.modbusProxy.proxied</code> is <code>true</code>.</li>
91
+ <li><b>Timeout:</b> exception code 4 (Gateway Path Unavailable) is sent to client; payload is <code>null</code>.</li>
92
+ <li><b>Connection error:</b> exception code 4 is sent to client; payload is <code>null</code>.</li>
93
+ <li><b>Parse error:</b> exception code 4 is sent to client; <code>msg.modbusProxy.parseError</code> is <code>true</code>.</li>
94
+ </ul>
95
+ <p>The output is useful for caching layers: capture the response payload and original request
96
+ parameters (<code>fc</code>, <code>address</code>, <code>quantity</code>) to implement a request
97
+ deduplication cache that avoids repeated proxying within a configured time window.</p>
98
+ <p>Flows must preserve <code>msg._modbus</code>:</p>
99
+ <pre><code>// CORRECT
100
+ msg.payload = newValue;
101
+ return msg;
102
+
103
+ // INCORRECT: request context is lost
104
+ msg = { payload: newValue };
105
+ return msg;</code></pre>
106
+ <p>Multiple proxy nodes can share the same target; requests are queued and processed in order.</p>
107
+ </script>
108
+