Wire Galaxy security_classification to OPC UA AccessLevel (ReadOnly for SecuredWrite/VerifiedWrite/ViewOnly). Use deployed package chain for attribute queries to exclude undeployed attributes. Group primitive attributes under their parent variable node (merged Variable+Object). Add is_historized and is_alarm detection via HistoryExtension/AlarmExtension primitives. Implement OPC UA HistoryRead backed by Wonderware Historian Runtime database. Implement AlarmConditionState nodes driven by InAlarm with condition refresh support. Add historyread and alarms CLI commands for testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
196 lines
8.0 KiB
C#
196 lines
8.0 KiB
C#
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
|
|
{
|
|
/// <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 readonly HistorianDataSource? _historianDataSource;
|
|
private ApplicationInstance? _application;
|
|
private LmxOpcUaServer? _server;
|
|
|
|
/// <summary>
|
|
/// Gets the active node manager that holds the published Galaxy namespace.
|
|
/// </summary>
|
|
public LmxNodeManager? NodeManager => _server?.NodeManager;
|
|
|
|
/// <summary>
|
|
/// Gets the number of currently connected OPC UA client sessions.
|
|
/// </summary>
|
|
public int ActiveSessionCount => _server?.ActiveSessionCount ?? 0;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the OPC UA server has been started and not yet stopped.
|
|
/// </summary>
|
|
public bool IsRunning => _server != null;
|
|
|
|
/// <summary>
|
|
/// Initializes a new host for the Galaxy-backed OPC UA server instance.
|
|
/// </summary>
|
|
/// <param name="config">The endpoint and session settings for the OPC UA host.</param>
|
|
/// <param name="mxAccessClient">The runtime client used by the node manager for live reads, writes, and subscriptions.</param>
|
|
/// <param name="metrics">The metrics collector shared with the node manager and runtime bridge.</param>
|
|
public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics,
|
|
HistorianDataSource? historianDataSource = null)
|
|
{
|
|
_config = config;
|
|
_mxAccessClient = mxAccessClient;
|
|
_metrics = metrics;
|
|
_historianDataSource = historianDataSource;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the OPC UA application instance, prepares certificates, and binds the Galaxy namespace to the configured endpoint.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the OPC UA application instance and releases its in-memory server objects.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the host and releases server resources.
|
|
/// </summary>
|
|
public void Dispose() => Stop();
|
|
}
|
|
}
|