Creating an OPC UA event filter in COM

From OPC Labs Knowledge Base


QuickOPC provides many useful "shortcuts" for creating OPC UA event filters on the .NET platform. It is also possible to create OPC UA event filters on the COM platform, but due to the missing "goodies", the resulting code is longer. This article describes how to create OPC UA event filters on the COM platform, explains the traps and pitfalls, and provides an example. The example happens to be written in Free Pascal (Lazarus), but the same operations will have to be performed in other COM-based tool or language.

The parts that have to be expressed on the COM platform verbatim, as opposed to the .NET, are primarily:

  • IEasyUAClient.SubscribeEvent overloads with various combinations of arguments are missing. Only an overload with no event filter exists under COM. Anything more complicated needs to go through IEasyUAClient.SubscribeMultipleMonitoredItems, passing in an array of EasyUAMonitoredItemArguments.
  • Overloaded (or any parameterized) constructors of EasyUAMonitoredItemArguments are not available under COM, and the parameters inside this objects need to be set one by one.
  • There is no UAEventFilterBuilder in COM, and the tree for the Where clause needs to be constructed using UAContentFilterElement and various operators and operands.
  • Useful static members describing common attributes operands, residing in UABaseEventObject.Operands, are not available under COM. Each such attribute operand need to be constructed in COM from scratch, and the qualified names contained in it usually obtained using an instance of the BrowsePathParser object.
  • No implicit conversion exists from UASimpleAttributeOperand to UAAttributeField in COM; this can be work around by assigning the attribute operand to the Operand property of a newly created UAAttributeField.
Warning-icon.png

Warning: Make sure you do not fall into one of the following traps. They can cause your code to receive no server-originating event notifications, for no obvious reason:

  • You need to set the AttributeId in the EasyUAMonitoredItemArguments to the EventNotifier attribute - the default is Value, and is not suitable for OPC UA Alarms & Conditions.
  • You need to set the QueueSize in the UAMonitoringParameters to a non-zero value (this should not be needed when the OPC server is compliant, but apparently is necessary with some servers).

The above is illustrated in an example, that is a direct equivalent of the following C# code:

            public static void Main()
            {
                // Instantiate the client object and hook events
                var easyUAClient = new EasyUAClient();
                easyUAClient.EventNotification += easyUAClient_EventNotification;

                Console.WriteLine("Subscribing...");
                easyUAClient.SubscribeEvent(
                    "opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer",
                    UAObjectIds.Server,
                    1000,
                    new UAEventFilterBuilder(
                        // Either the severity is >= 500, or the event comes from a specified source node
                        UAFilterElements.Or(
                            UAFilterElements.GreaterThanOrEqual(UABaseEventObject.Operands.Severity, 500),
                            UAFilterElements.Equals(
                                UABaseEventObject.Operands.SourceNode, 
                                new UANodeId("nsu=http://opcfoundation.org/Quickstarts/AlarmCondition;ns=2;s=1:Metals/SouthMotor"))),
                        UABaseEventObject.AllFields));

                Console.WriteLine("Processing event notifications for 30 seconds...");
                System.Threading.Thread.Sleep(30 * 1000);

                Console.WriteLine("Unsubscribing...");
                easyUAClient.UnsubscribeAllMonitoredItems();

                Console.WriteLine("Waiting for 5 seconds...");
                System.Threading.Thread.Sleep(5 * 1000);
            }

            static void easyUAClient_EventNotification(object sender, EasyUAEventNotificationEventArgs e)
            {
                // Display the event
                Console.WriteLine(e);
            }

A corresponding Free Pascal code is as follows:

// This example shows how to specify criteria for event notifications.

function ToUAAttributeField(Operand: UASimpleAttributeOperand): UAAttributeField;
var
  AttributeField: UAAttributeField;
begin
  AttributeField := CoUAAttributeField.Create;
  AttributeField.Operand := Operand;
  ToUAAttributeField := AttributeField;
end;

function ObjectTypeIds_BaseEventType: UANodeId;
var
  NodeId: UANodeId;
begin
  NodeId := CoUANodeId.Create;
  NodeId.StandardName := 'BaseEventType';
  ObjectTypeIds_BaseEventType := NodeId;
end;

function UAFilterElements_SimpleAttribute(TypeId: UANodeId; SimpleRelativeBrowsePathString: string): UASimpleAttributeOperand;
var
  Operand: UASimpleAttributeOperand;
  BrowsePathParser: UABrowsePathParser;
begin
  BrowsePathParser := CoUABrowsePathParser.Create;
  Operand := CoUASimpleAttributeOperand.Create;
  Operand.TypeId.NodeId := TypeId;
  Operand.QualifiedNames := BrowsePathParser.ParseRelative(SimpleRelativeBrowsePathString).ToUAQualifiedNameCollection;
  UAFilterElements_SimpleAttribute := Operand;
end;

function UABaseEventObject_Operands_NodeId: UASimpleAttributeOperand;
var
  Operand: UASimpleAttributeOperand;
begin
  Operand := CoUASimpleAttributeOperand.Create;
  Operand.TypeId.NodeId.StandardName := 'BaseEventType';
  Operand.AttributeId := UAAttributeId_NodeId;
  UABaseEventObject_Operands_NodeId := Operand;
end;

function UABaseEventObject_Operands_EventId: UASimpleAttributeOperand;
begin
  UABaseEventObject_Operands_EventId := UAFilterElements_SimpleAttribute(ObjectTypeIds_BaseEventType, '/EventId');
end;

function UABaseEventObject_Operands_EventType: UASimpleAttributeOperand;
begin
  UABaseEventObject_Operands_EventType := UAFilterElements_SimpleAttribute(ObjectTypeIds_BaseEventType, '/EventType');
end;

function UABaseEventObject_Operands_SourceNode: UASimpleAttributeOperand;
begin
  UABaseEventObject_Operands_SourceNode := UAFilterElements_SimpleAttribute(ObjectTypeIds_BaseEventType, '/SourceNode');
end;

function UABaseEventObject_Operands_SourceName: UASimpleAttributeOperand;
begin
  UABaseEventObject_Operands_SourceName := UAFilterElements_SimpleAttribute(ObjectTypeIds_BaseEventType, '/SourceName');
end;

function UABaseEventObject_Operands_Time: UASimpleAttributeOperand;
begin
  UABaseEventObject_Operands_Time := UAFilterElements_SimpleAttribute(ObjectTypeIds_BaseEventType, '/Time');
end;

function UABaseEventObject_Operands_ReceiveTime: UASimpleAttributeOperand;
begin
  UABaseEventObject_Operands_ReceiveTime := UAFilterElements_SimpleAttribute(ObjectTypeIds_BaseEventType, '/ReceiveTime');
end;

function UABaseEventObject_Operands_LocalTime: UASimpleAttributeOperand;
begin
  UABaseEventObject_Operands_LocalTime := UAFilterElements_SimpleAttribute(ObjectTypeIds_BaseEventType, '/LocalTime');
end;

function UABaseEventObject_Operands_Message: UASimpleAttributeOperand;
begin
  UABaseEventObject_Operands_Message := UAFilterElements_SimpleAttribute(ObjectTypeIds_BaseEventType, '/Message');
end;

function UABaseEventObject_Operands_Severity: UASimpleAttributeOperand;
begin
  UABaseEventObject_Operands_Severity := UAFilterElements_SimpleAttribute(ObjectTypeIds_BaseEventType, '/Severity');
end;

function UABaseEventObject_AllFields: UAAttributeFieldCollection;
var
  Fields: UAAttributeFieldCollection;
begin
  Fields := CoUAAttributeFieldCollection.Create;

  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_NodeId));

  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_EventId));
  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_EventType));
  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_SourceNode));
  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_SourceName));
  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_Time));
  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_ReceiveTime));
  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_LocalTime));
  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_Message));
  Fields.Add(ToUAAttributeField(UABaseEventObject_Operands_Severity));

  UABaseEventObject_AllFields := Fields;
end;

type
  TClientEventHandlers3 = class
    procedure OnEventNotification(
      Sender: TObject;
      sender0: OleVariant;
      eventArgs: _EasyUAEventNotificationEventArgs);
  end;

procedure TClientEventHandlers3.OnEventNotification(
  Sender: TObject;
  sender0: OleVariant;
  eventArgs: _EasyUAEventNotificationEventArgs);
begin
  // Display the event
  WriteLn(eventArgs.ToString);
end;

class procedure WhereClause.Main;
var
  Arguments: OleVariant;
  EvsClient: TEvsEasyUAClient;
  Client: EasyUAClient;
  ClientEventHandlers: TClientEventHandlers3;
  Handle: Cardinal;
  HandleArray: OleVariant;
  MonitoredItemArguments: EasyUAMonitoredItemArguments;
  MonitoringParameters: UAMonitoringParameters;
  EventFilter: UAEventFilter;
  WhereClause: UAContentFilterElement;
  Operand1: UASimpleAttributeOperand;
  Operand2: UALiteralOperand;
  Operand3: UASimpleAttributeOperand;
  Operand4: UALiteralOperand;
  SourceNodeId: UANodeId;
  Element1, Element2: UAContentFilterElement;
  BaseEventType: UANodeId;
begin
  // Instantiate the client object and hook events
  EvsClient := TEvsEasyUAClient.Create(nil);
  Client := EvsClient.ComServer;
  ClientEventHandlers := TClientEventHandlers3.Create;
  EvsClient.OnEventNotification := @ClientEventHandlers.OnEventNotification;

  WriteLn('Subscribing...');

  WhereClause := CoUAContentFilterElement.Create;
  BaseEventType := CoUaNodeId.Create;
  BaseEventType.StandardName := 'BaseEventType';

  // Either the severity is >= 500, or the event comes from a specified source node
  Operand1 := UABaseEventObject_Operands_Severity;
  Operand2 := CoUALiteralOperand.Create;
  Operand2.Value := 500;
  Element1 := CoUAContentFilterElement.Create;
  Element1.FilterOperator := UAFilterOperator_GreaterThanOrEqual;
  Element1.FilterOperands.Add(Operand1);
  Element1.FilterOperands.Add(Operand2);
  Operand3 := UABaseEventObject_Operands_SourceNode;
  SourceNodeId := CoUANodeId.Create;
  SourceNodeId.ExpandedText := 'nsu=http://opcfoundation.org/Quickstarts/AlarmCondition;ns=2;s=1:Metals/SouthMotor';
  Operand4 := CoUALiteralOperand.Create;
  Operand4.Value := SourceNodeId;
  Element2 := CoUAContentFilterElement.Create;
  Element2.FilterOperator := UAFilterOperator_Equals;
  Element2.FilterOperands.Add(Operand3);
  Element2.FilterOperands.Add(Operand4);
  WhereClause.FilterOperator := UAFilterOperator_Or;
  WhereClause.FilterOperands.Add(Element1);
  WhereClause.FilterOperands.Add(Element2);

  EventFilter := CoUAEventFilter.Create;
  EventFilter.SelectClauses := UABaseEventObject_AllFields;
  EventFilter.WhereClause := WhereClause;

  MonitoringParameters := CoUAMonitoringParameters.Create;
  MonitoringParameters.SamplingInterval := 1000;
  MonitoringParameters.EventFilter := EventFilter;
  MonitoringParameters.QueueSize := 1000;

  MonitoredItemArguments := CoEasyUAMonitoredItemArguments.Create;
  MonitoredItemArguments.EndpointDescriptor.UrlString := 'opc.tcp://opcua.demo-this.com:62544/Quickstarts/AlarmConditionServer';
  MonitoredItemArguments.NodeDescriptor.NodeId.StandardName := 'Server';
  MonitoredItemArguments.MonitoringParameters := MonitoringParameters;
  //MonitoredItemArguments.SubscriptionParameters.PublishingInterval := 0;
  MonitoredItemArguments.AttributeId := UAAttributeId_EventNotifier;

  Arguments := VarArrayCreate([0, 0], varVariant);
  Arguments[0] := MonitoredItemArguments;

  TVarData(HandleArray).VType := varArray or varVariant;
  TVarData(HandleArray).VArray := PVarArray(
    Client.SubscribeMultipleMonitoredItems(PSafeArray(TVarData(Arguments).VArray)));

  WriteLn('Processing event notifications for 30 seconds...');
  PumpSleep(30*1000);

  WriteLn('Unsubscribing...');
  Client.UnsubscribeAllMonitoredItems;

  WriteLn('Waiting for 5 seconds...');
  PumpSleep(5*1000);

  WriteLn('Done...');
end;

Note 1: This article has been written for/with in-the-works QuickOPC version 2016.2 (5.41), but it should apply equally to the current version 5.40.

Note 2: The code probably does not yet release the COM objects as it should.

Note 3: We are not quite happy with the amount of code it takes in COM to create the OPC UA event filter. It should be possible to add extra features to QuickOPC-COM to make it easier (although it will never be as easy as in .NET). We will work on this should there be enough interest.