Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/DL205.xmpp
Joseph Doherty 02a0e8efd1 Phase 3 PR 42 — ModbusPal simulator profiles for Standard Modbus + DL205/DL260 quirks. Two hand-authored .xmpp profiles in tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/ that integration tests load via the GUI to drive the suite without a real PLC. Both well-formed XML (verified via PowerShell [xml] cast); both copied to test-output as PreserveNewest content per the existing csproj rule.
Standard.xmpp — generic Modbus TCP server on port 502, slave id 1. HR[0..31] seeded with address-as-value (HR[5]=5 — easy mental map for diagnostics), HR[100] auto-incrementing via a 1Hz LinearGenerator binding (drives subscribe-and-receive integration tests so they have a register that actually changes without a write), HR[200..209] scratch range for write-roundtrip tests, coils 0..31 alternating on/off, coils 100..109 scratch. The Tick automation runs 0..65535 over 60s looping; bound to HR[100] via Binding_SINT16 — slow enough that a 250ms-poll integration test sees discrete jumps, fast enough that a 5s subscribe test sees several change notifications.
DL205.xmpp — AutomationDirect DirectLOGIC DL205/DL260 quirk simulator on port 502, slave id 1, modeling the behaviors documented in docs/v2/dl205.md as concrete register values so DL205 integration tests can assert each quirk WITHOUT a live PLC. Per-quirk encoding: V0 marker at HR[0]=0xCAFE proves register 0 is valid (rejects-register-0 rumour disproved); V2000 marker at HR[1024]=0x2000 proves V-memory octal-to-decimal mapping; V40400 marker at HR[8448]=0x4040 proves V40400→PDU 0x2100 (NOT register 0, contrary to the widespread shorthand); 'Hello' string at HR[1040..1042] packed first-char-low-byte (HR[1040]=0x6548 = 'H' lo + 'e' hi, HR[1041]=0x6C6C, HR[1042]=0x006F) — the headline string-byte-order quirk the user flagged; Float32 1.5f at HR[1056..1057] in CDAB word order (low word first: 0, then 0x3FC0); BCD register at HR[1072]=0x1234 representing decimal 1234 in BCD nibbles (NOT binary 0x04D2); 128-register block at HR[1280..1407] for FC03-128-cap testing; Y0 marker at coil 2048, C0 marker at coil 3072, scratch C-coils at 4000..4007 for write tests.
Critical limitation flagged inline + in README: ModbusPal 1.6b CANNOT represent the DL205 quirks semantically — it has no string binding, no BCD binding, no arbitrary-byte-layout binding (only SINT16/SINT32/FLOAT32 with word-order). So every DL205 quirk is encoded as a pre-computed raw 16-bit integer with the math worked out in inline comments above each register. Becomes unreadable past ~50 quirky registers; the README's 'alternatives' section recommends switching to pymodbus when that threshold approaches (pymodbus's ModbusSimulatorServer has first-class headless + scriptable callbacks for byte-level layouts).
Other ModbusPal 1.6b limitations called out in README: only holding_registers + coils sections in the official build (no input_registers / discrete_inputs — DL260 X-input markers can't be encoded faithfully here, FC02/FC04 tests wait for a fork or pymodbus); abandoned project (last release 1.6b, active forks at SCADA-LTS/ModbusPal, ControlThings-io/modbuspal, mrhenrike/ModbusPalEnhanced); no headless mode in the official JAR (-loadFile / -hide flags only in source-built forks); CVE-2018-10832 XXE on .xmpp import (don't import untrusted profiles — the in-repo ones are author-controlled).
README.md updated with: per-profile description tables, getting-started (download jar + java -jar + GUI File>Load>Run), MODBUS_SIM_ENDPOINT env-var override doc, two reference tables documenting which HR / coil address encodes which DL205 quirk + which test name asserts it (the same DL205_<behavior> naming convention from docs/v2/modbus-test-plan.md), 4-row alternatives comparison (pymodbus / diagslave / ModbusMechanic / ModRSsim2) for when ModbusPal can no longer carry the load, and a quick-reference XML format table at the bottom for future-me hand-authoring more profiles.
Pure documentation + test-asset PR — no code changes. The integration tests that consume these profiles (the actual DL205_<behavior> facts) land one at a time in PR 43+ as user validates each quirk via ModbusPal on the bench.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 20:05:20 -04:00

193 lines
9.9 KiB
XML

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE modbuspal_project SYSTEM "modbuspal.dtd">
<!--
DL205.xmpp — AutomationDirect DirectLOGIC DL205 / DL260 quirk simulator.
Slave id 1 on TCP 502. Models the real-PLC behaviors documented in
docs/v2/dl205.md as concrete register values, so integration tests can
assert each quirk WITHOUT a live PLC. The driver is correct when reads
against this profile produce the same logical values that an
AutomationDirect-aware client would see.
BIG WARNING: every "interesting" register here is encoded as a raw 16-bit
integer. ModbusPal 1.6b serves whatever you put in `value="..."` straight
onto the wire as a 16-bit big-endian register; it has no String / BCD /
Float / WordSwap binding (only SINT16 / SINT32 / FLOAT32 + word-order, none
of which capture the byte-level packing the DL series uses). So strings,
BCD, and CDAB floats live here as opaque integers with the math worked out
in the comment above each register. That math is reproduced in
docs/v2/dl205.md so the two stay in sync.
If this profile grows beyond ~50 quirky registers, switch to pymodbus
(see ModbusPal/README.md §"alternatives") — the magic-number table will
become unreadable. For the planned 12 DL205_<behavior> tests, raw values
are fine.
Loaded via the ModbusPal GUI: File > Load > pick this file > Run.
Run only ONE simulator at a time (they share TCP 502); to switch between
Standard and DL205, stop one before loading the other.
-->
<modbuspal_project>
<idgen value="200"/>
<links selected="TCP/IP">
<tcpip port="502"/>
<serial com="COM 1" baudrate="9600" parity="even" stops="1">
<flowcontrol xonxoff="false" rtscts="false"/>
</serial>
</links>
<slave id="1" enabled="true" name="DL205Sim" implementation="modbus">
<holding_registers>
<!-- ============================================================
V-MEMORY ADDRESSING MARKERS
============================================================
DirectLOGIC V-memory is octal natively; the CPU translates
V<oct> -> Modbus PDU <decimal>. Tests verify our address
helper produces the right PDU offset for known V-addresses.
Marker values are arbitrary but distinctive so a test that
reads the wrong PDU sees Goodread+wrong-value, not zero.
-->
<!-- V0 (octal) = PDU 0x0000. Decisively proves register 0 is valid
on DL205/DL260 with H2-ECOM100 in absolute mode (the default).
The "rejects register 0" rumour was a DL05/DL06 relative-mode
artefact — see dl205.md §Register Zero. -->
<!-- 0xCAFE = 51966 (signed 16-bit: -13570) -->
<register address="0" value="-13570" name="V0_marker_0xCAFE"/>
<!-- V2000 octal = decimal 1024 = PDU 0x0400. -->
<!-- 0x2000 = 8192 -->
<register address="1024" value="8192" name="V2000_marker_0x2000"/>
<!-- V40400 octal = decimal 8448 = PDU 0x2100. Proves the
"V40400 = register 0" myth wrong on absolute-mode firmware. -->
<!-- 0x4040 = 16448 -->
<register address="8448" value="16448" name="V40400_marker_0x4040"/>
<!-- ============================================================
STRING PACKING (the user's headline quirk)
============================================================
Two ASCII chars per register, FIRST CHAR in the LOW byte.
"Hello" at HR[0x410..0x412]:
HR[0x410] = 'H' (0x48) lo, 'e' (0x65) hi -> 0x6548 = 25928
HR[0x411] = 'l' (0x6C) lo, 'l' (0x6C) hi -> 0x6C6C = 27756
HR[0x412] = 'o' (0x6F) lo, '\0' (0x00) hi -> 0x006F = 111
A textbook (high-byte-first) decoder reads "eH" "ll" "\0o"
and prints "eHll \0o" — that's exactly the failure mode the
DL205 string test asserts NOT happens once we add the
ModbusStringByteOrder=LowFirst option to the driver.
Test: DL205_String_low_byte_first_within_register. -->
<register address="1040" value="25928" name="HelloStr_lo='H'_hi='e'"/>
<register address="1041" value="27756" name="HelloStr_lo='l'_hi='l'"/>
<register address="1042" value="111" name="HelloStr_lo='o'_hi=null"/>
<!-- ============================================================
32-BIT FLOAT IN CDAB WORD ORDER
============================================================
IEEE 754 float 1.5f = 0x3FC00000.
Standard ABCD: HR[N]=0x3FC0, HR[N+1]=0x0000
DL205 CDAB: HR[N]=0x0000, HR[N+1]=0x3FC0 (LOW word first)
Test: DL205_Float32_word_order_is_CDAB.
Driver must use ModbusByteOrder=WordSwap to decode this as 1.5. -->
<register address="1056" value="0" name="FloatCDAB_lo_word"/>
<!-- 0x3FC0 = 16320 -->
<register address="1057" value="16320" name="FloatCDAB_hi_word"/>
<!-- ============================================================
BCD-ENCODED REGISTER (DirectLOGIC default numeric storage)
============================================================
Ladder value 1234 stored as 0x1234 = 4660 (BCD nibbles, NOT
binary 1234 = 0x04D2). A driver in binary-int mode reads 4660
and reports the wrong value; in BCD mode it nibble-decodes
0x1234 -> 1234. Test: DL205_BCD_register_decodes_as_decimal. -->
<!-- 0x1234 = 4660 -->
<register address="1072" value="4660" name="BCD_1234_as_0x1234"/>
<!-- ============================================================
LOAD-LIMIT BOUNDARY MARKERS
============================================================
The DL series caps FC03 at 128 registers (above spec's 125)
and FC16 at 100 (BELOW spec's 123). The cap-tests don't need
specific values — they assert exception 03 IllegalDataValue
on an over-sized request. We pre-seed a contiguous block at
0x500..0x57F (128 regs) so a 128-register read returns Good
and a 129-register read can be tried for the failure case.
Per-register values: address - 0x500 (so HR[0x500]=0,
HR[0x501]=1, ..., HR[0x57F]=127). Easy mental verification.
Test: DL205_FC03_128_registers_returns_Good. -->
<!-- (Generated programmatically below for brevity — first / last + spot-check) -->
<register address="1280" value="0" name="FC03Block_first"/>
<register address="1281" value="1"/>
<register address="1282" value="2"/>
<register address="1343" value="63" name="FC03Block_mid"/>
<register address="1407" value="127" name="FC03Block_last"/>
<!-- Note: ModbusPal serves unlisted addresses as 0 by default for
reads that fall within the configured slave's address space.
The block-test relies on that behavior; the hand-listed
entries above are sanity markers. If the driver later wants
byte-perfect comparison across the whole 128-register range,
expand this section to one element per address (or switch to
pymodbus). -->
</holding_registers>
<coils>
<!-- ============================================================
COIL / DISCRETE-INPUT MAPPING MARKERS (DL260 layout)
============================================================
Per dl205.md, on the DL260:
X inputs -> discrete inputs 0..511 (FC02)
Y outputs -> coils 2048..2559 (FC01/05)
C relays -> coils 3072..4095 (FC01/05)
ModbusPal 1.6b does NOT have a discrete-inputs section in
the official build, so the X-input markers can't be
encoded faithfully (the driver test for FC02 against this
profile will need a fork or pymodbus). The Y and C coil
markers ARE encodable here.
-->
<!-- Y0 marker — coil 2048 ON proves "Y0 maps to coil 2048" mapping.
Test: DL205_Y0_maps_to_coil_2048. -->
<coil address="2048" value="1" name="Y0_marker"/>
<coil address="2049" value="0"/>
<coil address="2050" value="1"/>
<!-- C0 marker — coil 3072 ON proves "C0 maps to coil 3072" mapping.
Test: DL205_C0_maps_to_coil_3072. -->
<coil address="3072" value="1" name="C0_marker"/>
<coil address="3073" value="0"/>
<coil address="3074" value="1"/>
<!-- Scratch coils 4000..4007 for write-roundtrip tests against
the C-relay range. C ranges are writable on the real DL260. -->
<coil address="4000" value="0" name="Cscratch_0"/>
<coil address="4001" value="0"/>
<coil address="4002" value="0"/>
<coil address="4003" value="0"/>
<coil address="4004" value="0"/>
<coil address="4005" value="0"/>
<coil address="4006" value="0"/>
<coil address="4007" value="0"/>
</coils>
<tuning>
<!-- Zero delay / zero error rate. The DL205 H2-ECOM has a typical
2-10ms scan-cycle delay; if a test wants to simulate that,
tune via the ModbusPal GUI (Tuning > Reply delay). -->
<reply_delay min="0" max="0"/>
<error_rates no_reply="0.0"/>
</tuning>
</slave>
</modbuspal_project>