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.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/@types/interfaces/DPTs.d.ts +144 -0
- package/dist/@types/interfaces/DPTs.js +3 -0
- package/dist/@types/interfaces/EMI.d.ts +396 -0
- package/dist/@types/interfaces/EMI.js +2 -0
- package/dist/@types/interfaces/ServiceMessage.d.ts +11 -0
- package/dist/@types/interfaces/ServiceMessage.js +2 -0
- package/dist/@types/interfaces/SystemStatus.d.ts +18 -0
- package/dist/@types/interfaces/SystemStatus.js +2 -0
- package/dist/@types/interfaces/connection.d.ts +139 -0
- package/dist/@types/interfaces/connection.js +2 -0
- package/dist/@types/interfaces/localEndPoint.d.ts +5 -0
- package/dist/@types/interfaces/localEndPoint.js +2 -0
- package/dist/@types/types/AllDpts.d.ts +3 -0
- package/dist/@types/types/AllDpts.js +3 -0
- package/dist/@types/types/DecodedDPTType.d.ts +21 -0
- package/dist/@types/types/DecodedDPTType.js +2 -0
- package/dist/connection/KNXService.d.ts +58 -0
- package/dist/connection/KNXService.js +242 -0
- package/dist/connection/KNXTunneling.d.ts +44 -0
- package/dist/connection/KNXTunneling.js +509 -0
- package/dist/connection/KNXnetIPServer.d.ts +64 -0
- package/dist/connection/KNXnetIPServer.js +900 -0
- package/dist/connection/Router.d.ts +49 -0
- package/dist/connection/Router.js +269 -0
- package/dist/connection/TPUART.d.ts +32 -0
- package/dist/connection/TPUART.js +497 -0
- package/dist/connection/TunnelConnection.d.ts +57 -0
- package/dist/connection/TunnelConnection.js +167 -0
- package/dist/core/CEMI.d.ts +1130 -0
- package/dist/core/CEMI.js +1281 -0
- package/dist/core/ControlField.d.ts +57 -0
- package/dist/core/ControlField.js +120 -0
- package/dist/core/ControlFieldExtended.d.ts +56 -0
- package/dist/core/ControlFieldExtended.js +114 -0
- package/dist/core/EMI.d.ts +2515 -0
- package/dist/core/EMI.js +3898 -0
- package/dist/core/KNXAddInfoTypes.d.ts +225 -0
- package/dist/core/KNXAddInfoTypes.js +602 -0
- package/dist/core/KNXnetIPHeader.d.ts +10 -0
- package/dist/core/KNXnetIPHeader.js +38 -0
- package/dist/core/KNXnetIPStructures.d.ts +179 -0
- package/dist/core/KNXnetIPStructures.js +622 -0
- package/dist/core/MessageCodeField.d.ts +886 -0
- package/dist/core/MessageCodeField.js +399 -0
- package/dist/core/SystemStatus.d.ts +144 -0
- package/dist/core/SystemStatus.js +325 -0
- package/dist/core/data/KNXData.d.ts +7 -0
- package/dist/core/data/KNXData.js +30 -0
- package/dist/core/data/KNXDataDecode.d.ts +396 -0
- package/dist/core/data/KNXDataDecode.js +1186 -0
- package/dist/core/data/KNXDataEncode.d.ts +332 -0
- package/dist/core/data/KNXDataEncode.js +1504 -0
- package/dist/core/enum/APCIEnum.d.ts +587 -0
- package/dist/core/enum/APCIEnum.js +591 -0
- package/dist/core/enum/EnumControlField.d.ts +24 -0
- package/dist/core/enum/EnumControlField.js +36 -0
- package/dist/core/enum/EnumControlFieldExtended.d.ts +36 -0
- package/dist/core/enum/EnumControlFieldExtended.js +41 -0
- package/dist/core/enum/EnumShortACKFrame.d.ts +6 -0
- package/dist/core/enum/EnumShortACKFrame.js +10 -0
- package/dist/core/enum/ErrorCodeSet.d.ts +57 -0
- package/dist/core/enum/ErrorCodeSet.js +2 -0
- package/dist/core/enum/KNXnetIPEnum.d.ts +95 -0
- package/dist/core/enum/KNXnetIPEnum.js +90 -0
- package/dist/core/enum/SAP.d.ts +19 -0
- package/dist/core/enum/SAP.js +23 -0
- package/dist/core/layers/data/APDU.d.ts +38 -0
- package/dist/core/layers/data/APDU.js +115 -0
- package/dist/core/layers/data/NPDU.d.ts +73 -0
- package/dist/core/layers/data/NPDU.js +103 -0
- package/dist/core/layers/data/TPDU.d.ts +53 -0
- package/dist/core/layers/data/TPDU.js +73 -0
- package/dist/core/layers/interfaces/APCI.d.ts +61 -0
- package/dist/core/layers/interfaces/APCI.js +92 -0
- package/dist/core/layers/interfaces/TPCI.d.ts +110 -0
- package/dist/core/layers/interfaces/TPCI.js +196 -0
- package/dist/core/resources/DeviceDescriptorType.d.ts +46 -0
- package/dist/core/resources/DeviceDescriptorType.js +69 -0
- package/dist/errors/DPTNotFound.d.ts +6 -0
- package/dist/errors/DPTNotFound.js +15 -0
- package/dist/errors/InvalidKnxAddresExeption.d.ts +3 -0
- package/dist/errors/InvalidKnxAddresExeption.js +9 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +18 -0
- package/dist/utils/CEMIAdapter.d.ts +16 -0
- package/dist/utils/CEMIAdapter.js +94 -0
- package/dist/utils/KNXHelper.d.ts +78 -0
- package/dist/utils/KNXHelper.js +338 -0
- package/dist/utils/Logger.d.ts +17 -0
- package/dist/utils/Logger.js +96 -0
- package/dist/utils/MessageCodeTranslator.d.ts +19 -0
- package/dist/utils/MessageCodeTranslator.js +77 -0
- package/dist/utils/checksumFrame.d.ts +18 -0
- package/dist/utils/checksumFrame.js +41 -0
- package/dist/utils/localIp.d.ts +7 -0
- package/dist/utils/localIp.js +45 -0
- 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
|
+
}
|