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>
122 lines
7.7 KiB
Markdown
122 lines
7.7 KiB
Markdown
# Modbus driver — test plan + device-quirk catalog
|
||
|
||
The Modbus TCP driver unit tests (PRs 21–24) 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`](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 scaffold** — **DONE**.
|
||
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 doc** — **DONE**. `docs/v2/dl205.md`
|
||
documents every DL205/DL260 Modbus divergence with primary-source citations.
|
||
- **PR 42 — ModbusPal `.xmpp` profiles** — **SUPERSEDED 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 profiles** — **DONE**. `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`.
|