knxnetjs
Version:
A TypeScript library for KNXnet/IP communication
437 lines • 19 kB
JavaScript
"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