Getting Started with OPC UA — Collecting PLC and Device Data in a Windows App
· Go Komura · Windows, OPC UA, Manufacturing, PLC, C#, .NET, Networking, Certificates
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.
flowchart TB
accTitle: The difference between OPC Classic and OPC UA
accDescr: A 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.
C["OPC Classic"] --> D["Runs on COM/DCOM (Windows)"]
D --> DA["Split into DA, A&E and HDA specifications"]
DA -->|"Functions brought together"| U["OPC UA (2008)"]
U --> P["Not tied to a particular platform"]
P --> S["Standardized 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.
flowchart TB
accTitle: The setup I tried
accDescr: A 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.
A["C# client (LineCollector)"] -->|"opc.tcp, SignAndEncrypt"| B["Simulated equipment server (asyncua)"]
A -.-> L["OPC Foundation .NET library 2.0.1"]
B -.-> V["Four 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.
flowchart TB
accTitle: The address space of the simulated device
accDescr: A diagram showing a Line1 object under the Objects folder, with four variables under it: Temperature, PartCount, Running and Setpoint.
O["Objects"] --> L["Line1 (object)"]
L --> T["Temperature (variable)"]
L --> P["PartCount (variable)"]
L --> R["Running (variable)"]
L --> S["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.
flowchart TB
accTitle: Building a NodeId from a namespace URI
accDescr: A 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.
U["Namespace URI"] --> A["The server's NamespaceArray"]
A --> I["Index (2 in this example)"]
I --> N["NodeId ns=2;s=Line1.Temperature"]
K["Identifier Line1.Temperature"] --> N
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.
flowchart TB
accTitle: What a DataValue contains
accDescr: A 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.
D["DataValue"] --> V["Value"]
D --> S["Status code"]
D --> TS["SourceTimestamp (time at the source)"]
D --> TV["ServerTimestamp (time at the server)"]
S -->|"If Bad"| X["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.
flowchart TB
accTitle: How subscriptions and MonitoredItems work
accDescr: A 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.
S["Source (the equipment's value)"] --> M["MonitoredItem"]
M -->|"Sampling interval"| F["Filter (default: change in value or status)"]
F --> Q["Queue (Overflow when full)"]
Q --> SUB["Subscription"]
SUB -->|"Publishing interval"| C["Client"]
SUB -.->|"If no notifications"| K["Keep-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.
flowchart TB
accTitle: The difference between repeated reads and subscriptions
accDescr: A 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.
A["Following changes in values"] --> R["Repeat reads"]
A --> S["Subscribe"]
R --> R1["Client sets the interval and requests every time"]
S --> S1["Server samples and sends only changes"]
R1 --> X["May miss changes shorter than the interval"]
S1 --> X
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.
flowchart TB
accTitle: Steps to trust a certificate
accDescr: A 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.
A["First connection"] --> B["BadCertificateUntrusted"]
B --> C["Certificate saved to the rejected store"]
C --> D["Confirm the thumbprint with the server's administrator"]
D --> E["Move it to the trusted store"]
E --> F["The 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.”
flowchart TB
accTitle: Recording when the server stops
accDescr: A 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.
A["Subscribed"] --> B["The server stops"]
B --> C["The client keeps trying to reconnect"]
C --> D["The server returns and notifications resume"]
B -.-> E["Values from while it was stopped do not arrive"]
E -.-> F["Record 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.”
flowchart TB
accTitle: Where data collection goes wrong
accDescr: A 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).
A["Where data collection goes wrong"] --> R["Content of the records"]
A --> N["Connection preparation"]
R --> B["Values from a stop do not arrive"]
B ~~~ C["Which timestamp"]
C ~~~ D["Handling values that are not Good"]
N --> E["Firewall ports"]
E ~~~ F["Certificate 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.”
flowchart TB
accTitle: Choosing how to communicate with equipment
accDescr: A 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.
A["Communication the equipment provides"] --> B{"What is available?"}
B -->|"OPC UA server"| C["Browse and collect the variables"]
B -->|"Modbus/TCP only"| D["Confirm addresses and formats in the docs"]
B -->|"Vendor protocol only"| E["Follow the vendor's documentation and SDK"]
D --> F["Read the registers and convert them to values"]
Figure 11: What you collect with is often decided by what the equipment provides.
Related Articles
- Dropped Frames with GigE Vision and USB3 Vision Cameras — Locating the Cause and the Windows Settings to Review
- Serial Communication App Pitfalls - Through Reconnection and Log Design
- Best Practices for Checking and Displaying External Device State
- How to Work with USB Devices from a Windows App
- The Windows Firewall and Business Applications
- The Windows Certificate Store in Practice
- Windows Time Synchronization (w32time) and Business Systems
References
-
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
-
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). ↩
-
IANA, Service Name and Transport Protocol Port Number Registry. The registrations of
opcua-tcpon 4840/TCP andmbapon 502/TCP. ↩ ↩2 ↩3 -
OPC Foundation, UA-.NETStandard 2.0.1: Getting started. How to write a 2.0-line client, that
ReadValueAsyncthrows an exception on Bad, the rejected and trusted stores, that servers do not offerNoneby default, and the port numbers used in the samples. ↩ ↩2 ↩3 ↩4 ↩5 -
FreeOpcUa, asyncua (PyPI). The Python OPC UA library used for the simulated equipment server. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. The service that follows a node’s references. ↩
-
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. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. The Read and Write services. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. The definitions of value, status code, SourceTimestamp and ServerTimestamp. ↩ ↩2 ↩3
-
OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain and Bad as indicated by the top two bits. ↩ ↩2
-
OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. That the sampling interval is a best-effort cycle. ↩ ↩2
-
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. ↩
-
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
-
OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. The publishing interval and keep-alives. ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set.
SubscribeDataChangesAsync, and the sampling interval and queue size inMonitoredItemOptions. ↩ -
OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. The meanings of None, Sign and SignAndEncrypt. ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Certificates. The practice of moving certificates from the rejected store to the trusted store, and not using
AutoAcceptUntrustedCertificatesin production. ↩ ↩2 -
OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. The four kinds: anonymous, user name, X.509, and issued tokens. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. The list of user tokens an endpoint accepts (userIdentityTokens). ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. That
ManagedSessionhas a built-in reconnection mechanism. ↩ -
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. ↩
-
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). ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Why Arguments Break — The Rules of Windows Command-Line Arguments
Windows passes CreateProcess a single string that the receiver splits. Covers the CommandLineToArgvW, CRT, and .NET rules, ArgumentList, ...
End of Servicing for Windows Printer Drivers — How Business Apps Should Prepare Their Report and Label Printing
Microsoft is phasing out v3/v4 printer drivers. What Windows protected print mode removes, and how to inventory and prepare report and la...
Corporate Proxies and Windows Apps — Sorting Out Proxy Resolution in WinINET, WinHTTP, and .NET
Browser works, business app fails behind the corporate proxy? Usually WinINET, WinHTTP, environment variables, and .NET read different pr...
Practical Multithreading Best Practices: .NET Edition — What to Decide Before You Add More Threads
Keep .NET/C# threads from crashing or hanging. Ride on Task, cut shared mutable state, lock with discipline, stop with CancellationToken,...
Using WMI/CIM from C# and PowerShell — A Practical Guide to Hardware Inventory, Process Monitoring, and Remote Queries
WMI/CIM is the standard way to read a PC serial number, watch disk space, and detect process starts. Covers CIM cmdlets, Get-WmiObject mi...
Related Topics
These are topic pages close to this theme. Starting from the article, you can go on to related services and other articles.
Windows Technical Topics
Entry point collecting technical topics on Windows development, bug investigation, and use of existing assets.
Bug Investigation / Long-Run Operation Topic
Topic page sorting out hard-to-reproduce bugs, communication stoppages, long-running failures, and failure-path verification.
Services Connected to This Topic
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.
Technical Consulting & Design Review
If you want to decide whether to collect with OPC UA or use Modbus/TCP or the vendor's own protocol, and how to manage certificates, this can be handled as technical consulting and design review.
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.