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