Merge hw3-history (#655 delete dead driver-side history capability + client surfaces Bad HistoryRead)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -195,6 +195,10 @@ Reads historical data from a node. Supports raw history reads and aggregate (pro
|
||||
`--start` and `--end` are parsed with `CultureInfo.InvariantCulture` and treated as UTC; supply
|
||||
them in ISO 8601 UTC form (`YYYY-MM-DDTHH:MM:SSZ`) for unambiguous behaviour across hosts.
|
||||
|
||||
A Bad per-node status from the server (for example `BadHistoryOperationUnsupported` for a node that is not
|
||||
historized, or `BadUserAccessDenied`) fails the command with that status named. It is never printed as
|
||||
`0 values returned`; only `BadNoData` and Good/GoodNoData read as an empty result.
|
||||
|
||||
```bash
|
||||
# Raw history
|
||||
otopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
|
||||
|
||||
+7
-11
@@ -10,7 +10,7 @@ projects, the Runtime, and the ControlPlane. Each subsection below gives the
|
||||
purpose, the key members, and where it is implemented/used.
|
||||
|
||||
The capability interfaces a driver opts into (`IReadable`, `IWritable`,
|
||||
`ITagDiscovery`, `ISubscribable`, `IAlarmSource`, `IHistoryProvider`,
|
||||
`ITagDiscovery`, `ISubscribable`, `IAlarmSource`,
|
||||
`IHostConnectivityProbe`, `IPerCallHostResolver`, `IRediscoverable`) are
|
||||
covered elsewhere and discovered by the server via `is`-checks on the
|
||||
`IDriver` instance. The interfaces here are the *plumbing* the server uses to
|
||||
@@ -560,18 +560,14 @@ the interface currently has the contract defined but no implementations.
|
||||
|
||||
The server-side historian read surface. Registered with the server's history
|
||||
router and resolved **per OPC UA namespace**, independent of any driver's
|
||||
lifecycle. This is distinct from the driver capability `IHistoryProvider`:
|
||||
|
||||
- `IHistoryProvider` is a *driver capability* — the server dispatches to it via
|
||||
the driver instance.
|
||||
- `IHistorianDataSource` is a *server registration* — the server resolves it by
|
||||
namespace and calls it directly, so one historian (the HistorianGateway) can
|
||||
serve many drivers' nodes, and drivers can restart without dropping history
|
||||
availability.
|
||||
lifecycle. It is a *server registration*, not a driver capability: drivers carry
|
||||
no history surface (there is no `IHistoryProvider`; deleted in Gitea #655). The
|
||||
server resolves the data source by namespace and calls it directly, so one
|
||||
historian (the HistorianGateway) can serve many drivers' nodes, and drivers can
|
||||
restart without dropping history availability.
|
||||
|
||||
The interface is `: IDisposable` and declares the full read surface as
|
||||
**required** members (unlike `IHistoryProvider`, where at-time/event reads are
|
||||
optional default-impl methods so legacy drivers can stay raw-only):
|
||||
**required** members:
|
||||
|
||||
- `ReadRawAsync(fullReference, startUtc, endUtc, maxValuesPerNode, ct)` — raw
|
||||
historical samples over a time range.
|
||||
|
||||
+9
-11
@@ -364,18 +364,16 @@ HistorianGateway using that source. (Alarm-history `ReadEvents` requires the gat
|
||||
`BaseEventType` select clauses — `EventId`, `SourceName`, `Time`, `ReceiveTime`, `Message`,
|
||||
and `Severity`; an unsupported select operand returns a null field (spec-conformant).
|
||||
|
||||
### OpcUaClient driver — upstream passthrough for all four variants
|
||||
### No driver-side history (Gitea #655)
|
||||
|
||||
The OpcUaClient driver's `IHistoryProvider` implementation forwards **all four** history-read
|
||||
variants (Raw, Processed, AtTime, and Events) to its upstream OPC UA server. For the Events
|
||||
variant it sends a fixed canonical `BaseEventType` `EventFilter` selecting the standard six
|
||||
fields (`EventId`, `SourceName`, `Time`, `ReceiveTime`, `Message`, `Severity`) and maps the
|
||||
upstream `HistoryEvent` onto `HistoricalEvent` — the same six-field projection the OtOpcUa
|
||||
node-manager itself projects when serving event history. This is a **driver-level capability**:
|
||||
the OpcUaClient driver acts as a passthrough to whatever historian the upstream server exposes,
|
||||
and is independent of the single server-side `IHistorianDataSource` backend
|
||||
(`GatewayHistorianDataSource` / `NullHistorianDataSource`) that the OtOpcUa node-manager
|
||||
dispatches HistoryRead to for tags on other drivers (Galaxy, Modbus, S7, etc.).
|
||||
History is served **only** by the server-wide `IHistorianDataSource`
|
||||
(`GatewayHistorianDataSource` / `NullHistorianDataSource`). No driver contributes
|
||||
history: the OpcUaClient driver does **not** proxy its upstream server's
|
||||
HistoryRead (the former `IHistoryProvider` passthrough was never dispatched to and
|
||||
was deleted). On an OpcUaClient tag, `isHistorized` means "OtOpcUa records this
|
||||
value into the historian from now on", not "forward history reads upstream". A
|
||||
node without `isHistorized` has no HistoryRead access bit and answers
|
||||
`BadHistoryOperationUnsupported`.
|
||||
|
||||
### Graceful degradation
|
||||
|
||||
|
||||
+1
-2
@@ -27,7 +27,7 @@ The lifecycle facade `OpcUaApplicationHost` (`src/Server/ZB.MOM.WW.OtOpcUa.OpcUa
|
||||
|
||||
## Resilience and capability dispatch
|
||||
|
||||
Driver-capability calls (`IReadable.ReadAsync`, `IWritable.WriteAsync`, `ITagDiscovery.DiscoverAsync`, `ISubscribable.SubscribeAsync/UnsubscribeAsync`, the `IHostConnectivityProbe` probe loop, `IAlarmSource` surfaces, and the four `IHistoryProvider` reads) are routed through a `CapabilityInvoker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs`) so the Polly resilience pipeline (retry / timeout / breaker) applies. There is one invoker per `(DriverInstance, IDriver)` pair; all invokers share the process-singleton `DriverResiliencePipelineBuilder`, which keys pipelines on `(DriverInstanceId, hostName, DriverCapability)`. Per-instance resilience options come from the driver's tier — the `DriverTier` passed at factory registration, resolved via `DriverFactoryRegistry.GetTier` (every driver is Tier A today, since no factory passes one) — plus per-instance JSON overrides parsed from `DriverInstance.ResilienceConfig` by `DriverResilienceOptionsParser`. (It was never `DriverTypeRegistry`; that class was vestigial and was deleted in Gitea #522.)
|
||||
Driver-capability calls (`IReadable.ReadAsync`, `IWritable.WriteAsync`, `ITagDiscovery.DiscoverAsync`, `ISubscribable.SubscribeAsync/UnsubscribeAsync`, the `IHostConnectivityProbe` probe loop, `IAlarmSource` surfaces) are routed through a `CapabilityInvoker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs`) so the Polly resilience pipeline (retry / timeout / breaker) applies. There is one invoker per `(DriverInstance, IDriver)` pair; all invokers share the process-singleton `DriverResiliencePipelineBuilder`, which keys pipelines on `(DriverInstanceId, hostName, DriverCapability)`. Per-instance resilience options come from the driver's tier — the `DriverTier` passed at factory registration, resolved via `DriverFactoryRegistry.GetTier` (every driver is Tier A today, since no factory passes one) — plus per-instance JSON overrides parsed from `DriverInstance.ResilienceConfig` by `DriverResilienceOptionsParser`. (It was never `DriverTypeRegistry`; that class was vestigial and was deleted in Gitea #522.)
|
||||
|
||||
The `OTOPCUA0001` Roslyn analyzer (`src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs`, category `OtOpcUa.Resilience`, severity Warning) flags direct driver-capability calls that bypass the invoker.
|
||||
|
||||
@@ -37,7 +37,6 @@ The `OTOPCUA0001` Roslyn analyzer (`src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/Unwr
|
||||
| Write | `IWritable.WriteAsync` | `ExecuteWriteAsync(host, isIdempotent, …)` — disables retries for non-idempotent writes per `WriteIdempotentAttribute` / decisions #44-45, #143 |
|
||||
| Discovery | `ITagDiscovery.DiscoverAsync` | `ExecuteAsync(DriverCapability.Discover, host, …)` |
|
||||
| Subscribe / Unsubscribe | `ISubscribable.SubscribeAsync/UnsubscribeAsync` | `ExecuteAsync(DriverCapability.Subscribe, host, …)` |
|
||||
| HistoryRead (raw / processed / at-time / events) | `IHistoryProvider.*Async` | `ExecuteAsync(DriverCapability.HistoryRead, host, …)` |
|
||||
| Alarm subscribe / unsubscribe / acknowledge | `IAlarmSource.SubscribeAlarmsAsync/UnsubscribeAlarmsAsync/AcknowledgeAsync` | via `AlarmSurfaceInvoker` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs`), which fans out per host |
|
||||
|
||||
The host name fed to the invoker comes from `IPerCallHostResolver.ResolveHost(fullReference)` when the driver implements it (multi-host drivers: AB CIP, Modbus, FOCAS, TwinCAT, AB Legacy resolve per device). Single-host drivers fall back to `DriverInstanceId`, preserving the per-instance pipeline-key semantics (decision #144).
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ The project was originally called **LmxOpcUa** (a single-driver Galaxy/MXAccess
|
||||
> **v2 (2026-05-26):** the separate `OtOpcUa.Server` + `OtOpcUa.Admin` services fused into a single role-gated `OtOpcUa.Host` binary, joined by an Akka.NET cluster. See [v2 design](plans/2026-05-26-akka-hosting-alignment-design.md) for the architectural decision.
|
||||
|
||||
- **Core** owns shared abstractions (driver capability contracts, scripting, virtual tags, alarm historian).
|
||||
- **Drivers** plug in via capability interfaces in `ZB.MOM.WW.OtOpcUa.Core.Abstractions`: `IDriver`, `IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider`, `IPerCallHostResolver`. Each driver opts into whichever it supports.
|
||||
- **Drivers** plug in via capability interfaces in `ZB.MOM.WW.OtOpcUa.Core.Abstractions`: `IDriver`, `IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IPerCallHostResolver`. Each driver opts into whichever it supports.
|
||||
- **Host** (`src/Server/ZB.MOM.WW.OtOpcUa.Host`) is the single fused binary (.NET 10, AnyCPU). `OTOPCUA_ROLES` env decides what to mount: `admin` (Blazor + control-plane singletons), `driver` (OPC UA endpoint + per-node actors), or both. See [ServiceHosting.md](ServiceHosting.md).
|
||||
- **Cluster + ControlPlane + Runtime + AdminUI + Security** sit between Core and Host. The cluster glues per-node actors into one logical fleet; the control-plane singletons (deploy coordinator, audit writer, redundancy state) live on the admin role-leader. See [Redundancy.md](Redundancy.md).
|
||||
- The Galaxy driver still reaches MXAccess via gRPC to a separately-installed **mxaccessgw** sidecar (sibling repo).
|
||||
@@ -33,7 +33,7 @@ The project was originally called **LmxOpcUa** (a single-driver Galaxy/MXAccess
|
||||
| [AlarmHistorian.md](AlarmHistorian.md) | `Core.AlarmHistorian` store-and-forward sink — `LocalDbStoreAndForwardSink`, the replicated `alarm_sf_events` buffer + Primary-only drain, `IAlarmHistorianWriter`, dead-letter/retry/eviction |
|
||||
| [DataTypeMapping.md](v1/DataTypeMapping.md) | Per-driver `DriverAttributeInfo` → OPC UA variable types (v1 archive — live mapping is in `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs`) |
|
||||
| [IncrementalSync.md](IncrementalSync.md) | Address-space rebuild on redeploy + `sp_ComputeGenerationDiff` |
|
||||
| [HistoricalDataAccess.md](v1/HistoricalDataAccess.md) | `IHistoryProvider` as a per-driver optional capability (v1 archive) |
|
||||
| [HistoricalDataAccess.md](v1/HistoricalDataAccess.md) | Historical v1 design: history as a per-driver capability (retired; history is now server-side, see [Historian.md](Historian.md)) |
|
||||
| [VirtualTags.md](VirtualTags.md) | `Core.Scripting` + `Core.VirtualTags` — Roslyn script sandbox, engine, dispatch alongside driver tags |
|
||||
| [ScriptEditor.md](ScriptEditor.md) | Monaco script editor — Roslyn-backed IntelliSense (completions, diagnostics, hover, tag-path) for C# virtual-tag scripts; `AdminUI/ScriptAnalysis/` backend |
|
||||
| [ScriptedAlarms.md](ScriptedAlarms.md) | `Core.ScriptedAlarms` — script-predicate `IAlarmSource` + Part 9 state machine |
|
||||
|
||||
@@ -95,7 +95,7 @@ with `BadUserAccessDenied`.
|
||||
Four overrides — `HistoryReadRawModified`, `HistoryReadProcessed`, `HistoryReadAtTime`,
|
||||
`HistoryReadEvents` — route to the **server-wide** `IHistorianDataSource` (the HistorianGateway-backed
|
||||
reader, or `NullHistorianDataSource` when `ServerHistorian:Enabled=false`, which returns `GoodNoData`).
|
||||
This is not a per-driver `IHistoryProvider` dispatch.
|
||||
Drivers carry no history capability.
|
||||
|
||||
**No authorization check runs on any of the four.** Unlike `OnWriteValue`, the SDK does *not* hold the
|
||||
node-manager `Lock` while invoking them, so these paths may await.
|
||||
|
||||
@@ -84,7 +84,7 @@ multiple gateways is not.
|
||||
### 4. Alarms / history
|
||||
|
||||
PCCC has no alarm object + no history object. Driver doesn't implement
|
||||
`IAlarmSource` or `IHistoryProvider` — no test coverage is the correct shape.
|
||||
`IAlarmSource` (history is server-side for every driver) — no test coverage is the correct shape.
|
||||
|
||||
### 5. File-type coverage
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ For the driver spec (capability surface, config shape, addressing), see [docs/v2
|
||||
+---------------------------------------+
|
||||
```
|
||||
|
||||
History reads moved server-side in PR 7.2 (`IHistoryRouter`). Galaxy no longer implements `IHistoryProvider` of its own.
|
||||
History reads are served server-side by `IHistorianDataSource` (HistorianGateway); Galaxy has no history capability of its own.
|
||||
|
||||
`IAlarmSource` was retired with PR 7.2 and **restored in PR B.2** of the
|
||||
alarms-over-gateway epic ([docs/plans/alarms-over-gateway.md](../plans/alarms-over-gateway.md)).
|
||||
|
||||
@@ -87,8 +87,8 @@ boundary splits, etc.).
|
||||
|
||||
### 5. Alarms / history
|
||||
|
||||
Not a Modbus concept. Driver doesn't implement `IAlarmSource` or
|
||||
`IHistoryProvider`; no test coverage is the correct shape.
|
||||
Not a Modbus concept. Driver doesn't implement `IAlarmSource` (no driver has a history
|
||||
capability; history is server-side); no test coverage is the correct shape.
|
||||
|
||||
## When to trust the Modbus fixture, when to reach for a rig
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ coverage map, see [Modbus-Test-Fixture.md](Modbus-Test-Fixture.md).
|
||||
## Capability Surface
|
||||
|
||||
`ModbusDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable`
|
||||
(`Driver.Modbus/ModbusDriver.cs`). There is **no `IAlarmSource`** and no
|
||||
`IHistoryProvider` — the Modbus protocol expresses neither, so those capabilities
|
||||
are out of scope by design.
|
||||
(`Driver.Modbus/ModbusDriver.cs`). There is **no `IAlarmSource`** — the Modbus protocol has no
|
||||
alarm object, so that capability is out of scope by design. (No driver carries a
|
||||
history capability; history is server-side.)
|
||||
|
||||
| Capability | Implementation entry point | Notes |
|
||||
|------------|---------------------------|-------|
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed class MqttDriver
|
||||
| `IHostConnectivityProbe` | `GetHostStatuses` | 1-second poll of `MqttConnection.State`; `HostName` is `"{host}:{port}"` |
|
||||
| `IRediscoverable` | `OnRediscoveryNeeded` | Nothing raises it in Plain mode. **Sparkplug raises it** when a birth for an *authored* scope carries a metric-name set different from the last one seen for that scope — both conditions are anti-storm, see [Rediscovery](#rediscovery-sparkplug) |
|
||||
|
||||
**Not implemented:** `IPerCallHostResolver`, `IAlarmSource`, `IHistoryProvider`.
|
||||
**Not implemented:** `IPerCallHostResolver`, `IAlarmSource`.
|
||||
Materialized nodes are `IsAlarm: false`, `IsHistorized: false`. A node is
|
||||
`SecurityClassification.Operate` when its tag declares a write destination and
|
||||
`ViewOnly` when it does not — the browse-time answer comes from the same encoder the write path uses,
|
||||
|
||||
@@ -58,7 +58,7 @@ Wire-level surfaces verified: `IDriver` + `IReadable` + `ISubscribable` +
|
||||
against the in-process OtOpcUa server (`ServerEventRig`, authenticating with the rig's API-key credential):
|
||||
read (+ missing node), write through the node-write gateway (+ read-only refusal, + failed-write revert),
|
||||
subscribe/unsubscribe, host-status Running -> not-Running when the server stops, and discovery. These need
|
||||
no Docker. The HistoryRead wire leg is deferred (needs an `IHistorianDataSource` stub on the server).
|
||||
no Docker. There is no HistoryRead leg: the driver has no history capability (#655).
|
||||
|
||||
### Unit
|
||||
|
||||
@@ -82,11 +82,10 @@ mirrors the full capability matrix):
|
||||
driver config
|
||||
- `OpcUaClientAlarmTests` — A&E severity bucket (1–1000 → Low / Medium /
|
||||
High / Critical), subscribe / unsubscribe / ack contract
|
||||
- `OpcUaClientHistoryTests` — historical data read + interpolation contract
|
||||
|
||||
Capability surfaces whose contract is verified: `IDriver`, `ITagDiscovery`,
|
||||
`IReadable`, `IWritable`, `ISubscribable`, `IHostConnectivityProbe`,
|
||||
`IAlarmSource`, `IHistoryProvider`.
|
||||
`IAlarmSource`.
|
||||
|
||||
## What it does NOT cover
|
||||
|
||||
@@ -113,15 +112,7 @@ The lossy window is not characterized.
|
||||
session — the shape that breaks publish-queue-size tuning in the wild — is
|
||||
not exercised.
|
||||
|
||||
### 4. Real historian mappings
|
||||
|
||||
`IHistoryProvider.ReadRawAsync` + `ReadProcessedAsync` +
|
||||
`ReadAtTimeAsync` + `ReadEventsAsync` are contract-mocked. Against a real
|
||||
historian (AVEVA Historian, Prosys historian, Kepware LocalHistorian) each
|
||||
has specific interpolation + bad-quality-handling quirks the contract test
|
||||
doesn't see.
|
||||
|
||||
### 5. Real A&E events
|
||||
### 4. Real A&E events
|
||||
|
||||
Alarm subscription is mocked via filtered monitored items; the actual
|
||||
`EventFilter` select-clause behavior against a server that exposes typed
|
||||
@@ -187,8 +178,8 @@ Beyond that:
|
||||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml`
|
||||
— `mcr.microsoft.com/iotedge/opc-plc:2.14.10` with `--ut --aa --alm --pn=50000`
|
||||
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs` — ctor +
|
||||
session-factory seam tests mock through; implements `IAlarmSource` +
|
||||
`IHistoryProvider` (unique among drivers); does NOT implement `IRediscoverable`
|
||||
session-factory seam tests mock through; implements `IAlarmSource`;
|
||||
does NOT implement `IRediscoverable`
|
||||
- `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs` —
|
||||
the v2 dual-endpoint integration harness a future loopback client test could
|
||||
piggyback on (v1 `OpcUaServerIntegrationTests.cs` retired with the v1 server project)
|
||||
|
||||
+11
-29
@@ -79,7 +79,7 @@ initialise (the AdminUI form drops them on the next save). Pinned by
|
||||
|
||||
## Capability surface
|
||||
|
||||
`OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IAlarmSource, IHistoryProvider`
|
||||
`OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IAlarmSource`
|
||||
(`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs:31`).
|
||||
|
||||
| Capability | Path | Notes |
|
||||
@@ -90,7 +90,6 @@ initialise (the AdminUI form drops them on the next save). Pinned by
|
||||
| `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 |
|
||||
|
||||
> **Native alarms (#619).** An alarm raw tag is `TagConfig` `{ "nodeId": "<upstream SourceNode>", "alarm": {...} }`.
|
||||
> The host subscribes with RawPaths and routes on (driver, RawPath), so the driver matches each
|
||||
@@ -112,35 +111,18 @@ initialise (the AdminUI form drops them on the next save). Pinned by
|
||||
|
||||
> 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
|
||||
### History: not supported (Gitea #655)
|
||||
|
||||
`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.
|
||||
**The per-node `HistoryReadResult.StatusCode` is honoured (#649).** The driver never reads a Bad node status as
|
||||
empty data: `BadAggregateNotSupported` on a Processed read throws `HistoryAggregateNotSupportedException`, and any
|
||||
other Bad status (`BadUserAccessDenied`, `BadNodeIdUnknown`, `BadHistoryOperationUnsupported`, ...) throws
|
||||
`HistoryReadStatusException` carrying the code (the constructor rejects non-Bad codes). `Good`, `GoodNoData`,
|
||||
`GoodMoreData` (continuation point) and `Uncertain` are unchanged, and a node-level `BadNoData` is still an empty
|
||||
result. The status is matched on its code bits, so an info/overflow sub-status does not dodge the match.
|
||||
|
||||
⚠️ **This is currently latent.** The OtOpcUa server does not dispatch HistoryRead to a driver's `IHistoryProvider`
|
||||
(it serves only the server-wide `IHistorianDataSource`, see `docs/Historian.md`), so no OPC UA client of OtOpcUa
|
||||
sees this driver's history results or its refusals today. The server's HistoryRead arms (`ServeNodeAsync`,
|
||||
`ServeRawPagedAsync`) do map a `HistoryReadStatusException` thrown by an `IHistorianDataSource` to that node's
|
||||
status verbatim, and the driver/wire tests exercise that path; if driver history is ever routed to the server,
|
||||
refusals will reach clients correctly instead of as empty Good data. A routed deterministic refusal would also
|
||||
need excluding from the HistoryRead pipeline's retry/breaker handling.
|
||||
|
||||
Event-history (`ReadEventsAsync`) is left at the interface default — the
|
||||
interface doesn't yet carry the EventFilter surface needed to forward it.
|
||||
This driver does **not** proxy the upstream server's HistoryRead. The server serves
|
||||
HistoryRead only through the server-wide `IHistorianDataSource` (see `docs/Historian.md`);
|
||||
drivers carry no history capability, and the former `IHistoryProvider` passthrough
|
||||
(Raw / Processed / AtTime / Events) was never dispatched to and has been deleted.
|
||||
`isHistorized` on an OpcUaClient tag means OtOpcUa records the value into the historian
|
||||
from now on (numeric only), not that upstream history is forwarded. A tag without
|
||||
`isHistorized` has no HistoryRead access bit and answers `BadHistoryOperationUnsupported`.
|
||||
If upstream-history proxying is wanted later, it needs a new intent key, a dispatch path
|
||||
through `DriverHostActor`, and a continuation-point rework — see #655.
|
||||
|
||||
### Certificate trust
|
||||
|
||||
|
||||
@@ -9,10 +9,9 @@ OtOpcUa is a multi-driver OPC UA server. The Core (`ZB.MOM.WW.OtOpcUa.Core` + `C
|
||||
- `IHostConnectivityProbe` — per-host reachability events
|
||||
- `IPerCallHostResolver` — multi-host drivers that route each call to a target endpoint at dispatch time
|
||||
- `IAlarmSource` — driver-emitted OPC UA A&C events
|
||||
- `IHistoryProvider` — driver-side raw / processed / at-time / events HistoryRead (see [HistoricalDataAccess.md](../v1/HistoricalDataAccess.md))
|
||||
- `IRediscoverable` — driver-initiated address-space rebuild notifications
|
||||
- `ISkippedTagReporter` — authored tags the driver could not build into its poll table (see [Skipped tags](#skipped-tags) below)
|
||||
- `IHistorianDataSource` — server-side historian read backend registration (the HistorianGateway backend), distinct from the driver-side `IHistoryProvider` HistoryRead path
|
||||
- `IHistorianDataSource` — server-side historian read backend registration (the HistorianGateway backend) (drivers carry no history capability; Gitea #655)
|
||||
|
||||
Each driver opts into only the capabilities it supports. Every async capability call at the Server dispatch layer goes through `CapabilityInvoker` (`Core/Resilience/CapabilityInvoker.cs`), which wraps it in a Polly pipeline keyed on `(DriverInstanceId, HostName, DriverCapability)`. The `OTOPCUA0001` analyzer enforces the wrap at build time. Drivers themselves never depend on Polly; they just implement the capability interface and let the Core wrap it.
|
||||
|
||||
@@ -71,7 +70,7 @@ Three properties worth knowing:
|
||||
| [AB Legacy](AbLegacy.md) | `Driver.AbLegacy` | A | libplctag PCCC | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver | SLC 500 / MicroLogix. File-based addressing (`N7:0`, `F8:0`) — no symbol table, tag list is user-authored in the config DB |
|
||||
| [TwinCAT](TwinCAT.md) | `Driver.TwinCAT` | B | Beckhoff `TwinCAT.Ads` (`TcAdsClient`) | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable | The only native-notification driver outside Galaxy — ADS delivers `ValueChangedCallback` events the driver forwards straight to `ISubscribable.OnDataChange` without polling. Symbol tree uploaded via `SymbolLoaderFactory` |
|
||||
| [FOCAS](FOCAS.md) | `Driver.FOCAS` | A | Pure-managed `FocasWireClient` — FOCAS/2 Ethernet binary protocol on TCP:8193, inlined into the driver assembly | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource | `IWritable` is implemented but read-only by design — `WriteAsync` returns `BadNotWritable` for every point. CNC-shaped data model (axes, spindle, PMC, macros, alarms) not a flat tag map. Previously Tier-C (Host + P/Invoke + shim DLL); retired in the 2026-04-24 migration when the managed wire client landed |
|
||||
| [OPC UA Client](OpcUaClient.md) | `Driver.OpcUaClient` | B | OPCFoundation `Opc.Ua.Client` | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IAlarmSource, IHistoryProvider, IHostConnectivityProbe | Gateway/aggregation driver — the only driver implementing driver-side `IHistoryProvider` (forwards HistoryRead to the upstream server). Opens a single `Session` against a remote OPC UA server and re-exposes its address space. Owns its own `ApplicationConfiguration` (distinct from `Client.Shared`) because it's always-on with keep-alive + `TransferSubscriptions` across SDK reconnect, not an interactive CLI |
|
||||
| [OPC UA Client](OpcUaClient.md) | `Driver.OpcUaClient` | B | OPCFoundation `Opc.Ua.Client` | IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IAlarmSource, IHostConnectivityProbe | Gateway/aggregation driver — does not proxy upstream history (no driver has a history capability). Opens a single `Session` against a remote OPC UA server and re-exposes its address space. Owns its own `ApplicationConfiguration` (distinct from `Client.Shared`) because it's always-on with keep-alive + `TransferSubscriptions` across SDK reconnect, not an interactive CLI |
|
||||
| [MQTT](Mqtt.md) | `Driver.Mqtt` (+ `.Browser`, `.Contracts`) | A | [MQTTnet 5](https://github.com/dotnet/MQTTnet) | IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe, IRediscoverable | **Push, not poll, and read-only** — the broker delivers PUBLISH frames the driver forwards straight to `ISubscribable.OnDataChange`; `IReadable` serves the last retained/received value from cache, and there is **no `IWritable`** (nothing publishes back). Subscriptions are indexed **by topic filter, not by topic**, so wildcard tags survive a reconnect. A rejected CONNACK throws `MqttConnectRejectedException`: unrecoverable codes → `Faulted` + supervisor stop, transient → retry. **Two modes:** `Plain` binds a tag to a concrete topic + JSON path; `SparkplugB` decodes vendored Eclipse-Tahu protobuf under one `spBv1.0/{groupId}/#` subscription and binds by the `group/edgeNode/device?/metric` tuple (birth/alias/seq-gap state machine, death→STALE, scoped rebirth NCMD). `IRediscoverable` fires on a DBIRTH but is **inert platform-side** — see [#507](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/507) |
|
||||
| [MTConnect](MTConnect.md) | `Driver.MTConnect` (+ `.Contracts`) | — | Hand-rolled `System.Xml.Linq` HTTP/XML client against a vendor-neutral MTConnect Agent (`/probe`, `/current`, `/sample`) — no TrakHound dependency (dropped; see the driver doc) | IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe, IRediscoverable | Read-only by design (no `IWritable`). Production data plane is entirely `ISubscribable`/`OnDataChange` — `IReadable` is CLI/test-only. Browse is free via the Wave-0 universal discovery browser, no bespoke browser project. `IRediscoverable`/`IHostConnectivityProbe` have no server-side consumer yet — a pre-existing fleet-wide gap, not MTConnect-specific |
|
||||
| [Sql](Sql.md) | `Driver.Sql` (+ `.Browser`, `.Contracts`) | A | `Microsoft.Data.SqlClient` behind an `ISqlDialect` seam (`SqlServerDialect` shipped; Postgres/ODBC deferred) | IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe | Read-only DB-poll driver with a bespoke schema browser. **Credentials are never persisted** — the deploy gate rejects a stored `connectionString` (#498) and catalog identifiers are validated against the live catalog's own spelling (#496). `SupportsOnlineDiscovery => false` by design (the bespoke browser wins). `SqlTagModel.Query` is deferred to P3 and rejected end-to-end by parser, validator, planner and editor. Merged `4ad54037` |
|
||||
@@ -93,7 +92,7 @@ Three properties worth knowing:
|
||||
- [AbLegacy.md](AbLegacy.md) — AB Legacy PCCC driver (SLC 500 / MicroLogix / PLC-5): file-based addressing, user-authored tag list
|
||||
- [S7.md](S7.md) — Siemens S7 driver (S7-300/400/1200/1500 + S7-200): getting started, config, data-block addressing, serialized single-connection model
|
||||
- [TwinCAT.md](TwinCAT.md) — Beckhoff TwinCAT (ADS) driver: getting started, native-notification subscription, symbol-tree upload
|
||||
- [OpcUaClient.md](OpcUaClient.md) — OPC UA Client (gateway/aggregation) driver: remote-server session, driver-side HistoryRead forwarding, reconnect behaviour
|
||||
- [OpcUaClient.md](OpcUaClient.md) — OPC UA Client (gateway/aggregation) driver: remote-server session, reconnect behaviour
|
||||
- [Mqtt.md](Mqtt.md) — MQTT broker-subscribe driver: broker/TLS/auth config, topic + JSON-path tag binding, retained-value seeding, `#`-observation browser, CONNACK-rejection semantics, **Sparkplug B** (birth/alias/seq/rebirth state machine, birth-driven browse picker, death→STALE), and the **Known gaps** table
|
||||
|
||||
- **MTConnect** has a short getting-started doc because the hand-rolled-vs-TrakHound divergence, the
|
||||
@@ -122,7 +121,7 @@ Each driver has a dedicated fixture doc that lays out what the integration / uni
|
||||
|
||||
## Related cross-driver docs
|
||||
|
||||
- [HistoricalDataAccess.md](../v1/HistoricalDataAccess.md) — `IHistoryProvider` dispatch, aggregate mapping, continuation points. The OPC UA Client driver is the only driver that implements driver-side `IHistoryProvider` (it forwards HistoryRead to the upstream server); the AVEVA Historian path is served server-side by the HistorianGateway-backed `IHistorianDataSource` instead. Other drivers do not implement the interface and return `BadHistoryOperationUnsupported`.
|
||||
- [HistoricalDataAccess.md](../v1/HistoricalDataAccess.md) — v1 archive of the retired per-driver history design (aggregate mapping, continuation points). History is now served only by the server-side `IHistorianDataSource` (HistorianGateway); no driver implements a history capability, and nodes without `isHistorized` return `BadHistoryOperationUnsupported`.
|
||||
- [AlarmTracking.md](../AlarmTracking.md) — `IAlarmSource` event model and filtering. Implemented by Galaxy (native MxAccess alarms, working end-to-end), OPC UA Client, AB CIP, and FOCAS; AB Legacy, Modbus, S7, TwinCAT, and MQTT have no alarm source.
|
||||
- [Subscriptions.md](../v1/Subscriptions.md) — how the Server multiplexes subscriptions onto `ISubscribable.OnDataChange`.
|
||||
- [docs/v2/driver-stability.md](../v2/driver-stability.md) — historical design record of the A / B / C tier model. Only `DriverTier` + the `CapabilityPolicy` defaults per tier × capability (`DriverResilienceOptions.GetTierDefaults`) are live; its `MemoryTracking` / recycle / out-of-process isolation machinery was deleted (#522) and every driver runs Tier A.
|
||||
|
||||
@@ -155,7 +155,7 @@ TC3; TC2 compatibility is not exercised.
|
||||
|
||||
### 5. Alarms / history
|
||||
|
||||
Driver doesn't implement `IAlarmSource` or `IHistoryProvider` — not in scope
|
||||
Driver doesn't implement `IAlarmSource` (history is server-side for every driver) — not in scope
|
||||
for this driver family. TwinCAT 3's TcEventLogger could theoretically back
|
||||
an `IAlarmSource`, but shipping that is a separate feature.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The system shall expose an OPC UA server endpoint that OPC UA clients can connec
|
||||
|
||||
## HLR-002: Multi-Driver Plug-In Model
|
||||
|
||||
The system shall support pluggable driver modules that bind to specific data sources. v2.0 ships seven drivers: Galaxy (AVEVA System Platform via MXAccess), Modbus TCP (including DL205 via `AddressFormat=DL205`), Allen-Bradley CIP (ControlLogix/CompactLogix), Allen-Bradley Legacy (SLC/MicroLogix via PCCC), Siemens S7, Beckhoff TwinCAT (ADS), FANUC FOCAS, and OPC UA Client (aggregation/gateway). Drivers implement only the capability interfaces (`IDriver`, `ITagDiscovery`, `IReadable`, `IWritable`, `ISubscribable`, `IAlarmSource`, `IHistoryProvider`, `IHostConnectivityProbe`, `IPerCallHostResolver`, `IRediscoverable`) defined in `ZB.MOM.WW.OtOpcUa.Core.Abstractions` that apply to their protocol. Multiple instances of the same driver type are supported; each instance binds to its own OPC UA namespace index.
|
||||
The system shall support pluggable driver modules that bind to specific data sources. v2.0 ships seven drivers: Galaxy (AVEVA System Platform via MXAccess), Modbus TCP (including DL205 via `AddressFormat=DL205`), Allen-Bradley CIP (ControlLogix/CompactLogix), Allen-Bradley Legacy (SLC/MicroLogix via PCCC), Siemens S7, Beckhoff TwinCAT (ADS), FANUC FOCAS, and OPC UA Client (aggregation/gateway). Drivers implement only the capability interfaces (`IDriver`, `ITagDiscovery`, `IReadable`, `IWritable`, `ISubscribable`, `IAlarmSource`, `IHostConnectivityProbe`, `IPerCallHostResolver`, `IRediscoverable`) defined in `ZB.MOM.WW.OtOpcUa.Core.Abstractions` that apply to their protocol. Multiple instances of the same driver type are supported; each instance binds to its own OPC UA namespace index.
|
||||
|
||||
## HLR-003: Address Space Composition per Namespace
|
||||
|
||||
@@ -20,7 +20,7 @@ Each driver shall map its native data types to OPC UA built-in types via `Driver
|
||||
|
||||
## HLR-005: Live Data Access
|
||||
|
||||
For every data-path operation (read, write, subscribe notification, alarm event, history read, tag rediscovery, host connectivity probe), the system shall route the call through the capability interface owned by the target driver instance. Reads and subscriptions shall deliver a `DataValueSnapshot` carrying value, OPC UA `StatusCode`, and source timestamp regardless of the underlying protocol. Every async capability invocation at dispatch shall pass through `Core.Resilience.CapabilityInvoker`.
|
||||
For every data-path operation (read, write, subscribe notification, alarm event, tag rediscovery, host connectivity probe), the system shall route the call through the capability interface owned by the target driver instance. Reads and subscriptions shall deliver a `DataValueSnapshot` carrying value, OPC UA `StatusCode`, and source timestamp regardless of the underlying protocol. Every async capability invocation at dispatch shall pass through `Core.Resilience.CapabilityInvoker`. History reads are not a driver capability: OPC UA HistoryRead is served by the server-wide `IHistorianDataSource`.
|
||||
|
||||
## HLR-006: Change Detection and Rediscovery
|
||||
|
||||
@@ -75,7 +75,7 @@ The Admin service shall expose a `/metrics` endpoint using OpenTelemetry → Pro
|
||||
|
||||
## HLR-018: Roslyn Analyzer OTOPCUA0001
|
||||
|
||||
All direct call sites to capability-interface methods (`IReadable.ReadAsync`, `IWritable.WriteAsync`, `ITagDiscovery.DiscoverAsync`, `ISubscribable.SubscribeAsync`, `IAlarmSource.SubscribeAlarmsAsync` / `AcknowledgeAsync`, `IHistoryProvider.*`, `IHostConnectivityProbe.*`) made outside `Core.Resilience.CapabilityInvoker` shall produce Roslyn diagnostic **OTOPCUA0001** at build time. The analyzer is shipped in `ZB.MOM.WW.OtOpcUa.Analyzers` and referenced by every project that could host a capability call, guaranteeing that resilience cannot be accidentally bypassed.
|
||||
All direct call sites to capability-interface methods (`IReadable.ReadAsync`, `IWritable.WriteAsync`, `ITagDiscovery.DiscoverAsync`, `ISubscribable.SubscribeAsync`, `IAlarmSource.SubscribeAlarmsAsync` / `AcknowledgeAsync`, `IHostConnectivityProbe.*`) made outside `Core.Resilience.CapabilityInvoker` shall produce Roslyn diagnostic **OTOPCUA0001** at build time. The analyzer is shipped in `ZB.MOM.WW.OtOpcUa.Analyzers` and referenced by every project that could host a capability call, guaranteeing that resilience cannot be accidentally bypassed.
|
||||
|
||||
## Retired HLRs
|
||||
|
||||
|
||||
@@ -125,14 +125,16 @@ The server shall expose the OPC UA alarm and condition model backed by each driv
|
||||
|
||||
## OPC-009: Historical Access
|
||||
|
||||
The server shall surface OPC UA Historical Access (HA) via each driver's `IHistoryProvider` (where implemented).
|
||||
The server shall surface OPC UA Historical Access (HA) through a single server-wide historian data source (`IHistorianDataSource`, backed by the HistorianGateway). Drivers carry no history capability (Gitea #655).
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- `HistoryRead` for `Raw`, `Processed`, `AtTime`, and `Events` dispatches through `CapabilityInvoker.InvokeAsync(DriverCapability.HistoryRead, …)`.
|
||||
- Drivers implementing `IHistoryProvider` today: Galaxy (Wonderware Historian), OPC UA Client (proxy to remote historian).
|
||||
- Drivers not implementing `IHistoryProvider` return `Bad_HistoryOperationUnsupported` for history requests on their nodes.
|
||||
- History reads require the `Read` permission bit on the target node.
|
||||
- `HistoryRead` for `Raw`, `Processed` (the Part 13 aggregates the backend serves), and `AtTime` over historized variable nodes, and for `Events` over alarm-owning equipment-folder event notifiers, is served by the registered `IHistorianDataSource`. It is not dispatched to a driver and does not pass through `CapabilityInvoker`.
|
||||
- A variable is historized by `isHistorized` in its `TagConfig` (optionally `historianTagname`); such nodes carry the `HistoryRead` access bit and `Historizing = true`. A node without it answers `Bad_HistoryOperationUnsupported`.
|
||||
- With `ServerHistorian:Enabled = false` (the `NullHistorianDataSource`), historized nodes return `GoodNoData`, never an error.
|
||||
- A per-node backend failure surfaces as a Bad status for that node only (`Bad_Timeout` past the request deadline, `Bad_AggregateNotSupported` for an aggregate the backend cannot serve); it does not fail the batch.
|
||||
- When `Security:Acl:Enabled` is true, each node requires the `HistoryRead` permission (`Bad_UserAccessDenied` otherwise). With the flag off, history reads are ungated for any admitted session.
|
||||
- An OPC UA Client driver does not proxy its upstream server's history; `isHistorized` on its tags records the value into the historian.
|
||||
|
||||
---
|
||||
|
||||
@@ -250,7 +252,7 @@ The server shall expose standard OPC UA `Server` object nodes required by the sp
|
||||
### Acceptance Criteria
|
||||
|
||||
- `ServerStatus` / `ServerState` / `CurrentTime` / `StartTime` populated and compliant with the OPC UA 1.05 spec.
|
||||
- `ServerCapabilities` declares historical access capabilities for namespaces that have an `IHistoryProvider`-backed driver.
|
||||
- `ServerCapabilities` declares historical access capabilities consistent with the server-wide historian data source.
|
||||
- `ServerRedundancy.RedundancySupport` reflects the cluster's redundancy mode (`None` / `Warm` / `Hot`).
|
||||
- `ServerRedundancy.ServerUriArray` lists both cluster members' `ApplicationUri` values.
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ IReadable, ISubscribable, IHostConnectivityProbe` living in
|
||||
equipment-tag parser, and a `.Browser` project for the AdminUI address picker. It has **no native
|
||||
push model** — subscriptions overlay a polling loop via the shared `PollGroupEngine`
|
||||
(`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/PollGroupEngine.cs`), exactly as Modbus/S7/AB-CIP do.
|
||||
`IWritable` and `IAlarmSource`/`IHistoryProvider` are **out of scope for v1** (writes deferred;
|
||||
alarms/history aren't expressible from a poll query).
|
||||
`IWritable` and `IAlarmSource` are **out of scope for v1** (writes deferred;
|
||||
alarms aren't expressible from a poll query; drivers carry no history capability).
|
||||
|
||||
Points are ordinary equipment `Tag`s bound to the driver via `TagConfig.FullName`; reads/subscribes
|
||||
resolve `FullName` to a tag definition through the shared `EquipmentTagRefResolver<TDef>`
|
||||
|
||||
+1
-1
@@ -1104,7 +1104,7 @@ for the affected listener ports.
|
||||
|
||||
Per-capability resilience (retry, timeout, circuit-breaker) is applied by `CapabilityInvoker` in `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/`. A driver-capability call made **outside** the invoker bypasses resilience entirely — which in production looks like inconsistent timeouts, un-wrapped retries, and unbounded blocking.
|
||||
|
||||
`OTOPCUA0001` (Roslyn analyzer at `src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs`) fires with category `OtOpcUa.Resilience` and default severity **Warning** (per `AnalyzerReleases.Shipped.md`) when a method on one of the seven guarded capability interfaces (`IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource`, `IHistoryProvider` — all in `ZB.MOM.WW.OtOpcUa.Core.Abstractions`) is invoked **outside** a lambda passed to `CapabilityInvoker.ExecuteAsync` / `ExecuteWriteAsync`. `AlarmSurfaceInvoker` is **not** a wrapper home — its own implementation is covered transitively because it routes through the inner `CapabilityInvoker.ExecuteAsync`. The analyzer walks up the syntax tree from the call site, finds any enclosing invoker invocation, and verifies the call lives transitively inside that invocation's anonymous-function argument — a sibling pattern (do the call, then invoke `ExecuteAsync` on something unrelated nearby) does not satisfy the rule.
|
||||
`OTOPCUA0001` (Roslyn analyzer at `src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/UnwrappedCapabilityCallAnalyzer.cs`) fires with category `OtOpcUa.Resilience` and default severity **Warning** (per `AnalyzerReleases.Shipped.md`) when a method on one of the six guarded capability interfaces (`IReadable`, `IWritable`, `ITagDiscovery`, `ISubscribable`, `IHostConnectivityProbe`, `IAlarmSource` — all in `ZB.MOM.WW.OtOpcUa.Core.Abstractions`) is invoked **outside** a lambda passed to `CapabilityInvoker.ExecuteAsync` / `ExecuteWriteAsync`. `AlarmSurfaceInvoker` is **not** a wrapper home — its own implementation is covered transitively because it routes through the inner `CapabilityInvoker.ExecuteAsync`. The analyzer walks up the syntax tree from the call site, finds any enclosing invoker invocation, and verifies the call lives transitively inside that invocation's anonymous-function argument — a sibling pattern (do the call, then invoke `ExecuteAsync` on something unrelated nearby) does not satisfy the rule.
|
||||
|
||||
The xunit.v3 + Shouldly suite at `tests/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers.Tests/UnwrappedCapabilityCallAnalyzerTests.cs` covers the common fail/pass shapes + the sibling-pattern regression guard.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ Galaxy (MXAccess) is a **Tier-A in-process driver** that runs in the OtOpcUa ser
|
||||
| `ISubscribable` | `Runtime/GatewayGalaxySubscriber.cs` (+ `TracedGalaxySubscriber.cs`), `Runtime/EventPump.cs`, `Runtime/SubscriptionRegistry.cs`, `Runtime/ReconnectSupervisor.cs` |
|
||||
| `IHostConnectivityProbe` | `Health/HostStatusAggregator.cs`, `Health/HostConnectivityForwarder.cs`, `Health/PerPlatformProbeWatcher.cs` |
|
||||
|
||||
History reads + alarm condition tracking now live in the server-layer `IHistoryRouter` and `AlarmConditionService` (PR 7.2). Galaxy no longer carries `IHistoryProvider` or `IAlarmSource` of its own.
|
||||
History reads + alarm condition tracking are now server-side: history is served by `IHistorianDataSource` (HistorianGateway), alarm condition tracking by `AlarmConditionService` (PR 7.2). Galaxy carries neither a history capability nor `IAlarmSource` of its own.
|
||||
|
||||
### DriverConfig JSON shape
|
||||
|
||||
|
||||
Reference in New Issue
Block a user