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>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user