knxultimate
Version:
KNX IP protocol implementation for Node. This is the ENGINE of Node-Red KNX-Ultimate node.
1,704 lines (1,559 loc) • 141 kB
text/typescript
/**
* Implements the main KNX client managing tunnelling and routing.
*
* Written in Italy with love, sun and passion, by Massimo Saccani.
*
* Released under the MIT License.
* Use at your own risk; the author assumes no liability for damages.
*/
import dgram, { RemoteInfo, Socket as UDPSocket } from 'dgram'
import net, { Socket as TCPSocket } from 'net'
import * as crypto from 'crypto'
import { ConnectionStatus, KNX_CONSTANTS } from './protocol/KNXConstants'
import CEMIConstants from './protocol/cEMI/CEMIConstants'
import CEMIFactory from './protocol/cEMI/CEMIFactory'
import CEMIMessage from './protocol/cEMI/CEMIMessage'
import KNXProtocol, { KnxMessage, KnxResponse } from './protocol/KNXProtocol'
import KNXConnectResponse from './protocol/KNXConnectResponse'
import HPAI, { KnxProtocol } from './protocol/HPAI'
import TunnelCRI, { TunnelTypes } from './protocol/TunnelCRI'
import KNXConnectionStateResponse from './protocol/KNXConnectionStateResponse'
import * as errors from './errors'
import * as ipAddressHelper from './util/ipAddressHelper'
import KNXAddress from './protocol/KNXAddress'
import KNXDataBuffer, { IDataPoint } from './protocol/KNXDataBuffer'
import * as DPTLib from './dptlib'
import KnxLog, {
KNXLogger,
LogLevel,
module as createLogger,
setLogLevel,
KNXLoggerOptions,
} from './KnxLog'
import { KNXDescriptionResponse, KNXPacket } from './protocol'
import KNXRoutingIndication from './protocol/KNXRoutingIndication'
import KNXConnectRequest from './protocol/KNXConnectRequest'
import KNXTunnelingRequest from './protocol/KNXTunnelingRequest'
import { TypedEventEmitter } from './TypedEmitter'
import KNXHeader from './protocol/KNXHeader'
import KNXTunnelingAck from './protocol/KNXTunnelingAck'
import KNXSearchResponse from './protocol/KNXSearchResponse'
import KNXDisconnectResponse from './protocol/KNXDisconnectResponse'
import { wait, getTimestamp } from './utils'
import SerialFT12, {
SerialFT12Options,
SerialPortSummary,
} from './transports/SerialFT12'
import { performance } from 'perf_hooks'
// KNX Secure helpers (moved inlined usage from SecureTunnelTCP)
import { Keyring } from './secure/keyring'
import {
calculateMessageAuthenticationCodeCBC,
encryptDataCtr,
decryptCtr,
} from './secure/security_primitives'
import {
SCF_ENCRYPTION_S_A_DATA,
KNXIP,
CEMI as SEC_CEMI,
APCI,
APCI_SEC,
TPCI_DATA,
SECURE_WRAPPER_TAG,
SECURE_WRAPPER_CTR_SUFFIX,
SECURE_WRAPPER_MAC_SUFFIX,
SECURE_WRAPPER_OVERHEAD,
KNXIP_HDR_SECURE_WRAPPER,
KNXIP_HDR_TUNNELING_REQUEST,
KNXIP_HDR_TUNNELING_ACK,
KNXIP_HDR_TUNNELING_CONNECT_REQUEST,
KNXIP_HDR_SECURE_SESSION_REQUEST,
KNXIP_HDR_SECURE_SESSION_AUTHENTICATE,
DATA_SECURE_CTR_SUFFIX,
AUTH_CTR_IV,
CONNECT_SEND_DELAY_MS,
DEFAULT_STATUS_TIMEOUT_MS,
SECURE_SESSION_TIMEOUT_MS,
SECURE_AUTH_TIMEOUT_MS,
SECURE_CONNECT_TIMEOUT_MS,
HPAI_CONTROL_ENDPOINT_EMPTY,
HPAI_DATA_ENDPOINT_EMPTY,
CRD_TUNNEL_LINKLAYER,
TUNNEL_CONN_HEADER_LEN,
TUNNELING_ACK_TOTAL_LEN,
WAIT_FOR_STATUS_DEFAULT_MS,
KNXIP_HEADER_LEN,
DEFAULT_SRC_IA_FALLBACK,
PUBLIC_KEY_LEN,
SECURE_SEQ_LEN,
AES_BLOCK_LEN,
MAC_LEN_FULL,
MAC_LEN_SHORT,
} from './secure/secure_knx_constants'
export type DiscoveryInterface = {
ip: string
port: number
name: string
ia: string
services: string[]
type: 'tunnelling' | 'routing'
transport?: 'UDP' | 'TCP' | 'Multicast'
}
// Secure config moved here to avoid dependency on separate class file
export interface SecureConfig {
tunnelInterfaceIndividualAddress?: string
knxkeys_file_path?: string
knxkeys_password?: string
tunnelUserPassword?: string
tunnelUserId?: number
}
export enum ConncetionState {
STARTED = 'STARTED',
CONNECTING = 'CONNECTING',
CONNECTED = 'CONNECTED',
DISCONNECTING = 'DISCONNECTING',
DISCONNECTED = 'DISCONNECTED',
}
export enum SocketEvents {
error = 'error',
message = 'message',
listening = 'listening',
data = 'data',
close = 'close',
connect = 'connect',
}
export type KNXClientProtocol =
| 'TunnelUDP'
| 'Multicast'
| 'TunnelTCP'
| 'SerialFT12'
export enum KNXClientEvents {
error = 'error',
disconnected = 'disconnected',
discover = 'discover',
indication = 'indication',
connected = 'connected',
ready = 'ready',
response = 'response',
connecting = 'connecting',
ackReceived = 'ackReceived',
close = 'close',
descriptionResponse = 'descriptionResponse',
}
export interface KNXClientEventCallbacks {
error: (error: Error) => void
disconnected: (reason: string) => void
discover: (
host: string,
header: KNXHeader,
message: KNXSearchResponse,
) => void
getGatewayDescription: (searchResponse: KNXDescriptionResponse) => void
indication: (packet: KNXRoutingIndication, echoed: boolean) => void
connected: (options: KNXClientOptions) => void
ready: () => void
response: (host: string, header: KNXHeader, message: KnxResponse) => void
connecting: (options: KNXClientOptions) => void
ackReceived: (
packet: KNXTunnelingAck | KNXTunnelingRequest,
ack: boolean,
) => void
close: () => void
descriptionResponse: (packet: KNXDescriptionResponse) => void
}
export type KNXClientOptions = {
/** The physical address to be identified in the KNX bus */
physAddr?: string
/** Connection keep alive timeout. Time after which the connection is closed if no ping received */
connectionKeepAliveTimeout?: number
/** The IP of your KNX router/interface (for Routers, use "224.0.23.12") */
ipAddr?: string
/** The port, default is "3671" */
ipPort?: number | string
/** Default: "TunnelUDP". "Multicast" if you're connecting to a KNX Router. "TunnelUDP" for KNX Interfaces, or "TunnelTCP" for secure KNX Interfaces (not yet implemented) */
hostProtocol?: KNXClientProtocol
/** True: Enables the secure connection. Leave false until KNX-Secure has been released. */
isSecureKNXEnabled?: boolean
/** Avoid sending/receive the ACK telegram. Leave false. If you encounter issues with old interface, set it to true */
suppress_ack_ldatareq?: boolean
/** The local IP address to be used to connect to the KNX/IP Bus. Leave blank, will be automatically filled by KNXUltimate */
localIPAddress?: string
/** Specifies the local eth interface to be used to connect to the KNX Bus. */
interface?: string
/** Local socket address. Automatically filled by KNXClient */
localSocketAddress?: string
// ** Local queue interval between each KNX telegram. Default is 1 telegram each 25ms
KNXQueueSendIntervalMilliseconds?: number
/** Enables sniffing mode to monitor KNX */
sniffingMode?: boolean
/** Sets the tunnel_endpoint with the localIPAddress instead of the standard 0.0.0.0 */
theGatewayIsKNXVirtual?: boolean
/** Optional configuration for KNX/IP Secure over TCP (handshake + Data Secure helpers). */
secureTunnelConfig?: SecureConfig
/** Secure multicast: wait to send until timer is authenticated (default: true) */
secureRoutingWaitForTimer?: boolean
/** Serial FT1.2 configuration (used when hostProtocol === 'SerialFT12') */
serialInterface?: SerialFT12Options
} & KNXLoggerOptions
const optionsDefaults: KNXClientOptions = {
physAddr: '',
connectionKeepAliveTimeout: KNX_CONSTANTS.CONNECTION_ALIVE_TIME,
ipAddr: '224.0.23.12',
ipPort: 3671,
hostProtocol: 'Multicast',
isSecureKNXEnabled: false,
suppress_ack_ldatareq: false,
loglevel: 'info',
localIPAddress: '',
interface: '',
KNXQueueSendIntervalMilliseconds: 25,
theGatewayIsKNXVirtual: false,
secureRoutingWaitForTimer: true,
}
export enum KNXTimer {
/** Triggers when an ACK is not received in time */
ACK = 'ack',
/** Delay between heartbeats */
HEARTBEAT = 'heartbeat',
/** Triggers when no connection state response is received */
CONNECTION_STATE = 'connection_state',
/** Waiting for a connect response */
CONNECTION = 'connection',
/** Delay before sending the connect request */
CONNECT_REQUEST = 'connect_request',
/** Delay after receiving a disconnect request */
DISCONNECT = 'disconnect',
/** Waits for discovery responses */
DISCOVERY = 'discovery',
/** Waits for the gateway description gather responses */
GATEWAYDESCRIPTION = 'GatewayDescription',
}
export type SnifferPacket = {
reqType?: string
request?: string
response?: string
resType?: string
/** Time in ms between this request and the previous */
deltaReq: number
/** Time in ms between the request and the response */
deltaRes?: number
}
interface KNXQueueItem {
knxPacket: KNXPacket
ACK: KNXTunnelingRequest
expectedSeqNumberForACK: number
}
export default class KNXClient extends TypedEventEmitter<KNXClientEventCallbacks> {
private _channelID: number
private _connectionState: string
private _numFailedTelegramACK: number
private _clientTunnelSeqNumber: number
private _options: KNXClientOptions
private _peerHost: string
private _peerPort: number
private _heartbeatFailures: number
private _heartbeatRunning: boolean
private max_HeartbeatFailures: number
private _awaitingResponseType: number
private _clientSocket: UDPSocket | TCPSocket
private _serialDriver?: SerialFT12
private sysLogger: KNXLogger
private _clearToSend = false
private socketReady = false
private timers: Map<KNXTimer, NodeJS.Timeout>
public physAddr: KNXAddress
public theGatewayIsKNXVirtual: boolean
private commandQueue: Array<KNXQueueItem> = []
private exitProcessingKNXQueueLoop: boolean
private currentItemHandledByTheQueue: KNXQueueItem
private queueLock = false
private sniffingPackets: SnifferPacket[]
private lastSnifferRequest: number
// ==== KNX/IP Secure (migrated from SecureTunnelTCP) ====
private _tcpRxBuffer: Buffer
private _secureSessionKey?: Buffer
private _secureSessionId: number = 0
private _secureWrapperSeq: number = 0 // 6-byte counter (we store as number increment)
private _secureTunnelSeq: number = 0 // 1-byte seq in tunneling connection header
private _securePrivateKey?: crypto.KeyObject
private _securePublicKey?: Buffer // 32 bytes raw X25519 public key
private _secureUserId: number = 2
private _secureUserPasswordKey?: Buffer
private _secureGroupKeys: Map<number, Buffer> = new Map()
private _secureSendSeq48: bigint = 0n
private _secureSerial: Buffer = Buffer.from('000000000000', 'hex')
private _secureAssignedIa: number = 0
private _secureKeyring?: Keyring
// Track candidate secure interface IAs for fallback selection
private _secureCandidateIAs: string[] = []
private _secureCandidateIndex: number = 0
// Track hosts we have already probed with SECURE_SEARCH_REQUEST (unicast)
private _secureSearchProbed: Set<string> = new Set()
// ==== KNX/IP Secure Group (routing over multicast) ====
private _secureBackboneKey?: Buffer
private _secureRoutingTimerOffsetMs: number = 0
private _secureRoutingTimerAuthenticated: boolean = false
private _secureRoutingLatencyMs: number = 1000
// Logging helpers use KNXClient loglevel; no separate boolean
private _secureHandshakeSessionTimer?: NodeJS.Timeout
private _secureHandshakeAuthTimer?: NodeJS.Timeout
private _secureHandshakeConnectTimer?: NodeJS.Timeout
// Secure routing (multicast) initial timer sync helper
private _secureRoutingSyncTimer?: NodeJS.Timeout
private _secureHandshakeState?:
| 'connecting'
| 'session'
| 'auth'
| 'connect'
get udpSocket() {
if (this._clientSocket instanceof UDPSocket) {
return this._clientSocket
}
return null
}
get tcpSocket() {
if (this._clientSocket instanceof TCPSocket) {
return this._clientSocket
}
return null
}
private isSerialTransport(): boolean {
return this._options.hostProtocol === 'SerialFT12'
}
constructor(
options: KNXClientOptions,
createSocket?: (client: KNXClient) => void,
) {
super()
this.timers = new Map()
// This is the KNX telegram's queue list
this.commandQueue = []
this.exitProcessingKNXQueueLoop = false
if (options === undefined) {
options = optionsDefaults
} else {
options = {
...optionsDefaults,
...options,
}
}
this._options = options
this.sniffingPackets = []
this.sysLogger = createLogger(this._options.setPrefix || 'KNXEngine')
if (this._options.loglevel) {
setLogLevel(this._options.loglevel)
}
this._channelID = null
this._connectionState = ConncetionState.DISCONNECTED
this._numFailedTelegramACK = 0
this._clientTunnelSeqNumber = -1
this._options.connectionKeepAliveTimeout =
KNX_CONSTANTS.CONNECTION_ALIVE_TIME
this._peerHost = this._options.ipAddr
this._peerPort = parseInt(this._options.ipPort as string, 10)
this._options.localSocketAddress = options.localSocketAddress
this._heartbeatFailures = 0
this.max_HeartbeatFailures = 3
this._awaitingResponseType = null
this._clientSocket = null
// Configure the limiter
try {
if (Number(this._options.KNXQueueSendIntervalMilliseconds) < 20) {
this._options.KNXQueueSendIntervalMilliseconds = 20 // Protection avoiding handleKNXQueue hangs
}
} catch (error) {
this._options.KNXQueueSendIntervalMilliseconds = 25
this.sysLogger.error(
`KNXQueueSendIntervalMilliseconds:${error.message}. Defaulting to 25`,
)
}
// add an empty error listener, without this
// every "error" emitted throws an unhandled exception
this.on('error', (error) => {
this.sysLogger.error(error.stack)
})
if (this._options.physAddr !== '') {
this.physAddr = KNXAddress.createFromString(this._options.physAddr)
}
try {
this._options.localIPAddress = ipAddressHelper.getLocalAddress(
this._options.interface,
)
} catch (error) {
this.sysLogger.error(
`ipAddressHelper.getLocalAddress:${error.message}`,
)
throw error
}
if (createSocket) {
createSocket(this)
} else {
this.createSocket()
}
}
private createSocket() {
if (this.isSerialTransport()) {
this.socketReady = false
return
}
if (this._options.hostProtocol === 'TunnelUDP') {
this._clientSocket = dgram.createSocket({
type: 'udp4',
reuseAddr: true,
}) as UDPSocket
this.udpSocket.on(
SocketEvents.message,
(msg: Buffer, rinfo: RemoteInfo) => {
try {
// TunnelUDP never uses IP Secure wrapper; pass through
this.processInboundMessage(msg, rinfo)
} catch (e) {
this.emit(
KNXClientEvents.error,
e instanceof Error
? e
: new Error('UDP data error'),
)
}
},
)
this.udpSocket.on(SocketEvents.error, (error) => {
this.socketReady = false
this.emit(KNXClientEvents.error, error)
})
this.udpSocket.on(SocketEvents.close, () => {
this.socketReady = false
this.exitProcessingKNXQueueLoop = true
// For unexpected closes, emit a disconnected event
if (
this._connectionState !== ConncetionState.DISCONNECTING &&
this._connectionState !== ConncetionState.DISCONNECTED
) {
try {
this.setDisconnected('Socket closed by peer').catch(
() => {},
)
} catch {}
}
this.emit(KNXClientEvents.close)
})
this.udpSocket.on(SocketEvents.listening, () => {
this.socketReady = true
this.handleKNXQueue()
})
this.udpSocket.bind(
{
// port: this._peerPort, // Local port shall be assigned by the socket.
address: this._options.localIPAddress, // Force UDP to be heard trough this interface
},
() => {
try {
// For multicast SEARCH_REQUEST sending, ensure correct iface and TTL
try {
this.udpSocket.setMulticastInterface(
this._options.localIPAddress,
)
this.udpSocket.setMulticastTTL(55)
} catch {}
this.udpSocket.setTTL(55)
if (this._options.localSocketAddress === undefined) {
this._options.localSocketAddress =
this.udpSocket.address().address
}
} catch (error) {
this.sysLogger.error(
`UDP: Error setting SetTTL ${error.message}` || '',
)
}
},
)
} else if (this._options.hostProtocol === 'TunnelTCP') {
// KNX/IP Secure over TCP handled inline
this.initTcpSocket()
} else if (this._options.hostProtocol === 'Multicast') {
this._clientSocket = dgram.createSocket({
type: 'udp4',
reuseAddr: true,
}) as UDPSocket
// this._clientSocket.removeAllListeners()
this.udpSocket.on(SocketEvents.listening, () => {
this.socketReady = true
this.handleKNXQueue()
// For plain multicast, emit connected at listening; for secure multicast wait for timer auth (0955/0950)
if (
this._connectionState === ConncetionState.CONNECTING &&
!this._options.isSecureKNXEnabled
) {
this._connectionState = ConncetionState.CONNECTED
this._numFailedTelegramACK = 0
this.clearToSend = true
this._clientTunnelSeqNumber = -1
this.emit(KNXClientEvents.connected, this._options)
}
// If secure routing, proactively send a TimerNotify once to authenticate timer
if (
this._options.hostProtocol === 'Multicast' &&
this._options.isSecureKNXEnabled
) {
// small delay to ensure keyring/backbone key are loaded
try {
clearTimeout(this._secureRoutingSyncTimer as any)
} catch {}
this._secureRoutingSyncTimer = setTimeout(() => {
try {
// Ensure backbone key available
if (!this._secureBackboneKey)
this.secureEnsureKeyring()
this.secureSendTimerNotify()
} catch {}
}, 120)
}
})
this.udpSocket.on(
SocketEvents.message,
(msg: Buffer, rinfo: RemoteInfo) => {
try {
if (this._options.isSecureKNXEnabled) {
this.secureOnUdpData(msg, rinfo)
return
}
this.processInboundMessage(msg, rinfo)
} catch (e) {
this.emit(
KNXClientEvents.error,
e instanceof Error
? e
: new Error('UDP data error'),
)
}
},
)
this.udpSocket.on(SocketEvents.error, (error) => {
this.socketReady = false
this.emit(KNXClientEvents.error, error)
})
this.udpSocket.on(SocketEvents.close, () => {
this.socketReady = false
this.exitProcessingKNXQueueLoop = true
if (
this._connectionState !== ConncetionState.DISCONNECTING &&
this._connectionState !== ConncetionState.DISCONNECTED
) {
try {
this.setDisconnected('Socket closed by peer').catch(
() => {},
)
} catch {}
}
this.emit(KNXClientEvents.close)
})
// The multicast traffic is not sent to a specific local IP, so we cannot set the this._options.localIPAddress in the bind
// otherwise the socket will never ever receive a packet.
this.udpSocket.bind(
this._peerPort,
this._options.theGatewayIsKNXVirtual
? this._options.localIPAddress || '0.0.0.0'
: '0.0.0.0',
() => {
try {
this.udpSocket.setMulticastTTL(55)
this.udpSocket.setMulticastInterface(
this._options.localIPAddress,
)
// Ensure we receive our own multicast (useful for local echo/diagnostics)
try {
this.udpSocket.setMulticastLoopback(true)
} catch {}
this.sysLogger.debug(
`[${getTimestamp()}] Multicast socket bound on ${this._options.localIPAddress || '0.0.0.0'}:${this._peerPort}`,
)
} catch (error) {
this.sysLogger.error(
`Multicast: Error setting SetTTL ${error.message}` ||
'',
)
}
try {
this.udpSocket.addMembership(
this._peerHost,
this._options.localIPAddress,
)
this.sysLogger.debug(
`[${getTimestamp()}] Joined multicast group ${this._peerHost} on ${this._options.localIPAddress}`,
)
} catch (err) {
this.sysLogger.error(
'Multicast: cannot add membership (%s)',
err,
)
this.emit(KNXClientEvents.error, err)
}
},
)
}
}
// Initialize/Reinitialize TCP socket and its listeners for KNX/IP Secure over TCP
private initTcpSocket() {
this._clientSocket = new net.Socket()
// Buffer incoming TCP to complete frames
this._tcpRxBuffer = Buffer.alloc(0)
this.tcpSocket.on('connect', () => {
// TCP connected, start secure session handshake
this.socketReady = true
// Reset queue exit flag on fresh TCP connect
this.exitProcessingKNXQueueLoop = false
this.secureStartSession().catch((err) => {
this.emit(KNXClientEvents.error, err)
})
})
this.tcpSocket.on('data', (data: Buffer) => {
try {
this.secureOnTcpData(data)
} catch (e) {
this.emit(
KNXClientEvents.error,
e instanceof Error ? e : new Error('TCP data error'),
)
}
})
this.tcpSocket.on('error', (error) => {
this.socketReady = false
this.emit(KNXClientEvents.error, error)
})
this.tcpSocket.on('close', () => {
this.socketReady = false
this.exitProcessingKNXQueueLoop = true
try {
this.sysLogger.debug(
`[${getTimestamp()}] TCP close: set exitProcessingKNXQueueLoop=true`,
)
} catch {}
// If the socket closed unexpectedly, propagate a disconnected event
if (
this._connectionState !== ConncetionState.DISCONNECTING &&
this._connectionState !== ConncetionState.DISCONNECTED
) {
try {
this.setDisconnected('Socket closed by peer').catch(
() => {},
)
} catch {}
}
this.emit(KNXClientEvents.close)
})
}
private async connectSerialTransport() {
const options: SerialFT12Options = {
...(this._options.serialInterface || {}),
}
this._peerHost = options.path || '/dev/ttyAMA0'
this._peerPort = 0
// Prepare KNX Data Secure for serial mode (KBerry):
// if a .knxkeys file is configured, load group keys so that
// maybeApplyDataSecure / maybeDecryptDataSecure can work on cEMI frames.
if (this._options.isSecureKNXEnabled) {
try {
await this.secureEnsureKeyring()
} catch (err) {
try {
this.sysLogger.error(
`[${getTimestamp()}] Serial FT1.2: secure keyring error: ${(err as Error).message}`,
)
} catch {}
}
}
this._serialDriver = new SerialFT12(options)
this._serialDriver.on('cemi', (payload) =>
this.handleSerialCemi(payload),
)
this._serialDriver.on('error', (err) => {
this.emit(
KNXClientEvents.error,
err instanceof Error ? err : new Error(String(err)),
)
})
this._serialDriver.on('close', () => {
if (this.isSerialTransport()) {
this.socketReady = false
if (
this._connectionState !== ConncetionState.DISCONNECTED &&
this._connectionState !== ConncetionState.DISCONNECTING
) {
this.setDisconnected('Serial FT1.2 port closed').catch(
() => {},
)
}
}
})
await this._serialDriver.open()
}
private async closeSerialTransport() {
if (!this._serialDriver) return
try {
await this._serialDriver.close()
} catch (error) {
this.sysLogger.warn(
`[${getTimestamp()}] Serial FT1.2 close error: ${(error as Error).message}`,
)
} finally {
this._serialDriver = undefined
this.socketReady = false
}
}
private handleSerialCemi(payload: Buffer) {
try {
if (!payload || payload.length < 2) return
const msgCode = payload.readUInt8(0)
// eslint-disable-next-line default-case
switch (msgCode) {
case CEMIConstants.L_DATA_IND: {
const cemi = CEMIFactory.createFromBuffer(
msgCode,
payload,
1,
)
this.ensurePlainCEMI(cemi)
this.emit(
KNXClientEvents.indication,
new KNXRoutingIndication(cemi),
false,
)
break
}
case CEMIConstants.L_DATA_CON: {
break
}
}
} catch (error) {
this.sysLogger.error(
`[${getTimestamp()}] Serial FT1.2 parse error: ${
(error as Error).message
}`,
)
}
}
private extractCemiMessage(packet: KNXPacket): CEMIMessage {
const cemi = (packet as any)?.cEMIMessage
if (!cemi) {
throw new Error('KNX packet does not contain a cEMI message')
}
return cemi
}
/**
* The channel ID of the connection. Only defined after a successful connection
*/
get channelID() {
return this._channelID
}
/**
* Handle the busy state, for example while waiting for ACK. When true means we can send new telegrams to bus
*/
get clearToSend(): boolean {
return this._clearToSend
}
set clearToSend(val: boolean) {
this._clearToSend = val
if (val) {
this.handleKNXQueue()
}
}
private getKNXDataBuffer(data: Buffer, dptid: string | number) {
if (typeof dptid === 'number') {
dptid = dptid.toString()
}
const adpu = {} as DPTLib.APDU
DPTLib.populateAPDU(data, adpu, dptid)
const iDatapointType: number = parseInt(
dptid.substring(0, dptid.indexOf('.')),
)
const isSixBits: boolean = adpu.bitlength <= 6
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`isSixBits:${isSixBits} Includes (should be = isSixBits):${[
1, 2, 3, 5, 9, 10, 11, 14, 18,
].includes(iDatapointType)} ADPU BitLength:${adpu.bitlength}`,
)
const datapoint: IDataPoint = {
id: '',
value: 'any',
type: { type: isSixBits },
bind: null,
read: () => null,
write: null,
}
return new KNXDataBuffer(adpu.data, datapoint)
}
/** Waits till providden event occurs for at most the providden timeout */
private async waitForEvent(event: KNXClientEvents, timeout: number) {
let resolveRef: () => void
return Promise.race<void>([
new Promise<void>((resolve) => {
resolveRef = resolve
this.once(event, resolve)
}),
wait(timeout),
]).then(() => {
this.off(event, resolveRef)
})
}
private setTimer(type: KNXTimer, cb: () => void, delay: number) {
if (this.timers.has(type)) {
clearTimeout(this.timers.get(type))
this.timers.delete(type)
// TODO: should we throw error?
this.sysLogger.warn(`Timer "${type}" was already running`)
}
this.timers.set(
type,
setTimeout(() => {
this.timers.delete(type)
cb()
}, delay),
)
}
private clearTimer(type: KNXTimer) {
if (this.timers.has(type)) {
clearTimeout(this.timers.get(type))
this.timers.delete(type)
}
}
private clearAllTimers() {
// use dedicated methods where possible
this.stopDiscovery()
this.stopHeartBeat()
this.stopGatewayDescription()
// clear all other timers
for (const timer of this.timers.keys()) {
this.clearTimer(timer)
}
}
private processKnxPacketQueueItem(_knxPacket: KNXPacket): Promise<boolean> {
return new Promise((resolve) => {
// Prepare the debug log ************************
if (this.sysLogger.level === 'debug') {
if (
_knxPacket instanceof KNXTunnelingRequest ||
_knxPacket instanceof KNXRoutingIndication
) {
// Composing debug string
let sTPCI = ''
if (_knxPacket.cEMIMessage.npdu.isGroupRead) {
sTPCI = 'Read'
}
if (_knxPacket.cEMIMessage.npdu.isGroupResponse) {
sTPCI = 'Response'
}
if (_knxPacket.cEMIMessage.npdu.isGroupWrite) {
sTPCI = 'Write'
}
let sDebugString = ''
sDebugString = `peerHost:${this._peerHost}:${this._peerPort}`
sDebugString += ` dstAddress: ${_knxPacket.cEMIMessage.dstAddress.toString()}`
sDebugString += ` channelID:${this._channelID === null || this._channelID === undefined ? 'None' : this._channelID}`
sDebugString += ` npdu: ${sTPCI}`
sDebugString += ` knxHeader: ${_knxPacket.constructor.name}`
sDebugString += ` raw: ${JSON.stringify(_knxPacket)}`
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`KNXEngine: <outgoing telegram>: ${sDebugString} `,
)
} else if (_knxPacket instanceof KNXTunnelingAck) {
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`KNXEngine: <outgoing telegram ACK>:${this.getKNXConstantName(_knxPacket.status)} channelID:${_knxPacket.channelID} seqCounter:${_knxPacket.seqCounter}`,
)
}
}
// End Prepare the debug log ************************
if (this.isSerialTransport()) {
try {
const cemi = this.extractCemiMessage(_knxPacket)
if (
this._options.isSecureKNXEnabled &&
_knxPacket instanceof KNXRoutingIndication
) {
this.maybeApplyDataSecure(cemi as any)
try {
_knxPacket.length = cemi.length ?? _knxPacket.length
} catch {}
}
this._serialDriver
?.sendCemiPayload(cemi.toBuffer())
.then(() => resolve(true))
.catch((error) => {
this.emit(
KNXClientEvents.error,
error instanceof Error
? error
: new Error(String(error)),
)
resolve(false)
})
} catch (error) {
this.emit(
KNXClientEvents.error,
error instanceof Error
? error
: new Error(String(error)),
)
resolve(false)
}
return
}
if (
this._options.hostProtocol === 'Multicast' ||
this._options.hostProtocol === 'TunnelUDP'
) {
try {
// If Multicast+Secure, apply Data Secure (if GA has key) before wrapping
try {
if (
this._options.hostProtocol === 'Multicast' &&
this._options.isSecureKNXEnabled &&
_knxPacket instanceof KNXRoutingIndication
) {
const kri = _knxPacket as KNXRoutingIndication & {
header: any
length: number
}
this.maybeApplyDataSecure(kri.cEMIMessage as any)
// Update KNX/IP header length to include updated cEMI length
try {
kri.length =
kri.cEMIMessage?.length ?? kri.length
kri.header.length =
KNX_CONSTANTS.HEADER_SIZE_10 + kri.length
} catch {}
}
} catch {}
let outBuf = _knxPacket.toBuffer()
if (
this._options.hostProtocol === 'Multicast' &&
this._options.isSecureKNXEnabled &&
(_knxPacket instanceof KNXRoutingIndication ||
(_knxPacket as any)?.header?.service_type ===
KNX_CONSTANTS.ROUTING_INDICATION)
) {
try {
outBuf = this.secureWrapRouting(outBuf)
if (this.isLevelEnabled('debug')) {
this.sysLogger.debug(
`[${getTimestamp()}] TX 0950 SecureWrapper (routing) len=${outBuf.length}`,
)
}
} catch (e) {
this.sysLogger.error(
`Secure multicast wrap error: ${(e as Error).message}`,
)
}
}
this.udpSocket.send(
outBuf,
this._peerPort,
this._peerHost,
(error) => {
if (error) {
this.sysLogger.error(
`Sending KNX packet: Send UDP sending error: ${error.message}`,
)
this.emit(KNXClientEvents.error, error)
}
resolve(!error)
},
)
} catch (error) {
this.sysLogger.error(
`Sending KNX packet: Send UDP Catch error: ${
(error as Error).message
} ${typeof _knxPacket} seqCounter:${
(_knxPacket as any)?.seqCounter
}`,
)
this.emit(KNXClientEvents.error, error as Error)
resolve(false)
}
} else if (this._options.hostProtocol === 'TunnelTCP') {
// KNX Secure over TCP: wrap KNX/IP frame in SecureWrapper and send via TCP
try {
// Ensure Data Secure is applied at send time (after leaving the queue)
if (
this._options.isSecureKNXEnabled &&
_knxPacket instanceof KNXTunnelingRequest &&
(_knxPacket as any).cEMIMessage?.msgCode ===
CEMIConstants.L_DATA_REQ
) {
// Apply Data Secure right before sending
this.maybeApplyDataSecure(
(_knxPacket as any).cEMIMessage,
)
// IMPORTANT: update KNX/IP header length to include new cEMI length
try {
const ktr = _knxPacket as KNXTunnelingRequest
const cemiLen = ktr.cEMIMessage?.length ?? 0
// Header.length includes header size (10) + body length
ktr.header.length =
KNX_CONSTANTS.HEADER_SIZE_10 + (4 + cemiLen)
} catch {}
}
// Debug before wrapping: show if APDU is secure/plain, GA, src, flags and seq48
try {
if (_knxPacket instanceof KNXTunnelingRequest) {
const ktr: any = _knxPacket
const cemi: any = ktr?.cEMIMessage
const dstStr = cemi?.dstAddress?.toString?.()
const srcStr = cemi?.srcAddress?.toString?.()
const ctrlBuf: Buffer = cemi?.control?.toBuffer?.()
const flags16 = Buffer.isBuffer(ctrlBuf)
? (ctrlBuf[0] << 8) | ctrlBuf[1]
: undefined
const isSecApdu = !!(
cemi?.npdu &&
(cemi.npdu.tpci & 0xff) === APCI_SEC.HIGH &&
(cemi.npdu.apci & 0xff) === APCI_SEC.LOW
)
let scf: number | undefined
let seq48Hex: string | undefined
if (isSecApdu) {
const dbuf: Buffer = cemi.npdu.dataBuffer?.value
if (
Buffer.isBuffer(dbuf) &&
dbuf.length >= 1 + SECURE_SEQ_LEN
) {
scf = dbuf[0]
const seq = dbuf.subarray(
1,
1 + SECURE_SEQ_LEN,
)
seq48Hex = seq.toString('hex')
}
}
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`TX TunnelTCP: dst=${dstStr} src=${srcStr} flags=0x${(
flags16 ?? 0
).toString(
16,
)} dataSecure=${isSecApdu} scf=${
typeof scf === 'number' ? scf : 'n/a'
} seq48=${seq48Hex ?? 'n/a'}`,
)
try {
if (this.isLevelEnabled('debug')) {
const innerHex = ktr
.toBuffer()
.toString('hex')
this.sysLogger.debug(
`[${getTimestamp()}] TX inner (KNX/IP TunnelReq): ${innerHex}`,
)
}
} catch {}
}
} catch {}
const inner = _knxPacket.toBuffer()
const payload = this._options.isSecureKNXEnabled
? this.secureWrap(inner)
: inner
this.tcpSocket.write(payload, (error) => {
if (error) {
this.sysLogger.error(
`Sending KNX packet: Send TCP sending error: ${error.message}` ||
'Undef error',
)
this.emit(KNXClientEvents.error, error)
}
resolve(!error)
})
} catch (error) {
this.sysLogger.error(
`Sending KNX packet: Send TCP Catch error: ${(error as Error).message}` ||
'Undef error',
)
this.emit(KNXClientEvents.error, error as Error)
resolve(false)
}
}
})
}
private async handleKNXQueue() {
if (this.queueLock) {
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`KNXClient: handleKNXQueue: HandleQueue has called, but the queue loop is already running. Exit.`,
)
return
}
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`KNXClient: handleKNXQueue: Start Processing queued KNX. Found ${this.commandQueue.length} telegrams in queue.`,
)
// lock the queue
this.queueLock = true
// Limiter: limits max telegrams per second
while (this.commandQueue.length > 0) {
if (!this.clearToSend) {
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`KNXClient: handleKNXQueue: Clear to send is false. Pause processing queue.`,
)
break
}
if (this.exitProcessingKNXQueueLoop) {
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`KNXClient: handleKNXQueue: exitProcessingKNXQueueLoop is true. Exit processing queue loop`,
)
break
}
if (this.socketReady === false) {
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`KNXClient: handleKNXQueue: Socket is not ready. Stop processing queue.`,
)
break
}
const item = this.commandQueue.pop()
// Secure multicast gating: wait for timer authentication before sending RoutingIndication
if (
this._options.hostProtocol === 'Multicast' &&
this._options.isSecureKNXEnabled &&
(this._options.secureRoutingWaitForTimer ?? true) &&
!this._secureRoutingTimerAuthenticated &&
item.knxPacket instanceof KNXRoutingIndication
) {
try {
this.sysLogger.debug(
`[${getTimestamp()}] Secure multicast: waiting timer auth, deferring 0950 send`,
)
} catch {}
// push back item and wait briefly
this.commandQueue.push(item)
await wait(200)
continue
}
this.currentItemHandledByTheQueue = item
// Associa il sequence number di tunneling al momento dell'invio
try {
if (this._options.hostProtocol === 'TunnelTCP') {
// Solo per KNXTunnelingRequest: il seq dell'ACK deve eguagliare quello ricevuto, non va incrementato
if (item.knxPacket instanceof KNXTunnelingRequest) {
const ktr = item.knxPacket as any
const seq = this.secureIncTunnelSeq()
ktr.seqCounter = seq
if (item.ACK) {
item.expectedSeqNumberForACK = seq
}
try {
this.sysLogger.debug(
`[${getTimestamp()}] Assign tunnel seq=${seq} ch=${ktr?.channelID} dst=${ktr?.cEMIMessage?.dstAddress?.toString?.()}`,
)
} catch {}
}
}
} catch {}
if (
item.ACK !== undefined &&
this._options.hostProtocol !== 'TunnelTCP'
) {
this.setTimerWaitingForACK(item.ACK)
}
if (!(await this.processKnxPacketQueueItem(item.knxPacket))) {
this.sysLogger.error(
`KNXClient: handleKNXQueue: returning from processKnxPacketQueueItem ${JSON.stringify(item)}`,
)
// Clear the queue
this.commandQueue = []
break
}
await wait(this._options.KNXQueueSendIntervalMilliseconds)
}
this.queueLock = false
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`KNXClient: handleKNXQueue: End Processing queued KNX.`,
)
}
/**
* Write knxPacket to socket
*/
send(
_knxPacket: KNXPacket,
_ACK: KNXTunnelingRequest,
_priority: boolean,
_expectedSeqNumberForACK: number,
): void {
const toBeAdded: KNXQueueItem = {
knxPacket: _knxPacket,
ACK: _ACK,
expectedSeqNumberForACK: _expectedSeqNumberForACK,
}
if (this._options.sniffingMode) {
const buffer = _knxPacket.toBuffer()
this.sniffingPackets.push({
reqType: _knxPacket.constructor.name,
request: buffer.toString('hex'),
deltaReq: this.lastSnifferRequest
? Date.now() - this.lastSnifferRequest
: 0,
})
this.lastSnifferRequest = Date.now()
}
if (_priority) {
this.commandQueue.push(toBeAdded) // Put the item as first to be sent.
this.clearToSend = true
} else {
this.commandQueue.unshift(toBeAdded) // Put the item as last to be sent.
}
this.handleKNXQueue()
this.sysLogger.debug(
`[${getTimestamp()}] ` +
`KNXClient: <added telegram to queue> queueLength:${this.commandQueue.length} priority:${_priority} type:${this.getKNXConstantName(toBeAdded.knxPacket.type)} channelID:${toBeAdded.ACK?.channelID || 'filled later'} seqCounter:${toBeAdded.ACK?.seqCounter || 'filled later'}`,
)
}
/** Sends a WRITE telegram to the BUS.
* `dstAddress` is the group address (for example "0/0/1"),
* `data` is the value you want to send (for example true),
* `dptid` is a string/number representing the datapoint (for example "5.001")
*/
write(
dstAddress: KNXAddress | string,
data: any,
dptid: string | number,
): void {
if (this._connectionState !== ConncetionState.CONNECTED)
throw new Error(
'The socket is not connected. Unable to access the KNX BUS',
)
// Get the Data Buffer from the plain value
const knxBuffer = this.getKNXDataBuffer(data, dptid)
if (typeof dstAddress === 'string')
dstAddress = KNXAddress.createFromString(
dstAddress,
KNXAddress.TYPE_GROUP,
)
const srcAddress = this.physAddr
if (this._options.hostProtocol === 'Multicast') {
// Multicast: per KNX Routing spec, inject as L_DATA_IND
const cEMIMessage = CEMIFactory.newLDataIndicationMessage(
'write',
srcAddress,
dstAddress,
knxBuffer,
)
cEMIMessage.control.ack = 0
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
const knxPacketRequest =
KNXProtocol.newKNXRoutingIndication(cEMIMessage)
this.send(knxPacketRequest, undefined, false, this.getSeqNumber())
// 06/12/2021 Multicast automatically echoes telegrams
} else if (this.isSerialTransport()) {
// Serial FT1.2 (KBerry): send as L_DATA_REQ over cEMI/FT1.2
const cEMIMessage = CEMIFactory.newLDataRequestMessage(
'write',
srcAddress,
dstAddress,
knxBuffer,
)
// Request bus ACK unless suppressed
cEMIMessage.control.ack = this._options.suppress_ack_ldatareq
? 0
: 1
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
const knxPacketRequest =
KNXProtocol.newKNXRoutingIndication(cEMIMessage)
this.send(knxPacketRequest, undefined, false, this.getSeqNumber())
// Echo on local client as indication
this.ensurePlainCEMI(cEMIMessage)
this.emit(KNXClientEvents.indication, knxPacketRequest as any, true)
} else {
// Tunneling
const cEMIMessage = CEMIFactory.newLDataRequestMessage(
'write',
srcAddress,
dstAddress,
knxBuffer,
)
// Tunnelling UDP: request bus ACK unless suppressed; TunnelTCP: no bus ACK
cEMIMessage.control.ack =
// eslint-disable-next-line no-nested-ternary
this._options.hostProtocol === 'TunnelTCP'
? 0
: this._options.suppress_ack_ldatareq
? 0
: 1
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
// Data Secure si applica solo in TunnelTCP
// Nota: per TunnelTCP, il seq di tunneling viene assegnato al momento dell'invio in handleKNXQueue
const seqNum: number =
this._options.hostProtocol === 'TunnelTCP'
? 0
: this.incSeqNumber()
const knxPacketRequest = KNXProtocol.newKNXTunnelingRequest(
this._channelID,
seqNum,
cEMIMessage,
)
if (!this._options.suppress_ack_ldatareq) {
this.send(knxPacketRequest, knxPacketRequest, false, seqNum)
} else {
this.send(knxPacketRequest, undefined, false, seqNum)
}
// 06/12/2021 Echo the sent telegram. Emit entire telegram with plain cEMI
this.ensurePlainCEMI(knxPacketRequest.cEMIMessage)
this.emit(KNXClientEvents.indication, knxPacketRequest as any, true)
}
}
/**
* Sends a RESPONSE telegram to the BUS.
* `dstAddress` is the group address (for example "0/0/1"),
* `data` is the value you want to send (for example true),
* `dptid` is a string/number representing the datapoint (for example "5.001")
*/
respond(
dstAddress: KNXAddress | string,
data: Buffer,
dptid: string | number,
): void {
if (this._connectionState !== ConncetionState.CONNECTED)
throw new Error(
'The socket is not connected. Unable to access the KNX BUS',
)
// Get the Data Buffer from the plain value
const knxBuffer = this.getKNXDataBuffer(data, dptid)
if (typeof dstAddress === 'string')
dstAddress = KNXAddress.createFromString(
dstAddress,
KNXAddress.TYPE_GROUP,
)
const srcAddress = this.physAddr
if (this._options.hostProtocol === 'Multicast') {
// Multicast: per KNX Routing spec, inject as L_DATA_IND
const cEMIMessage = CEMIFactory.newLDataIndicationMessage(
'response',
srcAddress,
dstAddress,
knxBuffer,
)
cEMIMessage.control.ack = 0 // No ack like telegram sent from ETS (0 means don't care)
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
const knxPacketRequest =
KNXProtocol.newKNXRoutingIndication(cEMIMessage)
this.send(knxPacketRequest, undefined, false, this.getSeqNumber())
// 06/12/2021 Multicast automatically echoes telegrams
} else if (this.isSerialTransport()) {
// Serial FT1.2 (KBerry): send as L_DATA_REQ over cEMI/FT1.2
const cEMIMessage = CEMIFactory.newLDataRequestMessage(
'response',
srcAddress,
dstAddress,
knxBuffer,
)
// No ACK request on bus for responses
cEMIMessage.control.ack = 0
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
const knxPacketRequest =
KNXProtocol.newKNXRoutingIndication(cEMIMessage)
this.send(knxPacketRequest, undefined, false, this.getSeqNumber())
this.ensurePlainCEMI(cEMIMessage)
this.emit(KNXClientEvents.indication, knxPacketRequest as any, true)
} else {
// Tunneling
const cEMIMessage = CEMIFactory.newLDataRequestMessage(
'response',
srcAddress,
dstAddress,
knxBuffer,
)
// No ACK request on bus
cEMIMessage.control.ack = 0
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
// Data Secure si applica solo in TunnelTCP
const seqNum: number =
this._options.hostProtocol === 'TunnelTCP'
? 0
: this.incSeqNumber()
const knxPacketRequest = KNXProtocol.newKNXTunnelingRequest(
this._channelID,
seqNum,
cEMIMessage,
)
if (!this._options.suppress_ack_ldatareq) {
this.send(knxPacketRequest, knxPacketRequest, false, seqNum)
} else {
this.send(knxPacketRequest, undefined, false, seqNum)
}
// 06/12/2021 Echo the sent telegram. Emit entire telegram with plain cEMI
this.ensurePlainCEMI(knxPacketRequest.cEMIMessage)
this.emit(KNXClientEvents.indication, knxPacketRequest as any, true)
}
}
/**
* Sends a READ telegram to the BUS. GA is the group address (for example "0/0/1").
*/
read(dstAddress: KNXAddress | string): void {
if (this._connectionState !== ConncetionState.CONNECTED)
throw new Error(
'The socket is not connected. Unable to access the KNX BUS',
)
if (typeof dstAddress === 'string')
dstAddress = KNXAddress.createFromString(
dstAddress,
KNXAddress.TYPE_GROUP,
)
const srcAddress = this.physAddr
if (this._options.hostProtocol === 'Multicast') {
// Multicast: per KNX Routing spec, inject as L_DATA_IND
const cEMIMessage = CEMIFactory.newLDataIndicationMessage(
'read',
srcAddress,
dstAddress,
null,
)
cEMIMessage.control.ack = 0
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
const knxPacketRequest =
KNXProtocol.newKNXRoutingIndication(cEMIMessage)
this.send(knxPacketRequest, undefined, false, this.getSeqNumber())
// 06/12/2021 Multicast automatically echoes telegrams
} else if (this.isSerialTransport()) {
// Serial FT1.2 (KBerry): send as L_DATA_REQ over cEMI/FT1.2
const cEMIMessage = CEMIFactory.newLDataRequestMessage(
'read',
srcAddress,
dstAddress,
null,
)
// Request bus ACK unless suppressed
cEMIMessage.control.ack = this._options.suppress_ack_ldatareq
? 0
: 1
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
const knxPacketRequest =
KNXProtocol.newKNXRoutingIndication(cEMIMessage)
this.send(knxPacketRequest, undefined, false, this.getSeqNumber())
this.ensurePlainCEMI(cEMIMessage)
this.emit(KNXClientEvents.indication, knxPacketRequest as any, true)
} else {
// Tunneling
const cEMIMessage = CEMIFactory.newLDataRequestMessage(
'read',
srcAddress,
dstAddress,
null,
)
// Tunnelling UDP: request bus ACK unless suppressed; TunnelTCP: no bus ACK
cEMIMessage.control.ack =
// eslint-disable-next-line no-nested-ternary
this._options.hostProtocol === 'TunnelTCP'
? 0
: this._options.suppress_ack_ldatareq
? 0
: 1
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
const seqNum: number =
this._options.hostProtocol === 'TunnelTCP'
? 0
: this.incSeqNumber()
const knxPacketRequest = KNXProtocol.newKNXTunnelingRequest(
this._channelID,
seqNum,
cEMIMessage,
)
if (!this._options.suppress_ack_ldatareq) {
this.send(knxPacketRequest, knxPacketRequest, false, seqNum)
} else {
this.send(knxPacketRequest, undefined, false, seqNum)
}
// 06/12/2021 Echo the sent telegram. Emit entire telegram with plain cEMI
this.ensurePlainCEMI(knxPacketRequest.cEMIMessage)
this.emit(KNXClientEvents.indication, knxPacketRequest as any, true)
}
}
/**
* Sends a WRITE telegram to the BUS.
* `dstAddress` is the group address (for example "0/0/1"),
* `rawDataBuffer` is the buffer you want to send,
* `dptid` is a string/number representing the datapoint (for example "5.001")
*/
writeRaw(
dstAddress: KNXAddress | string,
rawDataBuffer: Buffer,
bitlength: number,
): void {
// bitlength is unused and only for backward compatibility
if (this._connectionState !== ConncetionState.CONNECTED)
throw new Error(
'The socket is not connected. Unable to access the KNX BUS',
)
if (!Buffer.isBuffer(rawDataBuffer)) {
this.sysLogger.error(
'KNXClient: writeRaw: Value must be a buffer! ',
)
return
}
const isSixBits: boolean = bitlength <= 6
const datapoint: IDataPoint = {
id: '',
value: 'any',
type: { type: isSixBits },
bind: null,
read: () => null,
write: null,
}
// Get the KNDDataBuffer
const baseBufferFromBitLength: Buffer = Buffer.alloc(
Math.ceil(bitlength / 8),
) // The buffer length must be like specified by bitlength
rawDataBuffer.copy(baseBufferFromBitLength, 0)
const data: KNXDataBuffer = new KNXDataBuffer(
baseBufferFromBitLength,
datapoint,
)
if (typeof dstAddress === 'string')
dstAddress = KNXAddress.createFromString(
dstAddress,
KNXAddress.TYPE_GROUP,
)
const srcAddress = this.physAddr
if (this._options.hostProtocol === 'Multicast') {
// Multicast: per KNX Routing spec, inject as L_DATA_IND
const cEMIMessage = CEMIFactory.newLDataIndicationMessage(
'write',
srcAddress,
dstAddress,
data,
)
cEMIMessage.control.ack = 0
cEMIMessage.control.broadcast = 1
cEMIMessage.control.priority = 3
cEMIMessage.control.addressType = 1
cEMIMessage.control.hopCount = 6
const knxPacketRequest =
KNXProtocol.newKNXRoutingIndication(cEMIMessage)
this.send(knxPacketRequest, undefined,