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.
@@ -0,0 +1,90 @@
1
+ <script type="text/html" data-template-name="modbus-dynamic-server">
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-server"><i class="fa fa-cog"></i> Server</label>
9
+ <input type="text" id="node-input-server">
10
+ </div>
11
+ </script>
12
+
13
+ <script type="text/html" data-help-name="modbus-dynamic-server">
14
+ <p>Receives incoming Modbus TCP requests from a <code>modbus-dynamic-server-config</code> node
15
+ and emits them as messages for downstream processing. Each message must eventually be answered
16
+ by a <code>modbus-dynamic-server-response</code> or <code>modbus-registers-respond</code> node.</p>
17
+
18
+ <h3>Outputs</h3>
19
+ <ol class="node-ports">
20
+ <li>Request message
21
+ <dl class="message-properties">
22
+ <dt>payload <span class="property-type">object</span></dt>
23
+ <dd>The full jsmodbus request object, including <code>body</code>, <code>unitId</code>,
24
+ and <code>id</code> (transaction ID).</dd>
25
+
26
+ <dt>modbus.requestId <span class="property-type">string</span></dt>
27
+ <dd>UUID that uniquely identifies this pending request. Must be passed unchanged to
28
+ the response node.</dd>
29
+
30
+ <dt>_modbus <span class="property-type">object</span></dt>
31
+ <dd>Internal request-response context used by downstream response nodes.
32
+ Includes <code>requestId</code> and <code>configNodeId</code>. This object must be
33
+ preserved in flow messages.</dd>
34
+
35
+ <dt>modbus.eventName <span class="property-type">string</span></dt>
36
+ <dd>Modbus function-code event name, e.g. <code>readHoldingRegisters</code>,
37
+ <code>writeSingleCoil</code>. See Details for the full list.</dd>
38
+
39
+ <dt>modbus.address <span class="property-type">number</span></dt>
40
+ <dd>Starting register or coil address from the request.</dd>
41
+
42
+ <dt>modbus.quantity <span class="property-type">number</span></dt>
43
+ <dd>Number of registers or coils requested.</dd>
44
+
45
+ <dt>topic <span class="property-type">string</span></dt>
46
+ <dd>Always <code>modbus/request</code>.</dd>
47
+ </dl>
48
+ </li>
49
+ </ol>
50
+
51
+ <h3>Details</h3>
52
+ <p>Supported <code>eventName</code> values:</p>
53
+ <ul>
54
+ <li><code>readCoils</code> (FC1)</li>
55
+ <li><code>readDiscreteInputs</code> (FC2)</li>
56
+ <li><code>readHoldingRegisters</code> (FC3)</li>
57
+ <li><code>readInputRegisters</code> (FC4)</li>
58
+ <li><code>writeSingleCoil</code> (FC5)</li>
59
+ <li><code>writeSingleRegister</code> (FC6)</li>
60
+ <li><code>writeMultipleCoils</code> (FC15)</li>
61
+ <li><code>writeMultipleRegisters</code> (FC16)</li>
62
+ </ul>
63
+ <p>Every emitted message must be answered within the timeout configured on the server config
64
+ node, otherwise the client will receive a Gateway Path Unavailable exception automatically.</p>
65
+ <p>When editing messages in function nodes, preserve the internal context:</p>
66
+ <pre><code>// CORRECT
67
+ msg.payload = newValue;
68
+ return msg;
69
+
70
+ // INCORRECT: request context is lost
71
+ msg = { payload: newValue };
72
+ return msg;</code></pre>
73
+ </script>
74
+
75
+ <script type="text/javascript">
76
+ RED.nodes.registerType('modbus-dynamic-server', {
77
+ category: 'modbus',
78
+ color: '#E9967A',
79
+ defaults: {
80
+ name: { value: '' },
81
+ server: { value: '', type: 'modbus-dynamic-server-config', required: true }
82
+ },
83
+ inputs: 0,
84
+ outputs: 1,
85
+ icon: 'bridge.svg',
86
+ label: function () {
87
+ return this.name || 'Modbus Dynamic Server Request'
88
+ }
89
+ })
90
+ </script>
@@ -0,0 +1,45 @@
1
+ module.exports = function (RED) {
2
+ function ModbusFlexServerNode (config) {
3
+ RED.nodes.createNode(this, config)
4
+ const node = this
5
+
6
+ node.server = RED.nodes.getNode(config.server)
7
+
8
+ if (!node.server) {
9
+ node.status({ fill: 'red', shape: 'ring', text: 'no server' })
10
+ return
11
+ }
12
+
13
+ node.status({ fill: 'grey', shape: 'ring', text: 'stub' })
14
+
15
+ node.server.emitter.on('request', function (data) {
16
+ const msg = {
17
+ topic: 'modbus/request',
18
+ _modbus: {
19
+ requestId: data.requestId,
20
+ configNodeId: node.server.id,
21
+ eventName: data.eventName,
22
+ address: data.request.address,
23
+ quantity: data.request.quantity,
24
+ connection: data.connection || null
25
+ },
26
+ modbus: {
27
+ requestId: data.requestId,
28
+ eventName: data.eventName,
29
+ address: data.request.address,
30
+ quantity: data.request.quantity,
31
+ connection: data.connection || null
32
+ },
33
+ payload: data.request
34
+ }
35
+
36
+ node.send(msg)
37
+ })
38
+
39
+ node.on('close', function () {
40
+ // cleanup later
41
+ })
42
+ }
43
+
44
+ RED.nodes.registerType('modbus-dynamic-server', ModbusFlexServerNode)
45
+ }
@@ -0,0 +1,205 @@
1
+ <script type="text/html" data-template-name="modbus-fc-filter">
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 style="vertical-align: top;"><i class="fa fa-check-square-o"></i> Allowed Standard FCs</label>
9
+ <div style="display: inline-block; width: calc(100% - 110px);">
10
+ <label for="node-input-allowFc1" style="display:block; width:auto; margin:2px 0;">
11
+ <input type="checkbox" id="node-input-allowFc1" style="width:auto; margin-right:6px;">1 - Read Coils
12
+ </label>
13
+ <label for="node-input-allowFc2" style="display:block; width:auto; margin:2px 0;">
14
+ <input type="checkbox" id="node-input-allowFc2" style="width:auto; margin-right:6px;">2 - Read Discrete Inputs
15
+ </label>
16
+ <label for="node-input-allowFc3" style="display:block; width:auto; margin:2px 0;">
17
+ <input type="checkbox" id="node-input-allowFc3" style="width:auto; margin-right:6px;">3 - Read Holding Registers
18
+ </label>
19
+ <label for="node-input-allowFc4" style="display:block; width:auto; margin:2px 0;">
20
+ <input type="checkbox" id="node-input-allowFc4" style="width:auto; margin-right:6px;">4 - Read Input Registers
21
+ </label>
22
+ <label for="node-input-allowFc5" style="display:block; width:auto; margin:2px 0;">
23
+ <input type="checkbox" id="node-input-allowFc5" style="width:auto; margin-right:6px;">5 - Write Single Coil
24
+ </label>
25
+ <label for="node-input-allowFc6" style="display:block; width:auto; margin:2px 0;">
26
+ <input type="checkbox" id="node-input-allowFc6" style="width:auto; margin-right:6px;">6 - Write Single Register
27
+ </label>
28
+ <label for="node-input-allowFc15" style="display:block; width:auto; margin:2px 0;">
29
+ <input type="checkbox" id="node-input-allowFc15" style="width:auto; margin-right:6px;">15 - Write Multiple Coils
30
+ </label>
31
+ <label for="node-input-allowFc16" style="display:block; width:auto; margin:2px 0;">
32
+ <input type="checkbox" id="node-input-allowFc16" style="width:auto; margin-right:6px;">16 - Write Multiple Registers
33
+ </label>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="form-row">
38
+ <label style="vertical-align:top;"><i class="fa fa-plus-square"></i> Additional FCs</label>
39
+ <ol id="node-input-custom-fc-container" style="display:inline-block; width: calc(100% - 110px); margin:0;"></ol>
40
+ </div>
41
+
42
+ <div class="form-row">
43
+ <label for="node-input-exceptionCode"><i class="fa fa-ban"></i> Exception on Reject</label>
44
+ <select id="node-input-exceptionCode" style="width: calc(100% - 110px);">
45
+ <option value="1">1 - Illegal Function</option>
46
+ <option value="2">2 - Illegal Data Address</option>
47
+ <option value="3">3 - Illegal Data Value</option>
48
+ <option value="4">4 - Server Device Failure</option>
49
+ </select>
50
+ </div>
51
+ </script>
52
+
53
+ <script type="text/html" data-help-name="modbus-fc-filter">
54
+ <p>Filters incoming Modbus requests by function code (<code>msg.payload.fc</code>).</p>
55
+
56
+ <p>If the function code is allowed, the message is forwarded unchanged.
57
+ If the function code is not allowed, the message is not emitted and a Modbus
58
+ exception response is sent for the pending request.</p>
59
+
60
+ <h3>Inputs</h3>
61
+ <dl class="message-properties">
62
+ <dt>payload.fc <span class="property-type">number</span></dt>
63
+ <dd>Required Modbus function code from the incoming request message.</dd>
64
+
65
+ <dt>_modbus.requestId <span class="property-type">string</span></dt>
66
+ <dd>Required internal request context used when blocked requests are terminated.</dd>
67
+
68
+ <dt>_modbus.configNodeId <span class="property-type">string</span></dt>
69
+ <dd>Required internal config-node identifier used to send the exception response.</dd>
70
+ </dl>
71
+
72
+ <h3>Outputs</h3>
73
+ <ol class="node-ports">
74
+ <li>Allowed request
75
+ <dl class="message-properties">
76
+ <dt>payload <span class="property-type">object</span></dt>
77
+ <dd>The original request message, forwarded unchanged when <code>payload.fc</code> is allowed.</dd>
78
+ </dl>
79
+ </li>
80
+ </ol>
81
+
82
+ <h3>Configuration</h3>
83
+ <dl class="message-properties">
84
+ <dt>Allowed Standard FCs <span class="property-type">checkboxes</span></dt>
85
+ <dd>Select from common Modbus function codes (1, 2, 3, 4, 5, 6, 15, 16).</dd>
86
+
87
+ <dt>Additional FCs <span class="property-type">list</span></dt>
88
+ <dd>Add custom/proprietary function codes (integer 1-255). Invalid entries are discarded.
89
+ Duplicates are ignored.</dd>
90
+
91
+ <dt>Exception on Reject <span class="property-type">select</span></dt>
92
+ <dd>Modbus exception code returned when a request is blocked. Default is
93
+ <b>1 - Illegal Function</b>.</dd>
94
+ </dl>
95
+
96
+ <h3>Details</h3>
97
+ <p>Matching is exact numeric equality only. No ranges and no multi-output routing are used.</p>
98
+ <p>This node is useful for read-only and restricted-function Modbus gateway flows.</p>
99
+ <p>Flows must preserve <code>msg._modbus</code>:</p>
100
+ <pre><code>// CORRECT
101
+ msg.payload = newValue;
102
+ return msg;
103
+
104
+ // INCORRECT: request context is lost
105
+ msg = { payload: newValue };
106
+ return msg;</code></pre>
107
+ </script>
108
+
109
+ <script type="text/javascript">
110
+ ;(function () {
111
+ function normalizeCustomCodes (values) {
112
+ if (!Array.isArray(values)) return []
113
+
114
+ const dedupe = new Set()
115
+ const out = []
116
+ values.forEach(function (value) {
117
+ const n = Number(value)
118
+ if (!Number.isInteger(n) || n < 1 || n > 255) return
119
+ if (dedupe.has(n)) return
120
+ dedupe.add(n)
121
+ out.push(n)
122
+ })
123
+ return out
124
+ }
125
+
126
+ function customCodeValidator (values) {
127
+ if (!Array.isArray(values)) return false
128
+ return values.every(function (value) {
129
+ const n = Number(value)
130
+ return Number.isInteger(n) && n >= 1 && n <= 255
131
+ })
132
+ }
133
+
134
+ RED.nodes.registerType('modbus-fc-filter', {
135
+ category: 'modbus',
136
+ color: '#E9967A',
137
+ defaults: {
138
+ name: { value: '' },
139
+ allowFc1: { value: true },
140
+ allowFc2: { value: true },
141
+ allowFc3: { value: true },
142
+ allowFc4: { value: true },
143
+ allowFc5: { value: false },
144
+ allowFc6: { value: false },
145
+ allowFc15: { value: false },
146
+ allowFc16: { value: false },
147
+ customFunctionCodes: {
148
+ value: [],
149
+ validate: customCodeValidator
150
+ },
151
+ exceptionCode: { value: 1, required: true }
152
+ },
153
+ inputs: 1,
154
+ outputs: 1,
155
+ icon: 'switch.svg',
156
+
157
+ label: function () {
158
+ return this.name || 'Modbus FC Filter'
159
+ },
160
+
161
+ oneditprepare: function () {
162
+ const container = $('#node-input-custom-fc-container')
163
+
164
+ container.css('min-height', '120px').editableList({
165
+ addButton: 'Add Function Code',
166
+ removable: true,
167
+ sortable: true,
168
+ addItem: function (row, index, data) {
169
+ const input = $('<input/>', {
170
+ type: 'number',
171
+ min: 1,
172
+ max: 255,
173
+ class: 'node-input-custom-fc',
174
+ placeholder: 'Function code (1-255)',
175
+ style: 'width:100%;'
176
+ })
177
+
178
+ if (data && data.value !== undefined && data.value !== null && data.value !== '') {
179
+ input.val(data.value)
180
+ }
181
+
182
+ row.css({ display: 'flex', alignItems: 'center' })
183
+ row.append(input)
184
+ }
185
+ })
186
+
187
+ const existing = normalizeCustomCodes(this.customFunctionCodes)
188
+ existing.forEach(function (fc) {
189
+ container.editableList('addItem', { value: fc })
190
+ })
191
+ },
192
+
193
+ oneditsave: function () {
194
+ const values = []
195
+ $('#node-input-custom-fc-container').editableList('items').each(function () {
196
+ const val = $(this).find('input.node-input-custom-fc').val()
197
+ if (val === '' || val === null || val === undefined) return
198
+ values.push(val)
199
+ })
200
+ this.customFunctionCodes = normalizeCustomCodes(values)
201
+ }
202
+ })
203
+ })()
204
+ </script>
205
+
@@ -0,0 +1,85 @@
1
+ const { respondWithPayload } = require('./modbus-response-context')
2
+
3
+ module.exports = function (RED) {
4
+ 'use strict'
5
+
6
+ const STANDARD_FC_FLAGS = [
7
+ ['allowFc1', 1],
8
+ ['allowFc2', 2],
9
+ ['allowFc3', 3],
10
+ ['allowFc4', 4],
11
+ ['allowFc5', 5],
12
+ ['allowFc6', 6],
13
+ ['allowFc15', 15],
14
+ ['allowFc16', 16]
15
+ ]
16
+
17
+ function toValidFc (value) {
18
+ const n = Number(value)
19
+ if (!Number.isInteger(n) || n < 1 || n > 255) return null
20
+ return n
21
+ }
22
+
23
+ function normalizeBoolean (value) {
24
+ if (typeof value === 'boolean') return value
25
+ if (typeof value === 'string') {
26
+ const lowered = value.trim().toLowerCase()
27
+ if (lowered === 'false' || lowered === '0' || lowered === 'off' || lowered === 'no') return false
28
+ }
29
+ return Boolean(value)
30
+ }
31
+
32
+ function buildAllowedFcSet (config) {
33
+ const allowed = new Set()
34
+
35
+ STANDARD_FC_FLAGS.forEach(function ([flag, fc]) {
36
+ if (normalizeBoolean(config[flag])) allowed.add(fc)
37
+ })
38
+
39
+ if (Array.isArray(config.customFunctionCodes)) {
40
+ config.customFunctionCodes.forEach(function (value) {
41
+ const fc = toValidFc(value)
42
+ if (fc !== null) allowed.add(fc)
43
+ })
44
+ }
45
+
46
+ return allowed
47
+ }
48
+
49
+ function ModbusFcFilterNode (config) {
50
+ RED.nodes.createNode(this, config)
51
+ const node = this
52
+
53
+ node.exceptionCode = toValidFc(config.exceptionCode) || 1
54
+ node.allowedFcSet = buildAllowedFcSet(config)
55
+
56
+ if (node.allowedFcSet.size === 0) {
57
+ node.status({ fill: 'yellow', shape: 'ring', text: 'no function codes allowed' })
58
+ } else {
59
+ node.status({ fill: 'grey', shape: 'ring', text: 'ready' })
60
+ }
61
+
62
+ node.on('input', function (msg, send, done) {
63
+ const incomingFc = toValidFc(msg && msg.payload ? msg.payload.fc : undefined)
64
+
65
+ if (incomingFc !== null && node.allowedFcSet.has(incomingFc)) {
66
+ node.status({ fill: 'green', shape: 'dot', text: `allow fc ${incomingFc}` })
67
+ send(msg)
68
+ done()
69
+ return
70
+ }
71
+
72
+ const responseResult = respondWithPayload(RED, node, msg, { exception: node.exceptionCode })
73
+ if (!responseResult.context) {
74
+ node.status({ fill: 'red', shape: 'ring', text: 'missing context' })
75
+ }
76
+
77
+ const blockedLabel = incomingFc === null ? 'invalid' : incomingFc
78
+ node.status({ fill: 'yellow', shape: 'ring', text: `blocked fc ${blockedLabel} -> ex ${node.exceptionCode}` })
79
+ done()
80
+ })
81
+ }
82
+
83
+ RED.nodes.registerType('modbus-fc-filter', ModbusFcFilterNode)
84
+ }
85
+
@@ -0,0 +1,175 @@
1
+ <script type="text/html" data-template-name="modbus-proxy-target">
2
+ <div class="form-row">
3
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-config-input-name" placeholder="Optional name">
5
+ </div>
6
+
7
+ <div class="form-row">
8
+ <label for="node-config-input-targetType"><i class="fa fa-exchange"></i> Target Type</label>
9
+ <select id="node-config-input-targetType" onchange="updateTargetTypeUI()">
10
+ <option value="tcp">TCP</option>
11
+ <option value="serial">Serial (RTU)</option>
12
+ </select>
13
+ </div>
14
+
15
+ <!-- TCP Settings -->
16
+ <div id="tcp-settings" class="form-row" style="display:block;">
17
+ <label for="node-config-input-tcpHost"><i class="fa fa-globe"></i> Target Host</label>
18
+ <input type="text" id="node-config-input-tcpHost" placeholder="localhost">
19
+ </div>
20
+
21
+ <div id="tcp-settings" class="form-row" style="display:block;">
22
+ <label for="node-config-input-tcpPort"><i class="fa fa-random"></i> Target Port</label>
23
+ <input type="number" id="node-config-input-tcpPort" min="1" max="65535" placeholder="502">
24
+ </div>
25
+
26
+ <!-- Serial Settings -->
27
+ <div id="serial-settings" class="form-row" style="display:none;">
28
+ <label for="node-config-input-serialPort"><i class="fa fa-usb"></i> Serial Port</label>
29
+ <input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0">
30
+ </div>
31
+
32
+ <div id="serial-settings" class="form-row" style="display:none;">
33
+ <label for="node-config-input-serialBaud"><i class="fa fa-tachometer"></i> Baud Rate</label>
34
+ <select id="node-config-input-serialBaud">
35
+ <option value="300">300</option>
36
+ <option value="600">600</option>
37
+ <option value="1200">1200</option>
38
+ <option value="2400">2400</option>
39
+ <option value="4800">4800</option>
40
+ <option value="9600" selected>9600</option>
41
+ <option value="14400">14400</option>
42
+ <option value="19200">19200</option>
43
+ <option value="28800">28800</option>
44
+ <option value="38400">38400</option>
45
+ <option value="57600">57600</option>
46
+ <option value="115200">115200</option>
47
+ </select>
48
+ </div>
49
+
50
+ <div id="serial-settings" class="form-row" style="display:none;">
51
+ <label for="node-config-input-serialParity"><i class="fa fa-check"></i> Parity</label>
52
+ <select id="node-config-input-serialParity">
53
+ <option value="none" selected>None</option>
54
+ <option value="even">Even</option>
55
+ <option value="odd">Odd</option>
56
+ </select>
57
+ </div>
58
+
59
+ <div id="serial-settings" class="form-row" style="display:none;">
60
+ <label for="node-config-input-serialWordLength"><i class="fa fa-binary"></i> Word Length</label>
61
+ <select id="node-config-input-serialWordLength">
62
+ <option value="5">5</option>
63
+ <option value="6">6</option>
64
+ <option value="7">7</option>
65
+ <option value="8" selected>8</option>
66
+ </select>
67
+ </div>
68
+
69
+ <div id="serial-settings" class="form-row" style="display:none;">
70
+ <label for="node-config-input-serialStopBits"><i class="fa fa-stop"></i> Stop Bits</label>
71
+ <select id="node-config-input-serialStopBits">
72
+ <option value="1" selected>1</option>
73
+ <option value="2">2</option>
74
+ </select>
75
+ </div>
76
+
77
+ <!-- Common Settings -->
78
+ <div class="form-row">
79
+ <label for="node-config-input-timeout"><i class="fa fa-clock-o"></i> Timeout (ms)</label>
80
+ <input type="number" id="node-config-input-timeout" min="100" placeholder="5000">
81
+ </div>
82
+ </script>
83
+
84
+ <script type="text/javascript">
85
+ RED.nodes.registerType('modbus-proxy-target', {
86
+ category: 'config',
87
+ defaults: {
88
+ name: { value: '' },
89
+ targetType: { value: 'tcp', required: true },
90
+ tcpHost: { value: 'localhost', required: true },
91
+ tcpPort: { value: 502, required: true, validate: RED.validators.number() },
92
+ serialPort: { value: '/dev/ttyUSB0', required: true },
93
+ serialBaud: { value: 9600, required: true, validate: RED.validators.number() },
94
+ serialParity: { value: 'none', required: true },
95
+ serialWordLength: { value: 8, required: true, validate: RED.validators.number() },
96
+ serialStopBits: { value: 1, required: true, validate: RED.validators.number() },
97
+ timeout: { value: 5000, required: true, validate: RED.validators.number() }
98
+ },
99
+ label: function () {
100
+ if (this.targetType === 'tcp') {
101
+ return this.name || (this.tcpHost + ':' + this.tcpPort)
102
+ } else {
103
+ return this.name || ('Serial: ' + this.serialPort)
104
+ }
105
+ },
106
+ oneditprepare: function () {
107
+ updateTargetTypeUI()
108
+ }
109
+ })
110
+
111
+ function updateTargetTypeUI () {
112
+ const targetType = $('#node-config-input-targetType').val()
113
+ if (targetType === 'tcp') {
114
+ $('[id="tcp-settings"]').show()
115
+ $('[id="serial-settings"]').hide()
116
+ } else {
117
+ $('[id="tcp-settings"]').hide()
118
+ $('[id="serial-settings"]').show()
119
+ }
120
+ }
121
+ </script>
122
+
123
+ <script type="text/html" data-help-name="modbus-proxy-target">
124
+ <p>Configures a Modbus proxy target (TCP or serial) that can be used by
125
+ <code>modbus-dynamic-proxy</code> nodes to forward incoming Modbus requests.</p>
126
+
127
+ <h3>Configuration</h3>
128
+ <dl class="message-properties">
129
+ <dt>Name <span class="property-type">string</span></dt>
130
+ <dd>Optional label shown in the editor.</dd>
131
+
132
+ <dt>Target Type <span class="property-type">select</span></dt>
133
+ <dd>Choose between <b>TCP</b> (network socket) or <b>Serial (RTU)</b> (RS-485/RS-232).</dd>
134
+
135
+ <dt>Target Host <span class="property-type">string</span></dt>
136
+ <dd><b>TCP only:</b> IP address or hostname of the remote Modbus device.
137
+ Default: <code>localhost</code>.</dd>
138
+
139
+ <dt>Target Port <span class="property-type">number</span></dt>
140
+ <dd><b>TCP only:</b> TCP port of the remote Modbus device.
141
+ Default: <code>502</code>.</dd>
142
+
143
+ <dt>Serial Port <span class="property-type">string</span></dt>
144
+ <dd><b>Serial only:</b> Device path for the serial port, e.g. <code>/dev/ttyUSB0</code> (Linux/Mac)
145
+ or <code>COM3</code> (Windows). Default: <code>/dev/ttyUSB0</code>.</dd>
146
+
147
+ <dt>Baud Rate <span class="property-type">number</span></dt>
148
+ <dd><b>Serial only:</b> Serial communication speed. Common values: 9600, 19200, 38400, 115200.
149
+ Default: <code>9600</code>.</dd>
150
+
151
+ <dt>Parity <span class="property-type">select</span></dt>
152
+ <dd><b>Serial only:</b> Parity check mode: <b>None</b>, <b>Even</b>, or <b>Odd</b>.
153
+ Default: <b>None</b>.</dd>
154
+
155
+ <dt>Word Length <span class="property-type">number</span></dt>
156
+ <dd><b>Serial only:</b> Data bits per character. Typically <b>8</b>.
157
+ Default: <code>8</code>.</dd>
158
+
159
+ <dt>Stop Bits <span class="property-type">number</span></dt>
160
+ <dd><b>Serial only:</b> Number of stop bits (1 or 2). Default: <code>1</code>.</dd>
161
+
162
+ <dt>Timeout (ms) <span class="property-type">number</span></dt>
163
+ <dd>Maximum wait time for a response from the target device. If a response is not received
164
+ within this time, the proxy automatically replies with a Modbus exception code&nbsp;4
165
+ (Gateway Path Unavailable). Default: <code>5000</code>.</dd>
166
+ </dl>
167
+
168
+ <h3>Details</h3>
169
+ <p>One config node represents one connection target. Requests from multiple
170
+ <code>modbus-dynamic-proxy</code> nodes sharing the same target are queued internally
171
+ and processed in order to ensure reliable operation.</p>
172
+ <p>Serial support requires the <code>serialport</code> package; TCP connections use the built-in
173
+ <code>net</code> module.</p>
174
+ </script>
175
+