advanced method patterns
This tutorial covers advanced patterns and use cases for OPC UA method calls, including complex parameter handling, method chaining, asynchronous execution, and integration with industrial workflows. These patterns demonstrate how to build sophisticated automation solutions using the OPC UA Call Node.
Understanding these patterns is essential for implementing complex industrial automation scenarios that require coordinated method execution, error recovery, and state management.
prerequisites
- completed Calling OPC UA Methods tutorial
- understanding of industrial automation workflows
- knowledge of JavaScript programming and Node-RED flows
- experience with OPC UA server method implementations
complex parameter handling
1. dynamic parameter generation
// Generate method parameters based on current system state
const systemMode = flow.get("operationMode") || "manual";
const currentRecipe = flow.get("activeRecipe") || {};
let methodParams = {};
switch (systemMode) {
    case "automatic":
        methodParams = {
            "Mode": "auto",
            "Recipe": currentRecipe.id,
            "Parameters": {
                "Temperature": currentRecipe.temperature || 25.0,
                "Pressure": currentRecipe.pressure || 1.0,
                "Duration": currentRecipe.duration || 3600,
                "MixingSpeed": currentRecipe.mixingSpeed || 500
            },
            "SafetyLimits": {
                "MaxTemp": currentRecipe.maxTemp || 200.0,
                "MaxPressure": currentRecipe.maxPressure || 5.0,
                "EmergencyStop": true
            }
        };
        break;
        
    case "manual":
        methodParams = {
            "Mode": "manual",
            "OperatorId": msg.operatorId || "UNKNOWN",
            "ManualSetpoints": {
                "Temperature": msg.temperature || 20.0,
                "Pressure": msg.pressure || 1.0
            },
            "ConfirmationRequired": true
        };
        break;
        
    case "maintenance":
        methodParams = {
            "Mode": "maintenance",
            "TestSequence": msg.testSequence || "basic",
            "DiagnosticLevel": "full",
            "BypassSafety": false
        };
        break;
}
msg.payload = methodParams;
msg.objectId = "ns=2;s=ProcessController";
msg.methodId = "ns=2;s=ConfigureOperation";
return msg;
2. parameter validation and transformation
// Validate and transform parameters before method call
const ParameterProcessor = {
    // Validate parameter ranges
    validateRanges: function(params, schema) {
        const errors = [];
        
        Object.keys(schema).forEach(param => {
            const value = params[param];
            const rules = schema[param];
            
            if (value === undefined && rules.required) {
                errors.push(`Missing required parameter: ${param}`);
                return;
            }
            
            if (value !== undefined) {
                if (rules.min !== undefined && value < rules.min) {
                    errors.push(`${param} below minimum: ${value} < ${rules.min}`);
                }
                if (rules.max !== undefined && value > rules.max) {
                    errors.push(`${param} above maximum: ${value} > ${rules.max}`);
                }
                if (rules.type && typeof value !== rules.type) {
                    errors.push(`${param} wrong type: expected ${rules.type}, got ${typeof value}`);
                }
            }
        });
        
        return errors;
    },
    
    // Apply unit conversions
    convertUnits: function(params, conversions) {
        const converted = { ...params };
        
        Object.keys(conversions).forEach(param => {
            if (converted[param] !== undefined) {
                const conversion = conversions[param];
                converted[param] = converted[param] * conversion.factor + (conversion.offset || 0);
            }
        });
        
        return converted;
    },
    
    // Apply default values
    applyDefaults: function(params, defaults) {
        return { ...defaults, ...params };
    }
};
// Define parameter schema
const parameterSchema = {
    Temperature: { type: "number", min: -50, max: 200, required: true },
    Pressure: { type: "number", min: 0, max: 10, required: true },
    FlowRate: { type: "number", min: 0, max: 1000, required: false },
    OperatorId: { type: "string", required: true }
};
// Define unit conversions (Celsius to Kelvin, Bar to PSI)
const unitConversions = {
    Temperature: { factor: 1, offset: 273.15 },  // C to K
    Pressure: { factor: 14.5038, offset: 0 }     // bar to PSI
};
// Define default values
const defaultValues = {
    FlowRate: 100.0,
    SafetyEnabled: true,
    Timeout: 3600
};
// Process parameters
const inputParams = msg.payload || {};
// Apply defaults
let processedParams = ParameterProcessor.applyDefaults(inputParams, defaultValues);
// Validate parameters
const validationErrors = ParameterProcessor.validateRanges(processedParams, parameterSchema);
if (validationErrors.length > 0) {
    node.error(`Parameter validation failed: ${validationErrors.join(", ")}`);
    return null;
}
// Convert units
processedParams = ParameterProcessor.convertUnits(processedParams, unitConversions);
// Set processed parameters
msg.payload = processedParams;
msg.objectId = "ns=2;s=ProcessUnit";
msg.methodId = "ns=2;s=SetParameters";
return msg;
3. structured parameter building
// Build complex structured parameters
function buildRecipeParameters(recipeData) {
    return {
        recipeHeader: {
            id: recipeData.id,
            name: recipeData.name,
            version: recipeData.version,
            createdBy: recipeData.author,
            createdDate: recipeData.createDate,
            lastModified: new Date().toISOString()
        },
        ingredients: recipeData.ingredients.map(ingredient => ({
            materialId: ingredient.id,
            quantity: {
                value: ingredient.amount,
                unit: ingredient.unit,
                tolerance: ingredient.tolerance || 0.05
            },
            additionOrder: ingredient.sequence,
            additionRate: ingredient.rate || "normal"
        })),
        processSteps: recipeData.steps.map((step, index) => ({
            stepNumber: index + 1,
            stepType: step.type,
            parameters: {
                duration: step.duration,
                temperature: step.temperature,
                pressure: step.pressure,
                mixingSpeed: step.mixingSpeed || 0
            },
            conditions: {
                startConditions: step.startWhen || [],
                endConditions: step.endWhen || [],
                alarmLimits: step.alarms || {}
            },
            description: step.description
        })),
        qualityChecks: recipeData.qualityChecks || [],
        safetyParameters: {
            emergencyStopEnabled: true,
            maxTemperature: recipeData.safetyLimits?.maxTemp || 200,
            maxPressure: recipeData.safetyLimits?.maxPressure || 5,
            timeoutMinutes: recipeData.timeout || 480
        }
    };
}
// Build recipe from input data
const recipeInput = msg.payload.recipe;
const structuredParams = buildRecipeParameters(recipeInput);
msg.payload = structuredParams;
msg.objectId = "ns=2;s=BatchReactor";
msg.methodId = "ns=2;s=LoadRecipe";
return msg;
method chaining and orchestration
1. sequential method execution
// Execute a sequence of methods with dependency management
const MethodSequencer = {
    sequences: {
        startup: [
            { 
                name: "Initialize",
                object: "ns=2;s=System",
                method: "ns=2;s=Initialize",
                params: { "mode": "production" },
                timeout: 30000
            },
            {
                name: "Check Safety",
                object: "ns=2;s=SafetySystem", 
                method: "ns=2;s=PerformSafetyCheck",
                params: { "level": "full" },
                timeout: 60000,
                dependencies: ["Initialize"]
            },
            {
                name: "Start Heater",
                object: "ns=2;s=Heater",
                method: "ns=2;s=TurnOn",
                params: { "targetTemp": 150.0 },
                timeout: 120000,
                dependencies: ["Check Safety"]
            },
            {
                name: "Start Mixer",
                object: "ns=2;s=Mixer",
                method: "ns=2;s=Start",
                params: { "speed": 500 },
                timeout: 30000,
                dependencies: ["Start Heater"]
            }
        ]
    },
    
    currentSequence: null,
    currentStep: 0,
    completedSteps: [],
    
    startSequence: function(sequenceName) {
        this.currentSequence = this.sequences[sequenceName];
        this.currentStep = 0;
        this.completedSteps = [];
        
        if (!this.currentSequence) {
            throw new Error(`Unknown sequence: ${sequenceName}`);
        }
        
        return this.getNextStep();
    },
    
    getNextStep: function() {
        if (!this.currentSequence || this.currentStep >= this.currentSequence.length) {
            return null; // Sequence complete
        }
        
        const step = this.currentSequence[this.currentStep];
        
        // Check dependencies
        if (step.dependencies) {
            const unmetDeps = step.dependencies.filter(dep => 
                !this.completedSteps.includes(dep)
            );
            
            if (unmetDeps.length > 0) {
                throw new Error(`Unmet dependencies for ${step.name}: ${unmetDeps.join(", ")}`);
            }
        }
        
        return step;
    },
    
    completeStep: function(success) {
        if (!this.currentSequence) return null;
        
        const step = this.currentSequence[this.currentStep];
        
        if (success) {
            this.completedSteps.push(step.name);
            this.currentStep++;
            return this.getNextStep();
        } else {
            throw new Error(`Step failed: ${step.name}`);
        }
    }
};
// Start or continue sequence
const sequenceName = msg.sequenceName || "startup";
const stepResult = msg.stepResult; // from previous method call
try {
    let nextStep;
    
    if (stepResult) {
        // Complete current step and get next
        const success = stepResult.statusCode === "Good" && stepResult.payload?.success !== false;
        nextStep = MethodSequencer.completeStep(success);
    } else {
        // Start new sequence
        nextStep = MethodSequencer.startSequence(sequenceName);
    }
    
    if (nextStep) {
        // Execute next step
        msg.payload = nextStep.params;
        msg.objectId = nextStep.object;
        msg.methodId = nextStep.method;
        msg.timeout = nextStep.timeout;
        msg.stepName = nextStep.name;
        
        node.log(`Executing step: ${nextStep.name}`);
        return msg;
    } else {
        // Sequence complete
        node.log(`Sequence ${sequenceName} completed successfully`);
        msg.payload = { sequenceComplete: true, sequence: sequenceName };
        return msg;
    }
} catch (error) {
    node.error(`Sequence execution failed: ${error.message}`);
    msg.payload = { 
        sequenceError: true, 
        sequence: sequenceName,
        error: error.message 
    };
    return msg;
}
2. parallel method execution
// Execute multiple methods in parallel with coordination
const ParallelExecutor = {
    activeOperations: {},
    
    startParallelOps: function(operations) {
        const opId = Date.now().toString();
        
        this.activeOperations[opId] = {
            operations: operations,
            results: {},
            completed: 0,
            total: operations.length,
            startTime: Date.now()
        };
        
        return opId;
    },
    
    recordResult: function(opId, opName, result) {
        const operation = this.activeOperations[opId];
        if (!operation) return false;
        
        operation.results[opName] = result;
        operation.completed++;
        
        return operation.completed >= operation.total;
    },
    
    getResults: function(opId) {
        const operation = this.activeOperations[opId];
        if (!operation) return null;
        
        const results = operation.results;
        delete this.activeOperations[opId];
        
        return results;
    }
};
// Define parallel operations
const parallelOps = [
    {
        name: "StartPump1",
        object: "ns=2;s=Pump1",
        method: "ns=2;s=Start",
        params: { "flowRate": 100 }
    },
    {
        name: "StartPump2", 
        object: "ns=2;s=Pump2",
        method: "ns=2;s=Start",
        params: { "flowRate": 150 }
    },
    {
        name: "OpenValve1",
        object: "ns=2;s=Valve1",
        method: "ns=2;s=Open",
        params: { "openingPercentage": 100 }
    },
    {
        name: "OpenValve2",
        object: "ns=2;s=Valve2", 
        method: "ns=2;s=Open",
        params: { "openingPercentage": 75 }
    }
];
const operationId = msg.operationId;
const operationResult = msg.operationResult;
if (operationResult) {
    // Process result from parallel operation
    const allComplete = ParallelExecutor.recordResult(
        operationId,
        operationResult.operationName,
        operationResult
    );
    
    if (allComplete) {
        // All operations complete
        const results = ParallelExecutor.getResults(operationId);
        
        msg.payload = {
            parallelComplete: true,
            results: results,
            summary: {
                total: parallelOps.length,
                successful: Object.values(results).filter(r => r.statusCode === "Good").length,
                failed: Object.values(results).filter(r => r.statusCode !== "Good").length
            }
        };
        
        return msg;
    } else {
        // Still waiting for other operations
        return null;
    }
} else {
    // Start parallel operations
    const opId = ParallelExecutor.startParallelOps(parallelOps);
    
    // Create output messages for each operation
    const outputMsgs = parallelOps.map(op => ({
        payload: op.params,
        objectId: op.object,
        methodId: op.method,
        operationId: opId,
        operationName: op.name
    }));
    
    // Return array of messages for parallel execution
    return outputMsgs;
}
state-based method execution
1. state machine method integration
// Integrate method calls with state machine
const ProcessStateMachine = {
    states: {
        IDLE: "idle",
        PREPARING: "preparing",
        HEATING: "heating", 
        PROCESSING: "processing",
        COOLING: "cooling",
        COMPLETED: "completed",
        ERROR: "error"
    },
    
    currentState: "idle",
    stateData: {},
    
    transition: function(newState, context) {
        const oldState = this.currentState;
        this.currentState = newState;
        this.stateData = { ...this.stateData, ...context };
        
        node.log(`State transition: ${oldState} -> ${newState}`);
        
        return this.getStateActions(newState, oldState);
    },
    
    getStateActions: function(state, previousState) {
        const actions = [];
        
        switch (state) {
            case this.states.PREPARING:
                actions.push({
                    object: "ns=2;s=System",
                    method: "ns=2;s=Prepare",
                    params: {
                        "recipe": this.stateData.recipeId,
                        "batchSize": this.stateData.batchSize
                    }
                });
                break;
                
            case this.states.HEATING:
                actions.push({
                    object: "ns=2;s=Heater",
                    method: "ns=2;s=SetTarget",
                    params: {
                        "temperature": this.stateData.targetTemp,
                        "rampRate": this.stateData.heatRate
                    }
                });
                break;
                
            case this.states.PROCESSING:
                actions.push(
                    {
                        object: "ns=2;s=Mixer",
                        method: "ns=2;s=Start",
                        params: { "speed": this.stateData.mixSpeed }
                    },
                    {
                        object: "ns=2;s=AdditionSystem",
                        method: "ns=2;s=AddMaterial",
                        params: {
                            "materialId": this.stateData.materialId,
                            "quantity": this.stateData.additionQuantity
                        }
                    }
                );
                break;
                
            case this.states.COOLING:
                actions.push({
                    object: "ns=2;s=CoolingSystem",
                    method: "ns=2;s=StartCooling",
                    params: {
                        "targetTemp": this.stateData.coolTemp,
                        "coolRate": this.stateData.coolRate
                    }
                });
                break;
                
            case this.states.ERROR:
                actions.push({
                    object: "ns=2;s=System",
                    method: "ns=2;s=EmergencyStop",
                    params: {
                        "reason": this.stateData.errorReason
                    }
                });
                break;
        }
        
        return actions;
    },
    
    handleEvent: function(event, data) {
        let newState = this.currentState;
        
        switch (this.currentState) {
            case this.states.IDLE:
                if (event === "START_PROCESS") {
                    newState = this.states.PREPARING;
                }
                break;
                
            case this.states.PREPARING:
                if (event === "PREPARATION_COMPLETE") {
                    newState = this.states.HEATING;
                } else if (event === "ERROR") {
                    newState = this.states.ERROR;
                }
                break;
                
            case this.states.HEATING:
                if (event === "TARGET_TEMPERATURE_REACHED") {
                    newState = this.states.PROCESSING;
                } else if (event === "ERROR") {
                    newState = this.states.ERROR;
                }
                break;
                
            case this.states.PROCESSING:
                if (event === "PROCESSING_COMPLETE") {
                    newState = this.states.COOLING;
                } else if (event === "ERROR") {
                    newState = this.states.ERROR;
                }
                break;
                
            case this.states.COOLING:
                if (event === "COOLING_COMPLETE") {
                    newState = this.states.COMPLETED;
                } else if (event === "ERROR") {
                    newState = this.states.ERROR;
                }
                break;
        }
        
        if (newState !== this.currentState) {
            return this.transition(newState, data);
        }
        
        return [];
    }
};
// Process event and execute state actions
const event = msg.event;
const eventData = msg.eventData || {};
const actions = ProcessStateMachine.handleEvent(event, eventData);
if (actions.length > 0) {
    // Execute first action (or could return array for parallel execution)
    const action = actions[0];
    
    msg.payload = action.params;
    msg.objectId = action.object;
    msg.methodId = action.method;
    msg.currentState = ProcessStateMachine.currentState;
    msg.remainingActions = actions.slice(1);
    
    return msg;
} else {
    // No actions required for this event
    msg.payload = { 
        noAction: true, 
        currentState: ProcessStateMachine.currentState 
    };
    return msg;
}
error recovery and fault tolerance
1. automatic retry with backoff
// Implement retry logic with exponential backoff
const RetryManager = {
    attempts: {},
    
    shouldRetry: function(methodKey, statusCode) {
        const retryableErrors = [
            "BadTooManyOps",
            "BadTimeout", 
            "BadServerNotConnected",
            "BadCommunicationError"
        ];
        
        if (!retryableErrors.includes(statusCode)) {
            return false;
        }
        
        const attempt = this.attempts[methodKey] || { count: 0, lastAttempt: 0 };
        return attempt.count < 3; // Max 3 retries
    },
    
    getRetryDelay: function(methodKey) {
        const attempt = this.attempts[methodKey] || { count: 0 };
        // Exponential backoff: 1s, 2s, 4s
        return Math.pow(2, attempt.count) * 1000;
    },
    
    recordAttempt: function(methodKey) {
        if (!this.attempts[methodKey]) {
            this.attempts[methodKey] = { count: 0, lastAttempt: 0 };
        }
        
        this.attempts[methodKey].count++;
        this.attempts[methodKey].lastAttempt = Date.now();
    },
    
    resetAttempts: function(methodKey) {
        delete this.attempts[methodKey];
    }
};
// Handle method result with retry logic
const statusCode = msg.statusCode;
const methodKey = `${msg.objectId}:${msg.methodId}`;
if (statusCode === "Good") {
    // Success - reset retry counter
    RetryManager.resetAttempts(methodKey);
    
    msg.payload.retryComplete = true;
    return msg;
} else {
    // Failed - check if we should retry
    if (RetryManager.shouldRetry(methodKey, statusCode)) {
        const delay = RetryManager.getRetryDelay(methodKey);
        RetryManager.recordAttempt(methodKey);
        
        node.warn(`Method call failed (${statusCode}), retrying in ${delay}ms (attempt ${RetryManager.attempts[methodKey].count})`);
        
        // Schedule retry
        setTimeout(() => {
            node.send({
                ...msg,
                payload: msg.originalPayload || msg.payload,
                isRetry: true,
                retryAttempt: RetryManager.attempts[methodKey].count
            });
        }, delay);
        
        return null; // Don't send message now
    } else {
        // Max retries exceeded or non-retryable error
        RetryManager.resetAttempts(methodKey);
        
        msg.payload = {
            finalError: true,
            statusCode: statusCode,
            message: `Method call failed permanently: ${statusCode}`
        };
        
        node.error(`Method call failed permanently: ${statusCode}`);
        return msg;
    }
}
2. graceful degradation
// Implement graceful degradation when methods fail
const FallbackManager = {
    fallbackStrategies: {
        "StartMainPump": [
            {
                description: "Use backup pump",
                object: "ns=2;s=BackupPump",
                method: "ns=2;s=Start",
                params: { "flowRate": "reducedFlow" }
            },
            {
                description: "Manual operation mode",
                object: "ns=2;s=System",
                method: "ns=2;s=SetManualMode",
                params: { "reason": "Main pump unavailable" }
            }
        ],
        "SetTemperature": [
            {
                description: "Use secondary heater",
                object: "ns=2;s=SecondaryHeater",
                method: "ns=2;s=SetTarget",
                params: { "temperature": "reducedTemp" }
            },
            {
                description: "Disable temperature control",
                object: "ns=2;s=TemperatureController",
                method: "ns=2;s=Disable",
                params: { "reason": "Heater failure" }
            }
        ]
    },
    
    getFallback: function(failedMethod, attemptNumber) {
        const strategies = this.fallbackStrategies[failedMethod];
        if (!strategies || attemptNumber >= strategies.length) {
            return null;
        }
        
        return strategies[attemptNumber];
    },
    
    adaptParameters: function(originalParams, fallbackParams) {
        const adapted = { ...fallbackParams };
        
        // Adapt parameters based on placeholders
        Object.keys(adapted).forEach(key => {
            const value = adapted[key];
            
            if (value === "reducedFlow" && originalParams.flowRate) {
                adapted[key] = originalParams.flowRate * 0.7; // 70% flow
            } else if (value === "reducedTemp" && originalParams.temperature) {
                adapted[key] = originalParams.temperature * 0.9; // 90% temp
            }
        });
        
        return adapted;
    }
};
// Check for fallback when method fails
const originalMethod = msg.originalMethod || msg.methodId;
const failureCount = msg.fallbackAttempt || 0;
if (msg.statusCode !== "Good") {
    const fallback = FallbackManager.getFallback(originalMethod, failureCount);
    
    if (fallback) {
        const adaptedParams = FallbackManager.adaptParameters(
            msg.originalPayload || msg.payload,
            fallback.params
        );
        
        node.warn(`Executing fallback for ${originalMethod}: ${fallback.description}`);
        
        msg.payload = adaptedParams;
        msg.objectId = fallback.object;
        msg.methodId = fallback.method;
        msg.originalMethod = originalMethod;
        msg.fallbackAttempt = failureCount + 1;
        msg.fallbackDescription = fallback.description;
        
        return msg;
    } else {
        // No more fallbacks available
        node.error(`All fallback strategies exhausted for ${originalMethod}`);
        
        msg.payload = {
            criticalFailure: true,
            failedMethod: originalMethod,
            finalStatus: msg.statusCode
        };
        
        return msg;
    }
} else {
    // Success
    if (msg.fallbackAttempt > 0) {
        node.log(`Fallback successful: ${msg.fallbackDescription}`);
    }
    
    return msg;
}
performance optimization
1. method call caching
// Cache method results to avoid unnecessary calls
const MethodCache = {
    cache: {},
    ttl: 300000, // 5 minutes
    
    getCacheKey: function(objectId, methodId, params) {
        return `${objectId}:${methodId}:${JSON.stringify(params)}`;
    },
    
    get: function(objectId, methodId, params) {
        const key = this.getCacheKey(objectId, methodId, params);
        const cached = this.cache[key];
        
        if (!cached) return null;
        
        const now = Date.now();
        if (now - cached.timestamp > this.ttl) {
            delete this.cache[key];
            return null;
        }
        
        return cached.result;
    },
    
    set: function(objectId, methodId, params, result) {
        const key = this.getCacheKey(objectId, methodId, params);
        this.cache[key] = {
            result: result,
            timestamp: Date.now()
        };
    },
    
    clear: function() {
        this.cache = {};
    }
};
// Check cache before making method call
const objectId = msg.objectId;
const methodId = msg.methodId;
const params = msg.payload;
// Check if method is cacheable (read-only methods)
const cacheableMethods = [
    "GetStatus",
    "GetConfiguration", 
    "GetDiagnostics",
    "ReadCalibration"
];
const methodName = methodId.split(";").pop(); // Get method name from NodeId
const isCacheable = cacheableMethods.some(m => methodName.includes(m));
if (isCacheable) {
    const cachedResult = MethodCache.get(objectId, methodId, params);
    
    if (cachedResult) {
        node.log(`Using cached result for ${methodName}`);
        
        msg.payload = cachedResult.payload;
        msg.statusCode = cachedResult.statusCode;
        msg.fromCache = true;
        
        return msg;
    }
}
// Cache result after method execution (in a separate function node after Call node)
if (msg.statusCode === "Good" && isCacheable) {
    MethodCache.set(objectId, methodId, params, {
        payload: msg.payload,
        statusCode: msg.statusCode
    });
    
    node.log(`Cached result for ${methodName}`);
}
return msg;
Best practices
- Method Discovery: Use browsing to discover available methods before calling
- Parameter Validation: Always validate parameters before method execution
- Error Handling: Implement comprehensive error handling and recovery
- State Management: Use proper state tracking for complex workflows
- Performance: Cache read-only method results when appropriate
- Monitoring: Track method execution metrics and performance
- Documentation: Document method signatures and expected behavior