Files
wago-modbustcp-simulator-py/README.md
T
2026-10-03 19:17:53 +02:00

394 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 registers
`0x1000–0x2FFF`, constants `0x2000–0x2008` (byte-order check `0x1234`).
- 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=ON` startup 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 `0x102B` semantics),
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, seamless
`repeat`, `time_factor` acceleration), while degradation models age
plant parameters (e.g., pump capacity) from wear counters — exercising
the PLC's compensation, maintenance-alarm and service-recovery logic.
Continuous `invariants` and `rate_limits` produce 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 stop` completes in < 1 s).
## Install (dev)
```bash
pip install -e .[dev]
```
## Usage
Run the Modbus TCP server against a node configuration:
```bash
wago-sim serve --node config/pump_node.yaml --port 502
```
Validate a scenario:
```bash
wago-sim validate --scenario scenarios/pump_station_fat.yaml
```
Run a scenario headless and write reports:
```bash
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:
```bash
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):
```bash
docker compose up -d # start
```
```bash
docker compose logs -f wago-sim
docker compose down # graceful SIGTERM shutdown
```
Bare metal / VM (systemd; unit file in `deploy/wago-sim.service`):
```bash
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_SERVICE` to a dedicated user).
- `docker stop` sends 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 `run` invocations go to the mounted `out/`.
## 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`).
## 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
```bash
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)
- [x] 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).
- [x] 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, constants `0x2000–0x2008`
(incl. byte-order check `0x2002` = `0x1234`).
- [x] Configuration/diagnostic/firmware registers (§3.5): image sizes
`0x1022–0x1025`, statistics `0x1029`, connections `0x102A`, idle
timeout `0x1030`, MAC-ID `0x1031`, response delay `0x1037`,
module diagnosis `0x1050`, LED error `0x1020/0x1021`.
- [x] Modbus watchdog (§3.4): registers `0x1000–0x100B`, Standard and
Alternative modes, trigger/masks/stop sequences, expiry clears
outputs, address-aware `0x04` lock in Standard mode, optional
TCP close on timeout.
- [x] Fault-injection paths (§3.6): response-delay injection,
connection refusal beyond 15, illegal-address probing responses,
diagnosis/LED registers scriptable.
- [x] 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).
- [x] Scenario DSL (§4): named aliases with `scale`/`offset`, macros
with parameters (inline expansion, blocking waits inside macros),
sequence steps `set` / `reset` / `toggle` / `pulse` / `ramp` /
`wait` / `assert` / `report`, rising-edge detection on all
aliases.
- [x] 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.
- [x] Assertions with `within:` timeouts → PASS/FAIL verdicts with
sim-time stamps (FR-4.3.5).
- [x] Python action/plant-model plugins: per-tick models and step
actions (FR-4.3.8), e.g. `plugins/pump_plant.py`.
- [x] Deterministic tick-driven engine: sim-time decoupled from
wall-clock (FR-4.3.9 core).
- [x] Service / container deployment (spec §12): env-var config
(`WAGO_SIM_NODE/HOST/PORT`, CLI args override), graceful
SIGINT/SIGTERM shutdown via `server.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 stop` in 0.34 s); CLI env tests added.
- [x] CLI: `wago-sim serve / run / validate`.
- [x] Reports: console verdict summary, JSONL and CSV export
(`--report-jsonl` / `--report-csv`).
- [x] asyncio Modbus TCP server: ≤ 15 concurrent connections, idle
connection reaper, response-delay task.
- [x] Test suite: 178 tests incl. real-socket server tests, `pymodbus`
interop, CLI env-config and bad-argument tests, and plain-text
trace tests; spec §7 acceptance criteria 1–7 covered.
- [x] Clean CLI error handling: invalid arguments (unknown command/
options, non-numeric or out-of-range `--port`, bad
`WAGO_SIM_PORT`), missing/unparsable node and scenario files, and
unwritable report paths all produce a one-line
`wago-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).
### 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-ordered `sequence:` is implemented.
- [ ] Actions `noise`, `sin`, `hold` on simulated inputs (FR-4.3.4) —
`set`/`reset`/`pulse`/`toggle`/`ramp` are done.
- [ ] `wait_edge` step 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`/`sin` or 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=ON` startup 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 --scenario` optional combination + passive-node hint
(FR-9.2.5). *(Plain `serve` without 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`, `repeat` with seamless seams,
`time_factor` acceleration, `until` limits, 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-level `until:` 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 as `pump_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_at` thresholds: diagnosis injection via register
`0x1050`, maintenance-alarm assertions, `maintenance_reset`
action restoring capacity and clearing diagnoses (FR-8.3.4–8.3.5,
FR-8.3.8).
- [ ] `scenario/invariants.py` — per-tick `invariants` with bounded
episode aggregation and `rate_limits` over sliding one-hour
windows (FR-8.4.1–8.4.2).
- [ ] `snapshot_every` periodic 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 via `0x2043` (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)
- [x] Plain-text request/response tracing in verbose mode (`-v` /
`WAGO_SIM_VERBOSE`): every transaction logged as readable
`REQ`/`RSP`/`EXC`/`RAW` lines 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 in `RAW` lines for malformed frames.
- [ ] Performance budget validation: ≥ 500 requests/s, ≤ 10 ms added
latency (NFR-6.1).