Compare commits
3 Commits
phase-3-pr
...
phase-3-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02fccbc762 | ||
|
|
a05b84858d | ||
| c59ac9e52d |
@@ -13,22 +13,30 @@ confirmed DL205 quirk lands in a follow-up PR as a named test in that project.
|
|||||||
|
|
||||||
## Harness
|
## Harness
|
||||||
|
|
||||||
**Chosen simulator: ModbusPal** (Java, scriptable). Rationale:
|
**Chosen simulator: pymodbus 3.13.0** (`pip install 'pymodbus[simulator]==3.13.0'`).
|
||||||
- Scriptable enough to mimic device-specific behaviors (non-standard register
|
Replaced ModbusPal in PR 43 — see `tests/.../Pymodbus/README.md` for the
|
||||||
layouts, custom exception codes, intentional response delays).
|
trade-off rationale. Headline reasons:
|
||||||
- Runs locally, no CI dependency. Tests skip when `localhost:502` (or the configured
|
|
||||||
simulator endpoint) isn't reachable.
|
|
||||||
- Free + long-maintained — physical PLC bench is unavailable in most dev
|
|
||||||
environments, and renting cloud PLCs isn't worth the per-test cost.
|
|
||||||
|
|
||||||
**Setup pattern** (not yet codified in a script — will land alongside the integration
|
- **Headless** pure-Python CLI; no Java GUI, runs cleanly on a CI runner.
|
||||||
test project):
|
- **Maintained** — current stable 3.13.0; ModbusPal 1.6b is abandoned.
|
||||||
1. Install ModbusPal, load the per-device `.xmpp` profile from
|
- **All four standard tables** (HR, IR, coils, DI) configurable; ModbusPal
|
||||||
`tests/Driver.Modbus.IntegrationTests/ModbusPal/` (TBD directory).
|
1.6b only exposed HR + coils.
|
||||||
2. Start the simulator listening on `localhost:502` (or override via
|
- **Built-in actions** (`increment`, `random`, `timestamp`, `uptime`) +
|
||||||
`MODBUS_SIM_ENDPOINT` env var).
|
optional custom-Python actions for declarative dynamic behaviors.
|
||||||
3. `dotnet test` the integration project — tests auto-skip when the endpoint is
|
- **Per-register raw uint16 seeding** — encoding the DL205 string-byte-order
|
||||||
unreachable, so forgetting to start the simulator doesn't wedge CI.
|
/ BCD / CDAB-float quirks stays explicit (the quirk math lives in the
|
||||||
|
`_quirk` JSON-comment fields next to each register).
|
||||||
|
- Pip-installable on Windows; sidesteps the privileged-port admin
|
||||||
|
requirement by defaulting to TCP **5020** instead of 502.
|
||||||
|
|
||||||
|
**Setup pattern**:
|
||||||
|
1. `pip install "pymodbus[simulator]==3.13.0"`.
|
||||||
|
2. Start the simulator with one of the in-repo profiles:
|
||||||
|
`tests\.../Pymodbus\serve.ps1 -Profile standard` (or `-Profile dl205`).
|
||||||
|
3. `dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` —
|
||||||
|
tests auto-skip when the endpoint is unreachable. Default endpoint is
|
||||||
|
`localhost:5020`; override via `MODBUS_SIM_ENDPOINT` for a real PLC on its
|
||||||
|
native port 502.
|
||||||
|
|
||||||
## Per-device quirk catalog
|
## Per-device quirk catalog
|
||||||
|
|
||||||
@@ -87,20 +95,27 @@ vendors get promoted into driver defaults or opt-in options:
|
|||||||
protocol end-to-end. The in-memory `FakeTransport` from the unit test suite is
|
protocol end-to-end. The in-memory `FakeTransport` from the unit test suite is
|
||||||
deliberately not used here — its value is speed + determinism, which doesn't
|
deliberately not used here — its value is speed + determinism, which doesn't
|
||||||
help reproduce device-specific issues.
|
help reproduce device-specific issues.
|
||||||
- **Don't depend on ModbusPal state between tests.** Each test resets the
|
- **Don't depend on simulator state between tests.** Each test resets the
|
||||||
simulator's register bank or uses a unique address range. Avoid relying on
|
simulator's register bank or uses a unique address range. Avoid relying on
|
||||||
"previous test left value at register 10" setups that flake when tests run in
|
"previous test left value at register 10" setups that flake when tests run in
|
||||||
parallel or re-order.
|
parallel or re-order. Either the test mutates the scratch ranges and restores
|
||||||
|
on finally, or it uses pymodbus's REST API to reset state between facts.
|
||||||
|
|
||||||
## Next concrete PRs
|
## Next concrete PRs
|
||||||
|
|
||||||
- **PR 30 — Integration test project + DL205 profile scaffold** — **DONE**.
|
- **PR 30 — Integration test project + DL205 profile scaffold** — **DONE**.
|
||||||
Shipped `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` with
|
Shipped `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` with
|
||||||
`ModbusSimulatorFixture` (TCP-probe, skips with a clear `SkipReason` when the
|
`ModbusSimulatorFixture` (TCP-probe, skips with a clear `SkipReason` when the
|
||||||
endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub — one
|
endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub), and
|
||||||
writable holding register at address 100), and `DL205/DL205SmokeTests.cs`
|
`DL205/DL205SmokeTests.cs` (write-then-read round-trip).
|
||||||
(write-then-read round-trip). `ModbusPal/` directory holds the README
|
- **PR 41 — DL205 quirk catalog doc** — **DONE**. `docs/v2/dl205.md`
|
||||||
pointing at the to-be-committed `DL205.xmpp` profile.
|
documents every DL205/DL260 Modbus divergence with primary-source citations.
|
||||||
- **PR 31+**: one PR per confirmed DL205 quirk, landing the named test + any
|
- **PR 42 — ModbusPal `.xmpp` profiles** — **SUPERSEDED by PR 43**. Replaced
|
||||||
driver-side adjustment (e.g., retry on dropped TxId) needed to pass it. Drop
|
with pymodbus JSON because ModbusPal 1.6b is abandoned, GUI-only, and only
|
||||||
the `DL205.xmpp` profile into `ModbusPal/` alongside the first quirk PR.
|
exposes 2 of the 4 standard tables.
|
||||||
|
- **PR 43 — pymodbus JSON profiles** — **DONE**. `Pymodbus/standard.json` +
|
||||||
|
`Pymodbus/dl205.json` + `Pymodbus/serve.ps1` runner. Both bind TCP 5020.
|
||||||
|
- **PR 44+**: one PR per confirmed DL205 quirk, landing the named test + any
|
||||||
|
driver-side adjustment (string byte order, BCD decoder, V-memory address
|
||||||
|
helper, FC16 cap-per-device-family) needed to pass it. Each quirk's value
|
||||||
|
is already pre-encoded in `Pymodbus/dl205.json`.
|
||||||
|
|||||||
@@ -28,10 +28,20 @@ public sealed class ModbusTcpTransport : IModbusTransport
|
|||||||
|
|
||||||
public async Task ConnectAsync(CancellationToken ct)
|
public async Task ConnectAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
_client = new TcpClient();
|
// Resolve the host explicitly + prefer IPv4. .NET's TcpClient default-constructor is
|
||||||
|
// dual-stack (IPv6 first, fallback to IPv4) — but most Modbus TCP devices (PLCs and
|
||||||
|
// simulators like pymodbus) bind 0.0.0.0 only, so the IPv6 attempt times out and we
|
||||||
|
// burn the entire ConnectAsync budget before even trying IPv4. Resolving first +
|
||||||
|
// dialing the IPv4 address directly sidesteps that.
|
||||||
|
var addresses = await System.Net.Dns.GetHostAddressesAsync(_host, ct).ConfigureAwait(false);
|
||||||
|
var ipv4 = System.Linq.Enumerable.FirstOrDefault(addresses,
|
||||||
|
a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork);
|
||||||
|
var target = ipv4 ?? (addresses.Length > 0 ? addresses[0] : System.Net.IPAddress.Loopback);
|
||||||
|
|
||||||
|
_client = new TcpClient(target.AddressFamily);
|
||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
cts.CancelAfter(_timeout);
|
cts.CancelAfter(_timeout);
|
||||||
await _client.ConnectAsync(_host, _port, cts.Token).ConfigureAwait(false);
|
await _client.ConnectAsync(target, _port, cts.Token).ConfigureAwait(false);
|
||||||
_stream = _client.GetStream();
|
_stream = _client.GetStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tag map for the AutomationDirect DL205 device class. Mirrors what the ModbusPal
|
/// Tag map for the AutomationDirect DL205 device class. Mirrors what the pymodbus
|
||||||
/// <c>.xmpp</c> profile in <c>ModbusPal/DL205.xmpp</c> exposes (or the real PLC, when
|
/// <c>dl205.json</c> profile in <c>Pymodbus/dl205.json</c> exposes (or the real PLC, when
|
||||||
/// <see cref="ModbusSimulatorFixture"/> is pointed at one).
|
/// <see cref="ModbusSimulatorFixture"/> is pointed at one).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This is the scaffold — each tag is deliberately generic so the smoke test has stable
|
/// This is the scaffold — each tag is deliberately generic so the smoke test has stable
|
||||||
/// addresses to read. Device-specific quirk tests (word order, max-register, register-zero
|
/// addresses to read. Device-specific quirk tests (word order, max-register, register-zero
|
||||||
/// access, etc.) will land in their own test classes alongside this profile as the user
|
/// access, etc.) will land in their own test classes alongside this profile as the user
|
||||||
/// validates each behavior in ModbusPal; see <c>docs/v2/modbus-test-plan.md</c> §per-device
|
/// validates each behavior in pymodbus; see <c>docs/v2/modbus-test-plan.md</c> §per-device
|
||||||
/// quirk catalog for the checklist.
|
/// quirk catalog for the checklist.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static class DL205Profile
|
public static class DL205Profile
|
||||||
{
|
{
|
||||||
/// <summary>Holding register the smoke test reads. Address 100 sidesteps the DL205
|
/// <summary>
|
||||||
/// register-zero quirk (pending confirmation) — see modbus-test-plan.md.</summary>
|
/// Holding register the smoke test writes + reads. Address 200 is the first cell of the
|
||||||
public const ushort SmokeHoldingRegister = 100;
|
/// scratch HR range in both <c>Pymodbus/standard.json</c> (HR[200..209] = 0) and
|
||||||
|
/// <c>Pymodbus/dl205.json</c> (HR[4096..4103] added in PR 43 for the same purpose), so
|
||||||
|
/// the smoke test runs identically against either simulator profile. Originally
|
||||||
|
/// targeted HR[100] — moved to HR[200] when the standard profile claimed HR[100] as
|
||||||
|
/// the auto-incrementing register that drives subscribe-and-receive tests.
|
||||||
|
/// </summary>
|
||||||
|
public const ushort SmokeHoldingRegister = 200;
|
||||||
|
|
||||||
/// <summary>Expected value the ModbusPal profile seeds into register 100. When running
|
/// <summary>Value the smoke test writes then reads back to assert round-trip integrity.</summary>
|
||||||
/// against a real DL205 (or a ModbusPal profile where this register is writable), the smoke
|
|
||||||
/// test seeds this value first, then reads it back.</summary>
|
|
||||||
public const short SmokeHoldingValue = 1234;
|
public const short SmokeHoldingValue = 1234;
|
||||||
|
|
||||||
public static ModbusDriverOptions BuildOptions(string host, int port) => new()
|
public static ModbusDriverOptions BuildOptions(string host, int port) => new()
|
||||||
@@ -32,7 +36,7 @@ public static class DL205Profile
|
|||||||
Tags =
|
Tags =
|
||||||
[
|
[
|
||||||
new ModbusTagDefinition(
|
new ModbusTagDefinition(
|
||||||
Name: "DL205_Smoke_HReg100",
|
Name: "Smoke_HReg200",
|
||||||
Region: ModbusRegion.HoldingRegisters,
|
Region: ModbusRegion.HoldingRegisters,
|
||||||
Address: SmokeHoldingRegister,
|
Address: SmokeHoldingRegister,
|
||||||
DataType: ModbusDataType.Int16,
|
DataType: ModbusDataType.Int16,
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ public sealed class DL205SmokeTests(ModbusSimulatorFixture sim)
|
|||||||
// zeroed at simulator start, and tests must not depend on prior-test state per the
|
// zeroed at simulator start, and tests must not depend on prior-test state per the
|
||||||
// test-plan conventions.
|
// test-plan conventions.
|
||||||
var writeResults = await driver.WriteAsync(
|
var writeResults = await driver.WriteAsync(
|
||||||
[new(FullReference: "DL205_Smoke_HReg100", Value: (short)DL205Profile.SmokeHoldingValue)],
|
[new(FullReference: "Smoke_HReg200", Value: (short)DL205Profile.SmokeHoldingValue)],
|
||||||
TestContext.Current.CancellationToken);
|
TestContext.Current.CancellationToken);
|
||||||
writeResults.Count.ShouldBe(1);
|
writeResults.Count.ShouldBe(1);
|
||||||
writeResults[0].StatusCode.ShouldBe(0u, "write must succeed against the ModbusPal DL205 profile");
|
writeResults[0].StatusCode.ShouldBe(0u, "write must succeed against the ModbusPal DL205 profile");
|
||||||
|
|
||||||
var readResults = await driver.ReadAsync(
|
var readResults = await driver.ReadAsync(
|
||||||
["DL205_Smoke_HReg100"],
|
["Smoke_HReg200"],
|
||||||
TestContext.Current.CancellationToken);
|
TestContext.Current.CancellationToken);
|
||||||
readResults.Count.ShouldBe(1);
|
readResults.Count.ShouldBe(1);
|
||||||
readResults[0].StatusCode.ShouldBe(0u);
|
readResults[0].StatusCode.ShouldBe(0u);
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
# ModbusPal simulator profiles
|
|
||||||
|
|
||||||
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 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. **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`).
|
|
||||||
|
|
||||||
## Switching between Standard and DL205
|
|
||||||
|
|
||||||
Stop the running simulator (toolbar's **Stop** button), **File > Load**
|
|
||||||
the other profile, **Run**.
|
|
||||||
|
|
||||||
## Environment variables
|
|
||||||
|
|
||||||
- `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.
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
<?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>
|
|
||||||
@@ -3,8 +3,9 @@ using System.Net.Sockets;
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests;
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reachability probe for a Modbus TCP simulator (ModbusPal or a real PLC). Parses
|
/// Reachability probe for a Modbus TCP simulator (pymodbus-driven, see
|
||||||
/// <c>MODBUS_SIM_ENDPOINT</c> (default <c>localhost:502</c>) and TCP-connects once at
|
/// <c>Pymodbus/serve.ps1</c>) or a real PLC. Parses
|
||||||
|
/// <c>MODBUS_SIM_ENDPOINT</c> (default <c>localhost:5020</c> per PR 43) and TCP-connects once at
|
||||||
/// fixture construction. Each test checks <see cref="SkipReason"/> and calls
|
/// fixture construction. Each test checks <see cref="SkipReason"/> and calls
|
||||||
/// <c>Assert.Skip</c> when the endpoint was unreachable, so a dev box without a running
|
/// <c>Assert.Skip</c> when the endpoint was unreachable, so a dev box without a running
|
||||||
/// simulator still passes `dotnet test` cleanly — matches the Galaxy live-smoke pattern in
|
/// simulator still passes `dotnet test` cleanly — matches the Galaxy live-smoke pattern in
|
||||||
@@ -25,7 +26,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class ModbusSimulatorFixture : IAsyncDisposable
|
public sealed class ModbusSimulatorFixture : IAsyncDisposable
|
||||||
{
|
{
|
||||||
private const string DefaultEndpoint = "localhost:502";
|
// PR 43: default port is 5020 (pymodbus convention) instead of 502 (Modbus standard).
|
||||||
|
// Picking 5020 sidesteps the privileged-port admin requirement on Windows + matches the
|
||||||
|
// port baked into the pymodbus simulator JSON profiles in Pymodbus/. Override with
|
||||||
|
// MODBUS_SIM_ENDPOINT to point at a real PLC on its native port 502.
|
||||||
|
private const string DefaultEndpoint = "localhost:5020";
|
||||||
private const string EndpointEnvVar = "MODBUS_SIM_ENDPOINT";
|
private const string EndpointEnvVar = "MODBUS_SIM_ENDPOINT";
|
||||||
|
|
||||||
public string Host { get; }
|
public string Host { get; }
|
||||||
@@ -41,18 +46,30 @@ public sealed class ModbusSimulatorFixture : IAsyncDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var client = new TcpClient();
|
// Force IPv4 family on the probe — pymodbus's TCP server binds 0.0.0.0 (IPv4 only)
|
||||||
var task = client.ConnectAsync(Host, Port);
|
// while .NET's TcpClient default-resolves "localhost" → IPv6 ::1 first, fails to
|
||||||
|
// connect, and only then tries IPv4. Under .NET 10 the IPv6 fail surfaces as a
|
||||||
|
// 2s timeout (no graceful fallback by default), so the C# probe times out even
|
||||||
|
// though a PowerShell probe of the same endpoint succeeds. Resolving + dialing
|
||||||
|
// explicit IPv4 sidesteps the dual-stack ordering.
|
||||||
|
using var client = new TcpClient(System.Net.Sockets.AddressFamily.InterNetwork);
|
||||||
|
var task = client.ConnectAsync(
|
||||||
|
System.Net.Dns.GetHostAddresses(Host)
|
||||||
|
.FirstOrDefault(a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||||
|
?? System.Net.IPAddress.Loopback,
|
||||||
|
Port);
|
||||||
if (!task.Wait(TimeSpan.FromSeconds(2)) || !client.Connected)
|
if (!task.Wait(TimeSpan.FromSeconds(2)) || !client.Connected)
|
||||||
{
|
{
|
||||||
SkipReason = $"Modbus simulator at {Host}:{Port} did not accept a TCP connection within 2s. " +
|
SkipReason = $"Modbus simulator at {Host}:{Port} did not accept a TCP connection within 2s. " +
|
||||||
$"Start ModbusPal (or override {EndpointEnvVar}) and re-run.";
|
$"Start the pymodbus simulator (Pymodbus\\serve.ps1 -Profile standard) " +
|
||||||
|
$"or override {EndpointEnvVar}, then re-run.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
SkipReason = $"Modbus simulator at {Host}:{Port} unreachable: {ex.GetType().Name}: {ex.Message}. " +
|
SkipReason = $"Modbus simulator at {Host}:{Port} unreachable: {ex.GetType().Name}: {ex.Message}. " +
|
||||||
$"Start ModbusPal (or override {EndpointEnvVar}) and re-run.";
|
$"Start the pymodbus simulator (Pymodbus\\serve.ps1 -Profile standard) " +
|
||||||
|
$"or override {EndpointEnvVar}, then re-run.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# pymodbus simulator profiles
|
||||||
|
|
||||||
|
Two JSON-config profiles for pymodbus's `ModbusSimulatorServer`. Replaces the
|
||||||
|
ModbusPal `.xmpp` profiles that lived here in PR 42 — pymodbus is headless,
|
||||||
|
maintained, semantic about register layout, and pip-installable on Windows.
|
||||||
|
|
||||||
|
| File | What it simulates | Test category |
|
||||||
|
|---|---|---|
|
||||||
|
| [`standard.json`](standard.json) | Generic Modbus TCP server — HR[0..31] = address-as-value, HR[100] declarative auto-increment via `"action": "increment"`, alternating coils, scratch ranges for write tests. | `Trait=Standard` |
|
||||||
|
| [`dl205.json`](dl205.json) | 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. Inline `_quirk` comments per register name the behavior. | `Trait=DL205` |
|
||||||
|
|
||||||
|
Both bind TCP **5020** (pymodbus convention; sidesteps the Windows admin
|
||||||
|
requirement for privileged port 502). The integration-test fixture
|
||||||
|
(`ModbusSimulatorFixture`) defaults to `localhost:5020` to match — override
|
||||||
|
via `MODBUS_SIM_ENDPOINT` to point at a real PLC on its native port 502.
|
||||||
|
|
||||||
|
Run only **one profile at a time** (they share TCP 5020).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pip install "pymodbus[simulator]==3.13.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `[simulator]` extra pulls in `aiohttp` for the optional web UI / REST API.
|
||||||
|
Pinned to 3.13.0 for reproducibility — avoid 4.x dev releases until stabilized.
|
||||||
|
Requires Python ≥ 3.10. Windows Firewall will prompt on first bind; allow
|
||||||
|
Private network.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
Foreground (Ctrl+C to stop). Use the `serve.ps1` wrapper:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\serve.ps1 -Profile standard
|
||||||
|
.\serve.ps1 -Profile dl205
|
||||||
|
```
|
||||||
|
|
||||||
|
Or invoke pymodbus directly:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pymodbus.simulator `
|
||||||
|
--modbus_server srv `
|
||||||
|
--modbus_device dev `
|
||||||
|
--json_file .\standard.json `
|
||||||
|
--http_port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Web UI at `http://localhost:8080` lets you inspect + poke registers manually.
|
||||||
|
Pass `--no_http` (or `-HttpPort 0` to `serve.ps1`) to disable.
|
||||||
|
|
||||||
|
## Run the integration tests
|
||||||
|
|
||||||
|
In a separate shell, with the simulator running:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\dohertj2\Desktop\lmxopcua
|
||||||
|
dotnet test tests\ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests auto-skip with a clear `SkipReason` if `localhost:5020` isn't reachable
|
||||||
|
within 2 seconds. Filter by trait when both profiles' tests coexist:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet test ... --filter "Trait=Standard"
|
||||||
|
dotnet test ... --filter "Trait=DL205"
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's encoded in each profile
|
||||||
|
|
||||||
|
### standard.json
|
||||||
|
|
||||||
|
- HR[0..31]: each register's value equals its address. Easy mental map.
|
||||||
|
- HR[100]: `"action": "increment"` ticks 0..65535 on every register access — drives subscribe-and-receive tests so they have a register that changes without a write.
|
||||||
|
- HR[200..209]: scratch range for write-roundtrip tests.
|
||||||
|
- Coils[0..31]: alternating on/off (even=on).
|
||||||
|
- Coils[100..109]: scratch.
|
||||||
|
- All addresses 0..1023 are writable (`"write": [[0, 1023]]`).
|
||||||
|
|
||||||
|
### dl205.json (per `docs/v2/dl205.md`)
|
||||||
|
|
||||||
|
| HR address | Quirk demonstrated | Raw value | Decoded |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `0` (V0) | Register 0 is valid (rejects-register-0 rumour disproved) | `51966` (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) | first/last/mid markers; rest defaults to 0 | 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 |
|
||||||
|
|
||||||
|
The DL260 X-input markers (FC02 discrete inputs) **are not encoded separately**
|
||||||
|
because the profile uses `shared blocks: true` (matches DL series memory
|
||||||
|
model) — coils/DI/HR/IR overlay the same word address space. Tests that
|
||||||
|
target FC02 against this profile end up reading the same bit positions as
|
||||||
|
the coils they share with.
|
||||||
|
|
||||||
|
## What's IN pymodbus that wasn't in ModbusPal
|
||||||
|
|
||||||
|
- **All four standard tables** (HR, IR, coils, DI) configurable via `co size` / `di size` / `hr size` / `ir size` setup keys.
|
||||||
|
- **Per-register raw uint16 seeding** — `{"addr": 1040, "value": 25928}` puts exactly that 16-bit value on the wire. No interpretation.
|
||||||
|
- **Built-in actions**: `increment`, `random`, `timestamp`, `reset`, `uptime` for declarative dynamic registers. No Python script alongside the config required.
|
||||||
|
- **Custom actions** — point `--custom_actions_module` at a `.py` file exposing callables to express anything more complex (per-second wall-clock ticks, BCD synthesis, etc.).
|
||||||
|
- **Headless** — pure CLI process, no Java, no Swing. Pip-installable. Plays well with CI runners.
|
||||||
|
- **Web UI / REST API** — `--http_port 8080` adds an aiohttp server for live inspection. Optional.
|
||||||
|
- **Maintained** — current stable 3.13.0 (April 2026), active development on 4.0 dev branch.
|
||||||
|
|
||||||
|
## Trade-offs vs the hand-authored ModbusPal profiles
|
||||||
|
|
||||||
|
- pymodbus's built-in `float32` type stores in pymodbus's word order; for explicit DL205 CDAB control we seed two raw `uint16` entries instead. Documented inline in `dl205.json`.
|
||||||
|
- `increment` action ticks per-access, not wall-clock. A 250ms-poll integration test sees variation either way; for strict 1Hz cadence add `--custom_actions_module my_actions.py` with a `time.time()`-based callable.
|
||||||
|
- `dl205.json` uses `shared blocks: true` because it matches DL series memory model; `standard.json` uses `shared blocks: false` so coils and HR address spaces are independent (more like a textbook PLC).
|
||||||
|
|
||||||
|
## File format reference
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_list": {
|
||||||
|
"<server-name>": {
|
||||||
|
"comm": "tcp",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 5020,
|
||||||
|
"framer": "socket",
|
||||||
|
"device_id": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"device_list": {
|
||||||
|
"<device-name>": {
|
||||||
|
"setup": {
|
||||||
|
"co size": N, "di size": N, "hr size": N, "ir size": N,
|
||||||
|
"shared blocks": false,
|
||||||
|
"type exception": false,
|
||||||
|
"defaults": { "value": {...}, "action": {...} }
|
||||||
|
},
|
||||||
|
"invalid": [],
|
||||||
|
"write": [[<from>, <to>]],
|
||||||
|
"bits": [{"addr": N, "value": 0|1}],
|
||||||
|
"uint16": [{"addr": N, "value": <0..65535>, "action"?: "increment", "parameters"?: {...}}],
|
||||||
|
"uint32": [{"addr": N, "value": <int>}],
|
||||||
|
"float32": [{"addr": N, "value": <float>}],
|
||||||
|
"string": [{"addr": N, "value": "<text>"}],
|
||||||
|
"repeat": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The CLI args `--modbus_server <server-name> --modbus_device <device-name>`
|
||||||
|
pick which entries the simulator binds.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [pymodbus on PyPI](https://pypi.org/project/pymodbus/) — install, version pin
|
||||||
|
- [Simulator config docs](https://pymodbus.readthedocs.io/en/dev/source/library/simulator/config.html) — full schema reference
|
||||||
|
- [Simulator REST API](https://pymodbus.readthedocs.io/en/latest/source/library/simulator/restapi.html) — for the optional web UI
|
||||||
|
- [`docs/v2/dl205.md`](../../../docs/v2/dl205.md) — what each DL205 profile entry simulates
|
||||||
|
- [`docs/v2/modbus-test-plan.md`](../../../docs/v2/modbus-test-plan.md) — the `DL205_<behavior>` test naming convention
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"_comment": "DL205.json — DirectLOGIC DL205/DL260 quirk simulator. Models docs/v2/dl205.md as concrete register values. NOTE: pymodbus rejects unknown keys at device-list / setup level; explanatory comments live at top-level _comment + in README + git. Inline _quirk keys WITHIN individual register entries are accepted by pymodbus 3.13.0 (it only validates addr / value / action / parameters per entry). Each quirky uint16 is a pre-computed raw 16-bit value; pymodbus serves it verbatim. shared blocks=true matches DL series memory model. write list mirrors each seeded block — pymodbus rejects sweeping write ranges that include undefined cells.",
|
||||||
|
|
||||||
|
"server_list": {
|
||||||
|
"srv": {
|
||||||
|
"comm": "tcp",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 5020,
|
||||||
|
"framer": "socket",
|
||||||
|
"device_id": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"device_list": {
|
||||||
|
"dev": {
|
||||||
|
"setup": {
|
||||||
|
"co size": 16384,
|
||||||
|
"di size": 8192,
|
||||||
|
"hr size": 16384,
|
||||||
|
"ir size": 1024,
|
||||||
|
"shared blocks": true,
|
||||||
|
"type exception": false,
|
||||||
|
"defaults": {
|
||||||
|
"value": {"bits": 0, "uint16": 0, "uint32": 0, "float32": 0.0, "string": " "},
|
||||||
|
"action": {"bits": null, "uint16": null, "uint32": null, "float32": null, "string": null}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"invalid": [],
|
||||||
|
"write": [
|
||||||
|
[0, 0],
|
||||||
|
[200, 209],
|
||||||
|
[1024, 1024],
|
||||||
|
[1040, 1042],
|
||||||
|
[1056, 1057],
|
||||||
|
[1072, 1072],
|
||||||
|
[1280, 1282],
|
||||||
|
[1343, 1343],
|
||||||
|
[1407, 1407],
|
||||||
|
[2048, 2050],
|
||||||
|
[3072, 3074],
|
||||||
|
[4000, 4007],
|
||||||
|
[8448, 8448]
|
||||||
|
],
|
||||||
|
|
||||||
|
"uint16": [
|
||||||
|
{"_quirk": "V0 marker. HR[0]=0xCAFE proves register 0 is valid on DL205/DL260 (rejects-register-0 was a DL05/DL06 relative-mode artefact). 0xCAFE = 51966.",
|
||||||
|
"addr": 0, "value": 51966},
|
||||||
|
|
||||||
|
{"_quirk": "Scratch HR range 200..209 — mirrors the standard.json scratch range so the smoke test (DL205Profile.SmokeHoldingRegister=200) round-trips identically against either profile.",
|
||||||
|
"addr": 200, "value": 0},
|
||||||
|
{"addr": 201, "value": 0},
|
||||||
|
{"addr": 202, "value": 0},
|
||||||
|
{"addr": 203, "value": 0},
|
||||||
|
{"addr": 204, "value": 0},
|
||||||
|
{"addr": 205, "value": 0},
|
||||||
|
{"addr": 206, "value": 0},
|
||||||
|
{"addr": 207, "value": 0},
|
||||||
|
{"addr": 208, "value": 0},
|
||||||
|
{"addr": 209, "value": 0},
|
||||||
|
|
||||||
|
{"_quirk": "V2000 marker. V2000 octal = decimal 1024 = PDU 0x0400. Marker 0x2000 = 8192.",
|
||||||
|
"addr": 1024, "value": 8192},
|
||||||
|
|
||||||
|
{"_quirk": "V40400 marker. V40400 octal = decimal 8448 = PDU 0x2100 (NOT register 0). Marker 0x4040 = 16448.",
|
||||||
|
"addr": 8448, "value": 16448},
|
||||||
|
|
||||||
|
{"_quirk": "String 'Hello' first char in LOW byte. HR[0x410] = 'H'(0x48) lo + 'e'(0x65) hi = 0x6548 = 25928.",
|
||||||
|
"addr": 1040, "value": 25928},
|
||||||
|
{"_quirk": "String 'Hello' second char-pair: 'l'(0x6C) lo + 'l'(0x6C) hi = 0x6C6C = 27756.",
|
||||||
|
"addr": 1041, "value": 27756},
|
||||||
|
{"_quirk": "String 'Hello' third char-pair: 'o'(0x6F) lo + null(0x00) hi = 0x006F = 111.",
|
||||||
|
"addr": 1042, "value": 111},
|
||||||
|
|
||||||
|
{"_quirk": "Float32 1.5f in CDAB word order. IEEE 754 1.5 = 0x3FC00000. CDAB = low word first: HR[0x420]=0x0000, HR[0x421]=0x3FC0=16320.",
|
||||||
|
"addr": 1056, "value": 0},
|
||||||
|
{"_quirk": "Float32 1.5f CDAB high word.",
|
||||||
|
"addr": 1057, "value": 16320},
|
||||||
|
|
||||||
|
{"_quirk": "BCD register. Decimal 1234 stored as BCD nibbles 0x1234 = 4660. NOT binary 1234 (= 0x04D2).",
|
||||||
|
"addr": 1072, "value": 4660},
|
||||||
|
|
||||||
|
{"_quirk": "FC03 cap test marker — first cell of a 128-register span the FC03 cap test reads. Other cells in the span aren't seeded explicitly, so reads of HR[1283..1342] / 1344..1406 return the default 0; the seeded markers at 1280, 1281, 1282, 1343, 1407 prove the span boundaries.",
|
||||||
|
"addr": 1280, "value": 0},
|
||||||
|
{"addr": 1281, "value": 1},
|
||||||
|
{"addr": 1282, "value": 2},
|
||||||
|
{"addr": 1343, "value": 63},
|
||||||
|
{"addr": 1407, "value": 127}
|
||||||
|
],
|
||||||
|
|
||||||
|
"bits": [
|
||||||
|
{"_quirk": "Y0 marker. DL260 maps Y0 to coil 2048 (0-based). Coil 2048 = ON proves the mapping.",
|
||||||
|
"addr": 2048, "value": 1},
|
||||||
|
{"addr": 2049, "value": 0},
|
||||||
|
{"addr": 2050, "value": 1},
|
||||||
|
|
||||||
|
{"_quirk": "C0 marker. DL260 maps C0 to coil 3072 (0-based). Coil 3072 = ON proves the mapping.",
|
||||||
|
"addr": 3072, "value": 1},
|
||||||
|
{"addr": 3073, "value": 0},
|
||||||
|
{"addr": 3074, "value": 1},
|
||||||
|
|
||||||
|
{"_quirk": "Scratch C-relays for write-roundtrip tests against the writable C range.",
|
||||||
|
"addr": 4000, "value": 0},
|
||||||
|
{"addr": 4001, "value": 0},
|
||||||
|
{"addr": 4002, "value": 0},
|
||||||
|
{"addr": 4003, "value": 0},
|
||||||
|
{"addr": 4004, "value": 0},
|
||||||
|
{"addr": 4005, "value": 0},
|
||||||
|
{"addr": 4006, "value": 0},
|
||||||
|
{"addr": 4007, "value": 0}
|
||||||
|
],
|
||||||
|
|
||||||
|
"uint32": [],
|
||||||
|
"float32": [],
|
||||||
|
"string": [],
|
||||||
|
"repeat": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Launches the pymodbus simulator with one of the integration-test profiles
|
||||||
|
(Standard or DL205). Foreground process — Ctrl+C to stop.
|
||||||
|
|
||||||
|
.PARAMETER Profile
|
||||||
|
Which simulator profile to run: 'standard' or 'dl205'. Both bind TCP 5020 by
|
||||||
|
default so they can't run simultaneously on the same box.
|
||||||
|
|
||||||
|
.PARAMETER HttpPort
|
||||||
|
Port for pymodbus's optional web UI / REST API. Default 8080. Pass 0 to
|
||||||
|
disable (passes --no_http).
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\serve.ps1 -Profile standard
|
||||||
|
Starts the standard server on TCP 5020 with web UI on 8080.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\serve.ps1 -Profile dl205 -HttpPort 0
|
||||||
|
Starts the DL205 server on TCP 5020, no web UI.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)] [ValidateSet('standard', 'dl205')] [string]$Profile,
|
||||||
|
[int]$HttpPort = 8080
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$here = $PSScriptRoot
|
||||||
|
|
||||||
|
# Confirm pymodbus.simulator is on PATH — clearer message than the
|
||||||
|
# 'CommandNotFoundException' dotnet style.
|
||||||
|
$cmd = Get-Command pymodbus.simulator -ErrorAction SilentlyContinue
|
||||||
|
if (-not $cmd) {
|
||||||
|
Write-Error "pymodbus.simulator not found. Install with: pip install 'pymodbus[simulator]==3.13.0'"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$jsonFile = Join-Path $here "$Profile.json"
|
||||||
|
if (-not (Test-Path $jsonFile)) {
|
||||||
|
Write-Error "Profile config not found: $jsonFile"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = @(
|
||||||
|
'--modbus_server', 'srv',
|
||||||
|
'--modbus_device', 'dev',
|
||||||
|
'--json_file', $jsonFile
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($HttpPort -gt 0) {
|
||||||
|
$args += @('--http_port', $HttpPort)
|
||||||
|
Write-Host "Web UI will be at http://localhost:$HttpPort"
|
||||||
|
} else {
|
||||||
|
$args += '--no_http'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Starting pymodbus simulator: profile=$Profile TCP=localhost:5020"
|
||||||
|
Write-Host "Ctrl+C to stop."
|
||||||
|
& pymodbus.simulator @args
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Standard.json — generic Modbus TCP server for the integration suite. See ../README.md. NOTE: pymodbus rejects unknown keys at device-list / setup level; explanatory comments live in the README + git history. Layout: HR[0..31]=address-as-value, HR[100]=auto-increment, HR[200..209]=scratch, coils 1024..1055=alternating, coils 1100..1109=scratch. Coils live at 1024+ because pymodbus stores all 4 standard tables in ONE underlying cell array — bits and uint16 at the same address conflict (each cell can only be typed once).",
|
||||||
|
|
||||||
|
"server_list": {
|
||||||
|
"srv": {
|
||||||
|
"comm": "tcp",
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 5020,
|
||||||
|
"framer": "socket",
|
||||||
|
"device_id": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"device_list": {
|
||||||
|
"dev": {
|
||||||
|
"setup": {
|
||||||
|
"co size": 2048,
|
||||||
|
"di size": 2048,
|
||||||
|
"hr size": 2048,
|
||||||
|
"ir size": 2048,
|
||||||
|
"shared blocks": true,
|
||||||
|
"type exception": false,
|
||||||
|
"defaults": {
|
||||||
|
"value": {"bits": 0, "uint16": 0, "uint32": 0, "float32": 0.0, "string": " "},
|
||||||
|
"action": {"bits": null, "uint16": null, "uint32": null, "float32": null, "string": null}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"invalid": [],
|
||||||
|
"write": [
|
||||||
|
[0, 31],
|
||||||
|
[100, 100],
|
||||||
|
[200, 209],
|
||||||
|
[1024, 1055],
|
||||||
|
[1100, 1109]
|
||||||
|
],
|
||||||
|
|
||||||
|
"uint16": [
|
||||||
|
{"addr": 0, "value": 0}, {"addr": 1, "value": 1},
|
||||||
|
{"addr": 2, "value": 2}, {"addr": 3, "value": 3},
|
||||||
|
{"addr": 4, "value": 4}, {"addr": 5, "value": 5},
|
||||||
|
{"addr": 6, "value": 6}, {"addr": 7, "value": 7},
|
||||||
|
{"addr": 8, "value": 8}, {"addr": 9, "value": 9},
|
||||||
|
{"addr": 10, "value": 10}, {"addr": 11, "value": 11},
|
||||||
|
{"addr": 12, "value": 12}, {"addr": 13, "value": 13},
|
||||||
|
{"addr": 14, "value": 14}, {"addr": 15, "value": 15},
|
||||||
|
{"addr": 16, "value": 16}, {"addr": 17, "value": 17},
|
||||||
|
{"addr": 18, "value": 18}, {"addr": 19, "value": 19},
|
||||||
|
{"addr": 20, "value": 20}, {"addr": 21, "value": 21},
|
||||||
|
{"addr": 22, "value": 22}, {"addr": 23, "value": 23},
|
||||||
|
{"addr": 24, "value": 24}, {"addr": 25, "value": 25},
|
||||||
|
{"addr": 26, "value": 26}, {"addr": 27, "value": 27},
|
||||||
|
{"addr": 28, "value": 28}, {"addr": 29, "value": 29},
|
||||||
|
{"addr": 30, "value": 30}, {"addr": 31, "value": 31},
|
||||||
|
|
||||||
|
{"addr": 100, "value": 0,
|
||||||
|
"action": "increment",
|
||||||
|
"parameters": {"minval": 0, "maxval": 65535}},
|
||||||
|
|
||||||
|
{"addr": 200, "value": 0}, {"addr": 201, "value": 0},
|
||||||
|
{"addr": 202, "value": 0}, {"addr": 203, "value": 0},
|
||||||
|
{"addr": 204, "value": 0}, {"addr": 205, "value": 0},
|
||||||
|
{"addr": 206, "value": 0}, {"addr": 207, "value": 0},
|
||||||
|
{"addr": 208, "value": 0}, {"addr": 209, "value": 0}
|
||||||
|
],
|
||||||
|
|
||||||
|
"bits": [
|
||||||
|
{"addr": 1024, "value": 1}, {"addr": 1025, "value": 0},
|
||||||
|
{"addr": 1026, "value": 1}, {"addr": 1027, "value": 0},
|
||||||
|
{"addr": 1028, "value": 1}, {"addr": 1029, "value": 0},
|
||||||
|
{"addr": 1030, "value": 1}, {"addr": 1031, "value": 0},
|
||||||
|
{"addr": 1032, "value": 1}, {"addr": 1033, "value": 0},
|
||||||
|
{"addr": 1034, "value": 1}, {"addr": 1035, "value": 0},
|
||||||
|
{"addr": 1036, "value": 1}, {"addr": 1037, "value": 0},
|
||||||
|
{"addr": 1038, "value": 1}, {"addr": 1039, "value": 0},
|
||||||
|
{"addr": 1040, "value": 1}, {"addr": 1041, "value": 0},
|
||||||
|
{"addr": 1042, "value": 1}, {"addr": 1043, "value": 0},
|
||||||
|
{"addr": 1044, "value": 1}, {"addr": 1045, "value": 0},
|
||||||
|
{"addr": 1046, "value": 1}, {"addr": 1047, "value": 0},
|
||||||
|
{"addr": 1048, "value": 1}, {"addr": 1049, "value": 0},
|
||||||
|
{"addr": 1050, "value": 1}, {"addr": 1051, "value": 0},
|
||||||
|
{"addr": 1052, "value": 1}, {"addr": 1053, "value": 0},
|
||||||
|
{"addr": 1054, "value": 1}, {"addr": 1055, "value": 0},
|
||||||
|
|
||||||
|
{"addr": 1100, "value": 0}, {"addr": 1101, "value": 0},
|
||||||
|
{"addr": 1102, "value": 0}, {"addr": 1103, "value": 0},
|
||||||
|
{"addr": 1104, "value": 0}, {"addr": 1105, "value": 0},
|
||||||
|
{"addr": 1106, "value": 0}, {"addr": 1107, "value": 0},
|
||||||
|
{"addr": 1108, "value": 0}, {"addr": 1109, "value": 0}
|
||||||
|
],
|
||||||
|
|
||||||
|
"uint32": [],
|
||||||
|
"float32": [],
|
||||||
|
"string": [],
|
||||||
|
"repeat": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="ModbusPal\**\*" CopyToOutputDirectory="PreserveNewest"/>
|
<None Update="Pymodbus\**\*" CopyToOutputDirectory="PreserveNewest"/>
|
||||||
<None Update="DL205\**\*" CopyToOutputDirectory="PreserveNewest"/>
|
<None Update="DL205\**\*" CopyToOutputDirectory="PreserveNewest"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user