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.
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:
- Create a node manager with one node per PLC variable.
- Update the values cyclically and report a PLC failure as bad quality.
- Forward client writes to the PLC.
- 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):
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;
}
}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 ClassUpdate 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:
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);
}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 SubYou call the update, for example, in a loop, from a timer, or from an event of the PLC library:
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);
}
}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 UsingForward 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:
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;
}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 FunctionPoll 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):
public void Poll()
{
// Only query values a client has subscribed to (see MachineNodeManager).
if (this.IsMonitored(this.temperatureNode.Id))
this.Update(Plc.ReadTemperature());
}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 SubIf 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
- Also provide the values as historical data: Historical data
- Report limit violations and faults of the PLC as alarms: Alarms and conditions
- Secure and configure the server: Secure servers and Configuration

