EonaCat.OPC.UA
0.0.4
Prefix Reserved
dotnet add package EonaCat.OPC.UA --version 0.0.4
NuGet\Install-Package EonaCat.OPC.UA -Version 0.0.4
<PackageReference Include="EonaCat.OPC.UA" Version="0.0.4" />
<PackageVersion Include="EonaCat.OPC.UA" Version="0.0.4" />
<PackageReference Include="EonaCat.OPC.UA" />
paket add EonaCat.OPC.UA --version 0.0.4
#r "nuget: EonaCat.OPC.UA, 0.0.4"
#:package EonaCat.OPC.UA@0.0.4
#addin nuget:?package=EonaCat.OPC.UA&version=0.0.4
#tool nuget:?package=EonaCat.OPC.UA&version=0.0.4
EonaCat.OPC.UA
EonaCat OPC UA client and server stack for .NET Standard 2.0.
The whole stack (binary encoding, UA-TCP transport, secure conversation, address space, sessions, subscriptions and certificate handling) is implemented from scratch on top of the base class library, so the package has no external NuGet dependencies.
Features
- Transport — OPC UA TCP (
opc.tcp://) with hello/acknowledge negotiation, message chunking, abort handling and security token renewal. - Encoding — complete OPC UA binary encoding including variants, matrices, extension objects and diagnostic information, with configurable limits against malformed input.
- Security — every security policy defined by the specification:
None,Basic128Rsa15,Basic256,Basic256Sha256,Aes128_Sha256_RsaOaepandAes256_Sha256_RsaPss, inNone,SignandSignAndEncryptmodes. - Certificates — self signed application instance certificates are generated in process (X.509 v3 with subject alternative name, PKCS#12 packaging) without any external library.
- Server — discovery, session, view, attribute, method, node management, subscription and monitored item service sets, plus hooks for history access.
- Client — endpoint discovery, anonymous / user name / certificate authentication, read, write, browse, browse next, translate browse paths, method calls and subscriptions.
- Model binding — expose plain CLR objects as nodes with a few attributes.
- PubSub — UADP binary encoding of network and data set messages (Part 14) over UDP
multicast/unicast, with a
UaPubSubPublisherthat periodically publishes node values from anAddressSpaceand aUaPubSubSubscriberthat applies received values back onto target nodes.
var configuration = new UaServerConfiguration
{
ApplicationName = "My Server",
EndpointHost = "localhost",
Port = 4840,
EndpointPath = "/MyServer"
};
using var server = new UaServer(configuration);
var ns = server.AddressSpace.AddNamespace("https://example.com/opcua");
var folder = server.AddressSpace.AddFolder(new NodeId(ObjectIds.ObjectsFolder), new NodeId("Demo", ns), "Demo", ns);
var counter = server.AddressSpace.AddVariable(folder.NodeId, new NodeId("Demo/Counter", ns), "Counter", new NodeId(DataTypeIds.Int32), 0, ns);
server.Start();
counter.SetValue(42); // subscribers are notified automatically
By default the server offers None, Basic256Sha256 (sign and sign and encrypt) and
Aes256_Sha256_RsaPss. Override UaServerConfiguration.SecurityPolicies to change that, and
supply UaServerConfiguration.ApplicationCertificate to use your own certificate instead of a
generated one.
Exposing your own models
[UaObject(BrowseName = "Boiler")]
public class Boiler : INotifyPropertyChanged
{
[UaVariable(MinimumSamplingInterval = 250)]
public double Temperature { get; set; }
[UaVariable(IsProperty = true, ReadOnly = true)]
public string SerialNumber { get; } = "EONA-0001";
[UaMethod]
public bool Start(double setPoint) { ... }
}
server.AddressSpace.AddModel(new Boiler(), folder.NodeId, "Boiler", ns);
Properties become variables, methods become method nodes with generated input and output
arguments, and members marked with [UaChildObject] become nested objects. When the model
implements INotifyPropertyChanged the matching variables raise data change notifications.
Authenticating users
configuration.UserIdentityValidator = token =>
{
if (token is UserNameIdentityToken user)
{
var password = Encoding.UTF8.GetString(user.Password);
if (user.UserName == "admin" && password == "secret")
{
return new UserIdentity(user.UserName, UserTokenType.UserName, token);
}
throw new ServiceResultException(StatusCodes.BadUserAccessDenied);
}
return UserIdentity.Anonymous;
};
Passwords are transported encrypted with the server certificate and are decrypted and nonce checked before the validator is called.
Creating a client
using var client = new UaClient();
// Connect without security.
await client.ConnectAsync("opc.tcp://localhost:4840/MyServer");
// Or pick a secure endpoint and authenticate.
var endpoint = await UaClient.SelectEndpointAsync(
"opc.tcp://localhost:4840/MyServer",
MessageSecurityMode.SignAndEncrypt,
SecurityPolicyUris.Basic256Sha256);
await client.ConnectAsync(endpoint, new UserNameIdentity("admin", "secret"));
var value = await client.ReadValueAsync(nodeId);
await client.WriteValueAsync(nodeId, 123);
foreach (var reference in await client.BrowseAsync(new NodeId(ObjectIds.ObjectsFolder)))
{
Console.WriteLine(reference.BrowseName);
}
var results = await client.CallAsync(objectId, methodId, new[] { new Variant(20), new Variant(22) });
var subscription = await client.CreateSubscriptionAsync(publishingInterval: 500);
var item = await subscription.AddMonitoredItemAsync(nodeId, samplingInterval: 250);
item.Notification += (s, e) => Console.WriteLine(e.Value);
The client keeps publish requests outstanding automatically for as long as subscriptions exist and renews the security token of the channel before it expires.
Certificate trust
CertificateValidator checks the validity period, the application uri and the trust status of a
peer certificate. Point it at directories to persist the trust lists:
configuration.CertificateValidator = new CertificateValidator
{
TrustedStore = new CertificateStore(@"pki\trusted"),
IssuerStore = new CertificateStore(@"pki\issuers"),
RejectedStore = new CertificateStore(@"pki\rejected"),
AutoAcceptUntrustedCertificates = false
};
configuration.CertificateValidator.CertificateValidationFailed += (s, e) =>
{
// Inspect e.Certificate and set e.Accept to override the decision.
};
Extending the type system
Any structure can be sent over the wire by implementing IEncodeable and registering it:
public class MyStructure : IEncodeable
{
public ExpandedNodeId TypeId => new ExpandedNodeId(new NodeId("MyStructure", 3));
public ExpandedNodeId BinaryEncodingId => new ExpandedNodeId(new NodeId("MyStructure_Encoding_DefaultBinary", 3));
public int Value { get; set; }
public void Encode(IEncoder encoder) => encoder.WriteInt32(Value);
public void Decode(IDecoder decoder) => Value = decoder.ReadInt32();
}
EncodeableFactory.GlobalFactory.AddEncodeableType(typeof(MyStructure));
PubSub
The publisher periodically samples node values from an AddressSpace and sends them as UADP
network messages over UDP; the subscriber listens on the same address/port and writes the
values it receives onto its own nodes.
PubSub publisher
var addressSpace = new AddressSpace();
var counter = addressSpace.AddVariable(ObjectIds.ObjectsFolder, new NodeId("Counter", 2), "Counter", new NodeId(DataTypeIds.Int32), 0);
var connection = new PubSubConnectionConfiguration("Connection1", "239.0.0.1", 4840) { PublisherId = (ushort)1 };
var dataSet = new PublishedDataSet("Set1");
dataSet.Fields.Add(new PublishedDataSetField("Counter", counter.NodeId));
connection.PublishedDataSets.Add(dataSet);
var writerGroup = new WriterGroupConfiguration(writerGroupId: 1, publishingIntervalMilliseconds: 500);
writerGroup.DataSetWriters.Add(new DataSetWriterConfiguration(dataSetWriterId: 1, publishedDataSetName: "Set1"));
connection.WriterGroups.Add(writerGroup);
using var publisher = new UaPubSubPublisher(connection, addressSpace);
publisher.Start();
// Update the source value on some interval; the publisher picks it up on the next publishing cycle.
counter.SetValue(counter.Value is int current ? current + 1 : 0);
PubSub subscriber
var addressSpace = new AddressSpace();
var target = addressSpace.AddVariable(ObjectIds.ObjectsFolder, new NodeId("RemoteCounter", 2), "RemoteCounter", new NodeId(DataTypeIds.Int32), 0);
// Same address, port, writer group id and data set writer id as the publisher above.
var connection = new PubSubConnectionConfiguration("Connection1", "239.0.0.1", 4840);
var reader = new DataSetReaderConfiguration(writerGroupId: 1, dataSetWriterId: 1);
reader.TargetNodeIds[0] = target.NodeId; // field index 0 ("Counter") maps to this node
var readerGroup = new ReaderGroupConfiguration();
readerGroup.DataSetReaders.Add(reader);
connection.ReaderGroups.Add(readerGroup);
using var subscriber = new UaPubSubSubscriber(connection, addressSpace);
subscriber.DataSetReceived += (sender, e) => Console.WriteLine("Received data set from writer " + e.DataSetMessage.DataSetWriterId);
subscriber.Start();
// target.Value is now kept in sync with the publisher's Counter node.
EonaCat OPC Simulator
A large OPC UA industrial simulation + OPC Designer HMI demo.
Run
dotnet run --project EonaCat.OPC.Example.Server
dotnet run --project EonaCat.OPC.Designer
The simulator endpoint is:
opc.tcp://localhost:48400/EonaCat/Simulator
Namespace:
urn:EonaCat:OPC:Simulator:Plant
The default Designer project contains a 72-widget industrial dashboard.
Simulated plant
- 3 production areas
- 18 devices (mixers, pumps, compressors, fans, ovens and tanks)
- Temperature, pressure, flow, level, RPM, current, voltage, power and vibration
- Setpoints, running state, remote mode, alarms, interlocks, modes, status and cycle counters
- Device diagnostics and communication health
- Steam, cooling water, compressed air, electricity and natural gas utilities
- Plant-wide KPIs
- Control methods: StartPlant, StopPlant, ResetAlarms
- Stable explicit
ns=2;s=...NodeIds and a stable namespace URI - Values update continuously once per second
Designer widgets
The canvas uses dedicated industrial layouts:
- Gauge: round analog meter with needle and scale
- Switch: physical-style ON/OFF switch
- Value: large digital process value
- Trend: miniature process trend chart
- Alarm: alarm state indicator
- Table: compact process table
- Button: HMI action button
- Text: display label
Dragging an OPC UA node onto the canvas preserves its complete binding metadata including NodeId, namespace index, namespace URI, identifier type and AttributeId.
EonaCat OPC Tool - production server bootstrap and OPC UA security
The OPC Tool can run one or more server configurations directly from the command line:
EonaCat.OPC.Tool.exe --server-config C:\OPC\Servers\server1.json --server-config C:\OPC\Servers\server2.json
EonaCat.OPC.Tool.exe --server-config-dir C:\OPC\Servers
Each configuration can set AutoStart to true. Use environment variables for certificate and OPC UA user passwords so credentials are not stored in the JSON file.
Production certificate layout
ApplicationCertificatePath: persistent.pfx/.p12application instance certificate with private key.TrustStorePath: persistent directory containing trusted.der/.crt/.cerpeer certificates.IssuerStorePath: persistent directory containing issuer certificates.RejectedStorePath: persistent directory for rejected certificates.CreateSelfSignedCertificate: creates and persists a self-signed application certificate when no application certificate exists.AcceptSelfSignedCertificates: accepts otherwise-valid self-signed peer certificates. This is intentionally separate from accepting every untrusted certificate.AutoAcceptUntrustedCertificates: development-only escape hatch; leave thisfalsein production.
For unattended operation, set ApplicationCertificatePasswordEnvironmentVariable and PasswordEnvironmentVariable instead of putting passwords in JSON.
The tool supports the OPC UA security profiles implemented by EonaCat.OPC.UA: None, Basic128Rsa15, Basic256, Basic256Sha256, Aes128-Sha256-RsaOaep and Aes256-Sha256-RsaPss, plus Anonymous, UserName and X.509 user authentication. Endpoint discovery selects the server's advertised endpoint, so the client does not require a hard-coded endpoint list.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.