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,400 @@
|
|
|
1
|
+
const jsmodbus = require('jsmodbus')
|
|
2
|
+
const ModbusTCPRequest = require('jsmodbus/dist/tcp-request').default
|
|
3
|
+
const { respondWithPayload } = require('./modbus-response-context')
|
|
4
|
+
|
|
5
|
+
module.exports = function (RED) {
|
|
6
|
+
function ModbusDynamicProxyNode (config) {
|
|
7
|
+
RED.nodes.createNode(this, config)
|
|
8
|
+
const node = this
|
|
9
|
+
|
|
10
|
+
node.name = config.name || ''
|
|
11
|
+
node.proxyTarget = RED.nodes.getNode(config.proxyTarget)
|
|
12
|
+
|
|
13
|
+
if (!node.proxyTarget) {
|
|
14
|
+
node.status({ fill: 'red', shape: 'ring', text: 'no proxy target' })
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
node._lastStatusText = 'ready'
|
|
19
|
+
|
|
20
|
+
function getConnectionStyle () {
|
|
21
|
+
if (!node.proxyTarget) {
|
|
22
|
+
return { fill: 'red', shape: 'ring' }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (node.proxyTarget.targetType === 'tcp') {
|
|
26
|
+
if (node.proxyTarget._connected) {
|
|
27
|
+
return { fill: 'green', shape: 'dot' }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (node.proxyTarget._connecting) {
|
|
31
|
+
return { fill: 'yellow', shape: 'ring' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { fill: 'red', shape: 'ring' }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Serial target exists but current proxy-target serial path is stubbed.
|
|
38
|
+
return { fill: 'yellow', shape: 'ring' }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function updateConnectionStatus (statusText) {
|
|
42
|
+
if (typeof statusText === 'string' && statusText.length > 0) {
|
|
43
|
+
node._lastStatusText = statusText
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const style = getConnectionStyle()
|
|
47
|
+
node.status({ fill: style.fill, shape: style.shape, text: node._lastStatusText })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
updateConnectionStatus()
|
|
51
|
+
node._statusTimer = setInterval(function () {
|
|
52
|
+
updateConnectionStatus()
|
|
53
|
+
}, 1000)
|
|
54
|
+
|
|
55
|
+
node.on('input', function (msg, send, done) {
|
|
56
|
+
const context = msg._modbus || {}
|
|
57
|
+
const requestId = context.requestId
|
|
58
|
+
const eventName = context.eventName
|
|
59
|
+
const address = context.address
|
|
60
|
+
const quantity = context.quantity
|
|
61
|
+
const request = msg.payload || {}
|
|
62
|
+
|
|
63
|
+
if (!requestId || !eventName) {
|
|
64
|
+
node.status({ fill: 'red', shape: 'ring', text: 'missing context' })
|
|
65
|
+
node.error('Missing Modbus request context', msg)
|
|
66
|
+
done()
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Extract request buffer from msg or construct from Modbus request
|
|
71
|
+
const requestBuffer = msg.requestBuffer || constructModbusRequest(request)
|
|
72
|
+
if (!requestBuffer) {
|
|
73
|
+
done(new Error('modbus-dynamic-proxy: unable to construct request buffer'))
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
// Send to proxy target queue
|
|
79
|
+
node.proxyTarget.enqueueRequest(requestBuffer, function (proxyResult) {
|
|
80
|
+
const debugEnabled = msg.modbusProxyDebug === true
|
|
81
|
+
if (!proxyResult.ok) {
|
|
82
|
+
// Send Modbus exception back to server
|
|
83
|
+
const exceptionCode = proxyResult.exception || 4
|
|
84
|
+
const responseResult = respondWithPayload(RED, node, msg, { exception: exceptionCode })
|
|
85
|
+
const ok = responseResult.ok
|
|
86
|
+
|
|
87
|
+
if (!responseResult.context) {
|
|
88
|
+
node.status({ fill: 'red', shape: 'ring', text: 'missing context' })
|
|
89
|
+
done()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!ok) {
|
|
94
|
+
node.status({ fill: 'red', shape: 'ring', text: 'unknown request' })
|
|
95
|
+
done(new Error(`modbus-dynamic-proxy: request ${requestId} could not be responded to`))
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
updateConnectionStatus(`exception ${exceptionCode}`)
|
|
100
|
+
|
|
101
|
+
const outputMsg = {
|
|
102
|
+
payload: null,
|
|
103
|
+
modbusProxy: {
|
|
104
|
+
requestId,
|
|
105
|
+
eventName,
|
|
106
|
+
fc: request.fc,
|
|
107
|
+
address: address !== undefined ? address : request.address,
|
|
108
|
+
quantity: quantity !== undefined ? quantity : request.quantity,
|
|
109
|
+
proxied: false,
|
|
110
|
+
proxyError: proxyResult.error,
|
|
111
|
+
exception: exceptionCode,
|
|
112
|
+
targetType: node.proxyTarget.targetType
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
send(outputMsg)
|
|
117
|
+
done()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Parse response and send back to server
|
|
122
|
+
const parsedResponse = parseModbusResponse(proxyResult.responseBuffer, eventName, quantity !== undefined ? quantity : request.quantity)
|
|
123
|
+
const responseSummary = summarizeTcpResponse(proxyResult.responseBuffer)
|
|
124
|
+
if (debugEnabled) {
|
|
125
|
+
node.warn(`modbus-dynamic-proxy raw response requestId=${requestId} summary=${JSON.stringify(responseSummary)}`)
|
|
126
|
+
}
|
|
127
|
+
if (!parsedResponse) {
|
|
128
|
+
const responseResult = respondWithPayload(RED, node, msg, { exception: 4 })
|
|
129
|
+
const ok = responseResult.ok
|
|
130
|
+
|
|
131
|
+
if (!responseResult.context) {
|
|
132
|
+
node.status({ fill: 'red', shape: 'ring', text: 'missing context' })
|
|
133
|
+
done()
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!ok) {
|
|
138
|
+
done(new Error(`modbus-dynamic-proxy: request ${requestId} could not be responded to`))
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
updateConnectionStatus('parse error')
|
|
143
|
+
|
|
144
|
+
const outputMsg = {
|
|
145
|
+
payload: null,
|
|
146
|
+
modbusProxy: {
|
|
147
|
+
requestId,
|
|
148
|
+
eventName,
|
|
149
|
+
fc: request.fc,
|
|
150
|
+
address: address !== undefined ? address : request.address,
|
|
151
|
+
quantity: quantity !== undefined ? quantity : request.quantity,
|
|
152
|
+
proxied: false,
|
|
153
|
+
parseError: true,
|
|
154
|
+
exception: 4,
|
|
155
|
+
targetType: node.proxyTarget.targetType
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
send(outputMsg)
|
|
160
|
+
done()
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Send response back to server
|
|
165
|
+
const responseResult = respondWithPayload(RED, node, msg, parsedResponse)
|
|
166
|
+
const ok = responseResult.ok
|
|
167
|
+
|
|
168
|
+
if (!responseResult.context) {
|
|
169
|
+
node.status({ fill: 'red', shape: 'ring', text: 'missing context' })
|
|
170
|
+
done()
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!ok) {
|
|
175
|
+
node.status({ fill: 'red', shape: 'ring', text: 'unknown request' })
|
|
176
|
+
done(new Error(`modbus-dynamic-proxy: request ${requestId} could not be responded to`))
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
updateConnectionStatus(`proxied ${eventName}`)
|
|
181
|
+
|
|
182
|
+
// Emit the proxied response for caching/inspection
|
|
183
|
+
const outputMsg = {
|
|
184
|
+
payload: proxyResult.responseBuffer,
|
|
185
|
+
modbusProxy: {
|
|
186
|
+
requestId,
|
|
187
|
+
eventName,
|
|
188
|
+
fc: request.fc,
|
|
189
|
+
address: address !== undefined ? address : request.address,
|
|
190
|
+
quantity: quantity !== undefined ? quantity : request.quantity,
|
|
191
|
+
proxied: true,
|
|
192
|
+
targetType: node.proxyTarget.targetType,
|
|
193
|
+
responseSummary
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (debugEnabled) {
|
|
198
|
+
outputMsg.modbusProxy.parsedResponsePreview = previewParsedResponse(parsedResponse)
|
|
199
|
+
node.warn(`modbus-dynamic-proxy parsed response requestId=${requestId} parsed=${JSON.stringify(outputMsg.modbusProxy.parsedResponsePreview)}`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
send(outputMsg)
|
|
203
|
+
done()
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
node.on('close', function (done) {
|
|
208
|
+
if (node._statusTimer) {
|
|
209
|
+
clearInterval(node._statusTimer)
|
|
210
|
+
node._statusTimer = null
|
|
211
|
+
}
|
|
212
|
+
// nothing to clean up; queue is owned by proxy target config node
|
|
213
|
+
done()
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function constructModbusRequest (request) {
|
|
218
|
+
if (!request || typeof request !== 'object') {
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const fc = Number(request.fc)
|
|
223
|
+
const address = Number(request.address)
|
|
224
|
+
const quantity = Number(request.quantity)
|
|
225
|
+
const unitId = Number(request.unitId) || 0
|
|
226
|
+
|
|
227
|
+
if (!Number.isFinite(fc) || !Number.isFinite(address) || !Number.isFinite(quantity)) {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Validate FC and address/quantity ranges
|
|
232
|
+
if (fc < 1 || fc > 16 || [7, 8, 9, 10, 11, 12, 13, 14].includes(fc)) {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (address < 0 || address > 65535 || quantity < 1 || quantity > 65535) {
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
let requestBody
|
|
242
|
+
|
|
243
|
+
switch (fc) {
|
|
244
|
+
case 1:
|
|
245
|
+
requestBody = new jsmodbus.requests.ReadCoilsRequestBody(address, quantity)
|
|
246
|
+
break
|
|
247
|
+
case 2:
|
|
248
|
+
requestBody = new jsmodbus.requests.ReadDiscreteInputsRequestBody(address, quantity)
|
|
249
|
+
break
|
|
250
|
+
case 3:
|
|
251
|
+
requestBody = new jsmodbus.requests.ReadHoldingRegistersRequestBody(address, quantity)
|
|
252
|
+
break
|
|
253
|
+
case 4:
|
|
254
|
+
requestBody = new jsmodbus.requests.ReadInputRegistersRequestBody(address, quantity)
|
|
255
|
+
break
|
|
256
|
+
case 5:
|
|
257
|
+
case 6:
|
|
258
|
+
case 15:
|
|
259
|
+
case 16:
|
|
260
|
+
// Write requests need values which aren't in this basic payload
|
|
261
|
+
// Return null to indicate request buffer should be passed via msg.requestBuffer
|
|
262
|
+
return null
|
|
263
|
+
default:
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!requestBody) {
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Wrap in Modbus TCP request
|
|
272
|
+
const tcpRequest = new ModbusTCPRequest(requestBody)
|
|
273
|
+
// Constructor puts requestBody in _id, move it to _body
|
|
274
|
+
tcpRequest._body = tcpRequest._id
|
|
275
|
+
tcpRequest._id = 1
|
|
276
|
+
tcpRequest._protocol = 0
|
|
277
|
+
tcpRequest._length = 0
|
|
278
|
+
tcpRequest._unitId = unitId
|
|
279
|
+
|
|
280
|
+
return tcpRequest.createPayload()
|
|
281
|
+
} catch (err) {
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseModbusResponse (responseBuffer, eventName, quantity) {
|
|
287
|
+
if (!Buffer.isBuffer(responseBuffer) || responseBuffer.length < 9) {
|
|
288
|
+
return null
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Modbus TCP MBAP header (7 bytes) + PDU
|
|
292
|
+
const pduOffset = 7
|
|
293
|
+
const fc = responseBuffer[pduOffset]
|
|
294
|
+
|
|
295
|
+
// Exception response: FC | 0x80, exception byte follows
|
|
296
|
+
if ((fc & 0x80) === 0x80) {
|
|
297
|
+
if (responseBuffer.length < pduOffset + 2) {
|
|
298
|
+
return null
|
|
299
|
+
}
|
|
300
|
+
return { exception: responseBuffer[pduOffset + 1] }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (eventName === 'readHoldingRegisters' || eventName === 'readInputRegisters') {
|
|
304
|
+
const byteCount = responseBuffer[pduOffset + 1]
|
|
305
|
+
const start = pduOffset + 2
|
|
306
|
+
const end = start + byteCount
|
|
307
|
+
if (responseBuffer.length < end) {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Decode into register word array so server config builds an unambiguous response.
|
|
312
|
+
const data = responseBuffer.slice(start, end)
|
|
313
|
+
const words = []
|
|
314
|
+
for (let i = 0; i + 1 < data.length; i += 2) {
|
|
315
|
+
words.push(data.readUInt16BE(i))
|
|
316
|
+
}
|
|
317
|
+
return words
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (eventName === 'readCoils' || eventName === 'readDiscreteInputs') {
|
|
321
|
+
const byteCount = responseBuffer[pduOffset + 1]
|
|
322
|
+
const start = pduOffset + 2
|
|
323
|
+
const end = start + byteCount
|
|
324
|
+
if (responseBuffer.length < end) {
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Decode packed bits to explicit coil/discrete array.
|
|
329
|
+
const count = Number(quantity)
|
|
330
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
331
|
+
return []
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const out = new Array(count).fill(0)
|
|
335
|
+
const data = responseBuffer.slice(start, end)
|
|
336
|
+
for (let i = 0; i < count; i++) {
|
|
337
|
+
const byteIndex = Math.floor(i / 8)
|
|
338
|
+
const bitIndex = i % 8
|
|
339
|
+
if (byteIndex < data.length) {
|
|
340
|
+
out[i] = (data[byteIndex] >> bitIndex) & 1
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return out
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// For write responses, return undefined (server builds auto-response)
|
|
347
|
+
if (eventName.startsWith('write')) {
|
|
348
|
+
return undefined
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function summarizeTcpResponse (responseBuffer) {
|
|
355
|
+
if (!Buffer.isBuffer(responseBuffer) || responseBuffer.length < 9) {
|
|
356
|
+
return { valid: false }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const transactionId = responseBuffer.readUInt16BE(0)
|
|
360
|
+
const protocolId = responseBuffer.readUInt16BE(2)
|
|
361
|
+
const mbapLength = responseBuffer.readUInt16BE(4)
|
|
362
|
+
const unitId = responseBuffer[6]
|
|
363
|
+
const fc = responseBuffer[7]
|
|
364
|
+
const byteCount = responseBuffer.length > 8 ? responseBuffer[8] : null
|
|
365
|
+
|
|
366
|
+
let dataWords = []
|
|
367
|
+
if ((fc & 0x80) === 0 && Number.isInteger(byteCount) && byteCount >= 2 && responseBuffer.length >= 9 + byteCount) {
|
|
368
|
+
const data = responseBuffer.slice(9, 9 + byteCount)
|
|
369
|
+
for (let i = 0; i + 1 < data.length; i += 2) {
|
|
370
|
+
dataWords.push(data.readUInt16BE(i))
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
valid: true,
|
|
376
|
+
transactionId,
|
|
377
|
+
protocolId,
|
|
378
|
+
mbapLength,
|
|
379
|
+
unitId,
|
|
380
|
+
fc,
|
|
381
|
+
byteCount,
|
|
382
|
+
dataWords,
|
|
383
|
+
rawHex: responseBuffer.toString('hex')
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function previewParsedResponse (parsedResponse) {
|
|
388
|
+
if (parsedResponse === undefined) return { type: 'undefined' }
|
|
389
|
+
if (Buffer.isBuffer(parsedResponse)) {
|
|
390
|
+
return { type: 'buffer', hex: parsedResponse.toString('hex') }
|
|
391
|
+
}
|
|
392
|
+
if (Array.isArray(parsedResponse)) {
|
|
393
|
+
return { type: 'array', values: parsedResponse }
|
|
394
|
+
}
|
|
395
|
+
return { type: typeof parsedResponse, value: parsedResponse }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
RED.nodes.registerType('modbus-dynamic-proxy', ModbusDynamicProxyNode)
|
|
399
|
+
}
|
|
400
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="modbus-dynamic-server-config">
|
|
2
|
+
|
|
3
|
+
<div class="form-row">
|
|
4
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
5
|
+
<input type="text" id="node-config-input-name" placeholder="Optional name">
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="form-row">
|
|
9
|
+
<label for="node-config-input-host"><i class="fa fa-globe"></i> Host</label>
|
|
10
|
+
<input type="text" id="node-config-input-host" placeholder="0.0.0.0">
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="form-row">
|
|
14
|
+
<label for="node-config-input-port"><i class="fa fa-random"></i> Port</label>
|
|
15
|
+
<input type="number" id="node-config-input-port" placeholder="502">
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="form-row">
|
|
19
|
+
<label for="node-config-input-enforceResponseOrder">
|
|
20
|
+
<i class="fa fa-sort-numeric-asc"></i> Enforce Response Order
|
|
21
|
+
</label>
|
|
22
|
+
<input type="checkbox" id="node-config-input-enforceResponseOrder" style="display:inline-block; width:auto;">
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="form-row">
|
|
26
|
+
<label for="node-config-input-pendingResponseTimeout">
|
|
27
|
+
<i class="fa fa-clock-o"></i> Pending Response Timeout (ms)
|
|
28
|
+
</label>
|
|
29
|
+
<input type="number" id="node-config-input-pendingResponseTimeout" placeholder="300000">
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<script type="text/html" data-help-name="modbus-dynamic-server-config">
|
|
35
|
+
<p>Configures and starts a Modbus TCP server. All incoming function-code requests are held as
|
|
36
|
+
<em>pending requests</em> and dispatched to connected <code>modbus-dynamic-server</code> nodes
|
|
37
|
+
for processing.</p>
|
|
38
|
+
|
|
39
|
+
<h3>Configuration</h3>
|
|
40
|
+
<dl class="message-properties">
|
|
41
|
+
<dt>Name <span class="property-type">string</span></dt>
|
|
42
|
+
<dd>Optional label shown in the editor.</dd>
|
|
43
|
+
|
|
44
|
+
<dt>Host <span class="property-type">string</span></dt>
|
|
45
|
+
<dd>IP address or hostname the TCP server will bind to. Use <code>0.0.0.0</code> to listen
|
|
46
|
+
on all interfaces. Default: <code>0.0.0.0</code>.</dd>
|
|
47
|
+
|
|
48
|
+
<dt>Port <span class="property-type">number</span></dt>
|
|
49
|
+
<dd>TCP port to listen on. Default: <code>502</code>.</dd>
|
|
50
|
+
|
|
51
|
+
<dt>Enforce Response Order <span class="property-type">boolean</span></dt>
|
|
52
|
+
<dd>When enabled, responses for a given TCP connection are written back in the same order
|
|
53
|
+
the requests were received, regardless of which response is completed first. Default: enabled.</dd>
|
|
54
|
+
|
|
55
|
+
<dt>Pending Response Timeout (ms) <span class="property-type">number</span></dt>
|
|
56
|
+
<dd>Maximum time in milliseconds a request may remain unanswered before the server
|
|
57
|
+
automatically replies with a Modbus exception code 4 (Server Failure).
|
|
58
|
+
Default: <code>300000</code> (5 minutes).</dd>
|
|
59
|
+
</dl>
|
|
60
|
+
|
|
61
|
+
<h3>Details</h3>
|
|
62
|
+
<p>One config node represents one Modbus TCP server instance. Multiple
|
|
63
|
+
<code>modbus-dynamic-server</code> nodes can reference the same config node; each
|
|
64
|
+
will receive a copy of every incoming request message.</p>
|
|
65
|
+
<p>Requests are held internally by a <em>requestId</em> UUID until a response node calls
|
|
66
|
+
<code>respondToRequest</code>. If no response arrives within the timeout window the server
|
|
67
|
+
sends a Gateway Path Unavailable exception to the client automatically.</p>
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<script type="text/javascript">
|
|
71
|
+
RED.nodes.registerType('modbus-dynamic-server-config', {
|
|
72
|
+
category: 'config',
|
|
73
|
+
defaults: {
|
|
74
|
+
name: { value: '' },
|
|
75
|
+
host: { value: '0.0.0.0', required: true },
|
|
76
|
+
port: { value: 502, required: true, validate: RED.validators.number() },
|
|
77
|
+
enforceResponseOrder: { value: true },
|
|
78
|
+
pendingResponseTimeout: { value: 300000, required: true, validate: RED.validators.number() }
|
|
79
|
+
|
|
80
|
+
},
|
|
81
|
+
label: function () {
|
|
82
|
+
return this.name || this.host + ':' + this.port
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
</script>
|