diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs
index e66f3a9e..e393cac4 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs
@@ -1,5 +1,17 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+/// How aggressively the host re-runs post-connect discovery for this driver.
+public enum DiscoveryRediscoverPolicy
+{
+ /// Retry every interval up to the cap or until the captured set is non-empty and stable
+ /// (for drivers whose discovered shape fills in asynchronously after connect, e.g. the FOCAS FixedTree).
+ UntilStable,
+ /// Run exactly one discovery pass on connect (drivers that discover synchronously in DiscoverAsync).
+ Once,
+ /// Never run post-connect discovery.
+ Never,
+}
+
///
/// Driver capability for discovering tags and hierarchy from the backend.
/// Streams discovered nodes into rather than
@@ -14,4 +26,7 @@ public interface ITagDiscovery
/// The address space builder to stream discovered nodes into.
/// A cancellation token for the discovery operation.
Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken);
+
+ /// Post-connect re-discovery policy. Default preserves the original retry-until-stable behavior.
+ DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.UntilStable;
}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
index 1e76a100..17bfc0b8 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
@@ -998,6 +998,14 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- ITagDiscovery ----
+ ///
+ /// Run-once: emits pre-declared tags and (when
+ /// EnableControllerBrowse is set) fully awaits the @tags symbol-table walk + UDT-shape
+ /// resolution within the single call, streaming the complete node set in one pass —
+ /// nothing fills in asynchronously after connect, so a single discovery pass is sufficient.
+ ///
+ public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
+
///
/// Stream the driver's tag set into the builder. Pre-declared tags from
/// emit first; optionally, the
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs
index 0c3e3c3a..547e109c 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs
@@ -422,6 +422,13 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
// ---- ITagDiscovery ----
+ ///
+ /// Run-once: emits the complete node set synchronously from
+ /// the configured device/tag tables within a single pass — there is no shape that fills
+ /// in asynchronously after connect, so a single discovery pass is sufficient.
+ ///
+ public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
+
///
/// Discovers tags and populates the address space asynchronously.
///
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 305efd21..1e588f13 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs
@@ -401,6 +401,14 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- ITagDiscovery ----
+ ///
+ /// Retry-until-stable: the FixedTree subtree is filled in asynchronously by
+ /// a couple of seconds AFTER connect, so the first
+ /// post-connect pass would miss it — the host must re-run
+ /// discovery until the captured node set is non-empty and stable.
+ ///
+ public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.UntilStable;
+
/// Discovers tags and builds the OPC UA address space asynchronously.
/// The address space builder for constructing the OPC UA namespace.
/// Cancellation token for the operation.
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs
index ee2efaa7..6065f072 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs
@@ -826,6 +826,14 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
// ---- ITagDiscovery ----
+ ///
+ /// Run-once: recursively browses the remote server's address
+ /// space and registers every variable within the single call (browse + enrich passes are
+ /// fully awaited) — nothing fills in asynchronously after connect, so a single discovery
+ /// pass is sufficient.
+ ///
+ public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
+
/// Discovers the remote OPC UA server's address space and materializes it through the supplied builder.
/// Address space builder for materializing discovered nodes.
/// Cancellation token for the operation.
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs
index 4fa3eee0..cc5ed79d 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs
@@ -377,6 +377,14 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
// ---- ITagDiscovery ----
+ ///
+ /// Run-once: emits pre-declared tags and (when
+ /// EnableControllerBrowse is set) fully awaits the controller symbol browse within the
+ /// single call, streaming the complete node set in one pass — nothing fills in
+ /// asynchronously after connect, so a single discovery pass is sufficient.
+ ///
+ public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
+
/// Discovers devices and tags from ADS configuration and optionally controller symbols.
/// Address space builder for adding discovered nodes.
/// Cancellation token.
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 290074b5..4489c2ed 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
@@ -203,6 +203,18 @@ public sealed class FocasScaffoldingTests
drv.DriverInstanceId.ShouldBe("drv-1");
}
+ ///
+ /// Verifies FOCAS opts into retry-until-stable post-connect re-discovery — its
+ /// FixedTree subtree is populated asynchronously by a background loop a couple of
+ /// seconds after connect, so a single DiscoverAsync pass would miss it.
+ ///
+ [Fact]
+ public void RediscoverPolicy_is_UntilStable()
+ {
+ var drv = new FocasDriver(new FocasDriverOptions(), "drv-1");
+ drv.RediscoverPolicy.ShouldBe(DiscoveryRediscoverPolicy.UntilStable);
+ }
+
/// Verifies InitializeAsync parses device addresses correctly.
[Fact]
public async Task InitializeAsync_parses_device_addresses()