Simple C# Client Tutorial

This simple OPC UA client is written using the TRAEGER OPC UA SDK.

Using this library, you can easily set up an OPC UA client. In this tutorial we will use Visual Studio Code and .NET Core to create an OPC UA console application. If you prefer, you can use Visual Studio as well.

Note

Check the license model and prices of the TREAGER OPC UA Client SDK before continuing.

1. Install .NET and VSCode

Start with installing .NET 6.0 on your PC. If you are using Linux you can e.g. follow for Ubuntu 22.04 this description. Having that done, you should get a similar output:

$ dotnet --version
6.0.113

Then, install VSCode by following this tutorial. Further, install the C# language support extension.

2. Create a C# console application

First, create a new console application using:

$ dotnet new console -o opcua-csharp-client
[...]
$ cd opcua-csharp-client

A simple method to add the TRAEGER OPC UA Client SDK to your project is through NuGet:

$ dotnet add package Opc.UaFx.Client
[...]
info : Adding PackageReference for package 'Opc.UaFx.Client' into project '<path>/opcua-csharp-client/opcua-csharp-client.csproj'.
[...]

Open the current project in VSCode:

$ code .

3. Run the Sample C# Client

With all necessary dependencies installed, we create the OPC UA sample client. This client accesses Variables like BrakesOpen or CartesianPose. Further, it reads and writes entries of / to the Key-Int-Map as well as the Key-Pose-Map of the robot’s OPC UA server. Replace your Program.cs file with the following code block:

using Opc.UaFx;
using Opc.UaFx.Client;

namespace OpcUaClient
{
    // Autogenerated with OPC Watch
    [OpcDataTypeAttribute("ns=2;i=3002")]
    [OpcDataTypeEncodingAttribute("ns=2;i=5001", Type = Opc.UaFx.OpcEncodingType.Binary, NamespaceUri = "http://opcfoundation.org/UA/DI/")]
    [OpcDataTypeEncodingAttribute("ns=2;i=5003", Type = Opc.UaFx.OpcEncodingType.Binary, NamespaceUri = "http://opcfoundation.org/UA/DI/")]
    [OpcDataTypeEncodingAttribute("ns=2;i=5002", Type = Opc.UaFx.OpcEncodingType.Binary, NamespaceUri = "http://opcfoundation.org/UA/DI/")]
    public class KeyIntPair
    {
        public String? Key { get; set; }

        public Int32 Value { get; set; }
    }

    // Autogenerated with OPC Watch
    [OpcDataTypeAttribute("ns=2;i=3004")]
    [OpcDataTypeEncodingAttribute("ns=2;i=5010", Type = Opc.UaFx.OpcEncodingType.Binary, NamespaceUri = "http://opcfoundation.org/UA/DI/")]
    [OpcDataTypeEncodingAttribute("ns=2;i=5011", Type = Opc.UaFx.OpcEncodingType.Binary, NamespaceUri = "http://opcfoundation.org/UA/DI/")]
    [OpcDataTypeEncodingAttribute("ns=2;i=5012", Type = Opc.UaFx.OpcEncodingType.Binary, NamespaceUri = "http://opcfoundation.org/UA/DI/")]
    public class KeyPosePair
    {
        public string? Key { get; set; }

        public int NoOfValue { get; set; }

        [OpcDataTypeMemberLengthAttribute(nameof(NoOfValue))]
        public double[]? Value { get; set; }
    }

    class Program
    {
        static void Main()
        {
            // Change IP address and login information
            string opcuaServerUrl = "opc.tcp://<robot-ip>:4840";
            string username = "username";
            string password = "password";

            OpcClient client = new OpcClient(opcuaServerUrl);
            client.Security.UserIdentity = new OpcClientIdentity(username, password);

            // Enforce an encrypted connection
            OpcSecurityPolicy securityPolicy = new OpcSecurityPolicy(OpcSecurityMode.SignAndEncrypt, OpcSecurityAlgorithm.Basic256Sha256);
            client.Security.EndpointPolicy = securityPolicy;

            client.UseDynamic = true;
            client.Connect();

            Console.WriteLine("Connection status: " + client.State + "\n");

            // Check if SPoC token is free
            OpcValue controlTokenActive = client.ReadNode("ns=2;i=7023");

            if (controlTokenActive.Status.IsGood && (bool)controlTokenActive.Value)
            {
                Console.WriteLine("Can not acquire SPoC token.");
                OpcValue controlTokenOwner = client.ReadNode("ns=2;i=7025");
                if (controlTokenOwner.Status.IsGood) Console.WriteLine("Robot is in use by: " + controlTokenOwner.Value);
                return;
            }

            // Get Status of brakes
            OpcValue brakeState = client.ReadNode("ns=2;i=6018");
            if (brakeState.Status.IsGood) Console.WriteLine("Checking Arm brake state: " + ((bool)brakeState.Value ? "Brakes are open" : "Brakes are closed"));

            // Get value for certain key in KeyIntMap
            Console.WriteLine("\n# Accessing the KeyIntMap");
            String key = "Franka Test Key";
            try
            {
                int value = (int)client.CallMethod("ns=2;i=5005", "ns=2;i=7002", key)[0];
                Console.WriteLine("  Getting value for key: '" + key + "', value: " + value);
            }
            catch (Opc.UaFx.OpcException ex)
            {
                if (ex.Code == OpcStatusCode.BadNotFound)
                {
                    Console.WriteLine("  [Warning] KeyIntMap does not contain an entry for '" + key + "'");

                }
                else
                {
                    Console.WriteLine("  [Error] Could not read " + key + " from KeyIntMap. Details: " + ex.Message);
                }
            }

            // Key Int Replace
            KeyIntPair keyIntPair = new KeyIntPair();
            keyIntPair.Key = key;
            keyIntPair.Value = 132;
            object[] keyIntReplaceResult = client.CallMethod("ns=2;i=5005", "ns=2;i=7001", keyIntPair);
            Console.WriteLine("  Writing KeyIntPair with key: '" + keyIntPair.Key + "' and value: " + keyIntPair.Value);

            // Read the previously set pair
            try
            {
                int value = (int)client.CallMethod("ns=2;i=5005", "ns=2;i=7002", keyIntPair.Key)[0];
                Console.WriteLine("  Getting value for key: '" + keyIntPair.Key + "', value: " + value);
            }
            catch (Opc.UaFx.OpcException ex)
            {
                Console.WriteLine("  [Error] Could not read " + keyIntPair.Key + " from KeyIntMap. Details: " + ex.Message);
            }

            // Read the current Cartesian pose
            Console.WriteLine("\n# Accessing runtime data");
            OpcValue result = client.ReadNode("ns=2;i=7020");
            if (result.Status.IsGood)
            {
                double[,] currentCartesianPose = (double[,])result.Value;
                double[] currentCartesianPose1D = new double[currentCartesianPose.GetLength(0) * currentCartesianPose.GetLength(1)];

                int index = 0;
                for (int i = 0; i < currentCartesianPose.GetLength(1); i++)
                {
                    for (int j = 0; j < currentCartesianPose.GetLength(1); j++)
                    {
                        currentCartesianPose1D[index] = currentCartesianPose[i, j];
                        index++;
                    }
                }

                Console.WriteLine("  Cartesian pose read at " + result.ServerTimestamp + ":\n  [" + String.Join(" ", currentCartesianPose1D) + "]");

                Console.WriteLine("\n# Accessing KeyPoseMap");

                // Write the current Cartesian Pose to the KeyPoseStore
                KeyPosePair keyPosePair = new KeyPosePair();

                keyPosePair.Key = "CartPose1";
                keyPosePair.Value = currentCartesianPose1D;
                keyPosePair.NoOfValue = currentCartesianPose1D.Length;

                object[] keyPoseReplaceResult = client.CallMethod("ns=2;i=5013", "ns=2;i=7011", keyPosePair);
                Console.WriteLine("  Wrote previously read Cartesian pose to key '" + keyPosePair.Key + "'");
            }

            client.Disconnect();
        }
    }
}

Before continuing, replace opcuaServerUrl, username as well as the password variable with your robot’s IP and credentials.

This example client can be downloaded here. Extract this zip to your machine. Then open a terminal, switch to the extracted directory and execute dotnet restore.

Now, you’re ready to run the example:

$ dotnet run
  Connection status: Connected

  Checking arm brake state: Brakes are closed

  # Accessing the KeyIntMap
    [Warning] KeyIntMap does not contain an entry for 'Franka Test Key'
    Writing KeyIntPair with key: 'Franka Test Key' and value: 132
    Getting value for key: 'Franka Test Key', value: 132

  # Accessing runtime data
    Cartesian pose read at 3/22/2023 3:49:22 PM:
    [0.7508472281473332 0.6590922337599904 -0.04250428545383584 0 0.658801559060077 -0.7519673171928284 -0.022503478731971595 0 -0.04679460250784976 -0.011105428708064309 -0.9988427977561546 0 0.3852891676296914 -0.011258086525858014 0.5045356314639352 1]

  # Accessing KeyPoseMap
    Wrote previously read Cartesian pose to key 'CartPose1'

The pose named CartPose1 and the key int pair Franka Test Key is now available on the OPC UA server of the robot. It can now be used by OPC UA apps and other clients.

If you want to read different variables or call other methods, a simple way to get the corresponding NodeIDs is by browsing the OPC UA Server through UA Expert.