Files
wago-modbustcp-simulator-py/README.md
T
2026-10-04 20:59:22 +02:00

569 lines
28 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 **numbered
0-based within its type** — `DI0`, `DO0`, `AI0`, `AO0` start each
kind — and channel 0-based; v1.5) — 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`).
## Web UI
`serve` starts a browser UI alongside the Modbus server (spec §10):
```bash
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.
## Plant environments (continuous simulation)
A **plant environment** simulates the physical world around the
distributed I/O: per deterministic tick it reads what the PLC
commanded (DO/AO), runs your plant physics, and writes what the field
now shows (DI/AI) — the sensors the PLC reads back. Environments run
**until manually stopped** (Ctrl+C / `docker stop`), optionally ending
sooner via an `is_done()` criterion.
Attach one to a live Modbus server (wall-clock-paced 10 ms ticks):
```bash
wago-sim serve --node config/pump_node.yaml --port 5020 \
--env envs.pump_station_env:PumpStationEnv
```
or run headless (unpaced, for plant-model development):
```bash
wago-sim sim --node config/pump_node.yaml \
--env envs.pump_station_env:PumpStationEnv
```
**Authoring one** is a small subclass — override `model()`:
```python
# myenvs/tank.py
from wago_sim.scenario.plantenv import PlantEnv
class TankEnv(PlantEnv):
def model(self, state, dt_s):
pump_on = bool(state.by_code("DO0.0")) # PLC command
level = float(state.by_code("AI0.0")) # current field
return {"AI0.0": level + (-5.0 if pump_on else 1.0) * dt_s}
Env = TankEnv # `--env myenvs.tank` picks this up
```
API surface (spec FR-8.2.14–17): `observe()` returns every
DI/DO/AI/AO channel (I/O codes, names, values, analog display
ranges) plus aliases and degradation parameters; `apply()` writes DI/AI (the field);
`force()` writes DO/AO master-side (headless runs, fault setup);
`is_done()` optional end; `reset()` deterministic replay.
`state.channel(key)` returns the full channel record by I/O code
or configured name; `state.channel_range(key)` gives an analog
channel's (min, max) directly. Reference
forms: `pkg.mod:ClassName`, `pkg.mod:factory`, or `pkg.mod` with
`Env`/`make_env`. The shipped example is
`envs/pump_station_env.py` (tank + pump physics with a low-level
switch). Scenario-continuous mode (§8 YAML: datasets, degradation,
invariants) remains available for scripted soak runs on the same
engine ticks.
## 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).
### Web UI (spec §10) — in implementation
- [x] 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.
- [x] 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/node` with `DI0.1`-style
and Modbus addresses; CLI wiring `--web/--no-web`,
`--web-port`, env `WAGO_SIM_WEB_USER/PASSWORD/READONLY/
NO_WEB/WEB_PORT`; login page + SPA shell.
- [x] M2: node overview page — DIN-rail strip with coupler block
(MS/NS/BF/SF LEDs), module bezels with article numbers, the
kind+slot designation (`DI0`, `DO2`, `AI4`) in the module's
lower left corner below the indicators, per-channel LEDs for
DI/DO with the channel number beneath, bar+numeric indicators
for AI/AO with the channel number outside the overflow/underflow
limit LEDs, live values via WebSocket (`/api/ws`, 10 Hz delta
push), `GET /api/values` snapshot (FR-10.2.2).
- [x] M3: manipulation — DI/DO toggle (field side via
`set_digital_input_channel`, master side via internal
`write_bits`/`write_words` dispatch so mirrors/statistics stay
consistent), AI value entry (raw counts), AO force;
unknown-channel → 404, kind-mismatch → 400, read-only → 403.
- [x] 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.
- [x] 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. Modules re-orderable by drag-and-drop or ▲/▼ arrows (FR-11.2.6); with per-type I/O codes (FR-4.3.11 v1.5) cross-kind moves don't renumber other kinds' channels.
- [x] 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.
- [x] M5b: analog display ranges (FR-10.2.14) — bar indicators fill
from the bottom against the channel's display range, default
0–4095 counts (100 % at 4095, WAGO 750 12-bit scaling), with
per-channel min/max configurable in the node config
(`channel_ranges:`) and editable on the Configuration page; values
outside the range fill the bar red with a red LED above the bar
(overflow) or below it (underflow); ranges carried through
module saves, export/import, and rebuilds; also fixes the bar
never filling (it set width instead of height on a vertical bar)
and the invisible numeric label (clipped by overflow: hidden).
Signed support (2026-10-04): range bounds and simulated values
may be negative (-65534–65535; stored as 16-bit two's
complement), for signed plant quantities. DI/DO channels render
in vertical columns of eight (0–7 left, 8–15 next) on thin,
tall module bezels mirroring real DIN-rail housings. Dense
store (2026-10-04): `node.channel_ranges` always carries every
analog channel of the current layout (unconfigured = the
materialized default, stale codes pruned on rebuild), so
environments read min/max without fallbacks; the YAML and the
config export stay lean (non-default entries only).
- [ ] M6: scenario run view + alias-ownership rejection (FR-10.2.6,
FR-10.2.9) — deferred.
- [x] Tests: 46 web-UI tests (`tests/test_webui.py`) via httpx
`ASGITransport` (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;
bar states verified for in-range (97.7 % at 4000), overflow,
custom-range, and underflow. Full suite: 224 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-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) — IMPLEMENTED 2026-10-04
The soak-testing feature specified in §8 is implemented: continuous
mode with the fixed tick pipeline, optional streaming CSV datasets,
degradation models, invariants/rate limits with bounded episode
reporting, snapshots, and the gym-style `PlantEnv`. 30 new tests
(`tests/test_continuous.py`); the shipped example
`scenarios/pump_soak.example.yaml` demonstrates the full DSL.
Datasets are **optional**: signal logic runs in parallel with CSV
streaming, or entirely without it.
- [x] Plant environment `scenario/plantenv.py` (FR-8.2.14–17, reworked
2026-10-04 to environment-first): standalone `PlantEnv` —
`observe()` returns ALL modules (DI/DO/AI/AO channels, per-type
codes, names, parameters); `apply()` writes the FIELD side
(DI/AI) by default; `force()` writes DO/AO master-side (mirrors/
statistics/watchdog stay consistent); `model()`/`is_done()`
override hooks (subclass authoring, FR-8.2.17); `reset()`
deterministic. Runners: `serve --env ...` wall-clock-paced
alongside Modbus, runs until stopped; `sim --env ...` headless
unpaced until is_done()/Ctrl+C. Example: `envs/pump_station_env.py`.
Verified live: PLC coil write → env physics → level registers
drain/refill over Modbus.
- [x] `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).
- [x] 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).
- [x] 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).
- [x] 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).
- [x] `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).
- [x] Degradation hooks in plant plugins: `engine.parameter()` accessor
used in loop physics, e.g. actual flow = nominal × capacity
(FR-8.3.3).
- [x] `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).
- [x] `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).
- [x] `snapshot_every` periodic dumps of aliases, wear counters, and
degradation parameters (FR-8.4.3).
- [x] Continuous-mode verdict rules and bounded reporting for unattended
multi-day runs (FR-8.4.4–8.4.5).
- [x] Scheduled sequence steps via `at: <duration>` interleaved with
datasets on the sim-time axis (FR-8.4.6).
- [x] 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).