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,133 @@
1
+ <script type="text/html" data-template-name="modbus-registers-read">
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
+ </script>
38
+
39
+ <script type="text/html" data-help-name="modbus-registers-read">
40
+ <p>Reads a value from a shared <code>modbus-registers-config</code> register map on each input
41
+ message and emits the decoded result as a normal Node-RED message.</p>
42
+
43
+ <h3>Inputs</h3>
44
+ <dl class="message-properties">
45
+ <dt class="optional">registerType <span class="property-type">string</span></dt>
46
+ <dd>Overrides the configured register type for this message only.
47
+ Allowed values: <code>holding</code>, <code>input</code>, <code>coil</code>,
48
+ <code>discrete</code>.</dd>
49
+
50
+ <dt class="optional">registerAddress <span class="property-type">number</span></dt>
51
+ <dd>Overrides the configured register address (0-based) for this message only.</dd>
52
+
53
+ <dt class="optional">registerFormat <span class="property-type">string</span></dt>
54
+ <dd>Overrides the configured data format for this message only.
55
+ Allowed values: <code>int16</code>, <code>uint16</code>, <code>int32</code>,
56
+ <code>uint32</code>, <code>float32</code>.</dd>
57
+ </dl>
58
+
59
+ <h3>Outputs</h3>
60
+ <ol class="node-ports">
61
+ <li>Read result message
62
+ <dl class="message-properties">
63
+ <dt>payload <span class="property-type">object</span></dt>
64
+ <dd>Structured read result object with:
65
+ <ul>
66
+ <li><code>registerType</code> — register type read from.</li>
67
+ <li><code>registerAddress</code> — address read from.</li>
68
+ <li><code>registerFormat</code> — format used for decoding.</li>
69
+ <li><code>value</code> — decoded value.</li>
70
+ <li><code>wordsRead</code> — present for word/register reads.</li>
71
+ <li><code>bitsRead</code> — present for coil/discrete reads.</li>
72
+ </ul>
73
+ </dd>
74
+
75
+ <dt>modbusRegistersRead <span class="property-type">object</span></dt>
76
+ <dd>Read metadata mirror for easier downstream inspection.</dd>
77
+ </dl>
78
+ </li>
79
+ </ol>
80
+
81
+ <h3>Configuration</h3>
82
+ <dl class="message-properties">
83
+ <dt>Register Map <span class="property-type">modbus-registers-config</span></dt>
84
+ <dd>The shared register map this node reads from. Required.</dd>
85
+
86
+ <dt>Register Type <span class="property-type">select</span></dt>
87
+ <dd>The type of register to read:
88
+ <ul>
89
+ <li><code>holding</code> — holding registers.</li>
90
+ <li><code>input</code> — input registers.</li>
91
+ <li><code>coil</code> — single-bit coil registers.</li>
92
+ <li><code>discrete</code> — discrete input bits.</li>
93
+ </ul>
94
+ </dd>
95
+
96
+ <dt>Register Address <span class="property-type">number</span></dt>
97
+ <dd>Zero-based register address to read from. For 32-bit formats, both
98
+ <code>address</code> and <code>address+1</code> are read.</dd>
99
+
100
+ <dt>Format <span class="property-type">select</span></dt>
101
+ <dd>Decoding format for register reads:
102
+ <ul>
103
+ <li><code>int16</code> — signed 16-bit integer.</li>
104
+ <li><code>uint16</code> — unsigned 16-bit integer.</li>
105
+ <li><code>int32</code> — signed 32-bit integer (2 registers).</li>
106
+ <li><code>uint32</code> — unsigned 32-bit integer (2 registers).</li>
107
+ <li><code>float32</code> — IEEE 754 single-precision float (2 registers).</li>
108
+ </ul>
109
+ 32-bit word/byte ordering follows the linked register map's
110
+ <em>32-bit Word Order</em> setting.</dd>
111
+ </dl>
112
+ </script>
113
+
114
+ <script type="text/javascript">
115
+ RED.nodes.registerType('modbus-registers-read', {
116
+ category: 'modbus',
117
+ color: '#E9967A',
118
+ defaults: {
119
+ name: { value: '' },
120
+ registerMap: { value: '', type: 'modbus-registers-config', required: true },
121
+ registerType: { value: 'holding', required: true },
122
+ registerAddress: { value: 0, required: true, validate: RED.validators.number() },
123
+ registerFormat: { value: 'uint16', required: true }
124
+ },
125
+ inputs: 1,
126
+ outputs: 1,
127
+ icon: 'file.svg',
128
+ label: function () {
129
+ return this.name || 'Modbus Registers Read'
130
+ }
131
+ })
132
+ </script>
133
+
@@ -0,0 +1,204 @@
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 ModbusRegistersReadNode (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
+
40
+ if (!node.registerMap) {
41
+ node.status({ fill: 'red', shape: 'ring', text: 'no register map' })
42
+ return
43
+ }
44
+
45
+ node.status({ fill: 'grey', shape: 'ring', text: 'ready' })
46
+
47
+ node.on('input', function (msg, send, done) {
48
+ const registerType = normalizeRegisterType(msg.registerType || node.registerType)
49
+ const registerAddress = normalizeAddress(msg.registerAddress, node.registerAddress)
50
+ const registerFormat = normalizeRegisterFormat(msg.registerFormat || node.registerFormat)
51
+
52
+ if (!registerType) {
53
+ node.status({ fill: 'red', shape: 'ring', text: 'invalid register type' })
54
+ done(new Error('modbus-registers-read: invalid registerType'))
55
+ return
56
+ }
57
+
58
+ if (!registerFormat) {
59
+ node.status({ fill: 'red', shape: 'ring', text: 'invalid format' })
60
+ done(new Error('modbus-registers-read: invalid registerFormat'))
61
+ return
62
+ }
63
+
64
+ const readResult = readValue(node.registerMap, registerType, registerAddress, registerFormat)
65
+ if (!readResult.ok) {
66
+ node.status({ fill: 'red', shape: 'ring', text: 'read failed' })
67
+ done(new Error(`modbus-registers-read: ${readResult.error}`))
68
+ return
69
+ }
70
+
71
+ msg.payload = {
72
+ registerType,
73
+ registerAddress,
74
+ registerFormat,
75
+ value: readResult.value
76
+ }
77
+
78
+ if (readResult.wordsRead) {
79
+ msg.payload.wordsRead = readResult.wordsRead
80
+ }
81
+ if (readResult.bitsRead) {
82
+ msg.payload.bitsRead = readResult.bitsRead
83
+ }
84
+
85
+ msg.modbusRegistersRead = {
86
+ registerType,
87
+ registerAddress,
88
+ registerFormat,
89
+ value: readResult.value,
90
+ wordsRead: readResult.wordsRead || 0,
91
+ bitsRead: readResult.bitsRead || 0
92
+ }
93
+
94
+ node.status({ fill: 'green', shape: 'dot', text: `read ${formatStatusValue(readResult.value)}` })
95
+ send(msg)
96
+ done()
97
+ })
98
+ }
99
+
100
+ function normalizeAddress (value, fallback) {
101
+ const parsed = Number(value)
102
+ if (!Number.isFinite(parsed) || parsed < 0) {
103
+ return fallback !== undefined ? fallback : 0
104
+ }
105
+ return Math.floor(parsed)
106
+ }
107
+
108
+ function normalizeRegisterType (value) {
109
+ if (value === 'holding' || value === 'input' || value === 'coil' || value === 'discrete') {
110
+ return value
111
+ }
112
+ return null
113
+ }
114
+
115
+ function normalizeRegisterFormat (value) {
116
+ if (value === 'int16' || value === 'uint16' || value === 'int32' || value === 'uint32' || value === 'float32') {
117
+ return value
118
+ }
119
+ return null
120
+ }
121
+
122
+ function readValue (registerMap, registerType, registerAddress, registerFormat) {
123
+ if (registerType === 'coil' || registerType === 'discrete') {
124
+ const bits = registerMap.readBits(registerType, registerAddress, 1)
125
+ if (!bits) {
126
+ return { ok: false, error: 'address out of range' }
127
+ }
128
+
129
+ return {
130
+ ok: true,
131
+ value: Boolean(bits[0]),
132
+ bitsRead: 1
133
+ }
134
+ }
135
+
136
+ const wordsNeeded = (registerFormat === 'int32' || registerFormat === 'uint32' || registerFormat === 'float32') ? 2 : 1
137
+ const words = registerMap.readWords(registerType, registerAddress, wordsNeeded)
138
+ if (!words) {
139
+ return { ok: false, error: 'address out of range' }
140
+ }
141
+
142
+ const value = decodeWords(words, registerFormat, registerMap.wordOrderMode)
143
+ if (value === null) {
144
+ return { ok: false, error: `unsupported format ${registerFormat}` }
145
+ }
146
+
147
+ return {
148
+ ok: true,
149
+ value,
150
+ wordsRead: wordsNeeded
151
+ }
152
+ }
153
+
154
+ function decodeWords (words, format, wordOrderMode) {
155
+ const w0 = Number(words[0]) & 0xffff
156
+
157
+ if (format === 'uint16') return w0
158
+ if (format === 'int16') {
159
+ return w0 >= 0x8000 ? w0 - 0x10000 : w0
160
+ }
161
+
162
+ if (format === 'int32' || format === 'uint32' || format === 'float32') {
163
+ if (!Array.isArray(words) || words.length < 2) return null
164
+ const w1 = Number(words[1]) & 0xffff
165
+ const bytes = decodeWordPairToABCDBytes(w0, w1, wordOrderMode)
166
+ const buf = Buffer.from(bytes)
167
+
168
+ if (format === 'uint32') return buf.readUInt32BE(0)
169
+ if (format === 'int32') return buf.readInt32BE(0)
170
+ return buf.readFloatBE(0)
171
+ }
172
+
173
+ return null
174
+ }
175
+
176
+ function decodeWordPairToABCDBytes (firstWord, secondWord, mode) {
177
+ const firstHi = (firstWord >> 8) & 0xff
178
+ const firstLo = firstWord & 0xff
179
+ const secondHi = (secondWord >> 8) & 0xff
180
+ const secondLo = secondWord & 0xff
181
+
182
+ const normalizedMode = normalizeWordOrderMode(mode)
183
+ if (normalizedMode === 'ABCD') {
184
+ return [firstHi, firstLo, secondHi, secondLo]
185
+ }
186
+ if (normalizedMode === 'CDAB') {
187
+ return [secondHi, secondLo, firstHi, firstLo]
188
+ }
189
+ if (normalizedMode === 'BADC') {
190
+ return [firstLo, firstHi, secondLo, secondHi]
191
+ }
192
+ // DCBA
193
+ return [secondLo, secondHi, firstLo, firstHi]
194
+ }
195
+
196
+ function normalizeWordOrderMode (value) {
197
+ const mode = String(value || '').trim().toUpperCase()
198
+ if (mode === 'ABCD' || mode === 'CDAB' || mode === 'BADC' || mode === 'DCBA') return mode
199
+ return 'ABCD'
200
+ }
201
+
202
+ RED.nodes.registerType('modbus-registers-read', ModbusRegistersReadNode)
203
+ }
204
+
@@ -0,0 +1,116 @@
1
+ <script type="text/html" data-template-name="modbus-registers-respond">
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
+ </script>
13
+
14
+ <script type="text/html" data-help-name="modbus-registers-respond">
15
+ <p>Handles an incoming Modbus request emitted by a <code>modbus-dynamic-server</code> node by
16
+ automatically reading from or writing to a shared <code>modbus-registers-config</code> register
17
+ map, then sending the correct Modbus response back to the client — all without manual payload
18
+ construction.</p>
19
+
20
+ <h3>Inputs</h3>
21
+ <dl class="message-properties">
22
+ <dt>_modbus.requestId <span class="property-type">string</span></dt>
23
+ <dd>Required internal request context field emitted by <code>modbus-dynamic-server</code>.</dd>
24
+
25
+ <dt>_modbus.eventName <span class="property-type">string</span></dt>
26
+ <dd>Required internal function-code event name from the incoming request, e.g.
27
+ <code>readHoldingRegisters</code>, <code>writeSingleCoil</code>.</dd>
28
+
29
+ <dt>_modbus.configNodeId <span class="property-type">string</span></dt>
30
+ <dd>Required internal config-node identifier used to complete the pending request.</dd>
31
+
32
+ <dt>payload <span class="property-type">object</span></dt>
33
+ <dd>The full jsmodbus request object as emitted by the <code>modbus-dynamic-server</code>
34
+ node. Must contain <code>address</code>, <code>quantity</code>, and <code>body</code>
35
+ fields.</dd>
36
+ </dl>
37
+
38
+ <h3>Outputs</h3>
39
+ <ol class="node-ports">
40
+ <li>Passthrough message
41
+ <dl class="message-properties">
42
+ <dt>payload <span class="property-type">object</span></dt>
43
+ <dd>The original request message, forwarded unchanged after the response has been
44
+ sent to the Modbus client.</dd>
45
+
46
+ <dt>modbusResponse <span class="property-type">object</span></dt>
47
+ <dd>Details of what this node submitted to <code>respondToRequest</code>, including:
48
+ <ul>
49
+ <li><code>requestId</code>, <code>eventName</code>, <code>address</code>, <code>quantity</code></li>
50
+ <li><code>payloadSubmitted</code> — the exact payload passed to server config</li>
51
+ <li><code>payloadType</code> — payload classification (<code>array</code>, <code>buffer</code>, <code>object</code>, ...)</li>
52
+ <li><code>actualDataSent</code> — normalized values actually encoded for read FC responses</li>
53
+ <li><code>actualDataType</code> — type of <code>actualDataSent</code></li>
54
+ <li><code>ok</code> — whether the response was accepted for the requestId</li>
55
+ <li><code>timestamp</code> — ISO timestamp when this node submitted it</li>
56
+ <li><code>note</code> — present for write FCs to indicate auto-built response behavior</li>
57
+ </ul>
58
+ </dd>
59
+ </dl>
60
+ </li>
61
+ </ol>
62
+
63
+ <h3>Configuration</h3>
64
+ <dl class="message-properties">
65
+ <dt>Register Map <span class="property-type">modbus-registers-config</span></dt>
66
+ <dd>The shared register map to read from (for read requests) or write to (for write requests).
67
+ Required.</dd>
68
+
69
+ </dl>
70
+
71
+ <h3>Details</h3>
72
+ <p>This node handles all eight standard function codes automatically:</p>
73
+ <ul>
74
+ <li><b>FC1 readCoils</b> — reads from the coil register map and returns the values.</li>
75
+ <li><b>FC2 readDiscreteInputs</b> — reads from the discrete input map.</li>
76
+ <li><b>FC3 readHoldingRegisters</b> — reads from the holding register map.</li>
77
+ <li><b>FC4 readInputRegisters</b> — reads from the input register map.</li>
78
+ <li><b>FC5 writeSingleCoil</b> — writes a single bit to the coil map.</li>
79
+ <li><b>FC6 writeSingleRegister</b> — writes a single word to the holding register map.</li>
80
+ <li><b>FC15 writeMultipleCoils</b> — writes multiple bits to the coil map.</li>
81
+ <li><b>FC16 writeMultipleRegisters</b> — writes multiple words to the holding register map.</li>
82
+ </ul>
83
+ <p>If a request addresses a register range that has been set to zero (disabled) in the register
84
+ map config, this node automatically responds with Modbus exception code&nbsp;2
85
+ (Illegal Data Address). An unknown function code returns exception code&nbsp;1 (Illegal
86
+ Function). Neither case raises a flow error.</p>
87
+ <p>Flows must preserve <code>msg._modbus</code>:</p>
88
+ <pre><code>// CORRECT
89
+ msg.payload = newValue;
90
+ return msg;
91
+
92
+ // INCORRECT: request context is lost
93
+ msg = { payload: newValue };
94
+ return msg;</code></pre>
95
+ <p>This node is the recommended response handler when using a static register map. For fully
96
+ custom response logic, use the lower-level <code>modbus-dynamic-server-response</code> node
97
+ instead.</p>
98
+ </script>
99
+
100
+ <script type="text/javascript">
101
+ RED.nodes.registerType('modbus-registers-respond', {
102
+ category: 'modbus',
103
+ color: '#E9967A',
104
+ defaults: {
105
+ name: { value: '' },
106
+ registerMap: { value: '', type: 'modbus-registers-config', required: true }
107
+ },
108
+ inputs: 1,
109
+ outputs: 1,
110
+ icon: 'arrow-out.svg',
111
+ label: function () {
112
+ return this.name || 'Modbus Registers Respond'
113
+ }
114
+ })
115
+ </script>
116
+