feat(otopcua): add ITagDiscovery.RediscoverPolicy + per-driver assignments (follow-up B)

This commit is contained in:
Joseph Doherty
2026-06-26 12:18:44 -04:00
parent c2c368dcec
commit a378b572af
7 changed files with 66 additions and 0 deletions
@@ -1,5 +1,17 @@
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>How aggressively the host re-runs post-connect discovery for this driver.</summary>
public enum DiscoveryRediscoverPolicy
{
/// <summary>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).</summary>
UntilStable,
/// <summary>Run exactly one discovery pass on connect (drivers that discover synchronously in DiscoverAsync).</summary>
Once,
/// <summary>Never run post-connect discovery.</summary>
Never,
}
/// <summary>
/// Driver capability for discovering tags and hierarchy from the backend.
/// Streams discovered nodes into <see cref="IAddressSpaceBuilder"/> rather than
@@ -14,4 +26,7 @@ public interface ITagDiscovery
/// <param name="builder">The address space builder to stream discovered nodes into.</param>
/// <param name="cancellationToken">A cancellation token for the discovery operation.</param>
Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken);
/// <summary>Post-connect re-discovery policy. Default preserves the original retry-until-stable behavior.</summary>
DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.UntilStable;
}
@@ -998,6 +998,14 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- ITagDiscovery ----
/// <summary>
/// Run-once: <see cref="DiscoverAsync"/> 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.
/// </summary>
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
/// <summary>
/// Stream the driver's tag set into the builder. Pre-declared tags from
/// <see cref="AbCipDriverOptions.Tags"/> emit first; optionally, the
@@ -422,6 +422,13 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
// ---- ITagDiscovery ----
/// <summary>
/// Run-once: <see cref="DiscoverAsync"/> 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.
/// </summary>
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
/// <summary>
/// Discovers tags and populates the address space asynchronously.
/// </summary>
@@ -401,6 +401,14 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- ITagDiscovery ----
/// <summary>
/// Retry-until-stable: the FixedTree subtree is filled in asynchronously by
/// <see cref="FixedTreeLoopAsync"/> a couple of seconds AFTER connect, so the first
/// post-connect <see cref="DiscoverAsync"/> pass would miss it — the host must re-run
/// discovery until the captured node set is non-empty and stable.
/// </summary>
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.UntilStable;
/// <summary>Discovers tags and builds the OPC UA address space asynchronously.</summary>
/// <param name="builder">The address space builder for constructing the OPC UA namespace.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
@@ -826,6 +826,14 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
// ---- ITagDiscovery ----
/// <summary>
/// Run-once: <see cref="DiscoverAsync"/> 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.
/// </summary>
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
/// <summary>Discovers the remote OPC UA server's address space and materializes it through the supplied builder.</summary>
/// <param name="builder">Address space builder for materializing discovered nodes.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
@@ -377,6 +377,14 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
// ---- ITagDiscovery ----
/// <summary>
/// Run-once: <see cref="DiscoverAsync"/> 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.
/// </summary>
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
/// <summary>Discovers devices and tags from ADS configuration and optionally controller symbols.</summary>
/// <param name="builder">Address space builder for adding discovered nodes.</param>
/// <param name="cancellationToken">Cancellation token.</param>
@@ -203,6 +203,18 @@ public sealed class FocasScaffoldingTests
drv.DriverInstanceId.ShouldBe("drv-1");
}
/// <summary>
/// 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.
/// </summary>
[Fact]
public void RediscoverPolicy_is_UntilStable()
{
var drv = new FocasDriver(new FocasDriverOptions(), "drv-1");
drv.RediscoverPolicy.ShouldBe(DiscoveryRediscoverPolicy.UntilStable);
}
/// <summary>Verifies InitializeAsync parses device addresses correctly.</summary>
[Fact]
public async Task InitializeAsync_parses_device_addresses()