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,286 @@
1
+ const { respondWithPayload } = require('./modbus-response-context')
2
+
3
+ module.exports = function (RED) {
4
+ const EXCEPTION_ILLEGAL_FUNCTION = 1
5
+ const EXCEPTION_ILLEGAL_DATA_ADDRESS = 2
6
+
7
+ function ModbusRegistersRespondNode (config) {
8
+ RED.nodes.createNode(this, config)
9
+ const node = this
10
+
11
+ node.name = config.name || ''
12
+ node.registerMap = RED.nodes.getNode(config.registerMap)
13
+
14
+ if (!node.registerMap) {
15
+ node.status({ fill: 'red', shape: 'ring', text: 'no register map' })
16
+ return
17
+ }
18
+
19
+ node.status({ fill: 'grey', shape: 'ring', text: 'ready' })
20
+
21
+ node.on('input', function (msg, send, done) {
22
+ const context = msg._modbus || {}
23
+ const requestId = context.requestId
24
+ const eventName = context.eventName
25
+ const request = msg.payload || {}
26
+ const body = request.body || {}
27
+
28
+ if (!requestId || !eventName) {
29
+ node.error('Missing Modbus request context', msg)
30
+ node.status({ fill: 'red', shape: 'ring', text: 'missing context' })
31
+ done()
32
+ return
33
+ }
34
+
35
+ const responsePayload = buildResponsePayload(node.registerMap, eventName, request, body)
36
+ const actualDataSent = deriveActualDataSent(eventName, request.quantity, responsePayload)
37
+
38
+ const responseResult = respondWithPayload(RED, node, msg, responsePayload)
39
+ if (!responseResult.context) {
40
+ node.status({ fill: 'red', shape: 'ring', text: 'missing context' })
41
+ done()
42
+ return
43
+ }
44
+ const ok = responseResult.ok
45
+ if (!ok) {
46
+ node.status({ fill: 'red', shape: 'ring', text: 'unknown request' })
47
+ done(new Error(`modbus-registers-respond: request ${requestId} could not be responded to`))
48
+ return
49
+ }
50
+
51
+ msg.modbusResponse = {
52
+ requestId,
53
+ eventName,
54
+ address: request.address,
55
+ quantity: request.quantity,
56
+ payloadSubmitted: clonePayload(responsePayload),
57
+ payloadType: describePayload(responsePayload),
58
+ actualDataSent,
59
+ actualDataType: describePayload(actualDataSent),
60
+ ok,
61
+ timestamp: new Date().toISOString()
62
+ }
63
+
64
+ if (typeof eventName === 'string' && eventName.startsWith('write')) {
65
+ msg.modbusResponse.note = 'Write FC response is auto-built from request body by server config'
66
+ }
67
+
68
+ if (responsePayload && responsePayload.exception) {
69
+ node.status({ fill: 'yellow', shape: 'ring', text: `exception ${responsePayload.exception}` })
70
+ } else {
71
+ node.status({ fill: 'green', shape: 'dot', text: eventName })
72
+ }
73
+ send(msg)
74
+ done()
75
+ })
76
+ }
77
+
78
+ function buildResponsePayload (registerMap, eventName, request, body) {
79
+ const address = Number(request.address)
80
+ const quantity = Number(request.quantity)
81
+
82
+ switch (eventName) {
83
+ case 'readHoldingRegisters':
84
+ return readWordsOrException(registerMap, 'holding', address, quantity)
85
+ case 'readInputRegisters':
86
+ return readWordsOrException(registerMap, 'input', address, quantity)
87
+ case 'readCoils':
88
+ return readBitsOrException(registerMap, 'coil', address, quantity)
89
+ case 'readDiscreteInputs':
90
+ return readBitsOrException(registerMap, 'discrete', address, quantity)
91
+ case 'writeSingleRegister':
92
+ return handleSingleWordWrite(registerMap, 'holding', body)
93
+ case 'writeMultipleRegisters':
94
+ return handleMultipleWordWrite(registerMap, 'holding', body)
95
+ case 'writeSingleCoil':
96
+ return handleSingleBitWrite(registerMap, 'coil', body)
97
+ case 'writeMultipleCoils':
98
+ return handleMultipleBitWrite(registerMap, 'coil', body)
99
+ default:
100
+ return { exception: EXCEPTION_ILLEGAL_FUNCTION }
101
+ }
102
+ }
103
+
104
+ function readWordsOrException (registerMap, registerType, address, quantity) {
105
+ const values = registerMap.readWords(registerType, address, quantity)
106
+ return values === null ? { exception: EXCEPTION_ILLEGAL_DATA_ADDRESS } : values
107
+ }
108
+
109
+ function readBitsOrException (registerMap, registerType, address, quantity) {
110
+ const values = registerMap.readBits(registerType, address, quantity)
111
+ return values === null ? { exception: EXCEPTION_ILLEGAL_DATA_ADDRESS } : values
112
+ }
113
+
114
+ function handleSingleWordWrite (registerMap, registerType, body) {
115
+ const address = Number(body.address)
116
+ const value = readSingleWordValue(body)
117
+ const result = registerMap.writeWords(registerType, address, [value])
118
+ return result.ok ? undefined : { exception: EXCEPTION_ILLEGAL_DATA_ADDRESS }
119
+ }
120
+
121
+ function handleMultipleWordWrite (registerMap, registerType, body) {
122
+ const address = Number(body.address)
123
+ const values = readWordValues(body)
124
+ const result = registerMap.writeWords(registerType, address, values)
125
+ return result.ok ? undefined : { exception: EXCEPTION_ILLEGAL_DATA_ADDRESS }
126
+ }
127
+
128
+ function handleSingleBitWrite (registerMap, registerType, body) {
129
+ const address = Number(body.address)
130
+ const value = readSingleBitValue(body)
131
+ const result = registerMap.writeBits(registerType, address, [value])
132
+ return result.ok ? undefined : { exception: EXCEPTION_ILLEGAL_DATA_ADDRESS }
133
+ }
134
+
135
+ function handleMultipleBitWrite (registerMap, registerType, body) {
136
+ const address = Number(body.address)
137
+ const values = readBitValues(body)
138
+ const result = registerMap.writeBits(registerType, address, values)
139
+ return result.ok ? undefined : { exception: EXCEPTION_ILLEGAL_DATA_ADDRESS }
140
+ }
141
+
142
+ function readSingleWordValue (body) {
143
+ if (body.value !== undefined) return Number(body.value) & 0xffff
144
+ if (body.values && body.values.length) return Number(body.values[0]) & 0xffff
145
+ return 0
146
+ }
147
+
148
+ function readWordValues (body) {
149
+ if (Array.isArray(body.values)) {
150
+ return body.values.map(function (value) {
151
+ return Number(value) & 0xffff
152
+ })
153
+ }
154
+
155
+ if (Buffer.isBuffer(body.valuesAsBuffer)) {
156
+ const values = []
157
+ for (let offset = 0; offset + 1 < body.valuesAsBuffer.length; offset += 2) {
158
+ values.push(body.valuesAsBuffer.readUInt16BE(offset))
159
+ }
160
+ return values
161
+ }
162
+
163
+ return []
164
+ }
165
+
166
+ function readSingleBitValue (body) {
167
+ if (body.value !== undefined) return body.value ? 1 : 0
168
+ if (body.values && body.values.length) return body.values[0] ? 1 : 0
169
+ return 0
170
+ }
171
+
172
+ function readBitValues (body) {
173
+ if (Array.isArray(body.values)) {
174
+ return body.values.map(function (value) {
175
+ return value ? 1 : 0
176
+ })
177
+ }
178
+
179
+ if (Buffer.isBuffer(body.valuesAsBuffer)) {
180
+ const bits = []
181
+ for (let i = 0; i < body.valuesAsBuffer.length; i++) {
182
+ const byte = body.valuesAsBuffer[i]
183
+ for (let b = 0; b < 8; b++) {
184
+ bits.push((byte >> b) & 1)
185
+ }
186
+ }
187
+ return bits
188
+ }
189
+
190
+ return []
191
+ }
192
+
193
+ function clonePayload (payload) {
194
+ if (Buffer.isBuffer(payload)) {
195
+ return Buffer.from(payload)
196
+ }
197
+ if (Array.isArray(payload)) {
198
+ return payload.slice()
199
+ }
200
+ if (payload && typeof payload === 'object') {
201
+ return { ...payload }
202
+ }
203
+ return payload
204
+ }
205
+
206
+ function describePayload (payload) {
207
+ if (Buffer.isBuffer(payload)) return 'buffer'
208
+ if (Array.isArray(payload)) return 'array'
209
+ return typeof payload
210
+ }
211
+
212
+ function deriveActualDataSent (eventName, quantity, payload) {
213
+ const count = Number(quantity)
214
+ if (!Number.isFinite(count) || count <= 0) {
215
+ return payload
216
+ }
217
+
218
+ if (eventName === 'readHoldingRegisters' || eventName === 'readInputRegisters') {
219
+ return normalizeWordPayload(payload, count)
220
+ }
221
+
222
+ if (eventName === 'readCoils' || eventName === 'readDiscreteInputs') {
223
+ return normalizeBitPayload(payload, count)
224
+ }
225
+
226
+ return payload
227
+ }
228
+
229
+ function normalizeWordPayload (payload, count) {
230
+ if (Buffer.isBuffer(payload)) {
231
+ const expectedBytes = count * 2
232
+ const buf = Buffer.alloc(expectedBytes, 0)
233
+ payload.copy(buf, 0, 0, Math.min(payload.length, expectedBytes))
234
+ const words = []
235
+ for (let i = 0; i < expectedBytes; i += 2) {
236
+ words.push(buf.readUInt16BE(i))
237
+ }
238
+ return words
239
+ }
240
+
241
+ if (Array.isArray(payload)) {
242
+ const out = new Array(count).fill(0)
243
+ payload.slice(0, count).forEach(function (v, i) {
244
+ out[i] = Number(v) & 0xffff
245
+ })
246
+ return out
247
+ }
248
+
249
+ if (payload && typeof payload === 'object' && Array.isArray(payload.values)) {
250
+ return normalizeWordPayload(payload.values, count)
251
+ }
252
+
253
+ return new Array(count).fill(0)
254
+ }
255
+
256
+ function normalizeBitPayload (payload, count) {
257
+ if (Buffer.isBuffer(payload)) {
258
+ const out = new Array(count).fill(0)
259
+ for (let i = 0; i < count; i++) {
260
+ const byteIndex = Math.floor(i / 8)
261
+ const bitIndex = i % 8
262
+ if (byteIndex < payload.length) {
263
+ out[i] = (payload[byteIndex] >> bitIndex) & 1
264
+ }
265
+ }
266
+ return out
267
+ }
268
+
269
+ if (Array.isArray(payload)) {
270
+ const out = new Array(count).fill(0)
271
+ payload.slice(0, count).forEach(function (v, i) {
272
+ out[i] = v ? 1 : 0
273
+ })
274
+ return out
275
+ }
276
+
277
+ if (payload && typeof payload === 'object' && Array.isArray(payload.values)) {
278
+ return normalizeBitPayload(payload.values, count)
279
+ }
280
+
281
+ return new Array(count).fill(0)
282
+ }
283
+
284
+ RED.nodes.registerType('modbus-registers-respond', ModbusRegistersRespondNode)
285
+ }
286
+
@@ -0,0 +1,157 @@
1
+ <script type="text/html" data-template-name="modbus-registers-write">
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-registerMap"><i class="fa fa-database"></i> Register Map</label>
9
+ <input type="text" id="node-input-registerMap">
10
+ </div>
11
+
12
+ <div class="form-row">
13
+ <label for="node-input-registerType"><i class="fa fa-filter"></i> Register Type</label>
14
+ <select id="node-input-registerType">
15
+ <option value="holding">holding</option>
16
+ <option value="input">input</option>
17
+ <option value="coil">coil</option>
18
+ <option value="discrete">discrete</option>
19
+ </select>
20
+ </div>
21
+
22
+ <div class="form-row">
23
+ <label for="node-input-registerAddress"><i class="fa fa-hashtag"></i> Register Address</label>
24
+ <input type="number" id="node-input-registerAddress" min="0" placeholder="0">
25
+ </div>
26
+
27
+ <div class="form-row">
28
+ <label for="node-input-registerFormat"><i class="fa fa-code"></i> Format</label>
29
+ <select id="node-input-registerFormat">
30
+ <option value="int16">int16</option>
31
+ <option value="uint16">uint16</option>
32
+ <option value="int32">int32</option>
33
+ <option value="uint32">uint32</option>
34
+ <option value="float32">float32</option>
35
+ </select>
36
+ </div>
37
+
38
+ <div class="form-row">
39
+ <label for="node-input-exceptionOnOutOfRangeValues"><i class="fa fa-exclamation-triangle"></i> Exception On Out Of Range Values</label>
40
+ <input type="checkbox" id="node-input-exceptionOnOutOfRangeValues" style="display:inline-block; width:auto;">
41
+ </div>
42
+ </script>
43
+
44
+ <script type="text/html" data-help-name="modbus-registers-write">
45
+ <p>Writes a value into a shared <code>modbus-registers-config</code> register map on each
46
+ input message. Supports 16-bit and 32-bit signed/unsigned integer and floating-point formats.
47
+ The message is forwarded unchanged (plus write metadata) after a successful write.</p>
48
+
49
+ <h3>Inputs</h3>
50
+ <dl class="message-properties">
51
+ <dt>payload <span class="property-type">number | string | boolean</span></dt>
52
+ <dd>The value to write. For integer formats the value is truncated to a whole number before
53
+ writing. For <code>coil</code> and <code>discrete</code> register types any truthy value
54
+ writes <code>1</code>, falsy writes <code>0</code>.</dd>
55
+
56
+ <dt class="optional">registerType <span class="property-type">string</span></dt>
57
+ <dd>Overrides the configured register type for this message only.
58
+ Allowed values: <code>holding</code>, <code>input</code>, <code>coil</code>,
59
+ <code>discrete</code>.</dd>
60
+
61
+ <dt class="optional">registerAddress <span class="property-type">number</span></dt>
62
+ <dd>Overrides the configured register address (0-based) for this message only.</dd>
63
+
64
+ <dt class="optional">registerFormat <span class="property-type">string</span></dt>
65
+ <dd>Overrides the configured data format for this message only.
66
+ Allowed values: <code>int16</code>, <code>uint16</code>, <code>int32</code>,
67
+ <code>uint32</code>, <code>float32</code>.</dd>
68
+
69
+ <dt class="optional">exceptionOnOutOfRangeValues <span class="property-type">boolean</span></dt>
70
+ <dd>Overrides the node's <em>Exception On Out Of Range Values</em> setting for this
71
+ message only.</dd>
72
+ </dl>
73
+
74
+ <h3>Outputs</h3>
75
+ <ol class="node-ports">
76
+ <li>Passthrough message
77
+ <dl class="message-properties">
78
+ <dt>modbusRegistersWrite <span class="property-type">object</span></dt>
79
+ <dd>Metadata added to the message after a successful write:
80
+ <ul>
81
+ <li><code>registerType</code> — register type written to.</li>
82
+ <li><code>registerAddress</code> — address written to.</li>
83
+ <li><code>registerFormat</code> — data format used.</li>
84
+ <li><code>originalValue</code> — value received in <code>msg.payload</code>.</li>
85
+ <li><code>writtenValue</code> — value actually stored (may differ if clamped).</li>
86
+ <li><code>outOfRange</code> — <code>true</code> if the value exceeded the format's valid range.</li>
87
+ <li><code>clamped</code> — <code>true</code> if the value was clamped rather than rejected.</li>
88
+ <li><code>wordsWritten</code> — number of 16-bit registers written (1 for 16-bit formats, 2 for 32-bit).</li>
89
+ <li><code>bitsWritten</code> — number of bits written (for coil/discrete writes).</li>
90
+ </ul>
91
+ </dd>
92
+ </dl>
93
+ </li>
94
+ </ol>
95
+
96
+ <h3>Configuration</h3>
97
+ <dl class="message-properties">
98
+ <dt>Register Map <span class="property-type">modbus-registers-config</span></dt>
99
+ <dd>The shared register map this node writes to. Required.</dd>
100
+
101
+ <dt>Register Type <span class="property-type">select</span></dt>
102
+ <dd>The type of register to write:
103
+ <ul>
104
+ <li><code>holding</code> — FC6/FC16 holding registers (read/write).</li>
105
+ <li><code>input</code> — input registers (read-only by Modbus clients, but writable here for simulation).</li>
106
+ <li><code>coil</code> — FC5/FC15 single-bit coil registers.</li>
107
+ <li><code>discrete</code> — discrete input bits (read-only by clients, writable here for simulation).</li>
108
+ </ul>
109
+ </dd>
110
+
111
+ <dt>Register Address <span class="property-type">number</span></dt>
112
+ <dd>Zero-based register address to write to. For 32-bit formats, registers
113
+ <code>address</code> and <code>address+1</code> are both written.</dd>
114
+
115
+ <dt>Format <span class="property-type">select</span></dt>
116
+ <dd>Data format to use when encoding the value into 16-bit register words:
117
+ <ul>
118
+ <li><code>int16</code> — signed 16-bit integer (−32768 to 32767).</li>
119
+ <li><code>uint16</code> — unsigned 16-bit integer (0 to 65535).</li>
120
+ <li><code>int32</code> — signed 32-bit integer (−2147483648 to 2147483647), occupies 2 registers.</li>
121
+ <li><code>uint32</code> — unsigned 32-bit integer (0 to 4294967295), occupies 2 registers.</li>
122
+ <li><code>float32</code> — IEEE 754 single-precision float, occupies 2 registers.</li>
123
+ </ul>
124
+ 32-bit word/byte ordering is controlled by the linked register map's <em>32-bit Word Order</em> setting.</dd>
125
+
126
+ <dt>Exception On Out Of Range Values <span class="property-type">boolean</span></dt>
127
+ <dd>Controls behaviour when a value falls outside the valid range for the selected format:
128
+ <ul>
129
+ <li><b>Checked (default)</b> — the write is aborted and an error is raised, catchable by a Catch node.</li>
130
+ <li><b>Unchecked</b> — the value is clamped to the nearest min/max boundary and written. <code>msg.modbusRegistersWrite.clamped</code> will be <code>true</code>.</li>
131
+ </ul>
132
+ Range checking applies to integer formats only. <code>float32</code> values are always passed through.
133
+ </dd>
134
+ </dl>
135
+ </script>
136
+
137
+ <script type="text/javascript">
138
+ RED.nodes.registerType('modbus-registers-write', {
139
+ category: 'modbus',
140
+ color: '#E9967A',
141
+ defaults: {
142
+ name: { value: '' },
143
+ registerMap: { value: '', type: 'modbus-registers-config', required: true },
144
+ registerType: { value: 'holding', required: true },
145
+ registerAddress: { value: 0, required: true, validate: RED.validators.number() },
146
+ registerFormat: { value: 'uint16', required: true },
147
+ exceptionOnOutOfRangeValues: { value: true }
148
+ },
149
+ inputs: 1,
150
+ outputs: 1,
151
+ icon: 'file.svg',
152
+ label: function () {
153
+ return this.name || 'Modbus Registers Write'
154
+ }
155
+ })
156
+ </script>
157
+
@@ -0,0 +1,156 @@
1
+ function formatStatusValue (value) {
2
+ if (value === null) return 'null'
3
+ if (value === undefined) return 'undefined'
4
+
5
+ if (typeof value === 'number') {
6
+ if (!Number.isFinite(value)) return String(value)
7
+ return String(value)
8
+ }
9
+
10
+ if (typeof value === 'bigint' || typeof value === 'boolean') {
11
+ return String(value)
12
+ }
13
+
14
+ if (typeof value === 'string') {
15
+ return value.length > 20 ? `${value.slice(0, 17)}...` : value
16
+ }
17
+
18
+ if (Array.isArray(value)) {
19
+ return `[${value.length} items]`
20
+ }
21
+
22
+ if (typeof value === 'object') {
23
+ return '[object]'
24
+ }
25
+
26
+ return String(value)
27
+ }
28
+
29
+ module.exports = function (RED) {
30
+ function ModbusRegistersWriteNode (config) {
31
+ RED.nodes.createNode(this, config)
32
+ const node = this
33
+
34
+ node.name = config.name || ''
35
+ node.registerMap = RED.nodes.getNode(config.registerMap)
36
+ node.registerType = config.registerType || 'holding'
37
+ node.registerAddress = normalizeAddress(config.registerAddress)
38
+ node.registerFormat = config.registerFormat || 'uint16'
39
+ node.exceptionOnOutOfRangeValues = normalizeBooleanDefaultTrue(config.exceptionOnOutOfRangeValues)
40
+
41
+ if (!node.registerMap) {
42
+ node.status({ fill: 'red', shape: 'ring', text: 'no register map' })
43
+ return
44
+ }
45
+
46
+ node.status({ fill: 'grey', shape: 'ring', text: 'ready' })
47
+
48
+ node.on('input', function (msg, send, done) {
49
+ const registerType = msg.registerType || node.registerType
50
+ const registerAddress = normalizeAddress(msg.registerAddress, node.registerAddress)
51
+ const registerFormat = msg.registerFormat || node.registerFormat
52
+ const exceptionOnOutOfRangeValues = normalizeBooleanDefaultTrue(
53
+ msg.exceptionOnOutOfRangeValues,
54
+ node.exceptionOnOutOfRangeValues
55
+ )
56
+
57
+ const valueResult = normalizeValueForFormat(registerFormat, msg.payload)
58
+ if (valueResult.outOfRange && exceptionOnOutOfRangeValues) {
59
+ node.status({ fill: 'red', shape: 'ring', text: 'value out of range' })
60
+ done(new Error(`modbus-registers-write: value ${valueResult.originalValue} out of range for ${registerFormat}`))
61
+ return
62
+ }
63
+
64
+ const value = valueResult.value
65
+
66
+ const result = node.registerMap.writeValue(registerType, registerAddress, registerFormat, value)
67
+ if (!result.ok) {
68
+ node.status({ fill: 'red', shape: 'ring', text: 'write failed' })
69
+ done(new Error(`modbus-registers-write: ${result.error}`))
70
+ return
71
+ }
72
+
73
+ msg.modbusRegistersWrite = {
74
+ registerType,
75
+ registerAddress,
76
+ registerFormat,
77
+ originalValue: valueResult.originalValue,
78
+ writtenValue: value,
79
+ outOfRange: valueResult.outOfRange,
80
+ clamped: valueResult.outOfRange && !exceptionOnOutOfRangeValues,
81
+ wordsWritten: result.wordsWritten || 0,
82
+ bitsWritten: result.bitsWritten || 0
83
+ }
84
+
85
+ const valueText = formatStatusValue(value)
86
+ node.status({ fill: 'green', shape: 'dot', text: `wrote ${valueText}` })
87
+
88
+ send(msg)
89
+ done()
90
+ })
91
+ }
92
+
93
+ function normalizeAddress (value, fallback) {
94
+ const parsed = Number(value)
95
+ if (!Number.isFinite(parsed) || parsed < 0) {
96
+ return fallback !== undefined ? fallback : 0
97
+ }
98
+ return Math.floor(parsed)
99
+ }
100
+
101
+ function normalizeBooleanDefaultTrue (value, fallback) {
102
+ if (value === undefined || value === null || value === '') {
103
+ if (fallback === undefined) return true
104
+ return fallback
105
+ }
106
+
107
+ if (typeof value === 'boolean') {
108
+ return value
109
+ }
110
+
111
+ const normalized = String(value).trim().toLowerCase()
112
+ if (normalized === 'false' || normalized === '0' || normalized === 'off' || normalized === 'no') {
113
+ return false
114
+ }
115
+
116
+ return true
117
+ }
118
+
119
+ function normalizeValueForFormat (format, value) {
120
+ if (format === 'uint16') return normalizeIntegerRange(value, 0, 65535)
121
+ if (format === 'int16') return normalizeIntegerRange(value, -32768, 32767)
122
+ if (format === 'uint32') return normalizeIntegerRange(value, 0, 0xffffffff)
123
+ if (format === 'int32') return normalizeIntegerRange(value, -2147483648, 2147483647)
124
+
125
+ return {
126
+ value,
127
+ originalValue: value,
128
+ outOfRange: false
129
+ }
130
+ }
131
+
132
+ function normalizeIntegerRange (value, min, max) {
133
+ const originalValue = value
134
+ const parsed = Number(value)
135
+ if (!Number.isFinite(parsed)) {
136
+ return { value: min, originalValue, outOfRange: true }
137
+ }
138
+
139
+ const integerValue = Math.trunc(parsed)
140
+ if (integerValue < min) {
141
+ return { value: min, originalValue, outOfRange: true }
142
+ }
143
+ if (integerValue > max) {
144
+ return { value: max, originalValue, outOfRange: true }
145
+ }
146
+
147
+ return {
148
+ value: integerValue,
149
+ originalValue,
150
+ outOfRange: false
151
+ }
152
+ }
153
+
154
+ RED.nodes.registerType('modbus-registers-write', ModbusRegistersWriteNode)
155
+ }
156
+
@@ -0,0 +1,52 @@
1
+ 'use strict'
2
+
3
+ function resolveRequestContext (RED, node, msg) {
4
+ const context = msg && msg._modbus
5
+ if (!context || typeof context !== 'object') {
6
+ node.error('Missing Modbus request context', msg)
7
+ return null
8
+ }
9
+
10
+ const requestId = context.requestId
11
+ const configNodeId = context.configNodeId
12
+
13
+ if (!requestId || !configNodeId) {
14
+ node.error('Missing Modbus request context', msg)
15
+ return null
16
+ }
17
+
18
+ const serverNode = RED.nodes.getNode(configNodeId)
19
+ if (!serverNode || typeof serverNode.respondToRequest !== 'function') {
20
+ node.error('Invalid Modbus request context', msg)
21
+ return null
22
+ }
23
+
24
+ return {
25
+ requestId,
26
+ configNodeId,
27
+ eventName: context.eventName,
28
+ address: context.address,
29
+ quantity: context.quantity,
30
+ serverNode
31
+ }
32
+ }
33
+
34
+ function respondWithPayload (RED, node, msg, payload) {
35
+ const context = resolveRequestContext(RED, node, msg)
36
+ if (!context) {
37
+ return { ok: false, context: null }
38
+ }
39
+
40
+ const ok = context.serverNode.respondToRequest(context.requestId, payload)
41
+ if (!ok) {
42
+ node.warn(`respondToRequest returned false for requestId ${context.requestId}`)
43
+ }
44
+
45
+ return { ok, context }
46
+ }
47
+
48
+ module.exports = {
49
+ resolveRequestContext,
50
+ respondWithPayload
51
+ }
52
+