UNPKG

knxnetjs

Version:

A TypeScript library for KNXnet/IP communication

366 lines 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CEMIFrame = exports.Priority = exports.CEMIMessageCode = void 0; var CEMIMessageCode; (function (CEMIMessageCode) { CEMIMessageCode[CEMIMessageCode["L_DATA_REQ"] = 17] = "L_DATA_REQ"; CEMIMessageCode[CEMIMessageCode["L_DATA_CON"] = 46] = "L_DATA_CON"; CEMIMessageCode[CEMIMessageCode["L_DATA_IND"] = 41] = "L_DATA_IND"; CEMIMessageCode[CEMIMessageCode["L_BUSMON_IND"] = 43] = "L_BUSMON_IND"; CEMIMessageCode[CEMIMessageCode["L_RAW_REQ"] = 16] = "L_RAW_REQ"; CEMIMessageCode[CEMIMessageCode["L_RAW_IND"] = 45] = "L_RAW_IND"; CEMIMessageCode[CEMIMessageCode["L_RAW_CON"] = 47] = "L_RAW_CON"; CEMIMessageCode[CEMIMessageCode["L_POLL_DATA_REQ"] = 19] = "L_POLL_DATA_REQ"; CEMIMessageCode[CEMIMessageCode["L_POLL_DATA_CON"] = 37] = "L_POLL_DATA_CON"; // Management services CEMIMessageCode[CEMIMessageCode["M_PROP_READ_REQ"] = 252] = "M_PROP_READ_REQ"; CEMIMessageCode[CEMIMessageCode["M_PROP_READ_CON"] = 251] = "M_PROP_READ_CON"; CEMIMessageCode[CEMIMessageCode["M_PROP_WRITE_REQ"] = 246] = "M_PROP_WRITE_REQ"; CEMIMessageCode[CEMIMessageCode["M_PROP_WRITE_CON"] = 245] = "M_PROP_WRITE_CON"; CEMIMessageCode[CEMIMessageCode["M_PROP_INFO_IND"] = 247] = "M_PROP_INFO_IND"; CEMIMessageCode[CEMIMessageCode["M_FUNC_PROP_COM_REQ"] = 248] = "M_FUNC_PROP_COM_REQ"; CEMIMessageCode[CEMIMessageCode["M_FUNC_PROP_ST_REQ"] = 249] = "M_FUNC_PROP_ST_REQ"; CEMIMessageCode[CEMIMessageCode["M_FUNC_PROP_CON"] = 250] = "M_FUNC_PROP_CON"; CEMIMessageCode[CEMIMessageCode["M_RESET_REQ"] = 241] = "M_RESET_REQ"; CEMIMessageCode[CEMIMessageCode["M_RESET_IND"] = 240] = "M_RESET_IND"; })(CEMIMessageCode || (exports.CEMIMessageCode = CEMIMessageCode = {})); var Priority; (function (Priority) { Priority[Priority["SYSTEM"] = 0] = "SYSTEM"; Priority[Priority["NORMAL"] = 1] = "NORMAL"; Priority[Priority["URGENT"] = 2] = "URGENT"; Priority[Priority["LOW"] = 3] = "LOW"; })(Priority || (exports.Priority = Priority = {})); class CEMIFrame { constructor(buffer) { if (buffer.length < 2) { throw new Error("Invalid cEMI frame: too short"); } this.buffer = buffer; } static fromBuffer(buffer) { return new CEMIFrame(buffer); } static create(messageCode, sourceAddress, destinationAddress, data, priority = Priority.LOW, hopCount = 6, additionalInfo = []) { // Calculate additional info total length let additionalInfoLength = 0; for (const info of additionalInfo) { additionalInfoLength += 2 + info.data.length; // type + length + data } // Calculate service info length: 1 (ctrl1) + 0|1 (ctrl2) + 2 (src) + 2 (dst) + 1 (len) + data const ctrl1 = (hopCount << 4) | (priority << 2) | 0x00; const isExtended = (ctrl1 & 0x80) === 0; const serviceInfoLength = (isExtended ? 7 : 6) + data.length; // Add 1 byte for ctrl2 if extended const frameLength = 2 + additionalInfoLength + serviceInfoLength; const buffer = Buffer.allocUnsafe(frameLength); let offset = 0; // Message Code buffer.writeUInt8(messageCode, offset++); // Additional Info Length buffer.writeUInt8(additionalInfoLength, offset++); // Additional Information for (const info of additionalInfo) { buffer.writeUInt8(info.type, offset++); buffer.writeUInt8(info.data.length, offset++); info.data.copy(buffer, offset); offset += info.data.length; } // Service Information (L_Data) // Control Field 1 (CTRL1) - reuse the calculated value buffer.writeUInt8(ctrl1, offset++); // Control Field 2 (for extended frames, bit 7 = 0 means extended frame) if ((ctrl1 & 0x80) === 0) { // Extended frame - add Control Field 2 const ctrl2 = (hopCount << 4) | 0x00; // Hop count and other flags buffer.writeUInt8(ctrl2, offset++); } // Source Address buffer.writeUInt16BE(sourceAddress, offset); offset += 2; // Destination Address buffer.writeUInt16BE(destinationAddress, offset); offset += 2; // Data Length buffer.writeUInt8(data.length, offset++); // TPCI + APCI + Data if (data.length > 0) { data.copy(buffer, offset); } else { buffer.writeUInt8(0x00, offset); // Default TPCI/APCI } return new CEMIFrame(buffer); } get messageCode() { return this.buffer.readUInt8(0); } get messageType() { switch (this.messageCode) { case CEMIMessageCode.L_DATA_REQ: return "L_DATA.req"; case CEMIMessageCode.L_DATA_CON: return "L_DATA.con"; case CEMIMessageCode.L_DATA_IND: return "L_DATA.ind"; case CEMIMessageCode.L_BUSMON_IND: return "L_BUSMON.ind"; case CEMIMessageCode.L_RAW_REQ: return "L_RAW.req"; case CEMIMessageCode.L_RAW_IND: return "L_RAW.ind"; case CEMIMessageCode.L_RAW_CON: return "L_RAW.con"; case CEMIMessageCode.L_POLL_DATA_REQ: return "L_POLL_DATA.req"; case CEMIMessageCode.L_POLL_DATA_CON: return "L_POLL_DATA.con"; case CEMIMessageCode.M_PROP_READ_REQ: return "M_PropRead.req"; case CEMIMessageCode.M_PROP_READ_CON: return "M_PropRead.con"; case CEMIMessageCode.M_PROP_WRITE_REQ: return "M_PropWrite.req"; case CEMIMessageCode.M_PROP_WRITE_CON: return "M_PropWrite.con"; case CEMIMessageCode.M_PROP_INFO_IND: return "M_PropInfo.ind"; case CEMIMessageCode.M_FUNC_PROP_COM_REQ: return "M_FuncPropCom.req"; case CEMIMessageCode.M_FUNC_PROP_ST_REQ: return "M_FuncPropSt.req"; case CEMIMessageCode.M_FUNC_PROP_CON: return "M_FuncProp.con"; case CEMIMessageCode.M_RESET_REQ: return "M_Reset.req"; case CEMIMessageCode.M_RESET_IND: return "M_Reset.ind"; default: return "Unknown"; } } get additionalInfoLength() { return this.buffer.length > 1 ? this.buffer.readUInt8(1) : 0; } get additionalInfo() { const additionalInfos = []; const addInfoLength = this.additionalInfoLength; if (addInfoLength === 0 || this.buffer.length < 2 + addInfoLength) { return additionalInfos; } let offset = 2; const endOffset = 2 + addInfoLength; while (offset < endOffset) { if (offset + 1 >= endOffset) break; const type = this.buffer.readUInt8(offset++); const length = this.buffer.readUInt8(offset++); if (offset + length > endOffset) break; const data = this.buffer.subarray(offset, offset + length); additionalInfos.push({ type, length, data }); offset += length; } return additionalInfos; } get serviceInfoOffset() { return 2 + this.additionalInfoLength; } get controlField1() { const offset = this.serviceInfoOffset; return this.buffer.length > offset ? this.buffer.readUInt8(offset) : 0; } get controlField2() { if (this.extendedFrame) { const offset = this.serviceInfoOffset + 1; return this.buffer.length > offset ? this.buffer.readUInt8(offset) : 0; } else { // In standard frames, Control Field 2 is in the upper 4 bits of the length field const offset = this.serviceInfoOffset + 5; if (this.buffer.length < offset + 1) return 0; return this.buffer.readUInt8(offset) & 0xf0; } } get priority() { return (this.controlField1 >> 2) & 0x03; } get priorityText() { switch (this.priority) { case Priority.SYSTEM: return "System"; case Priority.NORMAL: return "Normal"; case Priority.URGENT: return "Urgent"; case Priority.LOW: return "Low"; default: return "Unknown"; } } get extendedFrame() { return (this.controlField1 & 0x80) === 0; } get standardFrame() { return (this.controlField1 & 0x80) !== 0; } get repeatFlag() { return (this.controlField1 & 0x20) !== 0; } get systemBroadcast() { return (this.controlField1 & 0x10) !== 0; } get acknowledgeRequest() { return (this.controlField1 & 0x02) !== 0; } get confirmFlag() { return (this.controlField1 & 0x01) !== 0; } get hopCount() { // Hop count is always in bits 6-4 of Control Field 2 return (this.controlField2 >> 4) & 0x07; } get routingCounter() { return this.hopCount; } get sourceAddress() { // Standard frame: offset = 2 (msg+addinfo) + 1 (ctrl1) = 3 // Extended frame: offset = 2 (msg+addinfo) + 1 (ctrl1) + 1 (ctrl2) = 4 const offset = this.serviceInfoOffset + 1 + (this.extendedFrame ? 1 : 0); if (this.buffer.length < offset + 2) return 0; return this.buffer.readUInt16BE(offset); } get sourceAddressString() { const addr = this.sourceAddress; const area = (addr >> 12) & 0x0f; const line = (addr >> 8) & 0x0f; const device = addr & 0xff; return `${area}.${line}.${device}`; } get destinationAddress() { // Standard frame: offset = 2 (msg+addinfo) + 1 (ctrl1) + 2 (src addr) = 5 // Extended frame: offset = 2 (msg+addinfo) + 1 (ctrl1) + 1 (ctrl2) + 2 (src addr) = 6 const offset = this.serviceInfoOffset + 3 + (this.extendedFrame ? 1 : 0); if (this.buffer.length < offset + 2) return 0; return this.buffer.readUInt16BE(offset); } get destinationAddressString() { const addr = this.destinationAddress; if (this.isGroupAddress) { const main = (addr >> 11) & 0x1f; const middle = (addr >> 8) & 0x07; const sub = addr & 0xff; return `${main}/${middle}/${sub}`; } else { const area = (addr >> 12) & 0x0f; const line = (addr >> 8) & 0x0f; const device = addr & 0xff; return `${area}.${line}.${device}`; } } get isGroupAddress() { // Destination address type is always in bit 7 of Control Field 2 return (this.controlField2 & 0x80) !== 0; } get dataLength() { const offset = this.serviceInfoOffset + 5 + (this.extendedFrame ? 1 : 0); if (this.buffer.length < offset + 1) return 0; const lengthByte = this.buffer.readUInt8(offset); if (this.standardFrame) { // In standard frames, data length is in the lower 4 bits // This represents the number of application payload bytes (excluding TPCI/APCI) return lengthByte & 0x0f; } else { // In extended frames, full byte is data length // This represents the number of application payload bytes (excluding TPCI/APCI) return lengthByte; } } get data() { const offset = this.serviceInfoOffset + 6 + (this.extendedFrame ? 1 : 0); if (this.buffer.length <= offset) return Buffer.alloc(0); return this.buffer.subarray(offset); } get tpci() { const data = this.data; if (data.length === 0) return 0; // TPCI is 6 bits wide (bits 7-2), same for both standard and extended frames return (data.readUInt8(0) >> 2) & 0x3f; } get apci() { const data = this.data; if (data.length === 0) return 0; if (data.length === 1) { return data.readUInt8(0) & 0x03; } return ((data.readUInt8(0) & 0x03) << 8) | data.readUInt8(1); } get applicationData() { const data = this.data; if (data.length <= 1) return Buffer.alloc(0); return data.subarray(data.length === 1 ? 1 : 2); } get rawBuffer() { return this.buffer; } get length() { return this.buffer.length; } toBuffer() { return Buffer.from(this.buffer); } toString() { return this.buffer.toString("hex").toUpperCase(); } toFormattedString(includeTimestamp = true) { let output = ""; if (includeTimestamp) { output += `[${new Date().toISOString()}] `; } output += this.messageType; output += ` | Routing: ${this.hopCount} | Priority: ${this.priorityText}`; if (this.buffer.length >= 6) { output += ` | Src: ${this.sourceAddressString}`; output += ` | Dst: ${this.destinationAddressString}`; output += ` | Length: ${this.dataLength}`; if (this.data.length > 0) { output += ` | Data: ${this.data.toString("hex").toUpperCase()}`; } } output += ` | Raw: ${this.toString()}`; return output; } isValid() { if (this.buffer.length < 2) return false; const addInfoLength = this.additionalInfoLength; const minFrameLength = 2 + addInfoLength; if (this.buffer.length < minFrameLength) return false; // Check if we have service information for L_Data services const serviceOffset = this.serviceInfoOffset; if (this.buffer.length < serviceOffset + 6) return true; // Minimal frame const declaredLength = this.dataLength; const actualDataLength = this.buffer.length - (serviceOffset + 6); return actualDataLength >= declaredLength; } static isValidBuffer(buffer) { if (buffer.length < 2) return false; const messageCode = buffer.readUInt8(0); return Object.values(CEMIMessageCode).includes(messageCode); } } exports.CEMIFrame = CEMIFrame; //# sourceMappingURL=cemi.js.map