From a378b572af2c6e8c167383f6394d8f7d908ba316 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 26 Jun 2026 12:18:44 -0400 Subject: [PATCH] feat(otopcua): add ITagDiscovery.RediscoverPolicy + per-driver assignments (follow-up B) --- .../ITagDiscovery.cs | 15 +++++++++++++++ .../ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs | 8 ++++++++ .../AbLegacyDriver.cs | 7 +++++++ .../ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs | 8 ++++++++ .../OpcUaClientDriver.cs | 8 ++++++++ .../TwinCATDriver.cs | 8 ++++++++ .../FocasScaffoldingTests.cs | 12 ++++++++++++ 7 files changed, 66 insertions(+) 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()