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
JavaScript
"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