- docs/drivers/FOCAS.md and docs/v2/implementation/focas-wire-protocol.md pointed at focas-deployment.md and focas-simulator-plan.md, both of which were untracked drafts that have since been removed. Drop the refs (the wire-protocol companion now stands on its own; deployment guidance lives inline in the FOCAS driver doc). - Link the orphan v2 design docs from docs/README.md (multi-host dispatch, v2 release readiness, the historical lmx-followups tracker) and from modbus-test-plan.md (s7.md, mitsubishi.md per-family quirk catalogs, sibling to dl205.md). Surfaced by the doc audit; no content changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.3 KiB
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 packaged as a pinned Docker image
under tests/.../Modbus.IntegrationTests/Docker/. See that folder's
README.md for image-build notes + compose profiles. 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
_quirkJSON-comment fields next to each register). - Dockerized — pinned image means the CI simulator surface is
reproducible + no
pip installstep on the dev box. - Defaults to TCP 5020 (matches the compose port-map + the fixture default endpoint; sidesteps the Windows Firewall prompt on 502).
Setup pattern:
docker compose -f tests\...\Modbus.IntegrationTests\Docker\docker-compose.yml --profile <standard|dl205|mitsubishi|s7_1500> up -d.dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests— tests auto-skip when the endpoint is unreachable. Default endpoint islocalhost:5020; override viaMODBUS_SIM_ENDPOINTfor a real PLC on its native port 502.docker compose -f ... --profile <…> downwhen finished.
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).
Siemens SIMATIC S7
Quirk catalog at s7.md — covers S7-1200 / S7-1500 / S7-300 / S7-400 /
ET 200SP. Modbus TCP isn't native; each platform exposes it via a different
add-on module with its own register-mapping conventions.
Mitsubishi MELSEC
Quirk catalog at mitsubishi.md — Modbus TCP via add-on modules
across the MELSEC family.
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_Float32is easier to diagnose on failure than a genericFloat32_roundtrip. TheDL205_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 aSkipReasonstring, and have each test callAssert.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-memoryFakeTransportfrom 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.IntegrationTestswithModbusSimulatorFixture(TCP-probe, skips with a clearSkipReasonwhen the endpoint is unreachable),DL205/DL205Profile.cs(tag map stub), andDL205/DL205SmokeTests.cs(write-then-read round-trip). - PR 41 — DL205 quirk catalog doc — DONE.
docs/v2/dl205.mddocuments every DL205/DL260 Modbus divergence with primary-source citations. - PR 42 — ModbusPal
.xmppprofiles — 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. Dockerized under
Docker/profiles/(standard.json, dl205.json, mitsubishi.json, s7_1500.json); compose file launches each via a named profile. All 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
Docker/profiles/dl205.json.