fix(arrays): Modbus row honours the FC16/FC03 protocol ceilings, the RMW read cap and the address-space end
Review r0 of Integration-W3: - I1: the effective FC16 cap is min(configured maxRegistersPerWrite, 123). The driver trusts a larger configured cap and sends an over-ceiling FC16 (past 127 registers its byte-count byte wraps), which every conformant device refuses, so such an array is never advertised writable. - I2: a BitInRegister[] write reads its span first in one unchunked FC03, so it is also bounded by min(configured maxRegistersPerRead, 125) (absent/0 = 125). - m2: every span (registers and coils) must end at or before address 65535. - m1: the String-node wording says it converts only when the text parses, which the server cannot promise (DateTime never converts). - m4: the parity device now enforces FC01/02 2000, FC03/04 125 (or a smaller device read limit), FC15 1968, FC16 123, FC23 121/125, the FC15/FC16 byte-count field (exception 03) and the address-space end (exception 02). Tests: parity rows for each ceiling/cap/span boundary against the real write path, and Core.Abstractions row tests. The configured-cap test that asserted a cap of 200 lifts the ceiling now asserts the opposite. Refs #741 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
+43
-3
@@ -74,11 +74,50 @@ public sealed class DriverArrayWriteSupportTests
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", tag, "{\"maxRegistersPerWrite\":100}", "{\"maxRegistersPerWrite\":9}")
|
||||
.ShouldBeFalse();
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", tag, "{\"maxRegistersPerWrite\":0}", null, "not json").ShouldBeTrue();
|
||||
// A larger configured cap lifts the default: 124 registers fit under 200.
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", RegisterArray("Int16", 124), "{\"maxRegistersPerWrite\":200}")
|
||||
// A configured cap above the FC16 protocol ceiling (123) never lifts it: a conformant device refuses 124
|
||||
// registers (and past 127 the driver's one-byte byte-count field wraps), so the write always fails.
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", RegisterArray("Int16", 123), "{\"maxRegistersPerWrite\":200}")
|
||||
.ShouldBeTrue();
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", RegisterArray("Int16", 124), "{\"maxRegistersPerWrite\":200}")
|
||||
.ShouldBeFalse();
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", RegisterArray("BitInRegister", 124), "{\"maxRegistersPerWrite\":200}")
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>A BitInRegister[] write is a read-modify-write: one unchunked FC03 of the whole span first, so its
|
||||
/// length is also bounded by <c>maxRegistersPerRead</c> (smallest positive value across the configs; absent / 0 =>
|
||||
/// 125, and never above FC03's protocol ceiling of 125). The other register shapes never read on write.</summary>
|
||||
[Fact]
|
||||
public void Modbus_bit_in_register_arrays_respect_the_read_cap()
|
||||
{
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Boolean", RegisterArray("BitInRegister", 64), "{\"maxRegistersPerRead\":64}").ShouldBeTrue();
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Boolean", RegisterArray("BitInRegister", 65), "{\"maxRegistersPerRead\":64}").ShouldBeFalse();
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Boolean", RegisterArray("BitInRegister", 65), "{\"maxRegistersPerRead\":100}", "{\"MaxRegistersPerRead\":64}")
|
||||
.ShouldBeFalse();
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Boolean", RegisterArray("BitInRegister", 123), "{\"maxRegistersPerRead\":0}").ShouldBeTrue();
|
||||
// A numeric array of the same length is not read on write, so the read cap does not apply to it.
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", RegisterArray("Int16", 65), "{\"maxRegistersPerRead\":64}").ShouldBeTrue();
|
||||
// With the write cap lifted, the read cap is the bound.
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Boolean", RegisterArray("BitInRegister", 100),
|
||||
"{\"maxRegistersPerRead\":100,\"maxRegistersPerWrite\":200}").ShouldBeTrue();
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Boolean", RegisterArray("BitInRegister", 101),
|
||||
"{\"maxRegistersPerRead\":100,\"maxRegistersPerWrite\":200}").ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>Every shape's span must end at or before address 65535 - a conformant device refuses a write past it
|
||||
/// (exception 02).</summary>
|
||||
[Theory]
|
||||
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":65530,\"dataType\":\"Int16\",\"isArray\":true,\"arrayLength\":6}", true)]
|
||||
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":65530,\"dataType\":\"Int16\",\"isArray\":true,\"arrayLength\":7}", false)]
|
||||
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":65532,\"dataType\":\"Int32\",\"isArray\":true,\"arrayLength\":2}", true)]
|
||||
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":65533,\"dataType\":\"Int32\",\"isArray\":true,\"arrayLength\":2}", false)]
|
||||
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":65534,\"dataType\":\"String\",\"stringLength\":4,\"isArray\":true,\"arrayLength\":1}", true)]
|
||||
[InlineData("{\"region\":\"HoldingRegisters\",\"address\":65535,\"dataType\":\"String\",\"stringLength\":4,\"isArray\":true,\"arrayLength\":1}", false)]
|
||||
[InlineData("{\"region\":\"Coils\",\"address\":65000,\"isArray\":true,\"arrayLength\":536}", true)]
|
||||
[InlineData("{\"region\":\"Coils\",\"address\":65000,\"isArray\":true,\"arrayLength\":537}", false)]
|
||||
public void Modbus_array_spans_end_within_the_address_space(string tagConfig, bool expected) =>
|
||||
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", tagConfig).ShouldBe(expected);
|
||||
|
||||
/// <summary>FC15 carries at most 1968 coils and the driver sends one request, so a longer coil array is not
|
||||
/// writable.</summary>
|
||||
[Fact]
|
||||
@@ -104,7 +143,8 @@ public sealed class DriverArrayWriteSupportTests
|
||||
$"{{\"region\":\"HoldingRegisters\",\"address\":0,{typeKeys},\"isArray\":true,\"arrayLength\":{length}}}").ShouldBe(expected);
|
||||
|
||||
/// <summary>Elements CONVERT (ScadaBridge's <c>int[]</c> to an Int16 node), so the Modbus row keys on no exact node
|
||||
/// DataType; it leaves out only a String or DateTime node over a numeric / bit element, which never converts. A
|
||||
/// DataType; it leaves out only a String or DateTime node over a numeric / bit element (a DateTime never converts; a String converts only when its text happens to
|
||||
/// parse, which the server cannot promise). A
|
||||
/// String element takes any node type (each element is <c>Convert.ToString</c>'d).</summary>
|
||||
[Theory]
|
||||
[InlineData("Int16", "Int16", true)]
|
||||
|
||||
Reference in New Issue
Block a user