The net48 sidecar's TcpFrameServer.RunOneConnectionAsync registered the cancellation token to Stop() only the listener (to unblock a parked AcceptTcpClientAsync), but never closed the active client. On net48 NetworkStream.ReadAsync ignores the CancellationToken, so while the frame loop is parked reading an idle connected client, cancelling the token cannot unblock it — only closing the socket can. RunAsync therefore never returned on Ctrl-C/service-stop while a connection was open (Program.Main's RunAsync().GetAwaiter().GetResult() would hang until NSSM force-killed). Register the cancel to Close() the active client, and convert the resulting cancel-time read/handshake exception to OperationCanceledException so RunAsync unwinds cleanly without logging it as a connection failure or counting it toward MaxConsecutiveFailures. Caught by the first-ever net48 execution of TcpRoundTripTests on the Windows VM (these only compile on macOS): SingleActive_SecondClientHelloCompletesOnly AfterFirstCloses deadlocked in teardown. Full net48 historian suite now green (122 passed, 0 failed, 2 skipped); all 6 TcpRoundTrip tests pass.
OtOpcUa
OPC UA server (.NET 10 AnyCPU) that exposes a fleet of industrial drivers as a single OPC UA address space. Drivers ship in-process for AVEVA System Platform Galaxy (via the sibling mxaccessgw repo), Modbus TCP, Siemens S7, Allen-Bradley CIP (ControlLogix / CompactLogix), Allen-Bradley Legacy (SLC 500 / MicroLogix), Beckhoff TwinCAT (ADS), FANUC FOCAS, and OPC UA Client (gateway).
A cross-platform client stack (.NET 10) — shared library, CLI, and Avalonia desktop app — connects to any OPC UA server.
Architecture
OPC UA Clients (CLI, Desktop UI, 3rd-party)
|
v
+-------------------------------------+
| OtOpcUa.Server (.NET 10 AnyCPU) |
| address space + capability fan-out|
+-------------------------------------+
| | | | | | | |
Galaxy Modbus S7 AbCip AbLeg TwinCAT FOCAS OpcUaClient
|
v
mxaccessgw (sibling repo, gRPC)
|
v
MXAccess COM (x86 worker, on AVEVA box)
Galaxy is the only driver with an external runtime: it speaks gRPC to a separately installed mxaccessgw server (sibling repo at c:\Users\dohertj2\Desktop\mxaccessgw\) which owns the MXAccess COM apartment and the x86/STA bitness constraint server-side. Everything in this repo is platform-agnostic .NET 10.
Prerequisites
- .NET 10 SDK (server, drivers, clients all target .NET 10)
- SQL Server reachable for the central config DB
- For Galaxy specifically: a running
mxaccessgwdeployment — see docs/v2/Galaxy.ParityRig.md - For Wonderware Historian read-back: optional
OtOpcUaWonderwareHistoriansidecar — see docs/ServiceHosting.md
Quick Start
dotnet restore ZB.MOM.WW.OtOpcUa.slnx
dotnet build ZB.MOM.WW.OtOpcUa.slnx
dotnet test ZB.MOM.WW.OtOpcUa.slnx
# Run the server in dev (foreground)
dotnet run --project src/Server/ZB.MOM.WW.OtOpcUa.Host
The server starts on opc.tcp://localhost:4840 with the None security profile. Configure Security.Profiles in src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json to enable Basic256Sha256-Sign or Basic256Sha256-SignAndEncrypt. See docs/security.md.
Install as Windows Services
Production deployment is driven by scripts/install/Install-Services.ps1, which registers the OtOpcUa server service (and optionally the OtOpcUaWonderwareHistorian sidecar) under a chosen service account. Galaxy support requires a separately installed mxaccessgw — neither this repo nor the install script provisions it.
.\scripts\install\Install-Services.ps1 `
-InstallRoot 'C:\Program Files\OtOpcUa' `
-ServiceAccount 'DOMAIN\svc-otopcua'
Add -InstallWonderwareHistorian for the historian sidecar. See the script header and docs/ServiceHosting.md for full options.
Client CLI
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- connect -u opc.tcp://localhost:4840
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- browse -u opc.tcp://localhost:4840 -r -d 3
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode"
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- write -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -v 42
dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=SomeNode" -i 500
See docs/Client.CLI.md and docs/Client.UI.md.
Documentation
Architecture deep-dives
| Topic | Doc |
|---|---|
| OPC UA server composition, namespace fan-out, Polly invoker | docs/OpcUaServer.md |
| Address space layout | docs/AddressSpace.md |
| Read / Write dispatch (driver vs virtual vs scripted-alarm) | docs/ReadWriteOperations.md |
| Incremental sync (driver-backend rediscovery + config publishes) | docs/IncrementalSync.md |
| Service hosting (Server + Admin + optional historian sidecar) | docs/ServiceHosting.md |
| Security (transport, LDAP, certificates) | docs/security.md |
| Redundancy | docs/Redundancy.md |
| Status dashboard | docs/StatusDashboard.md |
Drivers
| Topic | Doc |
|---|---|
| Driver specs (per-driver capability surface, config, addressing) | docs/v2/driver-specs.md |
| Galaxy driver | docs/drivers/Galaxy.md |
| Modbus / S7 / AbCip / AbLegacy / TwinCAT / FOCAS / OpcUaClient | docs/drivers/ |
| Galaxy parity rig (mxaccessgw setup) | docs/v2/Galaxy.ParityRig.md |
| Galaxy performance + tracing | docs/v2/Galaxy.Performance.md |
Clients
| Topic | Doc |
|---|---|
| Client CLI | docs/Client.CLI.md |
| Client UI (Avalonia desktop) | docs/Client.UI.md |
v1 archive
The original v1 in-process MXAccess docs (Galaxy.Host topology, Configuration env vars, AlarmTracking, DataTypeMapping, HistoricalDataAccess, Subscriptions, etc.) are preserved under docs/v1/ — historical reference only. PR 7.2 retired the v1 architecture on 2026-04-30; current state is documented in the sections above.
License
Internal use only.