UNPKG

knxnetjs

Version:

A TypeScript library for KNXnet/IP communication

413 lines 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KNXNetTunnelingImpl = void 0; const events_1 = require("events"); const dgram_1 = require("dgram"); const constants_1 = require("../constants"); const frames_1 = require("../frames"); class KNXNetTunnelingImpl extends events_1.EventEmitter { constructor(serverAddress, serverPort, localPort, busmonitorMode) { super(); this.isConnected = false; this.connectionId = 0; this.sequenceCounter = 0; this.busmonitorMode = busmonitorMode ?? false; this.options = { serverAddress, serverPort: serverPort ?? constants_1.KNX_CONSTANTS.DEFAULT_PORT, localPort: localPort ?? 0, heartbeatInterval: constants_1.KNX_CONSTANTS.TUNNELING.DEFAULT_HEARTBEAT_INTERVAL, connectionTimeout: constants_1.KNX_CONSTANTS.TUNNELING.DEFAULT_CONNECTION_TIMEOUT, busmonitorMode: this.busmonitorMode, }; } async open() { if (this.isConnected) { return; } try { await this.setupSocket(); await this.establishConnection(); this.startHeartbeat(); this.isConnected = true; } catch (error) { await this.cleanup(); throw error; } } async send(frame) { if (!this.isConnected || !this.socket || !this.serverEndpoint) { throw new Error("Not connected to tunneling server"); } // In busmonitor mode, typically no frames are sent - this is monitor-only if (this.busmonitorMode) { throw new Error("Cannot send frames in busmonitor mode - this is a monitor-only connection"); } const tunnelFrame = this.createTunnelingRequestFrame(frame.toBuffer()); return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error("Tunneling request timeout")); }, this.options.connectionTimeout); const handleAck = (msg, rinfo) => { if (this.isTunnelingAck(msg)) { clearTimeout(timeoutId); this.socket.off("message", handleAck); const status = this.parseTunnelingAck(msg); if (status === constants_1.KNX_CONSTANTS.ERROR_CODES.E_NO_ERROR) { resolve(); } else { reject(new Error(`Tunneling error: 0x${status.toString(16)}`)); } } }; this.socket.on("message", handleAck); this.socket.send(tunnelFrame, this.serverEndpoint.port, this.serverEndpoint.address, (err) => { if (err) { clearTimeout(timeoutId); this.socket.off("message", handleAck); reject(err); } }); }); } async close() { if (!this.isConnected) { return; } try { await this.sendDisconnectRequest(); } catch (error) { // Ignore disconnect errors } await this.cleanup(); } /** * Check if this connection is in busmonitor mode */ isBusmonitorMode() { return this.busmonitorMode; } on(event, listener) { return super.on(event, listener); } async setupSocket() { return new Promise((resolve, reject) => { this.socket = (0, dgram_1.createSocket)({ type: "udp4", reuseAddr: true }); this.socket.on("error", (err) => { this.emit("error", err); reject(err); }); this.socket.on("message", (msg, rinfo) => { this.handleIncomingMessage(msg, rinfo); }); this.socket.on("listening", () => { const address = this.socket.address(); this.localEndpoint = { hostProtocol: 0x01, // UDP address: "0.0.0.0", // Any interface port: address.port, }; resolve(); }); // Bind to specified port or let system choose available port if (this.options.localPort === 0) { this.socket.bind(); // Let system choose port } else { this.socket.bind(this.options.localPort); } }); } async establishConnection() { const connectFrame = this.createConnectRequestFrame(); return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error("Connection timeout")); }, this.options.connectionTimeout); const handleResponse = (msg, rinfo) => { if (this.isConnectResponse(msg)) { clearTimeout(timeoutId); this.socket.off("message", handleResponse); try { const result = this.parseConnectResponse(msg, rinfo); if (result.status === constants_1.KNX_CONSTANTS.ERROR_CODES.E_NO_ERROR) { this.connectionId = result.connectionId; this.serverEndpoint = result.dataEndpoint; resolve(); } else { reject(new Error(`Connection failed: 0x${result.status.toString(16)}`)); } } catch (error) { reject(error); } } }; this.socket.on("message", handleResponse); this.socket.send(connectFrame, this.options.serverPort, this.options.serverAddress, (err) => { if (err) { clearTimeout(timeoutId); this.socket.off("message", handleResponse); reject(err); } }); }); } createConnectRequestFrame() { // KNXnet/IP Header (6 bytes) const header = Buffer.allocUnsafe(6); header.writeUInt8(constants_1.KNX_CONSTANTS.HEADER_SIZE, 0); header.writeUInt8(constants_1.KNX_CONSTANTS.KNXNETIP_VERSION, 1); header.writeUInt16BE(constants_1.KNX_CONSTANTS.SERVICE_TYPES.CONNECT_REQUEST, 2); header.writeUInt16BE(26, 4); // Total length: 6 + 8 + 8 + 4 // Control Endpoint HPAI (8 bytes) const controlHpai = this.createHPAI(this.localEndpoint); // Data Endpoint HPAI (8 bytes) const dataHpai = this.createHPAI(this.localEndpoint); // Connection Request Information (4 bytes) const cri = Buffer.allocUnsafe(4); cri.writeUInt8(4, 0); // Structure length cri.writeUInt8(constants_1.KNX_CONSTANTS.TUNNELING.CONNECTION_TYPE, 1); // Always tunneling connection type // Use appropriate layer type based on busmonitor mode const layerType = this.busmonitorMode ? constants_1.KNX_CONSTANTS.TUNNELING.LAYER_TYPE_BUSMONITOR : constants_1.KNX_CONSTANTS.TUNNELING.LAYER_TYPE_TUNNEL_LINKLAYER; cri.writeUInt8(layerType, 2); // Layer type cri.writeUInt8(0, 3); // Reserved return Buffer.concat([header, controlHpai, dataHpai, cri]); } createTunnelingRequestFrame(cemiFrame) { const currentSeq = this.sequenceCounter; this.sequenceCounter = (this.sequenceCounter + 1) & 0xff; // KNXnet/IP Header (6 bytes) const totalLength = 6 + 4 + cemiFrame.length; const header = Buffer.allocUnsafe(6); header.writeUInt8(constants_1.KNX_CONSTANTS.HEADER_SIZE, 0); header.writeUInt8(constants_1.KNX_CONSTANTS.KNXNETIP_VERSION, 1); header.writeUInt16BE(constants_1.KNX_CONSTANTS.SERVICE_TYPES.TUNNELLING_REQUEST, 2); header.writeUInt16BE(totalLength, 4); // Connection Header (4 bytes) const connectionHeader = Buffer.allocUnsafe(4); connectionHeader.writeUInt8(4, 0); // Structure length connectionHeader.writeUInt8(this.connectionId, 1); connectionHeader.writeUInt8(currentSeq, 2); connectionHeader.writeUInt8(0, 3); // Reserved return Buffer.concat([header, connectionHeader, cemiFrame]); } createHPAI(endpoint) { const hpai = Buffer.allocUnsafe(8); hpai.writeUInt8(8, 0); // Structure length hpai.writeUInt8(endpoint.hostProtocol, 1); const ipParts = endpoint.address === "0.0.0.0" ? [0, 0, 0, 0] : endpoint.address.split(".").map((x) => parseInt(x, 10)); hpai.writeUInt8(ipParts[0] || 0, 2); hpai.writeUInt8(ipParts[1] || 0, 3); hpai.writeUInt8(ipParts[2] || 0, 4); hpai.writeUInt8(ipParts[3] || 0, 5); hpai.writeUInt16BE(endpoint.port, 6); return hpai; } parseHPAI(buffer, offset) { const hostProtocol = buffer.readUInt8(offset + 1); const ip = [ buffer.readUInt8(offset + 2), buffer.readUInt8(offset + 3), buffer.readUInt8(offset + 4), buffer.readUInt8(offset + 5), ].join("."); const port = buffer.readUInt16BE(offset + 6); return { hostProtocol, address: ip, port, }; } isConnectResponse(msg) { if (msg.length < 6) return false; const serviceType = msg.readUInt16BE(2); return serviceType === constants_1.KNX_CONSTANTS.SERVICE_TYPES.CONNECT_RESPONSE; } isTunnelingAck(msg) { if (msg.length < 6) return false; const serviceType = msg.readUInt16BE(2); return serviceType === constants_1.KNX_CONSTANTS.SERVICE_TYPES.TUNNELLING_ACK; } parseConnectResponse(msg, rinfo) { let offset = constants_1.KNX_CONSTANTS.HEADER_SIZE; // Connection ID (1 byte) const connectionId = msg.readUInt8(offset); offset += 1; // Status (1 byte) const status = msg.readUInt8(offset); offset += 1; // Data Endpoint HPAI (8 bytes) const dataEndpoint = this.parseHPAI(msg, offset); // Use server's address and port if data endpoint is 0.0.0.0:0 if (dataEndpoint.address === "0.0.0.0") { dataEndpoint.address = rinfo.address; } if (dataEndpoint.port === 0) { dataEndpoint.port = rinfo.port; } return { status, connectionId, dataEndpoint }; } parseTunnelingAck(msg) { // Status is at offset 9 (6 byte header + 4 byte connection header - 1 for status position) return msg.readUInt8(9); } handleIncomingMessage(msg, _rinfo) { try { if (msg.length < constants_1.KNX_CONSTANTS.HEADER_SIZE) { return; } const serviceType = msg.readUInt16BE(2); switch (serviceType) { case constants_1.KNX_CONSTANTS.SERVICE_TYPES.TUNNELLING_REQUEST: this.handleTunnelingRequest(msg); break; case constants_1.KNX_CONSTANTS.SERVICE_TYPES.CONNECTIONSTATE_REQUEST: this.handleConnectionStateRequest(msg); break; // CONNECT_RESPONSE and TUNNELLING_ACK are handled by specific listeners } } catch (error) { this.emit("error", error); } } handleTunnelingRequest(msg) { let offset = constants_1.KNX_CONSTANTS.HEADER_SIZE; // Connection Header (4 bytes) const connectionId = msg.readUInt8(offset + 1); const sequenceCounter = msg.readUInt8(offset + 2); offset += 4; // Send ACK this.sendTunnelingAck(connectionId, sequenceCounter, constants_1.KNX_CONSTANTS.ERROR_CODES.E_NO_ERROR); // Extract cEMI frame const cemiFrameBuffer = msg.subarray(offset); if (frames_1.CEMIFrame.isValidBuffer(cemiFrameBuffer)) { const cemiFrame = frames_1.CEMIFrame.fromBuffer(cemiFrameBuffer); this.emit("recv", cemiFrame); } else { // For invalid frames, create a basic cEMI frame wrapper try { const cemiFrame = frames_1.CEMIFrame.fromBuffer(cemiFrameBuffer); this.emit("recv", cemiFrame); } catch (error) { this.emit("error", new Error(`Invalid cEMI frame received: ${error.message}`)); } } } sendTunnelingAck(connectionId, sequenceCounter, status) { if (!this.socket || !this.serverEndpoint) { return; } const ackFrame = Buffer.allocUnsafe(10); // KNXnet/IP Header ackFrame.writeUInt8(constants_1.KNX_CONSTANTS.HEADER_SIZE, 0); ackFrame.writeUInt8(constants_1.KNX_CONSTANTS.KNXNETIP_VERSION, 1); ackFrame.writeUInt16BE(constants_1.KNX_CONSTANTS.SERVICE_TYPES.TUNNELLING_ACK, 2); ackFrame.writeUInt16BE(10, 4); // Connection Header ackFrame.writeUInt8(4, 6); ackFrame.writeUInt8(connectionId, 7); ackFrame.writeUInt8(sequenceCounter, 8); ackFrame.writeUInt8(status, 9); this.socket.send(ackFrame, this.serverEndpoint.port, this.serverEndpoint.address); } handleConnectionStateRequest(_msg) { // Send connection state response this.sendConnectionStateResponse(constants_1.KNX_CONSTANTS.ERROR_CODES.E_NO_ERROR); } sendConnectionStateResponse(status) { if (!this.socket || !this.serverEndpoint) { return; } const responseFrame = Buffer.allocUnsafe(8); // KNXnet/IP Header responseFrame.writeUInt8(constants_1.KNX_CONSTANTS.HEADER_SIZE, 0); responseFrame.writeUInt8(constants_1.KNX_CONSTANTS.KNXNETIP_VERSION, 1); responseFrame.writeUInt16BE(constants_1.KNX_CONSTANTS.SERVICE_TYPES.CONNECTIONSTATE_RESPONSE, 2); responseFrame.writeUInt16BE(8, 4); // Connection ID and Status responseFrame.writeUInt8(this.connectionId, 6); responseFrame.writeUInt8(status, 7); this.socket.send(responseFrame, this.serverEndpoint.port, this.serverEndpoint.address); } async sendDisconnectRequest() { if (!this.socket || !this.serverEndpoint) { return; } const disconnectFrame = Buffer.allocUnsafe(16); // KNXnet/IP Header disconnectFrame.writeUInt8(constants_1.KNX_CONSTANTS.HEADER_SIZE, 0); disconnectFrame.writeUInt8(constants_1.KNX_CONSTANTS.KNXNETIP_VERSION, 1); disconnectFrame.writeUInt16BE(constants_1.KNX_CONSTANTS.SERVICE_TYPES.DISCONNECT_REQUEST, 2); disconnectFrame.writeUInt16BE(16, 4); // Connection ID disconnectFrame.writeUInt8(this.connectionId, 6); disconnectFrame.writeUInt8(0, 7); // Reserved // Control Endpoint HPAI const controlHpai = this.createHPAI(this.localEndpoint); controlHpai.copy(disconnectFrame, 8); return new Promise((resolve) => { this.socket.send(disconnectFrame, this.serverEndpoint.port, this.serverEndpoint.address, () => { resolve(); }); }); } startHeartbeat() { this.heartbeatTimer = setInterval(() => { this.sendConnectionStateRequest(); }, this.options.heartbeatInterval); } sendConnectionStateRequest() { if (!this.socket || !this.serverEndpoint) { return; } const requestFrame = Buffer.allocUnsafe(16); // KNXnet/IP Header requestFrame.writeUInt8(constants_1.KNX_CONSTANTS.HEADER_SIZE, 0); requestFrame.writeUInt8(constants_1.KNX_CONSTANTS.KNXNETIP_VERSION, 1); requestFrame.writeUInt16BE(constants_1.KNX_CONSTANTS.SERVICE_TYPES.CONNECTIONSTATE_REQUEST, 2); requestFrame.writeUInt16BE(16, 4); // Connection ID requestFrame.writeUInt8(this.connectionId, 6); requestFrame.writeUInt8(0, 7); // Reserved // Control Endpoint HPAI const controlHpai = this.createHPAI(this.localEndpoint); controlHpai.copy(requestFrame, 8); this.socket.send(requestFrame, this.serverEndpoint.port, this.serverEndpoint.address); } async cleanup() { this.isConnected = false; if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = undefined; } if (this.connectionTimer) { clearTimeout(this.connectionTimer); this.connectionTimer = undefined; } if (this.socket) { this.socket.removeAllListeners(); this.socket.close(); this.socket = undefined; } this.connectionId = 0; this.sequenceCounter = 0; this.serverEndpoint = undefined; this.localEndpoint = undefined; } } exports.KNXNetTunnelingImpl = KNXNetTunnelingImpl; //# sourceMappingURL=tunneling.js.map