node-red-contrib-modbus-tcp-full-avd 1.0.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/modbus-tcp.js ADDED
@@ -0,0 +1,1102 @@
1
+ /**
2
+ * Node-RED MODBUS TCP Node
3
+ * Volledige ondersteuning voor MODBUS TCP communicatie
4
+ */
5
+
6
+ module.exports = function(RED) {
7
+ "use strict";
8
+ const ModbusRTU = require("modbus-serial");
9
+ const net = require("net");
10
+ const EventEmitter = require("events");
11
+
12
+ /**
13
+ * MODBUS Protocol Handler
14
+ */
15
+ class ModbusProtocol {
16
+ constructor() {
17
+ this.transactionId = 0;
18
+ }
19
+
20
+ /**
21
+ * Genereer volgende transaction ID
22
+ */
23
+ getNextTransactionId() {
24
+ this.transactionId = (this.transactionId + 1) % 65536;
25
+ return this.transactionId;
26
+ }
27
+
28
+ /**
29
+ * Bouw MBAP header
30
+ */
31
+ buildMBAP(transactionId, unitId, pduLength) {
32
+ const buffer = Buffer.allocUnsafe(7);
33
+ buffer.writeUInt16BE(transactionId, 0);
34
+ buffer.writeUInt16BE(0, 2); // Protocol ID (0 = MODBUS)
35
+ buffer.writeUInt16BE(pduLength, 4);
36
+ buffer.writeUInt8(unitId, 6);
37
+ return buffer;
38
+ }
39
+
40
+ /**
41
+ * Parse MBAP header
42
+ */
43
+ parseMBAP(buffer) {
44
+ return {
45
+ transactionId: buffer.readUInt16BE(0),
46
+ protocolId: buffer.readUInt16BE(2),
47
+ length: buffer.readUInt16BE(4),
48
+ unitId: buffer.readUInt8(6)
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Bouw PDU voor request
54
+ */
55
+ buildPDU(functionCode, address, quantity, values) {
56
+ let buffer;
57
+
58
+ switch(functionCode) {
59
+ case 1: // Read Coils
60
+ case 2: // Read Discrete Inputs
61
+ case 3: // Read Holding Registers
62
+ case 4: // Read Input Registers
63
+ buffer = Buffer.allocUnsafe(5);
64
+ buffer.writeUInt8(functionCode, 0);
65
+ buffer.writeUInt16BE(address, 1);
66
+ buffer.writeUInt16BE(quantity, 3);
67
+ break;
68
+
69
+ case 5: // Write Single Coil
70
+ buffer = Buffer.allocUnsafe(5);
71
+ buffer.writeUInt8(functionCode, 0);
72
+ buffer.writeUInt16BE(address, 1);
73
+ buffer.writeUInt16BE(values ? 0xFF00 : 0x0000, 3);
74
+ break;
75
+
76
+ case 6: // Write Single Register
77
+ buffer = Buffer.allocUnsafe(5);
78
+ buffer.writeUInt8(functionCode, 0);
79
+ buffer.writeUInt16BE(address, 1);
80
+ buffer.writeUInt16BE(values, 3);
81
+ break;
82
+
83
+ case 15: // Write Multiple Coils
84
+ const coilBytes = Math.ceil(quantity / 8);
85
+ buffer = Buffer.allocUnsafe(6 + coilBytes);
86
+ buffer.writeUInt8(functionCode, 0);
87
+ buffer.writeUInt16BE(address, 1);
88
+ buffer.writeUInt16BE(quantity, 3);
89
+ buffer.writeUInt8(coilBytes, 5);
90
+ // Pack coils into bytes
91
+ for (let i = 0; i < quantity; i++) {
92
+ const byteIndex = 6 + Math.floor(i / 8);
93
+ const bitIndex = i % 8;
94
+ if (values[i]) {
95
+ buffer[byteIndex] |= (1 << bitIndex);
96
+ }
97
+ }
98
+ break;
99
+
100
+ case 16: // Write Multiple Registers
101
+ buffer = Buffer.allocUnsafe(6 + (quantity * 2));
102
+ buffer.writeUInt8(functionCode, 0);
103
+ buffer.writeUInt16BE(address, 1);
104
+ buffer.writeUInt16BE(quantity, 3);
105
+ buffer.writeUInt8(quantity * 2, 5);
106
+ for (let i = 0; i < quantity; i++) {
107
+ buffer.writeUInt16BE(values[i], 6 + (i * 2));
108
+ }
109
+ break;
110
+
111
+ default:
112
+ throw new Error(`Unsupported function code: ${functionCode}`);
113
+ }
114
+
115
+ return buffer;
116
+ }
117
+
118
+ /**
119
+ * Parse PDU response
120
+ */
121
+ parsePDU(buffer) {
122
+ const functionCode = buffer[0];
123
+
124
+ // Check for exception
125
+ if (functionCode & 0x80) {
126
+ return {
127
+ exception: true,
128
+ functionCode: functionCode & 0x7F,
129
+ exceptionCode: buffer[1]
130
+ };
131
+ }
132
+
133
+ let data = [];
134
+
135
+ switch(functionCode) {
136
+ case 1: // Read Coils
137
+ case 2: // Read Discrete Inputs
138
+ const byteCount = buffer[1];
139
+ for (let i = 0; i < byteCount; i++) {
140
+ const byte = buffer[2 + i];
141
+ for (let j = 0; j < 8; j++) {
142
+ data.push((byte & (1 << j)) !== 0);
143
+ }
144
+ }
145
+ break;
146
+
147
+ case 3: // Read Holding Registers
148
+ case 4: // Read Input Registers
149
+ const registerCount = buffer[1] / 2;
150
+ for (let i = 0; i < registerCount; i++) {
151
+ data.push(buffer.readUInt16BE(2 + (i * 2)));
152
+ }
153
+ break;
154
+
155
+ case 5: // Write Single Coil
156
+ case 6: // Write Single Register
157
+ if (buffer.length < 5) {
158
+ throw new Error(`Invalid response length for function ${functionCode}: expected 5 bytes, got ${buffer.length}`);
159
+ }
160
+ data = {
161
+ address: buffer.readUInt16BE(1),
162
+ value: functionCode === 5 ?
163
+ (buffer.readUInt16BE(3) === 0xFF00) :
164
+ buffer.readUInt16BE(3)
165
+ };
166
+ break;
167
+
168
+ case 15: // Write Multiple Coils
169
+ case 16: // Write Multiple Registers
170
+ data = {
171
+ address: buffer.readUInt16BE(1),
172
+ quantity: buffer.readUInt16BE(3)
173
+ };
174
+ break;
175
+ }
176
+
177
+ return {
178
+ exception: false,
179
+ functionCode: functionCode,
180
+ data: data
181
+ };
182
+ }
183
+ }
184
+
185
+ /**
186
+ * MODBUS TCP Client
187
+ */
188
+ class ModbusTCPClient {
189
+ constructor(config, node) {
190
+ this.config = config;
191
+ this.node = node;
192
+ this.protocol = new ModbusProtocol();
193
+ this.client = null;
194
+ this.connected = false;
195
+ this.pendingRequests = new Map();
196
+ this.reconnectTimer = null;
197
+ this.responseBuffer = Buffer.alloc(0);
198
+ }
199
+
200
+ connect() {
201
+ if (this.client && this.connected) {
202
+ return Promise.resolve();
203
+ }
204
+
205
+ return new Promise((resolve, reject) => {
206
+ this.client = new net.Socket();
207
+
208
+ this.client.on('connect', () => {
209
+ this.connected = true;
210
+ this.node.status({ fill: "green", shape: "dot", text: "connected" });
211
+ this.node.log("MODBUS TCP Client connected");
212
+ resolve();
213
+ });
214
+
215
+ this.client.on('data', (data) => {
216
+ this.handleResponse(data);
217
+ });
218
+
219
+ this.client.on('error', (err) => {
220
+ this.node.error(`MODBUS TCP Client error: ${err.message}`);
221
+ this.node.status({ fill: "red", shape: "ring", text: "error" });
222
+ this.connected = false;
223
+ });
224
+
225
+ this.client.on('close', () => {
226
+ this.connected = false;
227
+ this.node.status({ fill: "yellow", shape: "ring", text: "disconnected" });
228
+ this.node.log("MODBUS TCP Client disconnected");
229
+ this.scheduleReconnect();
230
+ });
231
+
232
+ this.client.setTimeout(this.config.connectionTimeout);
233
+ this.client.connect(this.config.port, this.config.host);
234
+ });
235
+ }
236
+
237
+ scheduleReconnect() {
238
+ if (this.reconnectTimer) {
239
+ clearTimeout(this.reconnectTimer);
240
+ }
241
+ this.reconnectTimer = setTimeout(() => {
242
+ this.node.log("Attempting to reconnect...");
243
+ this.connect().catch(err => {
244
+ this.node.error(`Reconnect failed: ${err.message}`);
245
+ });
246
+ }, this.config.reconnectInterval);
247
+ }
248
+
249
+ handleResponse(data) {
250
+ // Append new data to buffer
251
+ this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
252
+
253
+ // Process complete responses from buffer
254
+ while (this.responseBuffer.length >= 7) {
255
+ const mbap = this.protocol.parseMBAP(this.responseBuffer);
256
+ // MBAP length includes PDU length + 1 (unitId byte)
257
+ const expectedLength = 7 + mbap.length - 1;
258
+
259
+ if (this.responseBuffer.length < expectedLength) {
260
+ // Wait for more data
261
+ return;
262
+ }
263
+
264
+ const pdu = this.responseBuffer.slice(7, expectedLength);
265
+ const response = this.protocol.parsePDU(pdu);
266
+
267
+ const requestId = mbap.transactionId;
268
+ const pendingRequest = this.pendingRequests.get(requestId);
269
+
270
+ if (pendingRequest) {
271
+ clearTimeout(pendingRequest.timeout);
272
+ this.pendingRequests.delete(requestId);
273
+
274
+ if (response.exception) {
275
+ pendingRequest.reject(new Error(`MODBUS Exception ${response.exceptionCode}`));
276
+ } else {
277
+ pendingRequest.resolve(response);
278
+ }
279
+ }
280
+
281
+ // Remove processed response from buffer
282
+ this.responseBuffer = this.responseBuffer.slice(expectedLength);
283
+ }
284
+ }
285
+
286
+ async sendRequest(functionCode, address, quantity, values, unitId) {
287
+ if (!this.connected) {
288
+ await this.connect();
289
+ }
290
+
291
+ const transactionId = this.protocol.getNextTransactionId();
292
+ const pdu = this.protocol.buildPDU(functionCode, address, quantity, values);
293
+ const mbap = this.protocol.buildMBAP(transactionId, unitId || this.config.unitId, pdu.length + 1);
294
+ const request = Buffer.concat([mbap, pdu]);
295
+
296
+ return new Promise((resolve, reject) => {
297
+ const timeout = setTimeout(() => {
298
+ this.pendingRequests.delete(transactionId);
299
+ reject(new Error("Request timeout"));
300
+ }, this.config.requestTimeout);
301
+
302
+ this.pendingRequests.set(transactionId, {
303
+ resolve: (response) => {
304
+ clearTimeout(timeout);
305
+ resolve(response);
306
+ },
307
+ reject: (error) => {
308
+ clearTimeout(timeout);
309
+ reject(error);
310
+ },
311
+ timeout: timeout
312
+ });
313
+
314
+ this.client.write(request);
315
+ });
316
+ }
317
+
318
+ async requestWithRetry(functionCode, address, quantity, values, unitId) {
319
+ let lastError;
320
+ for (let i = 0; i <= this.config.retryCount; i++) {
321
+ try {
322
+ return await this.sendRequest(functionCode, address, quantity, values, unitId);
323
+ } catch (err) {
324
+ lastError = err;
325
+ if (i < this.config.retryCount) {
326
+ await new Promise(resolve => setTimeout(resolve, this.config.retryInterval));
327
+ }
328
+ }
329
+ }
330
+ throw lastError;
331
+ }
332
+
333
+ disconnect() {
334
+ if (this.reconnectTimer) {
335
+ clearTimeout(this.reconnectTimer);
336
+ }
337
+ if (this.client) {
338
+ this.client.destroy();
339
+ this.client = null;
340
+ }
341
+ this.connected = false;
342
+ this.responseBuffer = Buffer.alloc(0);
343
+ }
344
+ }
345
+
346
+ /**
347
+ * MODBUS TCP Server
348
+ */
349
+ class ModbusTCPServer {
350
+ constructor(config, node) {
351
+ this.config = config;
352
+ this.node = node;
353
+ this.protocol = new ModbusProtocol();
354
+ this.server = null;
355
+ this.clients = new Map();
356
+ this.dataModel = {
357
+ coils: new Array(65536).fill(false),
358
+ discreteInputs: new Array(65536).fill(false),
359
+ holdingRegisters: new Array(65536).fill(0),
360
+ inputRegisters: new Array(65536).fill(0)
361
+ };
362
+ }
363
+
364
+ start() {
365
+ this.server = net.createServer((socket) => {
366
+ const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
367
+ this.node.log(`New client connected: ${clientId}`);
368
+
369
+ if (this.clients.size >= this.config.maxConnections) {
370
+ this.node.warn(`Max connections reached, rejecting ${clientId}`);
371
+ socket.destroy();
372
+ return;
373
+ }
374
+
375
+ this.clients.set(clientId, socket);
376
+ this.node.status({ fill: "green", shape: "dot", text: `${this.clients.size} clients` });
377
+
378
+ let buffer = Buffer.alloc(0);
379
+
380
+ socket.on('data', (data) => {
381
+ buffer = Buffer.concat([buffer, data]);
382
+ this.processRequest(socket, buffer, clientId);
383
+ });
384
+
385
+ socket.on('error', (err) => {
386
+ this.node.error(`Client ${clientId} error: ${err.message}`);
387
+ });
388
+
389
+ socket.on('close', () => {
390
+ this.clients.delete(clientId);
391
+ this.node.log(`Client disconnected: ${clientId}`);
392
+ this.node.status({ fill: "green", shape: "dot", text: `${this.clients.size} clients` });
393
+ });
394
+ });
395
+
396
+ this.server.listen(this.config.port, () => {
397
+ this.node.log(`MODBUS TCP Server listening on port ${this.config.port}`);
398
+ this.node.status({ fill: "green", shape: "dot", text: `listening on ${this.config.port}` });
399
+ });
400
+
401
+ this.server.on('error', (err) => {
402
+ this.node.error(`MODBUS TCP Server error: ${err.message}`);
403
+ this.node.status({ fill: "red", shape: "ring", text: "error" });
404
+ });
405
+ }
406
+
407
+ processRequest(socket, buffer, clientId) {
408
+ while (buffer.length >= 7) {
409
+ const mbap = this.protocol.parseMBAP(buffer);
410
+ const expectedLength = 7 + mbap.length - 1; // -1 because unitId is included in length
411
+
412
+ if (buffer.length < expectedLength) {
413
+ return; // Wait for more data
414
+ }
415
+
416
+ const pdu = buffer.slice(7, expectedLength);
417
+ buffer = buffer.slice(expectedLength);
418
+
419
+ try {
420
+ const response = this.handleModbusRequest(pdu, mbap.unitId);
421
+ const responseMBAP = this.protocol.buildMBAP(mbap.transactionId, mbap.unitId, response.length + 1);
422
+ socket.write(Buffer.concat([responseMBAP, response]));
423
+ } catch (err) {
424
+ this.node.error(`Error processing request: ${err.message}`);
425
+ }
426
+ }
427
+ }
428
+
429
+ handleModbusRequest(pdu, unitId) {
430
+ if (unitId !== this.config.unitId && unitId !== 0) {
431
+ return this.buildExceptionResponse(pdu[0], 0x0A); // Gateway Target Device Failed
432
+ }
433
+
434
+ const functionCode = pdu[0];
435
+ let response;
436
+
437
+ try {
438
+ switch(functionCode) {
439
+ case 1: // Read Coils
440
+ response = this.readCoils(pdu);
441
+ break;
442
+ case 2: // Read Discrete Inputs
443
+ response = this.readDiscreteInputs(pdu);
444
+ break;
445
+ case 3: // Read Holding Registers
446
+ response = this.readHoldingRegisters(pdu);
447
+ break;
448
+ case 4: // Read Input Registers
449
+ response = this.readInputRegisters(pdu);
450
+ break;
451
+ case 5: // Write Single Coil
452
+ response = this.writeSingleCoil(pdu);
453
+ break;
454
+ case 6: // Write Single Register
455
+ response = this.writeSingleRegister(pdu);
456
+ break;
457
+ case 15: // Write Multiple Coils
458
+ response = this.writeMultipleCoils(pdu);
459
+ break;
460
+ case 16: // Write Multiple Registers
461
+ response = this.writeMultipleRegisters(pdu);
462
+ break;
463
+ default:
464
+ return this.buildExceptionResponse(functionCode, 0x01); // Illegal Function
465
+ }
466
+ } catch (err) {
467
+ if (err.code === 'ILLEGAL_ADDRESS') {
468
+ return this.buildExceptionResponse(functionCode, 0x02); // Illegal Data Address
469
+ } else if (err.code === 'ILLEGAL_VALUE') {
470
+ return this.buildExceptionResponse(functionCode, 0x03); // Illegal Data Value
471
+ } else {
472
+ return this.buildExceptionResponse(functionCode, 0x04); // Server Device Failure
473
+ }
474
+ }
475
+
476
+ return response;
477
+ }
478
+
479
+ readCoils(pdu) {
480
+ const address = pdu.readUInt16BE(1);
481
+ const quantity = pdu.readUInt16BE(3);
482
+
483
+ if (address + quantity > 65536) {
484
+ throw new Error('ILLEGAL_ADDRESS');
485
+ }
486
+
487
+ const byteCount = Math.ceil(quantity / 8);
488
+ const response = Buffer.allocUnsafe(2 + byteCount);
489
+ response.writeUInt8(1, 0);
490
+ response.writeUInt8(byteCount, 1);
491
+
492
+ for (let i = 0; i < quantity; i++) {
493
+ const byteIndex = 2 + Math.floor(i / 8);
494
+ const bitIndex = i % 8;
495
+ if (this.dataModel.coils[address + i]) {
496
+ response[byteIndex] |= (1 << bitIndex);
497
+ }
498
+ }
499
+
500
+ return response;
501
+ }
502
+
503
+ readDiscreteInputs(pdu) {
504
+ const address = pdu.readUInt16BE(1);
505
+ const quantity = pdu.readUInt16BE(3);
506
+
507
+ if (address + quantity > 65536) {
508
+ throw new Error('ILLEGAL_ADDRESS');
509
+ }
510
+
511
+ const byteCount = Math.ceil(quantity / 8);
512
+ const response = Buffer.allocUnsafe(2 + byteCount);
513
+ response.writeUInt8(2, 0);
514
+ response.writeUInt8(byteCount, 1);
515
+
516
+ for (let i = 0; i < quantity; i++) {
517
+ const byteIndex = 2 + Math.floor(i / 8);
518
+ const bitIndex = i % 8;
519
+ if (this.dataModel.discreteInputs[address + i]) {
520
+ response[byteIndex] |= (1 << bitIndex);
521
+ }
522
+ }
523
+
524
+ return response;
525
+ }
526
+
527
+ readHoldingRegisters(pdu) {
528
+ const address = pdu.readUInt16BE(1);
529
+ const quantity = pdu.readUInt16BE(3);
530
+
531
+ if (address + quantity > 65536) {
532
+ throw new Error('ILLEGAL_ADDRESS');
533
+ }
534
+
535
+ const response = Buffer.allocUnsafe(2 + (quantity * 2));
536
+ response.writeUInt8(3, 0);
537
+ response.writeUInt8(quantity * 2, 1);
538
+
539
+ for (let i = 0; i < quantity; i++) {
540
+ response.writeUInt16BE(this.dataModel.holdingRegisters[address + i], 2 + (i * 2));
541
+ }
542
+
543
+ return response;
544
+ }
545
+
546
+ readInputRegisters(pdu) {
547
+ const address = pdu.readUInt16BE(1);
548
+ const quantity = pdu.readUInt16BE(3);
549
+
550
+ if (address + quantity > 65536) {
551
+ throw new Error('ILLEGAL_ADDRESS');
552
+ }
553
+
554
+ const response = Buffer.allocUnsafe(2 + (quantity * 2));
555
+ response.writeUInt8(4, 0);
556
+ response.writeUInt8(quantity * 2, 1);
557
+
558
+ for (let i = 0; i < quantity; i++) {
559
+ response.writeUInt16BE(this.dataModel.inputRegisters[address + i], 2 + (i * 2));
560
+ }
561
+
562
+ return response;
563
+ }
564
+
565
+ writeSingleCoil(pdu) {
566
+ const address = pdu.readUInt16BE(1);
567
+ const value = pdu.readUInt16BE(3);
568
+
569
+ if (address > 65535) {
570
+ throw new Error('ILLEGAL_ADDRESS');
571
+ }
572
+
573
+ if (value !== 0x0000 && value !== 0xFF00) {
574
+ throw new Error('ILLEGAL_VALUE');
575
+ }
576
+
577
+ this.dataModel.coils[address] = (value === 0xFF00);
578
+
579
+ // Echo response
580
+ return Buffer.from(pdu);
581
+ }
582
+
583
+ writeSingleRegister(pdu) {
584
+ const address = pdu.readUInt16BE(1);
585
+ const value = pdu.readUInt16BE(3);
586
+
587
+ if (address > 65535) {
588
+ throw new Error('ILLEGAL_ADDRESS');
589
+ }
590
+
591
+ this.dataModel.holdingRegisters[address] = value;
592
+
593
+ // Echo response
594
+ return Buffer.from(pdu);
595
+ }
596
+
597
+ writeMultipleCoils(pdu) {
598
+ const address = pdu.readUInt16BE(1);
599
+ const quantity = pdu.readUInt16BE(3);
600
+ const byteCount = pdu[5];
601
+
602
+ if (address + quantity > 65536) {
603
+ throw new Error('ILLEGAL_ADDRESS');
604
+ }
605
+
606
+ for (let i = 0; i < quantity; i++) {
607
+ const byteIndex = 6 + Math.floor(i / 8);
608
+ const bitIndex = i % 8;
609
+ this.dataModel.coils[address + i] = (pdu[byteIndex] & (1 << bitIndex)) !== 0;
610
+ }
611
+
612
+ // Echo response
613
+ const response = Buffer.allocUnsafe(5);
614
+ response.writeUInt8(15, 0);
615
+ response.writeUInt16BE(address, 1);
616
+ response.writeUInt16BE(quantity, 3);
617
+ return response;
618
+ }
619
+
620
+ writeMultipleRegisters(pdu) {
621
+ const address = pdu.readUInt16BE(1);
622
+ const quantity = pdu.readUInt16BE(3);
623
+
624
+ if (address + quantity > 65536) {
625
+ throw new Error('ILLEGAL_ADDRESS');
626
+ }
627
+
628
+ for (let i = 0; i < quantity; i++) {
629
+ this.dataModel.holdingRegisters[address + i] = pdu.readUInt16BE(6 + (i * 2));
630
+ }
631
+
632
+ // Echo response
633
+ const response = Buffer.allocUnsafe(5);
634
+ response.writeUInt8(16, 0);
635
+ response.writeUInt16BE(address, 1);
636
+ response.writeUInt16BE(quantity, 3);
637
+ return response;
638
+ }
639
+
640
+ buildExceptionResponse(functionCode, exceptionCode) {
641
+ const response = Buffer.allocUnsafe(2);
642
+ response.writeUInt8(functionCode | 0x80, 0);
643
+ response.writeUInt8(exceptionCode, 1);
644
+ return response;
645
+ }
646
+
647
+ updateData(type, address, value, values) {
648
+ if (values && Array.isArray(values)) {
649
+ for (let i = 0; i < values.length; i++) {
650
+ this.updateData(type, address + i, values[i]);
651
+ }
652
+ } else {
653
+ switch(type) {
654
+ case 'coils':
655
+ this.dataModel.coils[address] = value;
656
+ break;
657
+ case 'discreteInputs':
658
+ this.dataModel.discreteInputs[address] = value;
659
+ break;
660
+ case 'holdingRegisters':
661
+ this.dataModel.holdingRegisters[address] = value;
662
+ break;
663
+ case 'inputRegisters':
664
+ this.dataModel.inputRegisters[address] = value;
665
+ break;
666
+ }
667
+ }
668
+ }
669
+
670
+ generateTestData(pattern, startAddress, quantity, dataType) {
671
+ startAddress = startAddress || 0;
672
+ quantity = quantity || 100;
673
+ dataType = dataType || 'holdingRegisters';
674
+ pattern = pattern || 'incrementing';
675
+
676
+ const dataArray = this.dataModel[dataType];
677
+ if (!dataArray) {
678
+ throw new Error(`Invalid data type: ${dataType}`);
679
+ }
680
+
681
+ let values = [];
682
+
683
+ switch(pattern) {
684
+ case 'incrementing':
685
+ for (let i = 0; i < quantity; i++) {
686
+ values.push((startAddress + i) % 65536);
687
+ }
688
+ break;
689
+
690
+ case 'random':
691
+ for (let i = 0; i < quantity; i++) {
692
+ values.push(Math.floor(Math.random() * 65536));
693
+ }
694
+ break;
695
+
696
+ case 'sine':
697
+ for (let i = 0; i < quantity; i++) {
698
+ const sineValue = Math.sin((i / quantity) * Math.PI * 2);
699
+ values.push(Math.floor((sineValue + 1) * 32767.5));
700
+ }
701
+ break;
702
+
703
+ case 'square':
704
+ for (let i = 0; i < quantity; i++) {
705
+ values.push((i % 2 === 0) ? 0 : 65535);
706
+ }
707
+ break;
708
+
709
+ case 'alternating':
710
+ for (let i = 0; i < quantity; i++) {
711
+ values.push(i % 2 === 0 ? 0 : 1);
712
+ }
713
+ break;
714
+
715
+ default:
716
+ // Default to incrementing
717
+ for (let i = 0; i < quantity; i++) {
718
+ values.push((startAddress + i) % 65536);
719
+ }
720
+ }
721
+
722
+ // Update the data model
723
+ if (dataType === 'coils' || dataType === 'discreteInputs') {
724
+ for (let i = 0; i < quantity; i++) {
725
+ dataArray[startAddress + i] = values[i] !== 0;
726
+ }
727
+ } else {
728
+ for (let i = 0; i < quantity; i++) {
729
+ dataArray[startAddress + i] = values[i];
730
+ }
731
+ }
732
+
733
+ this.node.log(`Generated ${quantity} test data values for ${dataType} starting at address ${startAddress} with pattern ${pattern}`);
734
+
735
+ return {
736
+ pattern: pattern,
737
+ dataType: dataType,
738
+ startAddress: startAddress,
739
+ quantity: quantity,
740
+ values: values.slice(0, 10) // Return first 10 as sample
741
+ };
742
+ }
743
+
744
+ stop() {
745
+ if (this.server) {
746
+ this.server.close();
747
+ this.server = null;
748
+ }
749
+ this.clients.forEach((socket) => socket.destroy());
750
+ this.clients.clear();
751
+ }
752
+ }
753
+
754
+ /**
755
+ * Helper function to get data summary
756
+ */
757
+ function getDataSummary(dataArray, type) {
758
+ const nonZeroIndices = [];
759
+ const sampleData = {};
760
+
761
+ // Find first 10 non-zero/non-false values
762
+ for (let i = 0; i < Math.min(dataArray.length, 100); i++) {
763
+ const value = dataArray[i];
764
+ if ((type === "coils" || type === "discreteInputs") ? value : value !== 0) {
765
+ nonZeroIndices.push(i);
766
+ if (nonZeroIndices.length <= 10) {
767
+ sampleData[i] = value;
768
+ }
769
+ }
770
+ }
771
+
772
+ return {
773
+ type: type,
774
+ totalRegisters: dataArray.length,
775
+ nonZeroCount: nonZeroIndices.length,
776
+ sampleData: sampleData,
777
+ firstNonZeroIndex: nonZeroIndices[0] !== undefined ? nonZeroIndices[0] : null,
778
+ lastNonZeroIndex: nonZeroIndices.length > 0 ? nonZeroIndices[nonZeroIndices.length - 1] : null
779
+ };
780
+ }
781
+
782
+ /**
783
+ * Helper function to create data map with addresses
784
+ */
785
+ function createDataMap(dataArray, startAddress) {
786
+ const map = {};
787
+ if (Array.isArray(dataArray)) {
788
+ dataArray.forEach((value, index) => {
789
+ map[startAddress + index] = value;
790
+ });
791
+ }
792
+ return map;
793
+ }
794
+
795
+ /**
796
+ * Node-RED Node Definition
797
+ */
798
+ function ModbusTCPNode(config) {
799
+ RED.nodes.createNode(this, config);
800
+
801
+ this.config = {
802
+ tcpMode: config.tcpMode || "client",
803
+ host: config.host || "127.0.0.1",
804
+ port: config.port || 502,
805
+ reconnectInterval: config.reconnectInterval || 5000,
806
+ connectionTimeout: config.connectionTimeout || 10000,
807
+ maxConnections: config.maxConnections || 10,
808
+ modbusMode: config.modbusMode || "client",
809
+ unitId: config.unitId || 1,
810
+ byteOrder: config.byteOrder || "bigEndian",
811
+ addressOffset: config.addressOffset || 0,
812
+ requestTimeout: config.requestTimeout || 5000,
813
+ retryCount: config.retryCount || 3,
814
+ retryInterval: config.retryInterval || 1000,
815
+ showData: config.showData || false,
816
+ debugMode: config.debugMode || false,
817
+ generateTestData: config.generateTestData || false,
818
+ testDataPattern: config.testDataPattern || "incrementing"
819
+ };
820
+
821
+ this.client = null;
822
+ this.server = null;
823
+
824
+ // Initialize based on mode
825
+ if (this.config.tcpMode === "client" && this.config.modbusMode === "client") {
826
+ this.client = new ModbusTCPClient(this.config, this);
827
+ this.client.connect().catch(err => {
828
+ this.error(`Failed to connect: ${err.message}`);
829
+ });
830
+ } else if (this.config.tcpMode === "server" && this.config.modbusMode === "server") {
831
+ this.server = new ModbusTCPServer(this.config, this);
832
+ this.server.start();
833
+
834
+ // Generate test data if enabled
835
+ if (this.config.generateTestData) {
836
+ setTimeout(() => {
837
+ this.server.generateTestData(
838
+ this.config.testDataPattern,
839
+ 0,
840
+ 100,
841
+ 'holdingRegisters'
842
+ );
843
+ this.server.generateTestData(
844
+ this.config.testDataPattern,
845
+ 0,
846
+ 100,
847
+ 'inputRegisters'
848
+ );
849
+ this.log("Test data generated automatically");
850
+ }, 1000);
851
+ }
852
+ } else {
853
+ this.error(`Unsupported mode combination: TCP ${this.config.tcpMode} + MODBUS ${this.config.modbusMode}`);
854
+ this.status({ fill: "red", shape: "ring", text: "invalid mode" });
855
+ return;
856
+ }
857
+
858
+ // Handle incoming messages
859
+ this.on('input', async (msg) => {
860
+ try {
861
+ // Check for special actions
862
+ const action = msg.action || msg.payload?.action;
863
+
864
+ // Client mode: read and show data
865
+ if (action === "readData" && this.config.modbusMode === "client" && this.client) {
866
+ const readType = msg.readType || msg.payload?.readType || "holdingRegisters";
867
+ const startAddress = (msg.address !== undefined ? msg.address : msg.payload?.address || 0) + this.config.addressOffset;
868
+ const quantity = msg.quantity || msg.payload?.quantity || 10;
869
+
870
+ let functionCode;
871
+ switch(readType) {
872
+ case "coils":
873
+ functionCode = 1;
874
+ break;
875
+ case "discreteInputs":
876
+ functionCode = 2;
877
+ break;
878
+ case "holdingRegisters":
879
+ functionCode = 3;
880
+ break;
881
+ case "inputRegisters":
882
+ functionCode = 4;
883
+ break;
884
+ default:
885
+ functionCode = 3;
886
+ }
887
+
888
+ try {
889
+ const response = await this.client.requestWithRetry(
890
+ functionCode,
891
+ startAddress,
892
+ quantity,
893
+ null,
894
+ msg.unitId || msg.payload?.unitId
895
+ );
896
+
897
+ msg.payload = {
898
+ action: "readData",
899
+ type: readType,
900
+ address: startAddress,
901
+ quantity: quantity,
902
+ data: response.data,
903
+ dataMap: createDataMap(response.data, startAddress),
904
+ timestamp: new Date()
905
+ };
906
+
907
+ if (this.config.debugMode) {
908
+ this.log(`Read Data - Type: ${readType}, Address: ${startAddress}, Data: ${JSON.stringify(response.data)}`);
909
+ }
910
+
911
+ if (this.config.showData) {
912
+ const dataPreview = Array.isArray(response.data)
913
+ ? response.data.slice(0, 5).join(", ") + (response.data.length > 5 ? "..." : "")
914
+ : JSON.stringify(response.data);
915
+ this.status({
916
+ fill: "green",
917
+ shape: "dot",
918
+ text: `${readType}[${startAddress}]: ${dataPreview}`
919
+ });
920
+ }
921
+
922
+ this.send(msg);
923
+ return;
924
+ } catch (err) {
925
+ msg.error = {
926
+ message: err.message,
927
+ type: readType,
928
+ address: startAddress
929
+ };
930
+ this.error(`Failed to read data: ${err.message}`);
931
+ this.send(msg);
932
+ return;
933
+ }
934
+ }
935
+
936
+ // Server mode: generate test data
937
+ if (action === "generateTestData" && this.config.modbusMode === "server" && this.server) {
938
+ const pattern = msg.pattern || msg.payload?.pattern || this.config.testDataPattern;
939
+ const startAddress = (msg.address !== undefined ? msg.address : msg.payload?.address || 0) + this.config.addressOffset;
940
+ const quantity = msg.quantity || msg.payload?.quantity || 100;
941
+ const dataType = msg.dataType || msg.payload?.dataType || "holdingRegisters";
942
+
943
+ try {
944
+ const result = this.server.generateTestData(pattern, startAddress, quantity, dataType);
945
+
946
+ msg.payload = {
947
+ action: "generateTestData",
948
+ result: result,
949
+ timestamp: new Date()
950
+ };
951
+
952
+ if (this.config.debugMode) {
953
+ this.log(`Test data generated - Pattern: ${pattern}, Type: ${dataType}, Address: ${startAddress}, Quantity: ${quantity}`);
954
+ }
955
+
956
+ this.status({
957
+ fill: "green",
958
+ shape: "dot",
959
+ text: `Test data: ${dataType}[${startAddress}-${startAddress + quantity - 1}]`
960
+ });
961
+
962
+ this.send(msg);
963
+ return;
964
+ } catch (err) {
965
+ msg.error = {
966
+ message: err.message,
967
+ pattern: pattern,
968
+ dataType: dataType
969
+ };
970
+ this.error(`Failed to generate test data: ${err.message}`);
971
+ this.send(msg);
972
+ return;
973
+ }
974
+ }
975
+
976
+ // Server mode: show all register data
977
+ if (action === "showData" && this.config.modbusMode === "server" && this.server) {
978
+ const dataSummary = {
979
+ coils: getDataSummary(this.server.dataModel.coils, "coils"),
980
+ discreteInputs: getDataSummary(this.server.dataModel.discreteInputs, "discreteInputs"),
981
+ holdingRegisters: getDataSummary(this.server.dataModel.holdingRegisters, "holdingRegisters"),
982
+ inputRegisters: getDataSummary(this.server.dataModel.inputRegisters, "inputRegisters")
983
+ };
984
+
985
+ msg.payload = {
986
+ action: "showData",
987
+ summary: dataSummary,
988
+ timestamp: new Date()
989
+ };
990
+
991
+ if (this.config.debugMode) {
992
+ this.log(JSON.stringify(dataSummary, null, 2));
993
+ }
994
+
995
+ this.send(msg);
996
+ return;
997
+ }
998
+
999
+ if (this.config.modbusMode === "client" && this.client) {
1000
+ // Client mode: send request
1001
+ const functionCode = msg.function || msg.payload?.function;
1002
+ const address = (msg.address !== undefined ? msg.address : msg.payload?.address) + this.config.addressOffset;
1003
+ const quantity = msg.quantity || msg.payload?.quantity;
1004
+ const values = msg.values || msg.payload?.values || msg.value || msg.payload?.value;
1005
+ const unitId = msg.unitId || msg.payload?.unitId;
1006
+
1007
+ if (!functionCode || address === undefined) {
1008
+ this.error("Missing required fields: function and address");
1009
+ return;
1010
+ }
1011
+
1012
+ const response = await this.client.requestWithRetry(
1013
+ functionCode,
1014
+ address,
1015
+ quantity,
1016
+ values,
1017
+ unitId
1018
+ );
1019
+
1020
+ msg.payload = {
1021
+ function: functionCode,
1022
+ address: address,
1023
+ quantity: quantity,
1024
+ data: response.data,
1025
+ timestamp: new Date(),
1026
+ responseTime: Date.now() - (msg.timestamp || Date.now())
1027
+ };
1028
+
1029
+ // Show data in status if enabled
1030
+ if (this.config.showData && response.data) {
1031
+ const dataPreview = Array.isArray(response.data)
1032
+ ? response.data.slice(0, 5).join(", ") + (response.data.length > 5 ? "..." : "")
1033
+ : JSON.stringify(response.data);
1034
+ this.status({
1035
+ fill: "green",
1036
+ shape: "dot",
1037
+ text: `Addr ${address}: ${dataPreview}`
1038
+ });
1039
+ }
1040
+
1041
+ // Debug mode: log data
1042
+ if (this.config.debugMode) {
1043
+ this.log(`MODBUS Response - Function: ${functionCode}, Address: ${address}, Data: ${JSON.stringify(response.data)}`);
1044
+ }
1045
+
1046
+ this.send(msg);
1047
+ } else if (this.config.modbusMode === "server" && this.server) {
1048
+ // Server mode: update data
1049
+ const type = msg.type || msg.payload?.type;
1050
+ const address = (msg.address !== undefined ? msg.address : msg.payload?.address) + this.config.addressOffset;
1051
+ const value = msg.value !== undefined ? msg.value : msg.payload?.value;
1052
+ const values = msg.values || msg.payload?.values;
1053
+
1054
+ if (type && address !== undefined) {
1055
+ this.server.updateData(type, address, value, values);
1056
+
1057
+ // Show data in status if enabled
1058
+ if (this.config.showData) {
1059
+ const displayValue = values ?
1060
+ `${values.slice(0, 3).join(",")}${values.length > 3 ? "..." : ""}` :
1061
+ value;
1062
+ this.status({
1063
+ fill: "green",
1064
+ shape: "dot",
1065
+ text: `${type}[${address}]: ${displayValue}`
1066
+ });
1067
+ } else {
1068
+ this.status({ fill: "green", shape: "dot", text: "data updated" });
1069
+ }
1070
+
1071
+ // Debug mode: log data update
1072
+ if (this.config.debugMode) {
1073
+ this.log(`MODBUS Server Data Update - Type: ${type}, Address: ${address}, Value: ${JSON.stringify(values || value)}`);
1074
+ }
1075
+ }
1076
+ }
1077
+ } catch (err) {
1078
+ msg.error = {
1079
+ code: err.message.includes("Exception") ? parseInt(err.message.match(/\d+/)?.[0] || 0) : 0,
1080
+ message: err.message,
1081
+ function: msg.function || msg.payload?.function,
1082
+ address: msg.address || msg.payload?.address
1083
+ };
1084
+ msg.payload = null;
1085
+ this.error(err.message);
1086
+ this.send(msg);
1087
+ }
1088
+ });
1089
+
1090
+ // Cleanup on close
1091
+ this.on('close', () => {
1092
+ if (this.client) {
1093
+ this.client.disconnect();
1094
+ }
1095
+ if (this.server) {
1096
+ this.server.stop();
1097
+ }
1098
+ });
1099
+ }
1100
+
1101
+ RED.nodes.registerType("modbus-tcp-avd", ModbusTCPNode);
1102
+ };