UNPKG

@gapit/node-red-contrib-gapit-modbus

Version:

A Node-RED node witch reads Modbus registers based on gapit code JSON schema

1,020 lines (993 loc) 36.5 kB
'use strict'; var ModbusRTU = require('modbus-serial'); function convertConfig(config, node) { if (!config) { node.warn("Missing gapit code"); return; } const modbusGroups = config.objects.map((group)=>{ const registerType = group.register_type; const startAddress = Number(group.group[0].address); const length = group.group.length; const endAddress = Number(group.group[length - 1].address); const lastByteType = group.group[length - 1].byte_type; const byteSize = group.group[length - 1].byte_size; let lastByteTypeNumber = Math.ceil(Number.parseInt(lastByteType.replace(/^\D+/g, "")) / 16); if (typeof lastByteType !== "number" && byteSize) { lastByteTypeNumber = byteSize; } else if (lastByteType.includes("MOD10")) { const byteSize1 = lastByteType.split("-")[1]; lastByteTypeNumber = Number.parseInt(byteSize1); } const absoluteEnd = endAddress + lastByteTypeNumber; const addressLength = absoluteEnd - startAddress; return { registerType, startAddress, addressLength, absoluteEnd }; }); return modbusGroups; } class GapitModbusRTU extends ModbusRTU { readRegister(registerType, dataAddress, length) { switch(registerType){ case "holding": { return this.readHoldingRegisters(dataAddress, length); } case "input": { return this.readInputRegisters(dataAddress, length); } default: { throw new Error(`Unknown register type: ${registerType}`); } } } readCoil(registerType, dataAddress, length) { switch(registerType){ case "status": { return this.readDiscreteInputs(dataAddress, length); } case "coil": { return this.readCoils(dataAddress, length); } default: { throw new Error(`Unknown register type: ${registerType}`); } } } } async function connectModbus(node, host, portOptions) { const client = new GapitModbusRTU(); await new Promise((resolve, reject)=>{ client.connectTCP(host, portOptions).then((response)=>{ // if (response !== undefined) node.warn({ response }); resolve(response); return; }).catch((error)=>{ node.error(`Cannot connect to modbus device: Host:${host}, Port ${portOptions.port}`); // if (err !== undefined) node.warn(err); reject(error); }); }); return client; } function checkNodeConfig(msg, config, node) { if (!config.gapitCode && !msg.gapit_code) { node.warn("Missing gapit code, add gapit code to gapit-modbus-node or msg.gapit_code"); return; } try { const gapitCode = config.gapitCode ? JSON.parse(config.gapitCode) : msg.gapit_code; const portOptions = {}; const port = config.port === undefined ? msg.port : +config.port; if (port !== undefined) { portOptions.port = port; } const nodeConfig = { host: config.host || msg.host || "127.0.0.1", portOptions: portOptions, socketAddress: `${config.host || msg.host}:${port}`, unitId: config.unitId || msg.unit_id, scalingType: gapitCode.scaling || "general", gapitCode: gapitCode, deviceName: config.deviceName || msg.device_name, deviceTag: config.deviceNameDbTag || msg.device_name_db_tag, customTags: config.customTags ? JSON.parse(config.customTags) : msg.custom_tags }; for (const [key, value] of Object.entries(nodeConfig)){ if (key != "unitId" && !value) { node.warn(`Missing value in: ${key}, add value in gapit-modbus-node or msg.`); return; } } msg.db_tags = nodeConfig.customTags; msg.tagname_device_name = nodeConfig.deviceTag; msg.custom_tags = nodeConfig.customTags; return nodeConfig; } catch (error) { node.warn(error); return; } } function initBuffer$2(byteEndian, wordEndian, start, bufferAmount, buffer) { if (wordEndian !== "little" && byteEndian !== "little") return buffer; return buffer.slice(start, start + bufferAmount); } function decodeSInt(byteEndian, wordEndian, start, byteTypeNumber, bufferAmount, result, bitNr) { const buffer = initBuffer$2(byteEndian, wordEndian, start, bufferAmount, result.buffer); if (wordEndian === "little") { if (byteEndian !== "little") buffer.swap16(); if (byteTypeNumber === 64) { return Number(buffer.readBigInt64LE(0)); } else if (byteTypeNumber === 8 && typeof bitNr === "number") { return buffer.readIntLE(bitNr, bufferAmount); } else { return buffer.readIntLE(0, bufferAmount); } } else { if (byteEndian === "little") { buffer.swap16(); if (byteTypeNumber === 64) { return Number(buffer.readBigInt64BE(0)); } else if (byteTypeNumber === 8 && typeof bitNr === "number") { return buffer.readIntBE(bitNr, bufferAmount); } else { return buffer.readIntBE(0, bufferAmount); } } else { if (byteTypeNumber === 64) { return Number(result.buffer.readBigInt64BE(start)); } else if (byteTypeNumber === 8 && typeof bitNr === "number") { return result.buffer.readIntBE(start + bitNr, bufferAmount); } else { return result.buffer.readIntBE(start, bufferAmount); } } } } function initBuffer$1(byteEndian, wordEndian, start, bufferAmount, buffer) { if (wordEndian !== "little" && byteEndian !== "little") return buffer; return buffer.slice(start, start + bufferAmount); } function decodeUInt(byteEndian, wordEndian, start, byteTypeNumber, bufferAmount, result, bitNr) { const buffer = initBuffer$1(byteEndian, wordEndian, start, bufferAmount, result.buffer); if (wordEndian === "little") { if (byteEndian !== "little") buffer.swap16(); if (byteTypeNumber === 64) { return Number(buffer.readBigUInt64LE(0)); } else if (byteTypeNumber === 8 && typeof bitNr === "number") { return buffer.readUIntLE(bitNr, bufferAmount); } else { return buffer.readUIntLE(0, bufferAmount); } } else { if (byteEndian === "little") { buffer.swap16(); if (byteTypeNumber === 64) { return Number(buffer.readBigUInt64BE(0)); } else if (byteTypeNumber === 8 && typeof bitNr === "number") { return buffer.readUIntBE(bitNr, bufferAmount); } else { return buffer.readUIntBE(0, bufferAmount); } } else { if (byteTypeNumber === 64) { return Number(result.buffer.readBigUInt64BE(start)); } else if (byteTypeNumber === 8 && typeof bitNr === "number") { return result.buffer.readUIntBE(start + bitNr, bufferAmount); } else { return result.buffer.readUIntBE(start, bufferAmount); } } } } function initBuffer(byteEndian, wordEndian, start, bufferAmount, buffer) { if (wordEndian !== "little" && byteEndian !== "little") return buffer; return buffer.slice(start, start + bufferAmount); } function decodeFloat(byteEndian, wordEndian, start, byteTypeNumber, bufferAmount, result) { const buffer = initBuffer(byteEndian, wordEndian, start, bufferAmount, result.buffer); if (wordEndian === "little") { if (byteEndian !== "little") buffer.swap16(); return byteTypeNumber === 64 ? buffer.readDoubleLE(0) : buffer.readFloatLE(0); } else { if (byteEndian === "little") { buffer.swap16(); return byteTypeNumber === 64 ? buffer.readDoubleBE(0) : buffer.readFloatBE(0); } else { return byteTypeNumber === 64 ? buffer.readDoubleBE(start) : buffer.readFloatBE(start); } } } function decodeBit(byteEndian, wordEndian, sw, start, byteTypeNumber, bufferAmount, result, bitNr) { if (byteTypeNumber === 16 && typeof bitNr === "number") { if (wordEndian === "little") { const value = result.buffer.readUIntBE(start, bufferAmount); const binary = value.toString(2).padStart(16, "0"); const bitValue = Number(binary.charAt(bitNr)) ?? 0; return bitValue; } const value1 = result.buffer.readUIntBE(start, bufferAmount); const binary1 = value1.toString(2).padStart(16, "0"); const currentBit = 15 - bitNr; const bitValue1 = Number(binary1.charAt(currentBit)) ?? 0; return bitValue1; } else if (byteTypeNumber === 1) { const value2 = result.buffer.readUIntBE(start, bufferAmount + 1); const binary2 = value2.toString(2); return Number(binary2.charAt(0)) ?? 0; } else if (byteTypeNumber === 32 && typeof bitNr === "number") { const value3 = result.buffer.readUIntBE(start, bufferAmount); const binary3 = value3.toString(2).padStart(32, "0"); const currentBit1 = 31 - bitNr; const bitValue2 = Number(binary3.charAt(currentBit1)) ?? 0; return bitValue2; } } function decodeHex(byteEndian, sw, start, byteTypeNumber, bufferAmount, result, node, byteSize) { if (!byteSize) return; const byteEndianLittle = sw !== undefined != (byteEndian === "little"); if (byteSize === 8) { const value = byteEndianLittle ? result.buffer.swap16().readBigInt64BE(start) : result.buffer.readBigInt64BE(start); return value.toString(16); } else { const value1 = byteEndianLittle ? result.buffer.swap16().readUIntBE(start, byteSize) : result.buffer.readUIntBE(start, byteSize); return value1.toString(16); } } function decodeAscii(byteEndian, sw, start, result, node, byteSize) { if (!byteSize) return; const byteEndianLittle = sw !== undefined != (byteEndian === "little"); if (byteEndianLittle) { const buffer = result.buffer.slice(start, start + byteSize).swap16().filter((x)=>32 <= x && x < 127 || 160 <= x && x < 256); const asciiString = String.fromCharCode.apply(undefined, buffer); return asciiString; } const buffer1 = result.buffer.slice(start, start + byteSize).filter((x)=>32 <= x && x < 127 || 160 <= x && x < 256); const asciiString1 = String.fromCharCode.apply(undefined, buffer1); return asciiString1; } function decodeMod(byteEndian, wordEndian, byteType, start, result) { const byteTypeSize = byteType.split("-")[1]; const byteTypeNumber = Number.parseInt(byteTypeSize); const bufferAmount = Math.ceil(byteTypeNumber / 8); const buffer = result.buffer.slice(start, start + bufferAmount); if (byteTypeNumber === 64) { let tmpValue; if (byteEndian === "little" && wordEndian === "little") { tmpValue = Number(buffer.readInt16LE(0)) * 10_000 ** 3; tmpValue += Number(buffer.readInt16LE(2)) * 10_000 ** 2; tmpValue += Number(buffer.readInt16LE(4)) * 10_000; tmpValue += Number(buffer.readInt16LE(6)); } else if (byteEndian === "big" && wordEndian === "little") { tmpValue = Number(buffer.readInt16BE(0)) * 10_000 ** 3; tmpValue += Number(buffer.readInt16BE(2)) * 10_000 ** 2; tmpValue += Number(buffer.readInt16BE(4)) * 10_000; tmpValue += Number(buffer.readInt16BE(6)); } else if (byteEndian === "little" && wordEndian === "big") { tmpValue = Number(buffer.readInt16LE(0)); tmpValue += Number(buffer.readInt16LE(2)) * 10_000; tmpValue += Number(buffer.readInt16LE(4)) * 10_000 ** 2; tmpValue += Number(buffer.readInt16LE(6)) * 10_000 ** 3; } else { tmpValue = Number(buffer.readInt16BE(0)); tmpValue += Number(buffer.readInt16BE(2)) * 10_000; tmpValue += Number(buffer.readInt16BE(4)) * 10_000 ** 2; tmpValue += Number(buffer.readInt16BE(6)) * 10_000 ** 3; } return tmpValue; } } function controlValueScaling(value, unit, node, scalingConfig, scalingFactor, byte_type, scalingType) { const bytetypePlain = { DUMMY16: undefined, DUMMY32: undefined, DUMMY64: undefined, BIT1: "bit1", BIT16: "bit16", "BIT16-SW": "bit16", BIT32: "bit32", "BIT32-SW": "bit32", HEX: "ascii", "HEX-SW": "ascii", ASCII: "ascii", "ASCII-SW": "ascii", UINT8: "uInt8", UINT16: "uInt16", SINT16: "sInt16", UINT32: "uInt32", SMINT32: "sInt32", SINT32: "sInt32", UINT64: "uInt64", SINT64: "sInt64", FLOAT32: "Float32", FLOAT64: "Float64", "MOD10-64": "sInt64" }; if (!byte_type) return; const plainType = bytetypePlain[byte_type]; if (scalingType && scalingConfig.hasOwnProperty(scalingType)) { const configType = scalingType; if (plainType && scalingConfig[configType].hasOwnProperty(plainType)) { for (const [key, plainTypeValue] of Object.entries(scalingConfig[configType])){ if (key === plainType) { const sizeControlValues = plainTypeValue; if (sizeControlValues.hasOwnProperty(value)) { const errorMessage = Object.values(sizeControlValues).map((controlValue)=>controlValue); node.warn(`${scalingType}:Control value ${value} for unit ${unit}: ${errorMessage[0]}`); return; } } } } } let scaledValue = value; if (scalingFactor) { scaledValue = value * scalingFactor; } return scaledValue; } function generalScaling(scalingFactor, value) { const scaledValue = value * scalingFactor; return scaledValue; } function generex(value, unit, node, scalingConfig, scalingFactor, scalingFunction) { let scaledValue = value; if (scalingFactor && ![ -1, -9999 ].includes(value)) { scaledValue = value * scalingFactor; } if (scalingFunction === "temperature") { scaledValue = (value - 78) / 2; } return scaledValue; } function scaling2(tempValue) { const value = Number(tempValue.toFixed(2)); const newUnit = "0: no alarm, 1: active alarm"; return [ value, newUnit ]; } function scaling3(tempValue, unit, ktaktvRatio) { if (ktaktvRatio < 5000) { const value = Number((tempValue * 0.01).toFixed(2)); return [ value, unit ]; } else { const value1 = Number((tempValue * 1).toFixed(2)); return [ value1, unit ]; } } function scaling3_2(tempValue, unit, ktaktvRatio) { if (ktaktvRatio < 5000) { const value = Number((tempValue * 0.01).toFixed(2)); return [ value, unit ]; } else { const value1 = Number((tempValue * 10).toFixed(2)); return [ value1, unit ]; } } function scaling3_CE4D(tempValue, unit, ktaktvRatio) { if (ktaktvRatio < 6000) { const value = Number((tempValue * 0.01).toFixed(2)); return [ value, unit ]; } else { const value1 = Number((tempValue * 1).toFixed(2)); return [ value1, unit ]; } } function scaling3_6(tempValue, unit, ktaktvRatio, result, node, start, offset = 0) { const signPosition = Number(result.data[start / 2 + offset]); const sign = signPosition === 1 ? -1 : 1; const multi = ktaktvRatio < 5000 ? 0.01 : 1; const value = Number((tempValue * multi * sign).toFixed(2)); return [ value, unit ]; } function scaling4(tempValue, unit, ktaktvRatio, node) { if (ktaktvRatio >= 1 && ktaktvRatio < 10) { const value = Number((tempValue * 10).toFixed(2)); return [ value, unit ]; } else if (ktaktvRatio >= 10 && ktaktvRatio < 100) { const value1 = Number((tempValue * 100).toFixed(2)); return [ value1, unit ]; } else if (ktaktvRatio >= 100 && ktaktvRatio < 1000) { const value2 = Number((tempValue * 1000).toFixed(2)); return [ value2, unit ]; } else if (ktaktvRatio >= 1000 && ktaktvRatio < 10_000) { const value3 = Number((tempValue * 10_000).toFixed(2)); return [ value3, unit ]; } else if (ktaktvRatio >= 10_000 && ktaktvRatio < 100_000) { const value4 = Number((tempValue * 100_000).toFixed(2)); return [ value4, unit ]; } else if (ktaktvRatio >= 100_000) { const value5 = Number((tempValue * 100_000).toFixed(2)); return [ value5, unit ]; } node.warn("No matching ktaktvRatio"); return [ tempValue, unit ]; } function scaling4_2(tempValue, unit, ktaktvRatio, node) { if (ktaktvRatio >= 1 && ktaktvRatio < 10) { const value = Number((tempValue * 10).toFixed(2)); return [ value, unit ]; } else if (ktaktvRatio >= 10 && ktaktvRatio < 100) { const value1 = Number((tempValue * 100).toFixed(2)); return [ value1, unit ]; } else if (ktaktvRatio >= 100 && ktaktvRatio < 1000) { const value2 = Number((tempValue * 1000).toFixed(2)); return [ value2, unit ]; } else if (ktaktvRatio >= 1000 && ktaktvRatio < 10_000) { const value3 = Number((tempValue * 10_000).toFixed(2)); return [ value3, unit ]; } else if (ktaktvRatio >= 10_000 && ktaktvRatio < 100_000) { const value4 = Number((tempValue * 100_000).toFixed(2)); return [ value4, unit ]; } else if (ktaktvRatio >= 100_000) { const value5 = Number((tempValue * 1_000_000).toFixed(2)); return [ value5, unit ]; } node.warn("No matching ktaktvRatio"); return [ tempValue, unit ]; } function scaling6(tempValue, unit) { const value = Number(tempValue.toFixed(2)); return [ value, unit ]; } function scaling7(tempValue) { const value = Number(tempValue.toFixed(2)); const newUnit = "High part energy, MWH/MVArh"; return [ value, newUnit ]; } function imeScaling(tmpValue, unit, node, result, start, scalingFactor, scalingFunction, offsetScaling) { if (!node.ktaktvRatio) { node.warn("Missing KTV KTA"); return [ tmpValue, unit ]; } if (scalingFactor) { const scaledValue = tmpValue * scalingFactor; return [ scaledValue, unit ]; } else { switch(scalingFunction){ case "scaling2": { return scaling2(tmpValue); } case "scaling3": { return scaling3(tmpValue, unit, node.ktaktvRatio); } case "scaling3_2": { return scaling3_2(tmpValue, unit, node.ktaktvRatio); } case "scaling3_CE4D": { return scaling3_CE4D(tmpValue, unit, node.ktaktvRatio); } case "scaling3_6": { return scaling3_6(tmpValue, unit, node.ktaktvRatio, result, node, start, offsetScaling); } case "scaling4": { return scaling4(tmpValue, unit, node.ktaktvRatio, node); } case "scaling4_2": { return scaling4_2(tmpValue, unit, node.ktaktvRatio, node); } case "scaling6": { return scaling6(tmpValue, unit); } case "scaling7": { return scaling7(tmpValue); } } } return [ tmpValue, unit ]; } function ion7500(value, unit, node, scalingConfig, scalingFactor) { let scaledValue = value; if (scalingFactor) { scaledValue = value * scalingFactor; } if (unit === "register1") { scalingConfig.ionConfig.register1 = scaledValue; } else if (unit === "register2") { scalingConfig.ionConfig.register2 = scaledValue; scalingConfig.ionConfig.register_tot = scalingConfig.ionConfig.register1 + scalingConfig.ionConfig.register2; scaledValue = scalingConfig.ionConfig.register_tot; } return scaledValue; } function janitza(value, unit, node, scalingConfig, scalingFactor, scalingFunction) { let scaledValue = value; if (scalingFactor) { scaledValue = value * scalingFactor; } if (scalingFunction === "energy_multiplier") { scalingConfig.janitza.energy_multiplier = scaledValue; } else if (scalingFunction === "energy") { scaledValue = value * 1000 * 1000 ** scalingConfig.janitza.energy_multiplier; } return scaledValue; } function schneiderPm800(value, unit, node, scalingConfig, scalingFactor) { let scaledValue = value; if (scalingFactor) { scaledValue = value * scalingFactor; } switch(unit){ case "register1": { scalingConfig.schneider.register1 = scaledValue; break; } case "register2": { scalingConfig.schneider.register2 = scaledValue; break; } case "register3": { scalingConfig.schneider.register3 = scaledValue; break; } case "register4": { scalingConfig.schneider.register4 = scaledValue; scalingConfig.schneider.register_tot = scalingConfig.schneider.register1 + scalingConfig.schneider.register2 + scalingConfig.schneider.register3 + scalingConfig.schneider.register4; scaledValue = scalingConfig.schneider.register_tot; break; } } return scaledValue; } function siemens(value, unit, node, scalingConfig, scalingFactor, scalingFunction) { let scaledValue = value; if (scalingFactor) { scaledValue = value * scalingFactor; } if (scalingFunction === "energy_multiplier") { scalingConfig.siemens.energy_multiplier = scaledValue; } else if (scalingFunction === "energy") { scaledValue = value * scalingConfig.siemens.energy_multiplier; } return scaledValue; } function checkScaling(scalingType, value, unit, node, result, start, scalingConfig, scalingFactor, scalingFunction, offsetScaling, byteType) { const scalingTypeLower = scalingType?.toLowerCase(); if (scalingTypeLower === "general" && scalingFactor) { return generalScaling(scalingFactor, value); } if (scalingTypeLower === "ime") { return imeScaling(value, unit, node, result, start, scalingFactor, scalingFunction, offsetScaling)[0]; } if (scalingTypeLower === "schneider_pm800") { return schneiderPm800(value, unit, node, scalingConfig, scalingFactor); } if (scalingTypeLower === "ion_7500") { return ion7500(value, unit, node, scalingConfig, scalingFactor); } if (scalingTypeLower === "siemens") { return siemens(value, unit, node, scalingConfig, scalingFactor, scalingFunction); } if (scalingTypeLower === "janitza") { return janitza(value, unit, node, scalingConfig, scalingFactor, scalingFunction); } if (scalingTypeLower === "generex") { return generex(value, unit, node, scalingConfig, scalingFactor, scalingFunction); } if (scalingType && [ "abb_energy", "cg_em", "weidmuller", "abb_cms", "deepsea" ].includes(scalingTypeLower || "")) { return controlValueScaling(value, unit, node, scalingConfig, scalingFactor, byteType, scalingType); } return value; } const scalingConfig = { schneider: { register1: 0, register2: 0, register3: 0, register4: 0, register_tot: 0 }, ionConfig: { register1: 0, register2: 0, register_tot: 0 }, siemens: { energy_multiplier: 0 }, janitza: { energy_multiplier: 0 }, abb_energy: { sInt16: { 0x7f_ff: "Error" }, uInt16: { 0xff_ff: "Error" }, sInt32: { 0x7f_ff_ff_ff: "Error" }, uInt32: { 0xff_ff_ff_ff: "Error" }, sInt64: { "9223372036854775807": "Error" }, uInt64: { "18446744073709551615": "Error" } }, cg_em: { sInt32: { 0x7f_fd_ff_ff: "Error" } }, weidmuller: { sInt16: { 0x7f_ff: "Error", 0x80_00: "Error" }, uInt16: { 0x7f_ff: "Error", 0x80_00: "Error" } }, abb_cms: { uInt16: { 0x7f_f0: "Data pending, acquisition in progress", 0x7f_fc: "The sensor is known but not accessible at the moment", 0x7f_fd: "Data type TrueRMS / AC / DC is disabled", 0x7f_fe: "Overload (beyond full range)", 0x7f_ff: "Forbidden (no sensor with ID xx)", 0x7f_f1: "reserved", 0x7f_f2: "reserved", 0x7f_f3: "reserved", 0x7f_f4: "reserved", 0x7f_f5: "reserved", 0x7f_f6: "reserved", 0x7f_f7: "reserved", 0x7f_f8: "reserved", 0x7f_f9: "reserved", 0x7f_fa: "reserved", 0x7f_fb: "reserved" }, sInt16: { 0x7f_f0: "Data pending, acquisition in progress", 0x7f_fc: "The sensor is known but not accessible at the moment", 0x7f_fd: "Data type TrueRMS / AC / DC is disabled", 0x7f_fe: "Overload (beyond full range)", 0x7f_ff: "Forbidden (no sensor with ID xx)", 0x7f_f1: "reserved", 0x7f_f2: "reserved", 0x7f_f3: "reserved", 0x7f_f4: "reserved", 0x7f_f5: "reserved", 0x7f_f6: "reserved", 0x7f_f7: "reserved", 0x7f_f8: "reserved", 0x7f_f9: "reserved", 0x7f_fa: "reserved", 0x7f_fb: "reserved" }, uInt32: { 0xff_ff_7f_f0: "Data pending, acquisition in progress", 0xff_ff_7f_fc: "The sensor is known but not accessible at the moment", 0xff_ff_7f_fd: "Data type TrueRMS / AC / DC is disabled", 0xff_ff_7f_fe: "Overload (beyond full range)", 0xff_ff_7f_ff: "Forbidden (no sensor with ID xx)", 0xff_ff_7f_f1: "reserved", 0xff_ff_7f_f2: "reserved", 0xff_ff_7f_f3: "reserved", 0xff_ff_7f_f4: "reserved", 0xff_ff_7f_f5: "reserved", 0xff_ff_7f_f6: "reserved", 0xff_ff_7f_f7: "reserved", 0xff_ff_7f_f8: "reserved", 0xff_ff_7f_f9: "reserved", 0xff_ff_7f_fa: "reserved", 0xff_ff_7f_fb: "reserved" } }, deepsea: { sInt16: { 0x7f_ff: "Error" }, uInt16: { 0xff_ff: "Error" }, sInt32: { 0x7f_ff_ff_ff: "Error" }, uInt32: { 0xff_ff_ff_ff: "Error" } } }; function decode_register_group(node, gapitCode, object, result, scalingType) { const first = object.group[0] ? object.group[0]["address"] : Number.POSITIVE_INFINITY; const group = object.group.map((variable)=>{ const [byteType, sw] = variable.byte_type.split("-"); const byteEndian = variable.byte_endian || gapitCode.byte_endian || "big"; const wordEndian = variable.word_endian || gapitCode.word_endian || "big"; const byteTypeNumber = Number.parseInt(byteType.replace(/^\D+/g, "")); const bufferAmount = Math.ceil(byteTypeNumber / 8); const byteTypeString = byteType.replace(/\d/g, ""); const start = (variable.address - first) * 2; let value = 0; if (byteTypeString.includes("DUMMY")) { value = 0; } else if (byteTypeString.includes("SINT")) { value = decodeSInt(byteEndian, wordEndian, start, byteTypeNumber, bufferAmount, result, variable.bit_nr); } else if (byteTypeString.includes("UINT")) { value = decodeUInt(byteEndian, wordEndian, start, byteTypeNumber, bufferAmount, result, variable.bit_nr); } else if (byteTypeString.includes("FLOAT")) { value = decodeFloat(byteEndian, wordEndian, start, byteTypeNumber, bufferAmount, result); } else if (byteTypeString.includes("BIT")) { value = decodeBit(byteEndian, wordEndian, sw, start, byteTypeNumber, bufferAmount, result, variable.bit_nr); } else if (byteTypeString.includes("HEX")) { value = decodeHex(byteEndian, sw, start, byteTypeNumber, bufferAmount, result, node, variable.byte_size); // eslint-disable-next-line unicorn/text-encoding-identifier-case } else if (byteTypeString.includes("ASCII")) { value = decodeAscii(byteEndian, sw, start, result, node, variable.byte_size); } else if (byteTypeString.includes("MOD")) { value = decodeMod(byteEndian, wordEndian, variable.byte_type, start, result); } else { return { address: variable.address, description: variable.description, scaling_factor: variable.scaling_factor, unit: variable.unit, byte_type: variable.byte_type, value: undefined }; } if (typeof value === "number" && variable.scaling_factor) { value = checkScaling(scalingType, value, variable.unit, node, result, start, scalingConfig, variable.scaling_factor, variable.scaling_function, variable.offset_scaling, variable.byte_type); } return { address: variable.address, description: variable.description, scaling_factor: variable.scaling_factor, unit: variable.unit, byte_type: variable.byte_type, value }; }); return { group_name: object.group_name, read_priority: object.read_priority, address_type: object.address_type, register_type: object.register_type, group: group }; } function decode_coil_group(node, gapitCode, object, result) { let bit_nr = -1; const first = object.group[0] ? object.group[0]["address"] : Number.POSITIVE_INFINITY; const group = object.group.map((variable)=>{ let value = 0; bit_nr = variable.address === undefined ? ++bit_nr : variable.address - first; if (variable.byte_type.startsWith("DUMMY")) { value = 0; } else if (variable.byte_type === "BIT1") { value = result.data[bit_nr] ? 1 : 0; } else { return { address: variable.address, description: variable.description, scaling_factor: variable.scaling_factor, unit: variable.unit, byte_type: variable.byte_type, value: undefined }; } return { address: variable.address, description: variable.description, scaling_factor: variable.scaling_factor, unit: variable.unit, byte_type: variable.byte_type, value }; }); return { group_name: object.group_name, read_priority: object.read_priority, address_type: object.address_type, register_type: object.register_type, group: group }; } class RegisterDecodeReader { async readDecode(node, client, unitId, gapitCode, object, scalingType) { if (unitId) { client.setID(unitId); } const read = await client.readRegister(this.group.registerType, this.group.startAddress, this.group.addressLength); const data = decode_register_group(node, gapitCode, object, read, scalingType); return { read: read, data: data }; } constructor(group){ this.group = group; } } class StatusDecoderReader { async readDecode(node, client, unitId, gapitCode, object, // eslint-disable-next-line @typescript-eslint/no-unused-vars scalingType) { if (unitId) { client.setID(unitId); } const read = await client.readCoil(this.group.registerType, this.group.startAddress, this.group.addressLength); const data = decode_coil_group(node, gapitCode, object, read); return { read: read, data: data }; } constructor(group){ this.group = group; } } const readDecoders = { input: RegisterDecodeReader, holding: RegisterDecodeReader, status: StatusDecoderReader, coil: StatusDecoderReader }; function decodeReaderFactory(group) { return new readDecoders[group.registerType](group); } const nodeInit = (RED)=>{ function gapitModbus(config) { RED.nodes.createNode(this, config); this.clients = {}; this.on("input", async (msg, send, done)=>{ try { const activeConfig = checkNodeConfig(msg, config, this); if (!activeConfig) return; const groups = convertConfig(activeConfig.gapitCode, this); if (!(activeConfig.socketAddress in this.clients) || !this.clients[activeConfig.socketAddress].isOpen) { this.clients[activeConfig.socketAddress] = await connectModbus(this, activeConfig.host, activeConfig.portOptions); } if (msg.ktv && msg.kta) { this.ktaktvRatio = msg.ktv * msg.kta; } if (groups instanceof Object) { msg.gapit_results = { [activeConfig.deviceName]: [] }; const result = []; for (const [index, group] of groups.entries()){ const decodeReader = decodeReaderFactory(group); const data = await decodeReader.readDecode(this, this.clients[activeConfig.socketAddress], activeConfig.unitId, activeConfig.gapitCode, activeConfig.gapitCode.objects[index], activeConfig.scalingType); msg.gapit_results[activeConfig.deviceName].push(data.data); result.push(data.read); } if (!result) return; msg.modbus_buffer = result; } send(msg); done(); } catch (error) { this.error(error); } }); this.on("close", ()=>{ try { for (const client of Object.values(this.clients)){ client.close(()=>{ /* do nothing */ }); } } catch (error) { this.error(error); } }); } RED.nodes.registerType("gapit-modbus", gapitModbus); }; module.exports = nodeInit;