diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs
index 22a58824..7273c002 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs
@@ -89,6 +89,11 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
Volatile.Write(ref _health, new DriverHealth(DriverState.Initializing, null, null));
try
{
+ // Fail fast if the factory is a stub/unimplemented backend — the operator must
+ // see an actionable error at init rather than a phantom-Healthy driver that fails
+ // every read/write/subscribe silently (Driver.FOCAS-009).
+ _clientFactory.EnsureUsable();
+
foreach (var device in _options.Devices)
{
var addr = FocasHostAddress.TryParse(device.HostAddress)
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs
index dbc46cef..fedca8d4 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs
@@ -282,6 +282,18 @@ public interface IFocasClientFactory
/// Creates a new FOCAS client instance.
/// A new IFocasClient instance.
IFocasClient Create();
+
+ ///
+ /// Config-time usability probe called by before
+ /// any background loops start. Must NOT create a live wire client. Implementations that
+ /// are permanently non-functional (e.g. )
+ /// throw here so the driver faults immediately at
+ /// init rather than lazily on the first read.
+ ///
+ ///
+ /// Thrown by stub/unimplemented backends to force a fail-fast init fault.
+ ///
+ void EnsureUsable();
}
///
@@ -292,12 +304,22 @@ public interface IFocasClientFactory
///
public sealed class UnimplementedFocasClientFactory : IFocasClientFactory
{
+ private const string Message =
+ "FOCAS driver backend is 'unimplemented'. Switch to 'Backend: \"wire\"' in driver config " +
+ "once the CNC is provisioned — see docs/drivers/FOCAS.md.";
+
+ ///
+ /// Config-time probe — throws immediately so
+ /// faults the driver before any background loops start. This prevents the footgun where
+ /// the driver appears Healthy at init but every read/write/subscribe fails.
+ ///
+ /// Always thrown.
+ public void EnsureUsable() => throw new NotSupportedException(Message);
+
/// Creates a new client instance (always throws NotSupportedException).
/// Never returns; always throws NotSupportedException.
/// Always thrown to indicate backend is not yet provisioned.
- public IFocasClient Create() => throw new NotSupportedException(
- "FOCAS driver backend is 'unimplemented'. Switch to 'Backend: \"wire\"' in driver config " +
- "once the CNC is provisioned — see docs/drivers/FOCAS.md.");
+ public IFocasClient Create() => throw new NotSupportedException(Message);
}
///
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs
index fd6f70de..58eb4682 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs
@@ -427,6 +427,12 @@ public sealed class WireFocasClientFactory : IFocasClientFactory
_logger = logger;
}
+ ///
+ /// No-op usability probe — the wire backend is always usable at config time.
+ /// Implements .
+ ///
+ public void EnsureUsable() { }
+
/// Creates a new WireFocasClient instance.
/// A new IFocasClient implementation.
public IFocasClient Create() => new WireFocasClient(_logger);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs
index d5d61351..3cd1ed28 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FakeFocasClient.cs
@@ -169,6 +169,9 @@ internal sealed class FakeFocasClientFactory : IFocasClientFactory
/// Gets or sets a customization function for creating clients.
public Func? Customise { get; set; }
+ /// No-op usability probe — the fake factory is always usable.
+ public void EnsureUsable() { }
+
/// Creates a fake FOCAS client.
public IFocasClient Create()
{
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs
index f1ba244d..4d5e6c30 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs
@@ -261,4 +261,36 @@ public sealed class FocasScaffoldingTests
ex.Message.ShouldContain("wire");
ex.Message.ShouldContain("docs/drivers/FOCAS.md");
}
+
+ ///
+ /// Verifies a driver configured with the unimplemented backend faults at
+ /// InitializeAsync — not lazily on the first read. The operator must get an
+ /// actionable error immediately rather than a phantom-Healthy driver that
+ /// fails every read silently.
+ ///
+ [Fact]
+ public async Task Unimplemented_backend_faults_at_InitializeAsync_not_on_first_read()
+ {
+ var drv = new FocasDriver(
+ new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")] },
+ "drv-unimpl",
+ clientFactory: new UnimplementedFocasClientFactory());
+
+ var ex = await Should.ThrowAsync(
+ () => drv.InitializeAsync("{}", CancellationToken.None));
+
+ ex.Message.ShouldContain("unimplemented");
+ drv.GetHealth().State.ShouldBe(DriverState.Faulted);
+ }
+
+ ///
+ /// Verifies WireFocasClientFactory.EnsureUsable is a no-op — the wire backend
+ /// must still initialize clean (no false-positive faults from the probe).
+ ///
+ [Fact]
+ public void Wire_factory_EnsureUsable_does_not_throw()
+ {
+ var factory = new Wire.WireFocasClientFactory();
+ Should.NotThrow(() => factory.EnsureUsable());
+ }
}