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:
165
src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs
Normal file
165
src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user