Background — Why the Change Was Made
If you are migrating a project from Automation Studio 4 to AS6 and you use OPC UA subscriptions, you will immediately hit a build error that stops the entire compilation:
'UA_MonitoredItemAdd' is not defined
'UA_MonitoredItemRemove' is not defined
'UA_GetNamespaceIndex' is not defined
This is not a bug in your code — B&R intentionally removed these function blocks when AS6 introduced the updated AsOpcUac library, which is now based on the OPC 30001 PLC client specification (IEC 61131-3 v1.2). The old single-item function blocks (UA_MonitoredItemAdd, UA_MonitoredItemRemove) were listed as obsolete in that client specification, and B&R took the opportunity of the AS6 major release (which permits breaking changes) to remove them entirely and replace them with list-based equivalents.
“These blocks are listed as obsolete in the client specification. The type of application is fundamentally different from the way the new list blocks must be applied, which is why the decision was made to discontinue the blocks.”
— Migration to AR 6, AsOpcUac help
The architectural difference is fundamental: where the old API registered one node per function block call, the new API registers up to 64 nodes in a single call. This is not just a renaming — it requires rethinking the subscription setup loop.
The good news: beyond fixing the build, the new approach delivers a significant performance benefit during subscription initialization. In a real-world project with ~2,400 monitored variables split across 8 modules, we measured ~60× fewer state machine iterations needed to set up all subscriptions.
Complete API Change Table
Here is a consolidated reference of all renames and removals in the AsOpcUac library between AS4 and AS6. This covers everything you will encounter during migration:
Function Blocks
| AS4 (Old) | AS6 (New) | Notes |
|---|---|---|
UA_MonitoredItemAdd |
UA_MonitoredItemAddList |
Registers up to 64 nodes per call |
UA_MonitoredItemRemove |
UA_MonitoredItemRemoveList |
Removes a list of items; requires SubscriptionHdl |
UA_GetNamespaceIndex |
UA_NamespaceGetIndex |
Renamed for spec conformance; now case-sensitive on NamespaceUri |
Online help references:
Data Types
| AS4 (Old) | AS6 (New) |
|---|---|
UAMonitoringSettings |
UAMonitoringParameter |
UAIdentifierType_String |
UAIT_String |
UAIdentifierType_Numeric |
UAIT_Numeric |
UASecurityMsgMode_None |
UASMM_None |
UASecurityPolicy_None |
UASP_None |
UANodeAdditionalInfo.AttributeId |
UANodeAdditionalInfo.AttributeID (capital D) |
The Core Migration: UA_MonitoredItemAdd → UA_MonitoredItemAddList
What Changed in the Interface
The new UA_MonitoredItemAddList FB has several significant interface differences beyond the name:
| Parameter | AS4 approach | AS6 approach |
|---|---|---|
| Target variables | Plain STRING per item |
UAMonitoredVariables structure — .Values holds the variable name |
| Quality IDs | Output DWORD per item | Must specify variable name in UAMonitoredVariables.NodeQualityIDs (persistent array!) |
| Timestamps | Output per item | Optional — specify variable name in UAMonitoredVariables.TimeStamps |
| Monitoring settings | UAMonitoringSettings |
UAMonitoringParameter (renamed) |
| Sync mode | Implicit via QueueSize | Explicit SyncMode input — use UAMS_FwSync for firmware sync |
| Subscription handle | Not required | SubscriptionHdl is now mandatory input |
MinLostValueCount |
N/A | New IN/OUT array — required for queue overflow tracking |
| Items per call | 1 | Up to 64 (MAX_INDEX_MONITORLIST + 1 = 64) |
Important: NodeQualityIDs Requires a Persistent Variable
The most non-obvious aspect of the new API: the NodeQualityIDs field in UAMonitoredVariables does not receive the quality value directly — it holds the name of a variable where the firmware will write the quality. That variable must remain valid for the entire lifetime of the monitored item (until UA_MonitoredItemRemoveList is called).
The practical solution is to declare a persistent array and reference its elements by name:
(* Variable declaration *)
NodeQualityIDs_ALL : ARRAY[0..MAX_NODEIDS_INDEXES] OF DWORD;
sQualIdxStr : STRING[10];
(* Build element name string at runtime — in the chunking loop *)
Variables_Chunk[iItemsInChunk].Values := NodeUrl[iSourceIdx];
brsitoa(INT_TO_DINT(iSourceIdx), ADR(sQualIdxStr));
Variables_Chunk[iItemsInChunk].NodeQualityIDs := 'NodeQualityIDs_ALL[';
brsstrcat(ADR(Variables_Chunk[iItemsInChunk].NodeQualityIDs), ADR(sQualIdxStr));
brsstrcat(ADR(Variables_Chunk[iItemsInChunk].NodeQualityIDs), ADR(']'));
Note on string assignment: Use
:=for the initial string prefix ('NodeQualityIDs_ALL['). The AS compiler does not accept multi-character string literals as the argument toADR(), sobrsstrcpy(ADR(...), ADR('prefix'))will fail with “Expression expected”. Single-character literals (like']') are accepted.
The B&R sample project (OpcUa_Sample_Array) uses getMembName() for the same purpose. Using brsitoa + direct := + brsstrcat from AsBrStr achieves the same result without needing getMembName.
Before / After Code Examples
Variable Declarations (.var file)
AS4:
UA_MonitoredItemAdd_0 : UA_MonitoredItemAdd;
UA_MonitoredItemAdd_1 : UA_MonitoredItemAdd;
MonitoringSettings_0 : UAMonitoringSettings;
AS6:
UA_MonitoredItemAddList_0 : UA_MonitoredItemAddList;
UA_MonitoredItemAddList_1 : UA_MonitoredItemAddList;
MonitoringSettings_0 : UAMonitoringParameter;
(* Chunk arrays — max 64 items per call *)
ChunkSize : UINT;
ChunkCount : UINT;
iChunkStartIdx : INT;
iItemsInChunk : INT;
iChunk : INT;
iSourceIdx : INT;
NodeHdls_Chunk : ARRAY[0..63] OF DWORD;
Variables_Chunk : ARRAY[0..63] OF UAMonitoredVariables; (* NOT STRING anymore! *)
NodeAddInfo_Chunk : ARRAY[0..63] OF UANodeAdditionalInfo;
MonitoringSettings_Chunk : ARRAY[0..63] OF UAMonitoringParameter;
MonitoredItemHdls_Chunk : ARRAY[0..63] OF DWORD;
NodeErrorIDs_Chunk : ARRAY[0..63] OF DWORD;
ValuesChanged_Chunk : ARRAY[0..63] OF BOOL;
MinLostValueCount_Chunk : ARRAY[0..63] OF UINT;
NodeQualityIDs_ALL : ARRAY[0..MAX_NODEIDS_INDEXES] OF DWORD; (* persistent! *)
sQualIdxStr : STRING[10];
Action File — MonitoredItemAdd.st
AS4:
UA_MonitoredItemAdd_0(
Execute := ExecuteMonitoredItemAdd_0,
NodeHandle := NodeHdl_0[index],
Variable := NodeUrl[index],
MonitoringSettings:= MonitoringSettings_0,
Timeout := T#10s);
AS6:
UA_MonitoredItemAddList_0(
Execute := ExecuteMonitoredItemAddList_0,
SubscriptionHdl := SubscriptionHdl_0,
NodeHdlCount := ChunkCount,
NodeHdls := NodeHdls_Chunk,
SyncMode := UAMS_FwSync,
NodeAddInfos := NodeAddInfo_Chunk,
Timeout := T#10s,
Variables := Variables_Chunk,
MonitoringParameter := MonitoringSettings_Chunk,
ValuesChanged := ValuesChanged_Chunk,
MinLostValueCount := MinLostValueCount_Chunk);
(* IMPORTANT: Explicitly copy outputs back — they are not written to chunk arrays automatically *)
IF UA_MonitoredItemAddList_0.Done OR UA_MonitoredItemAddList_0.Error THEN
MonitoredItemHdls_Chunk := UA_MonitoredItemAddList_0.MonitoredItemHdls;
NodeErrorIDs_Chunk := UA_MonitoredItemAddList_0.NodeErrorIDs;
END_IF;
State Machine — Chunking Pattern (ADD state)
This is the core of the migration for large variable lists. Instead of iterating one item at a time, you fill a 64-item buffer and call the batch FB once per chunk.
AS4 (single-item loop — 500 iterations for 500 variables):
IF CaseClientReadList = UA_MONITORED_ITEM_ADD THEN
ExecuteMonitoredItemAdd_0 := TRUE;
IF UA_MonitoredItemAdd_0.Done THEN
MonitoredItemHdl_0[index] := UA_MonitoredItemAdd_0.MonitoredItemHdl;
(* Find next node and continue *)
index := index + 1;
CaseClientReadList := UA_NODE_GET_HANDLE;
END_IF;
END_IF;
AS6 (chunking loop — ~8 iterations for 500 variables):
IF CaseClientReadList = UA_MONITORED_ITEM_ADD THEN
(* Step 1: Fill chunk arrays — up to 64 valid items starting from current index *)
iChunkStartIdx := index;
iItemsInChunk := 0;
FOR iChunk := 0 TO (ChunkSize - 1) DO
iSourceIdx := iChunkStartIdx + iChunk;
IF (iSourceIdx <= MAX_NODEIDS_INDEXES) AND
(NodeID[iSourceIdx].Identifier <> '') AND
(NodeUrl[iSourceIdx] <> '') THEN
NodeHdls_Chunk[iItemsInChunk] := NodeHdl_0[iSourceIdx];
NodeAddInfo_Chunk[iItemsInChunk] := NodeInfo[iSourceIdx];
MonitoringSettings_Chunk[iItemsInChunk].SamplingInterval := MonitoringSettings_0.SamplingInterval;
(* Build target variable name for Values and NodeQualityIDs *)
Variables_Chunk[iItemsInChunk].Values := NodeUrl[iSourceIdx];
brsitoa(INT_TO_DINT(iSourceIdx), ADR(sQualIdxStr));
Variables_Chunk[iItemsInChunk].NodeQualityIDs := 'NodeQualityIDs_ALL[';
brsstrcat(ADR(Variables_Chunk[iItemsInChunk].NodeQualityIDs), ADR(sQualIdxStr));
brsstrcat(ADR(Variables_Chunk[iItemsInChunk].NodeQualityIDs), ADR(']'));
iItemsInChunk := iItemsInChunk + 1;
END_IF;
END_FOR;
ChunkCount := iItemsInChunk;
(* Step 2: Trigger batch call using FlipFlop alternating instances *)
IF iItemsInChunk > 0 THEN
IF NOT(FlipFlop) THEN
ExecuteMonitoredItemAddList_0 := TRUE;
ELSE
ExecuteMonitoredItemAddList_1 := TRUE;
END_IF;
END_IF;
(* Step 3: Wait for Done and copy results back *)
IF UA_MonitoredItemAddList_0.Done OR UA_MonitoredItemAddList_1.Done THEN
FOR iChunk := 0 TO (iItemsInChunk - 1) DO
iSourceIdx := iChunkStartIdx + iChunk;
MonitoredItemHdl_0[iSourceIdx] := MonitoredItemHdls_Chunk[iChunk];
IF NodeErrorIDs_Chunk[iChunk] <> 0 THEN
BadVariables[BadVarsIndex] := NodeID[iSourceIdx].Identifier;
BadVariablesUrl[BadVarsIndex] := NodeUrl[iSourceIdx];
BadVariables_ErrorID[BadVarsIndex] := DWORD_TO_UDINT(NodeErrorIDs_Chunk[iChunk]);
BadVarsIndex := BadVarsIndex + 1;
END_IF;
END_FOR;
(* Step 4: Advance index to next chunk *)
FOR i := (iChunkStartIdx + ChunkSize) TO MAX_NODEIDS_INDEXES DO
IF NodeID[i].Identifier <> '' THEN
index := i;
CaseClientReadList := UA_NODE_GET_HANDLE;
EXIT;
END_IF;
END_FOR;
FlipFlop := UA_MonitoredItemAddList_0.Done;
END_IF;
END_IF;
Remove Action — MonitoredItemRemove.st
The remove FB also changed — it now requires SubscriptionHdl and MonitoredItemHdlCount. Reuse the MonitoredItemHdls_Chunk array (set [0] for single-item removal):
AS4:
UA_MonitoredItemRemove_0(
Execute := ExecuteMonitoredItemRemove_0,
MonitoredItemHdl := MonitoredItemHdl_0[index],
Timeout := T#10s);
AS6:
(* In state machine before triggering: *)
MonitoredItemHdls_Chunk[0] := MonitoredItemHdl_0[index];
(* In action file: *)
UA_MonitoredItemRemoveList_0(
Execute := ExecuteMonitoredItemRemove_0,
SubscriptionHdl := SubscriptionHdl_0,
MonitoredItemHdlCount := 1,
MonitoredItemHdls := MonitoredItemHdls_Chunk,
Timeout := T#10s);
Performance Impact
The batch API eliminates thousands of round-trips during subscription initialization. Real-world data from a project with 2,404 total monitored variables:
| Module | Variables | AS4 iterations | AS6 iterations | Speed-up |
|---|---|---|---|---|
| BoolSubscription | 801 | 801 | ~13 | 62× |
| Subscription | 500 | 500 | ~8 | 62× |
| AuxSubscription | 351 | 351 | ~6 | 58× |
| WizSubscription | 351 | 351 | ~6 | 58× |
| StringSubscription | 301 | 301 | ~5 | 60× |
| VizuSubs | 100 | 100 | ~2 | 50× |
| Total | 2,404 | 2,404 | ~40 | ~60× |
For small lists (< 64 items), no chunking loop is needed — simply fill the chunk array once and set ChunkCount to the actual item count.
Known Limitation — ValuesChanged / MinLostValueCount in Chunked Setup
The AS6 help states: “The variables connected to IN/OUT parameters must remain valid over the service life of the MonitoredItem.”
In a chunked setup, the ValuesChanged_Chunk and MinLostValueCount_Chunk arrays are reused for every chunk — meaning only the last chunk’s monitored items have valid change-detection and queue overflow tracking via these arrays.
Impact in practice: This is typically a non-issue when QueueSize = 0 (firmware sync mode, no queuing), because queue overflows cannot occur and the application logic does not rely on ValuesChanged_Chunk after setup is complete. If you need per-item ValuesChanged tracking in STANDBY state, you need separate permanent arrays matching your full variable list.
Tips for Migration
-
Do your renames globally — use Find & Replace across the project for type/enum renames (they affect many files). The full mapping is in the Migration to AR 6 help page.
-
Verify against the AS6 help and sample project — The AS6 interface for
UA_MonitoredItemAddListchanged significantly between the release candidate (used in some early documentation) and the final release. Cross-check parameter names against the official help before coding. -
The B&R sample project for AS6 OPC UA is located at
\AS6\Samples\OpcUa_Sample.zipin the AS installation folder. The relevant example isOpcUa_Sample_Array/ClientBasic/MonitoredItemList. -
Start with INIT — add
ChunkSize := 64;to your_INITstate. This makes the magic number explicit and easy to document. -
Small modules (< 64 items) don’t need the full chunking loop — just populate
Variables_Chunk[0], setChunkCount := 1, and call the batch FB. Same API, simpler usage. -
ADR() with string literals —
ADR('multi-char-string')is not valid in B&R AS. Use:=for initial assignment, thenbrsstrcatto append.ADR(']')with a single character works. -
Type conversion — AS6 removed implicit
DWORD → UDINT. UseDWORD_TO_UDINT(x). Also, C-style castDINT(x)is invalid — useINT_TO_DINT(x)orUINT_TO_DINT(x)depending on the source type.
Online Help References
| Topic | Link |
|---|---|
UA_MonitoredItemAddList |
B&R Online Help |
UA_MonitoredItemRemoveList |
B&R Online Help |
| Migration to AR 6 (full change table) | B&R Online Help |
