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.Historian; using ZB.MOM.WW.LmxOpcUa.Host.Metrics; namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa { /// /// Manages the OPC UA ApplicationInstance lifecycle. Programmatic config, no XML. (OPC-001, OPC-012, OPC-013) /// public class OpcUaServerHost : IDisposable { private static readonly ILogger Log = Serilog.Log.ForContext(); private readonly OpcUaConfiguration _config; private readonly IMxAccessClient _mxAccessClient; private readonly PerformanceMetrics _metrics; private readonly HistorianDataSource? _historianDataSource; private ApplicationInstance? _application; private LmxOpcUaServer? _server; /// /// Gets the active node manager that holds the published Galaxy namespace. /// public LmxNodeManager? NodeManager => _server?.NodeManager; /// /// Gets the number of currently connected OPC UA client sessions. /// public int ActiveSessionCount => _server?.ActiveSessionCount ?? 0; /// /// Gets a value indicating whether the OPC UA server has been started and not yet stopped. /// public bool IsRunning => _server != null; /// /// Initializes a new host for the Galaxy-backed OPC UA server instance. /// /// The endpoint and session settings for the OPC UA host. /// The runtime client used by the node manager for live reads, writes, and subscriptions. /// The metrics collector shared with the node manager and runtime bridge. public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics, HistorianDataSource? historianDataSource = null) { _config = config; _mxAccessClient = mxAccessClient; _metrics = metrics; _historianDataSource = historianDataSource; } /// /// Starts the OPC UA application instance, prepares certificates, and binds the Galaxy namespace to the configured endpoint. /// 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, _historianDataSource); await _application.Start(_server); Log.Information("OPC UA server started on opc.tcp://localhost:{Port}{EndpointPath} (namespace={Namespace})", _config.Port, _config.EndpointPath, namespaceUri); } /// /// Stops the OPC UA application instance and releases its in-memory server objects. /// 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; } } /// /// Stops the host and releases server resources. /// public void Dispose() => Stop(); } }