Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
10 KiB
OPC UA Client (Gateway) Driver
Getting-started guide for the OPC UA Client driver. This is the short path — for
the full per-field spec read docs/v2/driver-specs.md §8,
and for the test-harness map read OpcUaClient-Test-Fixture.md.
What it talks to
A remote OPC UA server. This driver runs the opposite direction from the
usual "server exposes PLC data" flow: it acts as an OPC UA client, opens a
Session against an upstream server, and re-exposes that server's address space
through the local OtOpcUa server. Browse, read, write, subscribe, alarm, and
history calls are passed through to the upstream endpoint.
It is built on the OPC Foundation UA .NET Standard reference SDK and runs in-process in the OtOpcUa server's .NET 10 AnyCPU host — pure managed, no out-of-process isolation.
There is no standalone driver CLI for the OPC UA Client driver. To exercise a remote OPC UA endpoint by hand, point the general-purpose Client CLI at it directly.
Project split
| Project | Target | Role |
|---|---|---|
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ |
net10.0 | In-process driver — session lifetime, read / write / subscribe / alarm / history passthrough |
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/ |
net10.0 | IDriverBrowser — live address-picker browse used by the AdminUI |
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/ |
net10.0 | Config records + enums bound from DriverConfig JSON |
Minimum deployment
"Drivers": {
"upstream-1": {
"Type": "OpcUaClient",
"Config": {
"EndpointUrl": "opc.tcp://plc.internal:4840",
"SecurityPolicy": "None",
"SecurityMode": "None",
"AuthType": "Anonymous",
"TargetNamespaceKind": "Equipment"
}
}
}
EndpointUrls (a list) takes precedence over the single-URL EndpointUrl and
provides ordered failover — the driver tries each candidate in turn at init
and on session drop, and the first to connect wins (e.g. a hot-standby pair on
4840 / 4841). See
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs
for every field (security policy/mode, auth type, session timeout, keep-alive,
reconnect period, browse root, node/depth caps).
Session lifetime
A single Session per driver instance; subscriptions multiplex onto it. The
SDK reconnect handler takes the session down and brings it back on remote-server
restart, re-sending subscriptions on reconnect so monitored-item handles don't
dangle. Stored NodeIds embed the server-stable namespace URI (not the
session-relative ns=N index) so a remote namespace-table reorder across a
restart doesn't silently re-point references at the wrong namespace.
Namespace assignment
This is the only driver that gateways into either namespace kind, decided
per instance via TargetNamespaceKind:
Equipment— the remote server exposes raw equipment data; remote browse paths are remapped to UNS via a requiredUnsMappingTable.SystemPlatform— the remote server exposes processed/derived data; the remote hierarchy is preserved with no UNS conversion (and the mapping table must be empty).
The choice is enforced at startup so a misconfiguration fails draft validation rather than surfacing as a runtime surprise.
Capability surface
OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IAlarmSource, IHistoryProvider
(src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs:31).
| Capability | Path | Notes |
|---|---|---|
ITagDiscovery |
DiscoverAsync (recursive browse) |
Mirrors the upstream tree from BrowseRoot (default ObjectsFolder i=85), bounded by MaxDiscoveredNodes / MaxBrowseDepth |
IReadable |
ReadAsync → Session.ReadAsync |
Upstream StatusCodes pass through verbatim (cascading-quality rule) |
IWritable |
WriteAsync → Session.WriteAsync |
Passthrough write |
ISubscribable |
native OPC UA subscriptions / monitored items | The remote server pushes data changes |
IHostConnectivityProbe |
session keep-alive | Host key is the endpoint URL actually connected to after the failover sweep |
IAlarmSource |
SubscribeAlarmsAsync (EventFilter) + AcknowledgeAsync |
Subscribes to upstream alarm/condition events and forwards acks |
IHistoryProvider |
ReadRawAsync / ReadProcessedAsync / ReadAtTimeAsync → Session.HistoryReadAsync |
Unique to this driver — passthrough history read against the upstream server |
This driver does not implement
IRediscoverable— there is no push-driven rediscovery signal from a remote OPC UA server in this driver.IHistoryProvideris implemented by no other driver; history reads for every other source route server-side throughIHistoryRouter.
History passthrough
IHistoryProvider forwards HistoryRead to the upstream server's own historian.
Raw, processed (the full HistoryAggregateType set — Average / Minimum / Maximum / Total / Count plus
Interpolative, TimeAverage, Start, End, Delta, Range, Min/Max ActualTime, Start/EndBound, DeltaBounds,
DurationGood/Bad, PercentGood/Bad, WorstQuality, NumberOfTransitions, AnnotationCount and the
population StandardDeviation/Variance — each mapped to its OPC UA Part 13 standard aggregate NodeId; #285),
and at-time reads are supported; each
returned DataValue keeps its upstream StatusCode and timestamps verbatim.
Event-history (ReadEventsAsync) is left at the interface default — the
interface doesn't yet carry the EventFilter surface needed to forward it.
Certificate trust
AutoAcceptCertificates accepts any self-signed / untrusted server certificate.
It is dev-only — leave it false in production so a MITM against the
opc.tcp channel fails closed. A revoked certificate (BadCertificateRevoked /
BadCertificateIssuerRevoked) is never auto-accepted (#277).
CertificateValidation (defaults change nothing):
| Key | Default | Meaning |
|---|---|---|
rejectUnknownRevocationStatus |
false |
Reject a server certificate whose revocation status cannot be determined (no CRL for its issuer, or a stale one). Leave false for brownfield sites without CRL infrastructure. |
issuerStorePath |
null (= %LocalAppData%/OtOpcUa/pki/issuers) |
Directory used as the trusted-issuer store. The SDK reads issuer CRLs (*.crl) from its crl/ sub-folder, so this is how an operator points the driver at a managed CRL set. |
Subscription tuning (#273)
The per-subscription parameters that used to be hardcoded are driver options; the defaults are the old values.
| Key | Default | Notes |
|---|---|---|
subscriptionPublishingInterval |
null |
null = the interval the runtime requests per subscribe (floored at 50 ms). When set it overrides that value (floor still applies); monitored-item sampling follows it. |
subscriptionKeepAliveCount |
10 |
>= 1. |
subscriptionLifetimeCount |
1000 |
Must be >= 3 x keep-alive count (Part 4 §5.13.2). |
subscriptionMaxNotificationsPerPublish |
0 |
0 = server decides. |
subscriptionPriority |
0 |
0-255. |
Out-of-range values fail InitializeAsync (ValidateTuning). The alarm subscription uses the same counts.
Per-tag tuning (deadband / sampling interval / queue size, #274) is not implemented — it needs an
ISubscribable signature change on Core.
Server OperationLimits (#275)
After connect the driver reads Server/ServerCapabilities/OperationLimits
(MaxNodesPerRead, MaxNodesPerWrite, MaxNodesPerBrowse, MaxMonitoredItemsPerCall,
MaxNodesPerHistoryReadData/Events; 0 or unreadable = no limit) and splits requests that exceed them:
ReadAsync,WriteAsyncand the discovery attribute-enrichment read go out inMaxNodesPerRead/MaxNodesPerWrite-sized chunks, results re-stitched positionally. A failed Read chunk marks only its own tagsBadCommunicationError; a Write that times out marks that chunk and every later oneBadTimeout, while earlier chunks keep their real results (those writes happened).SubscribeAsynccreates monitored items inMaxMonitoredItemsPerCallbatches.- Browse and HistoryRead already carry one node per call, so a limit
>= 1is always met and nothing is split.
honorServerOperationLimits (default true) switches it off. The limits are re-read after a successful reconnect.
Discovery URL (#278)
discoveryUrl (default null, e.g. opc.tcp://lds:4840): at InitializeAsync the driver runs FindServers on it
and GetEndpoints on every returned server, keeps the endpoints matching securityPolicy + securityMode, and
tries them before endpointUrls/endpointUrl (case-insensitive de-dup). A discovery failure is logged and
non-fatal — the static list is used. The discovery channel itself is always unsecured (spec).
Diagnostics counters (#276)
GetHealth().Diagnostics (a name -> double map, not part of the health-change fingerprint) carries:
PublishResponseCount, NotificationCount, DataChangeCount, BadStatusValueCount, PublishErrorCount,
KeepAliveMissCount, ReconnectAttemptCount, ReconnectSuccessCount, ReconnectExhaustedCount,
LastReconnectUtcTicks, ReadCallCount, ReadFailureCount, WriteCallCount, WriteFailureCount,
ChunkedRequestCount. All counters are Interlocked; they are cumulative for the driver instance's lifetime.
Testing
- Unit tests —
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/cover the session lifecycle, namespace remapping, alarm/history passthrough, and config binding against a faked SDK session. - Integration fixture — exercises the driver against a reference OPC UA server (opc-plc) on the shared docker host; see OpcUaClient-Test-Fixture.md for the coverage map.
Further reading
docs/v2/driver-specs.md §8— full per-field spec, namespace-assignment rules, and cascading-quality detail- OpcUaClient-Test-Fixture.md — test-harness map
- Client.CLI.md — general-purpose OPC UA client CLI for ad-hoc browsing of any endpoint