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,509 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.KNXTunneling = void 0;
7
+ const dgram_1 = __importDefault(require("dgram"));
8
+ const net_1 = __importDefault(require("net"));
9
+ const KNXService_1 = require("./KNXService");
10
+ const KNXnetIPHeader_1 = require("../core/KNXnetIPHeader");
11
+ const KNXnetIPStructures_1 = require("../core/KNXnetIPStructures");
12
+ const KNXnetIPEnum_1 = require("../core/enum/KNXnetIPEnum");
13
+ const CEMI_1 = require("../core/CEMI");
14
+ /**
15
+ * Handles KNXnet/IP Tunneling connections for point-to-point communication with a KNX gateway.
16
+ * This class manages the connection state, sequence numbering for reliable delivery,
17
+ * heartbeat monitoring (ConnectionState), and message queuing over both UDP and TCP transports.
18
+ */
19
+ class KNXTunneling extends KNXService_1.KNXService {
20
+ channelId = 0;
21
+ sequenceNumber = 0;
22
+ rxSequenceNumber = 0;
23
+ isConnected = false;
24
+ tcpBuffer = Buffer.alloc(0);
25
+ assignedAddress = 0; // Assigned Individual Address
26
+ // Heartbeat
27
+ heartbeatTimer = null;
28
+ heartbeatFailures = 0;
29
+ heartbeatRetryTimer = null;
30
+ // Message Queue
31
+ msgQueue = [];
32
+ isSending = false;
33
+ pendingAck = null;
34
+ activeRequest = null;
35
+ MAX_QUEUE_SIZE;
36
+ // Disconnect
37
+ disconnectTimeout = null;
38
+ constructor(options) {
39
+ super(options);
40
+ this._transport = options.transport || "UDP";
41
+ if (!this.options.connectionType) {
42
+ this.options.connectionType =
43
+ KNXnetIPEnum_1.ConnectionType.TUNNEL_CONNECTION;
44
+ }
45
+ this.MAX_QUEUE_SIZE = options.maxQueueSize || 100;
46
+ this.logger = this.logger.child({ module: "TunnelClient" });
47
+ }
48
+ async connect() {
49
+ this.rxSequenceNumber = 0;
50
+ if (this._transport === "TCP") {
51
+ await this.connectTCP();
52
+ }
53
+ else {
54
+ await this.connectUDP();
55
+ }
56
+ }
57
+ async connectUDP() {
58
+ this.socket = dgram_1.default.createSocket("udp4");
59
+ // Manejo de mensajes entrantes
60
+ this.socket.on("message", (msg) => this.handleMessage(msg));
61
+ // ERROR GLOBAL: Si el socket muere, desconectamos
62
+ this.socket.on("error", (err) => {
63
+ this.emit("error", err);
64
+ this.disconnect();
65
+ });
66
+ return new Promise((resolve, reject) => {
67
+ // Listener temporal para atrapar errores DURANTE la conexión inicial
68
+ const errorListener = (err) => {
69
+ this.removeListener("connected", successListener);
70
+ reject(err);
71
+ };
72
+ const successListener = (info) => {
73
+ this.removeListener("error", errorListener); // Limpiamos el listener de error temporal
74
+ resolve();
75
+ };
76
+ // Escuchamos ambos eventos
77
+ this.once("error", errorListener);
78
+ this.once("connected", successListener);
79
+ // Bind
80
+ this.socket.bind(this.options.localPort, this.options.localIp, () => {
81
+ try {
82
+ this.sendConnectRequest();
83
+ }
84
+ catch (e) {
85
+ reject(e);
86
+ }
87
+ });
88
+ });
89
+ }
90
+ async connectTCP() {
91
+ return new Promise((resolve, reject) => {
92
+ this.socket = new net_1.default.Socket();
93
+ this.socket.connect(this.options.port, this.options.ip, () => {
94
+ this.sendConnectRequest();
95
+ this.once("connected", resolve);
96
+ });
97
+ this.socket.on("data", (data) => {
98
+ this.tcpBuffer = Buffer.concat([this.tcpBuffer, data]);
99
+ while (this.tcpBuffer.length >= 6) {
100
+ const totalLength = this.tcpBuffer.readUInt16BE(4);
101
+ if (this.tcpBuffer.length >= totalLength) {
102
+ const frame = this.tcpBuffer.subarray(0, totalLength);
103
+ this.tcpBuffer = this.tcpBuffer.subarray(totalLength);
104
+ this.handleMessage(frame);
105
+ }
106
+ else {
107
+ break;
108
+ }
109
+ }
110
+ });
111
+ this.socket.on("error", (err) => {
112
+ this.emit("error", err);
113
+ this.disconnect();
114
+ reject(err);
115
+ });
116
+ this.socket.on("close", () => this.disconnect());
117
+ });
118
+ }
119
+ sendConnectRequest() {
120
+ const localPort = this._transport === "UDP"
121
+ ? this.socket.address().port
122
+ : this.socket.localPort;
123
+ const useRouteBack = this.options.useRouteBack;
124
+ const hpai = new KNXnetIPStructures_1.HPAI(this._transport === "TCP"
125
+ ? KNXnetIPEnum_1.HostProtocolCode.IPV4_TCP
126
+ : KNXnetIPEnum_1.HostProtocolCode.IPV4_UDP, useRouteBack ? "0.0.0.0" : this.options.localIp, useRouteBack ? 0 : localPort);
127
+ // @ts-ignore
128
+ const cri = new KNXnetIPStructures_1.CRI(this.options.connectionType);
129
+ const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_REQUEST, 0);
130
+ // CORRECCIÓN
131
+ // Estructura: HPAI (Control) -> HPAI (Data) -> CRI
132
+ const body = Buffer.concat([
133
+ hpai.toBuffer(),
134
+ hpai.toBuffer(),
135
+ cri.toBuffer(),
136
+ ]);
137
+ header.totalLength = 6 + body.length;
138
+ this.sendRaw(Buffer.concat([header.toBuffer(), body]));
139
+ }
140
+ disconnect() {
141
+ if (this.isConnected && this.channelId) {
142
+ const localPort = this._transport === "UDP"
143
+ ? this.socket.address().port
144
+ : this.socket.localPort;
145
+ const useRouteBack = this.options.useRouteBack;
146
+ const hpai = new KNXnetIPStructures_1.HPAI(this._transport === "TCP"
147
+ ? KNXnetIPEnum_1.HostProtocolCode.IPV4_TCP
148
+ : KNXnetIPEnum_1.HostProtocolCode.IPV4_UDP, useRouteBack ? "0.0.0.0" : this.options.localIp, useRouteBack ? 0 : localPort);
149
+ const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.DISCONNECT_REQUEST, 0);
150
+ const body = Buffer.concat([
151
+ Buffer.from([this.channelId, 0x00]),
152
+ hpai.toBuffer(),
153
+ ]);
154
+ header.totalLength = 6 + body.length;
155
+ this.sendRaw(Buffer.concat([header.toBuffer(), body]));
156
+ // Graceful disconnect: Wait for response or timeout (1s)
157
+ this.disconnectTimeout = setTimeout(() => {
158
+ this.closeSocket();
159
+ }, 1000);
160
+ }
161
+ else {
162
+ this.closeSocket();
163
+ }
164
+ }
165
+ closeSocket() {
166
+ this.stopHeartbeat();
167
+ if (this.disconnectTimeout) {
168
+ clearTimeout(this.disconnectTimeout);
169
+ this.disconnectTimeout = null;
170
+ }
171
+ this.isConnected = false;
172
+ this.channelId = 0;
173
+ if (this.socket) {
174
+ if (this._transport === "UDP")
175
+ this.socket.close();
176
+ else
177
+ this.socket.destroy();
178
+ this.socket = null;
179
+ }
180
+ this.emit("disconnected");
181
+ }
182
+ // #region Message Queue & Sending
183
+ async send(cemi) {
184
+ if (!this.isConnected)
185
+ throw new Error("Not connected");
186
+ if (this.msgQueue.length >= this.MAX_QUEUE_SIZE) {
187
+ throw new Error("Outgoing queue full");
188
+ }
189
+ const cemiBuffer = Buffer.isBuffer(cemi) ? cemi : cemi.toBuffer();
190
+ const isDeviceMgmt = this.options.connectionType ===
191
+ KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION;
192
+ const serviceType = isDeviceMgmt
193
+ ? KNXnetIPEnum_1.KNXnetIPServiceType.DEVICE_CONFIGURATION_REQUEST
194
+ : KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST;
195
+ return new Promise((resolve, reject) => {
196
+ this.msgQueue.push({ packet: cemiBuffer, serviceType, resolve, reject });
197
+ this.processQueue();
198
+ });
199
+ }
200
+ processQueue() {
201
+ if (this.isSending || this.msgQueue.length === 0)
202
+ return;
203
+ this.isSending = true;
204
+ const msg = this.msgQueue.shift();
205
+ this.activeRequest = msg;
206
+ const header = new KNXnetIPHeader_1.KNXnetIPHeader(msg.serviceType, 0);
207
+ const connHeader = Buffer.from([
208
+ 0x04,
209
+ this.channelId,
210
+ this.sequenceNumber,
211
+ 0x00,
212
+ ]);
213
+ header.totalLength = 6 + connHeader.length + msg.packet.length;
214
+ const packet = Buffer.concat([header.toBuffer(), connHeader, msg.packet]);
215
+ this.pendingAck = {
216
+ seq: this.sequenceNumber,
217
+ timer: setTimeout(() => this.handleAckTimeout(), 1000),
218
+ retryCount: 0,
219
+ currentMsg: msg,
220
+ };
221
+ this.sendRaw(packet);
222
+ }
223
+ handleAckTimeout() {
224
+ if (!this.pendingAck)
225
+ return;
226
+ if (this.pendingAck.retryCount < 1) {
227
+ // 1 retry (Spec 2.6.1)
228
+ this.pendingAck.retryCount++;
229
+ const msg = this.pendingAck.currentMsg;
230
+ const header = new KNXnetIPHeader_1.KNXnetIPHeader(msg.serviceType, 0);
231
+ const connHeader = Buffer.from([
232
+ 0x04,
233
+ this.channelId,
234
+ this.pendingAck.seq,
235
+ 0x00,
236
+ ]);
237
+ header.totalLength = 6 + connHeader.length + msg.packet.length;
238
+ const packet = Buffer.concat([header.toBuffer(), connHeader, msg.packet]);
239
+ this.logger.warn(`ACK timeout for seq ${this.pendingAck.seq}, retrying (1/1)...`);
240
+ this.sendRaw(packet);
241
+ this.pendingAck.timer = setTimeout(() => this.handleAckTimeout(), 1000);
242
+ }
243
+ else {
244
+ // Fail (Spec 2.6.1: terminate connection)
245
+ const reject = this.pendingAck.currentMsg.reject;
246
+ this.logger.error(`ACK timeout failed after retry for seq ${this.pendingAck.seq}. Terminating connection.`);
247
+ this.pendingAck = null;
248
+ this.isSending = false;
249
+ reject(new Error("Tunneling ACK Timeout"));
250
+ this.disconnect();
251
+ }
252
+ }
253
+ // #endregion
254
+ // #region Tunneling Features
255
+ async getFeature(featureId) {
256
+ if (!this.isConnected)
257
+ throw new Error("Not connected");
258
+ if (this.msgQueue.length >= this.MAX_QUEUE_SIZE) {
259
+ throw new Error("Outgoing queue full");
260
+ }
261
+ return new Promise((resolve, reject) => {
262
+ const body = Buffer.from([featureId, 0x00]); // FeatureID + Reserved
263
+ this.msgQueue.push({
264
+ packet: body,
265
+ serviceType: KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_GET,
266
+ resolve,
267
+ reject,
268
+ responseType: KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_RESPONSE,
269
+ });
270
+ this.processQueue();
271
+ });
272
+ }
273
+ // #endregion
274
+ handleMessage(msg) {
275
+ this.emit("raw_message", msg);
276
+ try {
277
+ const header = KNXnetIPHeader_1.KNXnetIPHeader.fromBuffer(msg);
278
+ const body = msg.subarray(6);
279
+ switch (header.serviceType) {
280
+ case KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_RESPONSE:
281
+ const status = body[1];
282
+ if (status === KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR) {
283
+ this.channelId = body[0];
284
+ this.sequenceNumber = 0;
285
+ this.rxSequenceNumber = 0;
286
+ this.isConnected = true;
287
+ // Parse CRD
288
+ if (body.length >= 14) {
289
+ const crd = KNXnetIPStructures_1.CRD.fromBuffer(body.subarray(10));
290
+ this.assignedAddress = crd.knxAddress;
291
+ this.emit("connected", {
292
+ channelId: this.channelId,
293
+ assignedAddress: crd.knxAddress,
294
+ });
295
+ }
296
+ else {
297
+ this.emit("connected", { channelId: this.channelId });
298
+ }
299
+ this.startHeartbeat();
300
+ }
301
+ else {
302
+ this.emit("error", new Error(`Connect Error: 0x${status.toString(16)}`));
303
+ }
304
+ break;
305
+ case KNXnetIPEnum_1.KNXnetIPServiceType.CONNECTIONSTATE_RESPONSE:
306
+ if (body[0] === this.channelId) {
307
+ if (body[1] === KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR) {
308
+ this.heartbeatFailures = 0;
309
+ if (this.heartbeatRetryTimer) {
310
+ clearTimeout(this.heartbeatRetryTimer);
311
+ this.heartbeatRetryTimer = null;
312
+ }
313
+ }
314
+ else {
315
+ this.logger.warn(`Heartbeat response error from server: 0x${body[1].toString(16)}`);
316
+ // If it's a connection ID error, we should probably disconnect
317
+ if (body[1] === KNXnetIPEnum_1.KNXnetIPErrorCodes.E_CONNECTION_ID) {
318
+ this.emit("error", new Error("Connection ID no longer valid on server"));
319
+ this.disconnect();
320
+ }
321
+ }
322
+ }
323
+ break;
324
+ case KNXnetIPEnum_1.KNXnetIPServiceType.CONNECTIONSTATE_REQUEST:
325
+ if (body[0] === this.channelId) {
326
+ const respHeader = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECTIONSTATE_RESPONSE, 0);
327
+ const respBody = Buffer.from([
328
+ this.channelId,
329
+ KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR,
330
+ ]);
331
+ respHeader.totalLength = 6 + respBody.length;
332
+ this.sendRaw(Buffer.concat([respHeader.toBuffer(), respBody]));
333
+ }
334
+ break;
335
+ case KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST:
336
+ this.handleRequest(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_ACK);
337
+ break;
338
+ case KNXnetIPEnum_1.KNXnetIPServiceType.DEVICE_CONFIGURATION_REQUEST:
339
+ this.handleRequest(body, KNXnetIPEnum_1.KNXnetIPServiceType.DEVICE_CONFIGURATION_ACK);
340
+ break;
341
+ case KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_ACK:
342
+ case KNXnetIPEnum_1.KNXnetIPServiceType.DEVICE_CONFIGURATION_ACK:
343
+ if (this.pendingAck && body[2] === this.pendingAck.seq) {
344
+ const status = body[3];
345
+ if (status !== KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR) {
346
+ this.logger.error(`Received ACK with error status: 0x${status.toString(16)}. Terminating.`);
347
+ if (this.activeRequest)
348
+ this.activeRequest.reject(new Error(`ACK Error: 0x${status.toString(16)}`));
349
+ clearTimeout(this.pendingAck.timer);
350
+ this.pendingAck = null;
351
+ this.isSending = false;
352
+ this.activeRequest = null;
353
+ this.disconnect();
354
+ return;
355
+ }
356
+ clearTimeout(this.pendingAck.timer);
357
+ this.pendingAck = null;
358
+ if (!this.activeRequest?.responseType) {
359
+ this.isSending = false;
360
+ if (this.activeRequest)
361
+ this.activeRequest.resolve();
362
+ this.activeRequest = null;
363
+ this.sequenceNumber = (this.sequenceNumber + 1) & 0xff;
364
+ this.processQueue();
365
+ }
366
+ else {
367
+ this.logger.debug(`ACK received for seq ${this.sequenceNumber}, waiting for response type 0x${this.activeRequest.responseType.toString(16)}`);
368
+ }
369
+ }
370
+ break;
371
+ case KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_RESPONSE:
372
+ // Body: ConnHeader(4) + FeatureID(1) + ReturnCode(1) + Value(n)
373
+ if (this.isSending &&
374
+ this.activeRequest?.responseType ===
375
+ KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_RESPONSE) {
376
+ if (body[0] === 0x04 &&
377
+ body[1] === this.channelId &&
378
+ body[2] === this.sequenceNumber) {
379
+ const returnCode = body[5];
380
+ const val = body.subarray(6);
381
+ const resolve = this.activeRequest.resolve;
382
+ const reject = this.activeRequest.reject;
383
+ this.isSending = false;
384
+ this.activeRequest = null;
385
+ this.sequenceNumber = (this.sequenceNumber + 1) & 0xff;
386
+ if (returnCode === KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR) {
387
+ resolve(val);
388
+ }
389
+ else {
390
+ reject(new Error(`Feature Error: 0x${returnCode.toString(16)}`));
391
+ }
392
+ this.processQueue();
393
+ }
394
+ }
395
+ break;
396
+ case KNXnetIPEnum_1.KNXnetIPServiceType.DISCONNECT_REQUEST:
397
+ // Server closed connection
398
+ this.closeSocket();
399
+ break;
400
+ case KNXnetIPEnum_1.KNXnetIPServiceType.DISCONNECT_RESPONSE:
401
+ this.closeSocket();
402
+ break;
403
+ case KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_INFO:
404
+ // Body: ConnHeader(4) + FeatureID(1) + Len(1) + Value...
405
+ if (body[0] === 0x04 && body[1] === this.channelId) {
406
+ // Check Conn Header length & Channel ID
407
+ const featureId = body[4];
408
+ const val = body.subarray(6);
409
+ this.emit("feature_info", featureId, val);
410
+ }
411
+ break;
412
+ }
413
+ }
414
+ catch (e) {
415
+ this.emit("error", e);
416
+ }
417
+ }
418
+ handleRequest(body, ackType) {
419
+ const seq = body[2];
420
+ // Check for sequence number
421
+ if (seq === this.rxSequenceNumber) {
422
+ // Correct sequence
423
+ this.sendAck(ackType, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR);
424
+ this.rxSequenceNumber = (this.rxSequenceNumber + 1) & 0xff;
425
+ try {
426
+ const len = body[0]; // Connection Header Length
427
+ const data = body.subarray(len);
428
+ const cemi = CEMI_1.CEMI.fromBuffer(data);
429
+ this.emit("indication", cemi);
430
+ this.emit("raw_indication", data);
431
+ }
432
+ catch (e) { }
433
+ }
434
+ else if (seq === ((this.rxSequenceNumber - 1) & 0xff)) {
435
+ // Duplicate frame, send ACK again but don't process
436
+ this.sendAck(ackType, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR);
437
+ }
438
+ else {
439
+ // Out of sequence, discard (TCP handles this mostly, but for UDP/Tunneling logic)
440
+ // Do not ACK
441
+ }
442
+ }
443
+ sendAck(type, seq, status) {
444
+ const header = new KNXnetIPHeader_1.KNXnetIPHeader(type, 0);
445
+ const body = Buffer.from([0x04, this.channelId, seq, status]);
446
+ header.totalLength = 6 + body.length;
447
+ this.sendRaw(Buffer.concat([header.toBuffer(), body]));
448
+ }
449
+ sendRaw(buffer) {
450
+ if (!this.socket)
451
+ return;
452
+ if (this._transport === "UDP") {
453
+ this.socket.send(buffer, this.options.port, this.options.ip);
454
+ }
455
+ else {
456
+ this.socket.write(buffer);
457
+ }
458
+ }
459
+ startHeartbeat() {
460
+ if (this.heartbeatTimer)
461
+ clearInterval(this.heartbeatTimer);
462
+ this.heartbeatFailures = 0;
463
+ // Check every 60s (as per spec)
464
+ this.heartbeatTimer = setInterval(() => {
465
+ this.sendHeartbeatRequest();
466
+ }, 60000);
467
+ }
468
+ sendHeartbeatRequest() {
469
+ const localPort = this._transport === "UDP"
470
+ ? this.socket.address().port
471
+ : this.socket.localPort;
472
+ const useRouteBack = this.options.useRouteBack;
473
+ const hpai = new KNXnetIPStructures_1.HPAI(this._transport === "TCP"
474
+ ? KNXnetIPEnum_1.HostProtocolCode.IPV4_TCP
475
+ : KNXnetIPEnum_1.HostProtocolCode.IPV4_UDP, useRouteBack ? "0.0.0.0" : this.options.localIp, useRouteBack ? 0 : localPort);
476
+ const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECTIONSTATE_REQUEST, 0);
477
+ const body = Buffer.concat([
478
+ Buffer.from([this.channelId, 0x00]),
479
+ hpai.toBuffer(),
480
+ ]);
481
+ header.totalLength = 6 + body.length;
482
+ this.sendRaw(Buffer.concat([header.toBuffer(), body]));
483
+ // Check timeout in 10s (spec recommendation)
484
+ if (this.heartbeatRetryTimer)
485
+ clearTimeout(this.heartbeatRetryTimer);
486
+ this.heartbeatRetryTimer = setTimeout(() => this.handleHeartbeatTimeout(), 10000);
487
+ }
488
+ handleHeartbeatTimeout() {
489
+ this.heartbeatFailures++;
490
+ this.logger.warn(`Heartbeat timeout (${this.heartbeatFailures}/3)`);
491
+ if (this.heartbeatFailures >= 3) {
492
+ this.emit("error", new Error("Heartbeat failed 3 times"));
493
+ this.disconnect();
494
+ }
495
+ else {
496
+ // Immediate retry
497
+ this.sendHeartbeatRequest();
498
+ }
499
+ }
500
+ stopHeartbeat() {
501
+ if (this.heartbeatTimer)
502
+ clearInterval(this.heartbeatTimer);
503
+ if (this.heartbeatRetryTimer)
504
+ clearTimeout(this.heartbeatRetryTimer);
505
+ this.heartbeatTimer = null;
506
+ this.heartbeatRetryTimer = null;
507
+ }
508
+ }
509
+ exports.KNXTunneling = KNXTunneling;
@@ -0,0 +1,64 @@
1
+ import { KNXService } from "./KNXService";
2
+ import { ServiceMessage } from "../@types/interfaces/ServiceMessage";
3
+ import { KNXnetIPServerOptions } from "../@types/interfaces/connection";
4
+ /**
5
+ * Implements a KNXnet/IP Server (Gateway) that supports Routing and Tunneling protocols.
6
+ * This class handles device discovery (Search/Description), manages multiple concurrent
7
+ * tunneling connections, and bridges communication between IP multicast (Routing) and
8
+ * point-to-point (Tunneling) clients. It includes implementation for flow control
9
+ * (RoutingBusy), rate limiting, and echo cancellation.
10
+ */
11
+ export declare class KNXnetIPServer extends KNXService {
12
+ private isRoutingBusy;
13
+ private routingBusyTimer;
14
+ private msgQueue;
15
+ private isProcessingQueue;
16
+ private lastSentTime;
17
+ private busyCounter;
18
+ private lastBusyTime;
19
+ private decrementTimer;
20
+ private decrementInterval;
21
+ private serverIAInt;
22
+ private _tunnelConnections;
23
+ private readonly MAX_QUEUE_SIZE;
24
+ private readonly BUSY_THRESHOLD;
25
+ private readonly HEARTBEAT_TIMEOUT;
26
+ private readonly RETRANSMIT_TIMEOUT;
27
+ private MAX_PENDING_REQUESTS_PER_CLIENT;
28
+ private maxTunnelConnections;
29
+ private clientAddrsStartInt;
30
+ private externalManager;
31
+ constructor(options: KNXnetIPServerOptions);
32
+ get individualAddress(): string;
33
+ connect(): Promise<void>;
34
+ disconnect(): void;
35
+ private resolveRouteBack;
36
+ private clearTimers;
37
+ send(data: Buffer | ServiceMessage): Promise<void>;
38
+ sendRaw(cemiBuffer: Buffer): Promise<void>;
39
+ private enqueuePacket;
40
+ private sendLostMessage;
41
+ private sendRoutingBusy;
42
+ private processQueue;
43
+ private handleMessage;
44
+ private handleTunnelingAck;
45
+ private handleSearchRequest;
46
+ private handleDescriptionRequest;
47
+ private getHPAI;
48
+ private handleConnectRequest;
49
+ private resetHeartbeat;
50
+ private closeConnection;
51
+ private handleConnectionStateRequest;
52
+ private handleDisconnectRequest;
53
+ private handleTunnelingRequest;
54
+ private sendTunnelACK;
55
+ private handleTunnelingFeatureGet;
56
+ private handleDeviceConfigurationRequest;
57
+ private handleDeviceConfigAck;
58
+ private sendDeviceConfigACK;
59
+ private getIdentificationDIBs;
60
+ private handleRoutingBusy;
61
+ private resetDecrementTimer;
62
+ private pauseSending;
63
+ private convertDataIndToBusmonInd;
64
+ }