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

106 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](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`](DL205.xmpp) | AutomationDirect DirectLOGIC DL205 / DL260 quirks per [`docs/v2/dl205.md`](../../../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](https://sourceforge.net/projects/modbuspal/) — `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.