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 +41 -0
- package/LICENSE +19 -0
- package/README.md +180 -0
- package/package.json +75 -0
- package/src/modbus-dynamic-proxy.html +108 -0
- package/src/modbus-dynamic-proxy.js +400 -0
- package/src/modbus-dynamic-server-config.html +85 -0
- package/src/modbus-dynamic-server-config.js +391 -0
- package/src/modbus-dynamic-server-response.html +88 -0
- package/src/modbus-dynamic-server-response.js +160 -0
- package/src/modbus-dynamic-server.html +90 -0
- package/src/modbus-dynamic-server.js +45 -0
- package/src/modbus-fc-filter.html +205 -0
- package/src/modbus-fc-filter.js +85 -0
- package/src/modbus-proxy-target.html +175 -0
- package/src/modbus-proxy-target.js +302 -0
- package/src/modbus-registers-config.html +109 -0
- package/src/modbus-registers-config.js +216 -0
- package/src/modbus-registers-read.html +133 -0
- package/src/modbus-registers-read.js +204 -0
- package/src/modbus-registers-respond.html +116 -0
- package/src/modbus-registers-respond.js +286 -0
- package/src/modbus-registers-write.html +157 -0
- package/src/modbus-registers-write.js +156 -0
- package/src/modbus-response-context.js +52 -0
- package/src/modbus-server-exception.html +82 -0
- package/src/modbus-server-exception.js +44 -0
- package/src/modbus-source-router.html +296 -0
- package/src/modbus-source-router.js +120 -0
|
@@ -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 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
|
+
|