Skip to main content

Explore to Monitor Pattern

Learn how to use the Explore node to discover an object's structure and feed it to a Monitor node for continuous real-time updates as a JSON structure.

Overview

This tutorial demonstrates a powerful pattern: use the Explore node to automatically discover all variables under an object, then pass that structure to a Monitor node to get continuous updates whenever any value changes.

Use Case

You want to monitor an entire object (like a Motor, Sensor, or Controller) without manually listing every variable. The Explore node discovers the structure once, and the Monitor node keeps it updated continuously.

Prerequisites

  • Node-RED with @opcua/for-node-red installed
  • An active OPC UA connection to a server
  • An object NodeId to explore (e.g., a Motor or Sensor object)

Step-by-Step Tutorial

Step 1: Create the Basic Flow

Create a flow with these nodes:

[Inject] → [Explore] → [Monitor] → [Debug]

Step 2: Configure the Inject Node

Set up an inject node to trigger the exploration:

  1. Payload: timestamp
  2. Topic: (leave empty)
  3. Repeat: none (one-time trigger)
  4. Once after: 0.1 seconds (optional, for auto-start)

Step 3: Configure the Explore Node

Configure the Explore node to discover the object structure:

  1. Connection: Select your OPC UA connection
  2. Input NodeId: Enter the object's NodeId
    • Example: ns=3;s=MyMotor
    • Or use: ns=3;i=1234
  3. Output Type: Select "Value"
    • This returns the full subtree with values
  4. Browse Filter: (leave default)
    • Or select "Variables" to skip method nodes

Why "Value" output type?

  • It provides a complete JSON structure with both NodeIds and current values
  • The Monitor node can accept this structure directly
  • Perfect for initial discovery before continuous monitoring

Insert a function node between Explore and Monitor to verify and prepare the structure:

// Log the explored structure
node.warn("Explored structure:");
node.warn(JSON.stringify(msg.payload, null, 2));

// Pass through to Monitor
return msg;

This helps you see what was discovered before monitoring begins.

Step 5: Configure the Monitor Node

Set up the Monitor node to receive the explored structure:

  1. Connection: Same connection as Explore node
  2. Input Type: Select "JSON Structure"
  3. Queue Size: 10 (default)
  4. Sampling Interval: 1000 ms (adjust as needed)
  5. Discard Oldest: checked
  6. Monitor Type: DataValue (or Value if you prefer simpler output)

Important: The Monitor node automatically accepts the Explore output format when using JSON Structure input type.

Step 6: Configure the Debug Node

Set up the debug output:

  1. Output: complete msg object (or just msg.payload)
  2. To: debug window
  3. Name: "Monitor Updates"

Complete Flow Example

[
{
"id": "inject1",
"type": "inject",
"name": "Trigger Exploration",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 140,
"y": 100,
"wires": [["explore1"]]
},
{
"id": "explore1",
"type": "opcua-explore",
"name": "Explore Motor",
"connection": "connection1",
"nodeId": "ns=3;s=MyMotor",
"outputType": "Value",
"x": 320,
"y": 100,
"wires": [["function1"]]
},
{
"id": "function1",
"type": "function",
"name": "Log Structure",
"func": "node.warn('Explored structure:');\nnode.warn(JSON.stringify(msg.payload, null, 2));\nreturn msg;",
"outputs": 1,
"x": 490,
"y": 100,
"wires": [["monitor1"]]
},
{
"id": "monitor1",
"type": "opcua-monitor",
"name": "Monitor Motor",
"connection": "connection1",
"inputType": "jsonStructure",
"monitorType": "DataValue",
"samplingInterval": 1000,
"queueSize": 10,
"discardOldest": true,
"x": 670,
"y": 100,
"wires": [["debug1"]]
},
{
"id": "debug1",
"type": "debug",
"name": "Monitor Updates",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"x": 860,
"y": 100,
"wires": []
}
]

Understanding the Data Flow

1. Explore Output

When you trigger the flow, the Explore node outputs a structure like:

{
"Speed": {
"nodeId": "ns=3;s=MyMotor.Speed",
"value": 1500
},
"Temperature": {
"nodeId": "ns=3;s=MyMotor.Temperature",
"value": 65.5
},
"Status": {
"nodeId": "ns=3;s=MyMotor.Status",
"value": "Running"
}
}

2. Monitor Input

The Monitor node receives this structure and automatically:

  • Extracts all NodeIds
  • Creates a subscription for each variable
  • Maintains the JSON structure format

3. Monitor Output

Whenever any value changes, you receive an updated structure:

{
"Speed": {
"value": 1520,
"statusCode": {
"value": 0,
"description": "Good"
},
"sourceTimestamp": "2025-11-24T10:30:45.123Z",
"serverTimestamp": "2025-11-24T10:30:45.125Z"
},
"Temperature": {
"value": 65.5,
"statusCode": {
"value": 0,
"description": "Good"
},
"sourceTimestamp": "2025-11-24T10:30:45.123Z",
"serverTimestamp": "2025-11-24T10:30:45.125Z"
},
"Status": {
"value": "Running",
"statusCode": {
"value": 0,
"description": "Good"
},
"sourceTimestamp": "2025-11-24T10:30:45.123Z",
"serverTimestamp": "2025-11-24T10:30:45.125Z"
}
}

Note: Only variables that changed are included in each update (unless all values change simultaneously).

Advanced Pattern: Periodic Re-exploration

For dynamic servers where the object structure might change, add periodic re-exploration:

[Inject Every Hour] → [Explore] → [Monitor]

Configure the Inject node:

  • Repeat: interval
  • Every: 1 hour (or as needed)

This refreshes the monitored structure periodically, catching any new variables added to the object.

Handling Complex Objects

Nested Objects

If your object has nested sub-objects, adjust the Explore settings:

// Explore configuration for complex object
{
"nodeId": "ns=3;s=ComplexDevice",
"outputType": "Value",
"browseFilter": "Variables" // Skip method nodes
}

The Monitor output maintains the nested structure:

{
"Motor": {
"Speed": {
"value": 1500,
"statusCode": { "value": 0 }
},
"Temperature": {
"value": 65.5,
"statusCode": { "value": 0 }
}
},
"Pump": {
"Pressure": {
"value": 250,
"statusCode": { "value": 0 }
},
"FlowRate": {
"value": 100,
"statusCode": { "value": 0 }
}
}
}

Large Objects

For objects with many variables (50+):

  1. Filter the Explore output before monitoring:
// Function node to filter only important variables
const filtered = {};

// Only monitor variables matching certain criteria
for (const [key, value] of Object.entries(msg.payload)) {
// Example: Only monitor numeric values
if (typeof value.value === 'number') {
filtered[key] = value;
}
}

msg.payload = filtered;
return msg;
  1. Adjust Monitor settings:
    • Increase queue size: 20-50
    • Increase sampling interval: 2000-5000 ms
    • Use Value instead of DataValue for less data

Use Cases

1. Equipment Dashboard

Monitor an entire machine in real-time:

  • Explore: Machine object (motors, sensors, actuators)
  • Monitor: Continuous updates
  • Dashboard: Display all parameters with auto-refresh

2. Data Logging

Capture all object changes for history:

  • Explore: Process unit
  • Monitor: All variable changes
  • Database: Store changes with timestamps

3. Alarm Monitoring

Track alarm objects automatically:

  • Explore: Alarm folder
  • Monitor: All alarm states
  • Filter: Process only active alarms

4. Dynamic Configuration

Handle servers with changing structures:

  • Explore: Equipment list (periodic)
  • Monitor: All discovered equipment
  • Adapt: Automatically monitor new equipment when added

Optimizing Performance

Sampling Interval

Choose appropriate sampling rates:

  • Fast changes (100-500 ms): Motor speeds, pressures
  • Medium changes (1000-2000 ms): Temperatures, flow rates
  • Slow changes (5000+ ms): Status, configuration values

Queue Size

Adjust based on update frequency:

  • High frequency: Queue size 20-50
  • Medium frequency: Queue size 10 (default)
  • Low frequency: Queue size 5

Deadband Filtering

Add deadband filtering for analog values:

// Function node after Monitor
const filtered = {};
const deadband = 0.5; // 0.5% change required

for (const [key, data] of Object.entries(msg.payload)) {
const current = data.value;
const previous = context.get(key) || current;

const change = Math.abs((current - previous) / previous * 100);

if (change >= deadband) {
filtered[key] = data;
context.set(key, current);
}
}

msg.payload = filtered;
return msg;

Troubleshooting

No Updates Received

Problem: Monitor node doesn't output anything after initial setup.

Solutions:

  1. Verify Explore output contains valid NodeIds
  2. Check Monitor node shows "subscribed" status
  3. Ensure values are actually changing on the server
  4. Check connection is active

Too Many Updates

Problem: Receiving updates too frequently.

Solutions:

  1. Increase sampling interval (e.g., 2000 ms)
  2. Add deadband filtering
  3. Filter Explore output to fewer variables
  4. Use Value output type instead of DataValue

Structure Not Preserved

Problem: Monitor output doesn't maintain the JSON structure.

Solutions:

  1. Verify Monitor input type is set to "JSON Structure"
  2. Check Explore output type is "Value"
  3. Ensure no modification between Explore and Monitor nodes

Memory Issues

Problem: Node-RED becomes slow or unresponsive.

Solutions:

  1. Reduce number of monitored variables
  2. Decrease queue size to 5-10
  3. Increase sampling interval
  4. Filter Explore output before monitoring

Best Practices

  1. Start with Exploration: Always verify the Explore output before connecting to Monitor
  2. Use Function Nodes: Add logging to understand the data flow
  3. Filter Early: Remove unnecessary variables before monitoring
  4. Optimize Settings: Adjust sampling interval and queue size based on your needs
  5. Monitor Performance: Watch Node-RED memory and CPU usage
  6. Handle Errors: Add error handling for disconnections
  7. Document NodeIds: Keep track of which objects you're monitoring

Next Steps