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:
+11
-2
@@ -80,7 +80,7 @@
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Max read gap (coalescing)</label>
|
||||
<InputNumber @bind-Value="_form.MaxReadGap" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
|
||||
<div class="form-text">Default 8. 0 = adjacent tags only. Refused ranges fall back per tag.</div>
|
||||
<div class="form-text">0 = no coalescing. Default 8. Refused ranges fall back per tag.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check form-switch mt-4">
|
||||
@@ -112,7 +112,13 @@
|
||||
<div class="col-md-4">
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.DisableFC23" @bind-Value:after="EmitAsync" class="form-check-input" id="disableFC23" />
|
||||
<label class="form-check-label" for="disableFC23">Disable FC23 (multi-register writes use FC16 instead of FC23 write + read-back)</label>
|
||||
<label class="form-check-label" for="disableFC23">Disable FC23 (never send FC23, even when opted in below)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.UseFC23ForMultiRegisterWrites" @bind-Value:after="EmitAsync" class="form-check-input" id="useFC23" />
|
||||
<label class="form-check-label" for="useFC23">Use FC23 write + read-back for multi-register writes (opt-in; default FC16)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@@ -324,6 +330,7 @@
|
||||
public bool UseFC15ForSingleCoilWrites { get; set; } = false;
|
||||
public bool UseFC16ForSingleRegisterWrites { get; set; } = false;
|
||||
public bool DisableFC23 { get; set; } = false;
|
||||
public bool UseFC23ForMultiRegisterWrites { get; set; } = false;
|
||||
public bool WriteOnChangeOnly { get; set; } = false;
|
||||
|
||||
// Keep-alive
|
||||
@@ -368,6 +375,7 @@
|
||||
UseFC15ForSingleCoilWrites = o.UseFC15ForSingleCoilWrites,
|
||||
UseFC16ForSingleRegisterWrites = o.UseFC16ForSingleRegisterWrites,
|
||||
DisableFC23 = o.DisableFC23,
|
||||
UseFC23ForMultiRegisterWrites = o.UseFC23ForMultiRegisterWrites,
|
||||
WriteOnChangeOnly = o.WriteOnChangeOnly,
|
||||
KeepAliveEnabled = o.KeepAlive.Enabled,
|
||||
KeepAliveTimeSeconds = (int)o.KeepAlive.Time.TotalSeconds,
|
||||
@@ -404,6 +412,7 @@
|
||||
UseFC15ForSingleCoilWrites = UseFC15ForSingleCoilWrites,
|
||||
UseFC16ForSingleRegisterWrites = UseFC16ForSingleRegisterWrites,
|
||||
DisableFC23 = DisableFC23,
|
||||
UseFC23ForMultiRegisterWrites = UseFC23ForMultiRegisterWrites,
|
||||
AutoProhibitReprobeInterval = AutoProhibitReprobeIntervalSeconds > 0
|
||||
? TimeSpan.FromSeconds(AutoProhibitReprobeIntervalSeconds) : null,
|
||||
MaxReadGap = (ushort)Math.Clamp(MaxReadGap, 0, 65535),
|
||||
|
||||
+4
-1
@@ -80,7 +80,10 @@
|
||||
if (ConfigJson == _lastConfigJson) { return; }
|
||||
_lastConfigJson = ConfigJson;
|
||||
_m = ModbusTagConfigModel.FromJson(ConfigJson);
|
||||
_addressText ??= _m.AddressString;
|
||||
// Reset on EVERY config change: a reused editor instance switched to another tag must not show the
|
||||
// previous tag's address text.
|
||||
_addressText = _m.AddressString;
|
||||
_addressError = null;
|
||||
_family = ModbusTagConfigModel.ReadFamily(GetDriverConfigJson()).Family;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,10 @@ public sealed class ModbusTagConfigModel
|
||||
|
||||
/// <summary>
|
||||
/// The address in the parser grammar (<c>40001:F:CDAB</c>, DL205 <c>V2000</c>, MELSEC <c>D100</c>) when
|
||||
/// the tag was authored that way; <c>null</c> for a tag authored by region + numeric address. When
|
||||
/// present the driver re-resolves it with ITS family at runtime, so it is authoritative — editing any
|
||||
/// resolved field by hand clears it (<see cref="ClearAddressString"/>).
|
||||
/// the tag was authored that way; <c>null</c> for a tag authored by region + numeric address. The
|
||||
/// persisted resolved fields are what the driver reads (the string is only resolved by the driver for a
|
||||
/// string-only blob), so editing any resolved field by hand clears it (<see cref="ClearAddressString"/>)
|
||||
/// to keep the two from disagreeing.
|
||||
/// </summary>
|
||||
public string? AddressString { get; set; }
|
||||
|
||||
@@ -130,9 +131,12 @@ public sealed class ModbusTagConfigModel
|
||||
_source = json ?? "",
|
||||
Region = TagConfigJson.GetEnum(o, "region", ModbusRegion.HoldingRegisters),
|
||||
Address = TagConfigJson.GetInt(o, "address"),
|
||||
// Absent dataType defaults per region, exactly as the driver's factory does (Bool for a bit region).
|
||||
DataType = TagConfigJson.GetEnum(o, "dataType",
|
||||
ModbusTagDefinitionFactory.DefaultDataType(TagConfigJson.GetEnum(o, "region", ModbusRegion.HoldingRegisters))),
|
||||
// Absent dataType defaults per region, exactly as the driver's factory does (Bool for a bit region),
|
||||
// and a bit region shows Bool whatever was persisted — the driver coerces a legacy coil/DI
|
||||
// "dataType":"Int16" (what the pre-#741 editor saved) to Bool, so the editor shows what is polled.
|
||||
DataType = TagConfigJson.GetEnum(o, "region", ModbusRegion.HoldingRegisters) is ModbusRegion.Coils or ModbusRegion.DiscreteInputs
|
||||
? ModbusDataType.Bool
|
||||
: TagConfigJson.GetEnum(o, "dataType", ModbusDataType.Int16),
|
||||
ByteOrder = TagConfigJson.GetEnum(o, "byteOrder", ModbusByteOrder.BigEndian),
|
||||
BitIndex = TagConfigJson.GetInt(o, "bitIndex"),
|
||||
StringLength = TagConfigJson.GetInt(o, "stringLength"),
|
||||
|
||||
Reference in New Issue
Block a user