Merge branch 'master' into b626-626
This commit is contained in:
@@ -116,6 +116,40 @@ that `ConditionClassId` / `ConditionClassName` do too. The two class fields are
|
||||
> a live↔history join key. Correlate on `ConditionId` / the RawPath instead. (Pre-existing; the
|
||||
> live-path fix above does not change the history path.)
|
||||
|
||||
### Condition child nodes are instance-scoped (#629)
|
||||
|
||||
Every child of a materialised condition — `ActiveState`, `ActiveState/Id`, `AckedState/Id`,
|
||||
`ShelvingState/UnshelveTime`, `Message`, `Severity`, `Retain`, the method nodes, … — carries the NodeId
|
||||
**`{ConditionId}/{browse path}`** in the condition's own namespace: e.g.
|
||||
`ns=<raw>;s=Plant/Modbus/dev1/temp_hi/ShelvingState/UnshelveTime`, or
|
||||
`ns=<uns>;s=<ScriptedAlarmId>/ActiveState/Id`. So `TranslateBrowsePathsToNodeIds` from the condition
|
||||
resolves, and a direct Read or data-change subscription on a child returns **this condition's** value.
|
||||
|
||||
Before #629 the children kept ns=0 ids: `Create(…, assignNodeIds: true)` asks the node manager's
|
||||
`INodeIdFactory.New`, and the SDK default returns the node's current id — the condition type's
|
||||
instance-declaration id (`i=9189` for UnshelveTime) or an SDK-generated ns=0 number. Every translate of
|
||||
a child path returned `BadNoMatch`; a Read of a browsed child id was either `BadNodeIdUnknown` or served
|
||||
from the **type** (UnshelveTime read `0` on a condition timed-shelved for a minute). `OtOpcUaNodeManager.New`
|
||||
now derives the id. Event delivery was never affected (events copy the instance's fields; clients select
|
||||
them by type + browse path), and neither were the Part 9 methods — calling them by the type's method id
|
||||
(`MethodIds.AcknowledgeableConditionType_Acknowledge`, …) still works, because the SDK matches a method on
|
||||
its declaration id. ⚠️ The ShelvingState methods needed help here: their SDK declaration id is the
|
||||
`ShelvedStateMachineType`'s (`i=2948`…), and they were only ever found by the `AlarmConditionType_ShelvingState_*`
|
||||
id (`i=9212`…) because that WAS their NodeId. `New` adopts that original id as the method's
|
||||
`MethodDeclarationId` (a fixed set of those three ids — no address-space lookup, which would take other
|
||||
node managers' locks under ours), so OneShotShelve/TimedShelve/Unshelve by the
|
||||
`AlarmConditionType_ShelvingState_*` id keep working, as do calls by the condition's own instance method
|
||||
ids (`{ConditionId}/ShelvingState/TimedShelve`, on the condition or its ShelvingState). Calling them by the
|
||||
`ShelvedStateMachineType_*` id returns `BadMethodInvalid` — a pinned trade-off.
|
||||
|
||||
- **ACL:** the slash-joined id makes a child's ACL scope its condition's scope plus the browse path, so
|
||||
with `Security:Acl:Enabled` a child is governed exactly like its condition (a grant covering the
|
||||
condition covers its children; one that does not, denies them). Before #629 a child was in ns=0 and
|
||||
bypassed the data-plane gate entirely.
|
||||
- **Removal:** removing or rebuilding a condition forgets its whole subtree, so a removed condition's
|
||||
children answer `BadNodeIdUnknown`, not their last value.
|
||||
- Guarded over the wire by `ConditionChildNodeIdTests`.
|
||||
|
||||
### Condition source-data Quality (#477)
|
||||
|
||||
`ConditionType.Quality` reports the quality of the condition's source data. It was never assigned, and
|
||||
|
||||
@@ -431,6 +431,17 @@ RetrievalMode.Integral`) — no client-side scaling. (This replaces the retired
|
||||
likewise a native gateway mode. Bucket status codes and timestamps are preserved unchanged; empty / null
|
||||
buckets surface as `BadNoData`.
|
||||
|
||||
### Aggregate set and `BadAggregateNotSupported` (#285)
|
||||
|
||||
`HistoryAggregateType` carries the wider Part 13 set (Interpolative, TimeAverage, Start, End, Delta, Range,
|
||||
Min/MaxActualTime, Start/EndBound, DeltaBounds, DurationGood/Bad, PercentGood/Bad, WorstQuality,
|
||||
NumberOfTransitions, AnnotationCount, StandardDeviationPopulation, VariancePopulation); `OtOpcUaNodeManager.MapAggregate`
|
||||
maps each standard aggregate NodeId. The **HistorianGateway** backend only has retrieval modes for the original
|
||||
five (Average, Minimum, Maximum, Total, Count): for any other member `AggregateModeMapper` throws
|
||||
`HistoryAggregateNotSupportedException`, which the HistoryRead path turns into `BadAggregateNotSupported` for that
|
||||
node only (not a generic failure, and not counted as a backend fault). The OPC UA Client driver forwards every
|
||||
member upstream. A NodeId outside the mapped set (e.g. `StandardDeviationSample`) is still `BadAggregateNotSupported`.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **Processed and AtTime are single-shot** (no continuation points). Unlike Raw, neither
|
||||
|
||||
@@ -99,8 +99,11 @@ rather than surfacing as a runtime surprise.
|
||||
### History passthrough
|
||||
|
||||
`IHistoryProvider` forwards `HistoryRead` to the upstream server's own historian.
|
||||
Raw, processed (Average / Minimum / Maximum / Total / Count aggregates mapped to
|
||||
OPC UA Part 13 standard aggregate NodeIds), and at-time reads are supported; each
|
||||
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.
|
||||
@@ -109,7 +112,67 @@ interface doesn't yet carry the EventFilter surface needed to forward it.
|
||||
|
||||
`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.
|
||||
opc.tcp channel fails closed. The SDK rejects a **revoked** certificate
|
||||
(`BadCertificateRevoked` / `BadCertificateIssuerRevoked`) before the auto-accept handler runs, and the handler
|
||||
also refuses them explicitly — defence in depth (#277), not a behaviour change.
|
||||
|
||||
`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. **Only effective with `autoAcceptCertificates` off** — the auto-accept handler accepts every suppressible error, including revocation-unknown. |
|
||||
| `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` | A .NET `TimeSpan` string (`"00:00:02"`), not a number. `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 (and a non-absolute `discoveryUrl`) are rejected when the options are loaded — driver creation / `InitializeAsync` fails once with a clear message rather than the driver faulting and retrying forever. 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)
|
||||
|
||||
Nothing driver-side: the SDK session already honours the remote server's
|
||||
`ServerCapabilities/OperationLimits`. `Opc.Ua.Client.Session` derives from `Opc.Ua.SessionClientBatched`,
|
||||
fetches the limits on open (`FetchOperationLimitsAsync`) and splits Read / Write / CreateMonitoredItems / Browse /
|
||||
HistoryRead requests that exceed them (a limit of `0` = unlimited). The driver issues one logical call per
|
||||
operation and a unit test pins the `Session : SessionClientBatched` relationship so an SDK change that dropped
|
||||
it is caught. (A driver-side splitter was tried and removed: it duplicated the SDK and, on a stalled link, held
|
||||
the driver gate for chunks x timeout.)
|
||||
|
||||
### 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, keeping the endpoints matching `securityPolicy` + `securityMode`.
|
||||
Discovered URLs are tried **after** `endpointUrls`/`endpointUrl` (case-insensitive de-dup) — discovery can add
|
||||
fallbacks but never outranks or replaces the configured server. Because an LDS commonly registers several servers
|
||||
(identical line PLCs, a gateway plus a historian) and the first to accept a connection would be bound silently:
|
||||
|
||||
- `discoveryServerUri` set: only endpoints of the server with that ApplicationUri are used.
|
||||
- unset and exactly one distinct server discovered: that server is used.
|
||||
- unset and more than one: the connect attempt **fails** with an error naming the discovered ApplicationUris and
|
||||
telling the operator to set `discoveryServerUri`.
|
||||
|
||||
A discovery *network* failure (LDS down) is logged and non-fatal — the static list is used (note it is retried on
|
||||
every `InitializeAsync`, including comms-loss reconnects). The discovery channel itself is always unsecured (spec).
|
||||
|
||||
### Diagnostics counters (#276)
|
||||
|
||||
Session/publish events are published on the shared OTel meter as the counter
|
||||
`otopcua.driver.opcuaclient.events`, tagged `driver_instance` (the driver instance id) and `event` =
|
||||
`publish_response`, `notification` (items), `data_change`, `bad_status_value`, `publish_error`,
|
||||
`keepalive_miss`, `reconnect_attempt`, `reconnect_success`, `reconnect_exhausted`. A climbing `keepalive_miss` /
|
||||
`reconnect_attempt` rate is the signature of a flapping upstream. They are not part of `DriverHealth` or the
|
||||
`/hosts` panel.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
Reference in New Issue
Block a user