# Steuerungsdaten bereitstellen

> Tutorial: Werte einer Steuerung oder eines Feldgeräts als OPC UA Server bereitstellen, mit Datenqualität bei Ausfall, Schreiben und Abfrage nur bei Bedarf.

Beteiligte Typen: `OpcServer`, `OpcNodeManager`, `OpcDataVariableNode`, `OpcStatusCode`, `OpcVariableValue`, `OpcWriteVariableValueContext`, `OpcAccessLevel`

Ein typischer Einsatz des OPC UA Servers ist ein Gateway: Er liest Werte aus einer Steuerung oder einem anderen Feldgerät und stellt sie Clients über OPC UA bereit. Diese Anleitung verbindet dafür die Bausteine aus den übrigen Seiten des Kapitels zu einem durchgängigen Ablauf. Im Code steht `Plc` stellvertretend für die Anbindung an die Steuerung, zum Beispiel über eine Bibliothek für das jeweilige Protokoll.

Der Ablauf besteht aus vier Schritten:

1. Einen Node-Manager mit einem Node je Datenpunkt anlegen.
2. Die Werte zyklisch aktualisieren und einen Ausfall der Steuerung als schlechte Qualität melden.
3. Schreibzugriffe der Clients an die Steuerung weitergeben.
4. Optional nur die Werte abfragen, die Clients gerade abonniert haben.

## Adressraum anlegen

Der Node-Manager legt beim Start des Servers für jeden Datenpunkt einen Variablen-Node an. Datenpunkte, die Clients schreiben dürfen, erhalten die passenden Zugriffsrechte und einen Callback, der den Wert an die Steuerung weitergibt (siehe [Schreibzugriffe weitergeben](https://www.traeger.de/docs/development/opcua/server/device-gateway#schreibzugriffe-weitergeben)):

```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
```

## Werte aktualisieren

Bei jeder Aktualisierung erhält der Node den neuen Wert, den Status und den Zeitstempel der Quelle. Erst `ApplyChanges` benachrichtigt die Clients, die den Node abonniert haben (siehe [Nodes aktualisieren](https://www.traeger.de/docs/development/opcua/server/node-management#nodes-aktualisieren)).

Wenn die Steuerung nicht erreichbar ist, behält der Node seinen letzten Wert, meldet aber den Status `BadCommunicationError`. Clients erkennen so, dass der Wert nicht mehr aktuell ist, statt einen eingefrorenen Wert als gültig zu übernehmen:

```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

```

Die Aktualisierung rufen Sie zum Beispiel in einer Schleife, über einen Timer oder aus einem Ereignis der Steuerungsbibliothek auf:

```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
```

## Schreibzugriffe weitergeben

Wenn ein Client einen Wert schreibt, ruft der Server den `WriteVariableValueCallback` des Nodes auf. Der Callback gibt den Wert an die Steuerung weiter. Wenn das nicht gelingt, liefert er einen Wert mit schlechtem Status zurück. Diesen Status erhält der Client als Ergebnis seiner Schreibanfrage:

```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

```

## Nur bei Bedarf abfragen

Viele Steuerungen vertragen keine beliebig häufigen Abfragen. Wenn der Node-Manager zählt, welche Nodes Clients gerade abonniert haben, fragt er nur diese Werte ab (siehe [Werte nur bei Bedarf erfassen](https://www.traeger.de/docs/development/opcua/server/read-write-values#werte-nur-bei-bedarf-erfassen)):

```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
```

Wenn Clients viele Werte direkt lesen (ohne Abonnement), lassen sich die Zugriffe einer Anfrage zudem mit einer einzigen Abfrage an die Steuerung beantworten (siehe [Zugriffe gebündelt verarbeiten](https://www.traeger.de/docs/development/opcua/server/read-write-values#zugriffe-gebundelt-verarbeiten)).

## Weiterführende Themen

- Die Werte zusätzlich historisch bereitstellen: [Historische Daten](https://www.traeger.de/docs/development/opcua/server/historical-data)
- Grenzwerte und Störungen der Steuerung als Alarme melden: [Alarme und Bedingungen](https://www.traeger.de/docs/development/opcua/server/alarms-conditions)
- Den Server absichern und konfigurieren: [Server absichern](https://www.traeger.de/docs/development/opcua/security/server) und [Konfiguration](https://www.traeger.de/docs/development/opcua/server/configuration)
