Table of Contents

Direct Calls from Custom DLL's

Many framework solutions processing machine data via OPC UA usually need custom DLL's to handle complex business logic. This mainly dus to the fact that integrating machine information with data from other sources like ERP systems and databases isn't a one-size-fits-all solution. At the same time a custom DLL, albeit sometimes triggered by an OPC UA monitoring event, needs to read additional data from the OPC UA server because that data wasn't provided with the monitoring event.

Initially access to OPC UA servers was limited via the OPC UA interface provided by the framework. That access has been extended for custom DLL's with direct calls available through the framework client. The main benefit of those direct calls is that, unlike the OPC UA interface, those calls don't use BCM slots and bypass the BCM process completely.

Differences and Similarities Compared to OPC UA Interface

The biggest difference between the direct calls and the interface is, as already stated above, that the direct calls don't need BCM slots for execution.

A second difference between them is that no meta-data is handled by the direct calls. For example, when reading values from an object via the interface, additional data like the object browse name, profile name, description, etc. will be returned as well. The direct calls ignore those fields completely.

In regards to the reading from and writing to the variables, the behaviour of the direct calls is similar to the OPU interface. When a variable is not found on the object, it will not be read and therefor won't show up in the result set, or it will not be written to because there is nothing to write to.

Similarly, the type conversions of the functions are exactly the same as those of the interface. Please consult the article Supported Data Types for more information about those conversions.

Reading from OPC UA Servers

With the help of the ReadFromDeviceAsync function, variables of a specific object on the OPC UA server can be read.

Let's start with an example where the code will read both variables attached to an object called TemperatureSensor01.

Selected Object

Example Object For Reading - Treeview

Code

// Step 1 - Initialise the framework client.
using (var frameworkClient = await BCClient.EnsureFrameworkClientAsync())
{
    // Step 2 - Define the parameters.
    var profileName = "Custom OPC UA profile";
    var objectBrowsePath = "/Objects/DeviceSet/I4DeviceSimulation/TemperatureSensor01";
    var variableDefinitions = new List<VariableDefinition>
    {
        new VariableDefinition { Name = "AssetId", TypeName = VariableTypeNames.Text },
        new VariableDefinition { Name = "MeasuredValue", TypeName = VariableTypeNames.Number }
    };
    
    // Enclose everything in a try-catch structure.
    try
    {
        // Step 3 - Make the read call.
        var variableResults = await frameworkClient.OpcUaSystem.ReadFromDeviceAsync(profileName, objectBrowsePath, variableDefinitions);
        
        // Step 4 - Read out the returned values.
        var assetIdVariable = variableResults.FirstOrDefault(var => var.Name.Equals("AssetId"));
        if (assetIdVariable != null)
        {
            var assetIdFieldName = assetIdVariable.Name;
            var assetId = (string) assetIdVariable.Value;
            var assetIdSourceTimeStamp = assetIdVariable.SourceTimestamp;
            var assetIdServerTimeStamp = assetIdVariable.ServerTimestamp;
            
            // Do something with the asset ID ...
        }
        
        var measuredValueVariable = variableResults.FirstOrDefault(var => var.Name.Equals("MeasuredValue"));
        if (measuredValueVariable != null)
        {
            var measuredValue = (double) measuredValueVariable.Value;
            
            // Do something with measured value ...
        }
    }
    catch (WebRequestException wre)
    {
        // Something went wrong with the ReadFromDeviceAsync call.
        // The 'Response' field contains the correct error message.
        Console.WriteLine(wre.Response);
    }
}

Explanation

In step 1 of the code, the framework client must be initialised.

The second step shows the definition of several parameters the ReadFromDeviceAsync call expects. The profile name refers to a profile defined in the framework. The object browse path defines the path where the ReadFromDeviceAsync function can find the object containing the OPC UA variables. (Compare the path name in the code with the treeview in the image above. Note: the 'root' name in the image is represented by the first '/' in the path.)

Stating the OPC UA variables to read, is done by defining a (C#) variable of type List<VariableDefinition>. Each OPC UA variable must must defined in the VariableDefinition instance. The VariableDefinition type contains the following fields:

Field Description
Name The name of the variable.
TypeName A value of type VariableTypeNames.

The VariableTypeNames contains several static fields available to choose from when defining the OPC UA variable reference. The type name is required so that the ReadFromDeviceAsync call knows to what type the value read must be converted to.

At last make the ReadFromDeviceAsync function call in step 3. The returned result set is of type List<VariableData>. The VariableData type has 4 fields:

Field Description
Name The name of the returned variable. Type: string
Value The (converted) value as read from the OPC UA server. Type: object so additional casting will be required.
SourceTimestamp The timestamp of the last real value change on the OPC UA server itself. Type: DateTime.
ServerTimestamp The timestamp of the last time the OPC UA server probed the variable. Type: DateTime.

In step 4, before using the values it is necessary to check whether the OPC UA variable is effectively present in the result set. If the ReadFromDeviceAsync call can't find the variable, it will NOT throw an exception but just leave it out from the result set.

Writing to OPC UA Servers

The function WriteToDeviceAsync allows for updating values of variables on a specific object on the OPC UA server.

Here is another example to write a value to the OPC UA variable called AssetId to the same object TemperatureSensor01.

Code

// Step 1 - Initialise the framework client.
using (var frameworkClient = await BCClient.EnsureFrameworkClientAsync())
{
    // Step 2 - Define the parameters.
    var profileName = "Custom OPC UA profile";
    var objectBrowsePath = "/Objects/DeviceSet/I4DeviceSimulation/TemperatureSensor1";
    var variableData = new List<VariableData>
    {
        new VariableData { Name = "AssetId", Value = "Id to write" }
    };
    
    // Enclose everything in a try-catch structure.
    try
    {
        // Step 3 - Make the write call.
        await frameworkClient.OpcUaSystem.WriteToDeviceAsync(profileName, objectBrowsePath, variableData);
        
        // When no exception is thrown, the write call itself succeeded.
        // Data not written to variables due to whatever reason is silently ignored.
    }
    catch (WebRequestException wre)
    {
        // Something went wrong with the WriteToDeviceAsync call.
        // The 'Response' field contains the correct error message.
        Console.WriteLine(wre.Response);
    }
}

Explanation

Again, initialise the framework client as the first step.

In the second step, the same profile name and object browse path must be defined. But instead of defining VariableDefinitions, a List of VariableData must be provided. Only 2 fields must be filled: Name and Value. The name is required so that WriteToDeviceAsync knows which OPC UA variable to update, and also a value must be given otherwise a write of null will be attempted. The timestamps can be left out as the write call ignores those fields.

Also there is no need provide any type definition. The write function will try to convert the value to the proper type of the OPC UA variable.

Then as the third step, make the call to WriteToDeviceAsync. The function doesn't return any values.

Note

The fact that WriteToDeviceAsync doesn't return any value won't mean that writing the data fully succeeded. When the write call can't find a variable on the object, it will not trigger an exception but will silently ignore the provided input data for that variable, no exception is thrown and the write call is considered successful.

Exceptions

When something goes wrong at the moment of calling the functions, one of 2 exceptions can be thrown.

  1. ArgumentNullException: this exception is thrown when one of the parameters doesn't have a value the function expects. For example, a profile name is always required, setting the parameter to null or just an empty string or only whitespace will result in the mentioned exception.
  2. WebRequestException: the function calls are directed over OData to the framework, so when something unexpected happens during the execution on the framework itself, the functions will throw an WebRequestException. This exception has a field called Response containing the real cause of the exception.

An example with all catch blocks:

try
{
    // Make the necessary calls.
}
catch (ArgumentNullException ane)
{
    // One of the parameters to WriteToDeviceAsync call has an incorrect value.
    Console.WriteLine($"Parameter {ane.ParamName} has a wrong value");
}
catch (WebRequestException wre)
{
    // Something went wrong with the WriteToDeviceAsync call.
    // The 'Response' field contains the correct error message.
    Console.WriteLine(wre.Response);
}
catch (Exception e)
{
    // Log any other error message.
    Console.WriteLine(e);
}