diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx index 2cebeed..7723ed6 100644 --- a/ZB.MOM.WW.OtOpcUa.slnx +++ b/ZB.MOM.WW.OtOpcUa.slnx @@ -12,6 +12,8 @@ + + @@ -48,6 +50,8 @@ + + diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/History/WonderwareHistorianBootstrap.cs b/src/ZB.MOM.WW.OtOpcUa.Server/History/WonderwareHistorianBootstrap.cs new file mode 100644 index 0000000..d557103 --- /dev/null +++ b/src/ZB.MOM.WW.OtOpcUa.Server/History/WonderwareHistorianBootstrap.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client; + +namespace ZB.MOM.WW.OtOpcUa.Server.History; + +/// +/// Hosted service that registers the configured +/// as a source on the server-level at startup. Per-namespace +/// prefix is the driver instance id the operator binds the historian to (typically +/// "galaxy"); future per-area or per-equipment overrides can register under longer prefixes. +/// +/// +/// PR 3.W only wires this when Historian:Wonderware:Enabled=true in config. The +/// hosted service does its work in and stays passive afterward; +/// is a no-op since router disposal happens through the singleton's +/// own DI lifecycle. +/// +public sealed class WonderwareHistorianBootstrap : IHostedService +{ + private readonly IHistoryRouter _router; + private readonly WonderwareHistorianClient _client; + private readonly string _prefix; + private readonly ILogger _logger; + + public WonderwareHistorianBootstrap( + IHistoryRouter router, + WonderwareHistorianClient client, + string fullReferencePrefix, + ILogger logger) + { + _router = router ?? throw new ArgumentNullException(nameof(router)); + _client = client ?? throw new ArgumentNullException(nameof(client)); + _prefix = fullReferencePrefix ?? throw new ArgumentNullException(nameof(fullReferencePrefix)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + try + { + _router.Register(_prefix, (IHistorianDataSource)_client); + _logger.LogInformation( + "Wonderware historian sidecar registered as IHistoryRouter source under prefix '{Prefix}'", + _prefix); + } + catch (InvalidOperationException ex) + { + // Prefix already registered (e.g. server restart without DI rebuild). Tolerate + // — the existing registration is the same singleton instance and stays valid. + _logger.LogWarning(ex, + "Wonderware historian source already registered for prefix '{Prefix}' — leaving existing entry", _prefix); + } + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs b/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs index c55d5e6..54d2f3f 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs @@ -9,6 +9,8 @@ using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache; using ZB.MOM.WW.OtOpcUa.Core.Hosting; using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; @@ -140,13 +142,44 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(); // PR 1+2.W — server-level history routing + alarm-condition state machine. Singletons -// shared across every DriverNodeManager. The router stays empty after this PR; -// PR 3.W registers the Wonderware historian sidecar as a router source. The alarm -// service runs the Active/Acknowledged/Inactive state machine for any driver that -// declares alarms via AlarmConditionInfo's sub-attribute refs. +// shared across every DriverNodeManager. The alarm service runs the Active / +// Acknowledged / Inactive state machine for any driver that declares alarms via +// AlarmConditionInfo's sub-attribute refs. builder.Services.AddSingleton(); builder.Services.AddSingleton(); +// PR 3.W — Wonderware historian sidecar wiring. Reads Historian:Wonderware:* from +// configuration; when Enabled=true, registers the .NET 10 client as both an +// IHistorianDataSource (via IHistoryRouter under the configured driver instance +// prefix; defaults to "galaxy") and an IAlarmHistorianWriter (consumed by the +// SqliteStoreAndForwardSink drain worker once task #248 wires it). Disabled +// deployments fall back to DriverNodeManager's legacy IHistoryProvider adapter +// for the read path and NullAlarmHistorianSink for the write path — keeping the +// sidecar fully optional until the legacy paths retire in PR 7.2. +var wonderwareSection = builder.Configuration.GetSection("Historian:Wonderware"); +var wonderwareEnabled = wonderwareSection.GetValue("Enabled", false); +if (wonderwareEnabled) +{ + var wonderwarePrefix = wonderwareSection.GetValue("DriverInstancePrefix", "galaxy") + ?? throw new InvalidOperationException("Historian:Wonderware:DriverInstancePrefix must be a string when configured."); + var wonderwareOptions = new WonderwareHistorianClientOptions( + PipeName: wonderwareSection.GetValue("PipeName") + ?? throw new InvalidOperationException("Historian:Wonderware:PipeName must be set when Enabled=true."), + SharedSecret: wonderwareSection.GetValue("SharedSecret") + ?? throw new InvalidOperationException("Historian:Wonderware:SharedSecret must be set when Enabled=true."), + PeerName: wonderwareSection.GetValue("PeerName", $"OtOpcUa-{options.NodeId}") ?? "OtOpcUa", + ConnectTimeout: TimeSpan.FromSeconds(wonderwareSection.GetValue("ConnectTimeoutSeconds", 10)), + CallTimeout: TimeSpan.FromSeconds(wonderwareSection.GetValue("CallTimeoutSeconds", 30))); + builder.Services.AddSingleton(wonderwareOptions); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => sp.GetRequiredService()); + builder.Services.AddHostedService(sp => new WonderwareHistorianBootstrap( + sp.GetRequiredService(), + sp.GetRequiredService(), + wonderwarePrefix, + sp.GetRequiredService>())); +} + builder.Services.AddSingleton(sp => { var registry = sp.GetRequiredService(); diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj b/src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj index e013286..5b3b87b 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj +++ b/src/ZB.MOM.WW.OtOpcUa.Server/ZB.MOM.WW.OtOpcUa.Server.csproj @@ -36,6 +36,7 @@ +