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
@@ -1,40 +1,33 @@
1
1
  import { EventEmitter } from "events";
2
2
  import { KNXService } from "./KNXService";
3
- import { ExternalManagerOptions } from "../@types/interfaces/connection";
3
+ import { RouterConnOptions } from "../@types/interfaces/connection";
4
+ import { CEMIInstance } from "../core/CEMI";
5
+ import { KnxDataEncoder } from "../core/data/KNXDataEncode";
6
+ import { AllDpts } from "../@types/types/AllDpts";
4
7
  /**
5
- * Router: A high-performance, robust Learning Bridge.
6
- * Architecture strictly follows knxd/src/libserver/router.cpp patterns:
7
- * 1. Loop Prevention via signature tracking AND Repeat Flag analysis.
8
- * 2. IA Learning & Selective Routing.
9
- * 3. Source Address Patching & Sanitization.
10
- * 4. Client Address Pool Management (KNXnet/IP Tunneling).
8
+ * Router: A robust, high-performance learning bridge.
9
+ * The architecture is based on the patterns in the knxd repository at knxd/src/libserver/router.cpp:
10
+ * 1. Loop prevention through destination address tracking.
11
+ * 2. AI learning and selective routing.
12
+ * 3. Source address correction and sanitization.
13
+ * 4. Address filtering between KNXnetIP and other connections, and vice versa.
11
14
  */
12
15
  export declare class Router extends EventEmitter {
13
- private links;
14
- private addressTable;
15
- private recentSignatures;
16
+ readonly links: Map<string, KNXService>;
17
+ readonly addressTable: Map<string, {
18
+ link: KNXService;
19
+ key: string;
20
+ }>;
21
+ private recentDestinationAddress;
16
22
  private readonly MAX_SIGNATURES_SIZE;
17
- private routerAddress;
18
- private clientAddrsStart;
19
- private clientAddrsCount;
20
- private clientAddrsUsed;
23
+ routerAddress: string;
21
24
  private logger;
22
- constructor(options: ExternalManagerOptions & {
23
- routerAddress?: string;
24
- clientAddrs?: string;
25
- });
26
- /**
27
- * Allocates a free physical address for a tunneling client.
28
- */
29
- getClientAddress(): string | null;
30
- /**
31
- * Releases an allocated physical address when a client disconnects.
32
- */
33
- releaseClientAddress(addr: string): void;
34
- private addrToInt;
35
- private intToAddr;
36
- registerLink(link: KNXService): void;
37
- unregisterLink(link: KNXService): void;
25
+ private gcInterval;
26
+ readonly toIPFilter: RouterConnOptions["toIpFilter"];
27
+ readonly toLocalFilter: RouterConnOptions["toLocalFilter"];
28
+ constructor(options: RouterConnOptions);
29
+ registerLink(key: string, link: KNXService): void;
30
+ unregisterLink(key: string | "TPUART" | "KNXUSB"): void;
38
31
  /**
39
32
  * Main entry point for any packet received from any link.
40
33
  * Based on knxd's Router::recv_L_Data and Router::trigger_cb logic.
@@ -42,8 +35,35 @@ export declare class Router extends EventEmitter {
42
35
  private processIncoming;
43
36
  private learnAddress;
44
37
  private route;
45
- private getSignature;
46
- private gcSignatures;
38
+ /**
39
+ * Evaluates if a message should be sent to a link based on configured filters.
40
+ * Logic: Skip if the policy is "discard all" and the address matches.
41
+ * Otherwise, send if the address is in the filter list or no filter exists.
42
+ */
43
+ private evaluateFilter;
44
+ private evaluateLocalFilter;
45
+ private evaluateIpFilter;
46
+ /**
47
+ * Sends data to a link with error handling.
48
+ */
49
+ private sendToLink;
50
+ /**
51
+ * Broadcasts a CEMI message to all registered links.
52
+ */
53
+ send(cemi: CEMIInstance): Promise<void>;
54
+ /**
55
+ * Send a GroupValue_Read telegram to a group address to all registered links.
56
+ * @param destination The group address (e.g., "1/1/1")
57
+ */
58
+ read(destination: string): Promise<void>;
59
+ /**
60
+ * Send a GroupValue_Write telegram to a group address.
61
+ * @param destination The group address (e.g., "1/1/1")
62
+ * @param value The value to write.
63
+ * @param dpt Optional Datapoint Type to help with encoding.
64
+ */
65
+ write<T extends (typeof KnxDataEncoder.dptEnum)[number] | string | null>(destination: string, dpt: T, value: AllDpts<T>): Promise<void>;
66
+ private gcDestinationAddress;
47
67
  connect(): Promise<void>;
48
68
  disconnect(): void;
49
69
  }
@@ -4,126 +4,114 @@ exports.Router = void 0;
4
4
  const events_1 = require("events");
5
5
  const TPUART_1 = require("./TPUART");
6
6
  const KNXTunneling_1 = require("./KNXTunneling");
7
+ const KNXUSBConnection_1 = require("./KNXUSBConnection");
7
8
  const Logger_1 = require("../utils/Logger");
9
+ const CEMI_1 = require("../core/CEMI");
10
+ const KNXnetIPServer_1 = require("./KNXnetIPServer");
11
+ const GroupAddressCache_1 = require("../core/cache/GroupAddressCache");
12
+ const KNXDataEncode_1 = require("../core/data/KNXDataEncode");
13
+ const ControlField_1 = require("../core/ControlField");
14
+ const ControlFieldExtended_1 = require("../core/ControlFieldExtended");
15
+ const TPDU_1 = require("../core/layers/data/TPDU");
16
+ const TPCI_1 = require("../core/layers/interfaces/TPCI");
17
+ const APDU_1 = require("../core/layers/data/APDU");
18
+ const APCI_1 = require("../core/layers/interfaces/APCI");
19
+ const APCIEnum_1 = require("../core/enum/APCIEnum");
8
20
  /**
9
- * Router: A high-performance, robust Learning Bridge.
10
- * Architecture strictly follows knxd/src/libserver/router.cpp patterns:
11
- * 1. Loop Prevention via signature tracking AND Repeat Flag analysis.
12
- * 2. IA Learning & Selective Routing.
13
- * 3. Source Address Patching & Sanitization.
14
- * 4. Client Address Pool Management (KNXnet/IP Tunneling).
21
+ * Router: A robust, high-performance learning bridge.
22
+ * The architecture is based on the patterns in the knxd repository at knxd/src/libserver/router.cpp:
23
+ * 1. Loop prevention through destination address tracking.
24
+ * 2. AI learning and selective routing.
25
+ * 3. Source address correction and sanitization.
26
+ * 4. Address filtering between KNXnetIP and other connections, and vice versa.
15
27
  */
16
28
  class Router extends events_1.EventEmitter {
17
- links = new Set();
29
+ links = new Map();
18
30
  addressTable = new Map();
19
31
  // knxd 'ignore' list: prevents infinite loops across different physical paths
20
32
  // Only ignores frames if they are marked as repeated in the KNX Control Field.
21
- recentSignatures = new Map();
33
+ recentDestinationAddress = new Map();
22
34
  MAX_SIGNATURES_SIZE = 10000;
23
35
  routerAddress = "15.15.0"; // Default, should be configurable
24
- // Dynamic address pool for clients (like KNXnet/IP Tunneling)
25
- clientAddrsStart = null;
26
- clientAddrsCount = 0;
27
- clientAddrsUsed = [];
28
36
  logger;
37
+ gcInterval;
38
+ toIPFilter = {};
39
+ toLocalFilter = {};
29
40
  constructor(options) {
30
41
  super();
31
- if (options.logOptions) {
32
- (0, Logger_1.setupLogger)(options.logOptions);
33
- }
34
42
  this.logger = Logger_1.knxLogger.child({ module: "Router" });
35
43
  if (options.routerAddress)
36
44
  this.routerAddress = options.routerAddress;
37
- // Parse client addrs e.g. "15.15.10:10"
38
- if (options.clientAddrs) {
39
- const parts = options.clientAddrs.split(":");
40
- if (parts.length === 2) {
41
- this.clientAddrsStart = parts[0];
42
- this.clientAddrsCount = parseInt(parts[1], 10);
43
- this.clientAddrsUsed = new Array(this.clientAddrsCount).fill(false);
44
- }
45
+ if (options.toIpFilter) {
46
+ this.toIPFilter = options.toIpFilter;
45
47
  }
46
- if (options.tpuart)
47
- this.registerLink(new TPUART_1.TPUARTConnection(options.tpuart));
48
- if (options.tunneling)
49
- options.tunneling.forEach((c) => this.registerLink(new KNXTunneling_1.KNXTunneling(c)));
50
- this.logger.info(`Router initialized at ${this.routerAddress}`);
51
- // Periodically clean the signature cache (knxd pattern)
52
- setInterval(() => this.gcSignatures(), 1000);
53
- }
54
- /**
55
- * Allocates a free physical address for a tunneling client.
56
- */
57
- getClientAddress() {
58
- if (!this.clientAddrsStart || this.clientAddrsCount === 0)
59
- return null;
60
- const startInt = this.addrToInt(this.clientAddrsStart);
61
- for (let i = 0; i < this.clientAddrsCount; i++) {
62
- if (!this.clientAddrsUsed[i]) {
63
- this.clientAddrsUsed[i] = true;
64
- return this.intToAddr(startInt + i);
65
- }
48
+ if (options.toLocalFilter) {
49
+ this.toLocalFilter = options.toLocalFilter;
66
50
  }
67
- return null; // Pool exhausted
68
- }
69
- /**
70
- * Releases an allocated physical address when a client disconnects.
71
- */
72
- releaseClientAddress(addr) {
73
- if (!this.clientAddrsStart || this.clientAddrsCount === 0)
74
- return;
75
- const startInt = this.addrToInt(this.clientAddrsStart);
76
- const addrInt = this.addrToInt(addr);
77
- const index = addrInt - startInt;
78
- if (index >= 0 && index < this.clientAddrsCount) {
79
- this.clientAddrsUsed[index] = false;
51
+ if (options.knxNetIpServer) {
52
+ options.knxNetIpServer.individualAddress = this.routerAddress;
53
+ const ipServer = new KNXnetIPServer_1.KNXnetIPServer(options.knxNetIpServer);
54
+ ipServer.isCacheDelegated = true;
55
+ ipServer.isEventsDelegated = true;
56
+ this.registerLink(`IP KNXnet/IP Server: ${ipServer.options.localIp}:${ipServer.options.port}`, ipServer);
80
57
  }
58
+ if (options.tpuart) {
59
+ options.tpuart.individualAddress = this.routerAddress;
60
+ this.registerLink("TPUART", new TPUART_1.TPUARTConnection(options.tpuart));
61
+ }
62
+ if (options.tunneling) {
63
+ options.tunneling.forEach((c) => {
64
+ // * Tunneling doesn't support individualAddress, is assigned by the tunnel connection
65
+ // c.individualAddress = this.routerAddress;
66
+ this.registerLink(`IP Tunneling: ${c.ip}:${c.port}`, new KNXTunneling_1.KNXTunneling(c));
67
+ });
68
+ }
69
+ if (options.usb) {
70
+ options.usb.individualAddress = this.routerAddress;
71
+ this.registerLink("KNXUSB", new KNXUSBConnection_1.KNXUSBConnection(options.usb));
72
+ }
73
+ this.logger.info(`Router initialized at ${this.routerAddress}`);
74
+ // Periodically clean the signature cache (knxd pattern)
75
+ this.gcInterval = setInterval(() => this.gcDestinationAddress(), 1000);
81
76
  }
82
- addrToInt(addr) {
83
- const [a, b, c] = addr.split(".").map(Number);
84
- return ((a & 0x0f) << 12) | ((b & 0x0f) << 8) | (c & 0xff);
85
- }
86
- intToAddr(val) {
87
- return `${(val >> 12) & 0x0f}.${(val >> 8) & 0x0f}.${val & 0xff}`;
88
- }
89
- registerLink(link) {
90
- if (this.links.has(link))
77
+ registerLink(key, link) {
78
+ if (this.links.has(key))
91
79
  return;
92
- this.links.add(link);
93
- this.logger.info(`Link registered: ${link.constructor.name}`);
80
+ this.links.set(key, link);
81
+ this.logger.info(`Link registered: ${key}`);
94
82
  link.on("indication", (cemi) => {
95
- this.processIncoming(cemi, link);
83
+ this.processIncoming(cemi, link, key);
96
84
  });
97
85
  link.on("error", (err) => {
98
- this.logger.error({ link: link.constructor.name, err: err.message }, "Link error");
86
+ this.logger.error({ link: key, err: err.message }, "Link error");
99
87
  this.emit("error", { link, error: err });
100
88
  });
101
89
  // knxd pattern: cleanup when link goes down
102
90
  link.on("disconnected", () => {
103
- this.logger.info(`Link disconnected: ${link.constructor.name}`);
104
- this.unregisterLink(link);
91
+ this.logger.info(`Link disconnected: ${key}`);
92
+ this.unregisterLink(key);
105
93
  });
106
94
  }
107
- unregisterLink(link) {
108
- if (!this.links.has(link))
95
+ unregisterLink(key) {
96
+ if (!this.links.has(key))
109
97
  return;
110
98
  // Cleanup routing table
111
99
  for (const [addr, l] of this.addressTable.entries()) {
112
- if (l === link) {
100
+ if (l.key === key) {
113
101
  this.addressTable.delete(addr);
114
- // If it was a dynamic client address, release it
115
- this.releaseClientAddress(addr);
116
102
  }
117
103
  }
118
- this.links.delete(link);
104
+ this.links.delete(key);
119
105
  }
120
106
  /**
121
107
  * Main entry point for any packet received from any link.
122
108
  * Based on knxd's Router::recv_L_Data and Router::trigger_cb logic.
123
109
  */
124
- processIncoming(cemi, source) {
125
- const cemiAny = cemi;
126
- let src = cemiAny.sourceAddress;
110
+ processIncoming(cemi, source, keySource) {
111
+ if (!("sourceAddress" in cemi))
112
+ return;
113
+ GroupAddressCache_1.GroupAddressCache.getInstance().processCEMI(cemi);
114
+ const src = cemi.sourceAddress;
127
115
  // 1. Source Validation (knxd pattern):
128
116
  // If we know this IA is on another link, discard to prevent loops/spoofing.
129
117
  if (src && src !== "0.0.0") {
@@ -132,137 +120,221 @@ class Router extends events_1.EventEmitter {
132
120
  return;
133
121
  }
134
122
  const existingLink = this.addressTable.get(src);
135
- if (existingLink && existingLink !== source) {
123
+ if (existingLink && existingLink.key !== keySource) {
136
124
  return; // Ignore packet from "wrong" interface
137
125
  }
138
126
  }
139
- // 2. Source Patching (Standard KNX Requirement)
140
- // If src is 0.0.0, it must be replaced by the router's/client's address
141
- if (!src || src === "0.0.0") {
142
- cemiAny.sourceAddress =
143
- "individualAddress" in source.options &&
144
- source.options.individualAddress
145
- ? source.options.individualAddress
146
- : this.routerAddress;
147
- src = cemiAny.sourceAddress;
148
- }
149
- // 3. IA Learning
150
- this.learnAddress(src, source);
151
- // 4. Loop Prevention (knxd strict pattern)
152
- const buf = cemi.toBuffer();
153
- // In CEMI, Ctrl1 is at offset `2 + AddIL`
154
- const addIL = buf[1];
155
- const ctrl1 = buf[2 + addIL];
127
+ // 2. IA Learning
128
+ this.learnAddress(src, source, keySource);
129
+ // 3. Loop Prevention (knxd strict pattern)
156
130
  // KNX Standard: Repeat bit is bit 5 (0x20). Active LOW (0 = repeated frame).
157
- const isRepeated = (ctrl1 & 0x20) === 0;
158
- const signature = this.getSignature(buf, addIL);
131
+ const isRepeated = !cemi.controlField1.repeat;
132
+ const destinationAddress = cemi.destinationAddress;
159
133
  if (isRepeated) {
160
- if (this.recentSignatures.has(signature)) {
161
- this.logger.debug({ signature, src }, "Loop prevented: duplicated repeated frame dropped");
134
+ if (this.recentDestinationAddress.has(destinationAddress)) {
135
+ this.logger.debug({ signature: destinationAddress, src }, "Loop prevented: duplicated repeated frame dropped");
162
136
  return; // Drop repeated packet we've recently seen on the bus
163
137
  }
164
138
  }
165
139
  // Always record the signature. If a loop occurs, the echoed packet will have
166
140
  // the repeat flag set to 0 (since it failed to ACK or was physically echoed),
167
141
  // and we will drop it next time.
168
- if (this.recentSignatures.size >= this.MAX_SIGNATURES_SIZE) {
169
- const firstKey = this.recentSignatures.keys().next().value;
142
+ if (this.recentDestinationAddress.size >= this.MAX_SIGNATURES_SIZE) {
143
+ const firstKey = this.recentDestinationAddress.keys().next().value;
170
144
  if (firstKey !== undefined)
171
- this.recentSignatures.delete(firstKey);
145
+ this.recentDestinationAddress.delete(firstKey);
172
146
  }
173
- this.recentSignatures.set(signature, Date.now());
147
+ this.recentDestinationAddress.set(destinationAddress, Date.now());
174
148
  // 5. Route
175
- this.route(cemi, source);
149
+ this.route(cemi, source, keySource);
176
150
  }
177
- learnAddress(src, source) {
151
+ learnAddress(src, source, keySource) {
178
152
  // knxd pattern: don't learn 0.0.0 or special 15.15.255 (0xFFFF) addresses
179
153
  if (src !== "0.0.0" && src !== "15.15.255") {
180
- if (this.addressTable.get(src) !== source) {
181
- this.addressTable.set(src, source);
182
- this.logger.debug(`Learned IA ${src} on link ${source.constructor.name}`);
154
+ if (this.addressTable.get(src)?.key !== keySource) {
155
+ this.addressTable.set(src, { link: source, key: keySource });
156
+ this.logger.debug(`Learned IA ${src} on link ${keySource}`);
183
157
  }
184
158
  }
185
159
  }
186
- route(cemi, source) {
187
- const cemiAny = cemi;
160
+ route(data, source, keySource) {
161
+ if (!("controlField2" in data))
162
+ return;
188
163
  // Hop Count Management (Protect the whole network)
189
- if (cemiAny.controlField2 &&
190
- typeof cemiAny.controlField2.hopCount === "number") {
191
- const hops = cemiAny.controlField2.hopCount;
164
+ if (data.controlField2 && typeof data.controlField2.hopCount === "number") {
165
+ const hops = data.controlField2.hopCount;
192
166
  if (hops === 0) {
193
- this.logger.debug({ src: cemiAny.sourceAddress, dst: cemiAny.destinationAddress }, "Packet dropped: hop count reached 0");
167
+ this.logger.debug({ src: data.sourceAddress, dst: data.destinationAddress }, "Packet dropped: hop count reached 0");
194
168
  return; // Drop packet
195
169
  }
196
170
  if (hops < 7)
197
- cemiAny.controlField2.hopCount = hops - 1;
171
+ data.controlField2.hopCount = hops - 1;
198
172
  }
199
- const isGroup = cemiAny.controlField2?.addressType === 1;
200
- const dest = cemiAny.destinationAddress;
173
+ const isGroup = data.controlField2?.addressType === 1;
174
+ const dest = data.destinationAddress;
201
175
  // If packet is destined for the router itself, consume it and don't route
202
176
  if (!isGroup && dest === this.routerAddress) {
203
- this.logger.debug({ src: cemiAny.sourceAddress }, "Packet consumed by router local address");
204
- this.emit("indication", cemi);
177
+ this.logger.debug({ src: data.sourceAddress }, "Packet consumed by router local address");
178
+ this.emit("indication_link", { src: source.constructor.name, msg: data });
205
179
  return;
206
180
  }
207
181
  // Selective Routing (IA)
208
182
  if (!isGroup && dest && dest !== "0.0.0" && dest !== "15.15.255") {
209
183
  const target = this.addressTable.get(dest);
210
184
  if (target) {
211
- if (target !== source) {
212
- target.send(cemi).catch((err) => {
213
- this.logger.debug({ target: target.constructor.name, err: err.message }, "Selective routing failed");
185
+ if (target.key !== keySource) {
186
+ target.link.send(data).catch((err) => {
187
+ this.logger.debug({ target: target.key, err: err.message }, "Selective routing failed");
214
188
  });
215
189
  }
216
190
  // Send to upper layers (KNXnet/IP server core)
217
- this.emit("indication", cemi);
191
+ this.emit("indication_link", { src: source.constructor.name, msg: data });
218
192
  return; // Do not flood
219
193
  }
220
194
  // If target is unknown, knxd broadcasts it to all interfaces
221
195
  }
196
+ const isSourceIP = keySource.includes("IP");
222
197
  // Flood to all links except source, respecting filters (knxd pattern)
223
- for (const link of this.links) {
224
- if (link === source)
198
+ for (const [key, link] of this.links) {
199
+ if (key === keySource)
225
200
  continue;
226
201
  // Avoid looping back to the physical source address if known via another route
227
- if (this.addressTable.get(cemiAny.sourceAddress) === link)
202
+ if (this.addressTable.get(data.sourceAddress)?.key === keySource)
228
203
  continue;
229
204
  // Check if the link should filter this message
230
- if (isGroup) {
231
- if (link.shouldFilterGroup && !link.shouldFilterGroup(dest))
232
- continue;
233
- }
234
- else {
235
- if (link.shouldFilterIA && !link.shouldFilterIA(dest))
236
- continue;
237
- }
238
- link.send(cemi).catch((err) => {
239
- this.logger.debug({ link: link.constructor.name, err: err.message }, "Flooding routing failed for link");
240
- });
205
+ const shouldSend = this.evaluateFilter(dest, isGroup, isSourceIP);
206
+ if (!shouldSend)
207
+ continue;
208
+ // Send to link
209
+ this.sendToLink(link, data);
241
210
  }
242
211
  // Notify upper layers
243
- this.emit("indication", cemi);
212
+ this.emit("indication_link", { src: source.constructor.name, msg: data });
213
+ }
214
+ /**
215
+ * Evaluates if a message should be sent to a link based on configured filters.
216
+ * Logic: Skip if the policy is "discard all" and the address matches.
217
+ * Otherwise, send if the address is in the filter list or no filter exists.
218
+ */
219
+ evaluateFilter(dest, isGroup, isSourceIP) {
220
+ if (isSourceIP) {
221
+ return this.evaluateLocalFilter(dest, isGroup);
222
+ }
223
+ else {
224
+ return this.evaluateIpFilter(dest, isGroup);
225
+ }
226
+ }
227
+ evaluateLocalFilter(dest, isGroup) {
228
+ if (isGroup) {
229
+ const filter = this.toLocalFilter?.groupAddress;
230
+ if (!filter?.addresses?.includes(dest))
231
+ return true;
232
+ return filter.groupAddressToLocalFilterPolicie !== "discard all";
233
+ }
234
+ else {
235
+ const filter = this.toLocalFilter?.individualAddress;
236
+ if (!filter?.addresses?.includes(dest))
237
+ return true;
238
+ return filter.individualAddressToLocalFilterPolicie !== "discard all";
239
+ }
244
240
  }
245
- getSignature(buf, addIL) {
246
- // Signature based on Dst + APCI + Data (knxd style)
247
- // We skip Src and HopCount because they change during routing
248
- // Payload starts after CEMI header (2) + AddIL + Ctrl1 (1) + Ctrl2 (1) + Src (2)
249
- // Actually, taking from Dst onwards is a solid signature.
250
- // Dst is at offset 2 + addIL + 4
251
- const dstOffset = 2 + addIL + 4;
252
- return buf.subarray(dstOffset).toString("hex");
241
+ evaluateIpFilter(dest, isGroup) {
242
+ if (isGroup) {
243
+ const filter = this.toIPFilter?.groupAddress;
244
+ if (!filter?.addresses?.includes(dest))
245
+ return true;
246
+ return filter.groupAddressToIpFilterPolicie !== "discard all";
247
+ }
248
+ else {
249
+ const filter = this.toIPFilter?.individualAddress;
250
+ if (!filter?.addresses?.includes(dest))
251
+ return true;
252
+ return filter.individualAddressToIpFilterPolicie !== "discard all";
253
+ }
254
+ }
255
+ /**
256
+ * Sends data to a link with error handling.
257
+ */
258
+ sendToLink(link, data) {
259
+ link.send(data).catch((err) => {
260
+ this.logger.debug({ link: link.constructor.name, err: err.message }, "Flooding routing failed for link");
261
+ });
262
+ }
263
+ /**
264
+ * Broadcasts a CEMI message to all registered links.
265
+ */
266
+ async send(cemi) {
267
+ const promises = Array.from(this.links.entries()).map(async ([key, link]) => {
268
+ try {
269
+ await link.send(cemi);
270
+ }
271
+ catch (err) {
272
+ this.logger.debug({ link: key, err: err.message }, "Broadcast failed for link");
273
+ }
274
+ });
275
+ await Promise.all(promises);
276
+ }
277
+ /**
278
+ * Send a GroupValue_Read telegram to a group address to all registered links.
279
+ * @param destination The group address (e.g., "1/1/1")
280
+ */
281
+ async read(destination) {
282
+ const cf1 = new ControlField_1.ControlField(0xbc);
283
+ const cf2 = new ControlFieldExtended_1.ExtendedControlField(0xe0);
284
+ const tpdu = new TPDU_1.TPDU(new TPCI_1.TPCI(TPCI_1.TPCIType.T_DATA_GROUP_PDU), new APDU_1.APDU(new TPCI_1.TPCI(TPCI_1.TPCIType.T_DATA_GROUP_PDU), new APCI_1.APCI(APCIEnum_1.APCIEnum.A_GroupValue_Read_Protocol_Data_Unit), Buffer.alloc(0), true), Buffer.alloc(0));
285
+ const cemi = new CEMI_1.CEMI.DataLinkLayerCEMI["L_Data.req"](null, cf1, cf2, this.routerAddress, destination, tpdu);
286
+ this.logger.debug({ service: cemi.constructor.name }, "Sending GroupValue_Read");
287
+ return this.send(cemi);
288
+ }
289
+ /**
290
+ * Send a GroupValue_Write telegram to a group address.
291
+ * @param destination The group address (e.g., "1/1/1")
292
+ * @param value The value to write.
293
+ * @param dpt Optional Datapoint Type to help with encoding.
294
+ */
295
+ async write(destination, dpt, value) {
296
+ let data;
297
+ let isShort = false;
298
+ // data validation
299
+ if (dpt !== undefined) {
300
+ data = KNXDataEncode_1.KnxDataEncoder.encodeThis(dpt, value);
301
+ isShort = KNXDataEncode_1.KnxDataEncoder.isShortDpt(dpt);
302
+ }
303
+ else if (typeof value === "boolean") {
304
+ data = Buffer.from([value ? 1 : 0]);
305
+ isShort = true;
306
+ }
307
+ else if (Buffer.isBuffer(value)) {
308
+ data = value;
309
+ isShort = data.length === 1 && data[0] <= 0x3f;
310
+ }
311
+ else if (typeof value === "number") {
312
+ data = Buffer.from([value]);
313
+ isShort = value <= 0x3f;
314
+ }
315
+ else {
316
+ throw new Error("Cannot encode value without DPT or basic type (boolean/number/Buffer)");
317
+ }
318
+ const cf1 = new ControlField_1.ControlField(0xbc);
319
+ const cf2 = new ControlFieldExtended_1.ExtendedControlField(0xe0);
320
+ const tpdu = new TPDU_1.TPDU(new TPCI_1.TPCI(TPCI_1.TPCIType.T_DATA_GROUP_PDU), new APDU_1.APDU(new TPCI_1.TPCI(TPCI_1.TPCIType.T_DATA_GROUP_PDU), new APCI_1.APCI(APCIEnum_1.APCIEnum.A_GroupValue_Write_Protocol_Data_Unit), data, isShort), data);
321
+ const cemi = new CEMI_1.CEMI.DataLinkLayerCEMI["L_Data.req"](null, cf1, cf2, this.routerAddress, destination, tpdu);
322
+ this.logger.debug({ service: cemi.constructor.name }, "Sending GroupValue_Write");
323
+ return this.send(cemi);
253
324
  }
254
- gcSignatures() {
325
+ gcDestinationAddress() {
255
326
  const now = Date.now();
256
- for (const [sig, time] of this.recentSignatures) {
327
+ for (const [sig, time] of this.recentDestinationAddress) {
257
328
  // knxd keeps them for 1 second
258
329
  if (now - time > 1000)
259
- this.recentSignatures.delete(sig);
330
+ this.recentDestinationAddress.delete(sig);
260
331
  }
261
332
  }
262
333
  async connect() {
263
- await Promise.all(Array.from(this.links).map((l) => l.connect()));
334
+ await Promise.all(Array.from(this.links.values()).map((link) => link.connect()));
264
335
  }
265
336
  disconnect() {
337
+ clearInterval(this.gcInterval);
266
338
  this.links.forEach((l) => l.disconnect());
267
339
  }
268
340
  }
@@ -1,10 +1,11 @@
1
1
  import { KNXService } from "./KNXService";
2
2
  import { TPUARTOptions } from "../@types/interfaces/connection";
3
- import { ServiceMessage } from "../@types/interfaces/ServiceMessage";
4
- export declare class TPUARTConnection extends KNXService {
3
+ import { CEMIInstance } from "../core/CEMI";
4
+ export declare class TPUARTConnection extends KNXService<TPUARTOptions> {
5
5
  private serialPort;
6
6
  private receiver;
7
7
  private connectionState;
8
+ private isOpening;
8
9
  private initPromise;
9
10
  private msgQueue;
10
11
  private isProcessing;
@@ -21,8 +22,12 @@ export declare class TPUARTConnection extends KNXService {
21
22
  connect(): Promise<void>;
22
23
  private sendResetRequest;
23
24
  disconnect(): Promise<void>;
25
+ /**
26
+ * Enable or disable busmonitor mode.
27
+ * @param enabled
28
+ */
24
29
  setBusmonitor(enabled: boolean): Promise<void>;
25
- send(data: Buffer | ServiceMessage): Promise<void>;
30
+ send(data: Buffer | CEMIInstance): Promise<void>;
26
31
  private enqueueFrame;
27
32
  private processQueue;
28
33
  private toUartServices;