# Software Specification **WAGO Modbus TCP Simulator — Autonomous FAT Testing Framework** | | | |---|---| | Project | `wago-modbustcp-simulator-py` | | Version | 1.4 (Draft for review) | | Date | 2026-10-03 | | Status | Framework phase; module addressing implemented (FR-4.3.11); service/container deployment implemented (§12); plain-text request tracing implemented (NFR-6.4); clean CLI error handling implemented (NFR-6.5 CLI part); web UI §10 in implementation (M1–M5a done: auth, overview, manipulation, diagnostics, configuration, channel names, import/export, split overview; M6 scenario view open); long-duration simulation (§8), bench mode (§9), runtime device configuration (§11 — coupler/module/channel-name subset now covered by web UI M5/M5a), multi-vendor device profiles (§13) and additional fieldbus protocols PROFINET/COMLI (§14) 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 is observable in verbose mode (`-v` / `WAGO_SIM_VERBOSE`) as plain text — decoded `REQ`/`RSP`/`EXC` lines with transaction id, unit, function name, values, and the WAGO 750-362 register-map meaning of each address (process areas with %IW/%QW mapping per Tables 119/120, read-back mirrors, configuration/constant registers, illegal areas); malformed frames are traced as `RAW` lines with PDU hex. *Implemented* (`src/wago_sim/trace.py`). - NFR-6.5 Robustness: malformed frames never crash the server; counted in statistics. Invalid operator input — unknown commands or options, non-numeric or out-of-range port numbers (CLI or `WAGO_SIM_PORT`), missing/unparsable node or scenario files, unwritable report paths — yields a one-line `wago-sim: error: …` on stderr and exit code 2, never a Python traceback; missing report directories are auto-created; unexpected internal errors keep the traceback (logged) for debugging. *CLI part implemented* (tested in `tests/test_cli.py`). - 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`, and both run **in parallel on the same sim-time axis**: 1. **Dataset playback** *(optional)* — designated inputs are driven from time-series files (e.g., CSV exported from a plant historian, or synthesized profiles). 2. **Signal logic** — plant-model plugins (FR-4.3.8) compute endogenous signals and closed-loop physics in Python, scheduled sequence steps (`at:`) perform discrete actions, and degradation models age plant parameters (e.g., pump capacity) from wear, exercising the PLC’s compensation and maintenance logic. Datasets are **not required**: a continuous run may be driven entirely by logic (plugin-generated demand profiles, scheduled events, degradation), entirely by datasets, or by any mix — some aliases fed from CSV while others are computed by logic, in the same run. The signal logic works unchanged whether or not CSV streaming is used. ### 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 **all active drivers are exhausted**: every dataset finished (or looped to its `repeat` limit), all scheduled steps executed, and — if a top-level `until: ` is given — the duration elapsed. With no datasets, `until:` is the primary run-length control. A continuous scenario with neither datasets, nor scheduled steps, nor `until:` is a validation error (it would run forever). - FR-8.2.2: Input aliases (di/ai) may be driven from **datasets**, which are entirely **optional**. Output aliases must not appear in datasets (validation error). Aliases not owned by a dataset are **logic-driven**: computed by a plant-model plugin (FR-4.3.8, closed loop), set by scheduled steps, or left at their current value. An endogenous process variable (e.g., tank level) is computed by the plant model. **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. - FR-8.2.12: **Parallel logic execution**: datasets and signal logic run in the same deterministic tick pipeline, in a fixed order per tick: (1) datasets apply their sampled/interpolated values to their aliases, (2) alias edges are detected, (3) plant-model plugins run — they may read dataset-driven AND logic-driven aliases and compute endogenous variables in the same tick, (4) degradation models update, (5) invariants and rate limits evaluate, (6) scheduled steps due at this tick execute, (7) snapshots are taken if due. This order is part of the specification and shall be covered by tests. - FR-8.2.13: **Logic-only continuous runs**: with no `datasets:` block at all, continuous mode shall work unchanged — demand profiles and other exogenous signals may be generated *inside* plant-model plugins (e.g., a daily sinusoid or a state-based profile computed in Python), with top-level `until:` bounding the run. All §8.3–8.4 machinery (degradation, invariants, rate limits, snapshots) applies identically, whether or not any CSV streaming is configured. ### 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 ``` **Variant without datasets** (logic-only, same soak): drop the `datasets:` block entirely, keep everything else, and let the plant plugin *also* generate the demand signal each tick: ```yaml name: pump_station_soak_28d_logic mode: continuous until: 28d # no datasets: until bounds the run inputs: ... # same aliases — none dataset-owned outputs: ... plugins: [plugins.pump_plant.pump_plant] # plugin computes, per tick (FR-8.2.12 step 3): # inflow_rate = 12 + 3*sin(2*pi*t/24h) # d(level)/dt = inflow - pump_flow(speed, capacity) degradation: ... # identical to the dataset version invariants: ... rate_limits: ... report: { snapshot_every: 1h } ``` Both variants test the same PLC behavior; the CSV version replays recorded demand, the logic version synthesizes it. Mixing is equally valid — e.g. `inflow_rate` from a dataset while `start_button` is toggled by scheduled `at:` steps, with the plant plugin reading both (FR-8.2.12). ### 8.6 Architecture impact (planned modules) ``` src/wago_sim/scenario/ ├── datasets.py # CsvDataset: streaming reader, interpolation, repeat, │ # time_factor — OPTIONAL driver in continuous mode ├── degradation.py # WearCounters, DegradationModel registry, maintenance actions ├── invariants.py # invariant & rate-limit evaluation, episode aggregation └── engine.py # mode: continuous, parameter() accessor, snapshot # scheduler; fixed tick pipeline per FR-8.2.12: # datasets -> edges -> plugins -> degradation -> # invariants -> scheduled steps -> snapshots ``` ```mermaid erDiagram ContinuousScenario ||--o{ Dataset : "datasets (optional)" Dataset ||--|{ ColumnMapping : "alias columns" ContinuousScenario ||--|{ PlantPlugin : "signal logic (always runs)" 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" PlantPlugin }o--o{ Alias : "reads dataset-driven + computes logic-driven" ``` ### 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. A continuous run with **no datasets** (plugin-generated demand, top-level `until:`) completes with degradation, invariants, rate limits and snapshots all active, and a correct verdict (FR-8.2.13). 10. A **mixed run** — one alias driven by a dataset, another computed by a plugin in the same run — is supported; the plugin observes the dataset value applied earlier in the same tick (FR-8.2.12 order), verified by a tick-order test. 11. A continuous scenario with no datasets, no scheduled steps, and no `until:` is rejected at validation time (FR-8.2.1). --- ## 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: in implementation (2026-10-03).** The implementation > decisions below are binding; milestones M1–M4 are implemented and > verified (M5 configuration and M6 scenario view remain). 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. On the node graphic (requested 2026-10-04, refined same day): each module shows its kind+slot designation (`DI0`, `DO2`, `AI4`, …) in the module's **lower left corner, below the channel indicators**, and the channel indicators carry only the 0-based channel number: below each DI/DO LED, and on analog modules outside the indicator stack, above the overflow and below the underflow LED (FR-10.2.14) so the limit LEDs stay adjacent to the bar. DI/DO channels lay out in **vertical columns of eight** — channels 0–7 in the left column, 8–15 in the next — and modules are drawn as **thin, tall bezels** mirroring the proportions of real WAGO DIN-rail housings (narrow width, vertically stacked LEDs). - 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. Clicking an analog **bar indicator** (requested 2026-10-04) sets the value proportionally to the click position within the channel's display range (FR-10.2.14) — AI = field side, AO = master-side force; exact values remain enterable via the channel list. - 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). - FR-10.2.10: The UI's **configuration** pages (Ethernet/port settings, Modbus watchdog settings, I/O Config with SAVE CONFIG, Modbus Mapping overview, address-selection DIP switch) are specified in §11; §10 covers value/state oversight and manipulation. - FR-10.2.11: **Channel names** (requested 2026-10-03): every module channel can carry an application-specific name (e.g. `start_button`, `tank_level`). Names live in the node configuration — not in the module catalog (catalog articles are shared per type) — persisted alongside the module list, and are re-applied after process-image rebuilds (§11). Unnamed channels fall back to the generic channel label (e.g. `DI 1`). Names surface everywhere the UI lists a channel: node overview, the channel list, the configuration editor, and tooltips. - FR-10.2.12: **Import/export of module configuration** (requested 2026-10-03): the complete module configuration — module list with slot order, per-channel names, and coupler settings — can be downloaded as a single JSON file and imported back. Import validates the payload (known articles, ≤ 64 modules, name shapes) and applies it atomically: module rebuild + channel names + coupler settings in one step, with the same persistence behavior as SAVE CONFIG (§11). Export/import makes FAT benches shareable and reproducible between engineers and benches. - FR-10.2.13: **Split overview** (requested 2026-10-03): the overview page is divided in two — the realistic node graphic (DIN rail with LEDs, FR-10.2.2) on top, and below it a scrollable **channel list**: one row per channel with the I/O code (`DI0.0`), the configured channel name (if any), and a control for the value — a switch for DI/DO and a numeric field for AI/AO — which both displays the live value and manipulates it (same semantics as FR-10.2.4: DI = field side, DO/AO = master-side force through internal paths). The list updates live like the rail (FR-10.2.3) and is the primary working surface for benches with many channels. - FR-10.2.14: **Analog display ranges** (requested 2026-10-04): the analog bar indicators (rail + list) scale a channel's raw value against a display range. The default range is 0–4095 counts (100 % at 4095, matching the WAGO 750 analog modules' 12-bit scaling); per-channel `min`/`max` can be configured in the node configuration — used by both AI (generated) and AO (used) values. Values within the range fill the bar 0–100 % from the bottom. Underflow/overflow are indicated: the bar fills in the limit color (red) and a small red LED above the bar lights for overflow, below the bar for underflow, clearing when the value returns to range. **Signed values** (2026-10-04): range bounds may be negative — `-65534 <= min < max <= 65535` (the raw 16-bit image holds 0–65535; a negative display bound lets signed plant quantities map onto the bar). Simulated AI values and AO forces accept negative numbers within the same window; values are stored in the image as their 16-bit two's-complement representation. ### 10.3 Implementation decisions (binding, 2026-10-03) - **Framework**: FastAPI (Starlette) + Uvicorn, embedded **in-process on the same asyncio event loop** as the Modbus TCP server — a UI write and a Modbus request never race on the datastore. - **Front-end**: single-page vanilla HTML/CSS/JS (no build step) under `src/wago_sim/webui/static/`; realistic node graphics (DIN-rail strip, module bezels, glowing per-channel LEDs for DI/DO, bar/numeric indicators for AI/AO) drawn with inline SVG/CSS. - **Authentication** (user request 2026-10-03): session-cookie login with in-memory token store; default credentials `admin` / `simu`, overridable via env (`WAGO_SIM_WEB_USER` / `WAGO_SIM_WEB_PASSWORD`); optional read-only mode via `WAGO_SIM_WEB_READONLY`. No third-party auth/session dependencies (`hashlib.scrypt` hashing; sessions need not survive restarts on a bench). - **Backend data access** (user request 2026-10-03): the API **never uses Modbus TCP**. It reads via the internal memory handler (`WagoDatastore` getters, `VirtualNode` channel model, `ConfigRegisterBank`, `ModbusWatchdog`, `ModbusStatistics`) and writes via internal paths chosen by role: simulated-field writes go through `set_digital_input_channel()` / `set_input_word()` (as the scenario engine does); master-side forcing goes through the datastore's internal `write_bits()` / `write_words()` dispatch (mirrors, watchdog kicks, and statistics stay consistent, FR-9.2.6 semantics) — but always as direct in-process calls, never via TCP. - **Live updates**: WebSocket push (`/api/ws`) — datastore `add_listener` for output changes plus a 10 Hz snapshot-difference sweep for input-side changes. - **Wiring**: `serve` starts the web UI by default (`--no-web` to disable; `--web-port`, env `WAGO_SIM_WEB_PORT`; default port 8080). ### 10.4 Architecture sketch (as-built) ``` src/wago_sim/ ├── webui/ │ ├── __init__.py │ ├── app.py # build_webui(app, ...) -> FastAPI; API + static │ ├── auth.py # login/logout, scrypt hash, session dependency │ ├── state.py # snapshot builder + change detection │ ├── api.py # REST + WebSocket routes │ └── static/ # single-page front-end (no build step) ``` ### 10.5 API surface (as-built) | Method/Route | Purpose | FR | |---|---|---| | `POST /api/auth/login`, `POST /api/auth/logout` | session cookie (`HttpOnly`) | auth | | `GET /api/node` | module list, channel tables with `DI0.1`-style and Modbus addresses | 10.2.2 | | `GET /api/values` | full channel-value snapshot | 10.2.3 | | `WS /api/ws` | live value/state deltas | 10.2.3, 10.2.7 | | `POST /api/channels/{kind}/{module}/{channel}` | set/toggle/pulse/ramp a channel (field side) | 10.2.4 | | `POST /api/force` | master-side force at a Modbus address (internal dispatch) | 10.2.4 | | `GET /api/diagnostics` | watchdog status, statistics, module diagnosis, LED errors | 10.2.5 | | `POST /api/diagnostics/reset-stats` | statistics reset (0x1029 semantics) | 10.2.5 | | `POST /api/diagnostics/inject-fault`, `POST /api/diagnostics/clear-fault` | LED / module-diagnosis injection | 10.2.5 | | `GET/PUT /api/config/coupler` | watchdog settings, response delay, idle timeout, MAC-ID | §11 subset | | `GET/PUT /api/config/modules` | module list CRUD + process-image rebuild | §11 FR-11.2.1–11.2.4 | | `GET /api/config/export` · `POST /api/config/import` | full configuration (modules + channel names + coupler) as JSON | 10.2.12 | | `GET/PUT /api/config/channel-names` | read/write per-channel names | 10.2.11 | ### 10.6 Milestones | # | Deliverable | |---|---| | M1 | WebUI skeleton, in-process FastAPI, CLI/env wiring, authentication (`admin`/`simu`), `GET /api/node` | | M2 | Node overview page — DIN-rail graphics, LEDs, analog indicators, live values (WS) | | M3 | Manipulation — toggle/pulse/ramp, AI engineering units, master-side force | | M4 | Diagnostics — watchdog/statistics/diagnosis view, reset, fault injection | | M5 | Configuration — coupler forms; module CRUD with image rebuild and persistence | | M5a | Channel names (FR-10.2.11) + configuration import/export (FR-10.2.12) + split overview with channel list (FR-10.2.13) | | M6 | (deferred) Scenario run view (FR-10.2.6) + alias-ownership rejection (FR-10.2.9) | ### 10.7 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. Feature Request — Runtime Device Configuration (WBM/DIP-Switch Parity) > **Status: requested 2026-10-03, planned (roadmap phase 5), not yet > implemented.** Mirrors the real 750-362's configuration mechanisms so > the FAT bench can commission the node the way a real one is > commissioned. ### 11.1 Motivation On the real coupler the *node* is configured through two mechanisms (manual §4.4.2, §8.3, §9.11–9.18): 1. **Web-Based Management (WBM)** pages: Ethernet/port settings (IP address, subnet, gateway, DHCP/BootP/EEPROM source, base IP), Modbus settings (connection idle timeout, Modbus watchdog type/timeout/trigger masks, keepalive), Modbus Mapping (per-module register/coil address map, read-only), and I/O Config (connected module list + **SAVE CONFIG**, which re-reads the terminal bus). 2. **Address-selection DIP switch** (8-pole): binary host ID 1–254 = last IP octet (base network 192.168.1.0/24 in delivery state); 0 = use the stored WBM port configuration; 255 = DHCP. Changing it requires a restart to take effect. The I/O modules themselves are **not** configured via WBM or DIP switch — they are physically plugged/unplugged, and the coupler re-reads the terminal bus (SAVE CONFIG / KBUS reset, register `0x102B`), rebuilding the process image. The simulator equivalent of "plugging" a module is a **runtime module-list change with image rebuild**. Today the simulator freezes all of this at startup: module list from YAML, watchdog only via Modbus registers, no DIP switch, no WBM pages. This feature request closes that gap. ### 11.2 Requirements — runtime module configuration ("plug/unplug") - FR-11.2.1: The node's module list shall be **mutable at runtime** via the web UI (§10), the console monitor (§9), and a programmatic API: insert/remove modules at slot positions, replicating physical plug/unplug on the local bus. - FR-11.2.2: After every change the process image is **rebuilt exactly like the real device after a KBUS reset** (`0x102B` or WBM SAVE CONFIG): word modules first in physical order, then bit modules byte-packed (§7.3.2.1); image sizes updated in `0x1022–0x1025`; module descriptions updated in `0x2030+` so a polling master sees the new configuration. - FR-11.2.3: Scenario aliases are **revalidated** after a rebuild. Module-based addresses (FR-4.3.11) are position-stable — `DI2.1` stays on module 2 when earlier slots change — while pool-based indexes may shift; aliases orphaned by a removal are reported as configuration errors. This difference shall be documented and covered by tests. - FR-11.2.4: While a scenario is running, module reconfiguration is rejected with a clear message (consistent with FR-10.2.9) — deterministic runs must not be disturbed. In bench mode (§9) it is always allowed. - FR-11.2.5: A reconfiguration event is logged (sim-time, change description, new image sizes) and visible in the web UI's I/O Config page, mirroring the WBM SAVE CONFIG flow. ### 11.3 Requirements — address-selection DIP switch emulation - FR-11.3.1: An 8-pole address-selection switch shall be emulated per node with the manual's exact coding (§4.4.2/§8.3.1): DIP 1 = LSB … DIP 8 = MSB; **1–254** = host ID appended to the base network (default 192.168.1.0/24, changeable); **0** = use the stored port configuration (WBM port settings); **255** = DHCP (virtual: an address from the configured DHCP pool is reported). - FR-11.3.2: The switch shall be visual and operable in the web UI (toggle poles) and settable in the console/config; the **derived virtual IP address** is displayed and introspectable via the API. - FR-11.3.3: **Apply semantics match the device**: a switch change marks the node "restart required"; on apply (explicit restart command or `serve` restart) the simulator rebinds its listen address to the derived IP when running in dedicated-IP mode, or updates the virtual IP model in shared-IP mode. Outputs and connections behave like a device restart. - FR-11.3.4: The stored port configuration (base network, static IP for switch position 0, DHCP pool) is part of the device state (FR-11.5.1). ### 11.4 Requirements — WBM-equivalent configuration pages (web UI) - FR-11.4.1: **Port/Ethernet page**: virtual IP settings — configuration source (static/EEPROM, DHCP, BootP), IP/subnet/gateway, base network for DIP addressing — persisted as device state and reported alongside the DIP-derived address. - FR-11.4.2: **Modbus settings page**: connection idle timeout (`0x1030`), Modbus watchdog fields (type Standard/Alternative, timeout, trigger masks FC1–16/FC17–32, close-socket-on-timeout) — the same fields the real WBM exposes (manual §9.16), mapped 1:1 onto registers `0x1000–0x100B`. Writes obey the register rules (e.g., timeout not changeable while the watchdog is active). - FR-11.4.3: **Modbus Mapping page**: per-module address map generated from the node layout — word addresses (input RO, output WO, read-back RW) and bit addresses (input, output, read-back) in the WBM's `[.]`–`[.]` notation (manual §9.17), plus the general area overview of §3.3. Read-only; auto-updates after FR-11.2.2 rebuilds. - FR-11.4.4: **I/O Config page**: module list (position, article, type, channel count) + **SAVE CONFIG** button = re-read/rebuild per FR-11.2.2, mirroring WBM §9.18. ### 11.5 Requirements — persistence & reset - FR-11.5.1: Device state (module list changes, DIP switch position, port configuration, watchdog settings written as remanent per `0x100B`) persists to a device-state file and is reloaded on start — the simulator's equivalent of the device's EEPROM. - FR-11.5.2: **Factory reset** (register `0x2043` write, CLI flag, web UI button): restores delivery state — module list from the node YAML, DIP switch to 0, watchdog defaults, cleared statistics — and persists that as the new device state. ### 11.6 Acceptance criteria 1. Removing a module and issuing SAVE CONFIG (or `0x102B`) rebuilds the image: `0x1022–0x1025` and `0x2030+` reflect the change and a pymodbus master reading process data sees the new layout. 2. Inserting a module before existing ones leaves module-addressed aliases (`DI2.1`) pointing at the same module, while pool-indexed aliases shift by the inserted channel count — asserted by tests. 3. Aliases orphaned by a module removal are reported as errors, not silently misaddressed. 4. Reconfiguration during a running scenario is rejected; in bench mode it succeeds and is logged. 5. DIP switch 00001001 (33) derives virtual IP 192.168.1.33; switch 0 shows the stored port configuration; 255 reports a DHCP-pool address; changing the switch shows "restart required" until applied. 6. Watchdog fields edited on the web settings page behave identically to register writes (same validation, same statistics). 7. Device state survives a simulator restart; factory reset restores delivery defaults. --- ## 12. Deployment — Service / Containerized Operation > **Status: implemented 2026-10-03 (v1.3).** The simulator can run as a > long-lived service: docker/compose container, or a systemd unit on bare > metal/VM. Verified: image builds, container answers Modbus requests, > and `docker stop` (SIGTERM) shuts down gracefully in < 1 s. ### 12.1 Motivation A FAT bench simulator is infrastructure: it must start on boot, survive restarts, log to the platform's log pipeline, and stop cleanly so a connected PLC sees a controlled fieldbus-node failure rather than a > hung socket — the exact condition watchdog/FAT fault cases test. ### 12.2 Requirements (all implemented) - FR-12.2.1: **Environment-variable configuration**: `WAGO_SIM_NODE`, `WAGO_SIM_HOST`, `WAGO_SIM_PORT` configure `serve` without CLI arguments (container `CMD ["wago-sim", "serve"]` needs zero arguments). Explicit CLI arguments override environment values. - FR-12.2.2: **Graceful shutdown**: on SIGINT/SIGTERM (docker stop, systemctl stop) the server closes all client connections and the listening socket via `server.stop()` before exiting — no forced kill, no lingering sockets. Verified: `docker stop` completes in 0.34 s. - FR-12.2.3: **Container image** (`Dockerfile`): `python:3.12-slim`, non-root `wago-sim` user, unbuffered stdout logging, baked-in example config for out-of-the-box startup, TCP health check, ~197 MB image. - FR-12.2.4: **Compose service** (`docker-compose.yml`): port mapping (5020→502 default; 502 needs privileges on some hosts), read-only mounts for config/scenarios/plugins, writable `out/` for reports, resource limits, log rotation, `restart: unless-stopped`. - FR-12.2.5: **systemd unit** (`deploy/wago-sim.service`): dedicated service user, env-var configuration, `Restart=on-failure`, hardened (NoNewPrivileges, ProtectSystem, CAP_NET_BIND_SERVICE for port 502 without root). - FR-12.2.6: Logs to stdout/stderr only (docker json-file / journald collect them); single-line format, no ANSI colors. ### 12.3 Acceptance criteria (all verified) 1. `docker build -t wago-sim .` succeeds; image ~197 MB. 2. `docker run -p :502 -e WAGO_SIM_NODE=... wago-sim` serves Modbus (constants `0x2000–0x2008` read correctly from the container). 3. `docker stop` completes in < 1 s with clean shutdown logs. 4. `docker compose up -d` starts a healthy service; `down` tears it down. 5. CLI tests cover env-var configuration and override precedence. --- ## 13. Feature Request — Multi-Vendor Device Profiles > **Status: requested 2026-10-03, not yet planned into a roadmap phase.** > Simulate additional Modbus TCP bus couplers — other WAGO 750 couplers > and other brands' Modbus TCP I/O heads — reusing the existing protocol > stack. This section captures the requirements; it does not commit to > a schedule. ### 13.1 Motivation FAT benches test more than one controller and fieldbus family. Today the simulator is single-device: the 750-362 register map, limits, watchdog, and statistics are hard-coded across the protocol stack (`pdu.py` limits, `datastore.py` address dispatch, `server.py` connection limit and process-area predicate, `registers.py`, `watchdog.py`, `stats.py`, `trace.py` annotations). The transport layer (`mbap.py`), PDU codecs, server skeleton, scenario engine, DSL, plugins, and reporting are protocol-generic and directly reusable. A second Modbus TCP device multiplies the value of the bench without a protocol rewrite: the deltas between Modbus TCP devices are — address maps, configuration areas, watchdog presence/semantics, statistics blocks, connection limits, and module catalogs with image-layout rules. These are exactly the facts the device manual provides. ### 13.2 Requirements - FR-13.2.1: **Device profile abstraction**: extract the per-device facts into a profile concept covering: supported function codes and per-FC limits, max concurrent connections, idle-timeout behavior, process-image address map (reads and writes), read-back mirror rules, configuration/diagnostic register areas and semantics, watchdog (presence, register layout, modes), statistics block layout, and identity/description registers. The 750-362 becomes the first profile; its behavior must be unchanged after the refactor (the existing test suite is the regression gate). - FR-13.2.2: **Scenario-engine independence**: the scenario engine, DSL, plugins, and reports must remain device-agnostic — they interact with a node only through the channel model (`ModuleType` / `Channel` / `ChannelLocation`) and the datastore's set/read surface. Scenarios must not require changes when a profile is added. - FR-13.2.3: **Per-vendor module catalogs**: catalogs keyed by brand; each entry defines word/bit widths and channels. A node config selects brand + module list; validation must reject cross-brand module mixes within one node (e.g., a WAGO 750-430 in an ABB S800 station). New catalogs are data-only additions (no engine changes). - FR-13.2.4: **Process-image layout rules per brand**: layout becomes a profile parameter (word-first + byte-packed bit modules is the WAGO §7.3.2.1 rule; other brands may pack differently — e.g., byte-per-channel images). The channel model and alias addressing (FR-4.3.11 module-based and pool-based schemes) must keep working on top of any layout. - FR-13.2.5: **Config registers / watchdog / statistics as pluggable blocks**: implementations following the existing `registers.py`/`watchdog.py`/`stats.py` pattern, one set per profile. Devices lacking a Modbus watchdog or statistics block expose nothing (or an illegal-address exception) there — that is a device fact, not a simulator limitation. - FR-13.2.6: **Trace annotations per profile**: `trace.py` address annotations (currently 750-362 Tables 119–122) must come from the active profile so verbose tracing stays meaningful for every device. - FR-13.2.7: **Profile selection**: node config gains a `device:` key (e.g. `device: wago-750-362`, the default for backward compatibility); CLI/`WAGO_SIM_NODE` paths unchanged. `wago-sim validate` must report unknown devices and cross-brand module mixes as clean errors. ### 13.3 Candidate devices (informative, non-binding) - WAGO 750 Modbus TCP family beyond the 362 (352/363/860…): same register-map family — profile effort is mainly constants and identity registers. - Other vendors' Modbus TCP I/O heads / controllers (e.g., Phoenix Contact Inline, Beckhoff BK/BC series): a profile written against the vendor manual; effort dominated by the address map and configuration-register semantics. ### 13.4 Acceptance criteria (draft) 1. The 750-362 profile passes the full existing test suite unchanged after the profile extraction. 2. A second profile (from §13.3) serves a PLC master that reads/writes its documented process image; identity/description registers match the vendor manual; the same scenario DSL drives its channels without modification. 3. Scenarios written for one device run unchanged against another device with equivalent channel configuration. 4. `wago-sim validate` reports unknown device keys and cross-brand module mixes with clean one-line errors. --- ## 14. Feature Request — Additional Fieldbus Protocols (PROFINET, COMLI) > **Status: requested 2026-10-03, not yet planned into a roadmap phase.** > Extend the simulator beyond Modbus TCP, motivated by ABB AC800M/800xA > systems and their distributed I/O: PROFINET IO (AC800M PN controllers > with S800 I/O via CI840-class interface modules) and COMLI (ABB's > protocol between AC800M controllers and S800/S900 I/O via CI51x/CI522 > communication interfaces, TCP-encapsulated). ### 14.1 Motivation An AC800M FAT bench talks to its distributed I/O over the protocol the controller is actually configured for. Simulating the I/O side of those protocols lets the same scenario engine, datasets (§8), bench mode (§9), and reporting drive FAT benches for ABB controllers, not only Modbus TCP masters. The architecture separates cleanly along the same seams the multi-vendor profiles (§13) expose: the scenario engine couples to a node only through the channel model and the datastore's set/read surface; a new protocol is a new transport + protocol layer binding to the same process-image core. ### 14.2 Requirements - FR-14.2.1: **Protocol abstraction**: a protocol binding defines how the field-side process image is exposed to a master: wire protocol, address/identifier model, cyclic vs. transactional access, and the device's diagnostics/alarm model. The simulator core (process image, scenario engine, plugins, reports) stays protocol-independent. - FR-14.2.2: **PROFINET IO device simulation** (candidate; scope to be defined against AC800M PN-controller configuration): simulate the device side of PROFINET IO — DCP identify/boot, cyclic RT I/O data exchange for a configured module list, and enough record/identification data for the IO controller to bring the device into data exchange. Scope note: a full PROFINET stack is a major undertaking; a realistic first target is an Ethernet-level RT simulation of one S800-style station profile with a fixed module list, driven by the existing deterministic clock and scenario engine. - FR-14.2.3: **COMLI device simulation** (candidate; scope to be defined against AC800M/CI51x/CI522 documentation): simulate the I/O-side endpoint of ABB COMLI over TCP as used between AC800M and S800/S900 I/O through CI communication interfaces — the register/point addressing model, cyclic polling, and the diagnostics responses the CI interface expects. COMLI's request/response register semantics map naturally onto the datastore's set/read surface. - FR-14.2.4: **Shared scenario engine**: scenarios, aliases, macros, assertions, plugins, and datasets (§8) work unchanged against protocol bindings; only the channel/identifier model of the target I/O system is expressed in the node config (module list with brand-specific article numbers). A scenario must not name wire-level addresses; alias resolution stays per-device via FR-4.3.11/FR-13.2.4. - FR-14.2.5: **Trace and diagnostics parity**: per-protocol verbose tracing equivalent to the Modbus plain-text trace (NFR-6.4): PROFINET RT frames and COMLI requests decoded in readable form with device-level annotations; protocol error counters analogous to the Modbus statistics block. - FR-14.2.6: **Fault-injection parity** (per protocol): for PROFINET — frame loss/corruption, device pull/plug, subslot mismatch, communication/watchdog timeouts (the PN equivalent of the Modbus watchdog §3.4); for COMLI — request timeouts, malformed messages, CI-interface failure simulation. These must be scriptable from scenarios like the Modbus fault paths (§3.6). - FR-14.2.7: **Deployment parity**: protocol servers run under the same service/container model (§12): env-var configuration, graceful shutdown; one container may host multiple protocol bindings for a node (a station reachable via different protocols in different bench setups). ### 14.3 Scoping questions (answer before planning) 1. PROFINET: full device stack vs. targeted RT simulation of one station profile? Which controller-side tooling (e.g., ABB Compact Builder) must accept the simulated device? 2. COMLI: exact CI51x/CI522 variants in scope; is TCP encapsulation sufficient, or is the serial CI variant also needed? 3. Are both protocols needed, or is one the realistic FAT target? (COMLI is far smaller in scope; PROFINET conformance is the larger effort by an order of magnitude.) 4. Which I/O module families per protocol (S800 modules via PROFINET/COMLI interfaces; WAGO 750 modules over Modbus as today)? ### 14.4 Acceptance criteria (draft) 1. An AC800M controller (or a protocol-level test master representing one) establishes data exchange with the simulated I/O station and reads/writes the documented process data. 2. The existing scenario DSL drives input changes and observes output reactions through the new protocol binding, unchanged. 3. Verbose tracing shows per-protocol decoded traffic with device annotations; fault-injection steps behave like the Modbus equivalents. 4. The 750-362 Modbus behavior is unaffected (regression suite green). --- ## 15. 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)**; **runtime device configuration (§11)**: module hot-configuration with image rebuild, DIP-switch emulation, WBM-equivalent pages, device-state persistence | | 6 | CI integration: run scenarios headless, export JUnit XML | | ? | **Multi-vendor device profiles (§13)** and **additional fieldbus protocols PROFINET/COMLI (§14)** — not yet scheduled; phase assignment after §13.3/§14.3 scoping | --- ## 16. 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) - ABB CI51x/CI522 communication interface and S800/S900 I/O documentation (COMLI) - PROFINET specification (PI, profibus.com) — if PROFINET scope is confirmed