perf(ablegacy): coalesce adjacent N/B/F/L scalar reads into one PCCC request

Review 07 P7 (rest): adjacent words on one data file were never coalesced
(one round trip per tag) although the array path proves the transport can read
a span. Within a device pass, scalar tags on one N/B/F/L file within 8
elements of each other (block <= 200 data bytes, under the 236-byte typed-read
limit) now ride one multi-element read and are decoded from their offsets with
exactly the CLR types the single-tag path returns (int / float / bool).

Only shapes the single path decodes identically are coalesced: indirect,
sub-element, array, file-number-less (S:/I:/O:) and width-mismatched tags keep
the single path. A block the device refuses (an authored element past the end
of the file, a native PLC-5 non-N file) falls back to per-tag reads for its
members and its shape is remembered; a comms fault fails the members together.

New driver-config key coalesceReads (default true) with an AdminUI switch
(Razor change: AbLegacyDriverForm, for the Wave 4 live check). Adds an
env-gated ab_server parity case (coalesced == per-tag values).

Refs #738
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 14:41:20 -04:00
parent e5527d0201
commit 530f4bdcd4
9 changed files with 563 additions and 6 deletions
@@ -256,6 +256,23 @@ public sealed class AbLegacyDriverPageFormSerializationTests
bound.Probe.ProbeAddress.ShouldBe("S:1");
}
/// <summary>Review 07 P7: the coalesceReads switch is authored here and consumed by the driver factory.</summary>
[Theory]
[InlineData("""{"devices":[]}""", true, false)]
[InlineData("""{"coalesceReads":false,"devices":[]}""", false, true)]
public void CoalesceReads_round_trips_to_the_factory_bound_options(string original, bool loadedValue, bool edited)
{
var loaded = AbLegacyDriverForm.TryDeserialize(original)!;
var form = AbLegacyDriverForm.FormModel.FromJson(original, loaded);
form.CoalesceReads.ShouldBe(loadedValue);
form.CoalesceReads = edited;
var saved = AbLegacyDriverForm.Serialize(form.ToOptions(loaded.Devices), original);
ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.AbLegacyDriverFactoryExtensions.ParseOptions("t", saved)
.CoalesceReads.ShouldBe(edited);
}
[Fact]
public void A_legacy_TimeSpan_string_key_is_loaded_then_migrated_to_ms_and_removed()
{