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
|
@@ -1,40 +1,33 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import { KNXService } from "./KNXService";
|
|
3
|
-
import {
|
|
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
|
|
6
|
-
*
|
|
7
|
-
* 1. Loop
|
|
8
|
-
* 2.
|
|
9
|
-
* 3. Source
|
|
10
|
-
* 4.
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
private clientAddrsStart;
|
|
19
|
-
private clientAddrsCount;
|
|
20
|
-
private clientAddrsUsed;
|
|
23
|
+
routerAddress: string;
|
|
21
24
|
private logger;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
10
|
-
*
|
|
11
|
-
* 1. Loop
|
|
12
|
-
* 2.
|
|
13
|
-
* 3. Source
|
|
14
|
-
* 4.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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.
|
|
47
|
-
this.
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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.
|
|
93
|
-
this.logger.info(`Link registered: ${
|
|
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:
|
|
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: ${
|
|
104
|
-
this.unregisterLink(
|
|
91
|
+
this.logger.info(`Link disconnected: ${key}`);
|
|
92
|
+
this.unregisterLink(key);
|
|
105
93
|
});
|
|
106
94
|
}
|
|
107
|
-
unregisterLink(
|
|
108
|
-
if (!this.links.has(
|
|
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 ===
|
|
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(
|
|
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
|
-
|
|
126
|
-
|
|
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 !==
|
|
123
|
+
if (existingLink && existingLink.key !== keySource) {
|
|
136
124
|
return; // Ignore packet from "wrong" interface
|
|
137
125
|
}
|
|
138
126
|
}
|
|
139
|
-
// 2.
|
|
140
|
-
|
|
141
|
-
|
|
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 =
|
|
158
|
-
const
|
|
131
|
+
const isRepeated = !cemi.controlField1.repeat;
|
|
132
|
+
const destinationAddress = cemi.destinationAddress;
|
|
159
133
|
if (isRepeated) {
|
|
160
|
-
if (this.
|
|
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.
|
|
169
|
-
const firstKey = this.
|
|
142
|
+
if (this.recentDestinationAddress.size >= this.MAX_SIGNATURES_SIZE) {
|
|
143
|
+
const firstKey = this.recentDestinationAddress.keys().next().value;
|
|
170
144
|
if (firstKey !== undefined)
|
|
171
|
-
this.
|
|
145
|
+
this.recentDestinationAddress.delete(firstKey);
|
|
172
146
|
}
|
|
173
|
-
this.
|
|
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) !==
|
|
181
|
-
this.addressTable.set(src, source);
|
|
182
|
-
this.logger.debug(`Learned IA ${src} on link ${
|
|
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(
|
|
187
|
-
|
|
160
|
+
route(data, source, keySource) {
|
|
161
|
+
if (!("controlField2" in data))
|
|
162
|
+
return;
|
|
188
163
|
// Hop Count Management (Protect the whole network)
|
|
189
|
-
if (
|
|
190
|
-
|
|
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:
|
|
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
|
-
|
|
171
|
+
data.controlField2.hopCount = hops - 1;
|
|
198
172
|
}
|
|
199
|
-
const isGroup =
|
|
200
|
-
const dest =
|
|
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:
|
|
204
|
-
this.emit("
|
|
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 !==
|
|
212
|
-
target.send(
|
|
213
|
-
this.logger.debug({ target: target.
|
|
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("
|
|
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 (
|
|
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(
|
|
202
|
+
if (this.addressTable.get(data.sourceAddress)?.key === keySource)
|
|
228
203
|
continue;
|
|
229
204
|
// Check if the link should filter this message
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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("
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
325
|
+
gcDestinationAddress() {
|
|
255
326
|
const now = Date.now();
|
|
256
|
-
for (const [sig, time] of this.
|
|
327
|
+
for (const [sig, time] of this.recentDestinationAddress) {
|
|
257
328
|
// knxd keeps them for 1 second
|
|
258
329
|
if (now - time > 1000)
|
|
259
|
-
this.
|
|
330
|
+
this.recentDestinationAddress.delete(sig);
|
|
260
331
|
}
|
|
261
332
|
}
|
|
262
333
|
async connect() {
|
|
263
|
-
await Promise.all(Array.from(this.links).map((
|
|
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 {
|
|
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 |
|
|
30
|
+
send(data: Buffer | CEMIInstance): Promise<void>;
|
|
26
31
|
private enqueueFrame;
|
|
27
32
|
private processQueue;
|
|
28
33
|
private toUartServices;
|