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.
Files changed (50) hide show
  1. package/LICENSE +51 -21
  2. package/README.md +274 -61
  3. package/dist/@types/interfaces/connection.d.ts +80 -13
  4. package/dist/@types/interfaces/servers.d.ts +18 -0
  5. package/dist/@types/interfaces/servers.js +2 -0
  6. package/dist/connection/KNXService.d.ts +13 -30
  7. package/dist/connection/KNXService.js +4 -164
  8. package/dist/connection/KNXTunneling.d.ts +4 -4
  9. package/dist/connection/KNXTunneling.js +35 -62
  10. package/dist/connection/KNXUSBConnection.d.ts +20 -0
  11. package/dist/connection/KNXUSBConnection.js +358 -0
  12. package/dist/connection/KNXnetIPServer.d.ts +29 -12
  13. package/dist/connection/KNXnetIPServer.js +261 -83
  14. package/dist/connection/Router.d.ts +52 -32
  15. package/dist/connection/Router.js +225 -153
  16. package/dist/connection/TPUART.d.ts +8 -3
  17. package/dist/connection/TPUART.js +41 -37
  18. package/dist/connection/TunnelConnection.d.ts +3 -1
  19. package/dist/connection/TunnelConnection.js +6 -4
  20. package/dist/core/CEMI.d.ts +7 -2
  21. package/dist/core/CEMI.js +5 -8
  22. package/dist/core/EMI.d.ts +312 -200
  23. package/dist/core/EMI.js +511 -1007
  24. package/dist/core/KNXnetIPStructures.d.ts +10 -1
  25. package/dist/core/KNXnetIPStructures.js +15 -10
  26. package/dist/core/MessageCodeField.d.ts +1 -1
  27. package/dist/core/cache/GroupAddressCache.d.ts +57 -0
  28. package/dist/core/cache/GroupAddressCache.js +227 -0
  29. package/dist/core/data/KNXDataDecode.d.ts +2 -2
  30. package/dist/core/data/KNXDataDecode.js +198 -183
  31. package/dist/core/enum/EnumControlField.d.ts +0 -5
  32. package/dist/core/enum/EnumControlField.js +1 -7
  33. package/dist/core/enum/EnumControlFieldExtended.d.ts +1 -1
  34. package/dist/core/enum/EnumShortACKFrame.d.ts +1 -1
  35. package/dist/core/enum/ErrorCodeSet.js +59 -0
  36. package/dist/core/enum/KNXnetIPEnum.d.ts +2 -2
  37. package/dist/core/enum/KNXnetIPEnum.js +19 -1
  38. package/dist/core/layers/data/NPDU.d.ts +2 -1
  39. package/dist/core/layers/data/NPDU.js +6 -3
  40. package/dist/index.d.ts +19 -2
  41. package/dist/index.js +36 -1
  42. package/dist/server/KNXMQTTGateway.d.ts +13 -0
  43. package/dist/server/KNXMQTTGateway.js +164 -0
  44. package/dist/server/KNXWebSocketServer.d.ts +12 -0
  45. package/dist/server/KNXWebSocketServer.js +118 -0
  46. package/dist/utils/CEMIAdapter.d.ts +4 -3
  47. package/dist/utils/CEMIAdapter.js +26 -30
  48. package/dist/utils/Logger.d.ts +4 -4
  49. package/dist/utils/Logger.js +3 -7
  50. 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
- assignedAddress = 0; // Assigned Individual Address
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 = (info) => {
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.assignedAddress = crd.knxAddress;
266
+ this.individualAddress = KNXHelper_1.KNXHelper.GetAddress(crd.knxAddress, ".");
291
267
  this.emit("connected", {
292
268
  channelId: this.channelId,
293
- assignedAddress: crd.knxAddress,
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.activeRequest?.responseType ===
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 { ServiceMessage } from "../@types/interfaces/ServiceMessage";
3
- import { KNXnetIPServerOptions } from "../@types/interfaces/connection";
2
+ import { CEMIInstance } from "../core/CEMI";
3
+ import { KNXDiscoveredDevice, KNXnetIPServerOptions } from "../@types/interfaces/connection";
4
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 {
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
- send(data: Buffer | ServiceMessage): Promise<void>;
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;