fix(driver-modbus-addressing): resolve High code-review finding (Driver.Modbus.Addressing-001)

The DL205 family-native branch routed every V-prefixed address through
DirectLogicAddress.UserVMemoryToPdu, a plain octal-to-decimal decode.
DL205/DL260 system V-memory (V40400 and up) is not a simple octal decode:
the CPU relocates the system bank to Modbus PDU 0x2100. Octal-decoding
V40400 produced 16640 (0x4100), the wrong register, so any tag addressing
a system register through the grammar string silently read/wrote the
wrong PLC memory.

- Add DirectLogicAddress.VMemoryToPdu, which decodes the octal V-address,
  detects the system bank (octal >= V40400 == SystemVMemoryOctalBase) and
  relocates it through SystemVMemoryToPdu to PDU 0x2100; user-bank
  addresses keep the plain octal decode.
- ModbusAddressParser's DL205 V branch now calls VMemoryToPdu instead of
  UserVMemoryToPdu. UserVMemoryToPdu is retained for user-bank-only callers.
- Correct the ModbusFamilyParserTests V40400 assertion (16640 -> 0x2100)
  and add system-bank regression cases plus direct helper coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 06:53:59 -04:00
parent 532b961cf2
commit 1837b5a828
4 changed files with 88 additions and 7 deletions

View File

@@ -21,9 +21,22 @@ public static class DirectLogicAddress
/// depends on CPU model — DL205 D2-260 user memory is V1400-V7377 + V10000-V17777
/// octal, DL260 extends to V77777 octal.
/// </summary>
/// <remarks>
/// This handles ONLY user V-memory (octal address below the system base, V40400). For a
/// V-address that may fall in either bank, call <see cref="VMemoryToPdu"/>, which routes
/// system-bank addresses through <see cref="SystemVMemoryToPdu"/> instead.
/// </remarks>
/// <exception cref="ArgumentException">Input is null / empty / contains non-octal digits (8,9).</exception>
/// <exception cref="OverflowException">Parsed value exceeds ushort.MaxValue (0xFFFF).</exception>
public static ushort UserVMemoryToPdu(string vAddress)
=> (ushort)DecodeOctalVAddress(vAddress); // DecodeOctalVAddress guards the 0xFFFF ceiling
/// <summary>
/// Decode the octal digits of a V-address (with optional <c>V</c> prefix) to a uint.
/// Used both as the user-memory PDU value and as the octal magnitude that decides which
/// bank a V-address belongs to.
/// </summary>
private static uint DecodeOctalVAddress(string vAddress)
{
if (string.IsNullOrWhiteSpace(vAddress))
throw new ArgumentException("V-memory address must not be empty", nameof(vAddress));
@@ -46,7 +59,7 @@ public static class DirectLogicAddress
throw new OverflowException(
$"V-memory address '{vAddress}' exceeds the 16-bit Modbus PDU address range");
}
return (ushort)result;
return result;
}
/// <summary>
@@ -58,6 +71,14 @@ public static class DirectLogicAddress
/// </summary>
public const ushort SystemVMemoryBasePdu = 0x2100;
/// <summary>
/// Octal magnitude of the first system V-memory register (V40400). A V-address whose
/// octal-decoded value is at or above this belongs to the system bank and must NOT be
/// mapped with the plain octal-to-decimal user formula — see <see cref="VMemoryToPdu"/>.
/// </summary>
/// <remarks>Octal 40400 == decimal 16640 (0x4100).</remarks>
public const ushort SystemVMemoryOctalBase = 0x4100; // octal 40400 decoded
/// <param name="offsetWithinSystemBank">
/// 0-based register offset within the system bank. Pass 0 for V40400 itself; pass 1 for
/// V40401 (octal), and so on. NOT an octal-decoded value — the system bank lives at
@@ -72,6 +93,35 @@ public static class DirectLogicAddress
return (ushort)pdu;
}
/// <summary>
/// Convert any DirectLOGIC V-memory address (octal, optional <c>V</c> prefix) to a 0-based
/// Modbus PDU address, routing user-bank and system-bank addresses through the correct
/// formula. User V-memory (octal &lt; V40400) is a plain octal-to-decimal decode; system
/// V-memory (octal &gt;= V40400) is relocated to the fixed system base at PDU 0x2100.
/// This is the address the parser must use — the system bank is NOT a simple octal decode.
/// </summary>
/// <remarks>
/// The system-bank consecutive offset is the octal-decoded distance from V40400, not an
/// octal-decoded value itself: V40400 → PDU 0x2100, V40401 → 0x2101, and so on.
/// See <c>docs/v2/dl205.md</c> §V-Memory Addressing.
/// </remarks>
/// <exception cref="ArgumentException">Input is null / empty / contains non-octal digits.</exception>
/// <exception cref="OverflowException">The result exceeds the 16-bit Modbus PDU range.</exception>
public static ushort VMemoryToPdu(string vAddress)
{
var octalValue = DecodeOctalVAddress(vAddress);
if (octalValue < SystemVMemoryOctalBase)
return (ushort)octalValue;
// System bank: the registers are contiguous from V40400, so the offset within the bank
// is the plain decimal distance from the octal base, not another octal decode.
var offsetWithinBank = octalValue - SystemVMemoryOctalBase;
if (offsetWithinBank > ushort.MaxValue)
throw new OverflowException(
$"V-memory address '{vAddress}' is outside the addressable system bank");
return SystemVMemoryToPdu((ushort)offsetWithinBank);
}
// Bit-memory bases per DL260 user manual §I/O-configuration.
// Numbers after X / Y / C / SP are OCTAL in DirectLOGIC notation. The Modbus base is
// added to the octal-decoded offset; e.g. Y017 = Modbus coil 2048 + octal(17) = 2048 + 15 = 2063.

View File

@@ -229,7 +229,9 @@ public static class ModbusAddressParser
// SP → DiscreteInputs (special relays).
if (text.StartsWith("V", StringComparison.OrdinalIgnoreCase))
{
offset = DirectLogicAddress.UserVMemoryToPdu(text);
// VMemoryToPdu routes user vs system V-memory: the system bank (octal
// >= V40400) is relocated to PDU 0x2100, NOT a plain octal decode.
offset = DirectLogicAddress.VMemoryToPdu(text);
region = ModbusRegion.HoldingRegisters;
return true;
}