fix(modbus): review r0 - FC23 opt-in, explicit maxReadGap 0 = off, legacy coil types load, one health writer

Coordinator rulings and Important findings from review r0:
1. FC23 is opt-in (new UseFC23ForMultiRegisterWrites, default false); a config
   without it - including the persisted "disableFC23": false every form-saved
   config carries - keeps the FC16 wire. DisableFC23 stays the hard kill-switch.
2. An explicit maxReadGap 0 means coalescing off again (its pre-lane meaning);
   the gap-8 default applies only to an absent key.
3. Coils/DiscreteInputs tags persisted with a register dataType (the old
   editor saved Int16) are coerced to Bool, never refused, with no deploy
   warning (a warning can be promoted to a blocking error). The editor loads
   them as Bool. Register region + Bool is still refused.
4. One comms-fault slot for reads and writes: a good read clears a write's
   transport fault and vice versa.
Minors: Dispose writes its own Unknown health (m1); persisted region/address
are authoritative over addressString so a Family change cannot remap a live
tag (m2); a malformed-but-delivered reply no longer fails the unit fast (m3);
a fresh socket published after a concurrent dispose is torn down (m4); the
editor resets address text per tag (m5); master merged (m6); the FC23 latch
log says re-init restores it (m7). Back-compat tests use the old persisted
JSON shapes and fail without the fixes.

Refs #741
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 15:55:32 -04:00
parent c12269c221
commit fd6ecf8293
15 changed files with 314 additions and 117 deletions
@@ -137,10 +137,24 @@ public sealed class ModbusTagConfigModelTests
ModbusTagConfigModel.FromJson(m.ToJson()).Validate().ShouldBeNull();
}
/// <summary>A Coils + Int16 blob (CSV / API) is refused at authoring, matching the driver.</summary>
/// <summary>A register region with Bool (CSV / API) is refused at authoring, matching the driver.</summary>
[Fact]
public void Validate_refuses_an_incompatible_region_and_type()
=> ModbusTagConfigModel.FromJson("""{"region":"Coils","address":1,"dataType":"Int16"}""").Validate().ShouldNotBeNull();
public void Validate_refuses_a_register_region_with_Bool()
=> ModbusTagConfigModel.FromJson("""{"region":"HoldingRegisters","address":1,"dataType":"Bool"}""").Validate().ShouldNotBeNull();
/// <summary>
/// Fix round 1: the exact blob the pre-#741 editor persisted for a coil (dataType defaulted to Int16)
/// still validates, loads as Bool (what the driver polls), and re-saves as Bool.
/// </summary>
[Fact]
public void A_legacy_Int16_coil_blob_validates_and_loads_as_Bool()
{
const string legacy = """{"region":"Coils","address":3,"dataType":"Int16","byteOrder":"BigEndian","bitIndex":0,"stringLength":0}""";
var m = ModbusTagConfigModel.FromJson(legacy);
m.Validate().ShouldBeNull();
m.DataType.ShouldBe(ModbusDataType.Bool);
m.ToJson().ShouldContain("\"dataType\":\"Bool\"");
}
/// <summary>Review 07 U1: the editor resolves an address string with the DRIVER's family and persists
/// both the string and the resolved fields; the driver re-resolves the string with its family.</summary>