Files
lmxopcua/docs
Joseph Doherty c985c50a96 OpcUaClient integration fixture — opc-plc in Docker closes the wire-level gap (#215). Closes task #215. The OpcUaClient driver had the richest capability matrix in the fleet (reads/writes/subscribe/alarms/history across 11 unit-test classes) + zero wire-level coverage; every test mocked the Session surface. opc-plc is Microsoft Industrial IoT's OPC UA PLC simulator — already containerized, already on MCR, pinned to 2.14.10 here. Wins vs the loopback-against-our-own-server option we'd originally scoped: (a) independent cert chain + user-token handling catches interop bugs loopback can't because both endpoints would share our own cert store; (b) pinned image tag fixes the test surface in a way our evolving server wouldn't; (c) the --alm flag opens the door to real IAlarmSource coverage later without building a custom FakeAlarmDriver. Loss vs loopback: both use the OPCFoundation.NetStandard stack internally so bugs common to that stack don't surface — addressed by a follow-up to add open62541/open62541 as a second independent-stack image (tracked). Docker is the fixture launcher — no PowerShell/Python wrapper like Modbus/pymodbus or S7/python-snap7 because opc-plc ships containerized. Docker/docker-compose.yml pins 2.14.10 + maps port 50000 + command flags --pn=50000 --ut --aa --alm; the healthcheck TCP-probes 50000 so docker ps surfaces ready state. Fixture OpcPlcFixture follows the same shape as Snap7ServerFixture + ModbusSimulatorFixture: collection-scoped, parses OPCUA_SIM_ENDPOINT (default opc.tcp://localhost:50000) into host + port, 2-second TCP probe at init, SkipReason records the failure for Assert.Skip. Forced IPv4 on the probe socket for the same reason those two fixtures do — .NET's dual-stack "localhost" resolves IPv6 ::1 first + hangs the full connect timeout when the target binds 0.0.0.0 (IPv4). OpcPlcProfile holds well-known node identifiers opc-plc exposes (ns=3;s=StepUp, FastUInt1, RandomSignedInt32, AlternatingBoolean) + builds OpcUaClientDriverOptions with SecurityPolicy.None + AutoAcceptCertificates=true since opc-plc regenerates its server cert on every container spin-up + there's no meaningful chain to validate against in CI. Three smoke tests covering what the unit suite couldn't reach: (1) Client_connects_and_reads_StepUp_node_through_real_OPC_UA_stack — full Secure Channel + Session + Read on ns=3;s=StepUp (counter that ticks every 1 s); (2) Client_reads_batch_of_varied_types_from_live_simulator — batch Read of UInt32 / Int32 / Boolean to prove typed Variant decoding, with an explicit ShouldBeOfType<bool> assertion on AlternatingBoolean to catch the common "variant gets stringified" regression; (3) Client_subscribe_receives_StepUp_data_changes_from_live_server — real MonitoredItem subscription on FastUInt1 (100 ms cadence) with a SemaphoreSlim gate + 3 s deadline on the first OnDataChange fire, tolerating container warm-up. Driver ran end-to-end against a live 2.14.10 container: all 3 pass; unit suite 78/78 unchanged. Container lifecycle verified (compose up → tests → compose down) clean, no leaked state. Docker/README.md documents install (Docker Desktop already on the dev box per Phase 1 decision #134), run (compose up / compose up -d / compose down), endpoint override (OPCUA_SIM_ENDPOINT), what opc-plc advertises with the current command flags, what's tunable via compose-file tweaks (--daa for username auth tests; --fn/--fr/--ft for subscription-stress nodes), known limitation that opc-plc shares the OPCFoundation stack with our driver. OpcUaClient-Test-Fixture.md updated — TL;DR flipped from "there is no integration fixture" to the new reality; "What it actually covers" gains an Integration section listing the three smoke tests. Follow-up the doc flags: add open62541/open62541 as a second image for fully-independent-stack interop coverage; once #219 (server-side IAlarmSource/IHistoryProvider integration tests) lands, re-run the client-side suite against opc-plc's --alm nodes to close the alarm gap from the client side too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:43:20 -04:00
..
OpcUaClient integration fixture — opc-plc in Docker closes the wire-level gap (#215). Closes task #215. The OpcUaClient driver had the richest capability matrix in the fleet (reads/writes/subscribe/alarms/history across 11 unit-test classes) + zero wire-level coverage; every test mocked the Session surface. opc-plc is Microsoft Industrial IoT's OPC UA PLC simulator — already containerized, already on MCR, pinned to 2.14.10 here. Wins vs the loopback-against-our-own-server option we'd originally scoped: (a) independent cert chain + user-token handling catches interop bugs loopback can't because both endpoints would share our own cert store; (b) pinned image tag fixes the test surface in a way our evolving server wouldn't; (c) the --alm flag opens the door to real IAlarmSource coverage later without building a custom FakeAlarmDriver. Loss vs loopback: both use the OPCFoundation.NetStandard stack internally so bugs common to that stack don't surface — addressed by a follow-up to add open62541/open62541 as a second independent-stack image (tracked). Docker is the fixture launcher — no PowerShell/Python wrapper like Modbus/pymodbus or S7/python-snap7 because opc-plc ships containerized. Docker/docker-compose.yml pins 2.14.10 + maps port 50000 + command flags --pn=50000 --ut --aa --alm; the healthcheck TCP-probes 50000 so docker ps surfaces ready state. Fixture OpcPlcFixture follows the same shape as Snap7ServerFixture + ModbusSimulatorFixture: collection-scoped, parses OPCUA_SIM_ENDPOINT (default opc.tcp://localhost:50000) into host + port, 2-second TCP probe at init, SkipReason records the failure for Assert.Skip. Forced IPv4 on the probe socket for the same reason those two fixtures do — .NET's dual-stack "localhost" resolves IPv6 ::1 first + hangs the full connect timeout when the target binds 0.0.0.0 (IPv4). OpcPlcProfile holds well-known node identifiers opc-plc exposes (ns=3;s=StepUp, FastUInt1, RandomSignedInt32, AlternatingBoolean) + builds OpcUaClientDriverOptions with SecurityPolicy.None + AutoAcceptCertificates=true since opc-plc regenerates its server cert on every container spin-up + there's no meaningful chain to validate against in CI. Three smoke tests covering what the unit suite couldn't reach: (1) Client_connects_and_reads_StepUp_node_through_real_OPC_UA_stack — full Secure Channel + Session + Read on ns=3;s=StepUp (counter that ticks every 1 s); (2) Client_reads_batch_of_varied_types_from_live_simulator — batch Read of UInt32 / Int32 / Boolean to prove typed Variant decoding, with an explicit ShouldBeOfType<bool> assertion on AlternatingBoolean to catch the common "variant gets stringified" regression; (3) Client_subscribe_receives_StepUp_data_changes_from_live_server — real MonitoredItem subscription on FastUInt1 (100 ms cadence) with a SemaphoreSlim gate + 3 s deadline on the first OnDataChange fire, tolerating container warm-up. Driver ran end-to-end against a live 2.14.10 container: all 3 pass; unit suite 78/78 unchanged. Container lifecycle verified (compose up → tests → compose down) clean, no leaked state. Docker/README.md documents install (Docker Desktop already on the dev box per Phase 1 decision #134), run (compose up / compose up -d / compose down), endpoint override (OPCUA_SIM_ENDPOINT), what opc-plc advertises with the current command flags, what's tunable via compose-file tweaks (--daa for username auth tests; --fn/--fr/--ft for subscription-stress nodes), known limitation that opc-plc shares the OPCFoundation stack with our driver. OpcUaClient-Test-Fixture.md updated — TL;DR flipped from "there is no integration fixture" to the new reality; "What it actually covers" gains an Integration section listing the three smoke tests. Follow-up the doc flags: add open62541/open62541 as a second image for fully-independent-stack interop coverage; once #219 (server-side IAlarmSource/IHistoryProvider integration tests) lands, re-run the client-side suite against opc-plc's --alm nodes to close the alarm gap from the client side too.
2026-04-20 11:43:20 -04:00
Client rename residuals: lmxopcua-cli → otopcua-cli + LmxOpcUaClient → OtOpcUaClient with migration shim. Closes task #208 (the executable-name + LocalAppData-folder slice that was called out in Client.CLI.md / Client.UI.md as a deliberately-deferred residual of the Phase 0 rename). Six source references flipped to the canonical OtOpcUaClient spelling: Program.cs CliFx executable name + description (lmxopcua-cli → otopcua-cli), DefaultApplicationConfigurationFactory.cs ApplicationName + ApplicationUri (LmxOpcUaClient + urn:localhost:LmxOpcUaClient → OtOpcUaClient + urn:localhost:OtOpcUaClient), OpcUaClientService.CreateSessionAsync session-name arg, ConnectionSettings.CertificateStorePath default, MainWindowViewModel.CertificateStorePath default, JsonSettingsService.SettingsDir. Two consuming tests (ConnectionSettingsTests + MainWindowViewModelTests) updated to assert the new canonical name. New ClientStoragePaths static helper at src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs is the migration shim — single entry point for the PKI root + pki subpath, runs a one-shot legacy-folder probe on first resolution: if {LocalAppData}/LmxOpcUaClient/ exists + {LocalAppData}/OtOpcUaClient/ does not, Directory.Move renames it in place (atomic on NTFS within the same volume) so trusted server certs + saved connection settings persist across the rename without operator action. Idempotent per-process via a Lock-guarded _migrationChecked flag so repeated CertificateStorePath getter calls on the hot path pay no IO cost beyond the first. Fresh-install path (neither folder exists) + already-migrated path (only canonical exists) + manual-override path (both exist — developer has set up something explicit) are all no-ops that leave state alone. IOException on the Directory.Move is swallowed + logged as a false return so a concurrent peer process losing the race doesn't crash the consumer; the losing process falls back to whatever state exists. Five new ClientStoragePathsTests assert: GetRoot ends with canonical name under LocalAppData, GetPkiPath nests pki under root, CanonicalFolderName is OtOpcUaClient, LegacyFolderName is LmxOpcUaClient (the migration contract — a typo here would leak the legacy folder past the shim), repeat invocation returns false after first-touch arms the in-process guard. Doc-side residual-explanation notes in docs/Client.CLI.md + docs/Client.UI.md are dropped now that the rename is real; replaced with a short "pre-#208 dev boxes migrate automatically on first launch" note that points at ClientStoragePaths. Sample CLI invocations in Client.CLI.md updated via sed from lmxopcua-cli to otopcua-cli across every command block (14 replacements). Pre-existing staleness in SubscribeCommandTests.Execute_PrintsSubscriptionMessage surfaced during the test run — the CLI's subscribe command has long since switched to an aggregate "Subscribed to {count}/{total} nodes (interval: ...)" output format but the test still asserted the original single-node form. Updated the assertion to match current output + added a comment explaining the change; this is unrelated to the rename but was blocking a green Client.CLI.Tests run. Full solution build 0 errors; Client.Shared.Tests 136/136 + 5 new shim tests passing; Client.UI.Tests 98/98; Client.CLI.Tests 52/52 (was 51/52 before the subscribe-test fix). No Admin/Core/Server changes — this touches only the client layer.
2026-04-20 01:50:40 -04:00
Client rename residuals: lmxopcua-cli → otopcua-cli + LmxOpcUaClient → OtOpcUaClient with migration shim. Closes task #208 (the executable-name + LocalAppData-folder slice that was called out in Client.CLI.md / Client.UI.md as a deliberately-deferred residual of the Phase 0 rename). Six source references flipped to the canonical OtOpcUaClient spelling: Program.cs CliFx executable name + description (lmxopcua-cli → otopcua-cli), DefaultApplicationConfigurationFactory.cs ApplicationName + ApplicationUri (LmxOpcUaClient + urn:localhost:LmxOpcUaClient → OtOpcUaClient + urn:localhost:OtOpcUaClient), OpcUaClientService.CreateSessionAsync session-name arg, ConnectionSettings.CertificateStorePath default, MainWindowViewModel.CertificateStorePath default, JsonSettingsService.SettingsDir. Two consuming tests (ConnectionSettingsTests + MainWindowViewModelTests) updated to assert the new canonical name. New ClientStoragePaths static helper at src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs is the migration shim — single entry point for the PKI root + pki subpath, runs a one-shot legacy-folder probe on first resolution: if {LocalAppData}/LmxOpcUaClient/ exists + {LocalAppData}/OtOpcUaClient/ does not, Directory.Move renames it in place (atomic on NTFS within the same volume) so trusted server certs + saved connection settings persist across the rename without operator action. Idempotent per-process via a Lock-guarded _migrationChecked flag so repeated CertificateStorePath getter calls on the hot path pay no IO cost beyond the first. Fresh-install path (neither folder exists) + already-migrated path (only canonical exists) + manual-override path (both exist — developer has set up something explicit) are all no-ops that leave state alone. IOException on the Directory.Move is swallowed + logged as a false return so a concurrent peer process losing the race doesn't crash the consumer; the losing process falls back to whatever state exists. Five new ClientStoragePathsTests assert: GetRoot ends with canonical name under LocalAppData, GetPkiPath nests pki under root, CanonicalFolderName is OtOpcUaClient, LegacyFolderName is LmxOpcUaClient (the migration contract — a typo here would leak the legacy folder past the shim), repeat invocation returns false after first-touch arms the in-process guard. Doc-side residual-explanation notes in docs/Client.CLI.md + docs/Client.UI.md are dropped now that the rename is real; replaced with a short "pre-#208 dev boxes migrate automatically on first launch" note that points at ClientStoragePaths. Sample CLI invocations in Client.CLI.md updated via sed from lmxopcua-cli to otopcua-cli across every command block (14 replacements). Pre-existing staleness in SubscribeCommandTests.Execute_PrintsSubscriptionMessage surfaced during the test run — the CLI's subscribe command has long since switched to an aggregate "Subscribed to {count}/{total} nodes (interval: ...)" output format but the test still asserted the original single-node form. Updated the assertion to match current output + added a comment explaining the change; this is unrelated to the rename but was blocking a green Client.CLI.Tests run. Full solution build 0 errors; Client.Shared.Tests 136/136 + 5 new shim tests passing; Client.UI.Tests 98/98; Client.CLI.Tests 52/52 (was 51/52 before the subscribe-test fix). No Admin/Core/Server changes — this touches only the client layer.
2026-04-20 01:50:40 -04:00
Doc refresh (task #206) — Client.CLI + Client.UI brand flip + new top-level docs/README.md index. Client.CLI.md: replaced stale LmxOpcUa-OPC UA-server references with OtOpcUa throughout the overview + sample output + applicationUri examples (opc.tcp://localhost:4840/OtOpcUa, urn:localhost:OtOpcUa:instanceN); confirmed against src/ZB.MOM.WW.OtOpcUa.Server/Program.cs:69-71 which sets the live endpoint url + application uri to those exact values. Added a driver-agnostic note in the overview — the CLI is reachable against every shipped driver surface because the OPC UA endpoint abstracts them all. Kept the lmxopcua-cli executable name + the {LocalAppData}/LmxOpcUaClient/pki/ PKI folder name AS-IS because those are real filesystem-level residuals the code still uses (Program.cs SetExecutableName + OpcUaClientService.cs:428) — flipping them requires migration shims so existing dev boxes don't lose their trusted-cert store; added explicit doc text explaining the residual + why it persists so future readers aren't confused. Fixed the sample connect-output "Server: LmxOpcUa" to "Server: OtOpcUa Server" matching the live ApplicationName in OpcUaServerOptions.cs:39. Client.UI.md: replaced the 4 LmxOpcUa references — overview one-liner, status-bar mock (now reads "OtOpcUa Server" matching the server's reported ApplicationName), endpoint-url example, settings persistence path. Same residual-explanation note added under the LmxOpcUaClient/settings.json path pointing at the Client.Shared session-factory literal at OpcUaClientService.cs:428. docs/README.md is new — a top-level index distinguishing the two documentation tiers (current reference at docs/*.md vs implementation history + design notes at docs/v2/*.md). Every current-reference doc gets a one-line role description in a section table (Architecture + data-path / Drivers / Operational / Client tooling / Requirements) so a new reader picking up the repo finds their way in without having to grep file names. Cross-link calls out that load-bearing references from top-level docs (plan.md decisions, admin-ui.md, acl-design.md, config-db-schema.md, driver-specs.md, dev-environment.md, test-data-sources.md) live under v2/. Notes up front that the project was renamed LmxOpcUa → OtOpcUa and that any remaining LmxOpcUa-string in paths is a deliberate residual with a migration follow-up, so readers don't chase phantom bugs. Four parallel doc-refresh agents currently working on the rest of docs/*.md (task #202 core architecture, #203 driver docs split, #204 operational, #205 requirements) — those commits will land on separate worktree branches + get folded in together once complete; this index already lists the docs they'll produce (drivers/README.md, drivers/Galaxy.md, drivers/Galaxy-Repository.md) so the final merge just has the content showing up where the index already points.
2026-04-20 01:25:18 -04:00

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

  • Core owns the OPC UA stack, address space, session/security/subscription machinery.
  • 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.
  • Server is the OPC UA endpoint process (net10, x64). Hosts every driver except Galaxy in-process; talks to Galaxy via a named pipe because MXAccess COM is 32-bit-only.
  • Admin is the Blazor Server operator UI (net10, x64). Owns the Config DB draft/publish flow, ACL + role-grant authoring, fleet status + /metrics scrape endpoint.
  • Galaxy.Host is a .NET Framework 4.8 x86 Windows service that wraps MXAccess COM on an STA thread for the Galaxy driver.

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 → CapabilityInvokerIReadable/IWritable
Subscriptions.md Monitored items → ISubscribable + per-driver subscription refcount
AlarmTracking.md IAlarmSource + AlarmSurfaceInvoker + OPC UA alarm conditions
DataTypeMapping.md Per-driver DriverAttributeInfo → OPC UA variable types
IncrementalSync.md Address-space rebuild on redeploy + sp_ComputeGenerationDiff
HistoricalDataAccess.md IHistoryProvider as a per-driver optional capability

Drivers

Doc Covers
drivers/README.md Index of the seven shipped drivers + capability matrix
drivers/Galaxy.md Galaxy driver — MXAccess bridge, Host/Proxy split, named-pipe IPC
drivers/Galaxy-Repository.md Galaxy-specific discovery via the ZB SQL database

For Modbus / S7 / AB CIP / AB Legacy / TwinCAT / FOCAS / OPC UA Client specifics, see v2/driver-specs.md.

Operational

Doc Covers
Configuration.md appsettings bootstrap + Config DB + Admin UI draft/publish
security.md Transport security profiles, LDAP auth, ACL trie, role grants, OTOPCUA0001 analyzer
Redundancy.md RedundancyCoordinator, ServiceLevelCalculator, apply-lease, Prometheus metrics
ServiceHosting.md Three-process deploy (Server + Admin + Galaxy.Host) install/uninstall
StatusDashboard.md Pointer — superseded by v2/admin-ui.md

Client tooling

Doc Covers
Client.CLI.md lmxopcua-cli — command-line client
Client.UI.md Avalonia desktop client

Requirements

Doc Covers
reqs/HighLevelReqs.md HLRs — numbered system-level requirements
reqs/OpcUaServerReqs.md OPC UA server-layer reqs
reqs/ServiceHostReqs.md Per-process hosting reqs
reqs/ClientRequirements.md Client CLI + UI reqs
reqs/GalaxyRepositoryReqs.md Galaxy-scoped repository reqs
reqs/MxAccessClientReqs.md Galaxy-scoped MXAccess reqs
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: