diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Messages/Admin/TestDriverConnect.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Messages/Admin/TestDriverConnect.cs
new file mode 100644
index 00000000..dafdaf47
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Messages/Admin/TestDriverConnect.cs
@@ -0,0 +1,27 @@
+namespace ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
+
+///
+/// AdminUI → AdminOperationsActor request: probe one driver type's connection using
+/// the supplied JSON config. Routed through IAdminOperationsClient; reply is
+/// .
+///
+/// Must match an installed IDriverProbe.DriverType.
+/// Driver config as JSON (same shape as DriverInstance.DriverConfig).
+/// Per-probe timeout; server clamps to [1, 60].
+/// Round-trip correlation token.
+public sealed record TestDriverConnect(
+ string DriverType,
+ string ConfigJson,
+ int TimeoutSeconds,
+ Guid CorrelationId);
+
+/// Reply for .
+/// True iff the probe succeeded.
+/// Failure reason; null on success.
+/// Round-trip latency in milliseconds; null on failure or timeout.
+/// Echoes the request's correlation token.
+public sealed record TestDriverConnectResult(
+ bool Ok,
+ string? Message,
+ double? LatencyMs,
+ Guid CorrelationId);
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverProbe.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverProbe.cs
new file mode 100644
index 00000000..2043f3cf
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverProbe.cs
@@ -0,0 +1,27 @@
+namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+///
+/// Test-connect probe for one driver type. Implementations deserialize a driver-config
+/// JSON, attempt a cheap connection (TCP open, OPC UA session, gRPC ping — whatever the
+/// driver's native protocol supports), and report success/failure with latency. Probes
+/// MUST NOT mutate any persistent state; the AdminUI invokes them against transient
+/// config from the typed form, NOT against the persisted DriverInstance row.
+///
+public interface IDriverProbe
+{
+ /// DriverInstance.DriverType string this probe handles. Used for DI lookup.
+ string DriverType { get; }
+
+ ///
+ /// Run the probe with the supplied config + timeout. Honour for
+ /// timeout cancellation. Never throw on connection failure; instead return a result
+ /// with Ok = false + a message.
+ ///
+ Task ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct);
+}
+
+/// Outcome of a single call.
+/// True iff the probe reached its target and the handshake succeeded.
+/// Human-readable status; null on success.
+/// Wall-clock duration of the successful probe; null on failure.
+public sealed record DriverProbeResult(bool Ok, string? Message, TimeSpan? Latency);
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs
index b84e1a7f..671524b3 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs
@@ -7,6 +7,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
@@ -19,28 +20,35 @@ public sealed class AdminOperationsActor : ReceiveActor
{
private readonly IDbContextFactory _dbFactory;
private readonly IActorRef _coordinator;
+ private readonly IReadOnlyDictionary _probesByType;
private readonly ILoggingAdapter _log = Context.GetLogger();
/// Creates actor props for the admin operations actor.
/// Factory for creating config database contexts.
/// Reference to the deployment coordinator actor.
+ /// Driver probes registered in DI; keyed by DriverType (case-insensitive).
/// Props configured to create an AdminOperationsActor.
public static Props Props(
IDbContextFactory dbFactory,
- IActorRef coordinator) =>
- Akka.Actor.Props.Create(() => new AdminOperationsActor(dbFactory, coordinator));
+ IActorRef coordinator,
+ IEnumerable probes) =>
+ Akka.Actor.Props.Create(() => new AdminOperationsActor(dbFactory, coordinator, probes));
/// Initializes a new instance of the AdminOperationsActor.
/// Factory for creating config database contexts.
/// Reference to the deployment coordinator actor.
+ /// Driver probes registered in DI; keyed by DriverType (case-insensitive).
public AdminOperationsActor(
IDbContextFactory dbFactory,
- IActorRef coordinator)
+ IActorRef coordinator,
+ IEnumerable probes)
{
_dbFactory = dbFactory;
_coordinator = coordinator;
+ _probesByType = probes.ToDictionary(p => p.DriverType, StringComparer.OrdinalIgnoreCase);
ReceiveAsync(HandleStartDeploymentAsync);
+ ReceiveAsync(HandleTestDriverConnectAsync);
}
private async Task HandleStartDeploymentAsync(StartDeployment msg)
@@ -112,4 +120,51 @@ public sealed class AdminOperationsActor : ReceiveActor
msg.CorrelationId));
}
}
+
+ private async Task HandleTestDriverConnectAsync(TestDriverConnect msg)
+ {
+ var replyTo = Sender;
+ if (!_probesByType.TryGetValue(msg.DriverType, out var probe))
+ {
+ replyTo.Tell(new TestDriverConnectResult(
+ false,
+ $"No probe registered for driver type '{msg.DriverType}'.",
+ null,
+ msg.CorrelationId));
+ return;
+ }
+
+ var clampedSec = Math.Clamp(msg.TimeoutSeconds, 1, 60);
+ var timeout = TimeSpan.FromSeconds(clampedSec);
+ using var cts = new CancellationTokenSource(timeout);
+
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ try
+ {
+ var result = await probe.ProbeAsync(msg.ConfigJson, timeout, cts.Token);
+ sw.Stop();
+ replyTo.Tell(new TestDriverConnectResult(
+ result.Ok,
+ result.Message,
+ result.Ok ? sw.Elapsed.TotalMilliseconds : (double?)null,
+ msg.CorrelationId));
+ }
+ catch (OperationCanceledException)
+ {
+ replyTo.Tell(new TestDriverConnectResult(
+ false,
+ $"Probe timed out after {clampedSec}s.",
+ null,
+ msg.CorrelationId));
+ }
+ catch (Exception ex)
+ {
+ _log.Error(ex, "Probe for {DriverType} threw", msg.DriverType);
+ replyTo.Tell(new TestDriverConnectResult(
+ false,
+ ex.Message,
+ null,
+ msg.CorrelationId));
+ }
+ }
}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ServiceCollectionExtensions.cs
index 4e174da3..e9276742 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ServiceCollectionExtensions.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ServiceCollectionExtensions.cs
@@ -9,6 +9,7 @@ using ZB.MOM.WW.OtOpcUa.ControlPlane.Audit;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Coordinators;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Fleet;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Redundancy;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.ControlPlane;
@@ -59,7 +60,8 @@ public static class ServiceCollectionExtensions
{
var dbFactory = resolver.GetService>();
var coordinator = registry.Get();
- return AdminOperationsActor.Props(dbFactory, coordinator);
+ var probes = resolver.GetService>() ?? Enumerable.Empty();
+ return AdminOperationsActor.Props(dbFactory, coordinator, probes);
},
singletonOptions);
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ZB.MOM.WW.OtOpcUa.ControlPlane.csproj b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ZB.MOM.WW.OtOpcUa.ControlPlane.csproj
index d30b3e25..57565199 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ZB.MOM.WW.OtOpcUa.ControlPlane.csproj
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/ZB.MOM.WW.OtOpcUa.ControlPlane.csproj
@@ -21,6 +21,7 @@
+
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/AdminOperationsActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/AdminOperationsActorTests.cs
index e0f4d73b..559110cb 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/AdminOperationsActorTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/AdminOperationsActorTests.cs
@@ -8,6 +8,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests;
@@ -19,7 +20,7 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
{
var dbFactory = NewInMemoryDbFactory();
var coordinator = CreateTestProbe("coord");
- var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref));
+ var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty()));
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
@@ -60,7 +61,7 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
}
var coordinator = CreateTestProbe("coord");
- var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref));
+ var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty()));
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));