UNPKG

n8n-nodes-modbus-fccomplete

Version:

n8n nodes for industrial automation with complete Modbus support (FC1-FC4) and intuitive data conversion with scaling options

819 lines 34.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModbusDataConverter = void 0; const n8n_workflow_1 = require("n8n-workflow"); const DataConversionUtils_1 = require("./DataConversionUtils"); class ModbusDataConverter { constructor() { this.description = { displayName: 'MODBUS Converter', name: 'modbusDataConverter', icon: 'file:modbus.svg', group: ['transform'], version: 4, description: 'Convert Modbus register data with quick convert and custom options', eventTriggerDescription: '', defaults: { name: 'Modbus Data Converter', }, inputs: ['main'], outputs: ['main'], properties: [ { displayName: 'Data Type', name: 'dataType', type: 'options', options: [ { name: 'Register Data (FC3/FC4)', value: 'register', description: 'Numeric register values for conversion', }, { name: 'Discrete Data (FC1/FC2)', value: 'discrete', description: 'Boolean coil/input values', }, ], default: 'register', description: 'Type of Modbus data being processed', }, { displayName: 'Conversion Mode', name: 'conversionMode', type: 'options', options: [ { name: 'Quick Convert', value: 'quick', description: 'Simple conversions for common Modbus data types', }, { name: 'Custom', value: 'custom', description: 'Manual configuration for advanced use cases', }, ], default: 'quick', description: 'Choose how to convert the data', }, { displayName: 'Quick Convert Type', name: 'quickConvertType', type: 'options', displayOptions: { show: { conversionMode: ['quick'], }, }, options: [ { name: 'Single Value (INT16/Scaled)', value: 'single', description: 'Convert single register value', }, { name: 'Float (2 Registers)', value: 'float', description: 'Convert two registers to floating point', }, { name: 'Long Integer (2 Registers)', value: 'long', description: 'Convert two registers to 32-bit integer', }, { name: 'Double (4 Registers)', value: 'double', description: 'Convert four registers to 64-bit double', }, { name: 'BCD (Binary Coded Decimal)', value: 'bcd', description: 'Convert BCD encoded values', }, { name: 'Bitfield (Status/Flags)', value: 'bitfield', description: 'Extract individual bits from register', }, { name: 'All Common Types', value: 'all', description: 'Show all possible conversions', action: 'Show all conversions for this data', }, { name: 'Discrete Bitfield', value: 'discrete_bitfield', description: 'Map coils to individual bit_0, bit_1, bit_2... fields', }, { name: 'Active Position', value: 'active_position', description: 'Find first active coil and return position number', }, { name: 'Discrete Summary', value: 'discrete_summary', description: 'Count active coils and list active indices', }, { name: 'All Discrete Types', value: 'discrete_all', description: 'Show all discrete conversion options', }, ], default: 'float', }, { displayName: 'Byte Order', name: 'quickByteOrder', type: 'options', displayOptions: { show: { conversionMode: ['quick'], dataType: ['register'], quickConvertType: ['float', 'long', 'double'], }, }, options: [ { name: 'Big Endian (BE)', value: 'BE', description: 'Most significant byte first (default)', }, { name: 'Little Endian (LE)', value: 'LE', description: 'Least significant byte first', }, ], default: 'BE', }, { displayName: 'Word Swap', name: 'wordSwap', type: 'boolean', displayOptions: { show: { conversionMode: ['quick'], dataType: ['register'], quickConvertType: ['float', 'long', 'double'], }, }, default: false, description: 'Swap the order of 16-bit words within multi-register values (ABCD→CDAB, DCBA→BADC)', }, { displayName: 'Scale', name: 'enableScaling', type: 'boolean', displayOptions: { show: { conversionMode: ['quick'], }, }, default: false, description: 'Apply scaling to the converted value', }, { displayName: 'Scale Factor', name: 'scaleFactorValue', type: 'number', displayOptions: { show: { conversionMode: ['quick'], enableScaling: [true], }, }, typeOptions: { numberPrecision: 6, minValue: 0.000001, maxValue: 1000000, }, default: 1, description: 'Multiply result by this factor (e.g., 0.001 for ÷1000, 1000 for ×1000)', }, { displayName: 'Long Integer Value', name: 'longIntegerValue', type: 'options', displayOptions: { show: { conversionMode: ['quick'], dataType: ['register'], quickConvertType: ['long'], }, }, options: [ { name: 'Signed (Allows Negative Values)', value: 'signed', description: 'Treats the value as signed 32-bit integer', }, { name: 'Unsigned (Positive Only)', value: 'unsigned', description: 'Treats the value as unsigned 32-bit integer', }, { name: 'Both (Returns Both Signed and Unsigned)', value: 'both', description: 'Returns both value_signed and value_unsigned', }, ], default: 'signed', description: 'Choose which value to return for 32-bit integers', }, { displayName: 'Include Conversion Metadata', name: 'includeMetadata', type: 'boolean', displayOptions: { show: { conversionMode: ['quick'], }, }, default: false, description: 'Whether to include conversion details (type, scale factor, raw registers)', }, { displayName: 'Add Timestamp', name: 'addTimestamp', type: 'boolean', displayOptions: { show: { conversionMode: ['quick'], }, }, default: false, description: 'Whether to add timestamp to output', }, { displayName: 'Output Field Name', name: 'outputFieldName', type: 'string', displayOptions: { show: { conversionMode: ['quick'], }, }, default: 'output', description: 'Name of the field containing converted values', }, { displayName: 'Metadata Field Name', name: 'metadataFieldName', type: 'string', displayOptions: { show: { conversionMode: ['quick'], includeMetadata: [true], }, }, default: 'conversion', description: 'Name of the field containing conversion metadata', }, { displayName: 'Custom Conversions', name: 'conversions', type: 'collection', placeholder: 'Add Conversion', default: {}, displayOptions: { show: { conversionMode: ['custom'], }, }, typeOptions: { multipleValues: true, multipleValueButtonText: 'Add Conversion Rule', }, options: [ { displayName: 'Name', name: 'name', type: 'string', default: '', description: 'Name for the converted value', }, { displayName: 'Start Register', name: 'startRegister', type: 'number', default: 0, description: 'Starting register index (0-based)', }, { displayName: 'Data Type', name: 'dataType', type: 'options', options: [ { name: 'INT16 - Signed 16-Bit Integer', value: 'int16', }, { name: 'UINT16 - Unsigned 16-Bit Integer', value: 'uint16', }, { name: 'INT32 - Signed 32-Bit Integer', value: 'int32', }, { name: 'UINT32 - Unsigned 32-Bit Integer', value: 'uint32', }, { name: 'FLOAT32 - IEEE 754 32-Bit Float', value: 'float32', }, { name: 'SCALED - Raw Value with Scaling', value: 'scaled', }, ], default: 'int16', description: 'Data type for conversion', }, { displayName: 'Byte Order', name: 'byteOrder', type: 'options', options: [ { name: 'Big Endian (ABCD)', value: 'big_endian', }, { name: 'Little Endian (DCBA)', value: 'little_endian', }, ], default: 'big_endian', description: 'Byte order for multi-register values', }, { displayName: 'Word Swap', name: 'wordSwap', type: 'boolean', displayOptions: { show: { dataType: ['int32', 'uint32', 'float32'], }, }, default: false, description: 'Swap the order of 16-bit words within multi-register values (ABCD→CDAB, DCBA→BADC)', }, { displayName: 'Scale Factor', name: 'scaleFactor', type: 'number', displayOptions: { show: { dataType: ['scaled'], }, }, default: 1, description: 'Divide raw value by this factor', }, { displayName: 'Offset', name: 'offset', type: 'number', displayOptions: { show: { dataType: ['scaled'], }, }, default: 0, description: 'Add this value after scaling', }, ], }, ], }; } async execute() { const items = this.getInputData(); const returnData = []; const conversionMode = this.getNodeParameter('conversionMode', 0); for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { const item = items[itemIndex]; try { let outputData = {}; switch (conversionMode) { case 'quick': outputData = await executeQuickMode(this, item, itemIndex); break; case 'custom': outputData = await executeCustomMode(this, item, itemIndex); break; } returnData.push({ json: outputData, pairedItem: { item: itemIndex }, }); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, { itemIndex }); } } return [returnData]; } } exports.ModbusDataConverter = ModbusDataConverter; async function executeQuickMode(context, item, itemIndex) { const dataType = context.getNodeParameter('dataType', itemIndex); const quickType = context.getNodeParameter('quickConvertType', itemIndex); const byteOrder = context.getNodeParameter('quickByteOrder', itemIndex, 'BE'); const wordSwap = context.getNodeParameter('wordSwap', itemIndex, false); const enableScaling = context.getNodeParameter('enableScaling', itemIndex, false); const scaleFactorValue = context.getNodeParameter('scaleFactorValue', itemIndex, 1); const longIntegerValue = context.getNodeParameter('longIntegerValue', itemIndex, 'signed'); const includeMetadata = context.getNodeParameter('includeMetadata', itemIndex, false); const addTimestamp = context.getNodeParameter('addTimestamp', itemIndex, false); const outputFieldName = context.getNodeParameter('outputFieldName', itemIndex, 'output'); const metadataFieldName = context.getNodeParameter('metadataFieldName', itemIndex, 'conversion'); const registers = extractRegisters(item.json); if (!registers || registers.length === 0) { throw new Error('No register/discrete data found in input'); } if (dataType === 'discrete') { return executeDiscreteMode(registers, quickType, includeMetadata, addTimestamp, outputFieldName, metadataFieldName); } const numericRegisters = registers; const result = {}; const convertedData = {}; const metadata = {}; const scaleFactor = enableScaling ? scaleFactorValue : 1; if (includeMetadata) { metadata.type = quickType; metadata.byteOrder = byteOrder; metadata.wordSwap = wordSwap; metadata.rawRegisters = numericRegisters; metadata.registerCount = numericRegisters.length; if (enableScaling) { metadata.scaleFactor = scaleFactor; metadata.scalingApplied = true; } if (quickType === 'long') { metadata.longIntegerValue = longIntegerValue; } } switch (quickType) { case 'single': if (numericRegisters.length >= 1) { const value = numericRegisters[0]; convertedData.value = enableScaling ? value * scaleFactor : value; if (includeMetadata) metadata.dataType = 'int16'; } break; case 'float': if (numericRegisters.length >= 2) { const floatResult = DataConversionUtils_1.DataConversionUtils.convertData(numericRegisters, { name: 'value', startRegister: 0, dataType: 'float32', byteOrder: byteOrder === 'BE' ? 'big_endian' : 'little_endian', wordSwap: wordSwap, }); if (floatResult.valid) { convertedData.value = enableScaling ? floatResult.value * scaleFactor : floatResult.value; if (includeMetadata) metadata.dataType = 'float32'; } } break; case 'long': if (numericRegisters.length >= 2) { const int32Result = DataConversionUtils_1.DataConversionUtils.convertData(numericRegisters, { name: 'value_signed', startRegister: 0, dataType: 'int32', byteOrder: byteOrder === 'BE' ? 'big_endian' : 'little_endian', wordSwap: wordSwap, }); const uint32Result = DataConversionUtils_1.DataConversionUtils.convertData(numericRegisters, { name: 'value_unsigned', startRegister: 0, dataType: 'uint32', byteOrder: byteOrder === 'BE' ? 'big_endian' : 'little_endian', wordSwap: wordSwap, }); if (longIntegerValue === 'signed' && int32Result.valid) { convertedData.value = enableScaling ? int32Result.value * scaleFactor : int32Result.value; if (includeMetadata) metadata.dataType = 'int32'; } else if (longIntegerValue === 'unsigned' && uint32Result.valid) { convertedData.value = enableScaling ? uint32Result.value * scaleFactor : uint32Result.value; if (includeMetadata) metadata.dataType = 'uint32'; } else if (longIntegerValue === 'both') { if (int32Result.valid) { convertedData.value_signed = enableScaling ? int32Result.value * scaleFactor : int32Result.value; } if (uint32Result.valid) { convertedData.value_unsigned = enableScaling ? uint32Result.value * scaleFactor : uint32Result.value; } if (includeMetadata) metadata.dataType = 'int32/uint32'; } } break; case 'double': if (numericRegisters.length >= 4) { const doubleValue = convertToDouble(numericRegisters, byteOrder === 'BE', wordSwap); if (typeof doubleValue === 'number') { convertedData.value = enableScaling ? doubleValue * scaleFactor : doubleValue; if (includeMetadata) metadata.dataType = 'double'; } } break; case 'bcd': if (numericRegisters.length >= 1) { const bcdValue = convertBCD(numericRegisters[0]); if (!isNaN(bcdValue)) { convertedData.value = enableScaling ? bcdValue * scaleFactor : bcdValue; if (includeMetadata) metadata.dataType = 'bcd'; } } break; case 'bitfield': if (numericRegisters.length >= 1) { convertedData.bits = extractBits(numericRegisters[0]); if (includeMetadata) metadata.dataType = 'bitfield'; } break; case 'all': return organizeAllConversionsOutput(numericRegisters, byteOrder, wordSwap, enableScaling, scaleFactor, includeMetadata, addTimestamp, outputFieldName, metadataFieldName); } result[outputFieldName] = convertedData; if (includeMetadata) { result[metadataFieldName] = metadata; } if (addTimestamp) { result.timestamp = new Date().toISOString(); } return result; } async function executeCustomMode(context, item, itemIndex) { const conversions = context.getNodeParameter('conversions', itemIndex, []); const registers = extractRegisters(item.json); if (!registers || registers.length === 0) { throw new Error('No register data found in input'); } const numericRegisters = registers; const output = { _raw_registers: numericRegisters, _register_count: numericRegisters.length, }; for (const conv of conversions) { if (!conv.name) continue; try { const rule = { name: conv.name, startRegister: conv.startRegister || 0, dataType: conv.dataType || 'int16', byteOrder: conv.byteOrder || 'big_endian', wordSwap: conv.wordSwap || false, scaleFactor: conv.scaleFactor, offset: conv.offset, }; const result = DataConversionUtils_1.DataConversionUtils.convertData(numericRegisters, rule); if (result.valid) { output[conv.name] = result.value; } else { output[conv.name + '_error'] = result.error; } } catch (error) { output[conv.name + '_error'] = error.message; } } return output; } function executeDiscreteMode(discreteData, quickType, includeMetadata, addTimestamp, outputFieldName, metadataFieldName) { const result = {}; const convertedData = {}; const metadata = {}; const boolArray = discreteData.map(val => Boolean(val)); if (includeMetadata) { metadata.type = quickType; metadata.dataType = 'discrete'; metadata.rawValues = discreteData; metadata.coilCount = boolArray.length; } switch (quickType) { case 'discrete_bitfield': boolArray.forEach((value, index) => { convertedData[`bit_${index}`] = value; }); break; case 'active_position': let position = "Unknown"; boolArray.forEach((value, index) => { convertedData[`mu_co_pos${index}`] = value; }); for (let i = 0; i < boolArray.length; i++) { if (boolArray[i]) { position = i.toString(); break; } } convertedData.active_position = position; convertedData.raw_values = discreteData; break; case 'discrete_summary': const activeIndices = []; let activeCount = 0; boolArray.forEach((value, index) => { if (value) { activeIndices.push(index); activeCount++; } }); convertedData.active_count = activeCount; convertedData.total_count = boolArray.length; convertedData.active_indices = activeIndices; convertedData.raw_values = discreteData; break; case 'discrete_all': const bits = {}; boolArray.forEach((value, index) => { bits[`bit_${index}`] = value; }); convertedData.bitfield = bits; let allPosition = "Unknown"; for (let i = 0; i < boolArray.length; i++) { if (boolArray[i]) { allPosition = i.toString(); break; } } convertedData.active_position = allPosition; const allActiveIndices = boolArray.map((val, idx) => val ? idx : -1).filter(idx => idx !== -1); convertedData.summary = { active_count: allActiveIndices.length, total_count: boolArray.length, active_indices: allActiveIndices, }; break; default: if (quickType === 'float' || quickType === 'single' || quickType === 'long' || quickType === 'double' || quickType === 'bcd' || quickType === 'bitfield' || quickType === 'all') { boolArray.forEach((value, index) => { convertedData[`bit_${index}`] = value; }); } break; } result[outputFieldName] = convertedData; if (includeMetadata) { result[metadataFieldName] = metadata; } if (addTimestamp) { result.timestamp = new Date().toISOString(); } return result; } function extractRegisters(inputData) { if (inputData.data && Array.isArray(inputData.data)) { return inputData.data; } if (Array.isArray(inputData)) { return inputData; } if (inputData.registers && Array.isArray(inputData.registers)) { return inputData.registers; } if (inputData.values && Array.isArray(inputData.values)) { return inputData.values; } return null; } function convertToDouble(registers, bigEndian, wordSwap = false) { if (registers.length < 4) return null; try { const buffer = Buffer.allocUnsafe(8); let regs = [...registers]; if (wordSwap) { regs = [registers[1], registers[0], registers[3], registers[2]]; } if (bigEndian) { buffer.writeUInt16BE(regs[0], 0); buffer.writeUInt16BE(regs[1], 2); buffer.writeUInt16BE(regs[2], 4); buffer.writeUInt16BE(regs[3], 6); } else { buffer.writeUInt16LE(regs[3], 0); buffer.writeUInt16LE(regs[2], 2); buffer.writeUInt16LE(regs[1], 4); buffer.writeUInt16LE(regs[0], 6); } return buffer.readDoubleLE(0); } catch { return null; } } function convertBCD(value) { let result = 0; let multiplier = 1; while (value > 0) { const digit = value & 0x0F; if (digit > 9) return NaN; result += digit * multiplier; multiplier *= 10; value >>= 4; } return result; } function extractBits(value) { const bits = {}; for (let i = 0; i < 16; i++) { bits[`bit_${i}`] = (value & (1 << i)) !== 0; } return bits; } function organizeAllConversionsOutput(registers, byteOrder, wordSwap, enableScaling, scaleFactor, includeMetadata, addTimestamp, outputFieldName, metadataFieldName) { const result = {}; const allConversions = {}; if (registers.length >= 1) { const reg = registers[0]; allConversions.single_int16 = enableScaling ? reg * scaleFactor : reg; allConversions.single_uint16 = enableScaling ? (reg & 0xFFFF) * scaleFactor : (reg & 0xFFFF); allConversions.single_bcd = enableScaling ? convertBCD(reg) * scaleFactor : convertBCD(reg); allConversions.single_bits = extractBits(reg); } if (registers.length >= 2) { const floatResult = DataConversionUtils_1.DataConversionUtils.convertData(registers, { name: 'float32', startRegister: 0, dataType: 'float32', byteOrder: byteOrder === 'BE' ? 'big_endian' : 'little_endian', wordSwap: wordSwap, }); const int32Result = DataConversionUtils_1.DataConversionUtils.convertData(registers, { name: 'int32', startRegister: 0, dataType: 'int32', byteOrder: byteOrder === 'BE' ? 'big_endian' : 'little_endian', wordSwap: wordSwap, }); const uint32Result = DataConversionUtils_1.DataConversionUtils.convertData(registers, { name: 'uint32', startRegister: 0, dataType: 'uint32', byteOrder: byteOrder === 'BE' ? 'big_endian' : 'little_endian', wordSwap: wordSwap, }); if (floatResult.valid) { allConversions.float32_value = enableScaling ? floatResult.value * scaleFactor : floatResult.value; } if (int32Result.valid) { allConversions.int32_value = enableScaling ? int32Result.value * scaleFactor : int32Result.value; } if (uint32Result.valid) { allConversions.uint32_value = enableScaling ? uint32Result.value * scaleFactor : uint32Result.value; } } if (registers.length >= 4) { const doubleValue = convertToDouble(registers, byteOrder === 'BE'); if (typeof doubleValue === 'number') { allConversions.double_value = enableScaling ? doubleValue * scaleFactor : doubleValue; } } result[outputFieldName] = allConversions; if (includeMetadata) { const metadata = { type: 'all', byteOrder: byteOrder, wordSwap: wordSwap, rawRegisters: registers, registerCount: registers.length, }; if (enableScaling) { metadata.scaleFactor = scaleFactor; metadata.scalingApplied = true; } result[metadataFieldName] = metadata; } if (addTimestamp) { result.timestamp = new Date().toISOString(); } return result; } //# sourceMappingURL=ModbusDataConverter.node.js.map