diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx
index 7723ed6..d54070b 100644
--- a/ZB.MOM.WW.OtOpcUa.slnx
+++ b/ZB.MOM.WW.OtOpcUa.slnx
@@ -12,6 +12,7 @@
+
@@ -50,6 +51,7 @@
+
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs
new file mode 100644
index 0000000..7362f54
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Config/GalaxyDriverOptions.cs
@@ -0,0 +1,70 @@
+namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
+
+///
+/// Driver-instance options for the in-process .NET 10 Galaxy driver. Maps to the
+/// DriverConfig JSON column on the central config DB. Decomposed into nested
+/// records so the JSON structure mirrors the runtime shape and operators can target
+/// individual sections (gateway endpoint, mxaccess client identity, reconnect policy)
+/// without touching the rest.
+///
+/// Connection details for the MxAccess gateway (mxaccessgw repo).
+/// MXAccess-specific knobs surfaced through gw — client name, publishing interval, write-user.
+/// Galaxy Repository browse options consumed by the discoverer.
+/// Backoff knobs for the in-driver reconnect supervisor (PR 4.5).
+public sealed record GalaxyDriverOptions(
+ GalaxyGatewayOptions Gateway,
+ GalaxyMxAccessOptions MxAccess,
+ GalaxyRepositoryOptions Repository,
+ GalaxyReconnectOptions Reconnect);
+
+///
+/// Connection details for the MxAccess gateway. resolves
+/// through the server-side secret store (DPAPI for production, environment override for
+/// dev) — the API key never appears in cleartext config.
+///
+public sealed record GalaxyGatewayOptions(
+ string Endpoint,
+ string ApiKeySecretRef,
+ bool UseTls = true,
+ string? CaCertificatePath = null,
+ int ConnectTimeoutSeconds = 10,
+ int DefaultCallTimeoutSeconds = 5,
+ int StreamTimeoutSeconds = 0);
+
+///
+/// MXAccess-specific knobs the gateway forwards to the worker process.
+///
+///
+/// Wonderware client identity. MUST be unique per OtOpcUa instance — when two instances
+/// share a name, the older session loses subscription state. Redundancy pairs (decision
+/// #149) enforce uniqueness via install scripts.
+///
+///
+/// Hint forwarded as buffered_update_interval_ms on subscribe; lets the worker
+/// coalesce updates at the OPC UA publishing cadence rather than every COM tick.
+///
+///
+/// Reserved for ArchestrA secured-write user mapping; PR 4.3 wires WriteSecured
+/// routing against this id. 0 = anonymous.
+///
+public sealed record GalaxyMxAccessOptions(
+ string ClientName,
+ int PublishingIntervalMs = 1000,
+ int WriteUserId = 0);
+
+///
+/// Galaxy Repository browse-side knobs consumed by PR 4.1's GalaxyDiscoverer.
+///
+public sealed record GalaxyRepositoryOptions(
+ int DiscoverPageSize = 5000,
+ bool WatchDeployEvents = true);
+
+///
+/// Backoff knobs for the in-driver reconnect supervisor (PR 4.5). Replay-on-session-lost
+/// calls the gw's ReplaySubscriptions RPC after reconnect rather than re-issuing
+/// subscribe-bulk for every tag.
+///
+public sealed record GalaxyReconnectOptions(
+ int InitialBackoffMs = 500,
+ int MaxBackoffMs = 30_000,
+ bool ReplayOnSessionLost = true);
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs
new file mode 100644
index 0000000..e09fb9f
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs
@@ -0,0 +1,102 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
+
+///
+/// In-process .NET 10 Galaxy driver — the v2 replacement for the Galaxy.Host /
+/// Galaxy.Proxy pair. PR 4.0 ships the project skeleton with
+/// bodies that wire to a future IGalaxyGatewayClient abstraction. Capability
+/// interfaces (browse, read, write, subscribe, history routing, host probes) land in
+/// PRs 4.1–4.7; the wiring sequence keeps every intermediate state buildable so the
+/// Galaxy:Backend flag (PR 4.W) can flip between legacy-host and mxgateway
+/// for parity testing.
+///
+///
+/// This driver is registered as a Tier A in-process driver alongside Modbus / S7 / etc.
+/// The legacy GalaxyProxyDriver (Driver.Galaxy.Proxy) coexists until PR 7.2;
+/// registers under driver-type name
+/// "GalaxyMxGateway" so both paths can be live simultaneously during parity testing.
+///
+public sealed class GalaxyDriver : IDriver, IDisposable
+{
+ private readonly string _driverInstanceId;
+ private readonly GalaxyDriverOptions _options;
+ private readonly ILogger _logger;
+
+ private DriverHealth _health = new(DriverState.Unknown, null, null);
+ private bool _disposed;
+
+ public GalaxyDriver(
+ string driverInstanceId,
+ GalaxyDriverOptions options,
+ ILogger? logger = null)
+ {
+ _driverInstanceId = !string.IsNullOrWhiteSpace(driverInstanceId)
+ ? driverInstanceId
+ : throw new ArgumentException("Driver instance id required.", nameof(driverInstanceId));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ _logger = logger ?? NullLogger.Instance;
+ }
+
+ ///
+ public string DriverInstanceId => _driverInstanceId;
+
+ ///
+ public string DriverType => GalaxyDriverFactoryExtensions.DriverTypeName;
+
+ /// Test-visible options snapshot.
+ internal GalaxyDriverOptions Options => _options;
+
+ ///
+ public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ // PR 4.0 skeleton — capability bodies (PRs 4.1-4.7) replace this stub with real
+ // MxGatewayClient session opening. The skeleton keeps the IDriver shape buildable
+ // so the Galaxy:Backend flag (PR 4.W) can register the driver factory now.
+ _logger.LogInformation(
+ "GalaxyDriver {InstanceId} initializing — endpoint={Endpoint} clientName={ClientName} (skeleton; real gateway connect in PR 4.1+)",
+ _driverInstanceId, _options.Gateway.Endpoint, _options.MxAccess.ClientName);
+ _health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
+ {
+ // In-place config reapply. PR 4.5's reconnect supervisor will swap the
+ // gateway-client options under the lock; for the skeleton we just refresh health.
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ _health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task ShutdownAsync(CancellationToken cancellationToken)
+ {
+ if (_disposed) return Task.CompletedTask;
+ _logger.LogInformation("GalaxyDriver {InstanceId} shutting down", _driverInstanceId);
+ _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public DriverHealth GetHealth() => _health;
+
+ ///
+ public long GetMemoryFootprint() => 0; // PR 4.4 sets this from SubscriptionRegistry size.
+
+ ///
+ public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ // No owned IDisposables until PR 4.2's GalaxyMxSession lands.
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs
new file mode 100644
index 0000000..b9cc8ce
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs
@@ -0,0 +1,121 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.Logging;
+using ZB.MOM.WW.OtOpcUa.Core.Hosting;
+using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
+
+///
+/// Static factory registration helper for . Mirrors
+/// GalaxyProxyDriverFactoryExtensions / ModbusDriverFactoryExtensions.
+/// Server's Program.cs calls once at startup; the driver
+/// bootstrap pipeline materialises DriverInstance rows whose DriverType matches
+/// into live instances.
+///
+///
+/// The driver-type name "GalaxyMxGateway" is intentionally distinct from the
+/// legacy proxy's "Galaxy" so both factories can be registered simultaneously
+/// during parity testing (Phase 5). PR 4.W will add a server-side Galaxy:Backend
+/// switch that materialises a Galaxy DriverInstance under one or the other type name.
+///
+public static class GalaxyDriverFactoryExtensions
+{
+ public const string DriverTypeName = "GalaxyMxGateway";
+
+ public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null)
+ {
+ ArgumentNullException.ThrowIfNull(registry);
+ registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory));
+ }
+
+ /// Convenience for tests + standalone callers.
+ public static GalaxyDriver CreateInstance(string driverInstanceId, string driverConfigJson)
+ => CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null);
+
+ public static GalaxyDriver CreateInstance(
+ string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
+ ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
+
+ var dto = JsonSerializer.Deserialize(driverConfigJson, JsonOptions)
+ ?? throw new InvalidOperationException(
+ $"Galaxy driver config for '{driverInstanceId}' deserialised to null");
+
+ var options = new GalaxyDriverOptions(
+ Gateway: new GalaxyGatewayOptions(
+ Endpoint: dto.Gateway?.Endpoint
+ ?? throw new InvalidOperationException(
+ $"Galaxy driver '{driverInstanceId}' missing required Gateway.Endpoint"),
+ ApiKeySecretRef: dto.Gateway.ApiKeySecretRef
+ ?? throw new InvalidOperationException(
+ $"Galaxy driver '{driverInstanceId}' missing required Gateway.ApiKeySecretRef"),
+ UseTls: dto.Gateway.UseTls ?? true,
+ CaCertificatePath: dto.Gateway.CaCertificatePath,
+ ConnectTimeoutSeconds: dto.Gateway.ConnectTimeoutSeconds ?? 10,
+ DefaultCallTimeoutSeconds: dto.Gateway.DefaultCallTimeoutSeconds ?? 5,
+ StreamTimeoutSeconds: dto.Gateway.StreamTimeoutSeconds ?? 0),
+ MxAccess: new GalaxyMxAccessOptions(
+ ClientName: dto.MxAccess?.ClientName
+ ?? throw new InvalidOperationException(
+ $"Galaxy driver '{driverInstanceId}' missing required MxAccess.ClientName"),
+ PublishingIntervalMs: dto.MxAccess.PublishingIntervalMs ?? 1000,
+ WriteUserId: dto.MxAccess.WriteUserId ?? 0),
+ Repository: new GalaxyRepositoryOptions(
+ DiscoverPageSize: dto.Repository?.DiscoverPageSize ?? 5000,
+ WatchDeployEvents: dto.Repository?.WatchDeployEvents ?? true),
+ Reconnect: new GalaxyReconnectOptions(
+ InitialBackoffMs: dto.Reconnect?.InitialBackoffMs ?? 500,
+ MaxBackoffMs: dto.Reconnect?.MaxBackoffMs ?? 30_000,
+ ReplayOnSessionLost: dto.Reconnect?.ReplayOnSessionLost ?? true));
+
+ return new GalaxyDriver(driverInstanceId, options, loggerFactory?.CreateLogger());
+ }
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true,
+ };
+
+ internal sealed class GalaxyDriverConfigDto
+ {
+ public GatewayDto? Gateway { get; init; }
+ public MxAccessDto? MxAccess { get; init; }
+ public RepositoryDto? Repository { get; init; }
+ public ReconnectDto? Reconnect { get; init; }
+ }
+
+ internal sealed class GatewayDto
+ {
+ public string? Endpoint { get; init; }
+ public string? ApiKeySecretRef { get; init; }
+ public bool? UseTls { get; init; }
+ public string? CaCertificatePath { get; init; }
+ public int? ConnectTimeoutSeconds { get; init; }
+ public int? DefaultCallTimeoutSeconds { get; init; }
+ public int? StreamTimeoutSeconds { get; init; }
+ }
+
+ internal sealed class MxAccessDto
+ {
+ public string? ClientName { get; init; }
+ public int? PublishingIntervalMs { get; init; }
+ public int? WriteUserId { get; init; }
+ }
+
+ internal sealed class RepositoryDto
+ {
+ public int? DiscoverPageSize { get; init; }
+ public bool? WatchDeployEvents { get; init; }
+ }
+
+ internal sealed class ReconnectDto
+ {
+ public int? InitialBackoffMs { get; init; }
+ public int? MaxBackoffMs { get; init; }
+ public bool? ReplayOnSessionLost { get; init; }
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj
new file mode 100644
index 0000000..4a2812d
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net10.0
+ AnyCPU;x64
+ enable
+ enable
+ latest
+ true
+ true
+ $(NoWarn);CS1591
+ ZB.MOM.WW.OtOpcUa.Driver.Galaxy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverFactoryTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverFactoryTests.cs
new file mode 100644
index 0000000..e0ffc6d
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverFactoryTests.cs
@@ -0,0 +1,166 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Core.Hosting;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests;
+
+///
+/// Smoke tests for the PR 4.0 driver skeleton. The IDriver shape, factory parsing,
+/// and lifecycle methods land in PR 4.0; capability bodies (browse / read / write /
+/// subscribe / health forwarder / probe watcher) are tested in PRs 4.1–4.7 each
+/// against their own seam.
+///
+public sealed class GalaxyDriverFactoryTests
+{
+ private const string MinimalConfig = """
+ {
+ "Gateway": { "Endpoint": "https://mxgw.test:5001", "ApiKeySecretRef": "galaxy:apiKey" },
+ "MxAccess": { "ClientName": "OtOpcUa-A" }
+ }
+ """;
+
+ [Fact]
+ public void CreateInstance_ParsesMinimalConfig_AndAppliesDefaults()
+ {
+ var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-instance-a", MinimalConfig);
+
+ driver.DriverInstanceId.ShouldBe("galaxy-instance-a");
+ driver.DriverType.ShouldBe(GalaxyDriverFactoryExtensions.DriverTypeName);
+ driver.Options.Gateway.Endpoint.ShouldBe("https://mxgw.test:5001");
+ driver.Options.Gateway.ApiKeySecretRef.ShouldBe("galaxy:apiKey");
+ driver.Options.Gateway.UseTls.ShouldBeTrue();
+ driver.Options.Gateway.ConnectTimeoutSeconds.ShouldBe(10);
+ driver.Options.MxAccess.ClientName.ShouldBe("OtOpcUa-A");
+ driver.Options.MxAccess.PublishingIntervalMs.ShouldBe(1000);
+ driver.Options.Repository.DiscoverPageSize.ShouldBe(5000);
+ driver.Options.Reconnect.ReplayOnSessionLost.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void CreateInstance_OverridesDefaults_FromFullConfig()
+ {
+ const string fullConfig = """
+ {
+ "Gateway": {
+ "Endpoint": "https://mxgw.prod:5001",
+ "ApiKeySecretRef": "secret:abc",
+ "UseTls": false,
+ "CaCertificatePath": "C:/certs/ca.crt",
+ "ConnectTimeoutSeconds": 5,
+ "DefaultCallTimeoutSeconds": 3,
+ "StreamTimeoutSeconds": 60
+ },
+ "MxAccess": {
+ "ClientName": "OtOpcUa-Prod",
+ "PublishingIntervalMs": 250,
+ "WriteUserId": 17
+ },
+ "Repository": { "DiscoverPageSize": 1000, "WatchDeployEvents": false },
+ "Reconnect": { "InitialBackoffMs": 100, "MaxBackoffMs": 5000, "ReplayOnSessionLost": false }
+ }
+ """;
+
+ var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-prod", fullConfig);
+
+ driver.Options.Gateway.UseTls.ShouldBeFalse();
+ driver.Options.Gateway.CaCertificatePath.ShouldBe("C:/certs/ca.crt");
+ driver.Options.Gateway.ConnectTimeoutSeconds.ShouldBe(5);
+ driver.Options.MxAccess.PublishingIntervalMs.ShouldBe(250);
+ driver.Options.MxAccess.WriteUserId.ShouldBe(17);
+ driver.Options.Repository.DiscoverPageSize.ShouldBe(1000);
+ driver.Options.Repository.WatchDeployEvents.ShouldBeFalse();
+ driver.Options.Reconnect.InitialBackoffMs.ShouldBe(100);
+ driver.Options.Reconnect.ReplayOnSessionLost.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void CreateInstance_MissingEndpoint_Throws()
+ {
+ const string bad = """{"Gateway":{"ApiKeySecretRef":"x"},"MxAccess":{"ClientName":"y"}}""";
+ Should.Throw(
+ () => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("Gateway.Endpoint");
+ }
+
+ [Fact]
+ public void CreateInstance_MissingApiKey_Throws()
+ {
+ const string bad = """{"Gateway":{"Endpoint":"x"},"MxAccess":{"ClientName":"y"}}""";
+ Should.Throw(
+ () => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("ApiKeySecretRef");
+ }
+
+ [Fact]
+ public void CreateInstance_MissingClientName_Throws()
+ {
+ const string bad = """{"Gateway":{"Endpoint":"x","ApiKeySecretRef":"y"}}""";
+ Should.Throw(
+ () => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("MxAccess.ClientName");
+ }
+
+ [Fact]
+ public void Register_AddsFactoryToRegistry()
+ {
+ var registry = new DriverFactoryRegistry();
+ GalaxyDriverFactoryExtensions.Register(registry);
+
+ registry.RegisteredTypes.ShouldContain(GalaxyDriverFactoryExtensions.DriverTypeName);
+
+ var factory = registry.TryGet(GalaxyDriverFactoryExtensions.DriverTypeName);
+ factory.ShouldNotBeNull();
+
+ var driver = factory!.Invoke("galaxy-x", MinimalConfig);
+ driver.ShouldNotBeNull();
+ driver.DriverInstanceId.ShouldBe("galaxy-x");
+ driver.DriverType.ShouldBe(GalaxyDriverFactoryExtensions.DriverTypeName);
+ }
+
+ [Fact]
+ public async Task DriverLifecycle_InitializeShutdown_ToggleHealth()
+ {
+ var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
+ driver.GetHealth().State.ShouldBe(DriverState.Unknown);
+
+ await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
+ driver.GetHealth().State.ShouldBe(DriverState.Healthy);
+ driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
+
+ await driver.ShutdownAsync(CancellationToken.None);
+ driver.GetHealth().State.ShouldBe(DriverState.Unknown);
+
+ driver.GetMemoryFootprint().ShouldBe(0);
+ await driver.FlushOptionalCachesAsync(CancellationToken.None); // no-op shouldn't throw
+ }
+
+ [Fact]
+ public async Task ReinitializeAsync_RefreshesHealth()
+ {
+ var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
+ await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
+ var firstStamp = driver.GetHealth().LastSuccessfulRead!.Value;
+
+ // Force a measurable clock delta so the comparison is stable on fast machines.
+ await Task.Delay(20);
+ await driver.ReinitializeAsync(MinimalConfig, CancellationToken.None);
+
+ driver.GetHealth().State.ShouldBe(DriverState.Healthy);
+ driver.GetHealth().LastSuccessfulRead!.Value.ShouldBeGreaterThan(firstStamp);
+ }
+
+ [Fact]
+ public void Dispose_IsIdempotent_AndShutdownAfterDisposeIsHarmless()
+ {
+ var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
+ driver.Dispose();
+ Should.NotThrow(() => driver.Dispose());
+ }
+
+ [Fact]
+ public async Task InitializeAfterDispose_Throws()
+ {
+ var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig);
+ driver.Dispose();
+ await Should.ThrowAsync(() =>
+ driver.InitializeAsync(MinimalConfig, CancellationToken.None));
+ }
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj
new file mode 100644
index 0000000..e5c3a06
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ true
+ ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+