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 SourceForge — modbuspal.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.