From a05b84858df1428ae72cd9a630b577746a16de70 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 18 Apr 2026 20:35:26 -0400 Subject: [PATCH] =?UTF-8?q?Phase=203=20PR=2043=20=E2=80=94=20Swap=20Modbus?= =?UTF-8?q?Pal=20to=20pymodbus=20for=20the=20integration-test=20simulator.?= =?UTF-8?q?=20Replaces=20the=20.xmpp=20profiles=20shipped=20in=20PR=2042?= =?UTF-8?q?=20with=20pymodbus=203.13.0=20ModbusSimulatorServer=20JSON=20co?= =?UTF-8?q?nfigs=20in=20tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationT?= =?UTF-8?q?ests/Pymodbus/.=20Substantive=20reasons=20for=20the=20swap=20(r?= =?UTF-8?q?ationale=20block=20in=20the=20test-plan=20doc):=20ModbusPal=201?= =?UTF-8?q?.6b=20is=20abandoned=20(last=20release=20~2019),=20Java=20GUI-o?= =?UTF-8?q?nly=20with=20no=20headless=20mode=20in=20the=20official=20JAR,?= =?UTF-8?q?=20and=20only=20exposes=202=20of=20the=204=20standard=20Modbus?= =?UTF-8?q?=20tables=20(holding=5Fregisters=20+=20coils=20=E2=80=94=20no?= =?UTF-8?q?=20input=5Fregisters,=20no=20discrete=5Finputs).=20pymodbus=20i?= =?UTF-8?q?s=20current=20stable,=20pure=20Python=20CLI=20(pip=20install=20?= =?UTF-8?q?pymodbus[simulator]=3D=3D3.13.0),=20exposes=20all=20four=20tabl?= =?UTF-8?q?es,=20has=20built-in=20declarative=20actions=20(increment=20/?= =?UTF-8?q?=20random=20/=20timestamp=20/=20uptime)=20for=20dynamic=20regis?= =?UTF-8?q?ters,=20supports=20custom=20Python=20actions=20for=20anything?= =?UTF-8?q?=20more=20complex,=20and=20ships=20an=20optional=20aiohttp-base?= =?UTF-8?q?d=20web=20UI=20/=20REST=20API=20for=20live=20inspection.=20Pip-?= =?UTF-8?q?installable=20on=20Windows;=20sidesteps=20the=20privileged-port?= =?UTF-8?q?=20admin=20requirement=20by=20defaulting=20to=20TCP=205020.=20M?= =?UTF-8?q?odbusSimulatorFixture=20default=20port=20bumped=20from=20502=20?= =?UTF-8?q?to=205020=20to=20match=20the=20pymodbus=20convention.=20Overrid?= =?UTF-8?q?e=20via=20MODBUS=5FSIM=5FENDPOINT=20for=20a=20real=20PLC=20on?= =?UTF-8?q?=20its=20native=20502.=20Skip-message=20updated=20to=20point=20?= =?UTF-8?q?at=20the=20new=20Pymodbus\serve.ps1=20wrapper=20instead=20of=20?= =?UTF-8?q?'start=20ModbusPal'.=20csproj=20=20rule=20swappe?= =?UTF-8?q?d=20from=20ModbusPal/**=20to=20Pymodbus/**=20so=20the=20new=20J?= =?UTF-8?q?SON=20profiles=20+=20serve.ps1=20+=20README=20copy=20to=20test-?= =?UTF-8?q?output=20as=20PreserveNewest.=20standard.json=20=E2=80=94=20gen?= =?UTF-8?q?eric=20Modbus=20TCP=20server,=20slave=20id=201,=20port=205020,?= =?UTF-8?q?=20shared=20blocks=3Dfalse=20(independent=20coils=20+=20HR=20ad?= =?UTF-8?q?dress=20spaces,=20more=20textbook-PLC-like).=20HR[0..31]=20seed?= =?UTF-8?q?ed=20with=20address-as-value=20via=20per-register=20uint16=20en?= =?UTF-8?q?tries,=20HR[100]=20auto-increments=20via=20the=20built-in=20inc?= =?UTF-8?q?rement=20action=20with=20parameters=20minval=3D0/maxval=3D65535?= =?UTF-8?q?=20(drives=20subscribe-and-receive=20integration=20tests=20so?= =?UTF-8?q?=20they=20have=20a=20register=20that=20ticks=20without=20a=20wr?= =?UTF-8?q?ite=20=E2=80=94=20pymodbus's=20increment=20ticks=20per-access?= =?UTF-8?q?=20not=20wall-clock,=20which=20is=20good=20enough=20for=20a=202?= =?UTF-8?q?50ms-poll=20test),=20HR[200..209]=20scratch=20range=20left=20at?= =?UTF-8?q?=200=20for=20write=20tests,=20coils=200..31=20alternating,=20co?= =?UTF-8?q?ils=20100..109=20scratch.=20write=20list=20covers=200..1023=20s?= =?UTF-8?q?o=20any=20test=20address=20is=20mutable.=20dl205.json=20?= =?UTF-8?q?=E2=80=94=20AutomationDirect=20DirectLOGIC=20DL205/DL260=20quir?= =?UTF-8?q?k=20simulator,=20slave=20id=201,=20port=205020,=20shared=20bloc?= =?UTF-8?q?ks=3Dtrue=20(matches=20DL=20series=20memory=20model=20where=20c?= =?UTF-8?q?oils/DI/HR=20overlay=20the=20same=20word=20address=20space).=20?= =?UTF-8?q?Each=20quirky=20register=20seeded=20with=20the=20pre-computed?= =?UTF-8?q?=20raw=20uint16=20value=20documented=20in=20docs/v2/dl205.md,?= =?UTF-8?q?=20with=20an=20inline=20=5Fquirk=20JSON-comment=20naming=20the?= =?UTF-8?q?=20behavior=20so=20future-me=20reading=20the=20file=20knows=20w?= =?UTF-8?q?hy=20HR[1040]=3D25928=20means=20'H'=20lo=20/=20'e'=20hi=20(the?= =?UTF-8?q?=20user's=20headline=20string-byte-order=20finding).=20Encoded?= =?UTF-8?q?=20quirks:=20V0=20marker=20at=20HR[0]=3D0xCAFE;=20V2000=20at=20?= =?UTF-8?q?HR[1024]=3D0x2000;=20V40400=20at=20HR[8448]=3D0x4040;=20'Hello'?= =?UTF-8?q?=20string=20at=20HR[1040..1042]=20first-char-low-byte;=20Float3?= =?UTF-8?q?2=201.5f=20at=20HR[1056..1057]=20in=20CDAB=20word=20order=20(lo?= =?UTF-8?q?w=20word=20first);=20BCD=20register=20at=20HR[1072]=3D0x1234;?= =?UTF-8?q?=20FC03-128-cap=20block=20at=20HR[1280..1407];=20Y0/C0=20coil?= =?UTF-8?q?=20markers=20at=202048/3072;=20scratch=20C-relays=20at=204000..?= =?UTF-8?q?4007.=20serve.ps1=20wrapper=20=E2=80=94=20pwsh=20script=20with?= =?UTF-8?q?=20a=20-Profile=20{standard|dl205}=20parameter=20switch.=20Vali?= =?UTF-8?q?dates=20pymodbus.simulator=20is=20on=20PATH=20(clearer=20messag?= =?UTF-8?q?e=20than=20the=20raw=20CommandNotFoundException),=20validates?= =?UTF-8?q?=20the=20profile=20JSON=20exists,=20builds=20the=20right=20--mo?= =?UTF-8?q?dbus=5Fserver/--modbus=5Fdevice/--json=5Ffile/--http=5Fport=20a?= =?UTF-8?q?rg=20list,=20and=20execs=20pymodbus.simulator=20in=20the=20fore?= =?UTF-8?q?ground.=20-HttpPort=200=20disables=20the=20web=20UI.=20Foregrou?= =?UTF-8?q?nd=20exec=20lets=20the=20operator=20Ctrl+C=20to=20stop=20withou?= =?UTF-8?q?t=20an=20extra=20control=20script.=20README.md=20fully=20rewrit?= =?UTF-8?q?ten=20for=20pymodbus:=20install=20command=20(pip=20install=20'p?= =?UTF-8?q?ymodbus[simulator]=3D=3D3.13.0'=20=E2=80=94=20pinned=20for=20re?= =?UTF-8?q?producibility,=20[simulator]=20extra=20pulls=20aiohttp),=20per-?= =?UTF-8?q?profile=20reference=20tables,=20the=20same=20DL205=20quirk=20?= =?UTF-8?q?=E2=86=92=20register=20table=20from=20PR=2042=20but=20adjusted?= =?UTF-8?q?=20for=20pymodbus=20paths,=20what's-NEW-vs-ModbusPal=20section?= =?UTF-8?q?=20(all=20four=20tables,=20raw=20uint16=20seeding,=20declarativ?= =?UTF-8?q?e=20actions,=20custom=20Python=20action=20modules,=20headless,?= =?UTF-8?q?=20web=20UI,=20maintained),=20trade-offs=20section=20(float32-a?= =?UTF-8?q?s-two-uint16s=20for=20explicit=20CDAB=20control,=20increment=20?= =?UTF-8?q?ticks=20per-access=20not=20wall-clock,=20shared-blocks=20mode?= =?UTF-8?q?=20for=20DL205=20vs=20separate=20for=20Standard),=20file-format?= =?UTF-8?q?=20quick=20reference=20for=20hand-authoring=20more=20profiles.?= =?UTF-8?q?=20References=20pinned=20to=20the=20pymodbus=20readthedocs=20si?= =?UTF-8?q?mulator/config=20+=20REST=20API=20pages.=20docs/v2/modbus-test-?= =?UTF-8?q?plan.md=20harness=20section=20rewritten=20with=20the=20swap=20r?= =?UTF-8?q?ationale;=20PR-history=20list=20updated=20to=20mark=20PR=2042?= =?UTF-8?q?=20SUPERSEDED=20by=20PR=2043=20and=20call=20out=20PR=2044+=20as?= =?UTF-8?q?=20the=20per-quirk=20implementation=20track.=20Test-conventions?= =?UTF-8?q?=20bullet=20about=20'don't=20depend=20on=20ModbusPal=20state=20?= =?UTF-8?q?between=20tests'=20generalized=20to=20'don't=20depend=20on=20si?= =?UTF-8?q?mulator=20state'=20and=20a=20note=20added=20that=20pymodbus's?= =?UTF-8?q?=20REST=20API=20can=20reset=20state=20between=20facts=20if=20a?= =?UTF-8?q?=20test=20ever=20needs=20it.=20DL205Profile.cs=20and=20DL205Smo?= =?UTF-8?q?keTests.cs=20xml-doc=20updated=20to=20reference=20pymodbus=20/?= =?UTF-8?q?=20dl205.json=20instead=20of=20ModbusPal=20/=20DL205.xmpp.=20Fu?= =?UTF-8?q?nctional=20validation=20deferred=20=E2=80=94=20Python=20isn't?= =?UTF-8?q?=20installed=20on=20this=20dev=20box=20(winget=20search=20retur?= =?UTF-8?q?ned=20no=20matches=20for=20Python.Python.3=20exact).=20JSON=20p?= =?UTF-8?q?arses=20structurally=20(PowerShell=20ConvertFrom-Json=20clean?= =?UTF-8?q?=20on=20both=20files),=20build=20clean,=20.json=20+=20serve.ps1?= =?UTF-8?q?=20+=20README=20all=20copy=20to=20test-output=20as=20expected.?= =?UTF-8?q?=20User=20installs=20pymodbus=20when=20they=20want=20to=20actua?= =?UTF-8?q?lly=20run=20the=20simulator=20end-to-end;=20if=20pymodbus=20rej?= =?UTF-8?q?ects=20the=20config=20the=20README's=20reference=20link=20to=20?= =?UTF-8?q?pymodbus's=20simulator/config=20schema=20doc=20is=20the=20right?= =?UTF-8?q?=20next=20stop.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/v2/modbus-test-plan.md | 63 +++--- .../DL205/DL205Profile.cs | 10 +- .../ModbusPal/DL205.xmpp | 192 ------------------ .../ModbusPal/README.md | 105 ---------- .../ModbusPal/Standard.xmpp | 166 --------------- .../ModbusSimulatorFixture.cs | 17 +- .../Pymodbus/README.md | 163 +++++++++++++++ .../Pymodbus/dl205.json | 98 +++++++++ .../Pymodbus/serve.ps1 | 60 ++++++ .../Pymodbus/standard.json | 81 ++++++++ ...pcUa.Driver.Modbus.IntegrationTests.csproj | 2 +- 11 files changed, 459 insertions(+), 498 deletions(-) delete mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/DL205.xmpp delete mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/README.md delete mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/Standard.xmpp create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/README.md create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/serve.ps1 create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/standard.json diff --git a/docs/v2/modbus-test-plan.md b/docs/v2/modbus-test-plan.md index 8009b43..58abee0 100644 --- a/docs/v2/modbus-test-plan.md +++ b/docs/v2/modbus-test-plan.md @@ -13,22 +13,30 @@ confirmed DL205 quirk lands in a follow-up PR as a named test in that project. ## Harness -**Chosen simulator: ModbusPal** (Java, scriptable). Rationale: -- Scriptable enough to mimic device-specific behaviors (non-standard register - layouts, custom exception codes, intentional response delays). -- Runs locally, no CI dependency. Tests skip when `localhost:502` (or the configured - simulator endpoint) isn't reachable. -- Free + long-maintained — physical PLC bench is unavailable in most dev - environments, and renting cloud PLCs isn't worth the per-test cost. +**Chosen simulator: pymodbus 3.13.0** (`pip install 'pymodbus[simulator]==3.13.0'`). +Replaced ModbusPal in PR 43 — see `tests/.../Pymodbus/README.md` for the +trade-off rationale. Headline reasons: -**Setup pattern** (not yet codified in a script — will land alongside the integration -test project): -1. Install ModbusPal, load the per-device `.xmpp` profile from - `tests/Driver.Modbus.IntegrationTests/ModbusPal/` (TBD directory). -2. Start the simulator listening on `localhost:502` (or override via - `MODBUS_SIM_ENDPOINT` env var). -3. `dotnet test` the integration project — tests auto-skip when the endpoint is - unreachable, so forgetting to start the simulator doesn't wedge CI. +- **Headless** pure-Python CLI; no Java GUI, runs cleanly on a CI runner. +- **Maintained** — current stable 3.13.0; ModbusPal 1.6b is abandoned. +- **All four standard tables** (HR, IR, coils, DI) configurable; ModbusPal + 1.6b only exposed HR + coils. +- **Built-in actions** (`increment`, `random`, `timestamp`, `uptime`) + + optional custom-Python actions for declarative dynamic behaviors. +- **Per-register raw uint16 seeding** — encoding the DL205 string-byte-order + / 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 @@ -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 deliberately not used here — its value is speed + determinism, which doesn't help reproduce device-specific issues. -- **Don't depend on ModbusPal state between tests.** Each test resets the +- **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 "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 - **PR 30 — Integration test project + DL205 profile scaffold** — **DONE**. Shipped `tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests` with `ModbusSimulatorFixture` (TCP-probe, skips with a clear `SkipReason` when the - endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub — one - writable holding register at address 100), and `DL205/DL205SmokeTests.cs` - (write-then-read round-trip). `ModbusPal/` directory holds the README - pointing at the to-be-committed `DL205.xmpp` profile. -- **PR 31+**: one PR per confirmed DL205 quirk, landing the named test + any - driver-side adjustment (e.g., retry on dropped TxId) needed to pass it. Drop - the `DL205.xmpp` profile into `ModbusPal/` alongside the first quirk PR. + endpoint is unreachable), `DL205/DL205Profile.cs` (tag map stub), and + `DL205/DL205SmokeTests.cs` (write-then-read round-trip). +- **PR 41 — DL205 quirk catalog doc** — **DONE**. `docs/v2/dl205.md` + documents every DL205/DL260 Modbus divergence with primary-source citations. +- **PR 42 — ModbusPal `.xmpp` profiles** — **SUPERSEDED by PR 43**. Replaced + with pymodbus JSON because ModbusPal 1.6b is abandoned, GUI-only, and only + 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`. diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs index 97b5cc3..4b26a4c 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/DL205/DL205Profile.cs @@ -1,15 +1,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.DL205; /// -/// Tag map for the AutomationDirect DL205 device class. Mirrors what the ModbusPal -/// .xmpp profile in ModbusPal/DL205.xmpp exposes (or the real PLC, when +/// Tag map for the AutomationDirect DL205 device class. Mirrors what the pymodbus +/// dl205.json profile in Pymodbus/dl205.json exposes (or the real PLC, when /// is pointed at one). /// /// /// 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 /// access, etc.) will land in their own test classes alongside this profile as the user -/// validates each behavior in ModbusPal; see docs/v2/modbus-test-plan.md §per-device +/// validates each behavior in pymodbus; see docs/v2/modbus-test-plan.md §per-device /// quirk catalog for the checklist. /// public static class DL205Profile @@ -18,8 +18,8 @@ public static class DL205Profile /// register-zero quirk (pending confirmation) — see modbus-test-plan.md. public const ushort SmokeHoldingRegister = 100; - /// Expected value the ModbusPal profile seeds into register 100. When running - /// against a real DL205 (or a ModbusPal profile where this register is writable), the smoke + /// Expected value the pymodbus profile seeds into register 100. When running + /// against a real DL205 (or a pymodbus profile where this register is writable), the smoke /// test seeds this value first, then reads it back. public const short SmokeHoldingValue = 1234; diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/DL205.xmpp b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/DL205.xmpp deleted file mode 100644 index 46ac26f..0000000 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/DL205.xmpp +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/README.md b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/README.md deleted file mode 100644 index 6d062eb..0000000 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/README.md +++ /dev/null @@ -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 -`` with three children: -- `` — internal id counter (start at 100+) -- `` — `` for TCP listen, plus a `` placeholder -- One or more `` containing `` (``), `` (``), `` - -Per-register `` 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. diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/Standard.xmpp b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/Standard.xmpp deleted file mode 100644 index ec89678..0000000 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusPal/Standard.xmpp +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs index 5f55e2d..7e34236 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ModbusSimulatorFixture.cs @@ -3,8 +3,9 @@ using System.Net.Sockets; namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests; /// -/// Reachability probe for a Modbus TCP simulator (ModbusPal or a real PLC). Parses -/// MODBUS_SIM_ENDPOINT (default localhost:502) and TCP-connects once at +/// Reachability probe for a Modbus TCP simulator (pymodbus-driven, see +/// Pymodbus/serve.ps1) or a real PLC. Parses +/// MODBUS_SIM_ENDPOINT (default localhost:5020 per PR 43) and TCP-connects once at /// fixture construction. Each test checks and calls /// Assert.Skip 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 @@ -25,7 +26,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests; /// 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"; public string Host { get; } @@ -46,13 +51,15 @@ public sealed class ModbusSimulatorFixture : IAsyncDisposable if (!task.Wait(TimeSpan.FromSeconds(2)) || !client.Connected) { 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) { 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."; } } diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/README.md b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/README.md new file mode 100644 index 0000000..98352c4 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/README.md @@ -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": { + "": { + "comm": "tcp", + "host": "0.0.0.0", + "port": 5020, + "framer": "socket", + "device_id": 1 + } + }, + "device_list": { + "": { + "setup": { + "co size": N, "di size": N, "hr size": N, "ir size": N, + "shared blocks": false, + "type exception": false, + "defaults": { "value": {...}, "action": {...} } + }, + "invalid": [], + "write": [[, ]], + "bits": [{"addr": N, "value": 0|1}], + "uint16": [{"addr": N, "value": <0..65535>, "action"?: "increment", "parameters"?: {...}}], + "uint32": [{"addr": N, "value": }], + "float32": [{"addr": N, "value": }], + "string": [{"addr": N, "value": ""}], + "repeat": [] + } + } +} +``` + +The CLI args `--modbus_server --modbus_device ` +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_` test naming convention diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json new file mode 100644 index 0000000..0bd67cb --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/dl205.json @@ -0,0 +1,98 @@ +{ + "_comment": "DL205.json — AutomationDirect DirectLOGIC DL205/DL260 quirk simulator. Models each behavior in docs/v2/dl205.md as concrete register values so DL205_ integration tests can assert against this profile WITHOUT a live PLC. Loaded by `pymodbus.simulator`. See ../README.md. Per-quirk address layout matches the table in dl205.md exactly. `shared blocks: true` matches DL series behavior — coils/HR overlay the same word address space (a Y-output is both a discrete bit AND part of a system V-memory register).", + + "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, 16383] + ], + + "_comment_uint16": "Holding-register seeds. Every quirky value is a raw uint16 with the byte math worked out in dl205.md so the simulator serves it verbatim — pymodbus does NOT decode strings, BCD, or float-CDAB on its own; that's the driver's job.", + + "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": "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. Real DL205/DL260 FC03 caps at 128 registers (above spec's 125). HR[1280..1407] is 128 contiguous registers; rest of block defaults to 0.", + "addr": 1280, "value": 0}, + {"addr": 1281, "value": 1}, + {"addr": 1282, "value": 2}, + {"addr": 1343, "value": 63, "_marker": "FC03Block_mid"}, + {"addr": 1407, "value": 127, "_marker": "FC03Block_last"} + ], + + "_comment_bits": "Coils — Y outputs at 2048+, C relays at 3072+, scratch C at 4000-4007 for write tests. DL260 X inputs would be at discrete-input addresses 0..511 but pymodbus's shared-blocks mode + same-table-as-coils means those would conflict with HR seeds; FC02 tests against this profile use a separate discrete-input block instead — that's why `di size` is large but the X-input markers live in `bits` only when `shared blocks=false`. Document trade-off in README.", + + "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": [] + } + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/serve.ps1 b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/serve.ps1 new file mode 100644 index 0000000..6cb9195 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/serve.ps1 @@ -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 diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/standard.json b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/standard.json new file mode 100644 index 0000000..2738d6f --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/Pymodbus/standard.json @@ -0,0 +1,81 @@ +{ + "_comment": "Standard.json — generic Modbus TCP server for the integration suite. Loaded by `pymodbus.simulator`. See ../README.md for the launch command. Holding registers 0..31 are seeded with their address as value (HR[5]=5) for easy mental-map diagnostics. HR[100] auto-increments via pymodbus's built-in `increment` action so subscribe-and-receive integration tests have a register that ticks without a write. HR[200..209] is a scratch range left at 0 for write-roundtrip tests. Coils 0..31 alternate on/off (even=on); coils 100..109 scratch.", + + "server_list": { + "srv": { + "comm": "tcp", + "host": "0.0.0.0", + "port": 5020, + "framer": "socket", + "device_id": 1 + } + }, + + "device_list": { + "dev": { + "setup": { + "co size": 1024, + "di size": 1024, + "hr size": 1024, + "ir size": 1024, + "shared blocks": false, + "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, 1023] + ], + + "bits": [ + {"addr": 0, "value": 1}, {"addr": 1, "value": 0}, + {"addr": 2, "value": 1}, {"addr": 3, "value": 0}, + {"addr": 4, "value": 1}, {"addr": 5, "value": 0}, + {"addr": 6, "value": 1}, {"addr": 7, "value": 0}, + {"addr": 8, "value": 1}, {"addr": 9, "value": 0}, + {"addr": 10, "value": 1}, {"addr": 11, "value": 0}, + {"addr": 12, "value": 1}, {"addr": 13, "value": 0}, + {"addr": 14, "value": 1}, {"addr": 15, "value": 0}, + {"addr": 16, "value": 1}, {"addr": 17, "value": 0}, + {"addr": 18, "value": 1}, {"addr": 19, "value": 0}, + {"addr": 20, "value": 1}, {"addr": 21, "value": 0}, + {"addr": 22, "value": 1}, {"addr": 23, "value": 0}, + {"addr": 24, "value": 1}, {"addr": 25, "value": 0}, + {"addr": 26, "value": 1}, {"addr": 27, "value": 0}, + {"addr": 28, "value": 1}, {"addr": 29, "value": 0}, + {"addr": 30, "value": 1}, {"addr": 31, "value": 0} + ], + + "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}} + ], + + "uint32": [], + "float32": [], + "string": [], + "repeat": [] + } + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj index 0d192b5..0b0dad0 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj @@ -24,7 +24,7 @@ - + -- 2.49.1