From 02a0e8efd13891d1abf2c4d30d6d06e6beb20997 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 18 Apr 2026 20:05:20 -0400 Subject: [PATCH] =?UTF-8?q?Phase=203=20PR=2042=20=E2=80=94=20ModbusPal=20s?= =?UTF-8?q?imulator=20profiles=20for=20Standard=20Modbus=20+=20DL205/DL260?= =?UTF-8?q?=20quirks.=20Two=20hand-authored=20.xmpp=20profiles=20in=20test?= =?UTF-8?q?s/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/?= =?UTF-8?q?=20that=20integration=20tests=20load=20via=20the=20GUI=20to=20d?= =?UTF-8?q?rive=20the=20suite=20without=20a=20real=20PLC.=20Both=20well-fo?= =?UTF-8?q?rmed=20XML=20(verified=20via=20PowerShell=20[xml]=20cast);=20bo?= =?UTF-8?q?th=20copied=20to=20test-output=20as=20PreserveNewest=20content?= =?UTF-8?q?=20per=20the=20existing=20csproj=20rule.=20Standard.xmpp=20?= =?UTF-8?q?=E2=80=94=20generic=20Modbus=20TCP=20server=20on=20port=20502,?= =?UTF-8?q?=20slave=20id=201.=20HR[0..31]=20seeded=20with=20address-as-val?= =?UTF-8?q?ue=20(HR[5]=3D5=20=E2=80=94=20easy=20mental=20map=20for=20diagn?= =?UTF-8?q?ostics),=20HR[100]=20auto-incrementing=20via=20a=201Hz=20Linear?= =?UTF-8?q?Generator=20binding=20(drives=20subscribe-and-receive=20integra?= =?UTF-8?q?tion=20tests=20so=20they=20have=20a=20register=20that=20actuall?= =?UTF-8?q?y=20changes=20without=20a=20write),=20HR[200..209]=20scratch=20?= =?UTF-8?q?range=20for=20write-roundtrip=20tests,=20coils=200..31=20altern?= =?UTF-8?q?ating=20on/off,=20coils=20100..109=20scratch.=20The=20Tick=20au?= =?UTF-8?q?tomation=20runs=200..65535=20over=2060s=20looping;=20bound=20to?= =?UTF-8?q?=20HR[100]=20via=20Binding=5FSINT16=20=E2=80=94=20slow=20enough?= =?UTF-8?q?=20that=20a=20250ms-poll=20integration=20test=20sees=20discrete?= =?UTF-8?q?=20jumps,=20fast=20enough=20that=20a=205s=20subscribe=20test=20?= =?UTF-8?q?sees=20several=20change=20notifications.=20DL205.xmpp=20?= =?UTF-8?q?=E2=80=94=20AutomationDirect=20DirectLOGIC=20DL205/DL260=20quir?= =?UTF-8?q?k=20simulator=20on=20port=20502,=20slave=20id=201,=20modeling?= =?UTF-8?q?=20the=20behaviors=20documented=20in=20docs/v2/dl205.md=20as=20?= =?UTF-8?q?concrete=20register=20values=20so=20DL205=20integration=20tests?= =?UTF-8?q?=20can=20assert=20each=20quirk=20WITHOUT=20a=20live=20PLC.=20Pe?= =?UTF-8?q?r-quirk=20encoding:=20V0=20marker=20at=20HR[0]=3D0xCAFE=20prove?= =?UTF-8?q?s=20register=200=20is=20valid=20(rejects-register-0=20rumour=20?= =?UTF-8?q?disproved);=20V2000=20marker=20at=20HR[1024]=3D0x2000=20proves?= =?UTF-8?q?=20V-memory=20octal-to-decimal=20mapping;=20V40400=20marker=20a?= =?UTF-8?q?t=20HR[8448]=3D0x4040=20proves=20V40400=E2=86=92PDU=200x2100=20?= =?UTF-8?q?(NOT=20register=200,=20contrary=20to=20the=20widespread=20short?= =?UTF-8?q?hand);=20'Hello'=20string=20at=20HR[1040..1042]=20packed=20firs?= =?UTF-8?q?t-char-low-byte=20(HR[1040]=3D0x6548=20=3D=20'H'=20lo=20+=20'e'?= =?UTF-8?q?=20hi,=20HR[1041]=3D0x6C6C,=20HR[1042]=3D0x006F)=20=E2=80=94=20?= =?UTF-8?q?the=20headline=20string-byte-order=20quirk=20the=20user=20flagg?= =?UTF-8?q?ed;=20Float32=201.5f=20at=20HR[1056..1057]=20in=20CDAB=20word?= =?UTF-8?q?=20order=20(low=20word=20first:=200,=20then=200x3FC0);=20BCD=20?= =?UTF-8?q?register=20at=20HR[1072]=3D0x1234=20representing=20decimal=2012?= =?UTF-8?q?34=20in=20BCD=20nibbles=20(NOT=20binary=200x04D2);=20128-regist?= =?UTF-8?q?er=20block=20at=20HR[1280..1407]=20for=20FC03-128-cap=20testing?= =?UTF-8?q?;=20Y0=20marker=20at=20coil=202048,=20C0=20marker=20at=20coil?= =?UTF-8?q?=203072,=20scratch=20C-coils=20at=204000..4007=20for=20write=20?= =?UTF-8?q?tests.=20Critical=20limitation=20flagged=20inline=20+=20in=20RE?= =?UTF-8?q?ADME:=20ModbusPal=201.6b=20CANNOT=20represent=20the=20DL205=20q?= =?UTF-8?q?uirks=20semantically=20=E2=80=94=20it=20has=20no=20string=20bin?= =?UTF-8?q?ding,=20no=20BCD=20binding,=20no=20arbitrary-byte-layout=20bind?= =?UTF-8?q?ing=20(only=20SINT16/SINT32/FLOAT32=20with=20word-order).=20So?= =?UTF-8?q?=20every=20DL205=20quirk=20is=20encoded=20as=20a=20pre-computed?= =?UTF-8?q?=20raw=2016-bit=20integer=20with=20the=20math=20worked=20out=20?= =?UTF-8?q?in=20inline=20comments=20above=20each=20register.=20Becomes=20u?= =?UTF-8?q?nreadable=20past=20~50=20quirky=20registers;=20the=20README's?= =?UTF-8?q?=20'alternatives'=20section=20recommends=20switching=20to=20pym?= =?UTF-8?q?odbus=20when=20that=20threshold=20approaches=20(pymodbus's=20Mo?= =?UTF-8?q?dbusSimulatorServer=20has=20first-class=20headless=20+=20script?= =?UTF-8?q?able=20callbacks=20for=20byte-level=20layouts).=20Other=20Modbu?= =?UTF-8?q?sPal=201.6b=20limitations=20called=20out=20in=20README:=20only?= =?UTF-8?q?=20holding=5Fregisters=20+=20coils=20sections=20in=20the=20offi?= =?UTF-8?q?cial=20build=20(no=20input=5Fregisters=20/=20discrete=5Finputs?= =?UTF-8?q?=20=E2=80=94=20DL260=20X-input=20markers=20can't=20be=20encoded?= =?UTF-8?q?=20faithfully=20here,=20FC02/FC04=20tests=20wait=20for=20a=20fo?= =?UTF-8?q?rk=20or=20pymodbus);=20abandoned=20project=20(last=20release=20?= =?UTF-8?q?1.6b,=20active=20forks=20at=20SCADA-LTS/ModbusPal,=20ControlThi?= =?UTF-8?q?ngs-io/modbuspal,=20mrhenrike/ModbusPalEnhanced);=20no=20headle?= =?UTF-8?q?ss=20mode=20in=20the=20official=20JAR=20(-loadFile=20/=20-hide?= =?UTF-8?q?=20flags=20only=20in=20source-built=20forks);=20CVE-2018-10832?= =?UTF-8?q?=20XXE=20on=20.xmpp=20import=20(don't=20import=20untrusted=20pr?= =?UTF-8?q?ofiles=20=E2=80=94=20the=20in-repo=20ones=20are=20author-contro?= =?UTF-8?q?lled).=20README.md=20updated=20with:=20per-profile=20descriptio?= =?UTF-8?q?n=20tables,=20getting-started=20(download=20jar=20+=20java=20-j?= =?UTF-8?q?ar=20+=20GUI=20File>Load>Run),=20MODBUS=5FSIM=5FENDPOINT=20env-?= =?UTF-8?q?var=20override=20doc,=20two=20reference=20tables=20documenting?= =?UTF-8?q?=20which=20HR=20/=20coil=20address=20encodes=20which=20DL205=20?= =?UTF-8?q?quirk=20+=20which=20test=20name=20asserts=20it=20(the=20same=20?= =?UTF-8?q?DL205=5F=20naming=20convention=20from=20docs/v2/modbu?= =?UTF-8?q?s-test-plan.md),=204-row=20alternatives=20comparison=20(pymodbu?= =?UTF-8?q?s=20/=20diagslave=20/=20ModbusMechanic=20/=20ModRSsim2)=20for?= =?UTF-8?q?=20when=20ModbusPal=20can=20no=20longer=20carry=20the=20load,?= =?UTF-8?q?=20and=20a=20quick-reference=20XML=20format=20table=20at=20the?= =?UTF-8?q?=20bottom=20for=20future-me=20hand-authoring=20more=20profiles.?= =?UTF-8?q?=20Pure=20documentation=20+=20test-asset=20PR=20=E2=80=94=20no?= =?UTF-8?q?=20code=20changes.=20The=20integration=20tests=20that=20consume?= =?UTF-8?q?=20these=20profiles=20(the=20actual=20DL205=5F=20fact?= =?UTF-8?q?s)=20land=20one=20at=20a=20time=20in=20PR=2043+=20as=20user=20v?= =?UTF-8?q?alidates=20each=20quirk=20via=20ModbusPal=20on=20the=20bench.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ModbusPal/DL205.xmpp | 192 ++++++++++++++++++ .../ModbusPal/README.md | 113 +++++++++-- .../ModbusPal/Standard.xmpp | 166 +++++++++++++++ 3 files changed, 452 insertions(+), 19 deletions(-) create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/DL205.xmpp create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/Standard.xmpp diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/DL205.xmpp b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/DL205.xmpp new file mode 100644 index 0000000..46ac26f --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/DL205.xmpp @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/README.md b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/README.md index c2fcdbd..6d062eb 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/README.md +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/README.md @@ -1,30 +1,105 @@ # ModbusPal simulator profiles -Drop device-specific `.xmpp` profiles here. The integration tests connect to the -endpoint in `MODBUS_SIM_ENDPOINT` (default `localhost:502`) and expect the -simulator to already be running — tests do not launch ModbusPal themselves, -because its Java GUI + JRE requirement is heavier than the harness is worth. +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 from SourceForge (`modbuspal.jar`). +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. Load a profile from this directory (or configure one manually) and start the - simulator on TCP port 502. -4. `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. +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`). -## Profile files +## Switching between Standard and DL205 -- `DL205.xmpp` — _to be added_ — register map reflecting the AutomationDirect - DL205 quirks tracked in `docs/v2/modbus-test-plan.md`. The scaffolded smoke - test in `DL205/DL205SmokeTests.cs` needs holding register 100 writable and - present; a minimal ModbusPal profile with a single holding-register bank at - address 100 is sufficient. +Stop the running simulator (toolbar's **Stop** button), **File > Load** +the other profile, **Run**. ## Environment variables -- `MODBUS_SIM_ENDPOINT` — override the simulator endpoint. Accepts `host:port`; - defaults to `localhost:502`. Useful when pointing the suite at a real PLC on - the bench. +- `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 +`` with three children: +- `` — internal id counter (start at 100+) +- `` — `` for TCP listen, plus a `` placeholder +- One or more `` containing `` (``), `` (``), `` + +Per-register `` 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. diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/Standard.xmpp b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/Standard.xmpp new file mode 100644 index 0000000..ec89678 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/Standard.xmpp @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- 2.49.1