knx.ts 1.0.2 → 1.0.4
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 +51 -21
- package/README.md +274 -61
- package/dist/@types/interfaces/connection.d.ts +80 -13
- package/dist/@types/interfaces/servers.d.ts +18 -0
- package/dist/@types/interfaces/servers.js +2 -0
- package/dist/connection/KNXService.d.ts +13 -30
- package/dist/connection/KNXService.js +4 -164
- package/dist/connection/KNXTunneling.d.ts +4 -4
- package/dist/connection/KNXTunneling.js +35 -62
- package/dist/connection/KNXUSBConnection.d.ts +20 -0
- package/dist/connection/KNXUSBConnection.js +358 -0
- package/dist/connection/KNXnetIPServer.d.ts +29 -12
- package/dist/connection/KNXnetIPServer.js +261 -83
- package/dist/connection/Router.d.ts +52 -32
- package/dist/connection/Router.js +225 -153
- package/dist/connection/TPUART.d.ts +8 -3
- package/dist/connection/TPUART.js +41 -37
- package/dist/connection/TunnelConnection.d.ts +3 -1
- package/dist/connection/TunnelConnection.js +6 -4
- package/dist/core/CEMI.d.ts +7 -2
- package/dist/core/CEMI.js +5 -8
- package/dist/core/EMI.d.ts +312 -200
- package/dist/core/EMI.js +511 -1007
- package/dist/core/KNXnetIPStructures.d.ts +10 -1
- package/dist/core/KNXnetIPStructures.js +15 -10
- package/dist/core/MessageCodeField.d.ts +1 -1
- package/dist/core/cache/GroupAddressCache.d.ts +57 -0
- package/dist/core/cache/GroupAddressCache.js +227 -0
- package/dist/core/data/KNXDataDecode.d.ts +2 -2
- package/dist/core/data/KNXDataDecode.js +198 -183
- package/dist/core/enum/EnumControlField.d.ts +0 -5
- package/dist/core/enum/EnumControlField.js +1 -7
- package/dist/core/enum/EnumControlFieldExtended.d.ts +1 -1
- package/dist/core/enum/EnumShortACKFrame.d.ts +1 -1
- package/dist/core/enum/ErrorCodeSet.js +59 -0
- package/dist/core/enum/KNXnetIPEnum.d.ts +2 -2
- package/dist/core/enum/KNXnetIPEnum.js +19 -1
- package/dist/core/layers/data/NPDU.d.ts +2 -1
- package/dist/core/layers/data/NPDU.js +6 -3
- package/dist/index.d.ts +19 -2
- package/dist/index.js +36 -1
- package/dist/server/KNXMQTTGateway.d.ts +13 -0
- package/dist/server/KNXMQTTGateway.js +164 -0
- package/dist/server/KNXWebSocketServer.d.ts +12 -0
- package/dist/server/KNXWebSocketServer.js +118 -0
- package/dist/utils/CEMIAdapter.d.ts +4 -3
- package/dist/utils/CEMIAdapter.js +26 -30
- package/dist/utils/Logger.d.ts +4 -4
- package/dist/utils/Logger.js +3 -7
- package/package.json +27 -7
|
@@ -11,6 +11,7 @@ const KNXnetIPHeader_1 = require("../core/KNXnetIPHeader");
|
|
|
11
11
|
const KNXnetIPStructures_1 = require("../core/KNXnetIPStructures");
|
|
12
12
|
const KNXnetIPEnum_1 = require("../core/enum/KNXnetIPEnum");
|
|
13
13
|
const CEMI_1 = require("../core/CEMI");
|
|
14
|
+
const KNXHelper_1 = require("../utils/KNXHelper");
|
|
14
15
|
/**
|
|
15
16
|
* Handles KNXnet/IP Tunneling connections for point-to-point communication with a KNX gateway.
|
|
16
17
|
* This class manages the connection state, sequence numbering for reliable delivery,
|
|
@@ -22,7 +23,7 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
22
23
|
rxSequenceNumber = 0;
|
|
23
24
|
isConnected = false;
|
|
24
25
|
tcpBuffer = Buffer.alloc(0);
|
|
25
|
-
|
|
26
|
+
individualAddress = "1.0.1"; // Assigned Individual Address
|
|
26
27
|
// Heartbeat
|
|
27
28
|
heartbeatTimer = null;
|
|
28
29
|
heartbeatFailures = 0;
|
|
@@ -39,8 +40,7 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
39
40
|
super(options);
|
|
40
41
|
this._transport = options.transport || "UDP";
|
|
41
42
|
if (!this.options.connectionType) {
|
|
42
|
-
this.options.connectionType =
|
|
43
|
-
KNXnetIPEnum_1.ConnectionType.TUNNEL_CONNECTION;
|
|
43
|
+
this.options.connectionType = KNXnetIPEnum_1.ConnectionType.TUNNEL_CONNECTION;
|
|
44
44
|
}
|
|
45
45
|
this.MAX_QUEUE_SIZE = options.maxQueueSize || 100;
|
|
46
46
|
this.logger = this.logger.child({ module: "TunnelClient" });
|
|
@@ -69,7 +69,7 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
69
69
|
this.removeListener("connected", successListener);
|
|
70
70
|
reject(err);
|
|
71
71
|
};
|
|
72
|
-
const successListener = (
|
|
72
|
+
const successListener = () => {
|
|
73
73
|
this.removeListener("error", errorListener); // Limpiamos el listener de error temporal
|
|
74
74
|
resolve();
|
|
75
75
|
};
|
|
@@ -117,23 +117,14 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
sendConnectRequest() {
|
|
120
|
-
const localPort = this._transport === "UDP"
|
|
121
|
-
? this.socket.address().port
|
|
122
|
-
: this.socket.localPort;
|
|
120
|
+
const localPort = this._transport === "UDP" ? this.socket.address().port : this.socket.localPort;
|
|
123
121
|
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
|
|
122
|
+
const hpai = new KNXnetIPStructures_1.HPAI(this._transport === "TCP" ? KNXnetIPEnum_1.HostProtocolCode.IPV4_TCP : KNXnetIPEnum_1.HostProtocolCode.IPV4_UDP, useRouteBack ? "0.0.0.0" : this.options.localIp, useRouteBack ? 0 : localPort);
|
|
128
123
|
const cri = new KNXnetIPStructures_1.CRI(this.options.connectionType);
|
|
129
124
|
const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_REQUEST, 0);
|
|
130
125
|
// CORRECCIÓN
|
|
131
126
|
// Estructura: HPAI (Control) -> HPAI (Data) -> CRI
|
|
132
|
-
const body = Buffer.concat([
|
|
133
|
-
hpai.toBuffer(),
|
|
134
|
-
hpai.toBuffer(),
|
|
135
|
-
cri.toBuffer(),
|
|
136
|
-
]);
|
|
127
|
+
const body = Buffer.concat([hpai.toBuffer(), hpai.toBuffer(), cri.toBuffer()]);
|
|
137
128
|
header.totalLength = 6 + body.length;
|
|
138
129
|
this.sendRaw(Buffer.concat([header.toBuffer(), body]));
|
|
139
130
|
}
|
|
@@ -143,14 +134,9 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
143
134
|
? this.socket.address().port
|
|
144
135
|
: this.socket.localPort;
|
|
145
136
|
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);
|
|
137
|
+
const hpai = new KNXnetIPStructures_1.HPAI(this._transport === "TCP" ? KNXnetIPEnum_1.HostProtocolCode.IPV4_TCP : KNXnetIPEnum_1.HostProtocolCode.IPV4_UDP, useRouteBack ? "0.0.0.0" : this.options.localIp, useRouteBack ? 0 : localPort);
|
|
149
138
|
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
|
-
]);
|
|
139
|
+
const body = Buffer.concat([Buffer.from([this.channelId, 0x00]), hpai.toBuffer()]);
|
|
154
140
|
header.totalLength = 6 + body.length;
|
|
155
141
|
this.sendRaw(Buffer.concat([header.toBuffer(), body]));
|
|
156
142
|
// Graceful disconnect: Wait for response or timeout (1s)
|
|
@@ -186,9 +172,9 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
186
172
|
if (this.msgQueue.length >= this.MAX_QUEUE_SIZE) {
|
|
187
173
|
throw new Error("Outgoing queue full");
|
|
188
174
|
}
|
|
175
|
+
this.emit("send", cemi);
|
|
189
176
|
const cemiBuffer = Buffer.isBuffer(cemi) ? cemi : cemi.toBuffer();
|
|
190
|
-
const isDeviceMgmt = this.options.connectionType ===
|
|
191
|
-
KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION;
|
|
177
|
+
const isDeviceMgmt = this.options.connectionType === KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION;
|
|
192
178
|
const serviceType = isDeviceMgmt
|
|
193
179
|
? KNXnetIPEnum_1.KNXnetIPServiceType.DEVICE_CONFIGURATION_REQUEST
|
|
194
180
|
: KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST;
|
|
@@ -204,12 +190,7 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
204
190
|
const msg = this.msgQueue.shift();
|
|
205
191
|
this.activeRequest = msg;
|
|
206
192
|
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
|
-
]);
|
|
193
|
+
const connHeader = Buffer.from([0x04, this.channelId, this.sequenceNumber, 0x00]);
|
|
213
194
|
header.totalLength = 6 + connHeader.length + msg.packet.length;
|
|
214
195
|
const packet = Buffer.concat([header.toBuffer(), connHeader, msg.packet]);
|
|
215
196
|
this.pendingAck = {
|
|
@@ -228,12 +209,7 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
228
209
|
this.pendingAck.retryCount++;
|
|
229
210
|
const msg = this.pendingAck.currentMsg;
|
|
230
211
|
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
|
-
]);
|
|
212
|
+
const connHeader = Buffer.from([0x04, this.channelId, this.pendingAck.seq, 0x00]);
|
|
237
213
|
header.totalLength = 6 + connHeader.length + msg.packet.length;
|
|
238
214
|
const packet = Buffer.concat([header.toBuffer(), connHeader, msg.packet]);
|
|
239
215
|
this.logger.warn(`ACK timeout for seq ${this.pendingAck.seq}, retrying (1/1)...`);
|
|
@@ -277,7 +253,7 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
277
253
|
const header = KNXnetIPHeader_1.KNXnetIPHeader.fromBuffer(msg);
|
|
278
254
|
const body = msg.subarray(6);
|
|
279
255
|
switch (header.serviceType) {
|
|
280
|
-
case KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_RESPONSE:
|
|
256
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_RESPONSE: {
|
|
281
257
|
const status = body[1];
|
|
282
258
|
if (status === KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR) {
|
|
283
259
|
this.channelId = body[0];
|
|
@@ -287,10 +263,10 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
287
263
|
// Parse CRD
|
|
288
264
|
if (body.length >= 14) {
|
|
289
265
|
const crd = KNXnetIPStructures_1.CRD.fromBuffer(body.subarray(10));
|
|
290
|
-
this.
|
|
266
|
+
this.individualAddress = KNXHelper_1.KNXHelper.GetAddress(crd.knxAddress, ".");
|
|
291
267
|
this.emit("connected", {
|
|
292
268
|
channelId: this.channelId,
|
|
293
|
-
|
|
269
|
+
individualAddress: this.individualAddress,
|
|
294
270
|
});
|
|
295
271
|
}
|
|
296
272
|
else {
|
|
@@ -302,6 +278,7 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
302
278
|
this.emit("error", new Error(`Connect Error: 0x${status.toString(16)}`));
|
|
303
279
|
}
|
|
304
280
|
break;
|
|
281
|
+
}
|
|
305
282
|
case KNXnetIPEnum_1.KNXnetIPServiceType.CONNECTIONSTATE_RESPONSE:
|
|
306
283
|
if (body[0] === this.channelId) {
|
|
307
284
|
if (body[1] === KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR) {
|
|
@@ -324,10 +301,7 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
324
301
|
case KNXnetIPEnum_1.KNXnetIPServiceType.CONNECTIONSTATE_REQUEST:
|
|
325
302
|
if (body[0] === this.channelId) {
|
|
326
303
|
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
|
-
]);
|
|
304
|
+
const respBody = Buffer.from([this.channelId, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR]);
|
|
331
305
|
respHeader.totalLength = 6 + respBody.length;
|
|
332
306
|
this.sendRaw(Buffer.concat([respHeader.toBuffer(), respBody]));
|
|
333
307
|
}
|
|
@@ -370,12 +344,8 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
370
344
|
break;
|
|
371
345
|
case KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_RESPONSE:
|
|
372
346
|
// Body: ConnHeader(4) + FeatureID(1) + ReturnCode(1) + Value(n)
|
|
373
|
-
if (this.isSending &&
|
|
374
|
-
this.
|
|
375
|
-
KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_RESPONSE) {
|
|
376
|
-
if (body[0] === 0x04 &&
|
|
377
|
-
body[1] === this.channelId &&
|
|
378
|
-
body[2] === this.sequenceNumber) {
|
|
347
|
+
if (this.isSending && this.activeRequest?.responseType === KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_FEATURE_RESPONSE) {
|
|
348
|
+
if (body[0] === 0x04 && body[1] === this.channelId && body[2] === this.sequenceNumber) {
|
|
379
349
|
const returnCode = body[5];
|
|
380
350
|
const val = body.subarray(6);
|
|
381
351
|
const resolve = this.activeRequest.resolve;
|
|
@@ -427,9 +397,15 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
427
397
|
const data = body.subarray(len);
|
|
428
398
|
const cemi = CEMI_1.CEMI.fromBuffer(data);
|
|
429
399
|
this.emit("indication", cemi);
|
|
400
|
+
if (!("destinationAddress" in cemi))
|
|
401
|
+
return;
|
|
402
|
+
this.emit(cemi.destinationAddress, cemi);
|
|
430
403
|
this.emit("raw_indication", data);
|
|
404
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
/* empty */
|
|
431
408
|
}
|
|
432
|
-
catch (e) { }
|
|
433
409
|
}
|
|
434
410
|
else if (seq === ((this.rxSequenceNumber - 1) & 0xff)) {
|
|
435
411
|
// Duplicate frame, send ACK again but don't process
|
|
@@ -450,7 +426,11 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
450
426
|
if (!this.socket)
|
|
451
427
|
return;
|
|
452
428
|
if (this._transport === "UDP") {
|
|
453
|
-
this.socket.send(buffer, this.options.port, this.options.ip)
|
|
429
|
+
this.socket.send(buffer, this.options.port, this.options.ip, (err) => {
|
|
430
|
+
if (err) {
|
|
431
|
+
this.emit("error", err);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
454
434
|
}
|
|
455
435
|
else {
|
|
456
436
|
this.socket.write(buffer);
|
|
@@ -466,18 +446,11 @@ class KNXTunneling extends KNXService_1.KNXService {
|
|
|
466
446
|
}, 60000);
|
|
467
447
|
}
|
|
468
448
|
sendHeartbeatRequest() {
|
|
469
|
-
const localPort = this._transport === "UDP"
|
|
470
|
-
? this.socket.address().port
|
|
471
|
-
: this.socket.localPort;
|
|
449
|
+
const localPort = this._transport === "UDP" ? this.socket.address().port : this.socket.localPort;
|
|
472
450
|
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);
|
|
451
|
+
const hpai = new KNXnetIPStructures_1.HPAI(this._transport === "TCP" ? KNXnetIPEnum_1.HostProtocolCode.IPV4_TCP : KNXnetIPEnum_1.HostProtocolCode.IPV4_UDP, useRouteBack ? "0.0.0.0" : this.options.localIp, useRouteBack ? 0 : localPort);
|
|
476
452
|
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
|
-
]);
|
|
453
|
+
const body = Buffer.concat([Buffer.from([this.channelId, 0x00]), hpai.toBuffer()]);
|
|
481
454
|
header.totalLength = 6 + body.length;
|
|
482
455
|
this.sendRaw(Buffer.concat([header.toBuffer(), body]));
|
|
483
456
|
// Check timeout in 10s (spec recommendation)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { KNXService } from "./KNXService";
|
|
2
|
+
import { CEMIInstance } from "../core/CEMI";
|
|
3
|
+
import { KNXUSBOptions } from "../@types/interfaces/connection";
|
|
4
|
+
export declare class KNXUSBConnection extends KNXService<KNXUSBOptions> {
|
|
5
|
+
private device;
|
|
6
|
+
private isConnected;
|
|
7
|
+
private busConnected;
|
|
8
|
+
private supportedEmiType;
|
|
9
|
+
constructor(options: KNXUSBOptions);
|
|
10
|
+
connect(): Promise<void>;
|
|
11
|
+
disconnect(): void;
|
|
12
|
+
private initializeDevice;
|
|
13
|
+
private discoverEmiType;
|
|
14
|
+
private sendUSBTransfer;
|
|
15
|
+
private sendHIDReport;
|
|
16
|
+
send(data: Buffer | CEMIInstance): Promise<void>;
|
|
17
|
+
private handleData;
|
|
18
|
+
private processTransferFrame;
|
|
19
|
+
private handleError;
|
|
20
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.KNXUSBConnection = void 0;
|
|
37
|
+
const hid = __importStar(require("node-hid"));
|
|
38
|
+
const KNXService_1 = require("./KNXService");
|
|
39
|
+
const CEMIAdapter_1 = require("../utils/CEMIAdapter");
|
|
40
|
+
const CEMI_1 = require("../core/CEMI");
|
|
41
|
+
class KNXUSBConnection extends KNXService_1.KNXService {
|
|
42
|
+
device = null;
|
|
43
|
+
isConnected = false;
|
|
44
|
+
busConnected = false;
|
|
45
|
+
supportedEmiType = 0x03;
|
|
46
|
+
constructor(options) {
|
|
47
|
+
super(options);
|
|
48
|
+
this.logger = this.logger.child({ module: "KNXUSBConnection" });
|
|
49
|
+
}
|
|
50
|
+
async connect() {
|
|
51
|
+
if (this.isConnected)
|
|
52
|
+
return;
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
try {
|
|
55
|
+
const options = this.options;
|
|
56
|
+
let devicePath = options.path;
|
|
57
|
+
if (!devicePath) {
|
|
58
|
+
const devices = hid.devices();
|
|
59
|
+
const knxDevice = devices.find((d) => (options.vendorId &&
|
|
60
|
+
d.vendorId === options.vendorId &&
|
|
61
|
+
options.productId &&
|
|
62
|
+
d.productId === options.productId) ||
|
|
63
|
+
d.vendorId === 0x28c2 || // Zennio
|
|
64
|
+
d.vendorId === 0x145c || // ABB/Busch-Jaeger
|
|
65
|
+
d.vendorId === 0x10a6 || // MDT
|
|
66
|
+
d.vendorId === 0x135e || // Siemens
|
|
67
|
+
d.vendorId === 0x0e77 || // Weinzierl/Siemens
|
|
68
|
+
d.vendorId === 0x147b || // Weinzierl
|
|
69
|
+
d.vendorId === 0x16d0 || // MCS
|
|
70
|
+
(d.product && d.product.toLowerCase().includes("knx")));
|
|
71
|
+
if (!knxDevice || !knxDevice.path) {
|
|
72
|
+
throw new Error("No KNX USB device found");
|
|
73
|
+
}
|
|
74
|
+
devicePath = knxDevice.path;
|
|
75
|
+
}
|
|
76
|
+
this.logger.info(`Opening KNX USB device at ${devicePath}`);
|
|
77
|
+
this.device = new hid.HID(devicePath);
|
|
78
|
+
this.device.on("data", (data) => {
|
|
79
|
+
this.handleData(data);
|
|
80
|
+
});
|
|
81
|
+
this.device.on("error", (err) => this.handleError(err));
|
|
82
|
+
this.isConnected = true;
|
|
83
|
+
this.busConnected = false;
|
|
84
|
+
this.initializeDevice()
|
|
85
|
+
.then(() => {
|
|
86
|
+
this.emit("connected");
|
|
87
|
+
resolve();
|
|
88
|
+
})
|
|
89
|
+
.catch((err) => {
|
|
90
|
+
this.logger.error("Failed to initialize KNX USB: " + err);
|
|
91
|
+
this.disconnect();
|
|
92
|
+
reject(err);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
this.logger.error("Failed to connect to KNX USB: " + err);
|
|
97
|
+
reject(err);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
disconnect() {
|
|
102
|
+
if (!this.isConnected || !this.device)
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
this.device.close();
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
// Ignore close errors
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
this.device = null;
|
|
113
|
+
this.isConnected = false;
|
|
114
|
+
this.busConnected = false;
|
|
115
|
+
this.emit("disconnected");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async initializeDevice() {
|
|
119
|
+
// 1. Discover supported EMI types FIRST before sending any resets that might lock up older interfaces
|
|
120
|
+
// knxd implementation uses up to 5 retries for EMI discovery
|
|
121
|
+
await this.discoverEmiType();
|
|
122
|
+
// 2. Set active EMI type to the discovered type
|
|
123
|
+
// protocolId: 0x0f (BusAccessServerFeatureService)
|
|
124
|
+
// emiId: 0x03 (service device feature set), feature: 0x05, value: this.supportedEmiType
|
|
125
|
+
await this.sendUSBTransfer(0x0f, 0x03, Buffer.from([0x05, this.supportedEmiType]));
|
|
126
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
127
|
+
if (this.supportedEmiType === 0x03) {
|
|
128
|
+
// 3. cEMI Specific Initialization
|
|
129
|
+
// Send Reset Request (M_RESET_REQ = 0xF1)
|
|
130
|
+
await this.sendUSBTransfer(0x01, 0x03, Buffer.from([0xf1]));
|
|
131
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
132
|
+
// Set Comm Mode (PID_COMM_MODE)
|
|
133
|
+
// M_PROP_WRITE_REQ (0xF6), ObjType (0x0008), ObjInst (0x01), PropId (0x34), Elements (1) + StartIdx (1) -> 0x1001, Mode: 0x00 (DataLinkLayer)
|
|
134
|
+
const commModeBuf = Buffer.from([
|
|
135
|
+
0xf6, // M_PROP_WRITE_REQ
|
|
136
|
+
0x00,
|
|
137
|
+
0x08, // Interface Object
|
|
138
|
+
0x01, // Object Instance
|
|
139
|
+
0x34, // Property ID (52)
|
|
140
|
+
0x10,
|
|
141
|
+
0x01, // Elements + Start Index
|
|
142
|
+
0x00, // Data (DataLinkLayer, 0x00)
|
|
143
|
+
]);
|
|
144
|
+
await this.sendUSBTransfer(0x01, 0x03, commModeBuf);
|
|
145
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async discoverEmiType() {
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
let attempts = 0;
|
|
151
|
+
let timeout;
|
|
152
|
+
const attemptDiscovery = () => {
|
|
153
|
+
attempts++;
|
|
154
|
+
this.sendUSBTransfer(0x0f, 0x01, Buffer.from([0x01])).catch((e) => {
|
|
155
|
+
this.logger.error("Error sending EMI discovery: " + e);
|
|
156
|
+
});
|
|
157
|
+
timeout = setTimeout(() => {
|
|
158
|
+
if (attempts < 5) {
|
|
159
|
+
attemptDiscovery();
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
this.removeListener("emi_discovery", onDiscovery);
|
|
163
|
+
this.logger.warn("EMI discovery timeout, defaulting to cEMI");
|
|
164
|
+
this.supportedEmiType = 0x03;
|
|
165
|
+
resolve();
|
|
166
|
+
}
|
|
167
|
+
}, 1000); // 1 second timeout for each retry like knxd
|
|
168
|
+
};
|
|
169
|
+
const onDiscovery = (version) => {
|
|
170
|
+
clearTimeout(timeout);
|
|
171
|
+
this.supportedEmiType = version;
|
|
172
|
+
this.logger.info(`Discovered EMI version: ${version === 0x03 ? "cEMI" : version === 0x01 ? "EMI1" : "EMI2"}`);
|
|
173
|
+
resolve();
|
|
174
|
+
};
|
|
175
|
+
this.once("emi_discovery", onDiscovery);
|
|
176
|
+
attemptDiscovery();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async sendUSBTransfer(protocolId, emiId, data) {
|
|
180
|
+
if (!this.device)
|
|
181
|
+
throw new Error("Device offline");
|
|
182
|
+
const header = Buffer.alloc(8);
|
|
183
|
+
header[0] = 0x00; // protocol version
|
|
184
|
+
header[1] = 0x08; // header length
|
|
185
|
+
header.writeUInt16BE(data.length, 2); // body length
|
|
186
|
+
header[4] = protocolId;
|
|
187
|
+
header[5] = emiId;
|
|
188
|
+
header.writeUInt16BE(0x0000, 6); // manufacturer code
|
|
189
|
+
const body = Buffer.concat([header, data]);
|
|
190
|
+
this.sendHIDReport(body);
|
|
191
|
+
}
|
|
192
|
+
sendHIDReport(data) {
|
|
193
|
+
if (!this.device)
|
|
194
|
+
return;
|
|
195
|
+
if (data.length > 61) {
|
|
196
|
+
this.logger.error("KNX USB: Frame too long for single packet, and knxd logic does not support fragmentation");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// knxd perfectly sizes each USB send to exactly 64 bytes padded with 0
|
|
200
|
+
const report = Buffer.alloc(64);
|
|
201
|
+
report[0] = 0x01; // Report ID
|
|
202
|
+
report[1] = 0x13; // Sequence=1, Type=3 (single-frame packet)
|
|
203
|
+
report[2] = data.length;
|
|
204
|
+
data.copy(report, 3);
|
|
205
|
+
try {
|
|
206
|
+
this.device.write(report);
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
this.logger.error("Failed to write to KNX USB device: " + e);
|
|
210
|
+
this.handleError(e);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async send(data) {
|
|
214
|
+
if (!this.isConnected || !this.device) {
|
|
215
|
+
throw new Error("KNX USB device offline");
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
let frame;
|
|
219
|
+
if (this.supportedEmiType === 0x03) {
|
|
220
|
+
if (Buffer.isBuffer(data)) {
|
|
221
|
+
frame = data;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
frame = data.toBuffer();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// EMI1 / EMI2 mode
|
|
229
|
+
let emiMsg = null;
|
|
230
|
+
if (Buffer.isBuffer(data)) {
|
|
231
|
+
frame = data;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
this.logger.debug("Converting cEMI to EMI");
|
|
235
|
+
emiMsg = CEMIAdapter_1.CEMIAdapter.cemiToEmi(data);
|
|
236
|
+
}
|
|
237
|
+
if (emiMsg) {
|
|
238
|
+
frame = emiMsg.toBuffer();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (frame) {
|
|
242
|
+
this.emit("send", frame);
|
|
243
|
+
await this.sendUSBTransfer(0x01, this.supportedEmiType, frame);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
this.logger.error("Error sending to USB:" + err);
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
handleData(data) {
|
|
252
|
+
if (data.length < 3 || data[0] !== 0x01)
|
|
253
|
+
return;
|
|
254
|
+
// knxd strictly processes single-frame packets
|
|
255
|
+
if ((data[1] & 0x0f) !== 0x03)
|
|
256
|
+
return;
|
|
257
|
+
// Connection State Check based on knxd USBLowLevelDriver logic
|
|
258
|
+
const wanted = Buffer.from([0x01, 0x13, 0x0a, 0x00, 0x08, 0x00, 0x02, 0x0f, 0x04, 0x00, 0x00, 0x03]);
|
|
259
|
+
if (data.length >= 12 && data.subarray(0, 12).equals(wanted)) {
|
|
260
|
+
const isConnectedToBus = (data[12] & 0x01) === 1;
|
|
261
|
+
if (isConnectedToBus) {
|
|
262
|
+
if (!this.busConnected) {
|
|
263
|
+
this.busConnected = true;
|
|
264
|
+
this.logger.info("KNX Bus Connected");
|
|
265
|
+
this.emit("bus_connected");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
if (this.busConnected) {
|
|
270
|
+
this.busConnected = false;
|
|
271
|
+
this.logger.error("KNX Bus Disconnected");
|
|
272
|
+
this.emit("bus_disconnected");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const bodyLength = data[2];
|
|
278
|
+
if (3 + bodyLength > data.length)
|
|
279
|
+
return;
|
|
280
|
+
const body = data.subarray(3, 3 + bodyLength);
|
|
281
|
+
this.processTransferFrame(body);
|
|
282
|
+
}
|
|
283
|
+
processTransferFrame(buffer) {
|
|
284
|
+
if (buffer.length < 8)
|
|
285
|
+
return;
|
|
286
|
+
const headerLength = buffer[1];
|
|
287
|
+
const bodyLength = buffer.readUInt16BE(2);
|
|
288
|
+
const protocolId = buffer[4];
|
|
289
|
+
const emiId = buffer[5];
|
|
290
|
+
if (headerLength !== 0x08 || buffer.length < headerLength + bodyLength)
|
|
291
|
+
return;
|
|
292
|
+
const bodyStart = headerLength;
|
|
293
|
+
const payload = buffer.subarray(bodyStart, bodyStart + bodyLength);
|
|
294
|
+
if (protocolId === 0x0f && emiId === 0x02 && payload.length >= 3 && payload[0] === 0x01) {
|
|
295
|
+
// EMI Discovery response, implementing exact knxd fallback logic
|
|
296
|
+
const bitmask = payload[2];
|
|
297
|
+
let version = 0x03; // fallback to cEMI
|
|
298
|
+
if (bitmask & 0x02)
|
|
299
|
+
version = 0x02; // vEMI2
|
|
300
|
+
else if (bitmask & 0x01)
|
|
301
|
+
version = 0x01; // vEMI1
|
|
302
|
+
else if (bitmask & 0x04)
|
|
303
|
+
version = 0x03; // vCEMI
|
|
304
|
+
else {
|
|
305
|
+
this.logger.warn(`EMI version bitmask 0x${bitmask.toString(16)} not recognized, defaulting to cEMI`);
|
|
306
|
+
version = 0x03;
|
|
307
|
+
}
|
|
308
|
+
this.emit("emi_discovery", version);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (protocolId === 0x01 && emiId === this.supportedEmiType) {
|
|
312
|
+
if (payload.length > 0) {
|
|
313
|
+
if (this.supportedEmiType === 0x03) {
|
|
314
|
+
// cEMI
|
|
315
|
+
try {
|
|
316
|
+
const cemiMsg = CEMI_1.CEMI.fromBuffer(payload);
|
|
317
|
+
if (cemiMsg) {
|
|
318
|
+
this.emit("indication", cemiMsg);
|
|
319
|
+
this.emit("raw_indication", payload);
|
|
320
|
+
try {
|
|
321
|
+
const emiMsg = CEMIAdapter_1.CEMIAdapter.cemiToEmi(cemiMsg);
|
|
322
|
+
if (emiMsg)
|
|
323
|
+
this.emit("indication_emi", emiMsg);
|
|
324
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
/* empty */
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
this.logger.debug(`Error parsing incoming USB cEMI data: ${e.message}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// EMI1 / EMI2
|
|
337
|
+
try {
|
|
338
|
+
const cemiMsg = CEMIAdapter_1.CEMIAdapter.emiToCemi(payload);
|
|
339
|
+
if (cemiMsg) {
|
|
340
|
+
this.emit("indication", cemiMsg);
|
|
341
|
+
this.emit("raw_indication", payload);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
this.logger.debug(`Error parsing incoming USB EMI data: ${e.message}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
handleError(err) {
|
|
352
|
+
this.logger.error("KNX USB Error:", err);
|
|
353
|
+
this.isConnected = false;
|
|
354
|
+
this.busConnected = false;
|
|
355
|
+
this.emit("error", err);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
exports.KNXUSBConnection = KNXUSBConnection;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { KNXService } from "./KNXService";
|
|
2
|
-
import {
|
|
3
|
-
import { KNXnetIPServerOptions } from "../@types/interfaces/connection";
|
|
2
|
+
import { CEMIInstance } from "../core/CEMI";
|
|
3
|
+
import { KNXDiscoveredDevice, KNXnetIPServerOptions } from "../@types/interfaces/connection";
|
|
4
4
|
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export declare class KNXnetIPServer extends KNXService {
|
|
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<KNXnetIPServerOptions> {
|
|
12
12
|
private isRoutingBusy;
|
|
13
13
|
private routingBusyTimer;
|
|
14
14
|
private msgQueue;
|
|
@@ -20,6 +20,8 @@ export declare class KNXnetIPServer extends KNXService {
|
|
|
20
20
|
private decrementInterval;
|
|
21
21
|
private serverIAInt;
|
|
22
22
|
private _tunnelConnections;
|
|
23
|
+
isCacheDelegated: boolean;
|
|
24
|
+
isEventsDelegated: boolean;
|
|
23
25
|
private readonly MAX_QUEUE_SIZE;
|
|
24
26
|
private readonly BUSY_THRESHOLD;
|
|
25
27
|
private readonly HEARTBEAT_TIMEOUT;
|
|
@@ -27,14 +29,29 @@ export declare class KNXnetIPServer extends KNXService {
|
|
|
27
29
|
private MAX_PENDING_REQUESTS_PER_CLIENT;
|
|
28
30
|
private maxTunnelConnections;
|
|
29
31
|
private clientAddrsStartInt;
|
|
30
|
-
private externalManager;
|
|
31
32
|
constructor(options: KNXnetIPServerOptions);
|
|
32
|
-
get individualAddress(): string;
|
|
33
33
|
connect(): Promise<void>;
|
|
34
34
|
disconnect(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Discovers KNXnet/IP devices on the network by sending SEARCH_REQUEST and SEARCH_REQUEST_EXTENDED
|
|
37
|
+
* multicasts to 224.0.23.12:3671. Returns an array of discovered devices with their properties.
|
|
38
|
+
*
|
|
39
|
+
* @param timeout Wait time in milliseconds for responses
|
|
40
|
+
* @param useExtended Whether to send SEARCH_REQUEST_EXTENDED alongside SEARCH_REQUEST
|
|
41
|
+
* @returns Promise resolving to an array of discovered devices
|
|
42
|
+
*/
|
|
43
|
+
static discover(ipLocal?: string, ipMulticast?: string, port?: number, timeout?: number, useExtended?: boolean): Promise<KNXDiscoveredDevice[]>;
|
|
35
44
|
private resolveRouteBack;
|
|
36
45
|
private clearTimers;
|
|
37
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Send a CEMI message or buffer CEMI to the bus.
|
|
48
|
+
* @param data The CEMI message or buffer to send.
|
|
49
|
+
*/
|
|
50
|
+
send(data: Buffer | CEMIInstance): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Send a raw CEMI buffer to the bus.
|
|
53
|
+
* @param cemiBuffer The CEMI buffer to send.
|
|
54
|
+
*/
|
|
38
55
|
sendRaw(cemiBuffer: Buffer): Promise<void>;
|
|
39
56
|
private enqueuePacket;
|
|
40
57
|
private sendLostMessage;
|