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