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 16f626da..773acc64 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs @@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; /// only at PR 1 time; read / write / discovery / subscribe / probe / /// host-resolver capabilities ship in PRs 2 and 3. /// -public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, +public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscovery, IBatchSubscribable, IHostConnectivityProbe, IPerCallHostResolver, ISkippedTagReporter, IDisposable, IAsyncDisposable { /// Authored tags this driver could not build into its poll table. @@ -64,6 +64,12 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag /// public event EventHandler? OnDataChange; + /// + /// One raise per poll cycle carrying every tag that changed in it (review 02 P1). Raised alongside the per-tag + /// ; a consumer subscribes to exactly one of the two. + /// + public event EventHandler? OnDataChangeBatch; + /// /// Occurs when host status changes. /// @@ -99,6 +105,8 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)), onError: HandlePollError, backoffCap: PollBackoffCap, + onBatch: (handle, items) => + OnDataChangeBatch?.Invoke(this, new DataChangeBatchEventArgs(handle, items)), // The engine owns no Meter (Core.Abstractions takes no metrics dependency), so this // closure IS the production consumer of its overrun signal. onOverrun: _ => OtOpcUaTelemetry.PollOverruns.Add(1, diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBatchSubscribeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBatchSubscribeTests.cs new file mode 100644 index 00000000..08c422b0 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBatchSubscribeTests.cs @@ -0,0 +1,50 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests; + +/// +/// Review 02 P1: AbLegacy ran but did not implement , +/// so every changed tag paid its own trip through the actor chain. It now raises one +/// per poll cycle, alongside the per-tag event. +/// +[Trait("Category", "Unit")] +public sealed class AbLegacyBatchSubscribeTests +{ + private const string Host = "ab://10.0.0.5/1,0"; + + [Fact] + public async Task One_batch_per_poll_carries_every_changed_tag_and_per_tag_events_still_fire() + { + var factory = new FakeAbLegacyTagFactory { Customise = p => new FakeAbLegacyTag(p) { Value = p.TagName == "N7:0" ? 1 : 2 } }; + var drv = new AbLegacyDriver(new AbLegacyDriverOptions + { + Devices = [new AbLegacyDeviceOptions(Host)], + RawTags = AbLegacyRawTags.Entries( + new AbLegacyTagDefinition("A", Host, "N7:0", AbLegacyDataType.Int), + new AbLegacyTagDefinition("B", Host, "N9:0", AbLegacyDataType.Int)), + Probe = new AbLegacyProbeOptions { Enabled = false }, + }, "drv-batch", factory); + await drv.InitializeAsync("{}", CancellationToken.None); + IBatchSubscribable batching = drv; // the capability the actor prefers + + var batches = new System.Collections.Concurrent.ConcurrentQueue(); + var singles = new System.Collections.Concurrent.ConcurrentQueue(); + batching.OnDataChangeBatch += (_, e) => batches.Enqueue(e); + drv.OnDataChange += (_, e) => singles.Enqueue(e); + + var handle = await drv.SubscribeAsync(["A", "B"], TimeSpan.FromMilliseconds(100), CancellationToken.None); + var deadline = DateTime.UtcNow.AddSeconds(5); + while (batches.IsEmpty && DateTime.UtcNow < deadline) await Task.Delay(10); + + batches.TryPeek(out var first).ShouldBeTrue(); + first!.SubscriptionHandle.ShouldBe(handle); + first.Items.Select(i => i.FullReference).ShouldBe(new[] { "A", "B" }, ignoreOrder: true); + first.Items.Single(i => i.FullReference == "A").Snapshot.Value.ShouldBe(1); + singles.Count.ShouldBeGreaterThanOrEqualTo(2); // positive control: the legacy per-tag event still fires + + await drv.UnsubscribeAsync(handle, CancellationToken.None); + await drv.ShutdownAsync(CancellationToken.None); + } +}