374 lines
18 KiB
Markdown
374 lines
18 KiB
Markdown
# 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).
|
||
|
||
### 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). |