1084 lines
55 KiB
Markdown
1084 lines
55 KiB
Markdown
# Software Specification
|
||
|
||
**WAGO Modbus TCP Simulator — Autonomous FAT Testing Framework**
|
||
|
||
| | |
|
||
|---|---|
|
||
| Project | `wago-modbustcp-simulator-py` |
|
||
| Version | 1.2 (Draft for review) |
|
||
| Date | 2026-10-03 |
|
||
| Status | Framework phase; module addressing implemented (FR-4.3.11); long-duration simulation (§8), bench mode (§9), web UI (§10) and runtime device configuration (§11) requested |
|
||
| Target device simulated | WAGO 750-362 / 750-362/0000-0001 FC Modbus TCP (G4) bus coupler |
|
||
| Reference documents | `docs/m07500362_xxxxxxxx_0en.pdf` (750-362 Modbus TCP manual), Modbus TCP specification (modbus.org) |
|
||
|
||
---
|
||
|
||
## 1. Introduction
|
||
|
||
### 1.1 Purpose
|
||
|
||
This document specifies a software system that **simulates a WAGO 750-362 Modbus TCP fieldbus coupler** on a standard PC, to be used as a test bench during **Factory Acceptance Tests (FAT)** of PLC programs.
|
||
|
||
During FAT the PLC under test is normally wired to a real WAGO I/O node. This simulator replaces the physical node entirely: the PLC (or any Modbus TCP master/SCADA) connects over TCP/IP to the simulator, which responds exactly like a 750-362 coupler — same function codes, same register map, same exception behavior, same timing characteristics.
|
||
|
||
### 1.2 Problem being solved
|
||
|
||
FAT of PLC programs requires either:
|
||
|
||
1. Physical I/O hardware (expensive, must be wired per project, error-prone, single-purpose), or
|
||
2. A generic Modbus slave (does not implement the WAGO-specific register map, digital-channel addressing, watchdog, statistics, or module-diagnostics behavior; PLC programs validated against it may fail against a real node).
|
||
|
||
The simulator provides the **exact WAGO behavior** plus **scriptable, autonomous scenario execution**, so a complete FAT round — including fault cases — can run unattended and produce a machine-verifiable result.
|
||
|
||
### 1.3 Scope
|
||
|
||
In scope (v1.0 framework):
|
||
|
||
- Modbus TCP server (slave) with the 750-362 function-code set.
|
||
- WAGO 750-362 register map: process image (words + bits), configuration, diagnostic, constant, and firmware-info registers.
|
||
- Virtual I/O node: a configurable list of WAGO 750/753 I/O modules forming the process image.
|
||
- **Scenario engine**: scripted macros that observe input states and act on outputs (the automation core).
|
||
- Deterministic simulation clock and scheduler.
|
||
- CLI runner with configuration files.
|
||
|
||
Out of scope (v1.0): HTTP/WBM web interface, FTP, SNMP, UDP variant of Modbus, DHCP/BootP provisioning, real-time behavior beyond response latency simulation, and module parameterization (mailbox traffic) beyond process data.
|
||
|
||
### 1.4 Definitions
|
||
|
||
| Term | Meaning |
|
||
|---|---|
|
||
| PII | Process Input Image — data the coupler presents to the master (from input modules) |
|
||
| PIO | Process Output Image — data the master writes (to output modules) |
|
||
| MBAP | Modbus Application Protocol header (7-byte TCP header) |
|
||
| FC | Modbus function code |
|
||
| FAT | Factory Acceptance Test |
|
||
| Scenario | A scripted, timed sequence of state changes and assertions |
|
||
| Macro | A reusable scripted action (e.g., "pulse output 3 for 500 ms") |
|
||
| Node | One simulated fieldbus coupler + its I/O module configuration |
|
||
| LTS | Long-Time Simulation — continuous (“soak”) mode running hours to weeks of simulated time |
|
||
| Dataset | A CSV time-series file driving designated input aliases in continuous mode |
|
||
| Wear counter | Engine-maintained per-alias statistic: runtime hours, edge counts, load-hours |
|
||
| Degradation model | Time/cycle-dependent transformation of a plant parameter (e.g., pump capacity) |
|
||
| Invariant | A condition that must hold every tick throughout a continuous run |
|
||
| Episode | One aggregated violation period of an invariant (bounded reporting) |
|
||
|
||
---
|
||
|
||
## 2. System Overview
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph DUT["Devices under test"]
|
||
PLC["PLC 1..n\n(Modbus TCP masters)"]
|
||
end
|
||
|
||
subgraph SIM["Simulator (this system)"]
|
||
MB["Modbus TCP Server\n(asyncio, ≤ 15 connections)"]
|
||
DS["Datastore\n(WAGO word & bit maps)"]
|
||
NODE["Virtual WAGO Node\n(module list → process image)"]
|
||
SE["Scenario Engine\n(states → actions)"]
|
||
CLK["Deterministic Clock\n+ Scheduler"]
|
||
LOG["Event Log / Traces"]
|
||
end
|
||
|
||
PLC -- "MBAP frames (FC1..FC23)" --> MB
|
||
MB <--> DS
|
||
DS <--> NODE
|
||
SE -- "read inputs / write outputs" --> NODE
|
||
SE <--> CLK
|
||
NODE -- change events --> SE
|
||
LOG -.records.- MB
|
||
LOG -.records.- SE
|
||
```
|
||
|
||
The system is a pure **slave/server**: it never initiates Modbus traffic. Autonomy comes from the **scenario engine**, which drives the *field side* of the virtual node (as if sensors moved and actuators did something), while the PLC drives the *master side* through normal Modbus writes.
|
||
|
||
---
|
||
|
||
## 3. Functional Requirements — Modbus Server
|
||
|
||
### 3.1 Transport & connections
|
||
|
||
- FR-3.1.1: The server shall listen on TCP port 502 (configurable) and speak Modbus TCP with the standard MBAP header.
|
||
- FR-3.1.2: The server shall support **up to 15 concurrent Modbus TCP connections** (per 750-362 spec, §7.3). Connection number 16 shall be refused.
|
||
- FR-3.1.3: Idle connections shall be closed after the timeout configured in register `0x1030` (default 600 × 100 ms). Outputs remain in last state (§11.2.5.5).
|
||
- FR-3.1.4: Protocol identifier other than 0 in MBAP shall increment the `BadProtocol` statistic and be ignored.
|
||
- FR-3.1.5: Unit identifier shall be ignored (accepted, echoed back), as on the real device.
|
||
|
||
### 3.2 Function codes
|
||
|
||
The simulator shall implement exactly the 750-362 function set (§11.2.2) and reject all others with exception `0x01` (Illegal Function):
|
||
|
||
| FC | Function | Limits (R/W) |
|
||
|----|----------|--------------|
|
||
| 01 | Read Coils | 2000 / — |
|
||
| 02 | Read Discrete Inputs | 2000 / — |
|
||
| 03 | Read Holding Registers | 125 / — |
|
||
| 04 | Read Input Registers | 125 / — |
|
||
| 05 | Write Single Coil | — / 1 |
|
||
| 06 | Write Single Register | — / 1 |
|
||
| 11 | Get Comm Event Count | — |
|
||
| 15 | Write Multiple Coils | — / 800 |
|
||
| 16 | Write Multiple Registers | — / 100 |
|
||
| 22 | Mask Write Register | — / 1 |
|
||
| 23 | Read/Write Multiple Registers | 125 / 100 |
|
||
|
||
Exceeding limits → exception `0x03` (Illegal Data Value) and increment `TooManyRegisters` / `TooManyBits`.
|
||
|
||
Exception codes (§11.2.3): `0x01` Illegal Function, `0x02` Illegal Data Address, `0x03` Illegal Data Value, `0x04` Slave Device Failure, `0x05` Acknowledge, `0x06` Server Busy, `0x08` Memory Parity Error, `0x0A`/`0x0B` Gateway errors.
|
||
|
||
### 3.3 Register map (WAGO 750-362)
|
||
|
||
#### 3.3.1 Word (register) access — reading (FC3/FC4)
|
||
|
||
| Modbus address (hex) | Area | Behavior |
|
||
|---|---|---|
|
||
| `0x0000–0x00FF` | Physical input area (1), %IW0–255 | First 256 words of input process image |
|
||
| `0x0100–0x01FF` | — | Exception `0x02` |
|
||
| `0x0200–0x02FF` | Physical output area (1) | Read-back mirror of `%QW0–255` (outputs written at `0x0000` are readable here, +`0x0200` offset) |
|
||
| `0x0300–0x0FFF` | — | Exception `0x02` |
|
||
| `0x1000–0x2FFF` | Configuration registers | See §3.5 |
|
||
| `0x3000–0x5FFF` | — | Exception `0x02` |
|
||
| `0x6000–0x62FB` | Physical input area (2), %IW256–1020 | Additional 764 words |
|
||
| `0x62FC–0x6FFF` | — | Exception `0x02` |
|
||
| `0x7000–0x72FB` | Physical output area (2), %QW256–1020 | Read-back mirror, +`0x01000` offset from write area (2) |
|
||
| `0x72FC–0xFFFF` | — | Exception `0x02` |
|
||
|
||
#### 3.3.2 Word access — writing (FC6/FC16)
|
||
|
||
| Modbus address (hex) | Area |
|
||
|---|---|
|
||
| `0x0000–0x00FF` | Physical output area (1), %QW0–255 |
|
||
| `0x0100–0x01FF` | Exception `0x02` |
|
||
| `0x0200–0x02FF` | Physical output area (1) (mirror accepted for writing) |
|
||
| `0x0300–0x0FFF` | Exception `0x02` |
|
||
| `0x1000–0x2FFF` | Configuration registers |
|
||
| `0x3000–0x5FFF` | Exception `0x02` |
|
||
| `0x6000–0x62FB` | Physical output area (2), %QW256–1020 |
|
||
| `0x62FC–0x6FFF` | Exception `0x02` |
|
||
| `0x7000–0x72FB` | Physical output area (2) (mirror) |
|
||
| `0x72FC–0xFFFF` | Exception `0x02` |
|
||
|
||
#### 3.3.3 Bit (coil) access — reading (FC1/FC2) & writing (FC5/FC15)
|
||
|
||
| Modbus address (hex) | Area |
|
||
|---|---|
|
||
| `0x0000–0x01FF` | Physical input area (1) — first 512 digital inputs (read) |
|
||
| `0x0200–0x03FF` | Physical output area (1) — first 512 digital outputs (read mirror + write) |
|
||
| `0x0800–0x85F7` | Physical input area (2) — digital inputs 513–2039 |
|
||
| `0x9000–0x95F7` | Physical output area (2) — digital outputs 513–2039 |
|
||
| all others | Exception `0x02` |
|
||
|
||
**Digital channel addressing rule** (§11.2.4): bit addresses are **channel numbers** — digital input no. 47 has Modbus address 46 — independent of the physical module layout. Word-oriented modules are laid out first (in physical order, from word 0), then bit-oriented modules are byte-packed in physical order.
|
||
|
||
#### 3.3.4 Constant registers (FC3/FC4 read-only)
|
||
|
||
| Addr | Value | Purpose |
|
||
|---|---|---|
|
||
| `0x2000` | `0x0000` | Zero |
|
||
| `0x2001` | `0xFFFF` | Ones |
|
||
| `0x2002` | `0x1234` | Byte-order check (Intel format expected) |
|
||
| `0x2003` | `0xAAAAl` | Bit mask 1 |
|
||
| `0x2004` | `0x5555` | Bit mask 2 |
|
||
| `0x2005` | `0x7FFF` | Max positive |
|
||
| `0x2006` | `0x8000` | Max negative |
|
||
| `0x2007` | `0x3FFF` | Half positive |
|
||
| `0x2008` | `0x4000` | Half negative |
|
||
|
||
#### 3.3.5 Firmware-info registers (read-only)
|
||
|
||
`0x2010` revision, `0x2011` series code (`0x02EE` = 750), `0x2012` coupler code (`0x0166` = 362), `0x2013`/`0x2014` major/minor, `0x2020` description (16 words), `0x2021` compile time, `0x2022` compile date, `0x2023` firmware loader info, `0x2030–0x2033` connected-module descriptions, `0x2035` process-image settings, `0x2036` diagnostics, `0x2040` software reset (write `0x55AA`/`0xAA55`), `0x2043` factory settings.
|
||
|
||
### 3.4 Watchdog (§11.2.5.2/3)
|
||
|
||
The Modbus watchdog shall be simulated:
|
||
|
||
- `0x1000` timeout value ×100 ms (default `0x0064`), writable only when WD inactive.
|
||
- `0x1001`/`0x1002` coding masks (bit *n−1* of the mask = FC *n*; default `0xFFFF`).
|
||
- `0x1003` trigger (write nonzero, alternating values), `0x1005` stop (`0xAAAA` then `0x5555`), `0x1008` simple stop (`0xAA55`/`0x55AA`), `0x1007` restart (write `1`).
|
||
- `0x1006` status: `0` inactive / `1` active / `2` expired.
|
||
- `0x1009` close-socket-on-timeout, `0x100A` type Standard/Alternative, `0x100B` save WD params (remanent).
|
||
- On timeout: **all analog outputs → 0, all digital outputs → OFF**; in Standard mode further process-data access is answered with exception `0x04`; WD registers remain accessible; `0x1009 = 1` additionally closes TCP connections.
|
||
- Watchdog events shall be visible to the scenario engine (assertions: "WD triggered at t seconds").
|
||
|
||
### 3.5 Configuration & diagnostic registers
|
||
|
||
- `0x1022`–`0x1025`: image sizes (analog in/out bits, digital in/out bits) computed from the configured module list.
|
||
- `0x1029` (9 words) Modbus TCP statistics: SlaveDeviceFailure, BadProtocol, BadLength, BadFunction, BadAddress, BadData, TooManyRegisters, TooManyBits, ModTcpMessageCounter. Reset by writing `0xAA55`/`0x55AA`.
|
||
- `0x102A` active TCP connections.
|
||
- `0x1030` Modbus TCP idle timeout (×100 ms).
|
||
- `0x1031` MAC-ID (3 words, configurable to emulate a specific device).
|
||
- `0x1037` response delay (ms) — the simulator shall actually delay responses, to emulate link latency.
|
||
- `0x1038` TOS/DSCP (stored, echoed).
|
||
- `0x1050` module diagnosis (3 words): module, channel, diagnosis bits (underrange … collective failure) — scriptable fault injection.
|
||
- `0x1020`/`0x1021` LED error code/argument — scenario-settable.
|
||
- `0x102B` KBUS reset (write): re-reads the module configuration; process image rebuilt.
|
||
|
||
### 3.6 Fault injection
|
||
|
||
FR-3.6.1: The scenario engine shall be able to simulate fieldbus faults:
|
||
|
||
- KBUS/local-bus failure → `SlaveDeviceFailure` statistic increments and process-data reads may return exception `0x04`.
|
||
- Response-delay injection via `0x1037`.
|
||
- Connection refusal beyond 15 concurrent connections.
|
||
- Illegal-address probing (read/write the exception areas) → correct `0x02` responses (this validates master error handling).
|
||
|
||
### 3.7 Process data semantics
|
||
|
||
- Word mapping of digital channels follows the **Intel format** (§7.3.3): channel 1 = bit 0 of the low byte (`D0`), high byte `D1` on top.
|
||
- Digital word mapping: `Bit15 … Bit0` = `Byte D1 D0`, bit 0 = lowest channel number.
|
||
- Analog modules contribute 16-bit data words (control/status bytes not accessible via Modbus).
|
||
- A module list defines the image; the framework computes addresses and exposes them in the log and via module-description registers (`0x2030+`).
|
||
|
||
---
|
||
|
||
## 4. Functional Requirements — Scenario & Macro Engine
|
||
|
||
### 4.1 Concept
|
||
|
||
A **scenario** is a scripted, time-ordered description of the *world around the PLC*: sensor inputs change over time, actuators respond, and the script asserts on PLC outputs. This is what makes FAT autonomous — no operator needs to toggle signals.
|
||
|
||
- **State-based**: rules keyed on input/output conditions (`when input 5 turns ON …`).
|
||
- **Time-based**: scheduled events (`after 30 s …`).
|
||
- **Output actions**: `set`, `reset`, `pulse`, `toggle`, `ramp`, `hold_value` on simulated field outputs (what the PLC sees as its inputs).
|
||
- **Input observations**: the engine reads the process output image (what the PLC wrote) with edge detection (rising/falling).
|
||
- **Assertions**: expected PLC behavior (output X ON within 2 s after input Y) → FAT verdicts.
|
||
|
||
### 4.2 Scenario DSL (YAML)
|
||
|
||
```yaml
|
||
name: pump_station_fat
|
||
description: "Full FAT round for pump station PLC program"
|
||
node: config/pump_node.yaml # module configuration
|
||
timing: { tick: 10ms } # simulation tick
|
||
|
||
inputs: # named aliases → digital/analog inputs
|
||
start_button: { type: di, module: 0, channel: 0 } # DI0.0
|
||
level_sensor: { type: ai, module: 4, channel: 0, scale: 0.1 } # AI4.0, mm → counts
|
||
em_stop_ok: { type: di, module: 0, channel: 3 } # DI0.3
|
||
|
||
outputs: # named aliases → PLC outputs (read-only for script)
|
||
pump_relay: { type: do, module: 2, channel: 0 } # DO2.0
|
||
valve_open: { type: do, module: 2, channel: 1 } # DO2.1
|
||
speed_setpoint: { type: ao, module: 5, channel: 0, scale: 0.1 } # AO5.0
|
||
|
||
macros:
|
||
pulse_start: # reusable macro
|
||
- set: start_button = ON
|
||
- wait: 250ms
|
||
- set: start_button = OFF
|
||
|
||
states: # optional state machine
|
||
idle:
|
||
on: { em_stop_ok: ON }
|
||
goto: running
|
||
running:
|
||
entry: [ ramp: level_sensor, to: 8500, over: 30s ]
|
||
on: { pump_relay: RISING }
|
||
assert: { valve_open: ON, within: 2s }
|
||
goto: full
|
||
...
|
||
|
||
sequence: # time-ordered script (or fully state-driven)
|
||
- macro: pulse_start
|
||
- assert: { pump_relay: ON, within: 2s }
|
||
- wait: 5s
|
||
- set: level_sensor = 4200
|
||
- assert: { speed_setpoint: ">= 1000", within: 3s }
|
||
- report: PASS
|
||
```
|
||
|
||
### 4.3 Requirements
|
||
|
||
- FR-4.3.1: Scenarios shall be defined in YAML (no programming required for standard FAT sequences).
|
||
- FR-4.3.2: The engine shall support **state machines** (states, entry actions, transitions on input/output edges or conditions, guards).
|
||
- FR-4.3.3: The engine shall support **macros** (reusable action groups, parameterizable).
|
||
- FR-4.3.4: Actions on simulated inputs: `set`, `reset`, `pulse`, `toggle`, `ramp` (linear over time), `noise` (± band), `sin`, `hold`.
|
||
- FR-4.3.5: Assertions on PLC outputs with timeout → **PASS/FAIL/ERROR verdict per check**, with timestamps; final scenario verdict.
|
||
- FR-4.3.6: Assertions may compare against expected values, ranges, edges, and statistics registers (e.g., "watchdog expired").
|
||
- FR-4.3.7: All scenario events and all Modbus transactions shall be logged (CSV/JSONL) for the FAT report.
|
||
- FR-4.3.8: Python **action plugins** shall be supported for actions beyond the built-in set (registered callables), for complex plant-model physics.
|
||
- FR-4.3.9: The engine shall run **deterministically**: simulation time decoupled from wall-clock; results reproducible.
|
||
- FR-4.3.10: A scenario may load multiple nodes (multi-coupler FAT benches).
|
||
- FR-4.3.11: **Module-based channel addressing** — aliases may be addressed
|
||
as *module type + module number + channel number*, all components
|
||
explicitly configurable: `{ type: di, module: 2, channel: 1 }` selects
|
||
digital input channel 1 (0-based) on module slot 2 (0-based position in
|
||
the node's module list), i.e. `DI2.1`. Both numbers are 0-based to match
|
||
physical slot/channel labeling. The channel kind must match the module
|
||
(`DO` on a DI-only module is a validation error). This is an alternative
|
||
to pool addressing (`{ type: di, channel: 3 }` = 3rd DI channel across
|
||
the whole node); both schemes are supported and may be mixed, but an
|
||
alias uses exactly one. **Implemented** in v1.1 (`channel_by_module`,
|
||
`Alias.module`, `alias_location`).
|
||
|
||
### 4.4 Example FAT flow
|
||
|
||
1. PLC program boots, connects to simulator (Modbus TCP client), reads input image.
|
||
2. Scenario `pump_station_fat` starts in `idle` state; `em_stop_ok` turns ON.
|
||
3. Scenario pulses `start_button`; PLC sees input, starts pump relay (writes coil).
|
||
4. Engine detects rising edge on `pump_relay`, asserts `valve_open` within 2 s.
|
||
5. Scenario ramps `level_sensor` to 8500 counts; PLC adjusts `speed_setpoint`; assertions verify.
|
||
6. Scenario injects fault (KBUS error / watchdog timeout); PLC must react (e.g., switch to safe state) — asserted.
|
||
7. Report generated: verdicts, timing, Modbus trace.
|
||
|
||
---
|
||
|
||
## 5. Architecture
|
||
|
||
### 5.1 Package layout
|
||
|
||
```
|
||
wago-modbustcp-simulator-py/
|
||
├── docs/ # WAGO manuals + this spec
|
||
│ └── spec/
|
||
│ ├── software-spec.md # this document
|
||
│ └── register-map.md # extracted WAGO tables
|
||
├── src/
|
||
│ └── wago_sim/
|
||
│ ├── __init__.py
|
||
│ ├── mbap.py # MBAP frame codec
|
||
│ ├── pdu.py # PDU codec (request/response/exception)
|
||
│ ├── server.py # asyncio Modbus TCP server
|
||
│ ├── datastore.py # WAGO word/coil datastores
|
||
│ │ # (holds process images + register bank)
|
||
│ ├── registers.py # config/diagnostic/constant/firmware regs
|
||
│ ├── modules.py # virtual WAGO I/O modules (module catalog)
|
||
│ ├── node.py # node = modules → process image mapping
|
||
│ ├── watchdog.py # Modbus watchdog simulation
|
||
│ ├── clock.py # deterministic clock + scheduler
|
||
│ ├── scenario/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── engine.py # interpreter for scenario DSL
|
||
│ │ ├── dsl.py # YAML schema + validation
|
||
│ │ │ ├── actions.py # set/pulse/ramp/... + plugin registry
|
||
│ │ │ ├── assertions.py # condition matcher
|
||
│ │ │ ├── statemachine.py
|
||
│ │ └── report.py # verdicts, CSV/JSONL log
|
||
│ ├── stats.py # Modbus TCP statistics counters
|
||
│ ├── logging_setup.py
|
||
│ └── cli.py # entry point (argparse)
|
||
├── config/
|
||
│ └── pump_node.yaml # example node configuration
|
||
├── scenarios/
|
||
│ └── pump_station_fat.yaml # example scenario
|
||
├── tests/
|
||
│ ├── test_mbap.py
|
||
│ ├── test_pdu.py
|
||
│ ├── test_server.py
|
||
│ ├── test_datastore.py
|
||
│ ├── test_registers.py
|
||
│ � ├── test_watchdog.py
|
||
│ ├── test_scenario_engine.py
|
||
│ └── conftest.py
|
||
├── pyproject.toml
|
||
└── README.md
|
||
```
|
||
|
||
### 5.2 Design decisions
|
||
|
||
| Decision | Rationale |
|
||
|---|---|
|
||
| Pure-Python, asyncio TCP server | No external Modbus library needed; full control over WAGO-specific behavior (register-map exceptions, statistics, watchdog); testable |
|
||
| Deterministic clock | Reproducible FAT runs; scenario times independent of host speed |
|
||
| YAML DSL + plugin boundary | Test engineers write scenarios, not code; complex physics via Python plugins |
|
||
| Datastore split: process image vs. register bank | Mirrors real device: process image vs. internal registers; the readback-mirror rule (+0x0200) is dataflow, not copy-on-read |
|
||
| Statistics as first-class objects | FAT assertions can check communication health (`0x1029`) |
|
||
|
||
### 5.3 Threading & concurrency model
|
||
|
||
- asyncio event loop owns the Modbus server and the scheduler tick.
|
||
- Scenario engine runs in the same loop (coroutines), so image updates and Modbus reads see consistent data without locks.
|
||
- Response delay (`0x1037`) implemented as asyncio sleep before response send.
|
||
|
||
### 5.4 Data model
|
||
|
||
```mermaid
|
||
erDiagram
|
||
NodeConfig ||--|{ ModuleConfig : modules
|
||
NodeConfig ||--|| ProcessImage : builds
|
||
ModuleConfig ||--|{ Channel : channels
|
||
ProcessImage ||--|| InputImage : inputs
|
||
ProcessImage ||--|| OutputImage : outputs
|
||
OutputImage ||--|| InputMirror : "read-back at +0x200/+0x1000"
|
||
Scenario ||--|{ State : states
|
||
Scenario ||--|{ SequenceStep : sequence
|
||
State ||--|{ Action : entry
|
||
State ||--|{ Transition : on
|
||
SequenceStep ||--|{ Action : actions
|
||
Scenario ||--|{ Alias : "inputs/outputs aliases"
|
||
Scenario ||--|{ Macro : macros
|
||
Macro ||--|{ Action : steps
|
||
```
|
||
|
||
### 5.5 State chart — server session
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> Listening
|
||
Listening --> Connected: TCP accept (≤15)
|
||
Connected --> RequestParsing: MBAP complete
|
||
RequestParsing --> Dispatching: PDU decoded
|
||
Dispatching --> Responding: result
|
||
Dispatching --> ExceptionResponse: FC/addr/val error
|
||
Responding --> Connected
|
||
ExceptionResponse --> Connected
|
||
RequestParsing --> Connected: malformed (count BadLength)
|
||
Connected --> Closed: idle timeout / WD 0x1009=1 / client close
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Non-Functional Requirements
|
||
|
||
- NFR-6.1 Performance: sustain ≥ 500 requests/s with ≤ 10 ms added latency (typical FAT bench load).
|
||
- NFR-6.2 Portability: Python 3.11+ on Windows/Linux.
|
||
- NFR-6.3 Determinism: two runs of a scenario with identical inputs and timing produce identical verdict logs (except wall-clock stamps).
|
||
- NFR-6.4 Observability: every Modbus transaction logged with timestamps (sim-time + wall-time), PDU hex, connection id.
|
||
- NFR-6.5 Robustness: malformed frames never crash the server; counted in statistics.
|
||
- NFR-6.6 Configurability: port, bind address, node config, firmware identity registers, MAC-ID configurable without code changes.
|
||
|
||
---
|
||
|
||
## 7. Acceptance Criteria (framework phase)
|
||
|
||
1. A Modbus TCP master (e.g., `pymodbus` client in tests) can connect, read constants `0x2000–0x2008`, and gets exactly `0x0000, 0xFFFF, 0x1234, 0xAAAA, 0x5555, 0x0166, ...` per spec — byte order verified.
|
||
2. Reads/writes in all mapped/unmapped areas produce values/exceptions per §3.3 tables.
|
||
3. Digital channel addressing: a configured 16-channel DI module + 8-channel DO module yields channel-number bit addressing (DI 47 → bit address 46).
|
||
4. Output readback mirrors at +`0x0200` (word) and +`0x0200`/+`0x1000` (bit/word area 2) work.
|
||
5. A YAML scenario can set a digital input, detect a PLC output rising edge, and produce a PASS/FAIL report file.
|
||
6. Watchdog: configure `0x1000`, trigger via `0x1003`, let it expire, outputs clear, `0x1006` = 2, process access → exception `0x04`, restart via `0x1007`.
|
||
7. Statistics counters increment correctly on bad frames/addresses; reset via `0x1029` write `0xAA55`.
|
||
|
||
---
|
||
|
||
## 8. Feature Request — Long-Duration (Continuous) Simulation
|
||
|
||
> **Status: requested 2026-10-03, planned (roadmap phase 3), not yet implemented.**
|
||
> Extends the scenario engine (§4) with a `continuous` mode for endurance
|
||
> (“soak”) testing: the facility runs for hours, days or weeks of simulated
|
||
> time against evolving inputs and an aging plant.
|
||
|
||
### 8.1 Motivation
|
||
|
||
FAT rounds (§4) validate functional behavior over minutes. A facility must
|
||
additionally be proven **over long time**:
|
||
|
||
- **Endurance/soak**: memory leaks, counter rollovers, watchdog stability over days.
|
||
- **Trend & historian verification**: SCADA trends, averaging, alarm shelving.
|
||
- **Cycle behavior**: daily/weekly demand profiles, shift patterns, weekend idle.
|
||
- **Maintenance logic**: wear-based alarms, degradation detection, service recovery.
|
||
- **Clock edge cases**: midnight rollovers and (via absolute-timestamp datasets) DST transitions.
|
||
|
||
Long runs cannot be hand-scripted step by step. Two mechanisms replace the
|
||
scripted `sequence`, 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: <duration>` 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: <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.
|
||
- 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: <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):
|
||
|
||
```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 <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).
|
||
- 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.
|
||
|
||
### 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. 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
|
||
`<SR>[.<SO>]`–`<ER>[.<EO>]` 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. 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 |
|
||
|
||
---
|
||
|
||
## 13. 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) |