From 679484ae78eae618eb51888fd2518f6c70b33fa1 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 15 Jul 2026 17:18:02 -0400 Subject: [PATCH] =?UTF-8?q?feat(browse):=20Wave-0=20Batch=20B=20=E2=80=94?= =?UTF-8?q?=20AbCip/TwinCAT/FOCAS=20opt=20into=20online=20discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 3/4/5: SupportsOnlineDiscovery => true on AbCipDriver, TwinCATDriver, FocasDriver (their device enumeration is real, config-gated on EnableControllerBrowse / FixedTree.Enabled which PatchForBrowse guarantees at open). One gate fact per driver test project. --- ...overy-browser-implementation.md.tasks.json | 14 ++++++------- .../AbCipDriver.cs | 4 ++++ .../FocasDriver.cs | 6 ++++++ .../TwinCATDriver.cs | 4 ++++ .../AbCipDiscoveryGateTests.cs | 19 ++++++++++++++++++ .../FocasDiscoveryGateTests.cs | 20 +++++++++++++++++++ .../TwinCATDiscoveryGateTests.cs | 19 ++++++++++++++++++ 7 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDiscoveryGateTests.cs create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasDiscoveryGateTests.cs create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATDiscoveryGateTests.cs diff --git a/docs/plans/2026-07-15-universal-discovery-browser-implementation.md.tasks.json b/docs/plans/2026-07-15-universal-discovery-browser-implementation.md.tasks.json index 741e7914..0758de6c 100644 --- a/docs/plans/2026-07-15-universal-discovery-browser-implementation.md.tasks.json +++ b/docs/plans/2026-07-15-universal-discovery-browser-implementation.md.tasks.json @@ -1,12 +1,12 @@ { "planPath": "docs/plans/2026-07-15-universal-discovery-browser-implementation.md", "tasks": [ - {"id": 0, "subject": "Task 0: Branch setup (feat/universal-discovery-browser)", "status": "pending"}, - {"id": 1, "subject": "Task 1: ITagDiscovery.SupportsOnlineDiscovery default member", "status": "pending", "blockedBy": [0], "parallelWith": [2, 13]}, - {"id": 2, "subject": "Task 2: Commons project references (Core.Abstractions)", "status": "pending", "blockedBy": [0], "parallelWith": [1, 13]}, - {"id": 3, "subject": "Task 3: AbCip opt-in SupportsOnlineDiscovery=true", "status": "pending", "blockedBy": [1], "parallelWith": [4, 5]}, - {"id": 4, "subject": "Task 4: TwinCAT opt-in SupportsOnlineDiscovery=true", "status": "pending", "blockedBy": [1], "parallelWith": [3, 5]}, - {"id": 5, "subject": "Task 5: FOCAS opt-in SupportsOnlineDiscovery=true", "status": "pending", "blockedBy": [1], "parallelWith": [3, 4]}, + {"id": 0, "subject": "Task 0: Branch setup (feat/universal-discovery-browser)", "status": "completed"}, + {"id": 1, "subject": "Task 1: ITagDiscovery.SupportsOnlineDiscovery default member", "status": "completed", "blockedBy": [0], "parallelWith": [2, 13]}, + {"id": 2, "subject": "Task 2: Commons project references (Core.Abstractions)", "status": "completed", "blockedBy": [0], "parallelWith": [1, 13]}, + {"id": 3, "subject": "Task 3: AbCip opt-in SupportsOnlineDiscovery=true", "status": "completed", "blockedBy": [1], "parallelWith": [4, 5]}, + {"id": 4, "subject": "Task 4: TwinCAT opt-in SupportsOnlineDiscovery=true", "status": "completed", "blockedBy": [1], "parallelWith": [3, 5]}, + {"id": 5, "subject": "Task 5: FOCAS opt-in SupportsOnlineDiscovery=true", "status": "completed", "blockedBy": [1], "parallelWith": [3, 4]}, {"id": 6, "subject": "Task 6: CapturingAddressSpaceBuilder + captured-tree model", "status": "pending", "blockedBy": [1, 2]}, {"id": 7, "subject": "Task 7: CapturedTreeBrowseSession", "status": "pending", "blockedBy": [6]}, {"id": 8, "subject": "Task 8: IUniversalDriverBrowser + DiscoveryDriverBrowser core open path", "status": "pending", "blockedBy": [7]}, @@ -14,7 +14,7 @@ {"id": 10, "subject": "Task 10: Capture coalescing + MaxConcurrentCaptures cap", "status": "pending", "blockedBy": [9], "parallelWith": [12]}, {"id": 11, "subject": "Task 11: BrowserSessionService universal fallback + CanBrowse", "status": "pending", "blockedBy": [8], "parallelWith": [9]}, {"id": 12, "subject": "Task 12: DI registration in AddAdminUI", "status": "pending", "blockedBy": [11], "parallelWith": [10]}, - {"id": 13, "subject": "Task 13: TagModal -> editor parameter plumbing (DriverType + GetDriverConfigJson)", "status": "pending", "blockedBy": [0], "parallelWith": [1, 2]}, + {"id": 13, "subject": "Task 13: TagModal -> editor parameter plumbing (DriverType + GetDriverConfigJson)", "status": "completed", "blockedBy": [0], "parallelWith": [1, 2]}, {"id": 14, "subject": "Task 14: AbCip picker body universal browse mode", "status": "pending", "blockedBy": [10, 12, 13, 3], "parallelWith": [15, 16]}, {"id": 15, "subject": "Task 15: TwinCAT picker body universal browse mode", "status": "pending", "blockedBy": [10, 12, 13, 4], "parallelWith": [14, 16]}, {"id": 16, "subject": "Task 16: FOCAS picker body universal browse mode", "status": "pending", "blockedBy": [10, 12, 13, 5], "parallelWith": [14, 15]}, 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 ca7e045f..8b2cd78f 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs @@ -999,6 +999,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; + /// Controller enumeration is real (CIP Symbol Object walk) — config-gated on + /// EnableControllerBrowse, which the universal browser's PatchForBrowse guarantees at open time. + public bool SupportsOnlineDiscovery => true; + /// public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { 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 9fca75f6..9df8851c 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs @@ -427,6 +427,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, // node set is non-empty and stable. public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.UntilStable; + /// The FixedTree device folder is real enumeration — config-gated on FixedTree.Enabled, + /// which the universal browser's PatchForBrowse turns on. FOCAS is the UntilStable driver: the + /// FixedTree cache fills a couple of seconds post-connect, so the browser's settle loop re-captures + /// until the node set is non-empty and stable. + public bool SupportsOnlineDiscovery => true; + /// public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { 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 7420e6a7..1460d0dc 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs @@ -380,6 +380,10 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery // discovery pass is sufficient. public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; + /// ADS symbol upload is real device enumeration — config-gated on + /// EnableControllerBrowse, which the universal browser's PatchForBrowse guarantees at open time. + public bool SupportsOnlineDiscovery => true; + /// public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDiscoveryGateTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDiscoveryGateTests.cs new file mode 100644 index 00000000..e832f236 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDiscoveryGateTests.cs @@ -0,0 +1,19 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.AbCip; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests; + +[Trait("Category", "Unit")] +public sealed class AbCipDiscoveryGateTests +{ + /// AbCip opts into the universal discovery browser — its CIP Symbol Object walk is real + /// device enumeration (config-gated on EnableControllerBrowse, guaranteed by PatchForBrowse at open). + [Fact] + public void SupportsOnlineDiscovery_IsTrue() + { + var drv = new AbCipDriver(new AbCipDriverOptions(), "drv-1"); + ((ITagDiscovery)drv).SupportsOnlineDiscovery.ShouldBeTrue(); + } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasDiscoveryGateTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasDiscoveryGateTests.cs new file mode 100644 index 00000000..a920d596 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasDiscoveryGateTests.cs @@ -0,0 +1,20 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; + +namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; + +[Trait("Category", "Unit")] +public sealed class FocasDiscoveryGateTests +{ + /// FOCAS opts into the universal discovery browser — its FixedTree device folder is real + /// enumeration (config-gated on FixedTree.Enabled, turned on by PatchForBrowse). FOCAS is the + /// UntilStable driver: the browser's settle loop re-captures until the FixedTree cache fills. + [Fact] + public void SupportsOnlineDiscovery_IsTrue() + { + var drv = new FocasDriver(new FocasDriverOptions(), "drv-1"); + ((ITagDiscovery)drv).SupportsOnlineDiscovery.ShouldBeTrue(); + } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATDiscoveryGateTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATDiscoveryGateTests.cs new file mode 100644 index 00000000..97da1f2c --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATDiscoveryGateTests.cs @@ -0,0 +1,19 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; + +namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests; + +[Trait("Category", "Unit")] +public sealed class TwinCATDiscoveryGateTests +{ + /// TwinCAT opts into the universal discovery browser — its ADS symbol upload is real + /// device enumeration (config-gated on EnableControllerBrowse, guaranteed by PatchForBrowse at open). + [Fact] + public void SupportsOnlineDiscovery_IsTrue() + { + var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1"); + ((ITagDiscovery)drv).SupportsOnlineDiscovery.ShouldBeTrue(); + } +}