Files
wago-modbustcp-simulator-py/docs/spec/software-spec.md
T
2026-10-03 09:46:11 +02:00

43 KiB
Raw Blame History

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

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)

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

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)

  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: <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, 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: <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 via at: <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)

  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 <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=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)