knx.ts 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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -0
  3. package/dist/@types/interfaces/DPTs.d.ts +144 -0
  4. package/dist/@types/interfaces/DPTs.js +3 -0
  5. package/dist/@types/interfaces/EMI.d.ts +396 -0
  6. package/dist/@types/interfaces/EMI.js +2 -0
  7. package/dist/@types/interfaces/ServiceMessage.d.ts +11 -0
  8. package/dist/@types/interfaces/ServiceMessage.js +2 -0
  9. package/dist/@types/interfaces/SystemStatus.d.ts +18 -0
  10. package/dist/@types/interfaces/SystemStatus.js +2 -0
  11. package/dist/@types/interfaces/connection.d.ts +139 -0
  12. package/dist/@types/interfaces/connection.js +2 -0
  13. package/dist/@types/interfaces/localEndPoint.d.ts +5 -0
  14. package/dist/@types/interfaces/localEndPoint.js +2 -0
  15. package/dist/@types/types/AllDpts.d.ts +3 -0
  16. package/dist/@types/types/AllDpts.js +3 -0
  17. package/dist/@types/types/DecodedDPTType.d.ts +21 -0
  18. package/dist/@types/types/DecodedDPTType.js +2 -0
  19. package/dist/connection/KNXService.d.ts +58 -0
  20. package/dist/connection/KNXService.js +242 -0
  21. package/dist/connection/KNXTunneling.d.ts +44 -0
  22. package/dist/connection/KNXTunneling.js +509 -0
  23. package/dist/connection/KNXnetIPServer.d.ts +64 -0
  24. package/dist/connection/KNXnetIPServer.js +900 -0
  25. package/dist/connection/Router.d.ts +49 -0
  26. package/dist/connection/Router.js +269 -0
  27. package/dist/connection/TPUART.d.ts +32 -0
  28. package/dist/connection/TPUART.js +497 -0
  29. package/dist/connection/TunnelConnection.d.ts +57 -0
  30. package/dist/connection/TunnelConnection.js +167 -0
  31. package/dist/core/CEMI.d.ts +1130 -0
  32. package/dist/core/CEMI.js +1281 -0
  33. package/dist/core/ControlField.d.ts +57 -0
  34. package/dist/core/ControlField.js +120 -0
  35. package/dist/core/ControlFieldExtended.d.ts +56 -0
  36. package/dist/core/ControlFieldExtended.js +114 -0
  37. package/dist/core/EMI.d.ts +2515 -0
  38. package/dist/core/EMI.js +3898 -0
  39. package/dist/core/KNXAddInfoTypes.d.ts +225 -0
  40. package/dist/core/KNXAddInfoTypes.js +602 -0
  41. package/dist/core/KNXnetIPHeader.d.ts +10 -0
  42. package/dist/core/KNXnetIPHeader.js +38 -0
  43. package/dist/core/KNXnetIPStructures.d.ts +179 -0
  44. package/dist/core/KNXnetIPStructures.js +622 -0
  45. package/dist/core/MessageCodeField.d.ts +886 -0
  46. package/dist/core/MessageCodeField.js +399 -0
  47. package/dist/core/SystemStatus.d.ts +144 -0
  48. package/dist/core/SystemStatus.js +325 -0
  49. package/dist/core/data/KNXData.d.ts +7 -0
  50. package/dist/core/data/KNXData.js +30 -0
  51. package/dist/core/data/KNXDataDecode.d.ts +396 -0
  52. package/dist/core/data/KNXDataDecode.js +1186 -0
  53. package/dist/core/data/KNXDataEncode.d.ts +332 -0
  54. package/dist/core/data/KNXDataEncode.js +1504 -0
  55. package/dist/core/enum/APCIEnum.d.ts +587 -0
  56. package/dist/core/enum/APCIEnum.js +591 -0
  57. package/dist/core/enum/EnumControlField.d.ts +24 -0
  58. package/dist/core/enum/EnumControlField.js +36 -0
  59. package/dist/core/enum/EnumControlFieldExtended.d.ts +36 -0
  60. package/dist/core/enum/EnumControlFieldExtended.js +41 -0
  61. package/dist/core/enum/EnumShortACKFrame.d.ts +6 -0
  62. package/dist/core/enum/EnumShortACKFrame.js +10 -0
  63. package/dist/core/enum/ErrorCodeSet.d.ts +57 -0
  64. package/dist/core/enum/ErrorCodeSet.js +2 -0
  65. package/dist/core/enum/KNXnetIPEnum.d.ts +95 -0
  66. package/dist/core/enum/KNXnetIPEnum.js +90 -0
  67. package/dist/core/enum/SAP.d.ts +19 -0
  68. package/dist/core/enum/SAP.js +23 -0
  69. package/dist/core/layers/data/APDU.d.ts +38 -0
  70. package/dist/core/layers/data/APDU.js +115 -0
  71. package/dist/core/layers/data/NPDU.d.ts +73 -0
  72. package/dist/core/layers/data/NPDU.js +103 -0
  73. package/dist/core/layers/data/TPDU.d.ts +53 -0
  74. package/dist/core/layers/data/TPDU.js +73 -0
  75. package/dist/core/layers/interfaces/APCI.d.ts +61 -0
  76. package/dist/core/layers/interfaces/APCI.js +92 -0
  77. package/dist/core/layers/interfaces/TPCI.d.ts +110 -0
  78. package/dist/core/layers/interfaces/TPCI.js +196 -0
  79. package/dist/core/resources/DeviceDescriptorType.d.ts +46 -0
  80. package/dist/core/resources/DeviceDescriptorType.js +69 -0
  81. package/dist/errors/DPTNotFound.d.ts +6 -0
  82. package/dist/errors/DPTNotFound.js +15 -0
  83. package/dist/errors/InvalidKnxAddresExeption.d.ts +3 -0
  84. package/dist/errors/InvalidKnxAddresExeption.js +9 -0
  85. package/dist/index.d.ts +7 -0
  86. package/dist/index.js +18 -0
  87. package/dist/utils/CEMIAdapter.d.ts +16 -0
  88. package/dist/utils/CEMIAdapter.js +94 -0
  89. package/dist/utils/KNXHelper.d.ts +78 -0
  90. package/dist/utils/KNXHelper.js +338 -0
  91. package/dist/utils/Logger.d.ts +17 -0
  92. package/dist/utils/Logger.js +96 -0
  93. package/dist/utils/MessageCodeTranslator.d.ts +19 -0
  94. package/dist/utils/MessageCodeTranslator.js +77 -0
  95. package/dist/utils/checksumFrame.d.ts +18 -0
  96. package/dist/utils/checksumFrame.js +41 -0
  97. package/dist/utils/localIp.d.ts +7 -0
  98. package/dist/utils/localIp.js +45 -0
  99. package/package.json +49 -0
@@ -0,0 +1,497 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TPUARTConnection = void 0;
4
+ const serialport_1 = require("serialport");
5
+ const KNXService_1 = require("./KNXService");
6
+ const CEMIAdapter_1 = require("../utils/CEMIAdapter");
7
+ const EMI_1 = require("../core/EMI");
8
+ const KNXHelper_1 = require("../utils/KNXHelper");
9
+ const CEMI_1 = require("../core/CEMI");
10
+ const UART_SERVICES = {
11
+ RESET_REQ: 0x01,
12
+ RESET_IND: 0x03,
13
+ STATE_REQ: 0x02,
14
+ STATE_IND: 0x07,
15
+ ACTIVATE_BUSMON: 0x05,
16
+ LDATA_CON_POS: 0x8b,
17
+ LDATA_CON_NEG: 0x0b,
18
+ ACK_INFO: 0x10,
19
+ LDATA_START: 0x80,
20
+ LDATA_END: 0x40,
21
+ BUSY: 0xc0,
22
+ };
23
+ var TPUARTState;
24
+ (function (TPUARTState) {
25
+ TPUARTState[TPUARTState["DISCONNECTED"] = 0] = "DISCONNECTED";
26
+ TPUARTState[TPUARTState["RESET_WAIT"] = 1] = "RESET_WAIT";
27
+ TPUARTState[TPUARTState["SET_ADDR_WAIT"] = 2] = "SET_ADDR_WAIT";
28
+ TPUARTState[TPUARTState["GET_STATE_WAIT"] = 3] = "GET_STATE_WAIT";
29
+ TPUARTState[TPUARTState["ONLINE"] = 4] = "ONLINE";
30
+ TPUARTState[TPUARTState["ERROR"] = 5] = "ERROR";
31
+ })(TPUARTState || (TPUARTState = {}));
32
+ class TPUARTConnection extends KNXService_1.KNXService {
33
+ serialPort;
34
+ receiver;
35
+ connectionState = TPUARTState.DISCONNECTED;
36
+ initPromise = null;
37
+ msgQueue = [];
38
+ isProcessing = false;
39
+ lastSentFrame = null;
40
+ keepaliveTimer = null;
41
+ confirmationTimer = null;
42
+ initTimer = null;
43
+ initRetryCount = 0;
44
+ isBusmonitorMode = false;
45
+ constructor(options) {
46
+ super(options);
47
+ this.serialPort = new serialport_1.SerialPort({
48
+ path: options.path,
49
+ baudRate: 19200,
50
+ dataBits: 8,
51
+ parity: "even",
52
+ stopBits: 1,
53
+ autoOpen: false,
54
+ });
55
+ this.receiver = new Receiver(this);
56
+ this.serialPort.on("data", (data) => this.receiver.handleData(data));
57
+ this.serialPort.on("error", (err) => {
58
+ this.handleFatalError(err);
59
+ });
60
+ this.on("raw_frame", (frame) => {
61
+ // 1. Echo Cancellation (knxd pattern: ignore repeat bit 0x20)
62
+ if (this.lastSentFrame && frame.length === this.lastSentFrame.length) {
63
+ if ((frame[0] & ~0x20) === (this.lastSentFrame[0] & ~0x20) &&
64
+ frame.subarray(1).equals(this.lastSentFrame.subarray(1))) {
65
+ this.lastSentFrame = null;
66
+ return;
67
+ }
68
+ }
69
+ this.resetKeepalive();
70
+ // 2. Hardware ACK (knxd pattern)
71
+ // Only if NOT in busmonitor mode
72
+ if (!this.isBusmonitorMode) {
73
+ const options = this.options;
74
+ let ackByte = 0x10; // Default: No ACK
75
+ if (options.ackGroup || options.ackIndividual) {
76
+ const isExtended = (frame[0] & 0x80) === 0;
77
+ const controlByte = isExtended ? frame[1] : frame[5];
78
+ const isGroup = (controlByte & 0x80) !== 0;
79
+ if ((isGroup && options.ackGroup) ||
80
+ (!isGroup && options.ackIndividual)) {
81
+ ackByte = 0x11; // Send ACK
82
+ }
83
+ }
84
+ this.writeRaw([ackByte]).catch(() => { });
85
+ }
86
+ try {
87
+ if (this.isBusmonitorMode) {
88
+ // In Busmonitor mode, we emit L_Busmon.ind
89
+ const cemi = new CEMI_1.CEMI.DataLinkLayerCEMI["L_Busmon.ind"](null, frame);
90
+ this.emit("indication", cemi);
91
+ this.emit("busmonitor", cemi);
92
+ }
93
+ else {
94
+ const emiBuffer = Buffer.concat([Buffer.from([0x29]), frame]);
95
+ const cemi = CEMIAdapter_1.CEMIAdapter.emiToCemi(emiBuffer);
96
+ if (cemi) {
97
+ this.emit("indication", cemi);
98
+ this.emit("raw_indication", cemi.toBuffer());
99
+ }
100
+ }
101
+ }
102
+ catch (e) { }
103
+ });
104
+ }
105
+ handleFatalError(err) {
106
+ this.stopTimers();
107
+ this.connectionState = TPUARTState.ERROR;
108
+ // Reject all pending messages
109
+ while (this.msgQueue.length > 0) {
110
+ this.msgQueue.shift()?.reject(err);
111
+ }
112
+ this.isProcessing = false;
113
+ this.emit("error", err);
114
+ }
115
+ stopTimers() {
116
+ if (this.keepaliveTimer)
117
+ clearTimeout(this.keepaliveTimer);
118
+ if (this.confirmationTimer)
119
+ clearTimeout(this.confirmationTimer);
120
+ if (this.initTimer)
121
+ clearTimeout(this.initTimer);
122
+ this.keepaliveTimer = null;
123
+ this.confirmationTimer = null;
124
+ this.initTimer = null;
125
+ }
126
+ resetKeepalive() {
127
+ if (this.keepaliveTimer)
128
+ clearTimeout(this.keepaliveTimer);
129
+ this.keepaliveTimer = setTimeout(() => {
130
+ if (this.connectionState === TPUARTState.ONLINE) {
131
+ this.initRetryCount = 0;
132
+ this.requestState();
133
+ }
134
+ }, 10000);
135
+ }
136
+ async connect() {
137
+ if (this.connectionState !== TPUARTState.DISCONNECTED &&
138
+ this.connectionState !== TPUARTState.ERROR)
139
+ return;
140
+ return new Promise((resolve, reject) => {
141
+ this.initPromise = { resolve, reject };
142
+ this.serialPort.open(async (err) => {
143
+ if (err) {
144
+ this.initPromise = null;
145
+ reject(err);
146
+ return;
147
+ }
148
+ this.initRetryCount = 0;
149
+ this.sendResetRequest();
150
+ });
151
+ });
152
+ }
153
+ sendResetRequest() {
154
+ this.connectionState = TPUARTState.RESET_WAIT;
155
+ this.writeRaw([UART_SERVICES.RESET_REQ]).catch(() => { });
156
+ if (this.initTimer)
157
+ clearTimeout(this.initTimer);
158
+ this.initTimer = setTimeout(() => {
159
+ if (this.connectionState === TPUARTState.RESET_WAIT) {
160
+ this.initRetryCount++;
161
+ if (this.initRetryCount < 3) {
162
+ this.sendResetRequest();
163
+ }
164
+ else {
165
+ if (this.initPromise) {
166
+ this.initPromise.reject(new Error("TPUART reset timeout"));
167
+ this.initPromise = null;
168
+ }
169
+ this.handleFatalError(new Error("TPUART reset timeout"));
170
+ }
171
+ }
172
+ }, 500);
173
+ }
174
+ async disconnect() {
175
+ this.stopTimers();
176
+ this.connectionState = TPUARTState.DISCONNECTED;
177
+ // Clear queue
178
+ while (this.msgQueue.length > 0) {
179
+ this.msgQueue.shift()?.reject(new Error("Disconnected by user"));
180
+ }
181
+ this.isProcessing = false;
182
+ return new Promise((resolve) => {
183
+ if (this.serialPort.isOpen) {
184
+ this.serialPort.close(() => {
185
+ this.emit("disconnected");
186
+ resolve();
187
+ });
188
+ }
189
+ else {
190
+ resolve();
191
+ }
192
+ });
193
+ }
194
+ async setBusmonitor(enabled) {
195
+ if (this.connectionState < TPUARTState.ONLINE)
196
+ throw new Error("TPUART offline");
197
+ this.isBusmonitorMode = enabled;
198
+ if (enabled) {
199
+ await this.writeRaw([UART_SERVICES.ACTIVATE_BUSMON]);
200
+ }
201
+ else {
202
+ // To exit busmonitor, we usually need a reset or re-init
203
+ this.initRetryCount = 0;
204
+ this.sendResetRequest();
205
+ }
206
+ }
207
+ async send(data) {
208
+ if (this.connectionState < TPUARTState.ONLINE)
209
+ throw new Error("TPUART offline");
210
+ let frame = Buffer.isBuffer(data)
211
+ ? tryEmi(data) || data
212
+ : CEMIAdapter_1.CEMIAdapter.cemiToEmi(data)?.toBuffer().subarray(1);
213
+ if (!frame)
214
+ throw new Error("Invalid data");
215
+ return this.enqueueFrame(frame);
216
+ }
217
+ async enqueueFrame(frame) {
218
+ return new Promise((resolve, reject) => {
219
+ this.msgQueue.push({ frame, resolve, reject, attempts: 0 });
220
+ this.processQueue();
221
+ });
222
+ }
223
+ async processQueue() {
224
+ if (this.isProcessing || this.msgQueue.length === 0)
225
+ return;
226
+ this.isProcessing = true;
227
+ const item = this.msgQueue[0];
228
+ // knxd pattern: If this is a retry (attempts > 0), set the Repeat Bit (bit 5) to 0
229
+ // This tells the bus that this is a repetition of a previously failed frame.
230
+ if (item.attempts > 0 && item.frame[0] & 0x20) {
231
+ item.frame[0] &= ~0x20; // Set repeat bit to 0
232
+ // Update checksum (XOR with 0x20 since we toggled one bit)
233
+ item.frame[item.frame.length - 1] ^= 0x20;
234
+ }
235
+ this.lastSentFrame = item.frame;
236
+ // Timeout for hardware confirmation (0x8B/0x0B)
237
+ this.confirmationTimer = setTimeout(() => {
238
+ this.isProcessing = false;
239
+ this.lastSentFrame = null;
240
+ if (item.attempts < 3) {
241
+ item.attempts++;
242
+ this.processQueue();
243
+ }
244
+ else {
245
+ this.msgQueue.shift();
246
+ item.reject(new Error("TPUART confirmation timeout"));
247
+ this.processQueue();
248
+ }
249
+ }, 2000);
250
+ this.serialPort.write(this.toUartServices(item.frame), (err) => {
251
+ if (err) {
252
+ if (this.confirmationTimer)
253
+ clearTimeout(this.confirmationTimer);
254
+ this.isProcessing = false;
255
+ this.lastSentFrame = null;
256
+ if (item.attempts < 3) {
257
+ item.attempts++;
258
+ setTimeout(() => this.processQueue(), 50);
259
+ }
260
+ else {
261
+ this.msgQueue.shift();
262
+ item.reject(err);
263
+ this.processQueue();
264
+ }
265
+ }
266
+ });
267
+ }
268
+ toUartServices(telegram) {
269
+ const result = Buffer.alloc(telegram.length * 2);
270
+ for (let i = 0; i < telegram.length; i++) {
271
+ const ctrl = i === telegram.length - 1
272
+ ? UART_SERVICES.LDATA_END | (i & 0x3f)
273
+ : UART_SERVICES.LDATA_START | (i & 0x3f);
274
+ result[i * 2] = ctrl;
275
+ result[i * 2 + 1] = telegram[i];
276
+ }
277
+ return result;
278
+ }
279
+ _handleControlByte(byte) {
280
+ this.resetKeepalive();
281
+ // 1. External ACKs/NACKs from other devices
282
+ if (byte === 0xcc || byte === 0x0c || byte === 0xc0) {
283
+ this.emit("bus_ack", {
284
+ type: byte === 0xcc ? "ACK" : byte === 0x0c ? "NACK" : "BUSY",
285
+ timestamp: Date.now(),
286
+ });
287
+ return;
288
+ }
289
+ // 2. NCN5120 / TPUART2 Frame State Indication
290
+ if ((byte & 0x17) === 0x13) {
291
+ const hasError = (byte & 0x07) !== 0;
292
+ if (hasError) {
293
+ const error = byte & 0x04
294
+ ? "Checksum Error"
295
+ : byte & 0x02
296
+ ? "Timing Error"
297
+ : "Bit Error";
298
+ this.emit("warning", `TPUART Frame Error: ${error}`);
299
+ }
300
+ return;
301
+ }
302
+ if (byte === UART_SERVICES.RESET_IND) {
303
+ if (this.connectionState === TPUARTState.RESET_WAIT) {
304
+ if (this.initTimer)
305
+ clearTimeout(this.initTimer);
306
+ this.initRetryCount = 0;
307
+ const options = this.options;
308
+ if (options.individualAddress) {
309
+ this.connectionState = TPUARTState.SET_ADDR_WAIT;
310
+ this.writeRaw(Buffer.concat([
311
+ Buffer.from([0x28]),
312
+ KNXHelper_1.KNXHelper.GetAddress(options.individualAddress, "."),
313
+ ])).catch((e) => this.emit("error", e));
314
+ // knxd immediately transitions to get state after setting address
315
+ this.requestState();
316
+ }
317
+ else {
318
+ this.requestState();
319
+ }
320
+ }
321
+ else if (this.connectionState >= TPUARTState.ONLINE) {
322
+ // Spurious reset (power glitch?) -> re-initialize
323
+ this.emit("warning", "TPUART spurious reset detected, re-initializing...");
324
+ this.initRetryCount = 0;
325
+ this.sendResetRequest();
326
+ }
327
+ return;
328
+ }
329
+ if (byte === UART_SERVICES.BUSY) {
330
+ if (this.confirmationTimer)
331
+ clearTimeout(this.confirmationTimer);
332
+ const item = this.msgQueue[0];
333
+ if (item && item.attempts < 3) {
334
+ item.attempts++;
335
+ this.isProcessing = false;
336
+ setTimeout(() => this.processQueue(), 50);
337
+ }
338
+ else if (item) {
339
+ this.msgQueue.shift();
340
+ this.isProcessing = false;
341
+ this.lastSentFrame = null;
342
+ item.reject(new Error("Bus Busy"));
343
+ this.processQueue();
344
+ }
345
+ return;
346
+ }
347
+ if (byte === UART_SERVICES.LDATA_CON_POS ||
348
+ byte === UART_SERVICES.LDATA_CON_NEG) {
349
+ if (this.confirmationTimer)
350
+ clearTimeout(this.confirmationTimer);
351
+ const item = this.msgQueue.shift();
352
+ this.isProcessing = false;
353
+ if (item) {
354
+ byte === UART_SERVICES.LDATA_CON_POS
355
+ ? item.resolve()
356
+ : item.reject(new Error("NAK"));
357
+ }
358
+ this.processQueue();
359
+ return;
360
+ }
361
+ if ((byte & 0x07) === UART_SERVICES.STATE_IND) {
362
+ if (this.initTimer)
363
+ clearTimeout(this.initTimer);
364
+ this.initRetryCount = 0;
365
+ // Decode error bits (knxd pattern)
366
+ if (byte !== 0x07 && byte !== 0x00) {
367
+ if (byte & 0x40)
368
+ this.emit("warning", "TPUART: Hardware ACK NOT supported by this chip");
369
+ if (byte & 0x04)
370
+ this.emit("warning", "TPUART: Slave collision detected");
371
+ if (byte & 0x02)
372
+ this.emit("warning", "TPUART: Receive error");
373
+ if (byte & 0x01)
374
+ this.emit("warning", "TPUART: Transmit error");
375
+ }
376
+ if (this.connectionState < TPUARTState.ONLINE) {
377
+ this.connectionState = TPUARTState.ONLINE;
378
+ if (this.initPromise) {
379
+ this.initPromise.resolve();
380
+ this.initPromise = null;
381
+ this.emit("connected");
382
+ }
383
+ }
384
+ }
385
+ }
386
+ async writeRaw(data) {
387
+ return new Promise((resolve, reject) => {
388
+ this.serialPort.write(data, (err) => {
389
+ if (err)
390
+ reject(err);
391
+ else
392
+ resolve();
393
+ });
394
+ });
395
+ }
396
+ requestState() {
397
+ this.connectionState = TPUARTState.GET_STATE_WAIT;
398
+ this.writeRaw([UART_SERVICES.STATE_REQ]).catch((e) => this.emit("error", e));
399
+ if (this.initTimer)
400
+ clearTimeout(this.initTimer);
401
+ this.initTimer = setTimeout(() => {
402
+ if (this.connectionState === TPUARTState.GET_STATE_WAIT) {
403
+ this.initRetryCount++;
404
+ if (this.initRetryCount < 5) {
405
+ this.requestState();
406
+ }
407
+ else {
408
+ if (this.initPromise) {
409
+ this.initPromise.reject(new Error("TPUART state request timeout"));
410
+ this.initPromise = null;
411
+ }
412
+ this.handleFatalError(new Error("TPUART state request timeout"));
413
+ }
414
+ }
415
+ }, 500);
416
+ }
417
+ }
418
+ exports.TPUARTConnection = TPUARTConnection;
419
+ function tryEmi(data) {
420
+ try {
421
+ return EMI_1.EMI.fromBuffer(data).toBuffer().subarray(1);
422
+ }
423
+ catch {
424
+ return null;
425
+ }
426
+ }
427
+ class Receiver {
428
+ connection;
429
+ buffer = Buffer.alloc(0);
430
+ lastRead = 0;
431
+ extFrame = false;
432
+ constructor(connection) {
433
+ this.connection = connection;
434
+ }
435
+ handleData(data) {
436
+ for (const byte of data)
437
+ this.processByte(byte);
438
+ }
439
+ processByte(byte) {
440
+ // BUG FIX: Only handle control bytes if we are NOT in the middle of a frame
441
+ // This prevents data bytes (like 0x03) from being interpreted as RESET_IND
442
+ if (this.buffer.length === 0) {
443
+ if (this.isControlByte(byte)) {
444
+ this.connection._handleControlByte(byte);
445
+ return;
446
+ }
447
+ // Support for NCN5120/TPUART2: Ignore frame end (0xCB) and state indication (0x13)
448
+ if (byte === 0xcb || (byte & 0x17) === 0x13)
449
+ return;
450
+ }
451
+ const now = Date.now();
452
+ // Inter-byte timeout (1000ms) to reset buffer if sync is lost (matches knxd T_wait_more)
453
+ if (this.buffer.length > 0 && now - this.lastRead > 1000)
454
+ this.buffer = Buffer.alloc(0);
455
+ if (this.buffer.length === 0) {
456
+ if (this.isFrameStart(byte)) {
457
+ this.buffer = Buffer.from([byte]);
458
+ this.lastRead = now;
459
+ }
460
+ }
461
+ else {
462
+ this.buffer = Buffer.concat([this.buffer, Buffer.from([byte])]);
463
+ this.lastRead = now;
464
+ this.checkCompleteFrame();
465
+ }
466
+ }
467
+ isControlByte(byte) {
468
+ return (byte === UART_SERVICES.RESET_IND ||
469
+ byte === UART_SERVICES.LDATA_CON_POS ||
470
+ byte === UART_SERVICES.LDATA_CON_NEG ||
471
+ byte === UART_SERVICES.BUSY ||
472
+ (byte & 0x07) === UART_SERVICES.STATE_IND);
473
+ }
474
+ isFrameStart(byte) {
475
+ this.extFrame = (byte & 0x80) === 0;
476
+ return (byte & 0x50) === 0x10;
477
+ }
478
+ checkCompleteFrame() {
479
+ const minLength = this.extFrame ? 7 : 6;
480
+ if (this.buffer.length >= minLength) {
481
+ const payloadLen = this.extFrame ? this.buffer[6] : this.buffer[5] & 0x0f;
482
+ const totalLen = payloadLen + (this.extFrame ? 9 : 8);
483
+ if (this.buffer.length >= totalLen) {
484
+ const frame = this.buffer.subarray(0, totalLen);
485
+ if (this.validateChecksum(frame))
486
+ this.connection.emit("raw_frame", frame);
487
+ this.buffer = Buffer.alloc(0);
488
+ }
489
+ }
490
+ }
491
+ validateChecksum(frame) {
492
+ let checksum = 0;
493
+ for (let i = 0; i < frame.length - 1; i++)
494
+ checksum ^= frame[i];
495
+ return frame[frame.length - 1] === (checksum ^ 0xff);
496
+ }
497
+ }
@@ -0,0 +1,57 @@
1
+ import dgram from "dgram";
2
+ import { HPAI } from "../core/KNXnetIPStructures";
3
+ import { KNXnetIPServiceType, KNXLayer } from "../core/enum/KNXnetIPEnum";
4
+ import { Logger } from "pino";
5
+ /**
6
+ * Encapsulates a single KNXnet/IP Tunnelling or Management connection state.
7
+ * Handles sequence numbers, heartbeats, reliable delivery (stop-and-wait),
8
+ * and retransmissions according to KNX Spec Vol 3/8/4.
9
+ */
10
+ export declare class TunnelConnection {
11
+ readonly channelId: number;
12
+ readonly controlHPAI: HPAI;
13
+ readonly dataHPAI: HPAI;
14
+ readonly knxAddress: number;
15
+ readonly knxAddressStr: string;
16
+ readonly knxLayer: KNXLayer;
17
+ private readonly socket;
18
+ private readonly heartbeatTimeoutMs;
19
+ private readonly retransmitTimeoutMs;
20
+ private readonly maxQueueSize;
21
+ private readonly onDisconnect;
22
+ sno: number;
23
+ rno: number;
24
+ private heartbeatTimer;
25
+ private pendingAck;
26
+ private queue;
27
+ private isSending;
28
+ rxCount: number;
29
+ lastRxTime: number;
30
+ private logger;
31
+ constructor(channelId: number, controlHPAI: HPAI, dataHPAI: HPAI, knxAddress: number, knxAddressStr: string, knxLayer: KNXLayer, socket: dgram.Socket, heartbeatTimeoutMs: number, retransmitTimeoutMs: number, maxQueueSize: number, onDisconnect: (channelId: number, sendDisconnect: boolean) => void, parentLogger: Logger);
32
+ /**
33
+ * Resets the heartbeat timer. Should be called on any valid activity.
34
+ */
35
+ resetHeartbeat(): void;
36
+ /**
37
+ * Enqueues a CEMI message to be sent to the client.
38
+ */
39
+ enqueue(cemiBuffer: Buffer, serviceType: KNXnetIPServiceType): void;
40
+ private processQueue;
41
+ private sendWithRetry;
42
+ /**
43
+ * Handles an incoming ACK from the client.
44
+ */
45
+ handleAck(seq: number, status: number): void;
46
+ /**
47
+ * Validates an incoming request from the client according to sequence number rules.
48
+ */
49
+ validateRequest(seq: number): {
50
+ action: 'process' | 'discard' | 'retransmit_ack';
51
+ status: number;
52
+ };
53
+ /**
54
+ * Closes the connection and cleans up resources.
55
+ */
56
+ close(): void;
57
+ }