ba62022e88
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
187 lines
10 KiB
Markdown
187 lines
10 KiB
Markdown
# 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`](../v2/driver-specs.md),
|
|
and for the test-harness map read [OpcUaClient-Test-Fixture.md](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](../Client.CLI.md) 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
|
|
|
|
```jsonc
|
|
"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 required `UnsMappingTable`.
|
|
- `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 `StatusCode`s 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.
|
|
> `IHistoryProvider` is implemented by no other driver; history reads for every
|
|
> other source route server-side through `IHistoryRouter`.
|
|
|
|
### 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`, `WriteAsync` and the discovery attribute-enrichment read go out in `MaxNodesPerRead` /
|
|
`MaxNodesPerWrite`-sized chunks, results re-stitched positionally. A failed Read chunk marks only its own tags
|
|
`BadCommunicationError`; a Write that times out marks that chunk and every later one `BadTimeout`, while
|
|
earlier chunks keep their real results (those writes happened).
|
|
- `SubscribeAsync` creates monitored items in `MaxMonitoredItemsPerCall` batches.
|
|
- Browse and HistoryRead already carry one node per call, so a limit `>= 1` is 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](OpcUaClient-Test-Fixture.md) for the coverage map.
|
|
|
|
## Further reading
|
|
|
|
- [`docs/v2/driver-specs.md §8`](../v2/driver-specs.md) — full per-field spec,
|
|
namespace-assignment rules, and cascading-quality detail
|
|
- [OpcUaClient-Test-Fixture.md](OpcUaClient-Test-Fixture.md) — test-harness map
|
|
- [Client.CLI.md](../Client.CLI.md) — general-purpose OPC UA client CLI for
|
|
ad-hoc browsing of any endpoint
|