fix(modbus): multi-write PDUs carry no trailing byte past their byte count

The FC15, FC16, BitInRegister[] RMW FC16 and FC23 builders allocated one byte
more than they filled (6 + 1 + n / 10 + 1 + n, data copied at offset 6 / 10), so
every multi-write request ended in a stray zero byte beyond the length its
byte-count field declares. A strict device or gateway that checks the frame
length against the byte count refuses it. Each PDU is now exactly 6 + n
(FC15/FC16) or 10 + n (FC23).

Tests: ModbusWritePduFramingTests asserts the exact length and byte-count field
for FC15 (1/8/9 coils), FC16 (array, Float64 array, scalar multi-register), the
BitInRegister RMW FC16 and FC23 (all red before the fix); the parity suite's
conformant device now refuses a frame-length mismatch on FC15/FC16/FC23.
Live: Driver.Modbus.IntegrationTests against 10.100.0.35:5020 (standard
profile) passes, including the FC16 and FC23 multi-register round trips.

Refs #741

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 17:35:40 -04:00
parent cf62007678
commit 0f305f1827
3 changed files with 120 additions and 7 deletions
@@ -1603,7 +1603,8 @@ public sealed class ModbusDriver
for (var i = 0; i < values.Length; i++)
if (values[i]) bitmap[i / 8] |= (byte)(1 << (i % 8));
var qty = (ushort)values.Length;
var pdu15 = new byte[6 + 1 + byteCount];
// [fc][addr:2][qty:2][byteCount] + data: exactly 6 + byteCount (no trailing byte past the declared data).
var pdu15 = new byte[6 + byteCount];
pdu15[0] = 0x0F;
pdu15[1] = (byte)(tag.Address >> 8); pdu15[2] = (byte)(tag.Address & 0xFF);
pdu15[3] = (byte)(qty >> 8); pdu15[4] = (byte)(qty & 0xFF);
@@ -1638,7 +1639,8 @@ public sealed class ModbusDriver
$"Write of {qty} registers to {tag.Name} exceeds MaxRegistersPerWrite={writeCap}. " +
$"Split the tag (e.g. shorter StringLength or smaller ArrayCount) — partial FC16 chunks would lose atomicity.");
if (await TryWriteViaFc23Async(transport, tag, bytes, qty, ct).ConfigureAwait(false)) return;
var pdu = new byte[6 + 1 + bytes.Length];
// [fc][addr:2][qty:2][byteCount] + data: exactly 6 + byteCount.
var pdu = new byte[6 + bytes.Length];
pdu[0] = 0x10;
pdu[1] = (byte)(tag.Address >> 8); pdu[2] = (byte)(tag.Address & 0xFF);
pdu[3] = (byte)(qty >> 8); pdu[4] = (byte)(qty & 0xFF);
@@ -1685,7 +1687,8 @@ public sealed class ModbusDriver
if (!_options.UseFC23ForMultiRegisterWrites || _options.DisableFC23
|| qty < 2 || qty > Fc23MaxWriteRegisters || unsupported.ContainsKey(unit)) return false;
var pdu = new byte[10 + 1 + bytes.Length];
// [fc][readAddr:2][readQty:2][writeAddr:2][writeQty:2][writeByteCount] + data: exactly 10 + byteCount.
var pdu = new byte[10 + bytes.Length];
pdu[0] = 0x17;
pdu[1] = (byte)(tag.Address >> 8); pdu[2] = (byte)(tag.Address & 0xFF); // read start
pdu[3] = (byte)(qty >> 8); pdu[4] = (byte)(qty & 0xFF); // read quantity
@@ -1824,7 +1827,8 @@ public sealed class ModbusDriver
payload[i * 2] = (byte)(reg >> 8);
payload[i * 2 + 1] = (byte)(reg & 0xFF);
}
var pdu = new byte[6 + 1 + payload.Length];
// [fc][addr:2][qty:2][byteCount] + data: exactly 6 + byteCount.
var pdu = new byte[6 + payload.Length];
pdu[0] = 0x10;
pdu[1] = (byte)(tag.Address >> 8); pdu[2] = (byte)(tag.Address & 0xFF);
pdu[3] = (byte)(count >> 8); pdu[4] = (byte)(count & 0xFF);