43 KiB
Software Specification
WAGO Modbus TCP Simulator — Autonomous FAT Testing Framework
| Project | wago-modbustcp-simulator-py |
| Version | 1.1 (Draft for review) |
| Date | 2026-10-03 |
| Status | Framework phase; module addressing implemented (FR-4.3.11); long-duration simulation (§8), bench mode (§9) and web UI (§10) requested |
| Target device simulated | WAGO 750-362 / 750-362/0000-0001 FC Modbus TCP (G4) bus coupler |
| Reference documents | docs/m07500362_xxxxxxxx_0en.pdf (750-362 Modbus TCP manual), Modbus TCP specification (modbus.org) |
1. Introduction
1.1 Purpose
This document specifies a software system that simulates a WAGO 750-362 Modbus TCP fieldbus coupler on a standard PC, to be used as a test bench during Factory Acceptance Tests (FAT) of PLC programs.
During FAT the PLC under test is normally wired to a real WAGO I/O node. This simulator replaces the physical node entirely: the PLC (or any Modbus TCP master/SCADA) connects over TCP/IP to the simulator, which responds exactly like a 750-362 coupler — same function codes, same register map, same exception behavior, same timing characteristics.
1.2 Problem being solved
FAT of PLC programs requires either:
- Physical I/O hardware (expensive, must be wired per project, error-prone, single-purpose), or
- A generic Modbus slave (does not implement the WAGO-specific register map, digital-channel addressing, watchdog, statistics, or module-diagnostics behavior; PLC programs validated against it may fail against a real node).
The simulator provides the exact WAGO behavior plus scriptable, autonomous scenario execution, so a complete FAT round — including fault cases — can run unattended and produce a machine-verifiable result.
1.3 Scope
In scope (v1.0 framework):
- Modbus TCP server (slave) with the 750-362 function-code set.
- WAGO 750-362 register map: process image (words + bits), configuration, diagnostic, constant, and firmware-info registers.
- Virtual I/O node: a configurable list of WAGO 750/753 I/O modules forming the process image.
- Scenario engine: scripted macros that observe input states and act on outputs (the automation core).
- Deterministic simulation clock and scheduler.
- CLI runner with configuration files.
Out of scope (v1.0): HTTP/WBM web interface, FTP, SNMP, UDP variant of Modbus, DHCP/BootP provisioning, real-time behavior beyond response latency simulation, and module parameterization (mailbox traffic) beyond process data.
1.4 Definitions
| Term | Meaning |
|---|---|
| PII | Process Input Image — data the coupler presents to the master (from input modules) |
| PIO | Process Output Image — data the master writes (to output modules) |
| MBAP | Modbus Application Protocol header (7-byte TCP header) |
| FC | Modbus function code |
| FAT | Factory Acceptance Test |
| Scenario | A scripted, timed sequence of state changes and assertions |
| Macro | A reusable scripted action (e.g., "pulse output 3 for 500 ms") |
| Node | One simulated fieldbus coupler + its I/O module configuration |
| LTS | Long-Time Simulation — continuous (“soak”) mode running hours to weeks of simulated time |
| Dataset | A CSV time-series file driving designated input aliases in continuous mode |
| Wear counter | Engine-maintained per-alias statistic: runtime hours, edge counts, load-hours |
| Degradation model | Time/cycle-dependent transformation of a plant parameter (e.g., pump capacity) |
| Invariant | A condition that must hold every tick throughout a continuous run |
| Episode | One aggregated violation period of an invariant (bounded reporting) |
2. System Overview
flowchart LR
subgraph DUT["Devices under test"]
PLC["PLC 1..n\n(Modbus TCP masters)"]
end
subgraph SIM["Simulator (this system)"]
MB["Modbus TCP Server\n(asyncio, ≤ 15 connections)"]
DS["Datastore\n(WAGO word & bit maps)"]
NODE["Virtual WAGO Node\n(module list → process image)"]
SE["Scenario Engine\n(states → actions)"]
CLK["Deterministic Clock\n+ Scheduler"]
LOG["Event Log / Traces"]
end
PLC -- "MBAP frames (FC1..FC23)" --> MB
MB <--> DS
DS <--> NODE
SE -- "read inputs / write outputs" --> NODE
SE <--> CLK
NODE -- change events --> SE
LOG -.records.- MB
LOG -.records.- SE
The system is a pure slave/server: it never initiates Modbus traffic. Autonomy comes from the scenario engine, which drives the field side of the virtual node (as if sensors moved and actuators did something), while the PLC drives the master side through normal Modbus writes.
3. Functional Requirements — Modbus Server
3.1 Transport & connections
- FR-3.1.1: The server shall listen on TCP port 502 (configurable) and speak Modbus TCP with the standard MBAP header.
- FR-3.1.2: The server shall support up to 15 concurrent Modbus TCP connections (per 750-362 spec, §7.3). Connection number 16 shall be refused.
- FR-3.1.3: Idle connections shall be closed after the timeout configured in register
0x1030(default 600 × 100 ms). Outputs remain in last state (§11.2.5.5). - FR-3.1.4: Protocol identifier other than 0 in MBAP shall increment the
BadProtocolstatistic and be ignored. - FR-3.1.5: Unit identifier shall be ignored (accepted, echoed back), as on the real device.
3.2 Function codes
The simulator shall implement exactly the 750-362 function set (§11.2.2) and reject all others with exception 0x01 (Illegal Function):
| FC | Function | Limits (R/W) |
|---|---|---|
| 01 | Read Coils | 2000 / — |
| 02 | Read Discrete Inputs | 2000 / — |
| 03 | Read Holding Registers | 125 / — |
| 04 | Read Input Registers | 125 / — |
| 05 | Write Single Coil | — / 1 |
| 06 | Write Single Register | — / 1 |
| 11 | Get Comm Event Count | — |
| 15 | Write Multiple Coils | — / 800 |
| 16 | Write Multiple Registers | — / 100 |
| 22 | Mask Write Register | — / 1 |
| 23 | Read/Write Multiple Registers | 125 / 100 |
Exceeding limits → exception 0x03 (Illegal Data Value) and increment TooManyRegisters / TooManyBits.
Exception codes (§11.2.3): 0x01 Illegal Function, 0x02 Illegal Data Address, 0x03 Illegal Data Value, 0x04 Slave Device Failure, 0x05 Acknowledge, 0x06 Server Busy, 0x08 Memory Parity Error, 0x0A/0x0B Gateway errors.
3.3 Register map (WAGO 750-362)
3.3.1 Word (register) access — reading (FC3/FC4)
| Modbus address (hex) | Area | Behavior |
|---|---|---|
0x0000–0x00FF |
Physical input area (1), %IW0–255 | First 256 words of input process image |
0x0100–0x01FF |
— | Exception 0x02 |
0x0200–0x02FF |
Physical output area (1) | Read-back mirror of %QW0–255 (outputs written at 0x0000 are readable here, +0x0200 offset) |
0x0300–0x0FFF |
— | Exception 0x02 |
0x1000–0x2FFF |
Configuration registers | See §3.5 |
0x3000–0x5FFF |
— | Exception 0x02 |
0x6000–0x62FB |
Physical input area (2), %IW256–1020 | Additional 764 words |
0x62FC–0x6FFF |
— | Exception 0x02 |
0x7000–0x72FB |
Physical output area (2), %QW256–1020 | Read-back mirror, +0x01000 offset from write area (2) |
0x72FC–0xFFFF |
— | Exception 0x02 |
3.3.2 Word access — writing (FC6/FC16)
| Modbus address (hex) | Area |
|---|---|
0x0000–0x00FF |
Physical output area (1), %QW0–255 |
0x0100–0x01FF |
Exception 0x02 |
0x0200–0x02FF |
Physical output area (1) (mirror accepted for writing) |
0x0300–0x0FFF |
Exception 0x02 |
0x1000–0x2FFF |
Configuration registers |
0x3000–0x5FFF |
Exception 0x02 |
0x6000–0x62FB |
Physical output area (2), %QW256–1020 |
0x62FC–0x6FFF |
Exception 0x02 |
0x7000–0x72FB |
Physical output area (2) (mirror) |
0x72FC–0xFFFF |
Exception 0x02 |
3.3.3 Bit (coil) access — reading (FC1/FC2) & writing (FC5/FC15)
| Modbus address (hex) | Area |
|---|---|
0x0000–0x01FF |
Physical input area (1) — first 512 digital inputs (read) |
0x0200–0x03FF |
Physical output area (1) — first 512 digital outputs (read mirror + write) |
0x0800–0x85F7 |
Physical input area (2) — digital inputs 513–2039 |
0x9000–0x95F7 |
Physical output area (2) — digital outputs 513–2039 |
| all others | Exception 0x02 |
Digital channel addressing rule (§11.2.4): bit addresses are channel numbers — digital input no. 47 has Modbus address 46 — independent of the physical module layout. Word-oriented modules are laid out first (in physical order, from word 0), then bit-oriented modules are byte-packed in physical order.
3.3.4 Constant registers (FC3/FC4 read-only)
| Addr | Value | Purpose |
|---|---|---|
0x2000 |
0x0000 |
Zero |
0x2001 |
0xFFFF |
Ones |
0x2002 |
0x1234 |
Byte-order check (Intel format expected) |
0x2003 |
0xAAAAl |
Bit mask 1 |
0x2004 |
0x5555 |
Bit mask 2 |
0x2005 |
0x7FFF |
Max positive |
0x2006 |
0x8000 |
Max negative |
0x2007 |
0x3FFF |
Half positive |
0x2008 |
0x4000 |
Half negative |
3.3.5 Firmware-info registers (read-only)
0x2010 revision, 0x2011 series code (0x02EE = 750), 0x2012 coupler code (0x0166 = 362), 0x2013/0x2014 major/minor, 0x2020 description (16 words), 0x2021 compile time, 0x2022 compile date, 0x2023 firmware loader info, 0x2030–0x2033 connected-module descriptions, 0x2035 process-image settings, 0x2036 diagnostics, 0x2040 software reset (write 0x55AA/0xAA55), 0x2043 factory settings.
3.4 Watchdog (§11.2.5.2/3)
The Modbus watchdog shall be simulated:
0x1000timeout value ×100 ms (default0x0064), writable only when WD inactive.0x1001/0x1002coding masks (bit n−1 of the mask = FC n; default0xFFFF).0x1003trigger (write nonzero, alternating values),0x1005stop (0xAAAAthen0x5555),0x1008simple stop (0xAA55/0x55AA),0x1007restart (write1).0x1006status:0inactive /1active /2expired.0x1009close-socket-on-timeout,0x100Atype Standard/Alternative,0x100Bsave WD params (remanent).- On timeout: all analog outputs → 0, all digital outputs → OFF; in Standard mode further process-data access is answered with exception
0x04; WD registers remain accessible;0x1009 = 1additionally closes TCP connections. - Watchdog events shall be visible to the scenario engine (assertions: "WD triggered at t seconds").
3.5 Configuration & diagnostic registers
0x1022–0x1025: image sizes (analog in/out bits, digital in/out bits) computed from the configured module list.0x1029(9 words) Modbus TCP statistics: SlaveDeviceFailure, BadProtocol, BadLength, BadFunction, BadAddress, BadData, TooManyRegisters, TooManyBits, ModTcpMessageCounter. Reset by writing0xAA55/0x55AA.0x102Aactive TCP connections.0x1030Modbus TCP idle timeout (×100 ms).0x1031MAC-ID (3 words, configurable to emulate a specific device).0x1037response delay (ms) — the simulator shall actually delay responses, to emulate link latency.0x1038TOS/DSCP (stored, echoed).0x1050module diagnosis (3 words): module, channel, diagnosis bits (underrange … collective failure) — scriptable fault injection.0x1020/0x1021LED error code/argument — scenario-settable.0x102BKBUS reset (write): re-reads the module configuration; process image rebuilt.
3.6 Fault injection
FR-3.6.1: The scenario engine shall be able to simulate fieldbus faults:
- KBUS/local-bus failure →
SlaveDeviceFailurestatistic increments and process-data reads may return exception0x04. - Response-delay injection via
0x1037. - Connection refusal beyond 15 concurrent connections.
- Illegal-address probing (read/write the exception areas) → correct
0x02responses (this validates master error handling).
3.7 Process data semantics
- Word mapping of digital channels follows the Intel format (§7.3.3): channel 1 = bit 0 of the low byte (
D0), high byteD1on top. - Digital word mapping:
Bit15 … Bit0=Byte D1 D0, bit 0 = lowest channel number. - Analog modules contribute 16-bit data words (control/status bytes not accessible via Modbus).
- A module list defines the image; the framework computes addresses and exposes them in the log and via module-description registers (
0x2030+).
4. Functional Requirements — Scenario & Macro Engine
4.1 Concept
A scenario is a scripted, time-ordered description of the world around the PLC: sensor inputs change over time, actuators respond, and the script asserts on PLC outputs. This is what makes FAT autonomous — no operator needs to toggle signals.
- State-based: rules keyed on input/output conditions (
when input 5 turns ON …). - Time-based: scheduled events (
after 30 s …). - Output actions:
set,reset,pulse,toggle,ramp,hold_valueon simulated field outputs (what the PLC sees as its inputs). - Input observations: the engine reads the process output image (what the PLC wrote) with edge detection (rising/falling).
- Assertions: expected PLC behavior (output X ON within 2 s after input Y) → FAT verdicts.
4.2 Scenario DSL (YAML)
name: pump_station_fat
description: "Full FAT round for pump station PLC program"
node: config/pump_node.yaml # module configuration
timing: { tick: 10ms } # simulation tick
inputs: # named aliases → digital/analog inputs
start_button: { type: di, module: 0, channel: 0 } # DI0.0
level_sensor: { type: ai, module: 4, channel: 0, scale: 0.1 } # AI4.0, mm → counts
em_stop_ok: { type: di, module: 0, channel: 3 } # DI0.3
outputs: # named aliases → PLC outputs (read-only for script)
pump_relay: { type: do, module: 2, channel: 0 } # DO2.0
valve_open: { type: do, module: 2, channel: 1 } # DO2.1
speed_setpoint: { type: ao, module: 5, channel: 0, scale: 0.1 } # AO5.0
macros:
pulse_start: # reusable macro
- set: start_button = ON
- wait: 250ms
- set: start_button = OFF
states: # optional state machine
idle:
on: { em_stop_ok: ON }
goto: running
running:
entry: [ ramp: level_sensor, to: 8500, over: 30s ]
on: { pump_relay: RISING }
assert: { valve_open: ON, within: 2s }
goto: full
...
sequence: # time-ordered script (or fully state-driven)
- macro: pulse_start
- assert: { pump_relay: ON, within: 2s }
- wait: 5s
- set: level_sensor = 4200
- assert: { speed_setpoint: ">= 1000", within: 3s }
- report: PASS
4.3 Requirements
- FR-4.3.1: Scenarios shall be defined in YAML (no programming required for standard FAT sequences).
- FR-4.3.2: The engine shall support state machines (states, entry actions, transitions on input/output edges or conditions, guards).
- FR-4.3.3: The engine shall support macros (reusable action groups, parameterizable).
- FR-4.3.4: Actions on simulated inputs:
set,reset,pulse,toggle,ramp(linear over time),noise(± band),sin,hold. - FR-4.3.5: Assertions on PLC outputs with timeout → PASS/FAIL/ERROR verdict per check, with timestamps; final scenario verdict.
- FR-4.3.6: Assertions may compare against expected values, ranges, edges, and statistics registers (e.g., "watchdog expired").
- FR-4.3.7: All scenario events and all Modbus transactions shall be logged (CSV/JSONL) for the FAT report.
- FR-4.3.8: Python action plugins shall be supported for actions beyond the built-in set (registered callables), for complex plant-model physics.
- FR-4.3.9: The engine shall run deterministically: simulation time decoupled from wall-clock; results reproducible.
- FR-4.3.10: A scenario may load multiple nodes (multi-coupler FAT benches).
- FR-4.3.11: Module-based channel addressing — aliases may be addressed
as module type + module number + channel number, all components
explicitly configurable:
{ type: di, module: 2, channel: 1 }selects digital input channel 1 (0-based) on module slot 2 (0-based position in the node's module list), i.e.DI2.1. Both numbers are 0-based to match physical slot/channel labeling. The channel kind must match the module (DOon a DI-only module is a validation error). This is an alternative to pool addressing ({ type: di, channel: 3 }= 3rd DI channel across the whole node); both schemes are supported and may be mixed, but an alias uses exactly one. Implemented in v1.1 (channel_by_module,Alias.module,alias_location).
4.4 Example FAT flow
- PLC program boots, connects to simulator (Modbus TCP client), reads input image.
- Scenario
pump_station_fatstarts inidlestate;em_stop_okturns ON. - Scenario pulses
start_button; PLC sees input, starts pump relay (writes coil). - Engine detects rising edge on
pump_relay, assertsvalve_openwithin 2 s. - Scenario ramps
level_sensorto 8500 counts; PLC adjustsspeed_setpoint; assertions verify. - Scenario injects fault (KBUS error / watchdog timeout); PLC must react (e.g., switch to safe state) — asserted.
- Report generated: verdicts, timing, Modbus trace.
5. Architecture
5.1 Package layout
wago-modbustcp-simulator-py/
├── docs/ # WAGO manuals + this spec
│ └── spec/
│ ├── software-spec.md # this document
│ └── register-map.md # extracted WAGO tables
├── src/
│ └── wago_sim/
│ ├── __init__.py
│ ├── mbap.py # MBAP frame codec
│ ├── pdu.py # PDU codec (request/response/exception)
│ ├── server.py # asyncio Modbus TCP server
│ ├── datastore.py # WAGO word/coil datastores
│ │ # (holds process images + register bank)
│ ├── registers.py # config/diagnostic/constant/firmware regs
│ ├── modules.py # virtual WAGO I/O modules (module catalog)
│ ├── node.py # node = modules → process image mapping
│ ├── watchdog.py # Modbus watchdog simulation
│ ├── clock.py # deterministic clock + scheduler
│ ├── scenario/
│ │ ├── __init__.py
│ │ ├── engine.py # interpreter for scenario DSL
│ │ ├── dsl.py # YAML schema + validation
│ │ │ ├── actions.py # set/pulse/ramp/... + plugin registry
│ │ │ ├── assertions.py # condition matcher
│ │ │ ├── statemachine.py
│ │ └── report.py # verdicts, CSV/JSONL log
│ ├── stats.py # Modbus TCP statistics counters
│ ├── logging_setup.py
│ └── cli.py # entry point (argparse)
├── config/
│ └── pump_node.yaml # example node configuration
├── scenarios/
│ └── pump_station_fat.yaml # example scenario
├── tests/
│ ├── test_mbap.py
│ ├── test_pdu.py
│ ├── test_server.py
│ ├── test_datastore.py
│ ├── test_registers.py
│ � ├── test_watchdog.py
│ ├── test_scenario_engine.py
│ └── conftest.py
├── pyproject.toml
└── README.md
5.2 Design decisions
| Decision | Rationale |
|---|---|
| Pure-Python, asyncio TCP server | No external Modbus library needed; full control over WAGO-specific behavior (register-map exceptions, statistics, watchdog); testable |
| Deterministic clock | Reproducible FAT runs; scenario times independent of host speed |
| YAML DSL + plugin boundary | Test engineers write scenarios, not code; complex physics via Python plugins |
| Datastore split: process image vs. register bank | Mirrors real device: process image vs. internal registers; the readback-mirror rule (+0x0200) is dataflow, not copy-on-read |
| Statistics as first-class objects | FAT assertions can check communication health (0x1029) |
5.3 Threading & concurrency model
- asyncio event loop owns the Modbus server and the scheduler tick.
- Scenario engine runs in the same loop (coroutines), so image updates and Modbus reads see consistent data without locks.
- Response delay (
0x1037) implemented as asyncio sleep before response send.
5.4 Data model
erDiagram
NodeConfig ||--|{ ModuleConfig : modules
NodeConfig ||--|| ProcessImage : builds
ModuleConfig ||--|{ Channel : channels
ProcessImage ||--|| InputImage : inputs
ProcessImage ||--|| OutputImage : outputs
OutputImage ||--|| InputMirror : "read-back at +0x200/+0x1000"
Scenario ||--|{ State : states
Scenario ||--|{ SequenceStep : sequence
State ||--|{ Action : entry
State ||--|{ Transition : on
SequenceStep ||--|{ Action : actions
Scenario ||--|{ Alias : "inputs/outputs aliases"
Scenario ||--|{ Macro : macros
Macro ||--|{ Action : steps
5.5 State chart — server session
stateDiagram-v2
[*] --> Listening
Listening --> Connected: TCP accept (≤15)
Connected --> RequestParsing: MBAP complete
RequestParsing --> Dispatching: PDU decoded
Dispatching --> Responding: result
Dispatching --> ExceptionResponse: FC/addr/val error
Responding --> Connected
ExceptionResponse --> Connected
RequestParsing --> Connected: malformed (count BadLength)
Connected --> Closed: idle timeout / WD 0x1009=1 / client close
6. Non-Functional Requirements
- NFR-6.1 Performance: sustain ≥ 500 requests/s with ≤ 10 ms added latency (typical FAT bench load).
- NFR-6.2 Portability: Python 3.11+ on Windows/Linux.
- NFR-6.3 Determinism: two runs of a scenario with identical inputs and timing produce identical verdict logs (except wall-clock stamps).
- NFR-6.4 Observability: every Modbus transaction logged with timestamps (sim-time + wall-time), PDU hex, connection id.
- NFR-6.5 Robustness: malformed frames never crash the server; counted in statistics.
- NFR-6.6 Configurability: port, bind address, node config, firmware identity registers, MAC-ID configurable without code changes.
7. Acceptance Criteria (framework phase)
- A Modbus TCP master (e.g.,
pymodbusclient in tests) can connect, read constants0x2000–0x2008, and gets exactly0x0000, 0xFFFF, 0x1234, 0xAAAA, 0x5555, 0x0166, ...per spec — byte order verified. - Reads/writes in all mapped/unmapped areas produce values/exceptions per §3.3 tables.
- Digital channel addressing: a configured 16-channel DI module + 8-channel DO module yields channel-number bit addressing (DI 47 → bit address 46).
- Output readback mirrors at +
0x0200(word) and +0x0200/+0x1000(bit/word area 2) work. - A YAML scenario can set a digital input, detect a PLC output rising edge, and produce a PASS/FAIL report file.
- Watchdog: configure
0x1000, trigger via0x1003, let it expire, outputs clear,0x1006= 2, process access → exception0x04, restart via0x1007. - Statistics counters increment correctly on bad frames/addresses; reset via
0x1029write0xAA55.
8. Feature Request — Long-Duration (Continuous) Simulation
Status: requested 2026-10-03, planned (roadmap phase 3), not yet implemented. Extends the scenario engine (§4) with a
continuousmode for endurance (“soak”) testing: the facility runs for hours, days or weeks of simulated time against evolving inputs and an aging plant.
8.1 Motivation
FAT rounds (§4) validate functional behavior over minutes. A facility must additionally be proven over long time:
- Endurance/soak: memory leaks, counter rollovers, watchdog stability over days.
- Trend & historian verification: SCADA trends, averaging, alarm shelving.
- Cycle behavior: daily/weekly demand profiles, shift patterns, weekend idle.
- Maintenance logic: wear-based alarms, degradation detection, service recovery.
- Clock edge cases: midnight rollovers and (via absolute-timestamp datasets) DST transitions.
Long runs cannot be hand-scripted step by step. Two mechanisms replace the
scripted sequence:
- Dataset playback — designated inputs are driven from time-series files (e.g., CSV exported from a plant historian, or synthesized profiles).
- Degradation models — plant-model parameters (e.g., pump capacity) degrade as a function of runtime and cycles, exercising the PLC’s compensation and maintenance logic.
8.2 Requirements — continuous mode & CSV dataset playback
- FR-8.2.1: A scenario may declare
mode: continuous. Thesequencebecomes optional; the run continues until the dataset schedule (plus anyuntil:duration) is exhausted. - FR-8.2.2: Input aliases (di/ai) may be driven from datasets. Output aliases must not appear in datasets (validation error). An endogenous process variable (e.g., tank level) may instead be computed by a plant-model plugin (FR-4.3.8, closed loop). Exactly one driver may own an alias (dataset, plugin-declared variable, ramp, pulse, or step) — conflicts are validation errors.
- FR-8.2.3: CSV format: a header row with the time column (default
time, configurable viatime_column) plus one column per alias. Time is elapsed seconds (default) or ISO-8601 absolute timestamps (time_format: iso8601). Analog values are numeric; digital values acceptON/OFF,1/0,true/false. Lines starting with#are comments. - FR-8.2.4: Values pass through the alias
scale/offset(engineering units → raw counts), identical toset:semantics. - FR-8.2.5: Between samples, analog aliases use per-dataset
interpolation: linear | hold; digital aliases always usehold(never interpolated). - FR-8.2.6: Datasets shall be streamed with bounded memory (one-row lookahead); files covering months of samples must be playable without loading them into RAM.
- FR-8.2.7:
repeat: nloops a dataset seamlessly and monotonically (no time jump at the seam); identical loops produce identical values (deterministic).until: <duration>limits total playback. - FR-8.2.8:
time_factor: k(default 1.0) — dataset time advances k× faster than sim-time, enabling a 28-day soak in a fraction of the wall-clock time. - FR-8.2.9: Multiple datasets share one sim-time axis; each has an optional
offset:start shift. The engine merges them; an alias’s value is that of its owning dataset. - FR-8.2.10: Pacing: in server mode (real PLC connected) sim-time follows wall-clock for real-time soak testing; headless runs are unpaced (as fast as deterministic processing allows).
- FR-8.2.11: Malformed data (unknown column, unparsable time or value, non-monotonic timestamps) ends the run with verdict ERROR, citing file and line number.
8.3 Requirements — degradation models (plant logic over time)
- FR-8.3.1: The engine maintains wear counters per alias, updated every tick: runtime hours (integral of truth), edge counts (rising/falling), and load-hours (integral of the analog value). Counters are deterministic state.
- FR-8.3.2: A degradation model computes a named plant parameter (e.g.
pump_capacity, range 0…1) from wear counters and sim-time. Built-in shapes:linear(per runtime hour, per start cycle),exponential,staircase(threshold table); custom shapes via Python plugin. - FR-8.3.3: Plant-model plugins read degraded parameters via
engine.parameter("pump_capacity")and shall use them in the loop physics (e.g., actual pump flow = nominal flow × capacity), so the PLC must compensate (e.g., speed creep) — the behavior under test. - FR-8.3.4:
maintenance_at:threshold crossing fires configured actions: fault injection (module diagnosis, register0x1050), assertions (maintenance alarm within a time bound), and logging. Amaintenance_resetrestores the parameter to 1.0 and clears injected diagnoses — verifying the PLC’s alarm clearing and performance recovery. - FR-8.3.5:
floor:/ceiling:clamp the parameter so the simulated plant stays physically plausible. - FR-8.3.6: Models must be pure functions of (sim-time, wear counters, configuration) — no wall-clock — keeping runs reproducible.
- FR-8.3.7: Degradation state and wear counters are included in snapshots (§8.4) and the final report.
- FR-8.3.8: New DSL actions introduced:
inject_diagnosis/clear_diagnosis(module-diagnosis fault injection) andmaintenance_reset.
8.4 Requirements — continuous assertions & monitoring
- FR-8.4.1:
invariants:— conditions evaluated every tick (analog bounds, expected digital states). Violations are recorded as episodes (start, end, extreme value, tick count) so a 30-day run yields a bounded report, not a per-tick flood. - FR-8.4.2:
rate_limits:— maximum rising/falling edges per sliding hour window per alias (e.g., pump starts ≤ 6/h), verifying anti-cycling logic. - FR-8.4.3:
report.snapshot_every: <duration>— periodic dumps of all alias values, wear counters, and degradation parameters (CSV/JSONL) for trend/historian verification. - FR-8.4.4: Continuous-mode verdict: PASS iff the dataset schedule completed, no invariant episode occurred, no rate limit was exceeded, and no assertion failed; otherwise FAIL/ERROR accordingly.
- FR-8.4.5: Report and log sizes shall be bounded independent of run length (episode aggregation + ring-buffered recent events).
- FR-8.4.6: A limited
sequence:may still be used in continuous mode; steps may be scheduled viaat: <sim duration>(e.g., a service visit at day 12). Scheduled steps and datasets interleave on the same sim-time axis.
8.5 Example — 28-day pump-station soak
Scenario (YAML):
name: pump_station_soak_28d
description: "28-day soak: demand/operator playback, closed-loop plant with pump degradation"
mode: continuous
timing: { tick: 100ms }
inputs:
start_button: { type: di, channel: 1 } # exogenous (operator, CSV)
em_stop_ok: { type: di, channel: 2 } # exogenous (CSV)
inflow_valve: { type: di, channel: 3 } # exogenous (CSV)
inflow_rate: { type: ai, channel: 2, scale: 0.01 } # exogenous (CSV), m³/h → counts
level_sensor: { type: ai, channel: 1, scale: 0.1 } # endogenous (plant model computes)
outputs:
pump_relay: { type: do, channel: 1 }
valve_open: { type: do, channel: 2 }
speed_setpoint: { type: ao, channel: 1, scale: 0.1 }
maintenance_alarm: { type: do, channel: 3 }
datasets:
demand_week: # exogenous demand profile
file: data/demand_week.csv
aliases: [inflow_rate]
interpolation: linear
repeat: 4 # 7-day profile × 4 = 28 sim-days
operator_week: # operator actions
file: data/operator_week.csv
aliases: [start_button, em_stop_ok, inflow_valve]
interpolation: hold
repeat: 4
degradation:
pump_wear:
model: linear
parameter: pump_capacity # consumed by the plant model
per_runtime_hour: 0.0004 # 0.04 %/h → ~10 % after ~10.4 sim-days
per_start_cycle: 0.0002 # 0.02 % per start
floor: 0.80
maintenance_at: 0.90
on_threshold: # fires when capacity crosses 90 %
- inject_diagnosis: { module: 4, channel: 1, bits: [user_overrange] }
- assert: { label: "maintenance alarm raised",
conditions: { maintenance_alarm: ON }, within: 60s }
on_reset: # after maintenance_reset
- clear_diagnosis: { module: 4 }
sequence: # scheduled events (sim-time)
- at: 12d
maintenance_reset: pump_wear # service visit: capacity restored
plugins: [plugins.pump_plant.pump_plant] # loop: d(level)/dt = inflow − pump_flow(speed, capacity)
invariants:
- { alias: level_sensor, min: 200, max: 9000, label: "tank level bounds" }
rate_limits:
- { alias: pump_relay, edge: rising, max_per_hour: 6, label: "pump start cycles" }
report:
snapshot_every: 1h
aggregate: episodes
Datasets (CSV):
# data/demand_week.csv (elapsed seconds, inflow in m³/h)
time,inflow_rate
0,12.0
3600,12.4
7200,13.1
...
604800,12.0 # week boundary — loops seamlessly with repeat: 4
# data/operator_week.csv
time,start_button,em_stop_ok,inflow_valve
0,OFF,ON,OFF
28800,ON,ON,ON # Mon 08:00: shift start, valve opened
28810,OFF,ON,ON
432000,OFF,OFF,ON # Sat 00:00: weekend e-stop drill
440000,OFF,ON,ON
8.6 Architecture impact (planned modules)
src/wago_sim/scenario/
├── datasets.py # CsvDataset: streaming reader, interpolation, repeat, time_factor
├── degradation.py # WearCounters, DegradationModel registry, maintenance actions
├── invariants.py # invariant & rate-limit evaluation, episode aggregation
└── engine.py # mode: continuous, parameter() accessor, snapshot scheduler
erDiagram
ContinuousScenario ||--|{ Dataset : datasets
Dataset ||--|{ ColumnMapping : "alias columns"
ContinuousScenario ||--|{ DegradationModel : degradation
DegradationModel ||--|{ WearCounter : counts
WearCounter }o--|| Alias : observes
ContinuousScenario ||--|{ Invariant : invariants
Invariant ||--o{ Episode : violations
ContinuousScenario ||--|{ RateLimit : rate_limits
ContinuousScenario ||--o{ Snapshot : "periodic dumps"
PlantPlugin }o--|| DegradationModel : "reads parameters"
8.7 Non-functional requirements (long-duration)
- NFR-8.7.1: Memory O(1) in dataset length and run duration (streaming + bounded caches).
- NFR-8.7.2: A 28-sim-day soak with two datasets completes headless in ≤ 1 hour wall-clock on a standard PC (budget to be confirmed after implementation).
- NFR-8.7.3: Determinism incl. degradation: identical config + datasets produce identical reports (except wall-clock stamps).
- NFR-8.7.4: Robustness: host sleep/hibernate or GC pauses never distort sim-time or dataset position (sim-time accumulates by tick, never from wall-clock deltas — except when explicitly paced per FR-8.2.10).
- NFR-8.7.5: Report and log artifacts stay bounded (FR-8.4.5 mechanism), enabling unattended multi-day runs.
8.8 Acceptance criteria (long-duration simulation)
- A ≥ 100,000-row CSV streams with constant memory; alias values at arbitrary ticks match the expected interpolation exactly.
linearinterpolation for analog andholdfor digital verified against hand-computed samples; digital aliases are never interpolated.repeat: 4of a 7-day dataset produces four identical per-loop value sequences (seamless seam, deterministic).time_factor: 10consumes a 7-day dataset in 16.8 h of sim-time.- An alias-ownership conflict (dataset + ramp on one alias) is rejected at validation time.
- Degradation:
pump_capacitydeclines per the configured formula; crossingmaintenance_atinjects the configured diagnosis and the maintenance-alarm assertion passes;maintenance_resetrestores capacity to 1.0 and clears the diagnosis. - An invariant breach is recorded as one episode (start, end, peak) regardless of duration; a rate-limit breach fails the run.
- A full 28-sim-day soak completes headless with a bounded report and a correct PASS/FAIL/ERROR verdict.
9. Feature Request — Bench Mode (Run Without a Scenario)
Status: requested 2026-10-03, planned (roadmap phase 2), partially available today.
wago-sim serve --node <config>already runs the Modbus TCP server with a static process image and no scenario engine; this section formalizes the mode and its supporting tooling.
9.1 Motivation
Early commissioning and interactive troubleshooting do not follow a script: an engineer wants the virtual node up, connected to the PLC/SCADA, and then manually observe and tweak values — the way one would probe a real node with a screwdriver and a multimeter. Requiring a scenario file for this is friction; a syntax error in an unrelated YAML file should not block a bench session.
Bench mode is also the foundation for manual FAT exploration and for the web UI (§10), which needs a long-lived node whose I/O can be manipulated at runtime without a scenario driving it.
9.2 Requirements
- FR-9.2.1:
wago-sim serve --node <config>shall start the Modbus TCP server with no scenario attached: the process image starts at defined defaults (all-zero), the watchdog/statistics behave normally, and the server runs until terminated. (Available today.) - FR-9.2.2:
--set alias=valueCLI options (repeatable) shall seed input values at startup, e.g.--set DI0.1=ON --set AI4.0=1234, addressing channels by the module notation of FR-4.3.11. Optional in v1: a small startup-commands file for larger value sets. - FR-9.2.3: An interactive console monitor (
--monitor): periodically prints a compact table of all I/O channels (module, type, address, value) and highlights changes, so a bench session is observable in a terminal without any UI. - FR-9.2.4: Runtime manipulation via the console monitor: simple commands
(
set DI0.1 ON,pulse DO2.0 500ms,watch 0x1029,reset stats) — the same operations the web UI (§10) offers, over stdio. - FR-9.2.5:
serveshall be combinable with a scenario later (--scenariobecomes optional); running with neither scenario nor monitor prints a hint that the node is passive and values can only be changed by a master or via the web UI. - FR-9.2.6: The console monitor and web UI (§10) must never violate device semantics — manipulations go through the same datastore paths as Modbus writes, so read-back mirrors, watchdog kicks, and statistics stay consistent.
9.3 Acceptance criteria
wago-sim serve --node config/pump_node.yamlstarts with no scenario file present and serves a static image; a pymodbus client can read and write process data.--set DI0.1=ONseeds the corresponding input bit; a master reading bit address 0 sees ON.- The console monitor shows the full channel table and flags value changes within one refresh interval.
- Console
set/pulse/watchcommands affect the image immediately and are visible to a connected master in the same way Modbus writes are.
10. Feature Request — Web UI (I/O Overview & Manipulation)
Status: requested 2026-10-03, planned (roadmap phase 5), not yet implemented. A browser front-end overlooking and manipulating the simulated node's I/O values and states at runtime.
10.1 Motivation
Terminal monitors (§9) suffice for single engineers, but FAT benches are collaborative: commissioning engineers, PLC programmers, and test witnesses need a shared, visual view of the virtual I/O — the equivalent of watching the LEDs on a real WAGO node, plus the ability to force signals the way WAGO's own WBM does on real devices. A web UI keeps the simulator usable from any machine on the bench network without installing tools.
10.2 Requirements
- FR-10.2.1: The simulator shall serve a web UI (default port 8080, configurable) alongside the Modbus TCP server, with no external backend dependencies beyond Python (FastAPI/Starlette + static files, or an equivalent embedded approach — implementation choice, to be decided).
- FR-10.2.2: The UI shall present the node overview: module list with
article numbers, per-module channel tables (type, module, channel,
addressing
DI0.1per FR-4.3.11, Modbus address, current value/state), derived from the same process image the Modbus server serves. - FR-10.2.3: Values shall update live (WebSocket push or short-polling), including changes made by the PLC/master over Modbus, by scenarios, by the console monitor, or by other UI clients — one shared source of truth.
- FR-10.2.4: Manipulation: clicking a DI/DO toggles it; an AI/AO gets a
numeric input (engineering units, using alias scale/offset when
defined);
pulse(with duration) andramp(target + duration) actions on DI/DO/AI. All writes go through the datastore's Modbus-equivalent paths (FR-9.2.6) so mirrors, watchdog, and statistics stay consistent. - FR-10.2.5: Diagnostics view: watchdog status (0x1006), statistics block (0x1029, with reset button), module diagnosis (0x1050), LED error registers (0x1020/0x1021) — mirroring WBM pages of the real device — with controls to inject/clear faults for FAT fault cases.
- FR-10.2.6: A scenario/run view: which scenario (if any) is loaded, current step, verdicts so far, and basic controls (start/abort). Headless result reports (JSONL/CSV) remain downloadable via the CLI.
- FR-10.2.7: Multi-client consistency: several simultaneous browser clients see the same state; forced values propagate to all clients and to the Modbus side within one refresh interval.
- FR-10.2.8: Access control for shared benches: optional read-only mode and (v2) token-based login, so witnesses can observe without accident rights.
- FR-10.2.9: The UI must not disturb deterministic scenario runs: when a scenario owns an alias, UI writes to it are rejected with a clear message (alias-ownership rules of §8 FR-8.2.2 apply).
10.3 Architecture sketch (planned)
src/wago_sim/
├── webui/
│ ├── __init__.py
│ ├── app.py # HTTP + WebSocket server (asyncio, same loop as Modbus)
│ ├── api.py # REST endpoints: node layout, values, writes, diagnostics
│ └── static/ # single-page front-end (no build step)
The web server runs in the same asyncio event loop as the Modbus TCP server, so a UI write and a Modbus read never race on the datastore.
10.4 Acceptance criteria
- Opening the UI shows the node's module list and channel table exactly
as configured, with
DI0.1-style addresses and Modbus addresses. - Toggling a DO in the browser is immediately visible to a connected Modbus master (and vice versa: a master write appears in the UI within one refresh interval).
- Forcing an AI value uses engineering units and the configured scale.
- The diagnostics page shows live watchdog status and statistics; the reset button clears the counters (register 0x1029 semantics).
- With a running scenario, writes to scenario-owned aliases are rejected with a visible message; the scenario continues unaffected.
- Two simultaneous browsers stay consistent with each other and with the Modbus image.
11. Roadmap (post-framework)
| Phase | Feature |
|---|---|
| 2 | Module catalog expansion (analog, counter, SSI, DALI/DSI, serial modules), mailbox parameters; bench mode tooling (§9): --set seeding, console monitor, runtime manipulation commands |
| 3 | Long-duration simulation (§8): CSV dataset playback, degradation models, invariants, soak-test reporting |
| 4 | FAT report generator (HTML/PDF), scenario comparison, template library per project type |
| 5 | Multi-node orchestration (several couplers in one FAT bench), UDP variant, WBM simulation; web UI (§10) |
| 6 | CI integration: run scenarios headless, export JUnit XML |
12. References
- WAGO 750-362 manual
m07500362_xxxxxxxx_0en.pdf, sections 7.2–7.3 (process image), 11.2 (Modbus), 12.2 (module data architecture) - WAGO I/O System 750 system description
m075xxxxx_xxxxxxxx_0en.pdf - MODBUS Application Protocol Specification V1.1b3 / MODBUS TCP specification (modbus.org)