Implement LmxOpcUa server — all 6 phases complete

Full OPC UA server on .NET Framework 4.8 (x86) exposing AVEVA System
Platform Galaxy tags via MXAccess. Mirrors Galaxy object hierarchy as
OPC UA address space, translating contained-name browse paths to
tag-name runtime references.

Components implemented:
- Configuration: AppConfiguration with 4 sections, validator
- Domain: ConnectionState, Quality, Vtq, MxDataTypeMapper, error codes
- MxAccess: StaComThread, MxAccessClient (partial classes), MxProxyAdapter
  using strongly-typed ArchestrA.MxAccess COM interop
- Galaxy Repository: SQL queries (hierarchy, attributes, change detection),
  ChangeDetectionService with auto-rebuild on deploy
- OPC UA Server: LmxNodeManager (CustomNodeManager2), LmxOpcUaServer,
  OpcUaServerHost with programmatic config, SecurityPolicy None
- Status Dashboard: HTTP server with HTML/JSON/health endpoints
- Integration: Full 14-step startup, graceful shutdown, component wiring

175 tests (174 unit + 1 integration), all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-25 05:55:27 -04:00
commit a7576ffb38
283 changed files with 16493 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
using System;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Configuration;
using Opc.Ua.Server;
using Serilog;
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
{
/// <summary>
/// Manages the OPC UA ApplicationInstance lifecycle. Programmatic config, no XML. (OPC-001, OPC-012, OPC-013)
/// </summary>
public class OpcUaServerHost : IDisposable
{
private static readonly ILogger Log = Serilog.Log.ForContext<OpcUaServerHost>();
private readonly OpcUaConfiguration _config;
private readonly IMxAccessClient _mxAccessClient;
private readonly PerformanceMetrics _metrics;
private ApplicationInstance? _application;
private LmxOpcUaServer? _server;
public LmxNodeManager? NodeManager => _server?.NodeManager;
public int ActiveSessionCount => _server?.ActiveSessionCount ?? 0;
public bool IsRunning => _server != null;
public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics)
{
_config = config;
_mxAccessClient = mxAccessClient;
_metrics = metrics;
}
public async Task StartAsync()
{
var namespaceUri = $"urn:{_config.GalaxyName}:LmxOpcUa";
var appConfig = new ApplicationConfiguration
{
ApplicationName = _config.ServerName,
ApplicationUri = namespaceUri,
ApplicationType = ApplicationType.Server,
ProductUri = namespaceUri,
ServerConfiguration = new ServerConfiguration
{
BaseAddresses = { $"opc.tcp://0.0.0.0:{_config.Port}{_config.EndpointPath}" },
MaxSessionCount = _config.MaxSessions,
MaxSessionTimeout = _config.SessionTimeoutMinutes * 60 * 1000, // ms
MinSessionTimeout = 10000,
SecurityPolicies =
{
new ServerSecurityPolicy
{
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None
}
},
UserTokenPolicies =
{
new UserTokenPolicy(UserTokenType.Anonymous)
}
},
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OPC Foundation", "pki", "own"),
SubjectName = $"CN={_config.ServerName}, O=ZB MOM, DC=localhost"
},
TrustedIssuerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OPC Foundation", "pki", "issuer")
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OPC Foundation", "pki", "trusted")
},
RejectedCertificateStore = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OPC Foundation", "pki", "rejected")
},
AutoAcceptUntrustedCertificates = true
},
TransportQuotas = new TransportQuotas
{
OperationTimeout = 120000,
MaxStringLength = 4 * 1024 * 1024,
MaxByteStringLength = 4 * 1024 * 1024,
MaxArrayLength = 65535,
MaxMessageSize = 4 * 1024 * 1024,
MaxBufferSize = 65535,
ChannelLifetime = 600000,
SecurityTokenLifetime = 3600000
},
TraceConfiguration = new TraceConfiguration
{
OutputFilePath = null,
TraceMasks = 0
}
};
await appConfig.Validate(ApplicationType.Server);
_application = new ApplicationInstance
{
ApplicationName = _config.ServerName,
ApplicationType = ApplicationType.Server,
ApplicationConfiguration = appConfig
};
// Check/create application certificate
bool certOk = await _application.CheckApplicationInstanceCertificate(false, 2048);
if (!certOk)
{
Log.Warning("Application certificate check failed, attempting to create...");
certOk = await _application.CheckApplicationInstanceCertificate(false, 2048);
}
_server = new LmxOpcUaServer(_config.GalaxyName, _mxAccessClient, _metrics);
await _application.Start(_server);
Log.Information("OPC UA server started on opc.tcp://localhost:{Port}{EndpointPath} (namespace={Namespace})",
_config.Port, _config.EndpointPath, namespaceUri);
}
public void Stop()
{
try
{
_server?.Stop();
Log.Information("OPC UA server stopped");
}
catch (Exception ex)
{
Log.Warning(ex, "Error stopping OPC UA server");
}
finally
{
_server = null;
_application = null;
}
}
public void Dispose() => Stop();
}
}