From 9de96554dc2ae111bae6dae60d17d87227cecf43 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 18 Apr 2026 19:49:35 -0400 Subject: [PATCH] =?UTF-8?q?Phase=203=20PR=2041=20=E2=80=94=20Document=20Au?= =?UTF-8?q?tomationDirect=20DL205=20/=20DL260=20Modbus=20quirks.=20Adds=20?= =?UTF-8?q?docs/v2/dl205.md=20(~300=20lines,=208=20H2=20sections,=20primar?= =?UTF-8?q?y-source=20citations)=20covering=20every=20place=20the=20DL205/?= =?UTF-8?q?DL260=20family=20diverges=20from=20textbook=20Modbus=20or=20has?= =?UTF-8?q?=20non-obvious=20behavior=20a=20generic=20client=20gets=20wrong?= =?UTF-8?q?.=20Replaces=20the=20placeholder=20=5Fpending=5F=20list=20in=20?= =?UTF-8?q?modbus-test-plan.md=20with=20a=20confirmed-behaviors=20table=20?= =?UTF-8?q?that=20doubles=20as=20the=20integration-test=20roadmap.=20The?= =?UTF-8?q?=20user=20explicitly=20flagged=20that=20DL205/DL260=20strings?= =?UTF-8?q?=20don't=20follow=20Modbus=20convention;=20research=20turned=20?= =?UTF-8?q?up=20that=20and=20a=20lot=20more.=20Headline=20findings:=20Stri?= =?UTF-8?q?ng=20packing=20=E2=80=94=20TWO=20chars=20per=20V-memory=20regis?= =?UTF-8?q?ter=20but=20the=20FIRST=20char=20is=20in=20the=20LOW=20byte=20(?= =?UTF-8?q?opposite=20of=20the=20big-endian=20Modbus=20convention=20generi?= =?UTF-8?q?c=20drivers=20default=20to).=20'Hello'=20in=20V2000=20reads=20b?= =?UTF-8?q?ack=20as=20'eHll=20o\0'=20on=20a=20textbook=20decoder.=20Kepwar?= =?UTF-8?q?e's=20DirectLogic=20driver=20exposes=20a=20per-tag=20'String=20?= =?UTF-8?q?Byte=20Order=20=3D=20Low/High'=20toggle=20specifically=20for=20?= =?UTF-8?q?this;=20we'll=20need=20the=20same.=20Null-terminated,=20no=20le?= =?UTF-8?q?ngth=20prefix,=20no=20dedicated=20KSTR=20address=20space=20?= =?UTF-8?q?=E2=80=94=20strings=20live=20wherever=20ladder=20allocates=20th?= =?UTF-8?q?em=20in=20V-memory.=20V-memory=20addressing=20=E2=80=94=20Direc?= =?UTF-8?q?tLOGIC's=20native=20V-memory=20is=20OCTAL=20(V2000,=20V40400)?= =?UTF-8?q?=20but=20Modbus=20is=20decimal.=20The=20CPU=20translates:=20V20?= =?UTF-8?q?00=20octal=20=3D=20decimal=201024=20=3D=20Modbus=20PDU=200x0400?= =?UTF-8?q?.=20The=20widespread=20'V40400=20=3D=20register=200'=20shorthan?= =?UTF-8?q?d=20is=20wrong=20on=20modern=20firmware=20(that=20was=20DL05/DL?= =?UTF-8?q?06=20relative=20mode);=20on=20H2-ECOM100=20absolute=20mode=20(f?= =?UTF-8?q?actory=20default)=20V40400=20=3D=20PDU=200x2100.=20We'd=20surfa?= =?UTF-8?q?ce=20this=20with=20an=20address-format=20helper=20in=20the=20de?= =?UTF-8?q?vice=20profile=20so=20operators=20write=20V2000=20instead=20of?= =?UTF-8?q?=20computing=201024=20by=20hand.=20Word=20order=20CDAB=20for=20?= =?UTF-8?q?all=2032-bit=20values=20=E2=80=94=20DL205=20and=20DL260=20agree?= =?UTF-8?q?,=20ECOM=20modules=20don't=20re-swap.=20Already=20supported=20v?= =?UTF-8?q?ia=20ModbusByteOrder.WordSwap;=20just=20needs=20to=20be=20the?= =?UTF-8?q?=20default=20in=20the=20DL205=20profile.=20BCD-as-default=20num?= =?UTF-8?q?eric=20storage=20=E2=80=94=20bit=20one=20I=20didn't=20expect.?= =?UTF-8?q?=20DirectLOGIC=20stores=20'V2000=20=3D=201234'=20as=200x1234=20?= =?UTF-8?q?on=20the=20wire=20(BCD=20nibbles),=20not=20as=200x04D2=20(decim?= =?UTF-8?q?al=201234).=20IEEE=20754=20Float32=20only=20works=20when=20ladd?= =?UTF-8?q?er=20used=20the=20explicit=20R=20type=20(LDR/OUTR=20instruction?= =?UTF-8?q?s).=20We=20need=20a=20new=20decoder=20mode=20for=20BCD-encoded?= =?UTF-8?q?=20registers=20=E2=80=94=20current=20code=20assumes=20binary=20?= =?UTF-8?q?integers.=20FC=20quantity=20caps=20=E2=80=94=20FC03/04=20cap=20?= =?UTF-8?q?at=20128=20(above=20spec's=20125=20=E2=80=94=20Bonus=20territor?= =?UTF-8?q?y,=20current=20code=20already=20respects=20125),=20FC16=20caps?= =?UTF-8?q?=20at=20100=20(BELOW=20spec's=20123=20=E2=80=94=20important=20b?= =?UTF-8?q?ulk-write=20batching=20gotcha).=20Quantity=20overrun=20returns?= =?UTF-8?q?=20exception=2003=20IllegalDataValue.=20Coil/discrete=20mapping?= =?UTF-8?q?s=20=E2=80=94=20DL260:=20X0->discrete=20input=200,=20Y0->coil?= =?UTF-8?q?=202048,=20C0->coil=203072.=20SP=20specials=20at=20discrete=20i?= =?UTF-8?q?nput=201024-1535=20RO.=20These=20are=20CPU-wired=20constants=20?= =?UTF-8?q?and=20cannot=20be=20remapped;=20need=20to=20be=20hardcoded=20in?= =?UTF-8?q?=20the=20DL205/DL260=20device=20profile.=20Register=200=20?= =?UTF-8?q?=E2=80=94=20accepted=20on=20DL205/DL260=20with=20ECOM=20in=20ab?= =?UTF-8?q?solute=20mode,=20contrary=20to=20the=20widespread=20internet=20?= =?UTF-8?q?claim=20that=20'DirectLOGIC=20rejects=20register=200'.=20That?= =?UTF-8?q?=20rumour=20was=20an=20older=20DL05/DL06=20relative-mode=20arte?= =?UTF-8?q?fact.=20Our=20ModbusProbeOptions.ProbeAddress=20default=20of=20?= =?UTF-8?q?0=20is=20therefore=20safe=20for=20DL205/DL260.=20Exception=20co?= =?UTF-8?q?des=20=E2=80=94=20only=20the=20standard=2001-04.=20Write-to-pro?= =?UTF-8?q?tected-bit=20returns=2002=20on=20newer=20firmware,=2004=20on=20?= =?UTF-8?q?older=20(firmware-transition=20revision=20unconfirmed);=20drive?= =?UTF-8?q?r=20should=20map=20both=20to=20BadNotWritable.=20No=20proprieta?= =?UTF-8?q?ry=20exception=20codes.=20Behavioral=20oddities=20=E2=80=94=20H?= =?UTF-8?q?2-ECOM100=20accepts=20MAX=204=20simultaneous=20TCP=20connection?= =?UTF-8?q?s=20(5th=20refused=20at=20TCP=20accept).=20No=20TCP=20keepalive?= =?UTF-8?q?=20(intermediate=20NAT/firewall=20drops=20idle=20sockets=20afte?= =?UTF-8?q?r=202-5=20min=20=E2=80=94=20periodic=20probe=20required).=20No?= =?UTF-8?q?=20mid-stream=20resync=20on=20malformed=20MBAP=20=E2=80=94=20dr?= =?UTF-8?q?iver=20must=20reconnect=20+=20replay.=20TxId-drop-under-load=20?= =?UTF-8?q?forum=20rumour=20is=20unconfirmed;=20our=20single-flight=20+=20?= =?UTF-8?q?TxId-match=20guard=20handles=20it=20either=20way.=20Each=20H2?= =?UTF-8?q?=20section=20ends=20with=20the=20integration-test=20names=20we'?= =?UTF-8?q?d=20ship=20per=20the=20modbus-test-plan.md=20DL205=5F?= =?UTF-8?q?=20convention=20=E2=80=94=20twelve=20named=20test=20slots=20rea?= =?UTF-8?q?dy=20for=20PR=2042+=20to=20fill=20in=20one=20at=20a=20time.=20R?= =?UTF-8?q?eferences=20(8)=20cited=20inline,=20primarily=20D2-USER-M,=20HA?= =?UTF-8?q?-ECOM-M,=20and=20the=20Kepware=20DirectLogic=20Ethernet=20drive?= =?UTF-8?q?r=20manual=20which=20documents=20these=20vendor=20quirks=20expl?= =?UTF-8?q?icitly=20because=20they=20have=20to=20cope=20with=20them.=20mod?= =?UTF-8?q?bus-test-plan.md=20DL205=20section=20rewritten=20as=20a=20prior?= =?UTF-8?q?ity-ordered=20table=20with=20three=20columns=20(quirk=20/=20dri?= =?UTF-8?q?ver=20impact=20/=20test=20name),=20pointing=20the=20reader=20at?= =?UTF-8?q?=20dl205.md=20for=20the=20full=20reference.=20Operator-reported?= =?UTF-8?q?=20items=20separated=20into=20a=20tail=20subsection=20so=20futu?= =?UTF-8?q?re-me=20knows=20which=20behaviors=20are=20documented=20vs=20rep?= =?UTF-8?q?roduced-on-hardware.=20Pure=20documentation=20PR=20=E2=80=94=20?= =?UTF-8?q?no=20code=20changes.=20The=20actual=20driver=20work=20(string-b?= =?UTF-8?q?yte-order=20option,=20BCD=20decoder=20mode,=20V-memory=20addres?= =?UTF-8?q?s=20helper,=20FC16=20cap-per-device-family,=20multi-client=20TC?= =?UTF-8?q?P=20handling)=20lands=20one=20PR=20per=20quirk=20in=20PR=2042+?= =?UTF-8?q?=20as=20ModbusPal=20validation=20completes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/v2/dl205.md | 295 ++++++++++++++++++++++++++++++++++++ docs/v2/modbus-test-plan.md | 52 +++---- 2 files changed, 320 insertions(+), 27 deletions(-) create mode 100644 docs/v2/dl205.md diff --git a/docs/v2/dl205.md b/docs/v2/dl205.md new file mode 100644 index 0000000..b5ff16d --- /dev/null +++ b/docs/v2/dl205.md @@ -0,0 +1,295 @@ +# AutomationDirect DirectLOGIC DL205 / DL260 — Modbus quirks + +AutomationDirect's DirectLOGIC DL205 family (D2-250-1, D2-260, D2-262, D2-262M) and +its larger DL260 sibling speak Modbus TCP (via the H2-ECOM100 / H2-EBC100 Ethernet +coprocessors, and the DL260's built-in Ethernet port) and Modbus RTU (via the CPU +serial ports in "Modbus" mode). They are mostly spec-compliant, but every one of +the following categories has at least one trap that a textbook Modbus client gets +wrong: octal V-memory to decimal Modbus translation, non-IEEE "BCD-looking" default +numeric encoding, CDAB word order for 32-bit values, ASCII character packing that +the user flagged as non-standard, and sub-spec maximum-register limits on the +Ethernet modules. This document catalogues each quirk, cites primary sources, and +names the ModbusPal integration test we'd write for it (convention from +`docs/v2/modbus-test-plan.md`: `DL205_`). + +## Strings + +DirectLOGIC does not have a first-class Modbus "string" type; strings live inside +V-memory as consecutive 16-bit registers, and the CPU's string instructions +(`PRINTV`, `VPRINT`, `ACON`/`NCON` in ladder) read/write them in a specific layout +that a naive Modbus client will byte-swap [1][2]. + +- **Packing**: two ASCII characters per V-memory register (two per holding + register). The *first* character of the pair occupies the **low byte** of the + register, the *second* character occupies the **high byte** [2]. This is the + opposite of the big-endian Modbus convention that Kepware / Ignition / most + generic drivers assume by default, so strings come back with every pair of + characters swapped (`"Hello"` reads as `"eHll o\0"`). +- **Termination**: null-terminated (`0x00` in the character byte). There is no + length prefix. Writes must pad the final register's unused byte with `0x00`. +- **Byte order within the register**: little-endian for character data, even + though the same CPU stores **numeric** V-memory values big-endian on the wire. + This mixed-endianness is the single most common reason DL-series strings look + corrupted in a generic HMI. Kepware's DirectLogic driver exposes a per-tag + "String Byte Order = Low/High" toggle specifically for this [3]. +- **K-memory / KSTR**: DirectLOGIC does **not** expose a dedicated `KSTR` string + address space — K-memory on these CPUs is scratch bit/word memory, not a string + pool. Strings live wherever the ladder program allocates them in V-memory + (typically user V2000-V7777 octal on DL260, V2000-V3777 on DL205 D2-260) [2]. +- **Maximum length**: bounded only by the V-memory region assigned. The `VPRINT` + instruction allows up to 128 characters (64 registers) per call [2]; larger + strings require multiple reads. +- **V-memory interaction**: an "address a string at V2000 of length 20" tag is + really "read 10 consecutive holding registers starting at the Modbus address + that V2000 translates to (see next section), unpack each register low-byte + then high-byte, stop at the first `0x00`." + +Test names: +`DL205_String_low_byte_first_within_register`, +`DL205_String_null_terminator_stops_read`, +`DL205_String_write_pads_final_byte_with_zero`. + +## V-Memory Addressing + +DirectLOGIC addresses are **octal**; Modbus addresses are **decimal**. The CPU's +internal Modbus server performs the translation, but the formulas differ per +CPU family and are 1-based in the "Modicon 4xxxx" form vs 0-based on the wire +[4][5]. + +Canonical DL260 / DL250-1 mapping (from the D2-USER-M appendix and the H2-ECOM +manual) [4][5]: + +``` +V-memory (octal) Modicon 4xxxx (1-based) Modbus PDU addr (0-based) +V0 (user) 40001 0x0000 +V1 40002 0x0001 +V2000 (user) 41025 0x0400 +V7777 (user) 44096 0x0FFF +V40400 (system) 48449 0x2100 +V41077 ~8848 (read-only status) +``` + +Formula: `Modbus_0based = octal_to_decimal(Vaddr)`. So `V2000` octal = `1024` +decimal = Modbus PDU address `0x0400`. The "4xxxx" Modicon view just adds 1 and +prefixes the register bank digit. + +- **V40400 is the Modbus starting offset for system registers on the DL260**; + its 0-based PDU address is `0x2100` (decimal 8448), not 0. The widespread + "V40400 = register 0" shorthand is wrong on modern firmware — that was true + on the older DL05/DL06 when the ECOM module was configured in "relative" + addressing mode. On the H2-ECOM100 factory default ("absolute" mode), V40400 + maps to 0x2100 [5]. +- **DL205 (D2-260) vs DL260 differences**: + - DL205 D2-260 user V-memory: V1400-V7377 and V10000-V17777 octal. + - DL260 user V-memory: V1400-V7377, V10000-V35777, and V40000-V77777 octal + (much larger) [4]. + - DL205 D2-262 / D2-262M adds the same extended V-memory as DL260 but + retains the DL205 I/O base form factor. + - Neither DL205 sub-model changes the *formula* — only the valid range. +- **Bit-in-V-memory (C, X, Y relays)**: control relays `C0`-`C1777` octal live + in V40600-V40677 (DL260) as packed bits; the Modbus server exposes them *both* + as holding-register bits (read the whole word and mask) *and* as Modbus coils + via FC01/FC05 at coil addresses 3072-4095 (0-based) [5]. `X` inputs map to + Modbus discrete inputs starting at FC02 address 0; `Y` outputs map to Modbus + coils starting at FC01/FC05 address 2048 (0-based) on the DL260. +- **Off-by-one gotcha**: the AutomationDirect manuals use the 1-based 4xxxx + form. Kepware, libmodbus, pymodbus, and the .NET stack all take the 0-based + PDU form. When the manual says "V2000 = 41025" you send `0x0400`, not + `0x0401`. + +Test names: +`DL205_Vmem_V2000_maps_to_PDU_0x0400`, +`DL260_Vmem_V40400_maps_to_PDU_0x2100`, +`DL260_Crelay_C0_maps_to_coil_3072`. + +## Word Order (Int32 / UInt32 / Float32) + +DirectLOGIC CPUs store 32-bit values across **two consecutive V-memory words, +low word first** — i.e., `CDAB` when viewed as a Modbus register pair [1][3]. +Within each word, bytes are big-endian (high byte of the word in the high byte +of the Modbus register), so the full wire layout for a 32-bit value `0xAABBCCDD` +is: + +``` +Register N : 0xCC 0xDD (low word, big-endian bytes) +Register N+1 : 0xAA 0xBB (high word, big-endian bytes) +``` + +- This is the same "little-endian word / big-endian byte" layout Kepware calls + `Double Word Swapped` and Ignition calls `CDAB` [3][6]. +- **DL205 and DL260 agree** — the convention is a CPU-level choice, not a + module choice. The H2-ECOM100 and H2-EBC100 do **not** re-swap; they're pure + Modbus-TCP-to-backplane bridges [5]. The DL260 built-in Ethernet port + behaves identically. +- **Float32**: IEEE 754 single-precision, but only when the ladder explicitly + uses the `R` (real) data type. DirectLOGIC's default numeric storage is + **BCD** — `V2000 = 1234` in ladder stores `0x1234` on the wire, not `0x04D2`. + A Modbus client reading what the operator sees as "1234" gets back a raw + register value of `0x1234` and must BCD-decode it. Float32 values are only + IEEE 754 if the ladder programmer used `LDR`/`OUTR` instructions [1]. +- **Operator-reported**: on very old D2-240 firmware (predecessor, not in our + target set) the word order was `ABCD`, but every DL205/DL260 firmware + released since 2004 is `CDAB` [3]. _Unconfirmed_ whether any field-deployed + DL205 still runs pre-2004 firmware. + +Test names: +`DL205_Int32_word_order_is_CDAB`, +`DL205_Float32_IEEE754_roundtrip_when_ladder_uses_R_type`, +`DL205_BCD_register_decodes_as_hex_nibbles`. + +## Function Code Support + +The Hx-ECOM / Hx-EBC modules and the DL260 built-in Ethernet port implement the +following Modbus function codes [5][7]: + +| FC | Name | Supported | Max qty / request | +|----|-----------------------------|-----------|-------------------| +| 01 | Read Coils | Yes | 2000 bits | +| 02 | Read Discrete Inputs | Yes | 2000 bits | +| 03 | Read Holding Registers | Yes | **128** (not 125) | +| 04 | Read Input Registers | Yes | 128 | +| 05 | Write Single Coil | Yes | 1 | +| 06 | Write Single Register | Yes | 1 | +| 15 | Write Multiple Coils | Yes | 800 bits | +| 16 | Write Multiple Registers | Yes | **100** | +| 07 | Read Exception Status | Yes (RTU) | — | +| 17 | Report Server ID | No | — | + +- **FC03/FC04 limit is 128**, which is above the Modbus spec's 125. Requesting + 129+ returns exception code `03` (Illegal Data Value) [5]. +- **FC16 limit is 100**, below the spec's 123. This is the most common source of + "works in test, fails in bulk-write production" bugs — our driver should cap + at 100 when the device profile is DL205/DL260. +- **No custom function codes** are exposed on the Modbus port. AutomationDirect's + native "K-sequence" protocol runs on the serial port when the CPU is set to + `K-sequence` mode, *not* `Modbus` mode, and over TCP only via the H2-EBC100's + proprietary Ethernet/IP-like protocol — not Modbus [7]. + +Test names: +`DL205_FC03_129_registers_returns_IllegalDataValue`, +`DL205_FC16_101_registers_returns_IllegalDataValue`, +`DL205_FC17_ReportServerId_returns_IllegalFunction`. + +## Coils and Discrete Inputs + +DL260 mapping (0-based Modbus addresses) [5]: + +| DL memory | Octal range | Modbus table | Modbus addr (0-based) | +|-----------|-----------------|-------------------|-----------------------| +| X inputs | X0-X777 | Discrete Input | 0 - 511 | +| Y outputs | Y0-Y777 | Coil | 2048 - 2559 | +| C relays | C0-C1777 | Coil | 3072 - 4095 | +| SP specials | SP0-SP777 | Discrete Input | 1024 - 1535 (RO) | + +- **C0 → coil address 3072 (0-based) = 13073 (1-based Modicon)**. Y0 → coil + 2048 = 12049. These offsets are wired into the CPU and cannot be remapped. +- **Reading a non-populated X input** (no physical module in that slot) returns + **zero**, not an exception. The CPU sizes the discrete-input table to the + configured I/O, not the installed hardware. Confirmed in the DL260 user + manual's I/O configuration chapter [4]. +- **Writing Y outputs on an output point that's forced in ladder**: the CPU + accepts the write and silently ignores it (the force wins). No exception is + returned. _Operator-reported_, matches Kepware driver release notes [3]. + +Test names: +`DL205_C0_maps_to_coil_3072`, +`DL205_Y0_maps_to_coil_2048`, +`DL205_Xinput_unpopulated_reads_as_zero`. + +## Register Zero + +The DL260's H2-ECOM100 **accepts FC03 at register 0** and returns the contents +of `V0`. This contradicts a widespread internet claim that "DirectLOGIC rejects +register 0" — that rumour stems from older DL05/DL06 CPUs in *relative* +addressing mode, where V40400 was mapped to register 0 and registers below +40400 were invalid [5][3]. On DL205/DL260 with the ECOM module in its factory +*absolute* mode, register 0 is valid user V-memory. + +- Our driver's `ModbusProbeOptions.ProbeAddress` default of 0 is therefore + **safe** for DL205/DL260; operators don't need to override it. +- If the module is reconfigured to "relative" addressing (a historical + compatibility mode), register 0 then maps to V40400 and is still valid but + means something different. The probe will still succeed. + +Test name: `DL205_FC03_register_0_returns_V0_contents`. + +## Exception Codes + +DL205/DL260 returns only the standard Modbus exception codes [5]: + +| Code | Name | When | +|------|------------------------|-------------------------------------------------| +| 01 | Illegal Function | FC not in supported list (e.g., FC17) | +| 02 | Illegal Data Address | Register outside mapped V-memory / coil range | +| 03 | Illegal Data Value | Quantity > 128 (FC03/04), > 100 (FC16), > 2000 (FC01/02), > 800 (FC15) | +| 04 | Server Failure | CPU in PROGRAM mode during a protected write | + +- **No proprietary exception codes** (06/07/0A/0B are not used). +- **Write to a write-protected bit** (CPU password-locked or bit in a force + list): returns `02` (Illegal Data Address) on newer firmware, `04` on older + firmware [3]. _Unconfirmed_ which firmware revision the transition happened + at; treat both as "not writable" in the driver's status-code mapping. +- **Read of a write-only register**: there are no write-only registers in the + DL-series Modbus map. Every writable register is also readable. + +Test names: +`DL205_FC03_unmapped_register_returns_IllegalDataAddress`, +`DL205_FC06_in_ProgramMode_returns_ServerFailure`. + +## Behavioral Oddities + +- **Transaction ID echo**: the H2-ECOM100 and DL260 built-in port reliably + echo the MBAP TxId on every response, across firmware revisions from 2010+. + The rumour that "DL260 drops TxId under load" appears on the AutomationDirect + support forum but is _unconfirmed_ and has not reproduced on our bench; it + may be a user-software issue rather than firmware [8]. Our driver's + single-flight + TxId-match guard handles it either way. +- **Concurrency**: the ECOM serializes requests internally. Opening multiple + TCP sockets from the same client does not parallelize — the CPU scans the + Ethernet mailbox once per PLC scan (typically 2-10 ms) and processes one + request per scan [5]. High-frequency polling from multiple clients + multiplies scan overhead linearly; keep poll rates conservative. +- **Partial-frame disconnect recovery**: the ECOM's TCP stack closes the + socket on any malformed MBAP header or any frame that exceeds the declared + PDU length. It does not resynchronize mid-stream. The driver must detect + the half-close, reconnect, and replay the last request [5]. +- **Keepalive**: the ECOM does **not** send TCP keepalives. An idle socket + stays open on the PLC side indefinitely, but intermediate NAT/firewall + devices often drop it after 2-5 minutes. Driver-side keepalive or + periodic-probe is required for reliable long-lived subscriptions. +- **Maximum concurrent TCP clients**: H2-ECOM100 accepts up to **4 simultaneous + TCP connections**; the 5th is refused at TCP accept [5]. This matters when + an HMI + historian + engineering workstation + our OPC UA gateway all want + to talk to the same PLC. + +Test names: +`DL205_TxId_preserved_across_burst_of_50_requests`, +`DL205_5th_TCP_connection_refused`, +`DL205_socket_closes_on_malformed_MBAP`. + +## References + +1. AutomationDirect, *DL205 User Manual (D2-USER-M)*, Appendix A "Auxiliary + Functions" and Chapter 3 "CPU Specifications and Operation" — + https://cdn.automationdirect.com/static/manuals/d2userm/d2userm.html +2. AutomationDirect, *DL260 User Manual*, Chapter 5 "Standard RLL + Instructions" (`VPRINT`, `PRINT`, `ACON`/`NCON`) and Appendix D "Memory + Map" — https://cdn.automationdirect.com/static/manuals/d2userm/d2userm.html +3. Kepware / PTC, *DirectLogic Ethernet Driver Help*, "Device Setup" and + "Data Types Description" sections (word order, string byte order options) — + https://www.kepware.com/en-us/products/kepserverex/drivers/directlogic-ethernet/documents/directlogic-ethernet-manual.pdf +4. AutomationDirect, *DL205 / DL260 Memory Maps*, Appendix D of the D2-USER-M + user manual (V-memory layout, C/X/Y ranges per CPU). +5. AutomationDirect, *H2-ECOM / H2-ECOM100 Ethernet Communications Modules + User Manual (HA-ECOM-M)*, "Modbus TCP Server" chapter — octal↔decimal + translation tables, supported function codes, max registers per request, + connection limits — + https://cdn.automationdirect.com/static/manuals/hxecomm/hxecomm.html +6. Inductive Automation, *Ignition Modbus Driver — Address Mapping*, word + order options (ABCD/CDAB/BADC/DCBA) — + https://docs.inductiveautomation.com/docs/8.1/ignition-modules/opc-ua/drivers/modbus-v2 +7. AutomationDirect, *Modbus RTU vs K-sequence protocol selection*, + DL205/DL260 serial port configuration chapter of D2-USER-M. +8. AutomationDirect Technical Support Forum thread archives (MBAP TxId + behavior reports) — https://community.automationdirect.com/ (search: + "ECOM100 transaction id"). _Unconfirmed_ operator reports only. diff --git a/docs/v2/modbus-test-plan.md b/docs/v2/modbus-test-plan.md index ada85f4..8009b43 100644 --- a/docs/v2/modbus-test-plan.md +++ b/docs/v2/modbus-test-plan.md @@ -32,36 +32,34 @@ test project): ## Per-device quirk catalog -### AutomationDirect DL205 +### AutomationDirect DL205 / DL260 -First known target device. Quirks to document and cover with named tests (to be -filled in when user validates each behavior in ModbusPal with a DL205 profile): +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. -- **Word order for 32-bit values**: _pending_ — confirm whether DL205 uses ABCD - (Modbus TCP standard) or CDAB (Siemens-style word-swap) for Int32/UInt32/Float32. - Test name: `DL205_Float32_word_order_is_CDAB` (or `ABCD`, whichever proves out). -- **Register-zero access**: _pending_ — some DL205 configurations reject FC03 at - register 0 with exception code 02 (illegal data address). If confirmed, the - integration test suite verifies `ModbusProbeOptions.ProbeAddress` default of 0 - triggers the rejection and operators must override; test name: - `DL205_FC03_at_register_0_returns_IllegalDataAddress`. -- **Coil addressing base**: _pending_ — DL205 documentation sometimes uses 1-based - coil addresses; verify the driver's zero-based addressing matches the physical - PLC without an off-by-one adjustment. -- **Maximum registers per FC03**: _pending_ — Modbus spec caps at 125; some DL205 - models enforce a lower limit (e.g., 64). Test name: - `DL205_FC03_beyond_max_registers_returns_IllegalDataValue`. -- **Response framing under sustained load**: _pending_ — the driver's - single-flight semaphore assumes the server pairs requests/responses by - transaction id; at least one DL205 firmware revision is reported to drop the - TxId under load. If reproduced in ModbusPal we add a retry + log-and-continue - path to `ModbusTcpTransport`. -- **Exception code on coil write to a protected bit**: _pending_ — some DL205 - setups protect internal coils; the driver should surface the PLC's exception - PDU as `BadNotWritable` rather than `BadInternalError`. +Confirmed quirks (priority order — top items are highest-impact for our driver +and ship first as PR 41+): -_User action item_: as each quirk is validated in ModbusPal, replace the _pending_ -marker with the confirmed behavior and file a named test in the integration suite. +| 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 -- 2.49.1