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 @@
+