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>
This commit is contained in:
Joseph Doherty
2026-04-18 20:05:20 -04:00
parent 7009483d16
commit 02a0e8efd1
3 changed files with 452 additions and 19 deletions

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>