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,391 @@
1
+ const net = require('net')
2
+ const modbus = require('jsmodbus')
3
+ const EventEmitter = require('events')
4
+ const { randomUUID } = require('crypto')
5
+ const resp = require('jsmodbus/dist/response')
6
+ const TcpResponse = require('jsmodbus/dist/tcp-response')
7
+
8
+ const DEFAULT_PENDING_RESPONSE_TIMEOUT_MS = 5 * 60 * 1000
9
+ const DEFAULT_TIMEOUT_EXCEPTION_CODE = 4
10
+
11
+ // FC event names jsmodbus emits when no buffer is provided
12
+ const FC_EVENTS = [
13
+ 'readCoils',
14
+ 'readDiscreteInputs',
15
+ 'readHoldingRegisters',
16
+ 'readInputRegisters',
17
+ 'writeSingleCoil',
18
+ 'writeSingleRegister',
19
+ 'writeMultipleCoils',
20
+ 'writeMultipleRegisters'
21
+ ]
22
+
23
+
24
+ module.exports = function (RED) {
25
+ 'use strict'
26
+
27
+ function ModbusFlexServerConfigNode (config) {
28
+ RED.nodes.createNode(this, config)
29
+ const node = this
30
+
31
+ node.name = config.name || ''
32
+ node.host = config.host || '0.0.0.0'
33
+ node.port = Number(config.port || 502)
34
+ node.enforceResponseOrder = config.enforceResponseOrder !== false
35
+ node.pendingResponseTimeout = normalizePendingResponseTimeout(config.pendingResponseTimeout)
36
+
37
+ // Emitter used by modbus-dynamic-server nodes to receive incoming requests
38
+ node.emitter = new EventEmitter()
39
+
40
+ // requestId → { request, cb, eventName }
41
+ node.pendingRequests = new Map()
42
+ node._requestContexts = new WeakMap()
43
+
44
+ // connectionId → { nextSeqOut: Number, queue: Map<seqNum, { buf, writeCb }> }
45
+ node._connectionStates = new Map()
46
+
47
+ // connectionId → nextSeqIn counter
48
+ node._connectionSeqCounters = new Map()
49
+
50
+ node.netServer = new net.Server()
51
+
52
+
53
+ // Pass null for all buffers so jsmodbus emits FC events for every request
54
+ // instead of serving them automatically from an internal buffer.
55
+ node.modbusServer = new modbus.server.TCP(node.netServer, {
56
+ coils: null,
57
+ discrete: null,
58
+ holding: null,
59
+ input: null
60
+ })
61
+
62
+
63
+ node.modbusServer.on('connection', function (client) {
64
+ const socket = client.socket
65
+ const connectionId = randomUUID()
66
+
67
+ const remoteAddress = socket.remoteAddress || ''
68
+ const remotePort = Number(socket.remotePort) || 0
69
+ const localAddress = socket.localAddress || node.host
70
+ const localPort = Number(socket.localPort) || node.port
71
+ const meta = {
72
+ remoteAddress,
73
+ remotePort,
74
+ localAddress,
75
+ localPort,
76
+ remoteEndpoint: formatEndpoint(remoteAddress, remotePort),
77
+ localEndpoint: formatEndpoint(localAddress, localPort)
78
+ }
79
+
80
+ node._connectionStates.set(connectionId, { nextSeqOut: 0, queue: new Map(), meta })
81
+ node._connectionSeqCounters.set(connectionId, 0)
82
+
83
+ // Wrap the response handler so we can inject sequence numbers for
84
+ // in-order delivery per connection.
85
+ const origHandle = client._responseHandler.handle.bind(client._responseHandler)
86
+ client._responseHandler.handle = function (request, writeCb) {
87
+ const seqNum = node._connectionSeqCounters.get(connectionId)
88
+ node._connectionSeqCounters.set(connectionId, seqNum + 1)
89
+ node._requestContexts.set(request, { connectionId, seqNum })
90
+
91
+ const wrappedCb = function (responseBuffer) {
92
+ if (!node.enforceResponseOrder) {
93
+ writeCb(responseBuffer)
94
+ return
95
+ }
96
+ const connState = node._connectionStates.get(connectionId)
97
+ if (!connState) return
98
+
99
+ if (seqNum === connState.nextSeqOut) {
100
+ writeCb(responseBuffer)
101
+ connState.nextSeqOut++
102
+ // Drain any already-ready queued responses
103
+ while (connState.queue.has(connState.nextSeqOut)) {
104
+ const queued = connState.queue.get(connState.nextSeqOut)
105
+ connState.queue.delete(connState.nextSeqOut)
106
+ queued.writeCb(queued.buf)
107
+ connState.nextSeqOut++
108
+ }
109
+ } else {
110
+ connState.queue.set(seqNum, { buf: responseBuffer, writeCb })
111
+ }
112
+ }
113
+
114
+ return origHandle(request, wrappedCb)
115
+ }
116
+
117
+ socket.on('close', function () {
118
+ node._connectionStates.delete(connectionId)
119
+ node._connectionSeqCounters.delete(connectionId)
120
+ const cleanedRequests = node.cleanupPendingRequestsForConnection(connectionId)
121
+
122
+ node.log(`Client disconnected: ${connectionId}${cleanedRequests ? ` (cleared ${cleanedRequests} pending requests)` : ''}`)
123
+ })
124
+
125
+ node.log(`Client connected from ${socket.remoteAddress}:${socket.remotePort} (${connectionId})`)
126
+ })
127
+
128
+ // For this new jsmodbus connectin, register a handler for every possible FC event.
129
+ // The cb received here is already the wrappedCb from the connection handler above.
130
+ // These events will just be emitted to any 'dynamic server' nodes attached to
131
+ // this config object (under all but very odd circumstances there will only
132
+ // be one).
133
+ FC_EVENTS.forEach(function (eventName) {
134
+ node.modbusServer.on(eventName, function (request, cb) {
135
+ const body = request.body
136
+ const requestId = node.registerPendingRequest(request, cb, eventName)
137
+ const requestContext = node._requestContexts.get(request) || {}
138
+ const connectionState = requestContext.connectionId ? node._connectionStates.get(requestContext.connectionId) : null
139
+ const connectionMeta = connectionState ? connectionState.meta : null
140
+
141
+ node.emitter.emit('request', {
142
+ requestId,
143
+ eventName,
144
+ connection: {
145
+ id: requestContext.connectionId || null,
146
+ source: connectionMeta
147
+ ? {
148
+ address: connectionMeta.remoteAddress,
149
+ port: connectionMeta.remotePort,
150
+ endpoint: connectionMeta.remoteEndpoint
151
+ }
152
+ : null,
153
+ destination: connectionMeta
154
+ ? {
155
+ address: connectionMeta.localAddress,
156
+ port: connectionMeta.localPort,
157
+ endpoint: connectionMeta.localEndpoint
158
+ }
159
+ : null
160
+ },
161
+ request: {
162
+ fc: body.fc,
163
+ // Reads use .start/.count; writes use .address/.quantity
164
+ address: body.address !== undefined ? body.address : body.start,
165
+ quantity: body.quantity !== undefined ? body.quantity : (body.count !== undefined ? body.count : 1),
166
+ unitId: request.unitId,
167
+ transactionId: request.id,
168
+ body
169
+ }
170
+ })
171
+ })
172
+ })
173
+
174
+ node.takePendingRequest = function (requestId) {
175
+ const pending = node.pendingRequests.get(requestId)
176
+ if (!pending) {
177
+ return null
178
+ }
179
+
180
+ if (pending.timeoutHandle) {
181
+ clearTimeout(pending.timeoutHandle)
182
+ }
183
+
184
+ node.pendingRequests.delete(requestId)
185
+ return pending
186
+ }
187
+
188
+ node.cleanupPendingRequestsForConnection = function (connectionId) {
189
+ let cleanedRequests = 0
190
+
191
+ for (const [requestId, pending] of node.pendingRequests) {
192
+ if (pending.connectionId === connectionId) {
193
+ node.takePendingRequest(requestId)
194
+ cleanedRequests++
195
+ }
196
+ }
197
+
198
+ return cleanedRequests
199
+ }
200
+
201
+ node.registerPendingRequest = function (request, cb, eventName) {
202
+ const requestId = randomUUID()
203
+ const requestContext = node._requestContexts.get(request) || {}
204
+ const pending = {
205
+ request,
206
+ cb,
207
+ eventName,
208
+ connectionId: requestContext.connectionId,
209
+ seqNum: requestContext.seqNum
210
+ }
211
+
212
+ pending.timeoutHandle = setTimeout(function () {
213
+ const expiredPending = node.takePendingRequest(requestId)
214
+ if (!expiredPending) {
215
+ return
216
+ }
217
+
218
+ const connectionIsOpen = !expiredPending.connectionId || node._connectionStates.has(expiredPending.connectionId)
219
+ if (!connectionIsOpen) {
220
+ node.warn(`Pending Modbus response expired after ${node.pendingResponseTimeout}ms for requestId ${requestId}; connection already closed`)
221
+ return
222
+ }
223
+
224
+ node.warn(`Pending Modbus response expired after ${node.pendingResponseTimeout}ms for requestId ${requestId}; replying with exception ${DEFAULT_TIMEOUT_EXCEPTION_CODE}`)
225
+ try {
226
+ sendPendingResponse(expiredPending, { exception: DEFAULT_TIMEOUT_EXCEPTION_CODE })
227
+ } catch (err) {
228
+ node.error(`respondToRequest timeout error for ${expiredPending.eventName}: ${err.message}`)
229
+ }
230
+ }, node.pendingResponseTimeout)
231
+
232
+ node.pendingRequests.set(requestId, pending)
233
+ return requestId
234
+ }
235
+
236
+ node.netServer.on('error', function (err) {
237
+ node.error(`Server error: ${err.message}`)
238
+ })
239
+
240
+ node.netServer.listen(node.port, node.host, function () {
241
+ node.log(`Listening on ${node.host}:${node.port}`)
242
+ })
243
+
244
+ /**
245
+ * Called by modbus-dynamic-server-response to complete a pending request.
246
+ *
247
+ * @param {string} requestId - The requestId from msg.modbus.requestId
248
+ * @param {Array|Buffer|{exception: number}|{values: Array}} payload
249
+ * - Read FCs: array of values or Buffer, or { values: [...] }
250
+ * - Write FCs: ignored (response is auto-built from the original request)
251
+ * - Exception: { exception: <modbus exception code 1–11> }
252
+ * @returns {boolean} true on success, false if requestId not found
253
+ */
254
+ node.respondToRequest = function (requestId, payload) {
255
+ const pending = node.takePendingRequest(requestId)
256
+ if (!pending) {
257
+ node.warn(`respondToRequest: unknown requestId ${requestId}`)
258
+ return false
259
+ }
260
+
261
+ try {
262
+ const connectionIsOpen = !pending.connectionId || node._connectionStates.has(pending.connectionId)
263
+ if (!connectionIsOpen) {
264
+ node.warn(`respondToRequest: requestId ${requestId} connection already closed`)
265
+ return false
266
+ }
267
+
268
+ sendPendingResponse(pending, payload)
269
+ return true
270
+ } catch (err) {
271
+ node.error(`respondToRequest error for ${pending.eventName}: ${err.message}`)
272
+ return false
273
+ }
274
+ }
275
+
276
+ function sendPendingResponse (pending, payload) {
277
+ const { request, cb, eventName } = pending
278
+ let responseBody
279
+
280
+ if (payload && typeof payload === 'object' && !Array.isArray(payload) && !Buffer.isBuffer(payload) && payload.exception) {
281
+ responseBody = new resp.ExceptionResponseBody(request.body.fc, payload.exception)
282
+ } else {
283
+ responseBody = buildResponseBody(eventName, request, payload)
284
+ }
285
+
286
+ const response = TcpResponse.default.fromRequest(request, responseBody)
287
+ cb(response.createPayload())
288
+ }
289
+
290
+ function buildResponseBody (eventName, request, payload) {
291
+ const values = Array.isArray(payload)
292
+ ? payload
293
+ : (payload && payload.values)
294
+ ? payload.values
295
+ : payload
296
+
297
+ switch (eventName) {
298
+ case 'readCoils': {
299
+ const count = request.body.count
300
+ const coilArr = normalizeCoilValues(values, count)
301
+ return new resp.ReadCoilsResponseBody(coilArr, Math.ceil(count / 8))
302
+ }
303
+ case 'readDiscreteInputs': {
304
+ const count = request.body.count
305
+ const discArr = normalizeCoilValues(values, count)
306
+ return new resp.ReadDiscreteInputsResponseBody(discArr, Math.ceil(count / 8))
307
+ }
308
+ case 'readHoldingRegisters': {
309
+ const count = request.body.count
310
+ const regArr = normalizeRegisterValues(values, count)
311
+ return new resp.ReadHoldingRegistersResponseBody(count * 2, regArr)
312
+ }
313
+ case 'readInputRegisters': {
314
+ const count = request.body.count
315
+ const regArr = normalizeRegisterValues(values, count)
316
+ return new resp.ReadInputRegistersResponseBody(count * 2, regArr)
317
+ }
318
+ case 'writeSingleCoil':
319
+ return resp.WriteSingleCoilResponseBody.fromRequest(request.body)
320
+ case 'writeSingleRegister':
321
+ return resp.WriteSingleRegisterResponseBody.fromRequest(request.body)
322
+ case 'writeMultipleCoils':
323
+ return resp.WriteMultipleCoilsResponseBody.fromRequest(request.body)
324
+ case 'writeMultipleRegisters':
325
+ return resp.WriteMultipleRegistersResponseBody.fromRequest(request.body)
326
+ default:
327
+ throw new Error(`Unknown FC event: ${eventName}`)
328
+ }
329
+ }
330
+
331
+ function normalizeRegisterValues (values, count) {
332
+ if (Buffer.isBuffer(values)) {
333
+ const expected = count * 2
334
+ if (values.length === expected) return values
335
+ // wrong size — reallocate and copy what we can
336
+ const buf = Buffer.alloc(expected, 0)
337
+ values.copy(buf, 0, 0, Math.min(values.length, expected))
338
+ return buf
339
+ }
340
+ if (Array.isArray(values)) {
341
+ const padded = new Array(count).fill(0)
342
+ values.slice(0, count).forEach((v, i) => { padded[i] = v })
343
+ return padded
344
+ }
345
+ return new Array(count).fill(0)
346
+ }
347
+
348
+ function normalizeCoilValues (values, count) {
349
+ if (Buffer.isBuffer(values)) {
350
+ const expected = Math.ceil(count / 8)
351
+ if (values.length === expected) return values
352
+ const buf = Buffer.alloc(expected, 0)
353
+ values.copy(buf, 0, 0, Math.min(values.length, expected))
354
+ return buf
355
+ }
356
+ if (Array.isArray(values)) {
357
+ const padded = new Array(count).fill(0)
358
+ values.slice(0, count).forEach((v, i) => { padded[i] = v ? 1 : 0 })
359
+ return padded
360
+ }
361
+ return new Array(count).fill(0)
362
+ }
363
+
364
+ node.on('close', function (done) {
365
+ for (const requestId of node.pendingRequests.keys()) {
366
+ node.takePendingRequest(requestId)
367
+ }
368
+ node._connectionStates.clear()
369
+ node._connectionSeqCounters.clear()
370
+ if (node.netServer) {
371
+ node.netServer.close(function (err) {
372
+ done(err || undefined)
373
+ })
374
+ } else {
375
+ done()
376
+ }
377
+ })
378
+ }
379
+
380
+ function normalizePendingResponseTimeout (value) {
381
+ const timeout = Number(value)
382
+ return Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_PENDING_RESPONSE_TIMEOUT_MS
383
+ }
384
+
385
+ function formatEndpoint (address, port) {
386
+ if (!address && !port) return ''
387
+ return `${address || ''}:${port || 0}`
388
+ }
389
+
390
+ RED.nodes.registerType('modbus-dynamic-server-config', ModbusFlexServerConfigNode)
391
+ }
@@ -0,0 +1,88 @@
1
+ <script type="text/html" data-template-name="modbus-dynamic-server-response">
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
+ </script>
8
+
9
+ <script type="text/html" data-help-name="modbus-dynamic-server-response">
10
+ <p>Completes a pending Modbus request by sending a response back to the connected client.
11
+ The input message must carry the <code>requestId</code> originally emitted by a
12
+ <code>modbus-dynamic-server</code> node.</p>
13
+
14
+ <h3>Inputs</h3>
15
+ <dl class="message-properties">
16
+ <dt>_modbus.requestId <span class="property-type">string</span></dt>
17
+ <dd>Required internal request context populated by <code>modbus-dynamic-server</code>.</dd>
18
+
19
+ <dt>_modbus.configNodeId <span class="property-type">string</span></dt>
20
+ <dd>Required internal config-node identifier used to complete the pending request.</dd>
21
+
22
+ <dt>payload <span class="property-type">array | Buffer | object</span></dt>
23
+ <dd>
24
+ The response value(s) to send back to the Modbus client:
25
+ <ul>
26
+ <li><b>Read requests (FC1–FC4):</b> an array of values, a <code>Buffer</code>,
27
+ or an object <code>{ values: [...] }</code>.</li>
28
+ <li><b>Write requests (FC5, FC6, FC15, FC16):</b> ignored — the response is
29
+ built automatically from the original request body.</li>
30
+ <li><b>Exception:</b> an object <code>{ exception: &lt;code&gt; }</code> where
31
+ code is a Modbus exception code (1–11).</li>
32
+ </ul>
33
+ </dd>
34
+ </dl>
35
+
36
+ <h3>Outputs</h3>
37
+ <ol class="node-ports">
38
+ <li>Response report message
39
+ <dl class="message-properties">
40
+ <dt>modbusResponse <span class="property-type">object</span></dt>
41
+ <dd>Details of what this node submitted to <code>respondToRequest</code>, including:
42
+ <ul>
43
+ <li><code>requestId</code>, <code>eventName</code>, <code>address</code>, <code>quantity</code></li>
44
+ <li><code>payloadSubmitted</code> — the exact payload passed to server config</li>
45
+ <li><code>payloadType</code> — payload classification (<code>array</code>, <code>buffer</code>, <code>object</code>, ...)</li>
46
+ <li><code>actualDataSent</code> — normalized values actually encoded for read FC responses</li>
47
+ <li><code>actualDataType</code> — type of <code>actualDataSent</code></li>
48
+ <li><code>ok</code> — whether the response was accepted for the requestId</li>
49
+ <li><code>timestamp</code> — ISO timestamp when this node submitted it</li>
50
+ <li><code>note</code> — present for write FCs to indicate auto-built response behavior</li>
51
+ </ul>
52
+ </dd>
53
+ </dl>
54
+ </li>
55
+ </ol>
56
+
57
+ <h3>Details</h3>
58
+ <p>Once <code>respondToRequest</code> is called, the pending request is consumed and cannot be
59
+ answered again. This node now emits one message so flows can inspect what was sent.</p>
60
+ <p>If <code>msg._modbus</code> is missing, this node cannot respond and logs
61
+ <em>Missing Modbus request context</em>.</p>
62
+ <p>Flows must preserve <code>msg._modbus</code>:</p>
63
+ <pre><code>// CORRECT
64
+ msg.payload = newValue;
65
+ return msg;
66
+
67
+ // INCORRECT: request context is lost
68
+ msg = { payload: newValue };
69
+ return msg;</code></pre>
70
+ <p>For a simpler alternative that automatically reads/writes values from a shared register map
71
+ without manual payload construction, use the <code>modbus-registers-respond</code> node instead.</p>
72
+ </script>
73
+
74
+ <script type="text/javascript">
75
+ RED.nodes.registerType('modbus-dynamic-server-response', {
76
+ category: 'modbus',
77
+ color: '#E9967A',
78
+ defaults: {
79
+ name: { value: '' }
80
+ },
81
+ inputs: 1,
82
+ outputs: 1,
83
+ icon: 'arrow-out.svg',
84
+ label: function () {
85
+ return this.name || 'Modbus Dynamic Response'
86
+ }
87
+ })
88
+ </script>
@@ -0,0 +1,160 @@
1
+ const { respondWithPayload } = require('./modbus-response-context')
2
+
3
+ module.exports = function (RED) {
4
+ function ModbusFlexServerResponseNode (config) {
5
+ RED.nodes.createNode(this, config)
6
+ const node = this
7
+
8
+ node.status({ fill: 'grey', shape: 'ring', text: 'ready' })
9
+
10
+ node.on('input', function (msg, send, done) {
11
+ const responseResult = respondWithPayload(RED, node, msg, msg.payload)
12
+ if (!responseResult.context) {
13
+ node.status({ fill: 'red', shape: 'ring', text: 'missing context' })
14
+ done()
15
+ return
16
+ }
17
+
18
+ const context = responseResult.context
19
+ const eventName = context.eventName || (msg.modbus && msg.modbus.eventName)
20
+ const address = context.address !== undefined ? context.address : (msg.modbus && msg.modbus.address)
21
+ const quantity = context.quantity !== undefined ? context.quantity : (msg.modbus && msg.modbus.quantity)
22
+ const requestId = context.requestId
23
+ const ok = responseResult.ok
24
+ const actualDataSent = deriveActualDataSent(eventName, quantity, msg.payload)
25
+
26
+ msg.modbusResponse = {
27
+ requestId,
28
+ eventName,
29
+ address,
30
+ quantity,
31
+ payloadSubmitted: clonePayload(msg.payload),
32
+ payloadType: describePayload(msg.payload),
33
+ actualDataSent,
34
+ actualDataType: describePayload(actualDataSent),
35
+ ok,
36
+ timestamp: new Date().toISOString()
37
+ }
38
+
39
+ // Write FC responses are generated from the original request body by jsmodbus.
40
+ if (typeof eventName === 'string' && eventName.startsWith('write')) {
41
+ msg.modbusResponse.note = 'Write FC response is auto-built from request body by server config'
42
+ }
43
+
44
+ if (ok) {
45
+ node.status({ fill: 'green', shape: 'dot', text: 'responded' })
46
+ send(msg)
47
+ done()
48
+ } else {
49
+ node.status({ fill: 'red', shape: 'ring', text: 'unknown requestId' })
50
+ done(new Error(`modbus-dynamic-server-response: unknown requestId ${requestId}`))
51
+ }
52
+ })
53
+
54
+ node.on('close', function () {
55
+ // nothing to clean up; pending requests are owned by the config node
56
+ })
57
+ }
58
+
59
+ function clonePayload (payload) {
60
+ if (Buffer.isBuffer(payload)) {
61
+ return Buffer.from(payload)
62
+ }
63
+ if (Array.isArray(payload)) {
64
+ return payload.slice()
65
+ }
66
+ if (payload && typeof payload === 'object') {
67
+ return { ...payload }
68
+ }
69
+ return payload
70
+ }
71
+
72
+ function describePayload (payload) {
73
+ if (Buffer.isBuffer(payload)) return 'buffer'
74
+ if (Array.isArray(payload)) return 'array'
75
+ return typeof payload
76
+ }
77
+
78
+ function deriveActualDataSent (eventName, quantity, payload) {
79
+ const count = Number(quantity)
80
+ if (!Number.isFinite(count) || count <= 0) {
81
+ return payload
82
+ }
83
+
84
+ if (eventName === 'readHoldingRegisters' || eventName === 'readInputRegisters') {
85
+ return normalizeWordPayload(payload, count)
86
+ }
87
+
88
+ if (eventName === 'readCoils' || eventName === 'readDiscreteInputs') {
89
+ return normalizeBitPayload(payload, count)
90
+ }
91
+
92
+ // write responses are auto-built from request body by server config
93
+ return payload
94
+ }
95
+
96
+ function normalizeWordPayload (payload, count) {
97
+ const values = resolveValues(payload)
98
+
99
+ if (Buffer.isBuffer(values)) {
100
+ const expectedBytes = count * 2
101
+ const buf = Buffer.alloc(expectedBytes, 0)
102
+ values.copy(buf, 0, 0, Math.min(values.length, expectedBytes))
103
+ const words = []
104
+ for (let i = 0; i < expectedBytes; i += 2) {
105
+ words.push(buf.readUInt16BE(i))
106
+ }
107
+ return words
108
+ }
109
+
110
+ if (Array.isArray(values)) {
111
+ const out = new Array(count).fill(0)
112
+ values.slice(0, count).forEach(function (v, i) {
113
+ out[i] = Number(v) & 0xffff
114
+ })
115
+ return out
116
+ }
117
+
118
+ return new Array(count).fill(0)
119
+ }
120
+
121
+ function normalizeBitPayload (payload, count) {
122
+ const values = resolveValues(payload)
123
+
124
+ if (Buffer.isBuffer(values)) {
125
+ const out = new Array(count).fill(0)
126
+ for (let i = 0; i < count; i++) {
127
+ const byteIndex = Math.floor(i / 8)
128
+ const bitIndex = i % 8
129
+ if (byteIndex < values.length) {
130
+ out[i] = (values[byteIndex] >> bitIndex) & 1
131
+ }
132
+ }
133
+ return out
134
+ }
135
+
136
+ if (Array.isArray(values)) {
137
+ const out = new Array(count).fill(0)
138
+ values.slice(0, count).forEach(function (v, i) {
139
+ out[i] = v ? 1 : 0
140
+ })
141
+ return out
142
+ }
143
+
144
+ return new Array(count).fill(0)
145
+ }
146
+
147
+ function resolveValues (payload) {
148
+ if (Array.isArray(payload) || Buffer.isBuffer(payload)) {
149
+ return payload
150
+ }
151
+
152
+ if (payload && typeof payload === 'object' && Array.isArray(payload.values)) {
153
+ return payload.values
154
+ }
155
+
156
+ return payload
157
+ }
158
+
159
+ RED.nodes.registerType('modbus-dynamic-server-response', ModbusFlexServerResponseNode)
160
+ }