Phase 3 PR 47 -- DL205 V-memory octal-address helper. Adds DirectLogicAddress static class with two entry points: UserVMemoryToPdu(string) parses a DirectLOGIC V-address (V-prefixed or bare, whitespace tolerated) as OCTAL and returns the 0-based Modbus PDU address. V2000 octal = decimal 1024 = PDU 0x0400, which is the canonical start of the user V-memory bank on DL205/DL260. SystemVMemoryBasePdu + SystemVMemoryToPdu(ushort offset) handle the system bank (V40400 and up) which does NOT follow the simple octal-to-decimal formula -- the CPU relocates the system bank to PDU 0x2100 in H2-ECOM100 absolute mode. A naive caller converting 40400 octal would land at PDU 0x4100 (decimal 16640) and miss the system registers entirely; the helper routes the correct 0x2100 base. Why this matters: DirectLOGIC operators think in OCTAL (the ladder-logic editor, the Productivity/Do-more UI, every AutomationDirect manual addresses V-memory octally) while the Modbus wire is DECIMAL. Integrators routinely copy V-addresses from the PLC documentation into client configs and read garbage because they treated V2000 as decimal 2000 (HR[2000] = 0 in the dl205 sim, zero in most PLCs). The helper makes the translation explicit per the D2-USER-M appendix + H2-ECOM-M \u00A76.5 references cited in docs/v2/dl205.md. Unit tests: UserVMemoryToPdu_converts_octal_V_prefix (V0, V1, V7, V10, V2000, V7777, V10000, V17777 -- the exact sweep documented in dl205.md), UserVMemoryToPdu_accepts_bare_or_prefixed_or_padded (case + whitespace tolerance), UserVMemoryToPdu_rejects_non_octal_digits (V8/V19/V2009 must throw ArgumentException with 'octal' in the message -- .NET has no base-8 int.Parse so we hand-walk digits to catch 8/9 instead of silently accepting them), UserVMemoryToPdu_rejects_empty_input, UserVMemoryToPdu_overflow_rejected (200000 octal = 0x10000 overflows ushort), SystemVMemoryBasePdu_is_0x2100_for_V40400, SystemVMemoryToPdu_offsets_within_bank, SystemVMemoryToPdu_rejects_overflow. 23/23 Modbus.Tests pass. Integration tests against dl205.json pymodbus profile: DL205_V2000_user_memory_resolves_to_PDU_0x0400_marker (reads HR[0x0400]=0x2000), DL205_V40400_system_memory_resolves_to_PDU_0x2100_marker (reads HR[0x2100]=0x4040). 5/5 DL205 integration tests pass. Caller opts into the helper per tag by calling DirectLogicAddress.UserVMemoryToPdu("V2000") as the ModbusTagDefinition Address -- no driver-wide "DL205 mode" flag needed, because users mix DL and non-DL tags in a single driver instance all the time.

This commit is contained in:
Joseph Doherty
2026-04-18 21:49:58 -04:00
parent 8248b126ce
commit 2b5222f5db
3 changed files with 242 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
[Trait("Category", "Unit")]
public sealed class DirectLogicAddressTests
{
[Theory]
[InlineData("V0", (ushort)0x0000)]
[InlineData("V1", (ushort)0x0001)]
[InlineData("V7", (ushort)0x0007)]
[InlineData("V10", (ushort)0x0008)]
[InlineData("V2000", (ushort)0x0400)] // canonical DL205/DL260 user-memory start
[InlineData("V7777", (ushort)0x0FFF)]
[InlineData("V10000", (ushort)0x1000)]
[InlineData("V17777", (ushort)0x1FFF)]
public void UserVMemoryToPdu_converts_octal_V_prefix(string v, ushort expected)
=> DirectLogicAddress.UserVMemoryToPdu(v).ShouldBe(expected);
[Theory]
[InlineData("0", (ushort)0)]
[InlineData("2000", (ushort)0x0400)]
[InlineData("v2000", (ushort)0x0400)] // lowercase v
[InlineData(" V2000 ", (ushort)0x0400)] // surrounding whitespace
public void UserVMemoryToPdu_accepts_bare_or_prefixed_or_padded(string v, ushort expected)
=> DirectLogicAddress.UserVMemoryToPdu(v).ShouldBe(expected);
[Theory]
[InlineData("V8")] // 8 is not a valid octal digit
[InlineData("V19")]
[InlineData("V2009")]
public void UserVMemoryToPdu_rejects_non_octal_digits(string v)
{
Should.Throw<ArgumentException>(() => DirectLogicAddress.UserVMemoryToPdu(v))
.Message.ShouldContain("octal");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("V")]
public void UserVMemoryToPdu_rejects_empty_input(string? v)
=> Should.Throw<ArgumentException>(() => DirectLogicAddress.UserVMemoryToPdu(v!));
[Fact]
public void UserVMemoryToPdu_overflow_rejected()
{
// 200000 octal = 0x10000 — one past ushort range.
Should.Throw<OverflowException>(() => DirectLogicAddress.UserVMemoryToPdu("V200000"));
}
[Fact]
public void SystemVMemoryBasePdu_is_0x2100_for_V40400()
{
// V40400 on DL260 / H2-ECOM100 absolute mode → PDU 0x2100 (decimal 8448), NOT 0x4100
// which a naive octal-to-decimal of 40400 octal would give (= 16640).
DirectLogicAddress.SystemVMemoryBasePdu.ShouldBe((ushort)0x2100);
DirectLogicAddress.SystemVMemoryToPdu(0).ShouldBe((ushort)0x2100);
}
[Fact]
public void SystemVMemoryToPdu_offsets_within_bank()
{
DirectLogicAddress.SystemVMemoryToPdu(1).ShouldBe((ushort)0x2101);
DirectLogicAddress.SystemVMemoryToPdu(0x100).ShouldBe((ushort)0x2200);
}
[Fact]
public void SystemVMemoryToPdu_rejects_overflow()
{
// ushort wrap: 0xFFFF - 0x2100 = 0xDEFF; anything above should throw.
Should.NotThrow(() => DirectLogicAddress.SystemVMemoryToPdu(0xDEFF));
Should.Throw<OverflowException>(() => DirectLogicAddress.SystemVMemoryToPdu(0xDF00));
}
}