docs(opcuaclient): design — register the OpcUaClient driver factory (fixes always-stubbed bug)

Mirror ModbusDriverFactoryExtensions: NEW OpcUaClientDriverFactoryExtensions
(Register + CreateInstance deserialising OpcUaClientDriverOptions, like the
probe) + one line in DriverFactoryBootstrap.Register. Unblocks the first
end-to-end live equipment-tag value (live-proves the FullName→NodeId router).
This commit is contained in:
Joseph Doherty
2026-06-13 08:11:41 -04:00
parent c4435e4fd6
commit 5aa1030be9
@@ -0,0 +1,52 @@
# OpcUaClient driver factory — Design
**Date:** 2026-06-13
**Status:** Approved — ready for implementation planning
**Scope:** Register the OpcUaClient driver factory so OpcUaClient driver instances are actually constructed instead of stubbed. Fixes a real bug AND yields the first end-to-end live equipment-tag value (proving the live-value router shipped in `c4435e4f`).
## Goal
Make `DriverType="OpcUaClient"` instances real: the runtime currently logs `"no factory for driver type OpcUaClient … falling back to stub"`, so OpcUaClient drivers never connect, subscribe, or publish. After this, an OpcUaClient equipment tag (`TagConfig.FullName = "<upstream NodeId>"`) subscribes to the upstream server and publishes values, which the (already-shipped) `FullName→NodeId` router delivers to the materialised variable.
## Root cause (confirmed)
- `src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs:98-107` (`Register`) wires AbCip/AbLegacy/FOCAS/Galaxy/Modbus/S7/TwinCAT factories — but **omits OpcUaClient**.
- The `Driver.OpcUaClient` project has **no `…FactoryExtensions` class** (only `OpcUaClientDriver.cs` + `OpcUaClientDriverProbe.cs`). So there is nothing to register.
- Result: `IDriverFactory.TryCreate("OpcUaClient", …)` returns null → `DriverInstanceActor` falls back to a stub. (Note: this is the *missing-factory* stub path, distinct from `ShouldStub`, which only stubs windows-only `"Galaxy"`/`"Historian.Wonderware"`. OpcUaClient is cross-platform, so once a factory exists it runs as a real driver — including in docker-dev Linux.)
## Approach — mirror `ModbusDriverFactoryExtensions`
The OpcUaClient config JSON **is** the `OpcUaClientDriverOptions` shape — the probe already does `JsonSerializer.Deserialize<OpcUaClientDriverOptions>(configJson, _opts)` with `_opts = { PropertyNameCaseInsensitive = true, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip }`. So no DTO is needed.
### Components
1. **NEW `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs`** — mirrors `ModbusDriverFactoryExtensions`:
- `const string DriverTypeName = "OpcUaClient";`
- `private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip };` (same as the probe).
- `public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null)``registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory));`
- `public static OpcUaClientDriver CreateInstance(string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory = null)`:
deserialize → throw if null → `new OpcUaClientDriver(options, driverInstanceId, loggerFactory?.CreateLogger<OpcUaClientDriver>())`.
- Validate required: at minimum a usable endpoint (`EndpointUrl` or `EndpointUrls`) — but keep validation light; the driver's `InitializeAsync` already fails clearly on a bad endpoint, and a too-strict factory could reject valid configs. (Match Modbus's level: it only checks `Host` is present.) Decision: deserialize-and-construct; let `InitializeAsync` own connection validation. Optionally a null/empty-endpoint guard with a clear message.
2. **MODIFY `DriverFactoryBootstrap.cs`** — add one line to `Register`:
`Driver.OpcUaClient.OpcUaClientDriverFactoryExtensions.Register(registry, loggerFactory);`
(The Host already references the `Driver.OpcUaClient` project and registers `OpcUaClientDriverProbe`.)
## Error handling
- Null/invalid config JSON → `InvalidOperationException` naming the instance (mirrors Modbus). Connection/endpoint problems surface through `InitializeAsync` (the driver's existing failover-sweep diagnostics), not the factory.
## Testing (no bUnit)
- **Unit (xUnit + Shouldly):** `OpcUaClientDriverFactoryExtensions.CreateInstance` with a representative config JSON (e.g. `{"EndpointUrl":"opc.tcp://host:4840","SecurityMode":"None","AutoAcceptCertificates":true}`) returns an `OpcUaClientDriver` whose `DriverType == "OpcUaClient"` and `DriverInstanceId` matches; null/garbage JSON throws. (Mirror the Modbus factory test; place in the OpcUaClient driver's test project, or `Admin.Tests` if that's where factory tests live — confirm at plan time.)
- **Live docker-dev `/run` (agent-driven; login disabled) — the payoff:** the `MAIN-opcua-eq` OpcUaClient driver + `FastUInt1` tag (`{"FullName":"ns=3;s=FastUInt1"}`) are **already authored + deployed** in the dev rig (left from the router live-verify); opc-plc sim is up at `opc.tcp://10.100.0.35:50000`. Rebuild central on the branch, redeploy, and confirm: (a) the logs no longer say "no factory for driver type OpcUaClient" — the driver connects to opc-plc + subscribes; (b) the variable `ns=2;s=EQ-55297329838d/FastUInt1` on the OtOpcUa server (`opc.tcp://localhost:4840`) shows a **live, changing value** via Client.CLI `read`/`subscribe` (not `BadWaitingForInitialData`). **This live-proves the router (`c4435e4f`) end-to-end with a real driver.**
## Out of scope
- Protocol-driver (Modbus/S7) equipment-tag↔tag-table linkage (the larger gap — separate milestone).
- Galaxy gateway availability.
- Any change to the OpcUaClient driver's connection/subscribe logic (it already works; it just was never instantiated).
## Hard rules
Stage by path; never `git add .`; never stage `sql_login.txt` / `src/Server/.../Host/pki/` / `pending.md` / `current.md`. No force-push, no `--no-verify`. No Configuration/EF migration change. Build on a feature branch off master.