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,302 @@
1
+ const net = require('net')
2
+
3
+ module.exports = function (RED) {
4
+ function ModbusProxyTargetNode (config) {
5
+ RED.nodes.createNode(this, config)
6
+ const node = this
7
+
8
+ node.name = config.name || ''
9
+ node.targetType = config.targetType || 'tcp'
10
+ node.timeout = normalizeTimeout(config.timeout, 5000)
11
+
12
+ // TCP settings
13
+ node.tcpHost = config.tcpHost || 'localhost'
14
+ node.tcpPort = normalizePort(config.tcpPort, 502)
15
+
16
+ // Serial settings
17
+ node.serialPort = config.serialPort || '/dev/ttyUSB0'
18
+ node.serialBaud = normalizeBaud(config.serialBaud, 9600)
19
+ node.serialParity = config.serialParity || 'none'
20
+ node.serialWordLength = normalizeWordLength(config.serialWordLength, 8)
21
+ node.serialStopBits = normalizeStopBits(config.serialStopBits, 1)
22
+
23
+ // Request queue and connection state
24
+ node._requestQueue = []
25
+ node._processing = false
26
+ node._connection = null
27
+ node._lastError = null
28
+ node._activeRequest = null
29
+ node._activeTimeout = null
30
+ node._incomingBuffer = Buffer.alloc(0)
31
+ node._connecting = false
32
+ node._connected = false
33
+ node._closing = false
34
+ node._reconnectTimer = null
35
+ node._reconnectDelay = 1000
36
+
37
+ node.enqueueRequest = function (requestBuffer, callback) {
38
+ node._requestQueue.push({
39
+ buffer: requestBuffer,
40
+ callback,
41
+ timestamp: Date.now(),
42
+ retries: 0
43
+ })
44
+
45
+ node._processQueue()
46
+ }
47
+
48
+ node._processQueue = function () {
49
+ if (node._activeRequest || node._closing) {
50
+ return
51
+ }
52
+
53
+ if (node._requestQueue.length === 0) {
54
+ node._processing = false
55
+ return
56
+ }
57
+
58
+ if (node.targetType === 'tcp') {
59
+ node._ensureTcpConnection()
60
+ if (!node._connected || !node._connection) {
61
+ node._processing = true
62
+ return
63
+ }
64
+
65
+ node._processing = true
66
+ const request = node._requestQueue.shift()
67
+ node._sendViaTcp(request)
68
+ return
69
+ }
70
+
71
+ // Serial path remains single-request and currently unimplemented.
72
+ node._processing = true
73
+ const request = node._requestQueue.shift()
74
+ node._sendRequest(request)
75
+ }
76
+
77
+ node._sendRequest = function (request) {
78
+ const requestTimeout = setTimeout(function () {
79
+ node._lastError = 'Timeout'
80
+ request.callback({
81
+ ok: false,
82
+ error: 'timeout',
83
+ exception: 4 // Gateway Path Unavailable
84
+ })
85
+
86
+ // Close connection on timeout and continue with next
87
+ if (node._connection) {
88
+ if (node.targetType === 'tcp') {
89
+ node._connection.destroy()
90
+ }
91
+ node._connection = null
92
+ }
93
+
94
+ node._processQueue()
95
+ }, node.timeout)
96
+
97
+ if (node.targetType === 'tcp') {
98
+ // TCP uses persistent connection flow and per-request timeout managed in _sendViaTcp.
99
+ clearTimeout(requestTimeout)
100
+ node._requestQueue.unshift(request)
101
+ node._processQueue()
102
+ } else if (node.targetType === 'serial') {
103
+ node._sendViaSerial(request, requestTimeout)
104
+ } else {
105
+ clearTimeout(requestTimeout)
106
+ request.callback({
107
+ ok: false,
108
+ error: 'unsupported target type',
109
+ exception: 4
110
+ })
111
+ node._processQueue()
112
+ }
113
+ }
114
+
115
+ node._sendViaTcp = function (request) {
116
+ if (!node._connected || !node._connection) {
117
+ node._requestQueue.unshift(request)
118
+ node._ensureTcpConnection()
119
+ return
120
+ }
121
+
122
+ node._activeRequest = request
123
+ node._activeTimeout = setTimeout(function () {
124
+ node._failActiveRequest('timeout')
125
+ if (node._connection) {
126
+ node._connection.destroy()
127
+ }
128
+ }, node.timeout)
129
+
130
+ try {
131
+ node._connection.write(request.buffer)
132
+ } catch (err) {
133
+ node._failActiveRequest(err.message || 'write failed')
134
+ if (node._connection) {
135
+ node._connection.destroy()
136
+ }
137
+ }
138
+ }
139
+
140
+ node._ensureTcpConnection = function () {
141
+ if (node._closing || node.targetType !== 'tcp') return
142
+ if (node._connected || node._connecting) return
143
+
144
+ node._connecting = true
145
+ node.status({ fill: 'yellow', shape: 'ring', text: 'connecting' })
146
+
147
+ const socket = net.createConnection({ host: node.tcpHost, port: node.tcpPort })
148
+ node._connection = socket
149
+
150
+ socket.on('connect', function () {
151
+ node._connecting = false
152
+ node._connected = true
153
+ node._lastError = null
154
+ node._incomingBuffer = Buffer.alloc(0)
155
+ node.status({ fill: 'green', shape: 'dot', text: 'connected' })
156
+ node._processQueue()
157
+ })
158
+
159
+ socket.on('data', function (chunk) {
160
+ node._incomingBuffer = Buffer.concat([node._incomingBuffer, chunk])
161
+ node._drainIncomingFrames()
162
+ })
163
+
164
+ socket.on('error', function (err) {
165
+ node._lastError = err.message
166
+ node._failActiveRequest(err.message)
167
+ })
168
+
169
+ socket.on('close', function () {
170
+ node._connected = false
171
+ node._connecting = false
172
+ node._connection = null
173
+ node._incomingBuffer = Buffer.alloc(0)
174
+ node._failActiveRequest(node._lastError || 'connection closed')
175
+ if (!node._closing) {
176
+ node.status({ fill: 'red', shape: 'ring', text: 'disconnected' })
177
+ node._scheduleReconnect()
178
+ }
179
+ })
180
+ }
181
+
182
+ node._drainIncomingFrames = function () {
183
+ while (node._incomingBuffer.length >= 6) {
184
+ const mbapLength = node._incomingBuffer.readUInt16BE(4)
185
+ const expectedFrameLength = 6 + mbapLength
186
+ if (node._incomingBuffer.length < expectedFrameLength) {
187
+ return
188
+ }
189
+
190
+ const frame = Buffer.from(node._incomingBuffer.slice(0, expectedFrameLength))
191
+ node._incomingBuffer = Buffer.from(node._incomingBuffer.slice(expectedFrameLength))
192
+
193
+ if (node._activeRequest) {
194
+ const active = node._activeRequest
195
+ node._activeRequest = null
196
+ clearTimeout(node._activeTimeout)
197
+ node._activeTimeout = null
198
+ active.callback({ ok: true, responseBuffer: frame, error: null })
199
+ node._processing = false
200
+ node._processQueue()
201
+ }
202
+ }
203
+ }
204
+
205
+ node._failActiveRequest = function (error) {
206
+ if (!node._activeRequest) return
207
+ const active = node._activeRequest
208
+ node._activeRequest = null
209
+ clearTimeout(node._activeTimeout)
210
+ node._activeTimeout = null
211
+ active.callback({ ok: false, error, exception: 4 })
212
+ node._processing = false
213
+ }
214
+
215
+ node._scheduleReconnect = function () {
216
+ if (node._reconnectTimer || node._closing || node.targetType !== 'tcp') return
217
+ node._reconnectTimer = setTimeout(function () {
218
+ node._reconnectTimer = null
219
+ node._ensureTcpConnection()
220
+ }, node._reconnectDelay)
221
+ }
222
+
223
+ node._sendViaSerial = function (request, requestTimeout) {
224
+ // Serial support stub - would require 'serialport' package
225
+ clearTimeout(requestTimeout)
226
+ node._lastError = 'Serial not yet implemented'
227
+ request.callback({
228
+ ok: false,
229
+ error: 'serial not implemented',
230
+ exception: 4
231
+ })
232
+ node._processQueue()
233
+ }
234
+
235
+ node.on('close', function (done) {
236
+ node._closing = true
237
+ if (node._reconnectTimer) {
238
+ clearTimeout(node._reconnectTimer)
239
+ node._reconnectTimer = null
240
+ }
241
+ if (node._activeTimeout) {
242
+ clearTimeout(node._activeTimeout)
243
+ node._activeTimeout = null
244
+ }
245
+ node._activeRequest = null
246
+ if (node._connection) {
247
+ node._connection.destroy()
248
+ node._connection = null
249
+ }
250
+ node._requestQueue = []
251
+ done()
252
+ })
253
+
254
+ if (node.targetType === 'tcp') {
255
+ node._ensureTcpConnection()
256
+ }
257
+ }
258
+
259
+ function normalizeTimeout (value, fallback) {
260
+ const parsed = Number(value)
261
+ if (!Number.isFinite(parsed) || parsed <= 0) {
262
+ return fallback
263
+ }
264
+ return Math.floor(parsed)
265
+ }
266
+
267
+ function normalizePort (value, fallback) {
268
+ const parsed = Number(value)
269
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
270
+ return fallback
271
+ }
272
+ return Math.floor(parsed)
273
+ }
274
+
275
+ function normalizeBaud (value, fallback) {
276
+ const validBauds = [300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200]
277
+ const parsed = Number(value)
278
+ if (validBauds.includes(parsed)) {
279
+ return parsed
280
+ }
281
+ return fallback
282
+ }
283
+
284
+ function normalizeWordLength (value, fallback) {
285
+ const parsed = Number(value)
286
+ if (parsed === 5 || parsed === 6 || parsed === 7 || parsed === 8) {
287
+ return parsed
288
+ }
289
+ return fallback
290
+ }
291
+
292
+ function normalizeStopBits (value, fallback) {
293
+ const parsed = Number(value)
294
+ if (parsed === 1 || parsed === 2) {
295
+ return parsed
296
+ }
297
+ return fallback
298
+ }
299
+
300
+ RED.nodes.registerType('modbus-proxy-target', ModbusProxyTargetNode)
301
+ }
302
+
@@ -0,0 +1,109 @@
1
+ <script type="text/html" data-template-name="modbus-registers-config">
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-holdingRegisters"><i class="fa fa-list-ol"></i> Holding Registers</label>
9
+ <input type="number" id="node-config-input-holdingRegisters" min="0" placeholder="100">
10
+ </div>
11
+
12
+ <div class="form-row">
13
+ <label for="node-config-input-inputRegisters"><i class="fa fa-list-ol"></i> Input Registers</label>
14
+ <input type="number" id="node-config-input-inputRegisters" min="0" placeholder="100">
15
+ </div>
16
+
17
+ <div class="form-row">
18
+ <label for="node-config-input-coilRegisters"><i class="fa fa-dot-circle-o"></i> Coil Registers</label>
19
+ <input type="number" id="node-config-input-coilRegisters" min="0" placeholder="100">
20
+ </div>
21
+
22
+ <div class="form-row">
23
+ <label for="node-config-input-discreteRegisters"><i class="fa fa-dot-circle-o"></i> Discrete Inputs</label>
24
+ <input type="number" id="node-config-input-discreteRegisters" min="0" placeholder="100">
25
+ </div>
26
+
27
+ <div class="form-row">
28
+ <label for="node-config-input-wordOrderMode"><i class="fa fa-exchange"></i> 32-bit Word Order</label>
29
+ <select id="node-config-input-wordOrderMode">
30
+ <option value="ABCD">ABCD (Big-endian word, big-endian byte)</option>
31
+ <option value="CDAB">CDAB (Little-endian word, big-endian byte)</option>
32
+ <option value="BADC">BADC (Big-endian word, little-endian byte)</option>
33
+ <option value="DCBA">DCBA (Little-endian word, little-endian byte)</option>
34
+ </select>
35
+ </div>
36
+ </script>
37
+
38
+ <script type="text/html" data-help-name="modbus-registers-config">
39
+ <p>Defines a shared in-memory Modbus register map that is referenced by
40
+ <code>modbus-registers-write</code> and <code>modbus-registers-respond</code> nodes.
41
+ All nodes sharing the same config instance read and write the same register arrays.</p>
42
+
43
+ <h3>Configuration</h3>
44
+ <dl class="message-properties">
45
+ <dt>Name <span class="property-type">string</span></dt>
46
+ <dd>Optional label shown in the editor.</dd>
47
+
48
+ <dt>Holding Registers <span class="property-type">number</span></dt>
49
+ <dd>Number of holding registers (FC3/FC6/FC16) to allocate. Set to <code>0</code> to
50
+ disable this register type entirely — any client request targeting holding registers will
51
+ receive a Modbus exception&nbsp;2 (Illegal Data Address). Default: <code>100</code>.</dd>
52
+
53
+ <dt>Input Registers <span class="property-type">number</span></dt>
54
+ <dd>Number of input registers (FC4) to allocate. Set to <code>0</code> to disable.
55
+ Default: <code>100</code>.</dd>
56
+
57
+ <dt>Coil Registers <span class="property-type">number</span></dt>
58
+ <dd>Number of coil registers (FC1/FC5/FC15) to allocate. Set to <code>0</code> to disable.
59
+ Default: <code>100</code>.</dd>
60
+
61
+ <dt>Discrete Inputs <span class="property-type">number</span></dt>
62
+ <dd>Number of discrete input bits (FC2) to allocate. Set to <code>0</code> to disable.
63
+ Default: <code>100</code>.</dd>
64
+
65
+ <dt>32-bit Word Order <span class="property-type">select</span></dt>
66
+ <dd>Controls the byte and word ordering used when encoding or decoding 32-bit values
67
+ (<code>int32</code>, <code>uint32</code>, <code>float32</code>) across two consecutive
68
+ 16-bit registers. Uses PLC-style byte-sequence notation where A is the most-significant byte:
69
+ <ul>
70
+ <li><b>ABCD</b> — Big-endian word, big-endian byte. Most significant word first, most
71
+ significant byte first within each word. Most common; matches network byte order.</li>
72
+ <li><b>CDAB</b> — Little-endian word, big-endian byte (word swap). Least significant
73
+ word first, bytes within each word remain big-endian.</li>
74
+ <li><b>BADC</b> — Big-endian word, little-endian byte. Most significant word first,
75
+ bytes within each word are swapped.</li>
76
+ <li><b>DCBA</b> — Little-endian word, little-endian byte (full swap). Both word order
77
+ and byte order are reversed.</li>
78
+ </ul>
79
+ Default: <b>ABCD</b>.</dd>
80
+ </dl>
81
+
82
+ <h3>Details</h3>
83
+ <p>A single config node represents one independent register space. Multiple
84
+ <code>modbus-registers-write</code> and <code>modbus-registers-respond</code> nodes linked to
85
+ the same config node share the same register data — writes from one node are immediately
86
+ visible to all others.</p>
87
+ <p>All register arrays are initialised to zero when Node-RED deploys the flow. Values persist
88
+ in memory for the lifetime of the runtime; they are not saved to disk.</p>
89
+ <p>The 32-bit word order setting applies globally to all write nodes that use this register map.
90
+ Ensure all writes and your Modbus client agree on the same ordering.</p>
91
+ </script>
92
+
93
+ <script type="text/javascript">
94
+ RED.nodes.registerType('modbus-registers-config', {
95
+ category: 'config',
96
+ defaults: {
97
+ name: { value: '' },
98
+ holdingRegisters: { value: 100, required: true, validate: RED.validators.number() },
99
+ inputRegisters: { value: 100, required: true, validate: RED.validators.number() },
100
+ coilRegisters: { value: 100, required: true, validate: RED.validators.number() },
101
+ discreteRegisters: { value: 100, required: true, validate: RED.validators.number() },
102
+ wordOrderMode: { value: 'ABCD', required: true }
103
+ },
104
+ label: function () {
105
+ return this.name || 'Registers Map'
106
+ }
107
+ })
108
+ </script>
109
+
@@ -0,0 +1,216 @@
1
+ module.exports = function (RED) {
2
+ function ModbusRegistersConfigNode (config) {
3
+ RED.nodes.createNode(this, config)
4
+ const node = this
5
+
6
+ node.name = config.name || ''
7
+ node.holdingRegisters = normalizeCount(config.holdingRegisters, 100)
8
+ node.inputRegisters = normalizeCount(config.inputRegisters, 100)
9
+ node.coilRegisters = normalizeCount(config.coilRegisters, 100)
10
+ node.discreteRegisters = normalizeCount(config.discreteRegisters, 100)
11
+ node.wordOrderMode = normalizeWordOrderMode(config.wordOrderMode)
12
+
13
+ node._maps = {
14
+ holding: new Uint16Array(node.holdingRegisters),
15
+ input: new Uint16Array(node.inputRegisters),
16
+ coil: new Array(node.coilRegisters).fill(0),
17
+ discrete: new Array(node.discreteRegisters).fill(0)
18
+ }
19
+
20
+ node.readWords = function (registerType, start, quantity) {
21
+ const map = getWordMap(registerType, node._maps)
22
+ if (!map || !isNonNegativeInteger(start) || !isPositiveInteger(quantity) || start + quantity > map.length) {
23
+ return null
24
+ }
25
+ return Array.from(map.slice(start, start + quantity))
26
+ }
27
+
28
+ node.readBits = function (registerType, start, quantity) {
29
+ const map = getBitMap(registerType, node._maps)
30
+ if (!map || !isNonNegativeInteger(start) || !isPositiveInteger(quantity) || start + quantity > map.length) {
31
+ return null
32
+ }
33
+ return map.slice(start, start + quantity).map(Boolean)
34
+ }
35
+
36
+ node.writeValue = function (registerType, address, format, value) {
37
+ if (!isNonNegativeInteger(address)) {
38
+ return { ok: false, error: 'address must be a non-negative integer' }
39
+ }
40
+
41
+ if (registerType === 'coil' || registerType === 'discrete') {
42
+ const bitMap = getBitMap(registerType, node._maps)
43
+ if (!bitMap || address >= bitMap.length) {
44
+ return { ok: false, error: 'address out of range' }
45
+ }
46
+
47
+ bitMap[address] = toBitValue(value)
48
+ return { ok: true, wordsWritten: 1 }
49
+ }
50
+
51
+ const map = getWordMap(registerType, node._maps)
52
+ if (!map) {
53
+ return { ok: false, error: `unsupported registerType ${registerType}` }
54
+ }
55
+
56
+ const words = encodeValueAsWords(format, value, node.wordOrderMode)
57
+ if (!words) {
58
+ return { ok: false, error: `unsupported format ${format}` }
59
+ }
60
+ if (address + words.length > map.length) {
61
+ return { ok: false, error: 'address out of range' }
62
+ }
63
+
64
+ words.forEach(function (word, idx) {
65
+ map[address + idx] = word
66
+ })
67
+ return { ok: true, wordsWritten: words.length }
68
+ }
69
+
70
+ node.writeWords = function (registerType, address, values) {
71
+ const map = getWordMap(registerType, node._maps)
72
+ if (!map || !isNonNegativeInteger(address) || !Array.isArray(values) || address + values.length > map.length) {
73
+ return { ok: false, error: 'invalid write range' }
74
+ }
75
+
76
+ values.forEach(function (value, idx) {
77
+ map[address + idx] = Number(value) & 0xffff
78
+ })
79
+
80
+ return { ok: true, wordsWritten: values.length }
81
+ }
82
+
83
+ node.writeBits = function (registerType, address, values) {
84
+ const map = getBitMap(registerType, node._maps)
85
+ if (!map || !isNonNegativeInteger(address) || !Array.isArray(values) || address + values.length > map.length) {
86
+ return { ok: false, error: 'invalid write range' }
87
+ }
88
+
89
+ values.forEach(function (value, idx) {
90
+ map[address + idx] = toBitValue(value)
91
+ })
92
+
93
+ return { ok: true, bitsWritten: values.length }
94
+ }
95
+ }
96
+
97
+ function normalizeCount (value, fallback) {
98
+ if (value === '' || value === null || value === undefined) {
99
+ return fallback
100
+ }
101
+
102
+ const n = Number(value)
103
+ if (!Number.isFinite(n) || n < 0) {
104
+ return fallback
105
+ }
106
+ return Math.floor(n)
107
+ }
108
+
109
+ function getWordMap (registerType, maps) {
110
+ if (registerType === 'holding') return maps.holding
111
+ if (registerType === 'input') return maps.input
112
+ return null
113
+ }
114
+
115
+ function getBitMap (registerType, maps) {
116
+ if (registerType === 'coil') return maps.coil
117
+ if (registerType === 'discrete') return maps.discrete
118
+ return null
119
+ }
120
+
121
+ function normalizeWordOrderMode (value) {
122
+ const mode = String(value || '').trim().toLowerCase()
123
+ if (mode === 'abcd' || mode === 'be-be') return 'ABCD'
124
+ if (mode === 'cdab' || mode === 'le-be') return 'CDAB'
125
+ if (mode === 'badc' || mode === 'be-le') return 'BADC'
126
+ if (mode === 'dcba' || mode === 'le-le') return 'DCBA'
127
+ return 'ABCD'
128
+ }
129
+
130
+ function isNonNegativeInteger (value) {
131
+ return Number.isInteger(value) && value >= 0
132
+ }
133
+
134
+ function isPositiveInteger (value) {
135
+ return Number.isInteger(value) && value > 0
136
+ }
137
+
138
+ function toBitValue (value) {
139
+ if (typeof value === 'string') {
140
+ const normalized = value.trim().toLowerCase()
141
+ if (normalized === 'true' || normalized === '1' || normalized === 'on') {
142
+ return 1
143
+ }
144
+ if (normalized === 'false' || normalized === '0' || normalized === 'off') {
145
+ return 0
146
+ }
147
+ }
148
+
149
+ return value ? 1 : 0
150
+ }
151
+
152
+ function encodeValueAsWords (format, value, wordOrderMode) {
153
+ switch (format) {
154
+ case 'int16':
155
+ case 'uint16': {
156
+ const n = Number(value)
157
+ if (!Number.isFinite(n)) return null
158
+ return [n & 0xffff]
159
+ }
160
+ case 'uint32': {
161
+ const n = Number(value)
162
+ if (!Number.isFinite(n) || n < 0 || n > 0xffffffff) return null
163
+ const buf = Buffer.alloc(4)
164
+ buf.writeUInt32BE(Math.floor(n), 0)
165
+ return encode32BitBufferToWords(buf, wordOrderMode)
166
+ }
167
+ case 'int32': {
168
+ const n = Number(value)
169
+ if (!Number.isFinite(n) || n < -2147483648 || n > 2147483647) return null
170
+ const buf = Buffer.alloc(4)
171
+ buf.writeInt32BE(Math.floor(n), 0)
172
+ return encode32BitBufferToWords(buf, wordOrderMode)
173
+ }
174
+ case 'float32': {
175
+ const n = Number(value)
176
+ if (!Number.isFinite(n)) return null
177
+ const buf = Buffer.alloc(4)
178
+ buf.writeFloatBE(n, 0)
179
+ return encode32BitBufferToWords(buf, wordOrderMode)
180
+ }
181
+ // Backward-compatible aliases for older flows
182
+ case 'float32be':
183
+ return encodeValueAsWords('float32', value, 'ABCD')
184
+ case 'float32le': {
185
+ return encodeValueAsWords('float32', value, 'BADC')
186
+ }
187
+ default:
188
+ return null
189
+ }
190
+ }
191
+
192
+ function encode32BitBufferToWords (buf, mode) {
193
+ const normalizedMode = normalizeWordOrderMode(mode)
194
+ let firstWordBytes = [buf[0], buf[1]]
195
+ let secondWordBytes = [buf[2], buf[3]]
196
+
197
+ if (normalizedMode === 'BADC' || normalizedMode === 'DCBA') {
198
+ firstWordBytes = [firstWordBytes[1], firstWordBytes[0]]
199
+ secondWordBytes = [secondWordBytes[1], secondWordBytes[0]]
200
+ }
201
+
202
+ if (normalizedMode === 'CDAB' || normalizedMode === 'DCBA') {
203
+ const tmp = firstWordBytes
204
+ firstWordBytes = secondWordBytes
205
+ secondWordBytes = tmp
206
+ }
207
+
208
+ return [
209
+ (firstWordBytes[0] << 8) | firstWordBytes[1],
210
+ (secondWordBytes[0] << 8) | secondWordBytes[1]
211
+ ]
212
+ }
213
+
214
+ RED.nodes.registerType('modbus-registers-config', ModbusRegistersConfigNode)
215
+ }
216
+