# 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: 1. Physical I/O hardware (expensive, must be wired per project, error-prone, single-purpose), or 2. 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 ```mermaid 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 `BadProtocol` statistic 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: - `0x1000` timeout value ×100 ms (default `0x0064`), writable only when WD inactive. - `0x1001`/`0x1002` coding masks (bit *n−1* of the mask = FC *n*; default `0xFFFF`). - `0x1003` trigger (write nonzero, alternating values), `0x1005` stop (`0xAAAA` then `0x5555`), `0x1008` simple stop (`0xAA55`/`0x55AA`), `0x1007` restart (write `1`). - `0x1006` status: `0` inactive / `1` active / `2` expired. - `0x1009` close-socket-on-timeout, `0x100A` type Standard/Alternative, `0x100B` save 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 = 1` additionally 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 writing `0xAA55`/`0x55AA`. - `0x102A` active TCP connections. - `0x1030` Modbus TCP idle timeout (×100 ms). - `0x1031` MAC-ID (3 words, configurable to emulate a specific device). - `0x1037` response delay (ms) — the simulator shall actually delay responses, to emulate link latency. - `0x1038` TOS/DSCP (stored, echoed). - `0x1050` module diagnosis (3 words): module, channel, diagnosis bits (underrange … collective failure) — scriptable fault injection. - `0x1020`/`0x1021` LED error code/argument — scenario-settable. - `0x102B` KBUS 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 → `SlaveDeviceFailure` statistic increments and process-data reads may return exception `0x04`. - Response-delay injection via `0x1037`. - Connection refusal beyond 15 concurrent connections. - Illegal-address probing (read/write the exception areas) → correct `0x02` responses (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 byte `D1` on 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_value` on 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) ```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 (`DO` on 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 1. PLC program boots, connects to simulator (Modbus TCP client), reads input image. 2. Scenario `pump_station_fat` starts in `idle` state; `em_stop_ok` turns ON. 3. Scenario pulses `start_button`; PLC sees input, starts pump relay (writes coil). 4. Engine detects rising edge on `pump_relay`, asserts `valve_open` within 2 s. 5. Scenario ramps `level_sensor` to 8500 counts; PLC adjusts `speed_setpoint`; assertions verify. 6. Scenario injects fault (KBUS error / watchdog timeout); PLC must react (e.g., switch to safe state) — asserted. 7. 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 ```mermaid 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 ```mermaid 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) 1. A Modbus TCP master (e.g., `pymodbus` client in tests) can connect, read constants `0x2000–0x2008`, and gets exactly `0x0000, 0xFFFF, 0x1234, 0xAAAA, 0x5555, 0x0166, ...` per spec — byte order verified. 2. Reads/writes in all mapped/unmapped areas produce values/exceptions per §3.3 tables. 3. Digital channel addressing: a configured 16-channel DI module + 8-channel DO module yields channel-number bit addressing (DI 47 → bit address 46). 4. Output readback mirrors at +`0x0200` (word) and +`0x0200`/+`0x1000` (bit/word area 2) work. 5. A YAML scenario can set a digital input, detect a PLC output rising edge, and produce a PASS/FAIL report file. 6. Watchdog: configure `0x1000`, trigger via `0x1003`, let it expire, outputs clear, `0x1006` = 2, process access → exception `0x04`, restart via `0x1007`. 7. Statistics counters increment correctly on bad frames/addresses; reset via `0x1029` write `0xAA55`. --- ## 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 `continuous` mode 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`: 1. **Dataset playback** — designated inputs are driven from time-series files (e.g., CSV exported from a plant historian, or synthesized profiles). 2. **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`. The `sequence` becomes optional; the run continues until the dataset schedule (plus any `until:` 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 via `time_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 accept `ON/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 to `set:` semantics. - FR-8.2.5: Between samples, analog aliases use per-dataset `interpolation: linear | hold`; digital aliases always use `hold` (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: n` loops a dataset seamlessly and monotonically (no time jump at the seam); identical loops produce identical values (deterministic). `until: ` 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, register `0x1050`), assertions (maintenance alarm within a time bound), and logging. A `maintenance_reset` restores 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) and `maintenance_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: ` — 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 via `at: ` (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): ```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): ```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 ``` ```csv # 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 ``` ```mermaid 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) 1. A ≥ 100,000-row CSV streams with constant memory; alias values at arbitrary ticks match the expected interpolation exactly. 2. `linear` interpolation for analog and `hold` for digital verified against hand-computed samples; digital aliases are never interpolated. 3. `repeat: 4` of a 7-day dataset produces four identical per-loop value sequences (seamless seam, deterministic). 4. `time_factor: 10` consumes a 7-day dataset in 16.8 h of sim-time. 5. An alias-ownership conflict (dataset + ramp on one alias) is rejected at validation time. 6. Degradation: `pump_capacity` declines per the configured formula; crossing `maintenance_at` injects the configured diagnosis and the maintenance-alarm assertion passes; `maintenance_reset` restores capacity to 1.0 and clears the diagnosis. 7. An invariant breach is recorded as one episode (start, end, peak) regardless of duration; a rate-limit breach fails the run. 8. 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 ` 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 ` 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=value` CLI 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: `serve` shall be combinable with a scenario later (`--scenario` becomes 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 1. `wago-sim serve --node config/pump_node.yaml` starts with no scenario file present and serves a static image; a pymodbus client can read and write process data. 2. `--set DI0.1=ON` seeds the corresponding input bit; a master reading bit address 0 sees ON. 3. The console monitor shows the full channel table and flags value changes within one refresh interval. 4. Console `set`/`pulse`/`watch` commands 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.1` per 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) and `ramp` (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 1. Opening the UI shows the node's module list and channel table exactly as configured, with `DI0.1`-style addresses and Modbus addresses. 2. 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). 3. Forcing an AI value uses engineering units and the configured scale. 4. The diagnostics page shows live watchdog status and statistics; the reset button clears the counters (register 0x1029 semantics). 5. With a running scenario, writes to scenario-owned aliases are rejected with a visible message; the scenario continues unaffected. 6. 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)