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
@@ -46,6 +46,7 @@ public sealed class ModbusDriverTests
0x06 => Task.FromResult(WriteSingleReg(pdu)),
0x0F => Task.FromResult(WriteMultipleCoils(pdu)),
0x10 => Task.FromResult(WriteMultipleRegs(pdu)),
0x17 => Task.FromResult(ReadWriteMultipleRegs(pdu)),
_ => Task.FromException<byte[]>(new ModbusException(fc, 0x01, $"fc={fc} not supported by fake")),
};
}
@@ -102,6 +103,26 @@ public sealed class ModbusDriverTests
return new byte[] { 0x10, pdu[1], pdu[2], pdu[3], pdu[4] };
}
// FC23 Read/Write Multiple Registers: write first, then read back the read range.
private byte[] ReadWriteMultipleRegs(byte[] pdu)
{
var readAddr = (ushort)((pdu[1] << 8) | pdu[2]);
var readQty = (ushort)((pdu[3] << 8) | pdu[4]);
var writeAddr = (ushort)((pdu[5] << 8) | pdu[6]);
var writeQty = (ushort)((pdu[7] << 8) | pdu[8]);
for (var i = 0; i < writeQty; i++)
HoldingRegisters[writeAddr + i] = (ushort)((pdu[10 + i * 2] << 8) | pdu[11 + i * 2]);
var resp = new byte[2 + readQty * 2];
resp[0] = 0x17;
resp[1] = (byte)(readQty * 2);
for (var i = 0; i < readQty; i++)
{
resp[2 + i * 2] = (byte)(HoldingRegisters[readAddr + i] >> 8);
resp[3 + i * 2] = (byte)(HoldingRegisters[readAddr + i] & 0xFF);
}
return resp;
}
private byte[] WriteMultipleCoils(byte[] pdu)
{
var addr = (ushort)((pdu[1] << 8) | pdu[2]);