Skip to content

Tutorial

Provide data from a device

Provide the values of a PLC or field device as an OPC UA server, with data quality on failure, write access, and polling only on demand.

This content was machine translated and may contain errors. Read in German

Tutorial

Provide data from a device ​

Types usedOpcServerOpcNodeManagerOpcDataVariableNodeOpcStatusCodeOpcVariableValueOpcWriteVariableValueContextOpcAccessLevel

A typical use of an OPC UA server is a gateway: it reads values from a PLC or another field device and provides them to clients over OPC UA. This tutorial combines the building blocks from the other pages of this chapter into one continuous workflow. In the code, Plc stands for the connection to the PLC, for example through a library for the protocol in question.

The workflow consists of four steps:

  1. Create a node manager with one node per PLC variable.
  2. Update the values cyclically and report a PLC failure as bad quality.
  3. Forward client writes to the PLC.
  4. Optionally, poll only the values that clients currently subscribe to.

Create the address space ​

When the server starts, the node manager creates one variable node for each PLC variable. Values that clients may write get the matching access rights and a callback that forwards the value to the PLC (see Forward writes):

csharp
public class PlcNodeManager : OpcNodeManager
{
    private OpcDataVariableNode<double> temperatureNode;
    private OpcDataVariableNode<int> speedNode;
 
    public PlcNodeManager()
        : base("http://example.com/plc")
    {
    }
 
    protected override IEnumerable<IOpcNode> CreateNodes(OpcNodeReferenceCollection references)
    {
        var machine = new OpcObjectNode(new OpcName("Machine", this.DefaultNamespaceIndex));
        references.Add(machine, OpcObjectTypes.ObjectsFolder);
 
        this.temperatureNode = new OpcDataVariableNode<double>(machine, "Temperature");
        this.speedNode = new OpcDataVariableNode<int>(machine, "Speed");
 
        // Clients may write the speed; the value is forwarded to the device.
        this.speedNode.AccessLevel = OpcAccessLevel.CurrentReadOrWrite;
        this.speedNode.UserAccessLevel = OpcAccessLevel.CurrentReadOrWrite;
        this.speedNode.WriteVariableValueCallback = this.HandleWriteSpeed;
 
        yield return machine;
    }
}
vbnet
Public Class PlcNodeManager
    Inherits OpcNodeManager

    Private temperatureNode As OpcDataVariableNode(Of Double)
    Private speedNode As OpcDataVariableNode(Of Integer)

    Public Sub New()
        MyBase.New("http://example.com/plc")
    End Sub

    Protected Overrides Iterator Function CreateNodes( _
            ByVal references As OpcNodeReferenceCollection) As IEnumerable(Of IOpcNode)
        Dim machine = New OpcObjectNode(New OpcName("Machine", Me.DefaultNamespaceIndex))
        references.Add(machine, OpcObjectTypes.ObjectsFolder)

        Me.temperatureNode = New OpcDataVariableNode(Of Double)(machine, "Temperature")
        Me.speedNode = New OpcDataVariableNode(Of Integer)(machine, "Speed")

        'Clients may write the speed; the value is forwarded to the device.
        Me.speedNode.AccessLevel = OpcAccessLevel.CurrentReadOrWrite
        Me.speedNode.UserAccessLevel = OpcAccessLevel.CurrentReadOrWrite
        Me.speedNode.WriteVariableValueCallback = AddressOf Me.HandleWriteSpeed

        Yield machine
    End Function
End Class

Update values ​

With each update, the node receives the new value, the status, and the source timestamp. Only ApplyChanges notifies the clients that subscribe to the node (see Update nodes).

If the PLC is not reachable, the node keeps its last value but reports the status BadCommunicationError. This way, clients see that the value is no longer current instead of accepting a frozen value as valid:

csharp
public void Update(PlcReading reading)
{
    if (reading.IsAvailable) {
        this.temperatureNode.Value = reading.Temperature;
        this.temperatureNode.Status.Update(OpcStatusCode.Good);
        this.temperatureNode.Timestamp = reading.Timestamp;
    }
    else {
        // Keep the last value, but report that it is no longer up to date.
        this.temperatureNode.Status.Update(OpcStatusCode.BadCommunicationError);
        this.temperatureNode.Timestamp = DateTime.UtcNow;
    }
 
    this.temperatureNode.ApplyChanges(this.SystemContext);
}
vbnet
Public Sub Update(ByVal reading As PlcReading)
    If reading.IsAvailable Then
        Me.temperatureNode.Value = reading.Temperature
        Me.temperatureNode.Status.Update(OpcStatusCode.Good)
        Me.temperatureNode.Timestamp = reading.Timestamp
    Else
        'Keep the last value, but report that it is no longer up to date.
        Me.temperatureNode.Status.Update(OpcStatusCode.BadCommunicationError)
        Me.temperatureNode.Timestamp = DateTime.UtcNow
    End If

    Me.temperatureNode.ApplyChanges(Me.SystemContext)
End Sub

You call the update, for example, in a loop, from a timer, or from an event of the PLC library:

csharp
using (var plcManager = new PlcNodeManager())
using (var server = new OpcServer("opc.tcp://localhost:4840/", plcManager)) {
    server.Start();
 
    while (!cancellation.IsCancellationRequested) {
        plcManager.Update(Plc.ReadTemperature());
        Thread.Sleep(millisecondsTimeout: 1000);
    }
}
vbnet
Using plcManager = New PlcNodeManager()
    Using server = New OpcServer("opc.tcp://localhost:4840/", plcManager)
        server.Start()

        While Not cancellation.IsCancellationRequested
            plcManager.Update(Plc.ReadTemperature())
            Thread.Sleep(millisecondsTimeout:=1000)
        End While
    End Using
End Using

Forward writes ​

When a client writes a value, the server calls the WriteVariableValueCallback of the node. The callback forwards the value to the PLC. If this fails, it returns a value with a bad status. The client receives this status as the result of its write request:

csharp
private OpcVariableValue<object> HandleWriteSpeed(
        OpcWriteVariableValueContext context,
        OpcVariableValue<object> value)
{
    if (!Plc.TryWriteSpeed((int)value.Value)) {
        // The client receives this status as the result of its write request.
        return new OpcVariableValue<object>(
                value.Value,
                timestamp: DateTime.UtcNow,
                statusCode: OpcStatusCode.BadCommunicationError);
    }
 
    return value;
}
vbnet
Private Function HandleWriteSpeed( _
        ByVal context As OpcWriteVariableValueContext, _
        ByVal value As OpcVariableValue(Of Object)) As OpcVariableValue(Of Object)
    If Not Plc.TryWriteSpeed(CInt(value.Value)) Then
        'The client receives this status as the result of its write request.
        Return New OpcVariableValue(Of Object)( _
                value.Value, _
                timestamp:=DateTime.UtcNow, _
                statusCode:=OpcStatusCode.BadCommunicationError)
    End If

    Return value
End Function

Poll only on demand ​

Many PLCs cannot handle polling at any rate. If the node manager counts which nodes clients currently subscribe to, it polls only these values (see Sample values only on demand):

csharp
public void Poll()
{
    // Only query values a client has subscribed to (see MachineNodeManager).
    if (this.IsMonitored(this.temperatureNode.Id))
        this.Update(Plc.ReadTemperature());
}
vbnet
Public Sub Poll()
    'Only query values a client has subscribed to (see MachineNodeManager).
    If Me.IsMonitored(Me.temperatureNode.Id) Then
        Me.Update(Plc.ReadTemperature())
    End If
End Sub

If clients read many values directly (without a subscription), you can also answer all accesses of one request with a single query to the PLC (see Process access in batches).

Further topics ​