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-Client2-Explore",
"name": "Explore Motor",
"endpoint": "opcua_endpoint1",
"nodeId": "ns=3;s=MyMotor",
"outputType": "NodeId",
"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-Client2-Monitor",
"name": "Monitor Motor",
"endpoint": "opcua_endpoint1",
"subscription": "subscription1",
"startImmediately": false,
"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​