Files
lmxopcua/docs/v2/modbus-test-plan.md
Joseph Doherty 9de96554dc Phase 3 PR 41 — Document AutomationDirect DL205 / DL260 Modbus quirks. Adds docs/v2/dl205.md (~300 lines, 8 H2 sections, primary-source citations) covering every place the DL205/DL260 family diverges from textbook Modbus or has non-obvious behavior a generic client gets wrong. Replaces the placeholder _pending_ list in modbus-test-plan.md with a confirmed-behaviors table that doubles as the integration-test roadmap.
The user explicitly flagged that DL205/DL260 strings don't follow Modbus convention; research turned up that and a lot more. Headline findings:
String packing — TWO chars per V-memory register but the FIRST char is in the LOW byte (opposite of the big-endian Modbus convention generic drivers default to). 'Hello' in V2000 reads back as 'eHll o\0' on a textbook decoder. Kepware's DirectLogic driver exposes a per-tag 'String Byte Order = Low/High' toggle specifically for this; we'll need the same. Null-terminated, no length prefix, no dedicated KSTR address space — strings live wherever ladder allocates them in V-memory.
V-memory addressing — DirectLOGIC's native V-memory is OCTAL (V2000, V40400) but Modbus is decimal. The CPU translates: V2000 octal = decimal 1024 = Modbus PDU 0x0400. The widespread 'V40400 = register 0' shorthand is wrong on modern firmware (that was DL05/DL06 relative mode); on H2-ECOM100 absolute mode (factory default) V40400 = PDU 0x2100. We'd surface this with an address-format helper in the device profile so operators write V2000 instead of computing 1024 by hand.
Word order CDAB for all 32-bit values — DL205 and DL260 agree, ECOM modules don't re-swap. Already supported via ModbusByteOrder.WordSwap; just needs to be the default in the DL205 profile.
BCD-as-default numeric storage — bit one I didn't expect. DirectLOGIC stores 'V2000 = 1234' as 0x1234 on the wire (BCD nibbles), not as 0x04D2 (decimal 1234). IEEE 754 Float32 only works when ladder used the explicit R type (LDR/OUTR instructions). We need a new decoder mode for BCD-encoded registers — current code assumes binary integers.
FC quantity caps — FC03/04 cap at 128 (above spec's 125 — Bonus territory, current code already respects 125), FC16 caps at 100 (BELOW spec's 123 — important bulk-write batching gotcha). Quantity overrun returns exception 03 IllegalDataValue.
Coil/discrete mappings — DL260: X0->discrete input 0, Y0->coil 2048, C0->coil 3072. SP specials at discrete input 1024-1535 RO. These are CPU-wired constants and cannot be remapped; need to be hardcoded in the DL205/DL260 device profile.
Register 0 — accepted on DL205/DL260 with ECOM in absolute mode, contrary to the widespread internet claim that 'DirectLOGIC rejects register 0'. That rumour was an older DL05/DL06 relative-mode artefact. Our ModbusProbeOptions.ProbeAddress default of 0 is therefore safe for DL205/DL260.
Exception codes — only the standard 01-04. Write-to-protected-bit returns 02 on newer firmware, 04 on older (firmware-transition revision unconfirmed); driver should map both to BadNotWritable. No proprietary exception codes.
Behavioral oddities — H2-ECOM100 accepts MAX 4 simultaneous TCP connections (5th refused at TCP accept). No TCP keepalive (intermediate NAT/firewall drops idle sockets after 2-5 min — periodic probe required). No mid-stream resync on malformed MBAP — driver must reconnect + replay. TxId-drop-under-load forum rumour is unconfirmed; our single-flight + TxId-match guard handles it either way.
Each H2 section ends with the integration-test names we'd ship per the modbus-test-plan.md DL205_<behavior> convention — twelve named test slots ready for PR 42+ to fill in one at a time. References (8) cited inline, primarily D2-USER-M, HA-ECOM-M, and the Kepware DirectLogic Ethernet driver manual which documents these vendor quirks explicitly because they have to cope with them.
modbus-test-plan.md DL205 section rewritten as a priority-ordered table with three columns (quirk / driver impact / test name), pointing the reader at dl205.md for the full reference. Operator-reported items separated into a tail subsection so future-me knows which behaviors are documented vs reproduced-on-hardware.
Pure documentation PR — no code changes. The actual driver work (string-byte-order option, BCD decoder mode, V-memory address helper, FC16 cap-per-device-family, multi-client TCP handling) lands one PR per quirk in PR 42+ as ModbusPal validation completes.

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

6.8 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: 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 — 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 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 — 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.