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