Getting Started with OPC UA — Collecting PLC and Device Data in a Windows App

· · Windows, OPC UA, Manufacturing, PLC, C#, .NET, Networking, Certificates

Post on X Submit to Hacker News Share on Reddit Share on LinkedIn Share on WhatsApp
Revision history (first version, published Nov 1, 2026)
First published
Cite this article(DOI: 10.5281/zenodo.23923016)

This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.

Go Komura (2026). Getting Started with OPC UA — Collecting PLC and Device Data in a Windows App. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23923016 https://comcomponent.com/en/blog/opc-ua-windows-data-collection/

DOI (latest version)
10.5281/zenodo.23923016
DOI (this version)
10.5281/zenodo.23923017

When you collect PLC or equipment data into a Windows app and the equipment has an OPC UA server, you may be able to avoid writing a separate protocol for each vendor. Connecting and reading values is not enough, though. Unless you know about the status and timestamps that come back with each value, subscriptions that notify you of changes, connection approval by certificate, and how to recover from disconnection, you will not be able to trust the collected data later.

This article explains the parts of OPC UA needed for data collection, together with results from actually running a C# client. The client uses the OPC Foundation’s .NET library, and the other end is a simulated equipment server. I have not tried it against any specific PLC or device. Check device-specific settings in that device’s documentation.

Collecting images from GigE Vision and USB3 Vision cameras is covered in “Dropped Frames with GigE Vision and USB3 Vision Cameras,” and devices that use serial communication in “Serial Communication App Pitfalls.”

The Difference Between OPC UA and OPC Classic

The OPC Classic specifications exchange data between software components using Windows COM/DCOM. OPC DA for process data, OPC A&E for alarms, and OPC HDA for history are defined as separate specifications1.

The OPC Foundation published OPC UA (Unified Architecture) in 2008. OPC UA is a service-oriented architecture that does not depend on a particular platform and brings together the functions of each Classic specification. As reasons for the redesign, the OPC Foundation cites that Microsoft came to emphasize cross-platform mechanisms over COM/DCOM, that it needed to run on non-Microsoft systems and embedded devices, and that it needed to connect securely across firewalls1.

The OPC UA specification has also been standardized by the IEC as the IEC 62541 series2. For the communication port, IANA has registered 4840/TCP as opcua-tcp3.

The difference between OPC Classic and OPC UAA diagram showing that OPC Classic is built on Windows COM and DCOM and split into separate DA, A&E and HDA specifications, while OPC UA brings their functions together into one platform-independent specification.Functions brought togetherOPC ClassicRuns on COM/DCOM (Windows)Split into DA, A&E and HDA specificationsOPC UA (2008)Not tied to a particular platformStandardized as IEC 62541

Figure 1: OPC Classic consists of separate specifications on top of COM/DCOM, and OPC UA brings them together into one.

If existing equipment has only an OPC DA server, you need something on the side that uses COM/DCOM. This article covers only OPC UA.

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (13 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

The Setup I Tried

This is the setup I ran.

Role What I used
Client C#, .NET 10 SDK 10.0.114, NuGet package OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1
Server A simulated device built with Python 3.13 and asyncua 2.1.0 (port 4840 on the same PC)
OS Linux (running on Windows was not tried)

The client library is the .NET implementation published by the OPC Foundation. For how to use the 2.0 line, I followed the approach in Getting started in the same repository4.

The setup I triedA diagram showing a C# client that uses the OPC Foundation's .NET library and connects over an encrypted connection to a simulated asyncua equipment server listening on port 4840.opc.tcp, SignAndEncryptC# client (LineCollector)Simulated equipment server (asyncua)OPC Foundation .NET library 2.0.1Four variables under Line1

Figure 2: Within the same PC, the C# client connected to the simulated equipment server.

The Simulated Equipment Server

In place of real equipment, I built a small server with asyncua5. Under an object named Line1, it has four variables: temperature (Temperature), part count (PartCount), running (Running), and setpoint (Setpoint). The temperature changes every 0.5 seconds and the part count every 2 seconds. For security, I provided two options: no encryption (None) and signing plus encryption (SignAndEncrypt with Basic256Sha256).

# A simulated device for testing. It is not a substitute for production equipment or PLCs.
import asyncio, math, random, socket
from pathlib import Path
from cryptography.x509.oid import ExtendedKeyUsageOID
from asyncua import Server, ua
from asyncua.crypto.cert_gen import setup_self_signed_certificate

HERE = Path(__file__).parent
APP_URI = "urn:komurasoft:demo-plc"


async def main():
    key, cert = HERE / "server_key.pem", HERE / "server_cert.der"
    await setup_self_signed_certificate(
        key, cert, APP_URI, socket.gethostname(),
        [ExtendedKeyUsageOID.SERVER_AUTH], {"commonName": "DemoPlcServer"})

    server = Server()
    await server.init()
    server.set_endpoint("opc.tcp://0.0.0.0:4840/demo/")
    server.set_server_name("DemoPlcServer")
    await server.set_application_uri(APP_URI)
    server.set_security_policy([
        ua.SecurityPolicyType.NoSecurity,
        ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt,
    ])
    await server.load_certificate(str(cert))
    await server.load_private_key(str(key))

    ns = await server.register_namespace("urn:komurasoft:demo-plc:line1")
    line1 = await server.nodes.objects.add_object(ua.NodeId("Line1", ns), "Line1")
    temperature = await line1.add_variable(ua.NodeId("Line1.Temperature", ns), "Temperature", 25.0)
    part_count = await line1.add_variable(
        ua.NodeId("Line1.PartCount", ns), "PartCount", ua.Variant(0, ua.VariantType.UInt32))
    await line1.add_variable(ua.NodeId("Line1.Running", ns), "Running", True)
    setpoint = await line1.add_variable(ua.NodeId("Line1.Setpoint", ns), "Setpoint", 30.0)
    await setpoint.set_writable()

    async with server:
        print("listening", flush=True)
        t, count = 0, 0
        while True:
            await asyncio.sleep(0.5)
            t += 1
            value = 25.0 + 2.0 * math.sin(t / 4) + random.uniform(-0.05, 0.05)
            await temperature.write_value(round(value, 2))
            if t % 4 == 0:
                count += 1
                await part_count.write_value(ua.Variant(count, ua.VariantType.UInt32))


asyncio.run(main())

This server accepts connecting clients’ certificates without checking them. That is a test setting and differs from how production equipment behaves.

The C# Client

I added these two packages to the project file.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="2.0.1" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
  </ItemGroup>

</Project>

The client connects, browses, reads, writes, and subscribes, in that order. The following sections explain what each part means.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.Subscriptions;
using Opc.Ua.Client.Subscriptions.Streaming;
using MonitoredItemOptions = Opc.Ua.Client.Subscriptions.MonitoredItems.MonitoredItemOptions;

string url = args.Length > 0 ? args[0] : "opc.tcp://localhost:4840/demo/";
int notifications = args.Length > 1 ? int.Parse(args[1]) : 6;
const string LineNamespace = "urn:komurasoft:demo-plc:line1";

HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Logging.SetMinimumLevel(LogLevel.Warning);

builder.Services
    .AddOpcUa()
    .AddClient(options =>
    {
        options.ApplicationName = "LineCollector";
        options.ApplicationUri = "urn:localhost:LineCollector";
        options.ProductUri = "urn:komurasoft:LineCollector";
        options.PkiRoot = Path.Combine(AppContext.BaseDirectory, "pki");
    })
    .AddDiscoveryAndConnect(options =>
    {
        options.DiscoveryUrl = url;
        options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
        options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
    });

using IHost host = builder.Build();
await host.StartAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(2));
CancellationToken ct = timeout.Token;

try
{
    var connect = host.Services
        .GetRequiredService<Func<CancellationToken, Task<ManagedSession>>>();
    ManagedSession session = await connect(ct);
    await using (session)
    {
        Console.WriteLine($"接続: {session.ConfiguredEndpoint?.EndpointUrl}");

        // The server decides the namespace index. Look it up from the URI and build NodeIds from it
        ushort ns = (ushort)session.NamespaceUris.GetIndex(LineNamespace);
        var line1 = new NodeId("Line1", ns);
        var temperature = new NodeId("Line1.Temperature", ns);
        var partCount = new NodeId("Line1.PartCount", ns);
        var setpoint = new NodeId("Line1.Setpoint", ns);

        // Browse: list the variables under Line1
        var browser = new Browser(session)
        {
            BrowseDirection = BrowseDirection.Forward,
            ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
            IncludeSubtypes = true,
            NodeClassMask = (uint)NodeClass.Variable,
        };
        Console.WriteLine("Line1 の変数:");
        foreach (ReferenceDescription child in await browser.BrowseAsync(line1, ct))
        {
            Console.WriteLine($"  {child.DisplayName}  {child.NodeId}");
        }

        // Read: look at the status and the two timestamps, not just the value
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // Reading a node that does not exist returns a Bad status instead of a value
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // Write: set Setpoint to 32.5
        var write = new WriteValue
        {
            NodeId = setpoint,
            AttributeId = Attributes.Value,
            Value = new DataValue(new Variant(32.5)),
        };
        WriteResponse wr = await session.WriteAsync(null, new[] { write }, ct);
        Console.WriteLine($"Setpoint の書き込み: {wr.Results[0]}");

        // Subscribe: ask the server to sample every 500 ms and notify us of changes
        Console.WriteLine($"購読({notifications}件まで):");
        await foreach (DataValueChange change in session.DefaultStreaming
            .SubscribeDataChangesAsync(new[] { temperature, partCount }, options: new MonitoredItemOptions
            {
                SamplingInterval = TimeSpan.FromMilliseconds(500),
                QueueSize = 5,
            }, ct: ct)
            .TakeAsync(notifications)
            .WithCancellation(ct))
        {
            DataValue v = change.Value;
            string name = change.MonitoredItem?.Name.Split("s=")[^1] ?? "?";
            Console.WriteLine($"  {Hms(v.SourceTimestamp)}  {name,-17} {v.WrappedValue,6}  {v.StatusCode}");
        }
    }
}
catch (ServiceResultException ex)
{
    Console.Error.WriteLine($"OPC UA エラー {ex.StatusCode}: {ex.Message}");
    Environment.ExitCode = 1;
}
finally
{
    await host.StopAsync();
}

static string Hms(DateTimeUtc time) => time.ToDateTime().ToString("HH:mm:ss.fff");

The first time I ran it, it could not connect. The reason is explained in the section “Approving Connections with Certificates.” Here is the output after trusting the server’s certificate. The library’s warning logs are omitted.

接続: opc.tcp://localhost:4840/demo/
Line1 の変数:
  Temperature  ns=2;s=Line1.Temperature
  PartCount  ns=2;s=Line1.PartCount
  Running  ns=2;s=Line1.Running
  Setpoint  ns=2;s=Line1.Setpoint
Temperature = 23.09  状態 Good [0x00000000]
  Source 13:40:51.302 / Server 13:40:51.302
Line1.Pressure の読み取り: BadNodeIdUnknown [0x80340000]
Setpoint の書き込み: Good [0x00000000]
購読(6件まで):
  13:40:51.302  Line1.Temperature  23.09  Good [0x00000000]
  13:40:50.801  Line1.PartCount       67  Good [0x00000000]
  13:40:51.802  Line1.Temperature  23.05  Good [0x00000000]
  13:40:52.304  Line1.Temperature  23.07  Good [0x00000000]
  13:40:52.806  Line1.Temperature   23.2  Good [0x00000000]
  13:40:52.806  Line1.PartCount       68  Good [0x00000000]

The times are in UTC.

Equipment Data Appears as Nodes in the Address Space

An OPC UA server exposes the data it holds as a collection of nodes. This collection is called the address space. Nodes come in kinds such as objects, which represent folders or equipment, and variables, which hold values, and nodes are connected to each other by references. The client follows references with the Browse service to find out which nodes exist6.

In the output above, the client follows hierarchical references from Line1 and finds four variables.

The address space of the simulated deviceA diagram showing a Line1 object under the Objects folder, with four variables under it: Temperature, PartCount, Running and Setpoint.ObjectsLine1 (object)Temperature (variable)PartCount (variable)Running (variable)Setpoint (variable, writable)

Figure 3: The address space of the simulated device. The client finds nodes by following references with Browse.

A NodeId Consists of a Namespace and an Identifier

Nodes are identified by NodeId. In the output, ns=2;s=Line1.Temperature means that the namespace index is 2 and the identifier is the string Line1.Temperature.

A namespace is really a URI. The specification says that namespace URIs are represented by numbers within services, and that the number corresponds to the position in the server’s NamespaceArray. Index 0 is the OPC UA namespace http://opcfoundation.org/UA/7.

For that reason, rather than writing the index number into your code, look up the index from the URI. The code above uses session.NamespaceUris.GetIndex(LineNamespace) to get 2 from the URI and builds the NodeIds from it.

Building a NodeId from a namespace URIA diagram showing that the client looks up the namespace URI in the server's NamespaceArray to get the index, and combines that index with the identifier to build a NodeId.Namespace URIThe server's NamespaceArrayIndex (2 in this example)NodeId ns=2;s=Line1.TemperatureIdentifier Line1.Temperature

Figure 4: Look up the namespace index from the URI and combine it with the identifier to make the NodeId.

Reads and Writes Return a Status and Timestamps with the Value

Variable values are read and written with the Read and Write services8. A read returns more than just the value. It returns a structure called DataValue that holds the value, a status code, a source timestamp, and a server timestamp9.

Item Meaning
Value The variable’s value. When the status code indicates an error, the value is ignored and the server sets it to null
Status code (StatusCode) Whether the server was able to obtain and provide the value
SourceTimestamp The time the data’s source attached to the value
ServerTimestamp The time the server received the value or learned that it was accurate

The top two bits of the status code indicate Good (success), Uncertain (partial success, which may not be usable for some purposes), or Bad (failure)10. The Good [0x00000000] in the output is Good.

When I read Line1.Pressure, which does not exist, BadNodeIdUnknown [0x80340000] came back instead of a value. This library’s ReadValueAsync throws an exception when the status is Bad4. If a system that records values also keeps the status code alongside each value, you can later tell apart the values recorded while the status was Bad.

What a DataValue containsA diagram showing that the DataValue returned by a read or a notification holds four things, the value, the status code, the source timestamp and the server timestamp, and that the value is ignored when the status code is Bad.If BadDataValueValueStatus codeSourceTimestamp (time at the source)ServerTimestamp (time at the server)Ignore the value

Figure 5: A read result holds not only the value but also a status code and two timestamps.

For the write, I wrote 32.5 to Setpoint, and Good came back as the resulting status code. The server decides which variables are writable. In this simulated server, only Setpoint is writable. Whether to let the equipment’s setpoints be rewritten from outside is a matter of the equipment’s safety design, so this article does not cover it.

Getting Notified of Changes: Subscriptions and MonitoredItems

To follow changes in values, the client can repeat Read at a fixed interval. Separately from that, OPC UA has a mechanism that leaves the monitoring of values to the server and has it notify you of changes. That mechanism is subscriptions and MonitoredItems.

A MonitoredItem is a single item being monitored, and it has a sampling interval. The sampling interval is the fastest rate at which the server samples values from the source, and the specification describes it as a “best effort” cycle11. When a sample meets the filter’s conditions, a notification is created. If no filter is specified, a notification is sent when the value or the status changes12.

Notifications first go into the MonitoredItem’s queue. You decide in advance whether the oldest or the newest is discarded when the queue overflows, and if the queue size is 2 or more, the Overflow bit that indicates a discard is set in the status code13.

A subscription groups several MonitoredItems and, every publishing interval, sends the client the notifications that have not yet been sent. When cycles pass with no notifications to send, it sends a keep-alive to show that the subscription is still alive14.

How subscriptions and MonitoredItems workA diagram showing that a MonitoredItem samples the source's value at the sampling interval and puts notifications that pass the filter into a queue, and that the subscription sends them together to the client at the publishing interval, sending a keep-alive when there are no notifications.Sampling intervalPublishing intervalIf no notificationsSource (the equipment's value)MonitoredItemFilter (default: change in value or status)Queue (Overflow when full)SubscriptionClientKeep-alive

Figure 6: The MonitoredItem samples and queues, and the subscription sends the notifications to the client together.

The code above subscribes to Temperature and PartCount with a 500-millisecond sampling interval and a queue size of 515. In the output, the temperature arrives roughly every 0.5 seconds and the part count roughly every 2 seconds. The first two notifications were the values each variable held when the subscription started. The part count’s 13:40:50.801 is the time its value changed, before the subscription started. Because the notifications display the source timestamp, they show the time the value changed, not the time the notification was received.

Choosing Between Repeated Reads and Subscriptions

Repeating reads lets the client decide the interval, and the mechanism is simple. In return, a request and response go back and forth every time even when the value has not changed, and changes shorter than the interval are missed. With a subscription, the server samples and sends only changes, so traffic does not grow much even for values that rarely change. Even with a subscription, though, changes shorter than the sampling interval may not be notified. The specification allows sampling faster than requested but does not require it11.

The difference between repeated reads and subscriptionsA diagram showing that repeated reads have the client decide the interval and send a request each time, that subscriptions have the server sample at the sampling interval and send only changes, and that both may miss changes shorter than the interval.Following changes in valuesRepeat readsSubscribeClient sets the interval and requests every timeServer samples and sends only changesMay miss changes shorter than the interval

Figure 7: With either method, changes shorter than the interval may be missed.

Approving Connections with Certificates

In an OPC UA connection, you choose the degree of message protection with MessageSecurityMode. The values are None, which provides no protection; Sign, which only signs; and SignAndEncrypt, which signs and encrypts16. The combination of algorithms used for signing and encryption is determined by the security policy, and this article uses Basic256Sha256.

Signing and encryption use a certificate for each application. The client and the server check each other’s certificates and refuse connections from parties they do not trust. A server built with the OPC Foundation’s .NET library does not offer the unencrypted None policy by default4.

The First Connection Stopped with BadCertificateUntrusted

On the first run, the client did not trust the server’s certificate, so the connection was refused. Here is part of the output.

fail: Opc.Ua.CertificateValidationCore[91]
      Certificate [Subject=CN=DemoPlcServer, CN=urn:komurasoft:demo-plc, Thumbprint=4E236DDA629FE2AB32D185BF681609FD6C525363, NotBefore=2026-11-01, NotAfter=2027-11-01, KeyAlgorithm=1.2.840.113549.1.1.1] validation failed with suppressible errors but was rejected. Reason=Id: BadCertificateUntrusted
...
OPC UA エラー BadCertificateUntrusted [0x801A0000]: [801A0000] (BadCertificateUntrusted) 'Could not verify security on OpenSecureChannel response.'

This library saves rejected certificates to the rejected store. When an administrator moves a certificate from there to the trusted store, the library trusts that party17. This time, I moved the server’s certificate from pki/rejected/certs to pki/trusted/certs, ran it again, and was able to connect. Before moving it, confirm the thumbprint, which is also part of the file name, with the server’s administrator4.

Steps to trust a certificateA diagram showing that on the first connection the client rejects the server's certificate and saves it to the rejected store, and that after confirming the thumbprint and moving it to the trusted store, the next connection trusts it and succeeds.First connectionBadCertificateUntrustedCertificate saved to the rejected storeConfirm the thumbprint with the server's administratorMove it to the trusted storeThe next connection trusts it and succeeds

Figure 8: After checking a rejected certificate and moving it to trusted, the client trusts that party.

There is also a setting, AutoAcceptUntrustedCertificates, that accepts untrusted certificates automatically. The default is false, and the library’s documentation says it is useful in development environments but should not be used in production17.

The NotAfter=2027-11-01 in the log is the expiry date of the simulated server’s certificate. It is a self-signed certificate created with an asyncua function, and it expired one year after the day it was created. Equipment certificates also expire, so decide as part of operations how to replace them before they expire and get the other side to trust the new ones. Handling of the Windows certificate store is covered in “The Windows Certificate Store in Practice.”

User Authentication

What certificates verify is the applications. Separately from that, a token identifying the user is passed when opening a session. The specification defines four kinds: anonymous, user name and password, X.509 certificate, and externally issued tokens18. The client in this article connected anonymously. Which tokens a server accepts is included in the server’s endpoint information19.

Where Data Collection in the Field Goes Wrong

From here, I list problems that tend to arise in systems that collect equipment data, based on this experiment and the specification.

Values from While the Server Was Stopped Do Not Arrive

During the subscription, I stopped the server and restarted it about 15 seconds later. This library’s ManagedSession reconnects automatically when the connection drops20. While the server was stopped, the client kept trying to reconnect.

warn: Opc.Ua.ChannelManager[6]
      Reconnect failed. Endpoint=opc.tcp://localhost:4840/demo/, Attempt=3, Outcome=transient-failure, Status=BadNotConnected, ErrorMessage=Could not connect to the remote endpoint.

When the server came back, the notifications came back too. Here is part of the subscription output.

  13:41:13.364  Line1.Temperature  25.61  Good [0x00000000]
  13:41:13.865  Line1.Temperature  25.12  Good [0x00000000]
  13:41:34.285  Line1.Temperature  26.55  Good [0x00000000]
  13:41:33.784  Line1.PartCount        2  Good [0x00000000]
  13:41:34.786  Line1.Temperature  26.16  Good [0x00000000]

Not a single value from between 13:41:13 and 13:41:34 arrived. The part count went back from 78 to 2. That is because restarting the simulated server made it count again from 0.

In this example the server itself was stopped, so the values from that period are not kept anywhere. The reconnection mechanism restores the connection, but it does not fill in values from the period when the server was down.

The situation is different when only the communication drops and the server keeps running. The specification has a Republish service that has the server resend notification messages that did not arrive, from its retransmission queue21. Notifications also accumulate in the MonitoredItem’s queue, but anything beyond the queue size is discarded13. I did not test this case in this experiment. In either case, keep a record of the disconnection and reconnection times so you can tell during which period no values arrived. Reconnection and log design for serial-communication equipment are also covered in “Serial Communication App Pitfalls.”

Recording when the server stopsA diagram showing that when the server stops during a subscription, the client keeps trying to reconnect and the notifications come back when the server returns, but the values from while it was stopped do not arrive, so the disconnection and reconnection times should be recorded.SubscribedThe server stopsThe client keeps trying to reconnectThe server returns and notifications resumeValues from while it was stopped do not arriveRecord the disconnection and reconnection times

Figure 9: Reconnecting restores the connection, but the values from while the server was stopped do not arrive.

Which Timestamp to Record

The specification says that SourceTimestamp is the time the data’s source attached to the value and should indicate when the value changed. ServerTimestamp is the time the server received the value or learned that it was accurate9. The simulated server in this article generates the values itself, so the two times were the same.

If you want to know when a value changed on the equipment, use SourceTimestamp. That time is stamped by the source’s clock, though, so if the equipment’s or server’s clock is off, the record is off too. If you will match the collected data against other records, also keep the time on the receiving PC alongside it so you can check for drift later. Time synchronization on the Windows side is covered in “Windows Time Synchronization (w32time) and Business Systems.”

Do Not Treat Values Whose Status Is Not Good as Plain Numbers

When the status code is Bad, the value is to be ignored9. When it is Uncertain, the value may not be usable for some purposes10. Before feeding values into graphs or aggregates, sort them by status code and decide how to handle values that are not Good. The Overflow bit for an overflowing queue is also set within the status code13. How to show equipment status on screen is covered in “Best Practices for Checking and Displaying External Device State.”

Open the Port in the Firewall

The OPC UA port registered with IANA is 4840/TCP3. Not every server uses this number, however. The OPC Foundation’s .NET samples use ports such as 625414. Check the port number in the equipment’s endpoint URL and allow connections to that port. If the server runs on Windows, allow it in that machine’s Windows Firewall, and if there is network equipment in between, allow it on that equipment. How to create inbound rules is covered in “The Windows Firewall and Business Applications.”

Where data collection goes wrongA diagram showing the places where equipment data collection goes wrong, divided into the content of the records (values from while the server was stopped not arriving, which timestamp to record, handling of values that are not Good) and connection preparation (firewall ports, certificate expiry).Where data collection goes wrongContent of the recordsConnection preparationValues from a stop do not arriveWhich timestampHandling values that are not GoodFirewall portsCertificate expiry

Figure 10: Even after you can connect, check for gaps in the records, the times, the status, the ports, and the certificates.

Choosing Between OPC UA, Modbus/TCP, and Vendor-Specific Protocols

For communicating with PLCs and equipment, besides OPC UA there is also Modbus/TCP, as well as protocols provided by PLC makers.

The Modbus specification handles data in four tables: Discrete Inputs, read-only single bits; Coils, read-write single bits; Input Registers, read-only 16-bit words; and Holding Registers, read-write 16-bit words22. IANA registers 502/TCP as the Modbus application protocol (mbap)3.

What goes into these tables is bits and 16-bit words, so which value sits at which address in which format must be confirmed in the equipment’s documentation and written on the client side. With OPC UA, as we have seen, the server exposes named variables in its address space, and the client can discover them with Browse. Values come with a status code and timestamps, and connections are approved by certificate.

Which one to use will often be decided by what the equipment provides. If the equipment has an OPC UA server, use it; if it has only Modbus/TCP, build an address map and read from that. If there is only the vendor’s protocol or SDK, follow its documentation. How to choose a communication method for USB devices is covered in “How to Work with USB Devices from a Windows App.”

Choosing how to communicate with equipmentA diagram showing that if the equipment has an OPC UA server you browse and collect variables with OPC UA, if it has only Modbus/TCP you confirm addresses and formats in the documentation and read registers, and if it has only the vendor's protocol you follow that documentation.OPC UA serverModbus/TCP onlyVendor protocol onlyCommunication the equipment providesWhat is available?Browse and collect the variablesConfirm addresses and formats in the docsFollow the vendor's documentation and SDKRead the registers and convert them to values

Figure 11: What you collect with is often decided by what the equipment provides.

References

  1. OPC Foundation, Classic. That OPC Classic is based on COM/DCOM, the DA, A&E and HDA specifications, the publication of OPC UA in 2008, and the reasons for the redesign. ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. Part 1 of the IEC 62541 series, covering the concepts and overview of OPC UA (since replaced by a later edition). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. The registrations of opcua-tcp on 4840/TCP and mbap on 502/TCP. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. How to write a 2.0-line client, that ReadValueAsync throws an exception on Bad, the rejected and trusted stores, that servers do not offer None by default, and the port numbers used in the samples. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). The Python OPC UA library used for the simulated equipment server. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. The service that follows a node’s references. ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. That namespace URIs are represented by numbers within services, and that the number corresponds to a position in the NamespaceArray. ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. The Read and Write services. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. The definitions of value, status code, SourceTimestamp and ServerTimestamp. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain and Bad as indicated by the top two bits. ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. That the sampling interval is a best-effort cycle. ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. That without a filter, a notification is sent on a change in value or status. ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. The queue size, the discard policy, and the Overflow bit. ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. The publishing interval and keep-alives. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync, and the sampling interval and queue size in MonitoredItemOptions. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. The meanings of None, Sign and SignAndEncrypt. ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. The practice of moving certificates from the rejected store to the trusted store, and not using AutoAcceptUntrustedCertificates in production. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. The four kinds: anonymous, user name, X.509, and issued tokens. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. The list of user tokens an endpoint accepts (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. That ManagedSession has a built-in reconnection mechanism. ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. The service that has a subscription’s retransmission queue send notification messages again. ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b (a copy hosted on a University of Chicago server). The data model in section 4.3 (Discrete Input, Coil, Input Register, Holding Register). ↩

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These are topic pages close to this theme. Starting from the article, you can go on to related services and other articles.

This article leads to the following service pages. Start from the entry point closest to your situation.

Windows App Development

Building a Windows app that collects data from equipment, including the subscription settings, reconnection, and recording of timestamps and status, can be handled as Windows app development.

Frequently Asked Questions

Questions commonly asked at the time of consultation about the topic of this article.

What is the difference between OPC UA and OPC Classic (OPC DA)?
OPC Classic is a set of specifications that exchange data over Windows COM/DCOM. OPC UA, published in 2008, does not depend on a particular OS and brings the functions of Classic together. OPC UA clients run outside Windows too, and no DCOM configuration is needed.
When collecting values with OPC UA, is it enough to read them at a fixed interval?
If there are few values and the interval is long, reading works. To follow changes in values, you can instead use a subscription, which leaves the sampling to the server and has it notify you of changes. Either way, record the status code and timestamps that come back with each value.
Why do I get BadCertificateUntrusted when I connect?
Because the client does not yet trust the server's certificate. The OPC Foundation's .NET library saves rejected certificates to the rejected store. Confirm the certificate's thumbprint with the server's administrator, then move it to the trusted store. Limit settings that accept certificates automatically to development environments.
What port number does OPC UA use?
IANA registers 4840/TCP as opcua-tcp. Real servers sometimes use a different port, though, so check the equipment's settings. If the server runs on Windows, you need to allow traffic to that port in Windows Firewall, and if there is network equipment in between, on that equipment too.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and bug investigation, with particular strength in projects where existing assets remain and in investigating failures whose cause is hard to see.

Back to the Blog