Layer 4 of the condition-Quality chain (#477 -> #478). #478 gave a scripted condition the worst quality of its input tags, but only for inputs whose driver PUBLISHES a Bad data change. A poll driver whose device goes unreachable publishes nothing — it raises ConnectivityChanged(false) and goes silent — so the engine held its last Good value, WorstInputStatus stayed Good, and worse, AreInputsReady stayed true, so the predicate went on evaluating a stale value after the source had died. `DriverHostActor.FanCommsLossToDependencyMux` pushes a synthetic BadNoCommunication with a null value into the dependency mux for every value ref the disconnected driver owns. The issue asked for a driver -> all-tag-refs map to be built for this. It already exists: `_nodeIdByDriverRef`, keyed (DriverInstanceId, RawPath) and rebuilt from the composition on every apply. Alarm-bearing tags live in the separate `_alarmNodeIdByDriverRef` and are deliberately excluded — a native condition is not a mux value and takes the Part 9 quality path instead. Nothing is pushed on reconnect, deliberately: publishing Good would assert a value quality this actor has not observed. Recovery rides the driver's own republish, and the mechanism was verified rather than assumed — the Connected -> Reconnecting edge calls DetachSubscription, which nulls the subscription handle, so the reconnect's ResubscribeDesired builds a FRESH subscription whose PollGroupEngine initial push runs with forceRaise: true. Without that detach the poll engine would compare against its retained LastValues, see an unchanged value, raise nothing, and strand these refs Bad forever — the value-dedup trap that bit the v3 Batch 4 virtual-tag reassert. Closes #478 Finding 3, which the above depends on. WorstInputStatus skipped every input whose VALUE was null as "no data yet" — and Bad-with-no-value is exactly the shape a comms-lost driver produces, so the skip was hiding the case that matters most; without this half the fan-out above would be silently inert. The upstream source now stamps a never-pushed path BadWaitingForInitialData — the honest Part 4 code for that state — and the engine skips that specific code rather than every null value. Deleting the null-skip outright is the obvious fix and is wrong in the other direction: it makes every not-yet-published input drag its condition Bad at deploy, flooding the quality path with load-time annotations, which is what the skip existed to prevent. Both mistakes are pinned by a test pair that goes red under the opposite error, and both were falsified by reverting the production code: old null-skip restored -> A_pushed_Bad_carrying_no_value RED null-skip deleted outright -> An_input_that_was_never_pushed RED fan-out call removed -> the three DriverHostActor tests RED (1 of 7) The never-pushed test was vacuous on first writing and is worth recording: the quality bucket only emits on CHANGE, so the load-time evaluation is the one that flashes Bad, and subscribing to OnEvent after LoadAsync made the assertion unfalsifiable — it passed against the broken behaviour. It now subscribes first. `FakeUpstream.ReadTag` returned 0x80340000 (BadNotConnected) for an unset path, a different Bad code from production's placeholder. Harmless while any Bad behaved alike; load-bearing now, so it is aligned and pinned by a reflection guard asserting the engine's private constant agrees with what the production source stamps. Behaviour change to weigh, flagged in the issue and confirmed: virtual tags computed from a comms-lost input now go Bad too. That follows from using the shared mux and reads as correct, but it is broader than scripted alarms. Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
OtOpcUa documentation
Two tiers of documentation live here:
- Current reference at the top level (
docs/*.md) — describes what's shipped today. Start here for operator + integrator reference. - Implementation history + design notes at
docs/v2/*.md— the authoritative plan + decision log the current reference is built from. Start here when you need the why behind an architectural choice, or when a top-level doc says "see plan.md § X".
The project was originally called LmxOpcUa (a single-driver Galaxy/MXAccess OPC UA server) and has since become OtOpcUa, a multi-driver OPC UA server platform. Any lingering LmxOpcUa-string in a path you see in docs is a deliberate residual (executable name lmxopcua-cli, client PKI folder {LocalAppData}/LmxOpcUaClient/) — fixing those requires migration shims + is tracked as follow-ups.
Platform overview
v2 (2026-05-26): the separate
OtOpcUa.Server+OtOpcUa.Adminservices fused into a single role-gatedOtOpcUa.Hostbinary, joined by an Akka.NET cluster. See v2 design 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. - Host (
src/Server/ZB.MOM.WW.OtOpcUa.Host) is the single fused binary (.NET 10, AnyCPU).OTOPCUA_ROLESenv decides what to mount:admin(Blazor + control-plane singletons),driver(OPC UA endpoint + per-node actors), or both. See 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.
- The Galaxy driver still reaches MXAccess via gRPC to a separately-installed mxaccessgw sidecar (sibling repo).
Where to find what
Architecture + data-path reference
| Doc | Covers |
|---|---|
| OpcUaServer.md | Top-level server architecture — Core, driver dispatch, Config DB, generations |
| AddressSpace.md | GenericDriverNodeManager + ITagDiscovery + IAddressSpaceBuilder |
| ReadWriteOperations.md | OPC UA Read/Write → CapabilityInvoker → IReadable/IWritable |
| DriverLifecycle.md | Server-side driver lifecycle + infrastructure contracts (IDriverFactory, IDriverProbe, IDriverHealthPublisher, IDriverConfigEditor, IHistorianDataSource) + the Commons library |
| Subscriptions.md | Monitored items → ISubscribable + per-driver subscription refcount (v1 archive) |
| AlarmTracking.md | IAlarmSource + AlarmSurfaceInvoker + OPC UA alarm conditions — native Galaxy alarms end-to-end (live) |
| AlarmTracking.md | Original alarm-tracking write-up (v1 archive) |
| 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 | 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 | Address-space rebuild on redeploy + sp_ComputeGenerationDiff |
| HistoricalDataAccess.md | IHistoryProvider as a per-driver optional capability (v1 archive) |
| VirtualTags.md | Core.Scripting + Core.VirtualTags — Roslyn script sandbox, engine, dispatch alongside driver tags |
| ScriptEditor.md | Monaco script editor — Roslyn-backed IntelliSense (completions, diagnostics, hover, tag-path) for C# virtual-tag scripts; AdminUI/ScriptAnalysis/ backend |
| ScriptedAlarms.md | Core.ScriptedAlarms — script-predicate IAlarmSource + Part 9 state machine |
One Core subsystem is shipped without a dedicated top-level doc; see the section in the linked doc:
| Project | See |
|---|---|
Analyzers (Roslyn OTOPCUA0001) |
security.md § OTOPCUA0001 Analyzer |
Drivers
| Doc | Covers |
|---|---|
| drivers/README.md | Index of the eight shipped drivers + capability matrix |
| drivers/Galaxy.md | Galaxy driver — in-process gRPC client to the mxaccessgw sidecar |
| v1/drivers/Galaxy-Repository.md | Galaxy-specific discovery via the ZB SQL database (v1 archive — the gateway owns this path now) |
For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics, see v2/driver-specs.md.
Operational
| Doc | Covers |
|---|---|
| Configuration.md | Live appsettings + environment-variable reference (current state) |
| Configuration.md | appsettings bootstrap + Config DB + Admin UI draft/publish (v1 archive — OTOPCUA_GALAXY_* env vars now live in mxaccessgw config) |
| Uns.md | The global /uns master tree — manage Area/Line/Equipment/Tag/VirtualTag fleet-wide; replaces the per-cluster UNS/Equipment/Tags tabs |
| security.md | Transport security profiles, LDAP auth, ACL trie, role grants, OTOPCUA0001 analyzer |
| Redundancy.md | RedundancyStateActor (cluster-scoped singleton), ServiceLevelCalculator, IRedundancyRoleView, per-cluster meshes, Prometheus metrics |
| Reservations.md | Fleet-wide ZTag / SAPID external-ID reservations — publish-time claim, release flow |
| ServiceHosting.md | Single fused OtOpcUa.Host binary install/uninstall with OTOPCUA_ROLES gating; the historian backend is the external HistorianGateway |
| StatusDashboard.md | Pointer — superseded by v2/admin-ui.md |
Client tooling
| Doc | Covers |
|---|---|
| Client.CLI.md | otopcua-cli — OPC UA command-line client |
| Client.UI.md | Avalonia desktop client |
| DriverClis.md | Driver test-client CLIs — index + shared commands |
| Driver.Modbus.Cli.md | otopcua-modbus-cli — Modbus-TCP |
| Driver.AbCip.Cli.md | otopcua-abcip-cli — ControlLogix / CompactLogix / Micro800 / GuardLogix |
| Driver.AbLegacy.Cli.md | otopcua-ablegacy-cli — SLC / MicroLogix / PLC-5 (PCCC) |
| Driver.S7.Cli.md | otopcua-s7-cli — Siemens S7-300 / S7-400 / S7-1200 / S7-1500 |
| Driver.TwinCAT.Cli.md | otopcua-twincat-cli — Beckhoff TwinCAT 2/3 ADS |
| Driver.FOCAS.Cli.md | otopcua-focas-cli — Fanuc FOCAS/2 CNC |
Requirements
| Doc | Covers |
|---|---|
| reqs/HighLevelReqs.md | HLRs — numbered system-level requirements |
| reqs/OpcUaServerReqs.md | OPC UA server-layer reqs |
| v1/reqs/ServiceHostReqs.md | Per-process hosting reqs (v1 archive — only OtOpcUa server hosting remains in scope post-PR-7.2) |
| reqs/ClientRequirements.md | Client CLI + UI reqs |
| v1/reqs/GalaxyRepositoryReqs.md | Galaxy-scoped repository reqs (v1 archive — owned by mxaccessgw today) |
| v1/reqs/MxAccessClientReqs.md | Galaxy-scoped MXAccess reqs (v1 archive — owned by mxaccessgw today) |
| reqs/StatusDashboardReqs.md | Pointer — superseded by Admin UI |
Implementation history (docs/v2/)
Design decisions + phase plans + execution notes. Load-bearing cross-references from the top-level docs:
- v2/plan.md — authoritative v2 vision doc + numbered decision log (referenced as "decision #N" elsewhere)
- v2/admin-ui.md — Admin UI spec
- v2/acl-design.md — data-plane ACL + permission-trie design (Phase 6.2)
- v2/config-db-schema.md — Config DB schema reference
- v2/driver-specs.md — per-driver addressing + quirks for every shipped protocol
- v2/dev-environment.md — dev-box bootstrap
- v2/test-data-sources.md — integration-test simulator matrix (includes the pinned libplctag
ab_serverversion for AB CIP tests) - v2/multi-host-dispatch.md — per-PLC circuit breakers (Phase 6.1 decision #144)
- v2/v2-release-readiness.md — release-readiness tracker
- v2/phase-7-status.md — Phase 7 reconciliation: what shipped vs. the plan, and the five remaining gaps
- v2/implementation/phase--.md — per-phase execution plans with exit-gate evidence
v1 archive
The v1 in-process MXAccess architecture (Galaxy.Host + Galaxy.Proxy + Galaxy.Shared, .NET 4.8 x86 COM, the OtOpcUaGalaxyHost Windows service) was retired in PR 7.2 (2026-04-30, commit ae7106d). Docs that described that shape are kept under v1/ as historical record — see v1/README.md for the index.