# Provide data from a device

> Tutorial: 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](https://www.traeger.de/docs/development/opcua/server/device-gateway)

Types used: `OpcServer`, `OpcNodeManager`, `OpcDataVariableNode`, `OpcStatusCode`, `OpcVariableValue`, `OpcWriteVariableValueContext`, `OpcAccessLevel`

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](https://www.traeger.de/en/docs/development/opcua/server/device-gateway#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](https://www.traeger.de/en/docs/development/opcua/server/node-management#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](https://www.traeger.de/en/docs/development/opcua/server/read-write-values#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](https://www.traeger.de/en/docs/development/opcua/server/read-write-values#process-access-in-batches)).

## Further topics

- Also provide the values as historical data: [Historical data](https://www.traeger.de/en/docs/development/opcua/server/historical-data)
- Report limit violations and faults of the PLC as alarms: [Alarms and conditions](https://www.traeger.de/en/docs/development/opcua/server/alarms-conditions)
- Secure and configure the server: [Secure servers](https://www.traeger.de/en/docs/development/opcua/security/server) and [Configuration](https://www.traeger.de/en/docs/development/opcua/server/configuration)
