Compare commits

...

4 Commits

Author SHA1 Message Date
Joseph Doherty
02a0e8efd1 Phase 3 PR 42 — ModbusPal simulator profiles for Standard Modbus + DL205/DL260 quirks. Two hand-authored .xmpp profiles in tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/ that integration tests load via the GUI to drive the suite without a real PLC. Both well-formed XML (verified via PowerShell [xml] cast); both copied to test-output as PreserveNewest content per the existing csproj rule.
Standard.xmpp — generic Modbus TCP server on port 502, slave id 1. HR[0..31] seeded with address-as-value (HR[5]=5 — easy mental map for diagnostics), HR[100] auto-incrementing via a 1Hz LinearGenerator binding (drives subscribe-and-receive integration tests so they have a register that actually changes without a write), HR[200..209] scratch range for write-roundtrip tests, coils 0..31 alternating on/off, coils 100..109 scratch. The Tick automation runs 0..65535 over 60s looping; bound to HR[100] via Binding_SINT16 — slow enough that a 250ms-poll integration test sees discrete jumps, fast enough that a 5s subscribe test sees several change notifications.
DL205.xmpp — AutomationDirect DirectLOGIC DL205/DL260 quirk simulator on port 502, slave id 1, modeling the behaviors documented in docs/v2/dl205.md as concrete register values so DL205 integration tests can assert each quirk WITHOUT a live PLC. Per-quirk encoding: V0 marker at HR[0]=0xCAFE proves register 0 is valid (rejects-register-0 rumour disproved); V2000 marker at HR[1024]=0x2000 proves V-memory octal-to-decimal mapping; V40400 marker at HR[8448]=0x4040 proves V40400→PDU 0x2100 (NOT register 0, contrary to the widespread shorthand); 'Hello' string at HR[1040..1042] packed first-char-low-byte (HR[1040]=0x6548 = 'H' lo + 'e' hi, HR[1041]=0x6C6C, HR[1042]=0x006F) — the headline string-byte-order quirk the user flagged; Float32 1.5f at HR[1056..1057] in CDAB word order (low word first: 0, then 0x3FC0); BCD register at HR[1072]=0x1234 representing decimal 1234 in BCD nibbles (NOT binary 0x04D2); 128-register block at HR[1280..1407] for FC03-128-cap testing; Y0 marker at coil 2048, C0 marker at coil 3072, scratch C-coils at 4000..4007 for write tests.
Critical limitation flagged inline + in README: ModbusPal 1.6b CANNOT represent the DL205 quirks semantically — it has no string binding, no BCD binding, no arbitrary-byte-layout binding (only SINT16/SINT32/FLOAT32 with word-order). So every DL205 quirk is encoded as a pre-computed raw 16-bit integer with the math worked out in inline comments above each register. Becomes unreadable past ~50 quirky registers; the README's 'alternatives' section recommends switching to pymodbus when that threshold approaches (pymodbus's ModbusSimulatorServer has first-class headless + scriptable callbacks for byte-level layouts).
Other ModbusPal 1.6b limitations called out in README: only holding_registers + coils sections in the official build (no input_registers / discrete_inputs — DL260 X-input markers can't be encoded faithfully here, FC02/FC04 tests wait for a fork or pymodbus); abandoned project (last release 1.6b, active forks at SCADA-LTS/ModbusPal, ControlThings-io/modbuspal, mrhenrike/ModbusPalEnhanced); no headless mode in the official JAR (-loadFile / -hide flags only in source-built forks); CVE-2018-10832 XXE on .xmpp import (don't import untrusted profiles — the in-repo ones are author-controlled).
README.md updated with: per-profile description tables, getting-started (download jar + java -jar + GUI File>Load>Run), MODBUS_SIM_ENDPOINT env-var override doc, two reference tables documenting which HR / coil address encodes which DL205 quirk + which test name asserts it (the same DL205_<behavior> naming convention from docs/v2/modbus-test-plan.md), 4-row alternatives comparison (pymodbus / diagslave / ModbusMechanic / ModRSsim2) for when ModbusPal can no longer carry the load, and a quick-reference XML format table at the bottom for future-me hand-authoring more profiles.
Pure documentation + test-asset PR — no code changes. The integration tests that consume these profiles (the actual DL205_<behavior> facts) land one at a time in PR 43+ as user validates each quirk via ModbusPal on the bench.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 20:05:20 -04:00
7009483d16 Merge pull request 'Phase 3 PR 41 — Document AutomationDirect DL205 / DL260 Modbus quirks' (#40) from phase-3-pr41-dl205-quirks-doc into v2 2026-04-18 19:52:20 -04:00
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
af35fac0ef Merge pull request 'Phase 3 PR 40 — LiveStack write + subscribe tests against TestMachine_001' (#39) from phase-3-pr40-livestack-write-subscribe into v2 2026-04-18 19:41:55 -04:00
5 changed files with 772 additions and 46 deletions

295
docs/v2/dl205.md Normal file
View File

@@ -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_<behavior>`).
## 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.

View File

@@ -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

View File

@@ -0,0 +1,192 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE modbuspal_project SYSTEM "modbuspal.dtd">
<!--
DL205.xmpp — AutomationDirect DirectLOGIC DL205 / DL260 quirk simulator.
Slave id 1 on TCP 502. Models the real-PLC behaviors documented in
docs/v2/dl205.md as concrete register values, so integration tests can
assert each quirk WITHOUT a live PLC. The driver is correct when reads
against this profile produce the same logical values that an
AutomationDirect-aware client would see.
BIG WARNING: every "interesting" register here is encoded as a raw 16-bit
integer. ModbusPal 1.6b serves whatever you put in `value="..."` straight
onto the wire as a 16-bit big-endian register; it has no String / BCD /
Float / WordSwap binding (only SINT16 / SINT32 / FLOAT32 + word-order, none
of which capture the byte-level packing the DL series uses). So strings,
BCD, and CDAB floats live here as opaque integers with the math worked out
in the comment above each register. That math is reproduced in
docs/v2/dl205.md so the two stay in sync.
If this profile grows beyond ~50 quirky registers, switch to pymodbus
(see ModbusPal/README.md §"alternatives") — the magic-number table will
become unreadable. For the planned 12 DL205_<behavior> tests, raw values
are fine.
Loaded via the ModbusPal GUI: File > Load > pick this file > Run.
Run only ONE simulator at a time (they share TCP 502); to switch between
Standard and DL205, stop one before loading the other.
-->
<modbuspal_project>
<idgen value="200"/>
<links selected="TCP/IP">
<tcpip port="502"/>
<serial com="COM 1" baudrate="9600" parity="even" stops="1">
<flowcontrol xonxoff="false" rtscts="false"/>
</serial>
</links>
<slave id="1" enabled="true" name="DL205Sim" implementation="modbus">
<holding_registers>
<!-- ============================================================
V-MEMORY ADDRESSING MARKERS
============================================================
DirectLOGIC V-memory is octal natively; the CPU translates
V<oct> -> Modbus PDU <decimal>. Tests verify our address
helper produces the right PDU offset for known V-addresses.
Marker values are arbitrary but distinctive so a test that
reads the wrong PDU sees Goodread+wrong-value, not zero.
-->
<!-- V0 (octal) = PDU 0x0000. Decisively proves register 0 is valid
on DL205/DL260 with H2-ECOM100 in absolute mode (the default).
The "rejects register 0" rumour was a DL05/DL06 relative-mode
artefact — see dl205.md §Register Zero. -->
<!-- 0xCAFE = 51966 (signed 16-bit: -13570) -->
<register address="0" value="-13570" name="V0_marker_0xCAFE"/>
<!-- V2000 octal = decimal 1024 = PDU 0x0400. -->
<!-- 0x2000 = 8192 -->
<register address="1024" value="8192" name="V2000_marker_0x2000"/>
<!-- V40400 octal = decimal 8448 = PDU 0x2100. Proves the
"V40400 = register 0" myth wrong on absolute-mode firmware. -->
<!-- 0x4040 = 16448 -->
<register address="8448" value="16448" name="V40400_marker_0x4040"/>
<!-- ============================================================
STRING PACKING (the user's headline quirk)
============================================================
Two ASCII chars per register, FIRST CHAR in the LOW byte.
"Hello" at HR[0x410..0x412]:
HR[0x410] = 'H' (0x48) lo, 'e' (0x65) hi -> 0x6548 = 25928
HR[0x411] = 'l' (0x6C) lo, 'l' (0x6C) hi -> 0x6C6C = 27756
HR[0x412] = 'o' (0x6F) lo, '\0' (0x00) hi -> 0x006F = 111
A textbook (high-byte-first) decoder reads "eH" "ll" "\0o"
and prints "eHll \0o" — that's exactly the failure mode the
DL205 string test asserts NOT happens once we add the
ModbusStringByteOrder=LowFirst option to the driver.
Test: DL205_String_low_byte_first_within_register. -->
<register address="1040" value="25928" name="HelloStr_lo='H'_hi='e'"/>
<register address="1041" value="27756" name="HelloStr_lo='l'_hi='l'"/>
<register address="1042" value="111" name="HelloStr_lo='o'_hi=null"/>
<!-- ============================================================
32-BIT FLOAT IN CDAB WORD ORDER
============================================================
IEEE 754 float 1.5f = 0x3FC00000.
Standard ABCD: HR[N]=0x3FC0, HR[N+1]=0x0000
DL205 CDAB: HR[N]=0x0000, HR[N+1]=0x3FC0 (LOW word first)
Test: DL205_Float32_word_order_is_CDAB.
Driver must use ModbusByteOrder=WordSwap to decode this as 1.5. -->
<register address="1056" value="0" name="FloatCDAB_lo_word"/>
<!-- 0x3FC0 = 16320 -->
<register address="1057" value="16320" name="FloatCDAB_hi_word"/>
<!-- ============================================================
BCD-ENCODED REGISTER (DirectLOGIC default numeric storage)
============================================================
Ladder value 1234 stored as 0x1234 = 4660 (BCD nibbles, NOT
binary 1234 = 0x04D2). A driver in binary-int mode reads 4660
and reports the wrong value; in BCD mode it nibble-decodes
0x1234 -> 1234. Test: DL205_BCD_register_decodes_as_decimal. -->
<!-- 0x1234 = 4660 -->
<register address="1072" value="4660" name="BCD_1234_as_0x1234"/>
<!-- ============================================================
LOAD-LIMIT BOUNDARY MARKERS
============================================================
The DL series caps FC03 at 128 registers (above spec's 125)
and FC16 at 100 (BELOW spec's 123). The cap-tests don't need
specific values — they assert exception 03 IllegalDataValue
on an over-sized request. We pre-seed a contiguous block at
0x500..0x57F (128 regs) so a 128-register read returns Good
and a 129-register read can be tried for the failure case.
Per-register values: address - 0x500 (so HR[0x500]=0,
HR[0x501]=1, ..., HR[0x57F]=127). Easy mental verification.
Test: DL205_FC03_128_registers_returns_Good. -->
<!-- (Generated programmatically below for brevity — first / last + spot-check) -->
<register address="1280" value="0" name="FC03Block_first"/>
<register address="1281" value="1"/>
<register address="1282" value="2"/>
<register address="1343" value="63" name="FC03Block_mid"/>
<register address="1407" value="127" name="FC03Block_last"/>
<!-- Note: ModbusPal serves unlisted addresses as 0 by default for
reads that fall within the configured slave's address space.
The block-test relies on that behavior; the hand-listed
entries above are sanity markers. If the driver later wants
byte-perfect comparison across the whole 128-register range,
expand this section to one element per address (or switch to
pymodbus). -->
</holding_registers>
<coils>
<!-- ============================================================
COIL / DISCRETE-INPUT MAPPING MARKERS (DL260 layout)
============================================================
Per dl205.md, on the DL260:
X inputs -> discrete inputs 0..511 (FC02)
Y outputs -> coils 2048..2559 (FC01/05)
C relays -> coils 3072..4095 (FC01/05)
ModbusPal 1.6b does NOT have a discrete-inputs section in
the official build, so the X-input markers can't be
encoded faithfully (the driver test for FC02 against this
profile will need a fork or pymodbus). The Y and C coil
markers ARE encodable here.
-->
<!-- Y0 marker — coil 2048 ON proves "Y0 maps to coil 2048" mapping.
Test: DL205_Y0_maps_to_coil_2048. -->
<coil address="2048" value="1" name="Y0_marker"/>
<coil address="2049" value="0"/>
<coil address="2050" value="1"/>
<!-- C0 marker — coil 3072 ON proves "C0 maps to coil 3072" mapping.
Test: DL205_C0_maps_to_coil_3072. -->
<coil address="3072" value="1" name="C0_marker"/>
<coil address="3073" value="0"/>
<coil address="3074" value="1"/>
<!-- Scratch coils 4000..4007 for write-roundtrip tests against
the C-relay range. C ranges are writable on the real DL260. -->
<coil address="4000" value="0" name="Cscratch_0"/>
<coil address="4001" value="0"/>
<coil address="4002" value="0"/>
<coil address="4003" value="0"/>
<coil address="4004" value="0"/>
<coil address="4005" value="0"/>
<coil address="4006" value="0"/>
<coil address="4007" value="0"/>
</coils>
<tuning>
<!-- Zero delay / zero error rate. The DL205 H2-ECOM has a typical
2-10ms scan-cycle delay; if a test wants to simulate that,
tune via the ModbusPal GUI (Tuning > Reply delay). -->
<reply_delay min="0" max="0"/>
<error_rates no_reply="0.0"/>
</tuning>
</slave>
</modbuspal_project>

View File

@@ -1,30 +1,105 @@
# ModbusPal simulator profiles
Drop device-specific `.xmpp` profiles here. The integration tests connect to the
endpoint in `MODBUS_SIM_ENDPOINT` (default `localhost:502`) and expect the
simulator to already be running — tests do not launch ModbusPal themselves,
because its Java GUI + JRE requirement is heavier than the harness is worth.
Two hand-authored `.xmpp` profiles you load into ModbusPal to drive the
integration-test suite without a real PLC:
| File | What it simulates | Test category |
|---|---|---|
| [`Standard.xmpp`](Standard.xmpp) | Generic Modbus TCP server — HR[0..31] = address-as-value, alternating coils, one auto-incrementing register at HR[100] for subscribe tests, scratch ranges for write-roundtrip tests. | `Trait=Standard` |
| [`DL205.xmpp`](DL205.xmpp) | AutomationDirect DirectLOGIC DL205 / DL260 quirks per [`docs/v2/dl205.md`](../../../docs/v2/dl205.md): low-byte-first string packing, CDAB Float32, BCD numerics, V-memory address markers, Y/C coil mappings. | `Trait=DL205` |
Both listen on TCP **port 502** (the standard Modbus port — change in the
ModbusPal GUI if a port conflict). Run **only one at a time** since they
share the port.
## Getting started
1. Download ModbusPal from SourceForge (`modbuspal.jar`).
1. Download ModbusPal 1.6b from
[SourceForge](https://sourceforge.net/projects/modbuspal/) — `modbuspal.jar`.
Requires Java 8+ (Java 17/21 work but emit Swing deprecation warnings).
2. `java -jar modbuspal.jar` to launch the GUI.
3. Load a profile from this directory (or configure one manually) and start the
simulator on TCP port 502.
4. `dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` tests
auto-skip with a clear `SkipReason` if the TCP probe at the configured
endpoint fails within 2 seconds.
3. **File > Load** → pick `Standard.xmpp` (or `DL205.xmpp`).
4. Click the **Run** button (top-right of the toolbar) to start serving on TCP 502.
5. `dotnet test tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests`
tests auto-skip with a clear `SkipReason` if the TCP probe at the
configured endpoint fails within 2 seconds (`ModbusSimulatorFixture`).
## Profile files
## Switching between Standard and DL205
- `DL205.xmpp`_to be added_ — register map reflecting the AutomationDirect
DL205 quirks tracked in `docs/v2/modbus-test-plan.md`. The scaffolded smoke
test in `DL205/DL205SmokeTests.cs` needs holding register 100 writable and
present; a minimal ModbusPal profile with a single holding-register bank at
address 100 is sufficient.
Stop the running simulator (toolbar's **Stop** button), **File > Load**
the other profile, **Run**.
## Environment variables
- `MODBUS_SIM_ENDPOINT` — override the simulator endpoint. Accepts `host:port`;
defaults to `localhost:502`. Useful when pointing the suite at a real PLC on
the bench.
- `MODBUS_SIM_ENDPOINT` — override the simulator endpoint
(`host:port`). Defaults to `localhost:502`. Useful when pointing the suite
at a real PLC on the bench, or running ModbusPal on a non-default port.
## What's encoded in each profile
### Standard
- HR[0..31]: each register's value equals its address.
- HR[100]: bound to a `LinearGenerator` (0..65535 over 60s, looping) — drives
subscribe-and-receive tests.
- HR[200..209]: scratch range for write-roundtrip tests.
- Coils[0..31]: alternating on/off (even=on).
- Coils[100..109]: scratch range.
### DL205 (per `docs/v2/dl205.md`)
| HR address | Quirk demonstrated | Raw value | Decoded value |
|---|---|---|---|
| `0` | Register zero is valid (rejects-register-0 rumour disproved) | `-13570` (0xCAFE) | marker |
| `1024` (= V2000 octal) | V-memory octal-to-decimal mapping | `8192` (0x2000) | marker |
| `8448` (= V40400 octal) | V40400 → PDU 0x2100 (NOT register 0) | `16448` (0x4040) | marker |
| `1040..1042` | String "Hello" packed first-char-low-byte | `25928, 27756, 111` | `"Hello"` |
| `1056..1057` | Float32 1.5f in CDAB word order | `0, 16320` | `1.5f` |
| `1072` | Decimal 1234 in BCD encoding | `4660` (0x1234) | `1234` |
| `1280..1407` | 128-register block (FC03 cap = 128 above spec's 125) | address 1280 | for FC03 cap test |
| Coil address | Quirk demonstrated |
|---|---|
| `2048` | Y0 maps to coil 2048 (DL260 layout) |
| `3072` | C0 maps to coil 3072 (DL260 layout) |
| `4000..4007` | Scratch C-relay range for write-roundtrip tests |
## Limitations of ModbusPal 1.6b
- **Only `holding_registers` + `coils`** sections in the official build —
no `input_registers` (FC04) and no `discrete_inputs` (FC02). DL205's
X-input markers can't be encoded faithfully here. Tests for FC02 / FC04
wait for a fork (e.g. `SCADA-LTS/ModbusPal`) or a pymodbus rewrite.
- **No semantic bindings** for strings / BCD / arbitrary byte layouts. The
DL205 profile encodes everything as pre-computed raw 16-bit integers
with the math worked out in inline comments. Anything fancier becomes
unreadable above ~50 quirky registers — switch to pymodbus when that
threshold approaches.
- **Project is abandoned** since 1.6b on the official SourceForge listing.
Active forks: `SCADA-LTS/ModbusPal`, `ControlThings-io/modbuspal`,
`mrhenrike/ModbusPalEnhanced`.
- **No headless mode** in the official 1.6b JAR (`-loadFile` / `-hide`
flags exist only in source-built forks). For CI use, plan to switch to
pymodbus's `ModbusSimulatorServer` (JSON config, scriptable callbacks,
first-class headless).
- **CVE-2018-10832** XXE in `.xmpp` import. Don't import `.xmpp` files from
untrusted sources. Profiles in this repo are author-controlled; safe.
## Alternatives if ModbusPal stops working
| Tool | Pros | Cons |
|---|---|---|
| **pymodbus `ModbusSimulatorServer`** | Headless-first, JSON config, per-register seeding, custom callbacks for byte-level layouts. Best CI fit. | Python dependency. |
| **diagslave** | Simple, headless, fast. | Flat register banks; no per-address seeding from config; no scripting. |
| **ModbusMechanic** | Headless config-file mode. | Lightly documented. |
| **ModRSsim2** | Windows GUI, CSV import, scripting. | GUI-centric. |
## File format reference
ModbusPal `.xmpp` is XML with a DTD reference (`modbuspal.dtd`). Root element
`<modbuspal_project>` with three children:
- `<idgen value="N"/>` — internal id counter (start at 100+)
- `<links selected="TCP/IP">``<tcpip port="502"/>` for TCP listen, plus a `<serial>` placeholder
- One or more `<slave id="..." enabled="true" name="..." implementation="modbus">` containing `<holding_registers>` (`<register address="N" value="V"/>`), `<coils>` (`<coil address="N" value="0|1"/>`), `<tuning>`
Per-register `<binding automation="..." class="Binding_SINT16|SINT32|FLOAT32" order="0|1"/>` ties a register to a `LinearGenerator` / `RandomGenerator` / `SineGenerator` automation declared at the project level. `order="0"` = LSW, `order="1"` = MSW for 32-bit types. There is **no string binding** and **no byte-swap-within-word** binding.

View File

@@ -0,0 +1,166 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE modbuspal_project SYSTEM "modbuspal.dtd">
<!--
Standard.xmpp — generic Modbus TCP server.
Slave id 1 on TCP port 502. Holding registers 0..31 seeded with their own
address as value (so HR[0]=0, HR[5]=5, easy mental map for diagnostics).
Coils 0..31 alternate true/false. One auto-incrementing register at HR[100]
bound to the "Tick" automation (1 Hz, wraps 0..65535) so subscribe-and-receive
integration tests have something that actually changes without a write.
Loaded via the ModbusPal GUI: File > Load > pick this file > Run.
The integration test fixture (MODBUS_SIM_ENDPOINT, default localhost:502)
connects on TCP. Tests filter by Trait=Standard.
Limitations of ModbusPal 1.6b that shape this profile:
- Only holding_registers + coils sections (no input_registers, no
discrete_inputs in the official 1.6b build). Tests for FC04 / FC02
wait until we switch to a fork or pymodbus.
- Per-register elements only — sparse maps fine, range form not
supported in the serialized format (the GUI lets you Add range,
but the .xmpp file expands them).
- Listens on all interfaces (no bind-address attribute).
-->
<modbuspal_project>
<!-- Monotonic id generator ModbusPal uses to internally name automations / slaves. -->
<idgen value="100"/>
<!-- TCP listen on 502 (standard Modbus port). Override via ModbusPal GUI if conflicting. -->
<links selected="TCP/IP">
<tcpip port="502"/>
<serial com="COM 1" baudrate="9600" parity="even" stops="1">
<flowcontrol xonxoff="false" rtscts="false"/>
</serial>
</links>
<!--
Tick automation: 0..65535 over 60 seconds, looping. Bound to HR[100]
below so each second the register climbs by ~1092. Slow enough that
a 250ms-poll integration test sees discrete jumps; fast enough that
a 5s subscribe test sees several change notifications.
-->
<automation name="Tick" step="1.0" loop="true" init="0.0">
<generator class="LinearGenerator" duration="60.0">
<start value="0.0" relative="false"/>
<end value="65535.0" relative="false"/>
</generator>
</automation>
<slave id="1" enabled="true" name="StandardSim" implementation="modbus">
<holding_registers>
<!-- HR[0..31] = address-as-value. Easy mental map for diagnostics + read tests. -->
<register address="0" value="0"/>
<register address="1" value="1"/>
<register address="2" value="2"/>
<register address="3" value="3"/>
<register address="4" value="4"/>
<register address="5" value="5"/>
<register address="6" value="6"/>
<register address="7" value="7"/>
<register address="8" value="8"/>
<register address="9" value="9"/>
<register address="10" value="10"/>
<register address="11" value="11"/>
<register address="12" value="12"/>
<register address="13" value="13"/>
<register address="14" value="14"/>
<register address="15" value="15"/>
<register address="16" value="16"/>
<register address="17" value="17"/>
<register address="18" value="18"/>
<register address="19" value="19"/>
<register address="20" value="20"/>
<register address="21" value="21"/>
<register address="22" value="22"/>
<register address="23" value="23"/>
<register address="24" value="24"/>
<register address="25" value="25"/>
<register address="26" value="26"/>
<register address="27" value="27"/>
<register address="28" value="28"/>
<register address="29" value="29"/>
<register address="30" value="30"/>
<register address="31" value="31"/>
<!-- HR[100] auto-increments via the Tick automation. Subscribe tests
read this and expect to see at least 2 change notifications in 5s. -->
<register address="100" value="0" name="AutoIncrement">
<binding automation="Tick" class="Binding_SINT16" order="0"/>
</register>
<!-- HR[200..209] — scratch range left at 0 for write-roundtrip tests
to mutate freely without touching the address-as-value set above. -->
<register address="200" value="0" name="Scratch0"/>
<register address="201" value="0" name="Scratch1"/>
<register address="202" value="0" name="Scratch2"/>
<register address="203" value="0" name="Scratch3"/>
<register address="204" value="0" name="Scratch4"/>
<register address="205" value="0" name="Scratch5"/>
<register address="206" value="0" name="Scratch6"/>
<register address="207" value="0" name="Scratch7"/>
<register address="208" value="0" name="Scratch8"/>
<register address="209" value="0" name="Scratch9"/>
</holding_registers>
<coils>
<!-- Coils 0..31 alternating. Even = on, odd = off. -->
<coil address="0" value="1"/>
<coil address="1" value="0"/>
<coil address="2" value="1"/>
<coil address="3" value="0"/>
<coil address="4" value="1"/>
<coil address="5" value="0"/>
<coil address="6" value="1"/>
<coil address="7" value="0"/>
<coil address="8" value="1"/>
<coil address="9" value="0"/>
<coil address="10" value="1"/>
<coil address="11" value="0"/>
<coil address="12" value="1"/>
<coil address="13" value="0"/>
<coil address="14" value="1"/>
<coil address="15" value="0"/>
<coil address="16" value="1"/>
<coil address="17" value="0"/>
<coil address="18" value="1"/>
<coil address="19" value="0"/>
<coil address="20" value="1"/>
<coil address="21" value="0"/>
<coil address="22" value="1"/>
<coil address="23" value="0"/>
<coil address="24" value="1"/>
<coil address="25" value="0"/>
<coil address="26" value="1"/>
<coil address="27" value="0"/>
<coil address="28" value="1"/>
<coil address="29" value="0"/>
<coil address="30" value="1"/>
<coil address="31" value="0"/>
<!-- Coils 100..109 — scratch range for write-roundtrip tests. -->
<coil address="100" value="0"/>
<coil address="101" value="0"/>
<coil address="102" value="0"/>
<coil address="103" value="0"/>
<coil address="104" value="0"/>
<coil address="105" value="0"/>
<coil address="106" value="0"/>
<coil address="107" value="0"/>
<coil address="108" value="0"/>
<coil address="109" value="0"/>
</coils>
<tuning>
<!-- Zero artificial reply delay or error rate. Set non-zero in the GUI to
simulate a slow / lossy link without re-authoring the file. -->
<reply_delay min="0" max="0"/>
<error_rates no_reply="0.0"/>
</tuning>
</slave>
</modbuspal_project>