feat(modbus): wire Family / MelsecSubFamily / DisableFC23, and the form's timing keys

U1: a tag's addressString (Modicon / mnemonic / DL205 V-memory / MELSEC
D-X-Y grammar) now resolves through ModbusAddressParser with the DRIVER's
Family and MelsecSubFamily at every initialize and is authoritative; the
family-unaware gates (deploy Inspect, AdminUI parse consult) accept what
some family resolves. The tag editor gets an address-text field that
resolves with the owning driver's family and persists both the string and
the resolved region/offset/type fields; a hand edit drops the string.
DisableFC23 now gates FC23: by default a 2..121-register holding write is
one FC23 write + read-back (a unit answering exception 01, or failing FC23 at
the transport, moves to FC16 for the rest of the generation); RTU framing
deframes the FC23 reply. Live-checked: the pymodbus fixture implements FC23.
Also: the AdminUI driver form serialises ModbusDriverOptions (TimeSpan
strings), which the factory DTO never read - every timing field on the form
was dead. The DTO now accepts both shapes (*Ms keys win).

Refs #741
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 15:35:58 -04:00
parent 1adc9d65e1
commit 4a3d7a9361
13 changed files with 656 additions and 58 deletions
@@ -89,10 +89,13 @@ public sealed class ModbusDriverOptions
public bool UseFC16ForSingleRegisterWrites { get; init; } = false;
/// <summary>
/// <b>Reserved / no-op</b> kill-switch for FC23 (Read/Write Multiple Registers). The
/// driver does not currently emit FC23 — toggling this option has no observable effect
/// today. The slot exists so a future block-read-coalescing enhancement that opts into
/// FC23 can be disabled per-deployment without a code change. Default <c>false</c>.
/// 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>.
/// </summary>
public bool DisableFC23 { get; init; } = false;
@@ -128,10 +131,11 @@ public sealed class ModbusDriverOptions
public bool DisableReadCoalescing { get; init; } = false;
/// <summary>
/// PLC family hint that drives the parser's family-native branch. When set to a
/// non-Generic value, address strings using that family's native syntax (DL205 V2000 /
/// MELSEC D100) parse to the right region + offset directly. Defaults to
/// <see cref="ModbusFamily.Generic"/> = Modicon-only behaviour preserved by default.
/// PLC family hint that drives the parser's family-native branch. A tag authored with an
/// <c>addressString</c> is resolved with this family at every initialize, so with a non-Generic
/// value that family's native syntax (DL205 V2000 octal / MELSEC D100) parses to the right
/// region + offset. Defaults to <see cref="ModbusFamily.Generic"/> (Modicon / mnemonic grammar).
/// Tags authored by region + numeric address are unaffected.
/// </summary>
public ModbusFamily Family { get; init; } = ModbusFamily.Generic;