23 KiB
wago-modbustcp-simulator-py
WAGO 750-362 Modbus TCP bus coupler simulator for autonomous PLC FAT testing.
The simulator replaces a physical WAGO I/O node during Factory Acceptance Tests: the PLC under test connects via Modbus TCP and talks to a virtual node whose I/O behavior is driven by scripted scenarios.
Features
- Faithful WAGO 750-362 register map (manual §11.2.4): process image
words/bits with the exact mapped/unmapped areas and exception behavior,
read-back mirrors (+
0x0200/ +0x1000), configuration registers0x1000–0x2FFF, constants0x2000–0x2008(byte-order check0x1234). - Function codes FC1, FC2, FC3, FC4, FC5, FC6, FC11, FC15, FC16, FC22, FC23 with the device limits (2000/800 bits, 125/100 registers).
- Modbus watchdog (
0x1000–0x100B), statistics block (0x1029), response-delay injection (0x1037), module diagnosis (0x1050). - Virtual node built from a YAML module list (WAGO 750 catalog subset, extensible).
- Scenario engine: YAML scenarios with aliases, macros, ramps, pulses, waits, and PASS/FAIL assertions with timeouts; state machines and Python action plugins for complex plant models.
- Two I/O addressing schemes for aliases: module-based —
{ type: di, module: 2, channel: 1 }=DI2.1(module slot and channel both 0-based) — and pool-based —{ type: di, channel: 3 }(1-based index across the node's DI pool). Both may be mixed. - Bench mode (requested, planned — spec §9): run the simulator
without a scenario (
wago-sim serve --node ...already works); planned additions:--set DI0.1=ONstartup seeding, an interactive console monitor with live I/O table, and runtime set/pulse/watch commands. - Web UI (requested, planned — spec §10): browser front-end to
overlook and manipulate the virtual node's I/O — live channel tables
with
DI0.1-style addresses, toggle/force/pulse/ramp actions, watchdog/statistics/diagnostics views with fault injection, and multi-client consistency for collaborative FAT benches. - Runtime device configuration (requested, planned — spec §11):
WBM/DIP-switch parity — module "plug/unplug" at runtime with
process-image rebuild (SAVE CONFIG / KBUS
0x102Bsemantics), address-selection DIP-switch emulation (host ID 1–254, 0 = stored config, 255 = DHCP), WBM-equivalent pages (Port/Ethernet, Modbus settings, Modbus Mapping, I/O Config), and device-state persistence with factory reset. - Long-duration (continuous) simulation (requested, planned — see
docs/spec/software-spec.md§8): endurance/soak testing over days or weeks of simulated time. Designated inputs are driven from streaming CSV datasets (historian exports, demand/operator profiles, seamlessrepeat,time_factoracceleration), while degradation models age plant parameters (e.g., pump capacity) from wear counters — exercising the PLC's compensation, maintenance-alarm and service-recovery logic. Continuousinvariantsandrate_limitsproduce bounded, episode-based reports for unattended runs. - ≤ 15 concurrent Modbus TCP connections, idle-timeout handling.
- Service / container deployment — run as infrastructure:
Dockerfile (non-root, health check, ~197 MB), docker-compose service
(restart policy, log rotation, read-only config mounts), systemd unit
example, env-var configuration (
WAGO_SIM_NODE/HOST/PORT), and graceful SIGTERM shutdown (docker stopcompletes in < 1 s).
Install (dev)
pip install -e .[dev]
Usage
Run the Modbus TCP server against a node configuration:
wago-sim serve --node config/pump_node.yaml --port 502
Validate a scenario:
wago-sim validate --scenario scenarios/pump_station_fat.yaml
Run a scenario headless and write reports:
wago-sim run --node config/pump_node.yaml \
--scenario scenarios/pump_station_fat.yaml \
--report-jsonl out/report.jsonl --report-csv out/report.csv
Deployment (service / container)
The simulator runs as a long-lived service. Configuration comes from
environment variables — WAGO_SIM_NODE, WAGO_SIM_HOST, WAGO_SIM_PORT —
so no CLI arguments are needed in the container command.
Docker:
docker build -t wago-sim .
docker run -d --name wago-sim -p 5020:502 \
-v ./config:/app/config:ro -v ./scenarios:/app/scenarios:ro \
-v ./plugins:/app/plugins:ro -v ./out:/app/out \
-e WAGO_SIM_NODE=config/pump_node.yaml \
wago-sim
Compose (restart policy, health check, log rotation included):
docker compose up -d # start
docker compose logs -f wago-sim
docker compose down # graceful SIGTERM shutdown
Bare metal / VM (systemd; unit file in deploy/wago-sim.service):
sudo cp deploy/wago-sim.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now wago-sim
journalctl -u wago-sim -f
Notes:
- Host port 5020 by default; use 502 if your bench expects it and your
host allows binding privileged ports (the container runs non-root;
the systemd unit grants
CAP_NET_BIND_SERVICEto a dedicated user). docker stopsends SIGTERM; the server closes all client connections and the listening socket cleanly (< 1 s) — useful for testing the PLC's fieldbus-failure reaction deliberately.- Reports from headless
runinvocations go to the mountedout/.
Verbose mode (request tracing)
Run with -v / --verbose (or WAGO_SIM_VERBOSE=1 for containers) to
trace every Modbus transaction in plain text — with the WAGO 750-362
register-map meaning of every address annotated:
$ wago-sim serve --node config/pump_node.yaml -v
REQ txn=0x0001 unit=1 FC03 Read Holding Registers
addr=0x2002 count=1 [config/constant regs 0x2002]
RSP txn=0x0001 unit=1 FC03 -> 1 regs: [0x1234]
REQ txn=0x0002 unit=1 FC06 Write Single Register
addr=0x0000 value=0x1234 [output area 1 (%QW0)]
RSP txn=0x0002 unit=1 FC06 -> echo
EXC txn=0x0006 unit=1 FC03 -> code=0x02 Illegal Data Address
RAW txn=0x0007 unit=1 PDU[3]: 07 00 00
Traces go to the standard log at DEBUG level, so they appear in the
terminal, in docker logs, and in journalctl alike. Read vs write
mappings are annotated correctly per the manual's Tables 119/120
(e.g. word address 0x0000 reads as %IW0 but writes to %QW0).
Web UI
serve starts a browser UI alongside the Modbus server (spec §10):
wago-sim serve --node config/pump_node.yaml --port 5020
# Web UI on http://127.0.0.1:8080 — default login: admin / simu
- Overview: realistic DIN-rail view of the coupler + modules with per-channel LEDs for digital I/O and bar/numeric indicators for analog values; updates live via WebSocket — including changes made by the PLC over Modbus.
- Manipulation: click a DI/DO LED to toggle it (field side), or force DO/AO master-side; AI values can be set directly. All writes go through the simulator's internal data paths — never via Modbus TCP — while keeping read-back mirrors and statistics consistent.
- Diagnostics: watchdog status, statistics with reset, and LED/module fault injection for FAT fault cases.
Configuration options: --no-web, --web-port <n> or env
WAGO_SIM_WEB_PORT (default 8080); credentials via
WAGO_SIM_WEB_USER / WAGO_SIM_WEB_PASSWORD (defaults
admin / simu); WAGO_SIM_WEB_READONLY=1 for observation-only
benches (all mutating endpoints answer 403). Plain HTTP — keep the
port inside the bench network.
Documentation
docs/spec/software-spec.md— the software specification (requirements, architecture, scenario DSL, acceptance criteria, and the long-duration simulation feature request in §8).docs/m07500362_xxxxxxxx_0en.pdf— WAGO 750-362 reference manual.
Testing
pytest
TODO
Work remaining, tracked against the software specification
(docs/spec/software-spec.md). Completed items are checked and grouped
first; open items follow, grouped by spec phase, with the spec section or
requirement ID for each item in parentheses.
Completed — framework phase (spec §3 + §4 core, 141 tests passing)
- MBAP + PDU codecs: function codes FC1, FC2, FC3, FC4, FC5, FC6, FC11, FC15, FC16, FC22, FC23 with the 750-362 device limits (2000/800 bits, 125/100 registers) and exception codes (§3.2).
- WAGO register map (§3.3): word/bit process-image areas with the
exact mapped/unmapped ranges, illegal-address exceptions, output
read-back mirrors (+
0x0200/ +0x1000), digital channel-number bit addressing, constants0x2000–0x2008(incl. byte-order check0x2002=0x1234). - Configuration/diagnostic/firmware registers (§3.5): image sizes
0x1022–0x1025, statistics0x1029, connections0x102A, idle timeout0x1030, MAC-ID0x1031, response delay0x1037, module diagnosis0x1050, LED error0x1020/0x1021. - Modbus watchdog (§3.4): registers
0x1000–0x100B, Standard and Alternative modes, trigger/masks/stop sequences, expiry clears outputs, address-aware0x04lock in Standard mode, optional TCP close on timeout. - Fault-injection paths (§3.6): response-delay injection, connection refusal beyond 15, illegal-address probing responses, diagnosis/LED registers scriptable.
- Virtual node built from a YAML module list (13-module WAGO 750 catalog subset; complex-first, bit-byte-packed image layout per manual §7.3.2.1).
- Scenario DSL (§4): named aliases with
scale/offset, macros with parameters (inline expansion, blocking waits inside macros), sequence stepsset/reset/toggle/pulse/ramp/wait/assert/report, rising-edge detection on all aliases. - Two I/O addressing schemes for scenario aliases (FR-4.3.11):
module-based (
{ type: di, module: 2, channel: 1 }=DI2.1, module slot and channel 0-based) and pool-based (channel:= 1-based index within the node's kind pool); kind/module/channel validation with clear errors; both verified by tests. - Assertions with
within:timeouts → PASS/FAIL verdicts with sim-time stamps (FR-4.3.5). - Python action/plant-model plugins: per-tick models and step
actions (FR-4.3.8), e.g.
plugins/pump_plant.py. - Deterministic tick-driven engine: sim-time decoupled from wall-clock (FR-4.3.9 core).
- Service / container deployment (spec §12): env-var config
(
WAGO_SIM_NODE/HOST/PORT, CLI args override), graceful SIGINT/SIGTERM shutdown viaserver.stop(), Dockerfile (python:3.12-slim, non-root user, TCP health check), docker-compose service (restart policy, resource limits, log rotation, read-only config mounts), systemd unit example with hardening; verified end-to-end (build, Modbus round-trip,docker stopin 0.34 s); CLI env tests added. - CLI:
wago-sim serve / run / validate. - Reports: console verdict summary, JSONL and CSV export
(
--report-jsonl/--report-csv). - asyncio Modbus TCP server: ≤ 15 concurrent connections, idle connection reaper, response-delay task.
- Test suite: 178 tests incl. real-socket server tests,
pymodbusinterop, CLI env-config and bad-argument tests, and plain-text trace tests; spec §7 acceptance criteria 1–7 covered. - Clean CLI error handling: invalid arguments (unknown command/
options, non-numeric or out-of-range
--port, badWAGO_SIM_PORT), missing/unparsable node and scenario files, and unwritable report paths all produce a one-linewago-sim: error: …on stderr with exit code 2 — never a Python traceback; missing report directories are auto-created; internal errors keep the full traceback for debugging (NFR-6.4 operational goal).
Requested — multi-vendor & multi-protocol (spec §13 + §14, not yet scheduled)
- Multi-vendor device profiles (spec §13): extract the per-device
facts (function codes/limits, connection limit, address maps,
mirrors, config/watchdog/statistics blocks, trace annotations)
into a device profile; other WAGO 750 Modbus couplers and other
brands' Modbus TCP I/O heads become data + a profile module
(FR-13.2.1–13.2.7); scenario engine, DSL, plugins and reports
stay device-agnostic;
device:key in node config with the 750-362 as default; validate rejects unknown devices and cross-brand module mixes cleanly. - Additional fieldbus protocols (spec §14): protocol bindings for ABB AC800M-style benches — PROFINET IO device simulation (RT cyclic data exchange, DCP, record data; FR-14.2.2) and COMLI over TCP as used between AC800M and S800/S900 I/O via CI51x/CI522 (FR-14.2.3) — reusing the same process image, scenario engine and reporting (FR-14.2.1, FR-14.2.4); per- protocol tracing, diagnostics and fault injection (FR-14.2.5– 14.2.6); scoping questions to answer before planning (§14.3).
Web UI (spec §10) — in implementation
- Spec §10.3–10.6 updated with binding implementation decisions:
FastAPI/Uvicorn in-process on the Modbus server's asyncio loop;
vanilla single-page front-end (no build step) with realistic
DIN-rail graphics and per-channel LEDs; session-cookie auth with
default credentials
admin/simu(env-overridable, read-only mode); backend API uses only internal memory handlers (WagoDatastore,VirtualNode, registers, watchdog, statistics) — never Modbus TCP; WebSocket live updates; milestones M1–M6. - M1: webui package (
src/wago_sim/webui/:app.py,auth.py,state.py,api.py, static SPA); session auth with scrypt hashing, lockout after 5 failed logins, read-only mode (WAGO_SIM_WEB_READONLY);GET /api/nodewithDI0.1-style and Modbus addresses; CLI wiring--web/--no-web,--web-port, envWAGO_SIM_WEB_USER/PASSWORD/READONLY/ NO_WEB/WEB_PORT; login page + SPA shell. - M2: node overview page — DIN-rail strip with coupler block
(MS/NS/BF/SF LEDs), module bezels with article numbers,
per-channel LEDs for DI/DO, bar+numeric indicators for AI/AO,
live values via WebSocket (
/api/ws, 10 Hz delta push),GET /api/valuessnapshot. - M3: manipulation — DI/DO toggle (field side via
set_digital_input_channel, master side via internalwrite_bits/write_wordsdispatch so mirrors/statistics stay consistent), AI value entry (raw counts), AO force; unknown-channel → 404, kind-mismatch → 400, read-only → 403. - M4: diagnostics — watchdog status card (0x1006/0x100A), statistics table (0x1029 words) with reset, LED-error (0x1020/0x1021) and module-diagnosis (0x1050) fault injection and clear; SF LED on the coupler reflects injected LED errors.
- M5: configuration — coupler settings forms (watchdog time/masks/type/close-on-timeout via 0x1000–0x100B, response delay 0x1037, idle timeout 0x1030) with the device's own validation surfaced (writes refused while the watchdog runs → field error); module list editor over the 13-article catalog with SAVE CONFIG → live process-image rebuild (
rebuild_node: datastore survives, values clear like a KBUS reset, 0x1022–0x1025 and 0x2030+ updated) + persistence to the node-config YAML via a comment-preserving custom emitter. - M5a: channel names for every module channel (FR-10.2.11) —
node-config persisted (
channel_names:mapping, I/O code → name), survive module saves and rebuilds, shown in the overview list/tooltips and a per-channel name editor; configuration import/export as JSON (FR-10.2.12) — modules, channel names, coupler settings in one file, atomic validated import with stale-name-code dropping; split overview (FR-10.2.13) — DIN-rail graphic on top, live scrollable channel list below with I/O code, configured name, and a switch (DI/DO) / numeric field (AI/AO) for display + manipulation. - M6: scenario run view + alias-ownership rejection (FR-10.2.6, FR-10.2.9) — deferred.
- Tests: 38 web-UI tests (
tests/test_webui.py) via httpxASGITransport(in-process, no socket) + live verification: browser SAVE CONFIG rebuilds the node and persists YAML; Modbus master reads the module description (0x2030) correctly after the loader-overlap register fix; channel-name save + list sync; export/import round-trip with a Modbus read after the rebuild. Full suite: 216 passing.
Framework gaps (spec §4 — Scenario & Macro Engine)
- State machines:
states:with entry actions, transitions on input/output edges or conditions, and guards (FR-4.3.2) — currently only the time-orderedsequence:is implemented. - Actions
noise,sin,holdon simulated inputs (FR-4.3.4) —set/reset/pulse/toggle/rampare done. wait_edgestep is accepted but not yet blocking — implement edge-wait semantics in the engine loop.- Assertions on statistics registers, e.g. "watchdog expired", "BadAddress counter > 0" (FR-4.3.6).
- Determinism guard: seed/record plugin RNG state so identical runs
produce identical reports (FR-4.3.9, NFR-6.3) — needed once
noise/sinor randomizing plugins exist. - Multi-node scenarios — several couplers in one scenario (FR-4.3.10).
Phase 2 — Module catalog expansion & bench mode (spec §9)
- Analog module variants, counter, pulse-width, SSI, incremental encoder, serial interface, DALI/DSI modules with their process-data layouts (spec §12.2 of the WAGO manual).
- Mailbox parameterization traffic beyond process data.
- Bench mode tooling (§9):
--set DI0.1=ONstartup seeding, interactive console monitor (--monitor) with live channel table and change highlighting (FR-9.2.2–9.2.3). - Console runtime manipulation commands:
set,pulse,watch,reset stats(FR-9.2.4). serve --scenariooptional combination + passive-node hint (FR-9.2.5). (Plainservewithout a scenario already works — see Completed above.)
Phase 3 — Long-duration (continuous) simulation (spec §8)
The soak-testing feature requested 2026-10-03 — specified in §8, not yet implemented. Datasets are optional: signal logic (plugins, scheduled steps, degradation) runs in parallel with CSV streaming, or entirely without it.
scenario/datasets.py— streaming CSV playback for designated inputs (OPTIONAL driver): header-driven time column,interpolation: linear|hold,repeatwith seamless seams,time_factoracceleration,untillimits, bounded memory, ERROR verdict with file/line on malformed data (FR-8.2.1–8.2.11).- Parallel logic pipeline — fixed per-tick order: datasets → edge detection → plant plugins → degradation → invariants/rate limits → scheduled steps → snapshots; plugins may read dataset-driven and logic-driven aliases in the same tick (FR-8.2.12).
- Logic-only continuous runs — no
datasets:needed: demand profiles generated inside plugins, top-leveluntil:bounds the run; degradation/invariants/rate limits/snapshots apply identically (FR-8.2.13). - Continuous-mode run-end + validation rules: run ends when all
drivers exhausted; scenario with no datasets, no scheduled steps
and no
until:rejected (FR-8.2.1). scenario/degradation.py— wear counters (runtime hours, start cycles, load-hours) and degradation models (linear,exponential,staircase, plugin shapes) computing plant parameters such aspump_capacity(FR-8.3.1–8.3.2).- Degradation hooks in plant plugins:
engine.parameter()accessor used in loop physics, e.g. actual flow = nominal × capacity (FR-8.3.3). maintenance_atthresholds: diagnosis injection via register0x1050, maintenance-alarm assertions,maintenance_resetaction restoring capacity and clearing diagnoses (FR-8.3.4–8.3.5, FR-8.3.8).scenario/invariants.py— per-tickinvariantswith bounded episode aggregation andrate_limitsover sliding one-hour windows (FR-8.4.1–8.4.2).snapshot_everyperiodic dumps of aliases, wear counters, and degradation parameters (FR-8.4.3).- Continuous-mode verdict rules and bounded reporting for unattended multi-day runs (FR-8.4.4–8.4.5).
- Scheduled sequence steps via
at: <duration>interleaved with datasets on the sim-time axis (FR-8.4.6). - Acceptance tests per spec §8.8 (streaming 100k-row CSV, interpolation checks, repeat determinism, time_factor math, alias-ownership validation, degradation/recovery round-trip, episode recording, full 28-day soak, logic-only run, mixed dataset+logic tick-order run).
Phase 4 — Reporting (spec §11)
- FAT report generator (HTML/PDF) from scenario results and Modbus traces (console/JSONL/CSV export is done — see Completed above).
- Scenario comparison (run-vs-run diff) and template library per project type.
Phase 5 — Multi-node orchestration & protocol variants (spec §11)
- Several couplers in one FAT bench (ties into FR-4.3.10).
- Modbus UDP variant.
- WBM (web-based management) simulation pages.
- Web UI (§10): browser front-end for overlooking and
manipulating I/O — node/module/channel tables with
DI0.1-style and Modbus addresses (FR-10.2.2), live value updates from all sources (FR-10.2.3), toggle/force/pulse/ramp manipulation through Modbus-consistent paths (FR-10.2.4), diagnostics views with fault injection and statistics reset (FR-10.2.5), scenario/run view (FR-10.2.6), multi-client consistency (FR-10.2.7), optional read-only/access control (FR-10.2.8), alias-ownership protection during scenario runs (FR-10.2.9). - Runtime device configuration (§11): module hot-configuration
(plug/unplug at slot positions) with process-image rebuild per
KBUS/SAVE-CONFIG semantics and updated
0x1022–0x1025/0x2030+registers (FR-11.2.1–11.2.2); alias revalidation after rebuilds with position-stable module addressing (FR-11.2.3); rejection during running scenarios (FR-11.2.4); address-selection DIP-switch emulation with the manual's exact coding (1–254 host ID, 0 = stored config, 255 = DHCP, restart-to-apply) (FR-11.3.1–11.3.4); WBM-equivalent pages — Port/Ethernet, Modbus settings, Modbus Mapping, I/O Config with SAVE CONFIG (FR-11.4.1–11.4.4); device-state persistence and factory reset via0x2043(FR-11.5.1–11.5.2).
Phase 6 — CI integration (spec §11)
- Headless scenario execution in CI pipelines.
- JUnit XML export of verdicts for test dashboards.
Operational hardening (spec §6 — NFRs)
- Plain-text request/response tracing in verbose mode (
-v/WAGO_SIM_VERBOSE): every transaction logged as readableREQ/RSP/EXC/RAWlines with WAGO register-map annotations (input/output areas, %IW/%QW mapping, read-back mirrors, config/constant registers, illegal areas) per Tables 119/120 —src/wago_sim/trace.py+ tests. Covers NFR-6.4's readability goal; PDU hex is included inRAWlines for malformed frames. - Performance budget validation: ≥ 500 requests/s, ≤ 10 ms added latency (NFR-6.1).