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
|
@@ -12,18 +12,18 @@ const CEMI_1 = require("../core/CEMI");
|
|
|
12
12
|
const KNXnetIPStructures_1 = require("../core/KNXnetIPStructures");
|
|
13
13
|
const KNXHelper_1 = require("../utils/KNXHelper");
|
|
14
14
|
const localIp_1 = require("../utils/localIp");
|
|
15
|
-
const Router_1 = require("./Router");
|
|
16
15
|
const node_os_1 = __importDefault(require("node:os"));
|
|
17
16
|
const DeviceDescriptorType_1 = require("../core/resources/DeviceDescriptorType");
|
|
18
17
|
const TunnelConnection_1 = require("./TunnelConnection");
|
|
19
18
|
const InvalidKnxAddresExeption_1 = require("../errors/InvalidKnxAddresExeption");
|
|
19
|
+
const GroupAddressCache_1 = require("../core/cache/GroupAddressCache");
|
|
20
20
|
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
* Implements a KNXnet/IP Server (Gateway) that supports Routing and Tunneling protocols.
|
|
22
|
+
* This class handles device discovery (Search/Description), manages multiple concurrent
|
|
23
|
+
* tunneling connections, and bridges communication between IP multicast (Routing) and
|
|
24
|
+
* point-to-point (Tunneling) clients. It includes implementation for flow control
|
|
25
|
+
* (RoutingBusy), rate limiting, and echo cancellation.
|
|
26
|
+
*/
|
|
27
27
|
class KNXnetIPServer extends KNXService_1.KNXService {
|
|
28
28
|
isRoutingBusy = false;
|
|
29
29
|
routingBusyTimer = null;
|
|
@@ -37,6 +37,8 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
37
37
|
// [MEJORA] Almacenamos la IA en formato entero para el filtro anti-eco rápido
|
|
38
38
|
serverIAInt;
|
|
39
39
|
_tunnelConnections = new Map();
|
|
40
|
+
isCacheDelegated = false;
|
|
41
|
+
isEventsDelegated = false;
|
|
40
42
|
MAX_QUEUE_SIZE = 100;
|
|
41
43
|
BUSY_THRESHOLD = 15;
|
|
42
44
|
HEARTBEAT_TIMEOUT = KNXnetIPEnum_1.KNXTimeoutConstants.CONNECTION_ALIVE_TIME * 1000;
|
|
@@ -44,7 +46,6 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
44
46
|
MAX_PENDING_REQUESTS_PER_CLIENT = 100; // [MEJORA] Límite de ráfagas
|
|
45
47
|
maxTunnelConnections;
|
|
46
48
|
clientAddrsStartInt;
|
|
47
|
-
externalManager = null;
|
|
48
49
|
constructor(options) {
|
|
49
50
|
super(options);
|
|
50
51
|
this._transport = "UDP";
|
|
@@ -53,6 +54,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
53
54
|
const netInfo = (0, localIp_1.getNetworkInfo)();
|
|
54
55
|
this.options.localIp = options.localIp || netInfo.address;
|
|
55
56
|
routingOptions.individualAddress = options.individualAddress || "15.15.0";
|
|
57
|
+
this.individualAddress = routingOptions.individualAddress;
|
|
56
58
|
if (!KNXHelper_1.KNXHelper.isValidIndividualAddress(routingOptions.individualAddress)) {
|
|
57
59
|
throw new InvalidKnxAddresExeption_1.InvalidKnxAddressException(`This ${routingOptions.individualAddress} is not individual address`);
|
|
58
60
|
}
|
|
@@ -60,6 +62,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
60
62
|
this.logger = this.logger.child({ module: this.constructor.name });
|
|
61
63
|
// Serial must be deterministic and unique per instance (MAC + Port), similar to knxd
|
|
62
64
|
if (!options.serialNumber) {
|
|
65
|
+
// eslint-disable-next-line no-useless-escape
|
|
63
66
|
const macBuf = Buffer.from(netInfo.mac.replace(/[:\-]/g, ""), "hex");
|
|
64
67
|
const port = options.port || 3671;
|
|
65
68
|
const serial = Buffer.from(macBuf);
|
|
@@ -70,7 +73,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
70
73
|
else {
|
|
71
74
|
routingOptions.serialNumber = options.serialNumber;
|
|
72
75
|
}
|
|
73
|
-
routingOptions.friendlyName = options.friendlyName || "KNX.ts
|
|
76
|
+
routingOptions.friendlyName = options.friendlyName || "KNX.ts";
|
|
74
77
|
routingOptions.macAddress = options.macAddress || netInfo.mac;
|
|
75
78
|
routingOptions.routingDelay = options.routingDelay ?? 20;
|
|
76
79
|
if (routingOptions.MAX_PENDING_REQUESTS_PER_CLIENT)
|
|
@@ -94,14 +97,11 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
94
97
|
this.maxTunnelConnections = 15;
|
|
95
98
|
this.clientAddrsStartInt = serverIA + 1;
|
|
96
99
|
}
|
|
97
|
-
if (options.externals) {
|
|
98
|
-
this.externalManager = new Router_1.Router(options.externals);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
get individualAddress() {
|
|
102
|
-
return KNXHelper_1.KNXHelper.GetAddress(this.serverIAInt, ".", true);
|
|
103
100
|
}
|
|
104
101
|
async connect() {
|
|
102
|
+
if (this.socket) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
105
|
this.socket = dgram_1.default.createSocket({ type: "udp4", reuseAddr: true });
|
|
106
106
|
this.socket.on("message", (msg, rinfo) => {
|
|
107
107
|
this.handleMessage(msg, rinfo);
|
|
@@ -121,11 +121,12 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
121
121
|
const joinedInterfaces = new Set();
|
|
122
122
|
const useAllInterfaces = this.options.useAllInterfaces ?? true;
|
|
123
123
|
// Siempre intenta unirse primero a la localIp especificada
|
|
124
|
-
if (this.options.localIp && this.options.localIp !== "0.0.0.0") {
|
|
124
|
+
if (this.options.localIp && this.options.localIp !== "0.0.0.0" && this.options.ip) {
|
|
125
125
|
try {
|
|
126
126
|
socket.addMembership(this.options.ip, this.options.localIp);
|
|
127
127
|
joinedInterfaces.add(this.options.localIp);
|
|
128
128
|
this.logger.info(`Joined multicast on primary interface (${this.options.localIp})`);
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
129
130
|
}
|
|
130
131
|
catch (e) {
|
|
131
132
|
this.logger.debug(`Failed to join multicast on primary interface ${this.options.localIp}`);
|
|
@@ -141,6 +142,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
141
142
|
socket.addMembership(this.options.ip, net.address);
|
|
142
143
|
joinedInterfaces.add(net.address);
|
|
143
144
|
this.logger.info(`Joined multicast on interface ${name} (${net.address})`);
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
144
146
|
}
|
|
145
147
|
catch (err) {
|
|
146
148
|
// Ignora interfaces virtuales que no soportan IGMP
|
|
@@ -153,31 +155,6 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
153
155
|
else {
|
|
154
156
|
this.logger.info("Multi-homing disabled. Only primary interface used for multicast.");
|
|
155
157
|
}
|
|
156
|
-
// Central listener for all KNX indications (from IP Multicast, TP, or Tunnels)
|
|
157
|
-
this.on("indication", (cemi) => {
|
|
158
|
-
const body = cemi.toBuffer();
|
|
159
|
-
const srcIAStr = cemi.sourceAddress;
|
|
160
|
-
let busmonBody = null;
|
|
161
|
-
// Optional: Re-emit by Group Address for specific listening (e.g., server.on("1/1/1", (cemi) => ...))
|
|
162
|
-
if (cemi.controlField2 && cemi.controlField2.addressType === 1) {
|
|
163
|
-
this.emit(cemi.destinationAddress, cemi);
|
|
164
|
-
}
|
|
165
|
-
this._tunnelConnections.forEach((conn) => {
|
|
166
|
-
// Echo cancellation: Don't forward back to the client that originated this message
|
|
167
|
-
if (srcIAStr === conn.knxAddressStr) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER) {
|
|
171
|
-
if (!busmonBody)
|
|
172
|
-
busmonBody = this.convertDataIndToBusmonInd(body);
|
|
173
|
-
conn.enqueue(busmonBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
// Link Layer or Raw Layer
|
|
177
|
-
conn.enqueue(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
158
|
this.emit("connected");
|
|
182
159
|
resolve();
|
|
183
160
|
}
|
|
@@ -188,22 +165,124 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
188
165
|
});
|
|
189
166
|
});
|
|
190
167
|
await connectPromise;
|
|
191
|
-
if (this.externalManager) {
|
|
192
|
-
this.externalManager.registerLink(this);
|
|
193
|
-
await this.externalManager.connect();
|
|
194
|
-
}
|
|
195
168
|
}
|
|
196
169
|
disconnect() {
|
|
197
|
-
if (this.externalManager) {
|
|
198
|
-
this.externalManager.unregisterLink(this);
|
|
199
|
-
this.externalManager.disconnect();
|
|
200
|
-
}
|
|
201
170
|
if (this.socket) {
|
|
202
171
|
this.socket.close();
|
|
203
172
|
this.socket = null;
|
|
204
173
|
}
|
|
205
174
|
this.clearTimers();
|
|
206
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Discovers KNXnet/IP devices on the network by sending SEARCH_REQUEST and SEARCH_REQUEST_EXTENDED
|
|
178
|
+
* multicasts to 224.0.23.12:3671. Returns an array of discovered devices with their properties.
|
|
179
|
+
*
|
|
180
|
+
* @param timeout Wait time in milliseconds for responses
|
|
181
|
+
* @param useExtended Whether to send SEARCH_REQUEST_EXTENDED alongside SEARCH_REQUEST
|
|
182
|
+
* @returns Promise resolving to an array of discovered devices
|
|
183
|
+
*/
|
|
184
|
+
static async discover(ipLocal = "", ipMulticast = "224.0.23.12", port = 3671, timeout = 3000, useExtended = true) {
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
const socket = dgram_1.default.createSocket({ type: "udp4", reuseAddr: true });
|
|
187
|
+
const discoveredDevices = new Map();
|
|
188
|
+
socket.on("message", (msg) => {
|
|
189
|
+
try {
|
|
190
|
+
const header = KNXnetIPHeader_1.KNXnetIPHeader.fromBuffer(msg);
|
|
191
|
+
if (header.serviceType === KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE ||
|
|
192
|
+
header.serviceType === KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE_EXTENDED) {
|
|
193
|
+
const body = msg.subarray(KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10);
|
|
194
|
+
if (body.length < 8)
|
|
195
|
+
return;
|
|
196
|
+
const hpai = KNXnetIPStructures_1.HPAI.fromBuffer(body);
|
|
197
|
+
let offset = 8; // Size of HPAI
|
|
198
|
+
let deviceInfo = null;
|
|
199
|
+
while (offset < body.length) {
|
|
200
|
+
const dibLen = body.readUInt8(offset);
|
|
201
|
+
if (dibLen === 0)
|
|
202
|
+
break;
|
|
203
|
+
if (offset + dibLen > body.length)
|
|
204
|
+
break;
|
|
205
|
+
const dibBuffer = body.subarray(offset, offset + dibLen);
|
|
206
|
+
const dib = KNXnetIPStructures_1.DIB.fromBuffer(dibBuffer);
|
|
207
|
+
if (dib instanceof KNXnetIPStructures_1.DeviceInformationDIB) {
|
|
208
|
+
deviceInfo = dib;
|
|
209
|
+
}
|
|
210
|
+
offset += dibLen;
|
|
211
|
+
}
|
|
212
|
+
if (deviceInfo) {
|
|
213
|
+
const deviceKey = `${hpai.ipAddress}:${hpai.port}`; // Unique by IP/Port
|
|
214
|
+
if (!discoveredDevices.has(deviceKey)) {
|
|
215
|
+
let knxMediumStr = `Unknown (${deviceInfo.knxMedium})`;
|
|
216
|
+
if (deviceInfo.knxMedium === KNXnetIPEnum_1.KNXMedium.TP1)
|
|
217
|
+
knxMediumStr = "TP1";
|
|
218
|
+
else if (deviceInfo.knxMedium === KNXnetIPEnum_1.KNXMedium.PL110)
|
|
219
|
+
knxMediumStr = "PL110";
|
|
220
|
+
else if (deviceInfo.knxMedium === KNXnetIPEnum_1.KNXMedium.RF)
|
|
221
|
+
knxMediumStr = "RF";
|
|
222
|
+
else if (deviceInfo.knxMedium === KNXnetIPEnum_1.KNXMedium.KNXIP)
|
|
223
|
+
knxMediumStr = "KNXIP";
|
|
224
|
+
const deviceStatusStr = deviceInfo.deviceStatus === 1
|
|
225
|
+
? "Programmed"
|
|
226
|
+
: deviceInfo.deviceStatus === 0
|
|
227
|
+
? "Not Programmed"
|
|
228
|
+
: `Unknown (${deviceInfo.deviceStatus})`;
|
|
229
|
+
discoveredDevices.set(deviceKey, {
|
|
230
|
+
ip: hpai.ipAddress,
|
|
231
|
+
port: hpai.port,
|
|
232
|
+
knxMediumRaw: deviceInfo.knxMedium,
|
|
233
|
+
knxMedium: knxMediumStr,
|
|
234
|
+
deviceStatusRaw: deviceInfo.deviceStatus,
|
|
235
|
+
deviceStatus: deviceStatusStr,
|
|
236
|
+
individualAddress: deviceInfo.individualAddress,
|
|
237
|
+
projectInstallationId: deviceInfo.projectInstallationId,
|
|
238
|
+
serialNumber: deviceInfo.serialNumber,
|
|
239
|
+
routingMulticastAddress: deviceInfo.routingMulticastAddress,
|
|
240
|
+
macAddress: deviceInfo.macAddress,
|
|
241
|
+
friendlyName: deviceInfo.friendlyName,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
// Ignore parsing errors for individual packets
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
const timer = setTimeout(() => {
|
|
253
|
+
socket.close();
|
|
254
|
+
resolve(Array.from(discoveredDevices.values()));
|
|
255
|
+
}, timeout);
|
|
256
|
+
socket.on("error", (err) => {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
socket.close();
|
|
259
|
+
reject(err);
|
|
260
|
+
});
|
|
261
|
+
socket.bind(0, () => {
|
|
262
|
+
const netInfo = (0, localIp_1.getNetworkInfo)();
|
|
263
|
+
const serverHPAI = new KNXnetIPStructures_1.HPAI(KNXnetIPEnum_1.HostProtocolCode.IPV4_UDP, ipLocal !== "" ? ipLocal : netInfo.address, socket.address().port);
|
|
264
|
+
const sendRequest = (serviceType) => {
|
|
265
|
+
const hpaiBuf = serverHPAI.toBuffer();
|
|
266
|
+
const header = new KNXnetIPHeader_1.KNXnetIPHeader(serviceType, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + hpaiBuf.length);
|
|
267
|
+
const packet = Buffer.concat([header.toBuffer(), hpaiBuf]);
|
|
268
|
+
socket.send(packet, port, ipMulticast);
|
|
269
|
+
};
|
|
270
|
+
try {
|
|
271
|
+
socket.setBroadcast(true);
|
|
272
|
+
socket.setMulticastTTL(128);
|
|
273
|
+
sendRequest(KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_REQUEST);
|
|
274
|
+
if (useExtended) {
|
|
275
|
+
sendRequest(KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_REQUEST_EXTENDED);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
socket.close();
|
|
281
|
+
reject(e);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
207
286
|
// [MEJORA] Validación estricta Route Back (NAT Traversal) según Especificación 8.6.2.2
|
|
208
287
|
resolveRouteBack(hpai, rinfo) {
|
|
209
288
|
const isIpZero = hpai.ipAddress === "0.0.0.0";
|
|
@@ -235,22 +314,28 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
235
314
|
conn.close();
|
|
236
315
|
});
|
|
237
316
|
this._tunnelConnections.clear();
|
|
238
|
-
this.removeAllListeners(
|
|
317
|
+
this.removeAllListeners();
|
|
239
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Send a CEMI message or buffer CEMI to the bus.
|
|
321
|
+
* @param data The CEMI message or buffer to send.
|
|
322
|
+
*/
|
|
240
323
|
async send(data) {
|
|
241
324
|
let cemiBuffer;
|
|
242
|
-
let cemi;
|
|
325
|
+
let cemi = undefined;
|
|
243
326
|
if (Buffer.isBuffer(data)) {
|
|
244
327
|
cemiBuffer = data;
|
|
245
328
|
try {
|
|
246
329
|
cemi = CEMI_1.CEMI.fromBuffer(data);
|
|
247
330
|
}
|
|
248
|
-
catch (e) {
|
|
331
|
+
catch (e) {
|
|
332
|
+
this.logger.debug("Error parsing CEMI buffer" + e.message);
|
|
333
|
+
}
|
|
249
334
|
}
|
|
250
335
|
else {
|
|
251
336
|
cemi = data;
|
|
252
|
-
if (
|
|
253
|
-
const cf2 =
|
|
337
|
+
if ("controlField2" in cemi) {
|
|
338
|
+
const cf2 = cemi.controlField2;
|
|
254
339
|
const hopCount = cf2.hopCount;
|
|
255
340
|
if (hopCount === 0)
|
|
256
341
|
return;
|
|
@@ -259,17 +344,71 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
259
344
|
}
|
|
260
345
|
cemiBuffer = data.toBuffer();
|
|
261
346
|
}
|
|
262
|
-
if (cemi) {
|
|
263
|
-
this.emit("
|
|
347
|
+
if (cemi && "destinationAddress" in cemi && "sourceAddress" in cemi) {
|
|
348
|
+
this.emit("send", cemi);
|
|
349
|
+
if (!this.isCacheDelegated) {
|
|
350
|
+
GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemi);
|
|
351
|
+
}
|
|
352
|
+
if (!this.isEventsDelegated && cemi.destinationAddress) {
|
|
353
|
+
this.emit(cemi.destinationAddress, cemi);
|
|
354
|
+
}
|
|
355
|
+
const body = cemiBuffer;
|
|
356
|
+
const srcIAStr = cemi.sourceAddress;
|
|
357
|
+
let busmonBody = null;
|
|
358
|
+
this._tunnelConnections.forEach((conn) => {
|
|
359
|
+
// Echo cancellation: Don't forward back to the client that originated this message
|
|
360
|
+
if (srcIAStr === conn.knxAddressStr) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER) {
|
|
364
|
+
if (!busmonBody)
|
|
365
|
+
busmonBody = this.convertDataIndToBusmonInd(body);
|
|
366
|
+
conn.enqueue(busmonBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// Link Layer or Raw Layer
|
|
370
|
+
conn.enqueue(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
264
373
|
}
|
|
265
374
|
await this.enqueuePacket(cemiBuffer);
|
|
266
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Send a raw CEMI buffer to the bus.
|
|
378
|
+
* @param cemiBuffer The CEMI buffer to send.
|
|
379
|
+
*/
|
|
267
380
|
async sendRaw(cemiBuffer) {
|
|
381
|
+
let cemi = undefined;
|
|
268
382
|
try {
|
|
269
|
-
|
|
270
|
-
this.emit("indication", cemi);
|
|
383
|
+
cemi = CEMI_1.CEMI.fromBuffer(cemiBuffer);
|
|
271
384
|
}
|
|
272
|
-
catch (e) {
|
|
385
|
+
catch (e) {
|
|
386
|
+
this.logger.debug("Error parsing CEMI buffer" + e.message);
|
|
387
|
+
}
|
|
388
|
+
if (!cemi || !("destinationAddress" in cemi) || !("sourceAddress" in cemi))
|
|
389
|
+
return;
|
|
390
|
+
this.emit("send", cemi);
|
|
391
|
+
if (!this.isEventsDelegated && cemi.destinationAddress) {
|
|
392
|
+
this.emit(cemi.destinationAddress, cemi);
|
|
393
|
+
}
|
|
394
|
+
const body = cemiBuffer;
|
|
395
|
+
const srcIAStr = cemi.sourceAddress;
|
|
396
|
+
let busmonBody = null;
|
|
397
|
+
this._tunnelConnections.forEach((conn) => {
|
|
398
|
+
// Echo cancellation: Don't forward back to the client that originated this message
|
|
399
|
+
if (srcIAStr === conn.knxAddressStr) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER) {
|
|
403
|
+
if (!busmonBody)
|
|
404
|
+
busmonBody = this.convertDataIndToBusmonInd(body);
|
|
405
|
+
conn.enqueue(busmonBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// Link Layer or Raw Layer
|
|
409
|
+
conn.enqueue(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
273
412
|
await this.enqueuePacket(cemiBuffer);
|
|
274
413
|
}
|
|
275
414
|
async enqueuePacket(cemiBuffer) {
|
|
@@ -362,7 +501,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
362
501
|
if (rinfo.address === this.options.localIp && rinfo.port === ourAddress.port)
|
|
363
502
|
return;
|
|
364
503
|
switch (header.serviceType) {
|
|
365
|
-
case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_INDICATION:
|
|
504
|
+
case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_INDICATION: {
|
|
366
505
|
// [MEJORA] Filtro Anti-Eco Seguro leyendo la Individual Address (IA) origen del CEMI
|
|
367
506
|
const addInfoLen = body[1];
|
|
368
507
|
if (body.length >= 6 + addInfoLen) {
|
|
@@ -374,10 +513,37 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
374
513
|
this.emit("raw_indication", body);
|
|
375
514
|
try {
|
|
376
515
|
const cemi = CEMI_1.CEMI.fromBuffer(body);
|
|
516
|
+
if (!("destinationAddress" in cemi) || !("sourceAddress" in cemi))
|
|
517
|
+
return;
|
|
377
518
|
this.emit("indication", cemi);
|
|
519
|
+
if (!this.isCacheDelegated) {
|
|
520
|
+
GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemi);
|
|
521
|
+
}
|
|
522
|
+
this.emit(cemi.destinationAddress, cemi);
|
|
523
|
+
const srcIAStr = cemi.sourceAddress;
|
|
524
|
+
let busmonBody = null;
|
|
525
|
+
this._tunnelConnections.forEach((conn) => {
|
|
526
|
+
// Echo cancellation: Don't forward back to the client that originated this message
|
|
527
|
+
if (srcIAStr === conn.knxAddressStr) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (conn.knxLayer === KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER) {
|
|
531
|
+
if (!busmonBody)
|
|
532
|
+
busmonBody = this.convertDataIndToBusmonInd(body);
|
|
533
|
+
conn.enqueue(busmonBody, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
// Link Layer or Raw Layer
|
|
537
|
+
conn.enqueue(body, KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_REQUEST);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
/* empty */
|
|
378
544
|
}
|
|
379
|
-
catch (e) { }
|
|
380
545
|
break;
|
|
546
|
+
}
|
|
381
547
|
case KNXnetIPEnum_1.KNXnetIPServiceType.ROUTING_BUSY:
|
|
382
548
|
this.handleRoutingBusy(KNXnetIPStructures_1.RoutingBusy.fromBuffer(body));
|
|
383
549
|
break;
|
|
@@ -442,7 +608,9 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
442
608
|
if (!this.resolveRouteBack(clientHPAI, rinfo)) {
|
|
443
609
|
return; // Silently drop invalid HPAI
|
|
444
610
|
}
|
|
445
|
-
const responseType = isExtended
|
|
611
|
+
const responseType = isExtended
|
|
612
|
+
? KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE_EXTENDED
|
|
613
|
+
: KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE;
|
|
446
614
|
const serverHPAI = this.getHPAI(rinfo);
|
|
447
615
|
const localIp = serverHPAI.ipAddress;
|
|
448
616
|
const localPort = serverHPAI.port;
|
|
@@ -477,12 +645,12 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
477
645
|
// Ensure we report the local IP of the interface that can actually route back to the client.
|
|
478
646
|
if (rinfo && rinfo.address) {
|
|
479
647
|
const interfaces = node_os_1.default.networkInterfaces();
|
|
480
|
-
const rinfoNum = rinfo.address.split(
|
|
648
|
+
const rinfoNum = rinfo.address.split(".").reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
|
481
649
|
for (const name of Object.keys(interfaces)) {
|
|
482
650
|
for (const net of interfaces[name]) {
|
|
483
651
|
if (net.family === "IPv4" && !net.internal) {
|
|
484
|
-
const netNum = net.address.split(
|
|
485
|
-
const maskNum = net.netmask.split(
|
|
652
|
+
const netNum = net.address.split(".").reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
|
653
|
+
const maskNum = net.netmask.split(".").reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
|
486
654
|
if ((rinfoNum & maskNum) === (netNum & maskNum)) {
|
|
487
655
|
localIp = net.address;
|
|
488
656
|
}
|
|
@@ -538,7 +706,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
538
706
|
const body = Buffer.concat([
|
|
539
707
|
Buffer.from([channelId, status]),
|
|
540
708
|
serverDataHPAI.toBuffer(),
|
|
541
|
-
Buffer.from([0x02, KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION])
|
|
709
|
+
Buffer.from([0x02, KNXnetIPEnum_1.ConnectionType.DEVICE_MGMT_CONNECTION]),
|
|
542
710
|
]);
|
|
543
711
|
const responseHeader = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.CONNECT_RESPONSE, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
544
712
|
if (this.socket)
|
|
@@ -549,7 +717,9 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
549
717
|
if (knxAddress === null || knxAddress === 0) {
|
|
550
718
|
knxAddress = this.clientAddrsStartInt + channelId - 1;
|
|
551
719
|
}
|
|
552
|
-
if (cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.LINK_LAYER &&
|
|
720
|
+
if (cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.LINK_LAYER &&
|
|
721
|
+
cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.BUSMONITOR_LAYER &&
|
|
722
|
+
cri.knxLayer !== KNXnetIPEnum_1.KNXLayer.RAW_LAYER) {
|
|
553
723
|
this.logger.warn(`Connect Request refused: Invalid layer ${cri.knxLayer}`);
|
|
554
724
|
status = KNXnetIPEnum_1.KNXnetIPErrorCodes.E_TUNNELLING_LAYER;
|
|
555
725
|
}
|
|
@@ -655,11 +825,11 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
655
825
|
return;
|
|
656
826
|
}
|
|
657
827
|
const { action, status } = conn.validateRequest(seq);
|
|
658
|
-
if (action ===
|
|
828
|
+
if (action === "retransmit_ack") {
|
|
659
829
|
this.sendTunnelACK(channelId, seq, status, rinfo);
|
|
660
830
|
return;
|
|
661
831
|
}
|
|
662
|
-
if (action ===
|
|
832
|
+
if (action === "discard")
|
|
663
833
|
return;
|
|
664
834
|
this.sendTunnelACK(channelId, seq, status, rinfo);
|
|
665
835
|
const msgCode = cemiBuffer[0];
|
|
@@ -694,8 +864,8 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
694
864
|
const header = new KNXnetIPHeader_1.KNXnetIPHeader(KNXnetIPEnum_1.KNXnetIPServiceType.TUNNELLING_ACK, KNXnetIPHeader_1.KNXnetIPHeader.HEADER_SIZE_10 + body.length);
|
|
695
865
|
const conn = this._tunnelConnections.get(channelId);
|
|
696
866
|
if (this.socket) {
|
|
697
|
-
const port = conn ? conn.dataHPAI.port :
|
|
698
|
-
const addr = conn ? conn.dataHPAI.ipAddress :
|
|
867
|
+
const port = conn ? conn.dataHPAI.port : rinfo ? rinfo.port : 0;
|
|
868
|
+
const addr = conn ? conn.dataHPAI.ipAddress : rinfo ? rinfo.address : "";
|
|
699
869
|
if (port > 0 && addr !== "") {
|
|
700
870
|
this.socket.send(Buffer.concat([header.toBuffer(), body]), port, addr);
|
|
701
871
|
}
|
|
@@ -712,11 +882,11 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
712
882
|
}
|
|
713
883
|
const { action, status } = conn.validateRequest(seq);
|
|
714
884
|
this.logger.debug(`Feature Get for channel ${channelId}, feat: ${featId}, seq: ${seq}`);
|
|
715
|
-
if (action ===
|
|
885
|
+
if (action === "retransmit_ack") {
|
|
716
886
|
this.sendTunnelACK(channelId, seq, status);
|
|
717
887
|
return;
|
|
718
888
|
}
|
|
719
|
-
if (action ===
|
|
889
|
+
if (action === "discard")
|
|
720
890
|
return;
|
|
721
891
|
this.sendTunnelACK(channelId, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR);
|
|
722
892
|
let featVal;
|
|
@@ -748,16 +918,17 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
748
918
|
return;
|
|
749
919
|
}
|
|
750
920
|
const { action, status } = conn.validateRequest(seq);
|
|
751
|
-
if (action ===
|
|
921
|
+
if (action === "retransmit_ack") {
|
|
752
922
|
this.sendDeviceConfigACK(channelId, seq, status);
|
|
753
923
|
return;
|
|
754
924
|
}
|
|
755
|
-
if (action ===
|
|
925
|
+
if (action === "discard")
|
|
756
926
|
return;
|
|
757
927
|
this.sendDeviceConfigACK(channelId, seq, KNXnetIPEnum_1.KNXnetIPErrorCodes.E_NO_ERROR);
|
|
758
928
|
try {
|
|
759
929
|
const msgCode = cemiBuffer.readUInt8(0);
|
|
760
|
-
if (msgCode ===
|
|
930
|
+
if (msgCode === 0xfc) {
|
|
931
|
+
// M_PropRead.req
|
|
761
932
|
const req = CEMI_1.CEMI.ManagementCEMI["M_PropRead.req"].fromBuffer(cemiBuffer);
|
|
762
933
|
this.logger.debug(`Management PropRead: Obj=${req.interfaceObjectType}, Prop=${req.propertyId} on channel ${channelId}`);
|
|
763
934
|
let data = Buffer.alloc(0);
|
|
@@ -809,12 +980,12 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
809
980
|
}
|
|
810
981
|
}
|
|
811
982
|
// Spec says use TP1 (0x02) for gateway reporting
|
|
812
|
-
const devInfo = new KNXnetIPStructures_1.DeviceInformationDIB(
|
|
983
|
+
const devInfo = new KNXnetIPStructures_1.DeviceInformationDIB(KNXnetIPEnum_1.KNXMedium.KNXIP, 0, KNXHelper_1.KNXHelper.GetAddress(routingOptions.individualAddress, ".").readUint16BE(), 0, routingOptions.serialNumber, this.options.ip, routingOptions.macAddress, routingOptions.friendlyName);
|
|
813
984
|
const suppSvc = new KNXnetIPStructures_1.SupportedServicesDIB([
|
|
814
|
-
{ family:
|
|
815
|
-
{ family:
|
|
816
|
-
{ family:
|
|
817
|
-
{ family:
|
|
985
|
+
{ family: KNXnetIPEnum_1.AllowedSupportedServiceFamilies.Core, version: 1 },
|
|
986
|
+
{ family: KNXnetIPEnum_1.AllowedSupportedServiceFamilies.DeviceManagement, version: 1 },
|
|
987
|
+
{ family: KNXnetIPEnum_1.AllowedSupportedServiceFamilies.Tunnelling, version: 1 },
|
|
988
|
+
{ family: KNXnetIPEnum_1.AllowedSupportedServiceFamilies.Routing, version: 1 },
|
|
818
989
|
]);
|
|
819
990
|
if (serviceType === KNXnetIPEnum_1.KNXnetIPServiceType.SEARCH_RESPONSE) {
|
|
820
991
|
return [devInfo, suppSvc];
|
|
@@ -832,7 +1003,7 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
832
1003
|
status.free = !conn;
|
|
833
1004
|
slots.push({
|
|
834
1005
|
address: conn ? conn.knxAddress : this.clientAddrsStartInt + i - 1,
|
|
835
|
-
status: status
|
|
1006
|
+
status: status,
|
|
836
1007
|
});
|
|
837
1008
|
}
|
|
838
1009
|
return [devInfo, suppSvc, extDevInfo, ipConfig, ipCurrent, new KNXnetIPStructures_1.TunnellingInfoDIB(254, slots)];
|
|
@@ -889,7 +1060,14 @@ class KNXnetIPServer extends KNXService_1.KNXService {
|
|
|
889
1060
|
const dst = cemiBuffer.subarray(baseOffset + 4, baseOffset + 6);
|
|
890
1061
|
const dataLen = cemiBuffer[baseOffset + 6];
|
|
891
1062
|
const tpdu = cemiBuffer.subarray(baseOffset + 7);
|
|
892
|
-
const lpdu = Buffer.concat([
|
|
1063
|
+
const lpdu = Buffer.concat([
|
|
1064
|
+
Buffer.from([cf1]),
|
|
1065
|
+
src,
|
|
1066
|
+
dst,
|
|
1067
|
+
Buffer.from([(cf2 & 0xf0) | (dataLen + 1)]),
|
|
1068
|
+
tpdu,
|
|
1069
|
+
Buffer.alloc(1),
|
|
1070
|
+
]);
|
|
893
1071
|
let xor = 0;
|
|
894
1072
|
for (let i = 0; i < lpdu.length - 1; i++)
|
|
895
1073
|
xor ^= lpdu[i];
|