Files
lmxopcua/docs/v2/modbus-test-plan.md
Joseph Doherty a05b84858d Phase 3 PR 43 — Swap ModbusPal to pymodbus for the integration-test simulator. Replaces the .xmpp profiles shipped in PR 42 with pymodbus 3.13.0 ModbusSimulatorServer JSON configs in tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/. Substantive reasons for the swap (rationale block in the test-plan doc): ModbusPal 1.6b is abandoned (last release ~2019), Java GUI-only with no headless mode in the official JAR, and only exposes 2 of the 4 standard Modbus tables (holding_registers + coils — no input_registers, no discrete_inputs). pymodbus is current stable, pure Python CLI (pip install pymodbus[simulator]==3.13.0), exposes all four tables, has built-in declarative actions (increment / random / timestamp / uptime) for dynamic registers, supports custom Python actions for anything more complex, and ships an optional aiohttp-based web UI / REST API for live inspection. Pip-installable on Windows; sidesteps the privileged-port admin requirement by defaulting to TCP 5020.
ModbusSimulatorFixture default port bumped from 502 to 5020 to match the pymodbus convention. Override via MODBUS_SIM_ENDPOINT for a real PLC on its native 502. Skip-message updated to point at the new Pymodbus\serve.ps1 wrapper instead of 'start ModbusPal'. csproj <None Update> rule swapped from ModbusPal/** to Pymodbus/** so the new JSON profiles + serve.ps1 + README copy to test-output as PreserveNewest.
standard.json — generic Modbus TCP server, slave id 1, port 5020, shared blocks=false (independent coils + HR address spaces, more textbook-PLC-like). HR[0..31] seeded with address-as-value via per-register uint16 entries, HR[100] auto-increments via the built-in increment action with parameters minval=0/maxval=65535 (drives subscribe-and-receive integration tests so they have a register that ticks without a write — pymodbus's increment ticks per-access not wall-clock, which is good enough for a 250ms-poll test), HR[200..209] scratch range left at 0 for write tests, coils 0..31 alternating, coils 100..109 scratch. write list covers 0..1023 so any test address is mutable.
dl205.json — AutomationDirect DirectLOGIC DL205/DL260 quirk simulator, slave id 1, port 5020, shared blocks=true (matches DL series memory model where coils/DI/HR overlay the same word address space). Each quirky register seeded with the pre-computed raw uint16 value documented in docs/v2/dl205.md, with an inline _quirk JSON-comment naming the behavior so future-me reading the file knows why HR[1040]=25928 means 'H' lo / 'e' hi (the user's headline string-byte-order finding). Encoded quirks: V0 marker at HR[0]=0xCAFE; V2000 at HR[1024]=0x2000; V40400 at HR[8448]=0x4040; 'Hello' string at HR[1040..1042] first-char-low-byte; Float32 1.5f at HR[1056..1057] in CDAB word order (low word first); BCD register at HR[1072]=0x1234; FC03-128-cap block at HR[1280..1407]; Y0/C0 coil markers at 2048/3072; scratch C-relays at 4000..4007.
serve.ps1 wrapper — pwsh script with a -Profile {standard|dl205} parameter switch. Validates pymodbus.simulator is on PATH (clearer message than the raw CommandNotFoundException), validates the profile JSON exists, builds the right --modbus_server/--modbus_device/--json_file/--http_port arg list, and execs pymodbus.simulator in the foreground. -HttpPort 0 disables the web UI. Foreground exec lets the operator Ctrl+C to stop without an extra control script.
README.md fully rewritten for pymodbus: install command (pip install 'pymodbus[simulator]==3.13.0' — pinned for reproducibility, [simulator] extra pulls aiohttp), per-profile reference tables, the same DL205 quirk → register table from PR 42 but adjusted for pymodbus paths, what's-NEW-vs-ModbusPal section (all four tables, raw uint16 seeding, declarative actions, custom Python action modules, headless, web UI, maintained), trade-offs section (float32-as-two-uint16s for explicit CDAB control, increment ticks per-access not wall-clock, shared-blocks mode for DL205 vs separate for Standard), file-format quick reference for hand-authoring more profiles. References pinned to the pymodbus readthedocs simulator/config + REST API pages.
docs/v2/modbus-test-plan.md harness section rewritten with the swap rationale; PR-history list updated to mark PR 42 SUPERSEDED by PR 43 and call out PR 44+ as the per-quirk implementation track. Test-conventions bullet about 'don't depend on ModbusPal state between tests' generalized to 'don't depend on simulator state' and a note added that pymodbus's REST API can reset state between facts if a test ever needs it.
DL205Profile.cs and DL205SmokeTests.cs xml-doc updated to reference pymodbus / dl205.json instead of ModbusPal / DL205.xmpp.
Functional validation deferred — Python isn't installed on this dev box (winget search returned no matches for Python.Python.3 exact). JSON parses structurally (PowerShell ConvertFrom-Json clean on both files), build clean, .json + serve.ps1 + README all copy to test-output as expected. User installs pymodbus when they want to actually run the simulator end-to-end; if pymodbus rejects the config the README's reference link to pymodbus's simulator/config schema doc is the right next stop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 20:35:26 -04:00

7.7 KiB
Raw Blame History

Modbus driver — test plan + device-quirk catalog

The Modbus TCP driver unit tests (PRs 2124) cover the protocol surface against an in-memory fake transport. They validate the codec, state machine, and function-code routing against a textbook Modbus server. That's necessary but not sufficient: real PLC populations disagree with the spec in small, device-specific ways, and a driver that passes textbook tests can still misbehave against actual equipment.

This doc is the harness-and-quirks playbook. The project it describes lives at tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ — scaffolded in PR 30 with the simulator fixture, DL205 profile stub, and one write/read smoke test. Each confirmed DL205 quirk lands in a follow-up PR as a named test in that project.

Harness

Chosen simulator: pymodbus 3.13.0 (pip install 'pymodbus[simulator]==3.13.0'). Replaced ModbusPal in PR 43 — see tests/.../Pymodbus/README.md for the trade-off rationale. Headline reasons:

  • Headless pure-Python CLI; no Java GUI, runs cleanly on a CI runner.
  • Maintained — current stable 3.13.0; ModbusPal 1.6b is abandoned.
  • All four standard tables (HR, IR, coils, DI) configurable; ModbusPal 1.6b only exposed HR + coils.
  • Built-in actions (increment, random, timestamp, uptime) + optional custom-Python actions for declarative dynamic behaviors.
  • Per-register raw uint16 seeding — encoding the DL205 string-byte-order / BCD / CDAB-float quirks stays explicit (the quirk math lives in the _quirk JSON-comment fields next to each register).
  • Pip-installable on Windows; sidesteps the privileged-port admin requirement by defaulting to TCP 5020 instead of 502.

Setup pattern:

  1. pip install "pymodbus[simulator]==3.13.0".
  2. Start the simulator with one of the in-repo profiles: tests\.../Pymodbus\serve.ps1 -Profile standard (or -Profile dl205).
  3. dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests — tests auto-skip when the endpoint is unreachable. Default endpoint is localhost:5020; override via MODBUS_SIM_ENDPOINT for a real PLC on its native port 502.

Per-device quirk catalog

AutomationDirect DL205 / DL260

First known target device family. Full quirk catalog with primary-source citations and per-quirk integration-test names lives at dl205.md — that doc is the reference; this section is the testing roadmap.

Confirmed quirks (priority order — top items are highest-impact for our driver and ship first as PR 41+):

Quirk Driver impact Integration-test name
String packing: 2 chars/register, first char in low byte (opposite of generic Modbus) ModbusDataType.String decoder must be configurable per-device family — current code assumes high-byte-first DL205_String_low_byte_first_within_register
Word order CDAB for Int32/UInt32/Float32 Already configurable via ModbusByteOrder.WordSwap; default per device profile DL205_Int32_word_order_is_CDAB
BCD-as-default numeric storage (only IEEE 754 when ladder uses R type) New decoder mode — register reads as 0x1234 for ladder value 1234, not as decimal 4660 DL205_BCD_register_decodes_as_hex_nibbles
FC16 capped at 100 registers (below the spec's 123) Bulk-write batching must cap per-device-family DL205_FC16_101_registers_returns_IllegalDataValue
FC03/04 capped at 128 (above the spec's 125) Less impactful — clients that respect the spec's 125 stay safe DL205_FC03_129_registers_returns_IllegalDataValue
V-memory octal-to-decimal addressing (V2000 octal → 0x0400 decimal) New address-format helper in profile config so operators can write V2000 instead of computing 1024 themselves DL205_Vmem_V2000_maps_to_PDU_0x0400
C-relay → coil 3072 / Y-output → coil 2048 offsets Hard-coded constants in DL205 device profile DL205_C0_maps_to_coil_3072, DL205_Y0_maps_to_coil_2048
Register 0 is valid (rejects-register-0 rumour was DL05/DL06 relative-mode artefact) None — current default is safe DL205_FC03_register_0_returns_V0_contents
Max 4 simultaneous TCP clients on H2-ECOM100 Connect-time: handle TCP-accept failure with a clearer error message DL205_5th_TCP_connection_refused
No TCP keepalive Driver-side periodic-probe (already wired via IHostConnectivityProbe) Covered by existing ModbusProbeTests
No mid-stream resync on malformed MBAP Already covered — single-flight + reconnect-on-error Covered by existing ModbusDriverTests
Write-protect exception code: 02 newer / 04 older Translate either to BadNotWritable DL205_FC06_in_ProgramMode_returns_ServerFailure

Operator-reported / unconfirmed — covered defensively in the driver but no integration tests until reproduced on hardware:

  • TxId drop under load (forum rumour; not reproduced).
  • Pre-2004 firmware ABCD word order (every shipped DL205/DL260 since 2004 is CDAB).

Future devices

One section per device class, same shape as DL205. Quirks that apply across multiple devices (e.g., "all AB PLCs use CDAB") can be noted in the cross-device patterns section below once we have enough data points.

Cross-device patterns

Once multiple device catalogs accumulate, quirks that recur across two or more vendors get promoted into driver defaults or opt-in options:

  • (empty — filled in as catalogs grow)

Test conventions

  • One named test per quirk. DL205_word_order_is_CDAB_for_Float32 is easier to diagnose on failure than a generic Float32_roundtrip. The DL205_ prefix makes filtering by device class trivial (--filter "DisplayName~DL205").
  • Skip with a clear SkipReason. Follow the pattern from GalaxyRepositoryLiveSmokeTests: check reachability in the fixture, capture a SkipReason string, and have each test call Assert.Skip(SkipReason) when it's set. Don't throw — skipped tests read cleanly in CI logs.
  • Use the real ModbusTcpTransport. Integration tests exercise the wire protocol end-to-end. The in-memory FakeTransport from the unit test suite is deliberately not used here — its value is speed + determinism, which doesn't help reproduce device-specific issues.
  • Don't depend on simulator state between tests. Each test resets the simulator's register bank or uses a unique address range. Avoid relying on "previous test left value at register 10" setups that flake when tests run in parallel or re-order. Either the test mutates the scratch ranges and restores on finally, or it uses pymodbus's REST API to reset state between facts.

Next concrete PRs

  • PR 30 — Integration test project + DL205 profile scaffoldDONE. Shipped tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests with ModbusSimulatorFixture (TCP-probe, skips with a clear SkipReason when the endpoint is unreachable), DL205/DL205Profile.cs (tag map stub), and DL205/DL205SmokeTests.cs (write-then-read round-trip).
  • PR 41 — DL205 quirk catalog docDONE. docs/v2/dl205.md documents every DL205/DL260 Modbus divergence with primary-source citations.
  • PR 42 — ModbusPal .xmpp profilesSUPERSEDED by PR 43. Replaced with pymodbus JSON because ModbusPal 1.6b is abandoned, GUI-only, and only exposes 2 of the 4 standard tables.
  • PR 43 — pymodbus JSON profilesDONE. Pymodbus/standard.json + Pymodbus/dl205.json + Pymodbus/serve.ps1 runner. Both bind TCP 5020.
  • PR 44+: one PR per confirmed DL205 quirk, landing the named test + any driver-side adjustment (string byte order, BCD decoder, V-memory address helper, FC16 cap-per-device-family) needed to pass it. Each quirk's value is already pre-encoded in Pymodbus/dl205.json.