knxnetjs
Version:
A TypeScript library for KNXnet/IP communication
413 lines • 17.2 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 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