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>
107 lines
6.8 KiB
Markdown
107 lines
6.8 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: 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.
|