PR 3.W — Phase 3 wire-up: Wonderware sidecar DI registration
Solution + DI plumbing to complete Phase 3. With this PR the .NET 10 server
can boot with the Wonderware historian sidecar in the loop, gated by config
so existing deployments are unaffected.
slnx: registers Driver.Historian.Wonderware (net48 sidecar),
Driver.Historian.Wonderware.Client (net10 client), and both test projects.
Server.csproj: adds ProjectReference to the .NET 10 client.
Program.cs: reads Historian:Wonderware:* configuration. When Enabled=true,
constructs a WonderwareHistorianClient singleton and:
- Registers it as IAlarmHistorianWriter so the SqliteStoreAndForwardSink
drain (task #248) can pick it up.
- Registers a WonderwareHistorianBootstrap hosted service that, on
StartAsync, calls IHistoryRouter.Register(prefix, client) under the
configured DriverInstancePrefix (default "galaxy") — lets the
HistoryRead* dispatch in DriverNodeManager find the sidecar via
longest-prefix-match resolution.
When Enabled=false (the default), DriverNodeManager keeps using its
internal LegacyDriverHistoryAdapter for the read path and the existing
NullAlarmHistorianSink stays in place — drop-in compatible with every
deployment that hasn't moved off Galaxy.Host yet.
42 server integration tests + 10 client tests pass. Full solution build
clean (0/0).
Note: scripts/install/Install-Services.ps1 and
src/.../Server/appsettings.json carry intermixed user WIP and are NOT
committed in this PR. Equivalent edits applied locally:
Install-Services.ps1: new -InstallWonderwareHistorian switch installs the
OtOpcUaWonderwareHistorian service alongside OtOpcUaGalaxyHost;
generates a fresh historian shared secret; OtOpcUa service depends on
both when historian sidecar is installed.
Server/appsettings.json: new Historian.Wonderware section with
Enabled=false default, PipeName/SharedSecret/PeerName/
DriverInstancePrefix/ConnectTimeoutSeconds/CallTimeoutSeconds keys.
Both pieces should land in a follow-up commit once the user's WIP on those
files clears.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.csproj"/>
|
||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.S7/ZB.MOM.WW.OtOpcUa.Driver.S7.csproj"/>
|
||||
@@ -48,6 +50,8 @@
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing.Tests.csproj"/>
|
||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj"/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Hosted service that registers the configured <see cref="WonderwareHistorianClient"/>
|
||||
/// as a source on the server-level <see cref="IHistoryRouter"/> 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// PR 3.W only wires this when <c>Historian:Wonderware:Enabled=true</c> in config. The
|
||||
/// hosted service does its work in <see cref="StartAsync"/> and stays passive afterward;
|
||||
/// <see cref="StopAsync"/> is a no-op since router disposal happens through the singleton's
|
||||
/// own DI lifecycle.
|
||||
/// </remarks>
|
||||
public sealed class WonderwareHistorianBootstrap : IHostedService
|
||||
{
|
||||
private readonly IHistoryRouter _router;
|
||||
private readonly WonderwareHistorianClient _client;
|
||||
private readonly string _prefix;
|
||||
private readonly ILogger<WonderwareHistorianBootstrap> _logger;
|
||||
|
||||
public WonderwareHistorianBootstrap(
|
||||
IHistoryRouter router,
|
||||
WonderwareHistorianClient client,
|
||||
string fullReferencePrefix,
|
||||
ILogger<WonderwareHistorianBootstrap> 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;
|
||||
}
|
||||
@@ -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<EquipmentNamespaceContentLoader>();
|
||||
builder.Services.AddSingleton<AuthorizationBootstrap>();
|
||||
|
||||
// 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<IHistoryRouter, HistoryRouter>();
|
||||
builder.Services.AddSingleton<AlarmConditionService>();
|
||||
|
||||
// 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<string>("PipeName")
|
||||
?? throw new InvalidOperationException("Historian:Wonderware:PipeName must be set when Enabled=true."),
|
||||
SharedSecret: wonderwareSection.GetValue<string>("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<WonderwareHistorianClient>();
|
||||
builder.Services.AddSingleton<IAlarmHistorianWriter>(sp => sp.GetRequiredService<WonderwareHistorianClient>());
|
||||
builder.Services.AddHostedService(sp => new WonderwareHistorianBootstrap(
|
||||
sp.GetRequiredService<IHistoryRouter>(),
|
||||
sp.GetRequiredService<WonderwareHistorianClient>(),
|
||||
wonderwarePrefix,
|
||||
sp.GetRequiredService<ILogger<WonderwareHistorianBootstrap>>()));
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton<OpcUaApplicationHost>(sp =>
|
||||
{
|
||||
var registry = sp.GetRequiredService<DriverEquipmentContentRegistry>();
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms\ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian\ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.FOCAS\ZB.MOM.WW.OtOpcUa.Driver.FOCAS.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Modbus\ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.AbCip\ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj"/>
|
||||
|
||||
Reference in New Issue
Block a user