# 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: ModbusPal** (Java, scriptable). Rationale: - Scriptable enough to mimic device-specific behaviors (non-standard register layouts, custom exception codes, intentional response delays). - Runs locally, no CI dependency. Tests skip when `localhost:502` (or the configured simulator endpoint) isn't reachable. - Free + long-maintained — physical PLC bench is unavailable in most dev environments, and renting cloud PLCs isn't worth the per-test cost. **Setup pattern** (not yet codified in a script — will land alongside the integration test project): 1. Install ModbusPal, load the per-device `.xmpp` profile from `tests/Driver.Modbus.IntegrationTests/ModbusPal/` (TBD directory). 2. Start the simulator listening on `localhost:502` (or override via `MODBUS_SIM_ENDPOINT` env var). 3. `dotnet test` the integration project — tests auto-skip when the endpoint is unreachable, so forgetting to start the simulator doesn't wedge CI. ## 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 ModbusPal 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. ## 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 — one writable holding register at address 100), and `DL205/DL205SmokeTests.cs` (write-then-read round-trip). `ModbusPal/` directory holds the README pointing at the to-be-committed `DL205.xmpp` profile. - **PR 31+**: one PR per confirmed DL205 quirk, landing the named test + any driver-side adjustment (e.g., retry on dropped TxId) needed to pass it. Drop the `DL205.xmpp` profile into `ModbusPal/` alongside the first quirk PR.