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]