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:
@@ -89,16 +89,24 @@ public sealed class ModbusDriverOptions
|
||||
public bool UseFC16ForSingleRegisterWrites { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Kill-switch for FC23 (Read/Write Multiple Registers). By default a multi-register holding
|
||||
/// write (2–121 registers: 32/64-bit values, arrays, strings) is sent as ONE FC23 that writes
|
||||
/// the block and reads the same block back, so the device confirms what it now holds. A unit
|
||||
/// that answers FC23 with exception 01 (MELSEC QJ71MT91, S7 Modbus servers) is moved to FC16
|
||||
/// automatically, for that write and every later one; one that fails FC23 at the transport is
|
||||
/// moved too (that one write is reported as a comms failure). Set <c>true</c> to always use
|
||||
/// FC16. Default <c>false</c>.
|
||||
/// Hard kill-switch for FC23 (Read/Write Multiple Registers): when <c>true</c> the driver never
|
||||
/// emits FC23, even with <see cref="UseFC23ForMultiRegisterWrites"/> set. Default <c>false</c>.
|
||||
/// FC23 is OPT-IN — a config without <see cref="UseFC23ForMultiRegisterWrites"/> keeps the FC16 wire
|
||||
/// behaviour, so the persisted <c>"disableFC23": false</c> every form-saved config carries changes
|
||||
/// nothing on the wire.
|
||||
/// </summary>
|
||||
public bool DisableFC23 { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in: send a multi-register holding write (2–121 registers: 32/64-bit values, arrays, strings)
|
||||
/// as ONE FC23 that writes the block and reads the same block back, so the device confirms what it
|
||||
/// now holds. A unit that answers FC23 with exception 01 (MELSEC QJ71MT91, S7 Modbus servers) moves
|
||||
/// to FC16 for that write and every later one until the next initialize; one that fails FC23 at the
|
||||
/// transport moves too (that one write is reported as a comms failure). Ignored when
|
||||
/// <see cref="DisableFC23"/> is set. Default <c>false</c> = FC16, the historical behaviour.
|
||||
/// </summary>
|
||||
public bool UseFC23ForMultiRegisterWrites { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Interval for the background re-probe loop that retries auto-prohibited
|
||||
/// coalesced ranges. When non-null, every <c>AutoProhibitReprobeInterval</c>
|
||||
@@ -110,14 +118,13 @@ public sealed class ModbusDriverOptions
|
||||
public TimeSpan? AutoProhibitReprobeInterval { get; init; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Block-read coalescing budget. The read planner combines tags in the same (UnitId, Region)
|
||||
/// group whose addresses are at most this many registers (Coils / DiscreteInputs: bits) apart
|
||||
/// into a single FC03/FC04/FC01/FC02 read, then slices the response back into per-tag values.
|
||||
/// <c>0</c> coalesces only adjacent or overlapping tags (several <c>BitInRegister</c> tags on one
|
||||
/// status word still fold into one read). Default <c>8</c>. A range the device refuses is
|
||||
/// auto-prohibited and read per tag, so a protected hole costs one extra round trip, not data. To
|
||||
/// turn coalescing off entirely set <see cref="DisableReadCoalescing"/>, or exempt one tag with
|
||||
/// its <c>coalesceProhibited</c> key.
|
||||
/// Block-read coalescing budget. When non-zero, the read planner combines tags in the same
|
||||
/// (UnitId, Region) group whose addresses are at most this many registers (Coils / DiscreteInputs:
|
||||
/// bits) apart into a single FC03/FC04/FC01/FC02 read, then slices the response back into per-tag
|
||||
/// values (several <c>BitInRegister</c> tags on one status word fold into one read). <c>0</c> = no
|
||||
/// coalescing — an explicitly authored 0 keeps that historical meaning. Default <c>8</c> applies
|
||||
/// only when the key is ABSENT. A range the device refuses is auto-prohibited and read per tag.
|
||||
/// <see cref="DisableReadCoalescing"/> also turns it off; <c>coalesceProhibited</c> exempts one tag.
|
||||
/// </summary>
|
||||
public ushort MaxReadGap { get; init; } = DefaultMaxReadGap;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user