UNPKG

knxnetjs

Version:

A TypeScript library for KNXnet/IP communication

437 lines 19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KNXNetTunnelingImpl = void 0; const events_1 = require("events"); const dgram_1 = require("dgram"); const types_1 = require("../types"); const hpai_1 = require("../types/hpai"); const constants_1 = require("../constants"); const frames_1 = require("../frames"); const cemi_properties_1 = require("../frames/cemi-properties"); 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; } async writeProperty(interfaceObject, objectInstance, propertyId, numberOfElements, startIndex, data) { if (!this.isConnected || !this.socket || !this.serverEndpoint) { throw new Error("Not connected to tunneling server"); } const propertyWrite = new cemi_properties_1.CEMIPropertyWrite(interfaceObject, objectInstance, propertyId, numberOfElements, startIndex, data); const configFrame = this.createDeviceConfigurationFrame(propertyWrite.toBuffer()); await new Promise((resolve, reject) => { this.socket.send(configFrame, this.serverEndpoint.port, this.serverEndpoint.address, (err) => { if (err) { reject(err); } else { resolve(); } }); }); return new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, 2000); }); } async readProperty(interfaceObject, objectInstance, propertyId, numberOfElements, startIndex) { if (!this.isConnected || !this.socket || !this.serverEndpoint) { throw new Error("Not connected to tunneling server"); } const propertyRead = new cemi_properties_1.CEMIPropertyReadReq(interfaceObject, objectInstance, propertyId, numberOfElements, startIndex); const configFrame = this.createDeviceConfigurationFrame(propertyRead.toBuffer()); return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error("Property read timeout")); }, 5000); // Set up one-time response listener for property read confirmation const responseHandler = (frame) => { if (frame.messageCode === frames_1.CEMIMessageCode.M_PROP_READ_CON) { clearTimeout(timeout); this.off("recv", responseHandler); // Return the frame data which contains the property value resolve(frame.data); } }; this.on("recv", responseHandler); this.socket.send(configFrame, this.serverEndpoint.port, this.serverEndpoint.address, (err) => { if (err) { clearTimeout(timeout); this.off("recv", responseHandler); reject(err); } }); }); } 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 = new types_1.HPAI(hpai_1.HostProtocol.IPV4_UDP, "0.0.0.0", // Any interface 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); } }); }); } createDeviceConfigurationFrame(propertyData) { // Connection Header (4 bytes) const connectionHeader = Buffer.allocUnsafe(2); connectionHeader.writeUInt8(2, 0); // Structure length connectionHeader.writeUInt8(3, 1); // Device management connection const payload = Buffer.concat([connectionHeader, propertyData]); const frame = new frames_1.KNXnetIPFrame(constants_1.KNX_CONSTANTS.SERVICE_TYPES.DEVICE_CONFIGURATION_REQUEST, payload); return frame.toBuffer(); } createConnectRequestFrame() { // Control Endpoint HPAI (8 bytes) const controlHpai = this.localEndpoint.toBuffer(); // Data Endpoint HPAI (8 bytes) const dataHpai = this.localEndpoint.toBuffer(); // 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 const payload = Buffer.concat([controlHpai, dataHpai, cri]); const frame = new frames_1.KNXnetIPFrame(constants_1.KNX_CONSTANTS.SERVICE_TYPES.CONNECT_REQUEST, payload); return frame.toBuffer(); } createTunnelingRequestFrame(cemiFrame) { const currentSeq = this.sequenceCounter; this.sequenceCounter = (this.sequenceCounter + 1) & 0xff; // 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 const payload = Buffer.concat([connectionHeader, cemiFrame]); const frame = new frames_1.KNXnetIPFrame(constants_1.KNX_CONSTANTS.SERVICE_TYPES.TUNNELLING_REQUEST, payload); return frame.toBuffer(); } parseHPAI(buffer, offset) { // Parse HPAI from buffer at offset const hapiBuffer = buffer.subarray(offset, offset + 8); return types_1.HPAI.fromBuffer(hapiBuffer); } 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) { try { const frame = frames_1.KNXnetIPFrame.fromBuffer(msg); return frame.service === constants_1.KNX_CONSTANTS.SERVICE_TYPES.TUNNELLING_ACK; } catch { return false; } } 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; if (status != constants_1.KNX_CONSTANTS.ERROR_CODES.E_NO_ERROR) { return { status, connectionId, dataEndpoint: new types_1.HPAI(hpai_1.HostProtocol.IPV4_UDP, "0.0.0.0", 0), }; } // 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 let finalEndpoint = dataEndpoint; if (dataEndpoint.address === "0.0.0.0" || dataEndpoint.port === 0) { finalEndpoint = new types_1.HPAI(dataEndpoint.hostProtocol, dataEndpoint.address === "0.0.0.0" ? rinfo.address : dataEndpoint.address, dataEndpoint.port === 0 ? rinfo.port : dataEndpoint.port); } return { status, connectionId, dataEndpoint: finalEndpoint }; } parseTunnelingAck(msg) { const frame = frames_1.KNXnetIPFrame.fromBuffer(msg); // Status is at offset 3 in payload (4 byte connection header - 1 for status position) return frame.payload.readUInt8(3); } 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; } // Connection Header (4 bytes) const connectionHeader = Buffer.allocUnsafe(4); connectionHeader.writeUInt8(4, 0); // Structure length connectionHeader.writeUInt8(connectionId, 1); connectionHeader.writeUInt8(sequenceCounter, 2); connectionHeader.writeUInt8(status, 3); const frame = new frames_1.KNXnetIPFrame(constants_1.KNX_CONSTANTS.SERVICE_TYPES.TUNNELLING_ACK, connectionHeader); const ackFrame = frame.toBuffer(); 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; } // Connection ID and Status (2 bytes) const payload = Buffer.allocUnsafe(2); payload.writeUInt8(this.connectionId, 0); payload.writeUInt8(status, 1); const frame = new frames_1.KNXnetIPFrame(constants_1.KNX_CONSTANTS.SERVICE_TYPES.CONNECTIONSTATE_RESPONSE, payload); const responseFrame = frame.toBuffer(); this.socket.send(responseFrame, this.serverEndpoint.port, this.serverEndpoint.address); } async sendDisconnectRequest() { if (!this.socket || !this.serverEndpoint) { return; } // Connection ID and Reserved byte (2 bytes) const connectionInfo = Buffer.allocUnsafe(2); connectionInfo.writeUInt8(this.connectionId, 0); connectionInfo.writeUInt8(0, 1); // Reserved // Control Endpoint HPAI (8 bytes) const controlHpai = this.localEndpoint.toBuffer(); const payload = Buffer.concat([connectionInfo, controlHpai]); const frame = new frames_1.KNXnetIPFrame(constants_1.KNX_CONSTANTS.SERVICE_TYPES.DISCONNECT_REQUEST, payload); const disconnectFrame = frame.toBuffer(); 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; } // Connection ID and Reserved byte (2 bytes) const connectionInfo = Buffer.allocUnsafe(2); connectionInfo.writeUInt8(this.connectionId, 0); connectionInfo.writeUInt8(0, 1); // Reserved // Control Endpoint HPAI (8 bytes) const controlHpai = this.localEndpoint.toBuffer(); const payload = Buffer.concat([connectionInfo, controlHpai]); const frame = new frames_1.KNXnetIPFrame(constants_1.KNX_CONSTANTS.SERVICE_TYPES.CONNECTIONSTATE_REQUEST, payload); const requestFrame = frame.toBuffer(); 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