# 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