Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal
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
..

ModbusPal simulator profiles

Two hand-authored .xmpp profiles you load into ModbusPal to drive the integration-test suite without a real PLC:

File What it simulates Test category
Standard.xmpp Generic Modbus TCP server — HR[0..31] = address-as-value, alternating coils, one auto-incrementing register at HR[100] for subscribe tests, scratch ranges for write-roundtrip tests. Trait=Standard
DL205.xmpp AutomationDirect DirectLOGIC DL205 / DL260 quirks per docs/v2/dl205.md: low-byte-first string packing, CDAB Float32, BCD numerics, V-memory address markers, Y/C coil mappings. Trait=DL205

Both listen on TCP port 502 (the standard Modbus port — change in the ModbusPal GUI if a port conflict). Run only one at a time since they share the port.

Getting started

  1. Download ModbusPal 1.6b from SourceForgemodbuspal.jar. Requires Java 8+ (Java 17/21 work but emit Swing deprecation warnings).
  2. java -jar modbuspal.jar to launch the GUI.
  3. File > Load → pick Standard.xmpp (or DL205.xmpp).
  4. Click the Run button (top-right of the toolbar) to start serving on TCP 502.
  5. dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests — tests auto-skip with a clear SkipReason if the TCP probe at the configured endpoint fails within 2 seconds (ModbusSimulatorFixture).

Switching between Standard and DL205

Stop the running simulator (toolbar's Stop button), File > Load the other profile, Run.

Environment variables

  • MODBUS_SIM_ENDPOINT — override the simulator endpoint (host:port). Defaults to localhost:502. Useful when pointing the suite at a real PLC on the bench, or running ModbusPal on a non-default port.

What's encoded in each profile

Standard

  • HR[0..31]: each register's value equals its address.
  • HR[100]: bound to a LinearGenerator (0..65535 over 60s, looping) — drives subscribe-and-receive tests.
  • HR[200..209]: scratch range for write-roundtrip tests.
  • Coils[0..31]: alternating on/off (even=on).
  • Coils[100..109]: scratch range.

DL205 (per docs/v2/dl205.md)

HR address Quirk demonstrated Raw value Decoded value
0 Register zero is valid (rejects-register-0 rumour disproved) -13570 (0xCAFE) marker
1024 (= V2000 octal) V-memory octal-to-decimal mapping 8192 (0x2000) marker
8448 (= V40400 octal) V40400 → PDU 0x2100 (NOT register 0) 16448 (0x4040) marker
1040..1042 String "Hello" packed first-char-low-byte 25928, 27756, 111 "Hello"
1056..1057 Float32 1.5f in CDAB word order 0, 16320 1.5f
1072 Decimal 1234 in BCD encoding 4660 (0x1234) 1234
1280..1407 128-register block (FC03 cap = 128 above spec's 125) address 1280 for FC03 cap test
Coil address Quirk demonstrated
2048 Y0 maps to coil 2048 (DL260 layout)
3072 C0 maps to coil 3072 (DL260 layout)
4000..4007 Scratch C-relay range for write-roundtrip tests

Limitations of ModbusPal 1.6b

  • Only holding_registers + coils sections in the official build — no input_registers (FC04) and no discrete_inputs (FC02). DL205's X-input markers can't be encoded faithfully here. Tests for FC02 / FC04 wait for a fork (e.g. SCADA-LTS/ModbusPal) or a pymodbus rewrite.
  • No semantic bindings for strings / BCD / arbitrary byte layouts. The DL205 profile encodes everything as pre-computed raw 16-bit integers with the math worked out in inline comments. Anything fancier becomes unreadable above ~50 quirky registers — switch to pymodbus when that threshold approaches.
  • Project is abandoned since 1.6b on the official SourceForge listing. Active forks: SCADA-LTS/ModbusPal, ControlThings-io/modbuspal, mrhenrike/ModbusPalEnhanced.
  • No headless mode in the official 1.6b JAR (-loadFile / -hide flags exist only in source-built forks). For CI use, plan to switch to pymodbus's ModbusSimulatorServer (JSON config, scriptable callbacks, first-class headless).
  • CVE-2018-10832 XXE in .xmpp import. Don't import .xmpp files from untrusted sources. Profiles in this repo are author-controlled; safe.

Alternatives if ModbusPal stops working

Tool Pros Cons
pymodbus ModbusSimulatorServer Headless-first, JSON config, per-register seeding, custom callbacks for byte-level layouts. Best CI fit. Python dependency.
diagslave Simple, headless, fast. Flat register banks; no per-address seeding from config; no scripting.
ModbusMechanic Headless config-file mode. Lightly documented.
ModRSsim2 Windows GUI, CSV import, scripting. GUI-centric.

File format reference

ModbusPal .xmpp is XML with a DTD reference (modbuspal.dtd). Root element <modbuspal_project> with three children:

  • <idgen value="N"/> — internal id counter (start at 100+)
  • <links selected="TCP/IP"><tcpip port="502"/> for TCP listen, plus a <serial> placeholder
  • One or more <slave id="..." enabled="true" name="..." implementation="modbus"> containing <holding_registers> (<register address="N" value="V"/>), <coils> (<coil address="N" value="0|1"/>), <tuning>

Per-register <binding automation="..." class="Binding_SINT16|SINT32|FLOAT32" order="0|1"/> ties a register to a LinearGenerator / RandomGenerator / SineGenerator automation declared at the project level. order="0" = LSW, order="1" = MSW for 32-bit types. There is no string binding and no byte-swap-within-word binding.