diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs index 36b21186..c083d592 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs @@ -51,9 +51,12 @@ public sealed class AbLegacyDriverOptions /// /// Whether scalar tags on adjacent elements of one N / B / F / L file are read as ONE multi-element PCCC read /// (driver-config key coalesceReads, default ). A block the device refuses falls - /// back to per-tag reads on its own; set this false to read every tag singly. + /// back to per-tag reads on its own; set this false to read every tag singly. (the key + /// absent) means the default, ON - and null is never written, so a save through any serializer (the AdminUI + /// form included) keeps a defaulted driver defaulted instead of pinning today's default (review 07 r0 M9). /// - public bool CoalesceReads { get; init; } = true; + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull)] + public bool? CoalesceReads { get; init; } /// /// Timeout for the AdminUI Test Connect probe, in seconds. The AdminUI clamps to a diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.Coalescing.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.Coalescing.cs index 401ce098..416d1b62 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.Coalescing.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.Coalescing.cs @@ -72,7 +72,7 @@ public sealed partial class AbLegacyDriver /// private List PlanReadUnits(DeviceState device, List items) { - if (!_options.CoalesceReads || items.Count < 2) return [.. items.Select(i => new ReadUnit(i, null))]; + if (_options.CoalesceReads == false || items.Count < 2) return [.. items.Select(i => new ReadUnit(i, null))]; var units = new List(items.Count); var groups = new Dictionary<(string Letter, int File, AbLegacyDataType Type), List>(); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs index d06effd9..418ead02 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs @@ -97,7 +97,7 @@ public static class AbLegacyDriverFactoryExtensions ProbeAddress = dto.Probe?.ProbeAddress ?? "S:0", }, Timeout = PositiveTimeoutOrDefault(dto.TimeoutMs, 2_000), - CoalesceReads = dto.CoalesceReads ?? true, + CoalesceReads = dto.CoalesceReads, // Absent ⇒ null ("use the default" = Libplctag, resolved by AbLegacyTransportResolver); // a bad value still fails exactly as any other bad enum field does. Transport = string.IsNullOrWhiteSpace(dto.Transport) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/AbLegacyDriverForm.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/AbLegacyDriverForm.razor index a938652b..81ec9d9c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/AbLegacyDriverForm.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/AbLegacyDriverForm.razor @@ -208,7 +208,7 @@ _loadedProbeTimeout = o.Probe.Timeout, TimeoutSeconds = (int)o.Timeout.TotalSeconds, Transport = o.Transport, - CoalesceReads = o.CoalesceReads, + CoalesceReads = o.CoalesceReads ?? true, ProbeEnabled = o.Probe.Enabled, ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds, ProbeTimeoutSeconds = (int)o.Probe.Timeout.TotalSeconds, @@ -229,7 +229,8 @@ }, Timeout = DriverConfigMs.Save(TimeoutSeconds, _loadedTimeout), Transport = Transport, - CoalesceReads = CoalesceReads, + // On = the default: OMIT the key (never pin a concrete default); off is the only value written. + CoalesceReads = CoalesceReads ? null : false, ProbeTimeoutSeconds = AdminProbeTimeoutSeconds, }; } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadCoalescingTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadCoalescingTests.cs index 0dc8cf89..b1adfc1a 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadCoalescingTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadCoalescingTests.cs @@ -180,5 +180,5 @@ public sealed class AbLegacyReadCoalescingTests [InlineData("""{"coalesceReads":false,"devices":[{"hostAddress":"ab://10.0.0.5/1,0"}]}""", false)] [InlineData("""{"coalesceReads":true,"someFutureKey":1,"devices":[{"hostAddress":"ab://10.0.0.5/1,0"}]}""", true)] public void ParseOptions_reads_coalesceReads_defaulting_true(string json, bool expected) => - AbLegacyDriverFactoryExtensions.ParseOptions("drv", json).CoalesceReads.ShouldBe(expected); + (AbLegacyDriverFactoryExtensions.ParseOptions("drv", json).CoalesceReads != false).ShouldBe(expected); } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbLegacyDriverPageFormSerializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbLegacyDriverPageFormSerializationTests.cs index 162336cc..7dc4c263 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbLegacyDriverPageFormSerializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/AbLegacyDriverPageFormSerializationTests.cs @@ -269,8 +269,12 @@ public sealed class AbLegacyDriverPageFormSerializationTests var saved = AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices), original); - ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.AbLegacyDriverFactoryExtensions.ParseOptions("t", saved) - .CoalesceReads.ShouldBe(edited); + (ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.AbLegacyDriverFactoryExtensions.ParseOptions("t", saved) + .CoalesceReads != false).ShouldBe(edited); + // Review 07 r0 M9: ON is the default and is OMITTED, never pinned; only an explicit off is written. + var root = System.Text.Json.Nodes.JsonNode.Parse(saved)!.AsObject(); + if (edited) root.ContainsKey("coalesceReads").ShouldBeFalse(); + else root["coalesceReads"]!.GetValue().ShouldBeFalse(); } [Fact]