1616 lines
86 KiB
Markdown
1616 lines
86 KiB
Markdown
# Software Specification
|
||
|
||
**WAGO Modbus TCP Simulator — Autonomous FAT Testing Framework**
|
||
|
||
| | |
|
||
|---|---|
|
||
| Project | `wago-modbustcp-simulator-py` |
|
||
| Version | 1.4 (Draft for review) |
|
||
| Date | 2026-10-03 |
|
||
| Status | Framework phase; module addressing implemented (FR-4.3.11 v1.5 per-type numbering); service/container deployment implemented (§12); plain-text request tracing implemented (NFR-6.4); clean CLI error handling implemented (NFR-6.5 CLI part); web UI §10 in implementation (M1–M5a done: auth, overview, manipulation, diagnostics, configuration, channel names, import/export, split overview; M6 scenario view open); long-duration simulation (§8) implemented 2026-10-04 incl. PlantEnv (FR-8.2.14–16), datasets, degradation, invariants, snapshots; bench mode (§9), runtime device configuration (§11 — coupler/module/channel-name subset covered by web UI M5/M5a), multi-vendor device profiles (§13) and additional fieldbus protocols PROFINET/COMLI (§14) requested |
|
||
| Target device simulated | WAGO 750-362 / 750-362/0000-0001 FC Modbus TCP (G4) bus coupler |
|
||
| Reference documents | `docs/m07500362_xxxxxxxx_0en.pdf` (750-362 Modbus TCP manual), Modbus TCP specification (modbus.org) |
|
||
|
||
---
|
||
|
||
## 1. Introduction
|
||
|
||
### 1.1 Purpose
|
||
|
||
This document specifies a software system that **simulates a WAGO 750-362 Modbus TCP fieldbus coupler** on a standard PC, to be used as a test bench during **Factory Acceptance Tests (FAT)** of PLC programs.
|
||
|
||
During FAT the PLC under test is normally wired to a real WAGO I/O node. This simulator replaces the physical node entirely: the PLC (or any Modbus TCP master/SCADA) connects over TCP/IP to the simulator, which responds exactly like a 750-362 coupler — same function codes, same register map, same exception behavior, same timing characteristics.
|
||
|
||
### 1.2 Problem being solved
|
||
|
||
FAT of PLC programs requires either:
|
||
|
||
1. Physical I/O hardware (expensive, must be wired per project, error-prone, single-purpose), or
|
||
2. A generic Modbus slave (does not implement the WAGO-specific register map, digital-channel addressing, watchdog, statistics, or module-diagnostics behavior; PLC programs validated against it may fail against a real node).
|
||
|
||
The simulator provides the **exact WAGO behavior** plus **scriptable, autonomous scenario execution**, so a complete FAT round — including fault cases — can run unattended and produce a machine-verifiable result.
|
||
|
||
### 1.3 Scope
|
||
|
||
In scope (v1.0 framework):
|
||
|
||
- Modbus TCP server (slave) with the 750-362 function-code set.
|
||
- WAGO 750-362 register map: process image (words + bits), configuration, diagnostic, constant, and firmware-info registers.
|
||
- Virtual I/O node: a configurable list of WAGO 750/753 I/O modules forming the process image.
|
||
- **Scenario engine**: scripted macros that observe input states and act on outputs (the automation core).
|
||
- Deterministic simulation clock and scheduler.
|
||
- CLI runner with configuration files.
|
||
|
||
Out of scope (v1.0): HTTP/WBM web interface, FTP, SNMP, UDP variant of Modbus, DHCP/BootP provisioning, real-time behavior beyond response latency simulation, and module parameterization (mailbox traffic) beyond process data.
|
||
|
||
### 1.4 Definitions
|
||
|
||
| Term | Meaning |
|
||
|---|---|
|
||
| PII | Process Input Image — data the coupler presents to the master (from input modules) |
|
||
| PIO | Process Output Image — data the master writes (to output modules) |
|
||
| MBAP | Modbus Application Protocol header (7-byte TCP header) |
|
||
| FC | Modbus function code |
|
||
| FAT | Factory Acceptance Test |
|
||
| Scenario | A scripted, timed sequence of state changes and assertions |
|
||
| Macro | A reusable scripted action (e.g., "pulse output 3 for 500 ms") |
|
||
| Node | One simulated fieldbus coupler + its I/O module configuration |
|
||
| LTS | Long-Time Simulation — continuous (“soak”) mode running hours to weeks of simulated time |
|
||
| Dataset | A CSV time-series file driving designated input aliases in continuous mode |
|
||
| Wear counter | Engine-maintained per-alias statistic: runtime hours, edge counts, load-hours |
|
||
| Degradation model | Time/cycle-dependent transformation of a plant parameter (e.g., pump capacity) |
|
||
| Invariant | A condition that must hold every tick throughout a continuous run |
|
||
| Episode | One aggregated violation period of an invariant (bounded reporting) |
|
||
|
||
---
|
||
|
||
## 2. System Overview
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph DUT["Devices under test"]
|
||
PLC["PLC 1..n\n(Modbus TCP masters)"]
|
||
end
|
||
|
||
subgraph SIM["Simulator (this system)"]
|
||
MB["Modbus TCP Server\n(asyncio, ≤ 15 connections)"]
|
||
DS["Datastore\n(WAGO word & bit maps)"]
|
||
NODE["Virtual WAGO Node\n(module list → process image)"]
|
||
SE["Scenario Engine\n(states → actions)"]
|
||
CLK["Deterministic Clock\n+ Scheduler"]
|
||
LOG["Event Log / Traces"]
|
||
end
|
||
|
||
PLC -- "MBAP frames (FC1..FC23)" --> MB
|
||
MB <--> DS
|
||
DS <--> NODE
|
||
SE -- "read inputs / write outputs" --> NODE
|
||
SE <--> CLK
|
||
NODE -- change events --> SE
|
||
LOG -.records.- MB
|
||
LOG -.records.- SE
|
||
```
|
||
|
||
The system is a pure **slave/server**: it never initiates Modbus traffic. Autonomy comes from the **scenario engine**, which drives the *field side* of the virtual node (as if sensors moved and actuators did something), while the PLC drives the *master side* through normal Modbus writes.
|
||
|
||
---
|
||
|
||
## 3. Functional Requirements — Modbus Server
|
||
|
||
### 3.1 Transport & connections
|
||
|
||
- FR-3.1.1: The server shall listen on TCP port 502 (configurable) and speak Modbus TCP with the standard MBAP header.
|
||
- FR-3.1.2: The server shall support **up to 15 concurrent Modbus TCP connections** (per 750-362 spec, §7.3). Connection number 16 shall be refused.
|
||
- FR-3.1.3: Idle connections shall be closed after the timeout configured in register `0x1030` (default 600 × 100 ms). Outputs remain in last state (§11.2.5.5).
|
||
- FR-3.1.4: Protocol identifier other than 0 in MBAP shall increment the `BadProtocol` statistic and be ignored.
|
||
- FR-3.1.5: Unit identifier shall be ignored (accepted, echoed back), as on the real device.
|
||
|
||
### 3.2 Function codes
|
||
|
||
The simulator shall implement exactly the 750-362 function set (§11.2.2) and reject all others with exception `0x01` (Illegal Function):
|
||
|
||
| FC | Function | Limits (R/W) |
|
||
|----|----------|--------------|
|
||
| 01 | Read Coils | 2000 / — |
|
||
| 02 | Read Discrete Inputs | 2000 / — |
|
||
| 03 | Read Holding Registers | 125 / — |
|
||
| 04 | Read Input Registers | 125 / — |
|
||
| 05 | Write Single Coil | — / 1 |
|
||
| 06 | Write Single Register | — / 1 |
|
||
| 11 | Get Comm Event Count | — |
|
||
| 15 | Write Multiple Coils | — / 800 |
|
||
| 16 | Write Multiple Registers | — / 100 |
|
||
| 22 | Mask Write Register | — / 1 |
|
||
| 23 | Read/Write Multiple Registers | 125 / 100 |
|
||
|
||
Exceeding limits → exception `0x03` (Illegal Data Value) and increment `TooManyRegisters` / `TooManyBits`.
|
||
|
||
Exception codes (§11.2.3): `0x01` Illegal Function, `0x02` Illegal Data Address, `0x03` Illegal Data Value, `0x04` Slave Device Failure, `0x05` Acknowledge, `0x06` Server Busy, `0x08` Memory Parity Error, `0x0A`/`0x0B` Gateway errors.
|
||
|
||
### 3.3 Register map (WAGO 750-362)
|
||
|
||
#### 3.3.1 Word (register) access — reading (FC3/FC4)
|
||
|
||
| Modbus address (hex) | Area | Behavior |
|
||
|---|---|---|
|
||
| `0x0000–0x00FF` | Physical input area (1), %IW0–255 | First 256 words of input process image |
|
||
| `0x0100–0x01FF` | — | Exception `0x02` |
|
||
| `0x0200–0x02FF` | Physical output area (1) | Read-back mirror of `%QW0–255` (outputs written at `0x0000` are readable here, +`0x0200` offset) |
|
||
| `0x0300–0x0FFF` | — | Exception `0x02` |
|
||
| `0x1000–0x2FFF` | Configuration registers | See §3.5 |
|
||
| `0x3000–0x5FFF` | — | Exception `0x02` |
|
||
| `0x6000–0x62FB` | Physical input area (2), %IW256–1020 | Additional 764 words |
|
||
| `0x62FC–0x6FFF` | — | Exception `0x02` |
|
||
| `0x7000–0x72FB` | Physical output area (2), %QW256–1020 | Read-back mirror, +`0x01000` offset from write area (2) |
|
||
| `0x72FC–0xFFFF` | — | Exception `0x02` |
|
||
|
||
#### 3.3.2 Word access — writing (FC6/FC16)
|
||
|
||
| Modbus address (hex) | Area |
|
||
|---|---|
|
||
| `0x0000–0x00FF` | Physical output area (1), %QW0–255 |
|
||
| `0x0100–0x01FF` | Exception `0x02` |
|
||
| `0x0200–0x02FF` | Physical output area (1) (mirror accepted for writing) |
|
||
| `0x0300–0x0FFF` | Exception `0x02` |
|
||
| `0x1000–0x2FFF` | Configuration registers |
|
||
| `0x3000–0x5FFF` | Exception `0x02` |
|
||
| `0x6000–0x62FB` | Physical output area (2), %QW256–1020 |
|
||
| `0x62FC–0x6FFF` | Exception `0x02` |
|
||
| `0x7000–0x72FB` | Physical output area (2) (mirror) |
|
||
| `0x72FC–0xFFFF` | Exception `0x02` |
|
||
|
||
#### 3.3.3 Bit (coil) access — reading (FC1/FC2) & writing (FC5/FC15)
|
||
|
||
| Modbus address (hex) | Area |
|
||
|---|---|
|
||
| `0x0000–0x01FF` | Physical input area (1) — first 512 digital inputs (read) |
|
||
| `0x0200–0x03FF` | Physical output area (1) — first 512 digital outputs (read mirror + write) |
|
||
| `0x0800–0x85F7` | Physical input area (2) — digital inputs 513–2039 |
|
||
| `0x9000–0x95F7` | Physical output area (2) — digital outputs 513–2039 |
|
||
| all others | Exception `0x02` |
|
||
|
||
**Digital channel addressing rule** (§11.2.4): bit addresses are **channel numbers** — digital input no. 47 has Modbus address 46 — independent of the physical module layout. Word-oriented modules are laid out first (in physical order, from word 0), then bit-oriented modules are byte-packed in physical order.
|
||
|
||
#### 3.3.4 Constant registers (FC3/FC4 read-only)
|
||
|
||
| Addr | Value | Purpose |
|
||
|---|---|---|
|
||
| `0x2000` | `0x0000` | Zero |
|
||
| `0x2001` | `0xFFFF` | Ones |
|
||
| `0x2002` | `0x1234` | Byte-order check (Intel format expected) |
|
||
| `0x2003` | `0xAAAAl` | Bit mask 1 |
|
||
| `0x2004` | `0x5555` | Bit mask 2 |
|
||
| `0x2005` | `0x7FFF` | Max positive |
|
||
| `0x2006` | `0x8000` | Max negative |
|
||
| `0x2007` | `0x3FFF` | Half positive |
|
||
| `0x2008` | `0x4000` | Half negative |
|
||
|
||
#### 3.3.5 Firmware-info registers (read-only)
|
||
|
||
`0x2010` revision, `0x2011` series code (`0x02EE` = 750), `0x2012` coupler code (`0x0166` = 362), `0x2013`/`0x2014` major/minor, `0x2020` description (16 words), `0x2021` compile time, `0x2022` compile date, `0x2023` firmware loader info, `0x2030–0x2033` connected-module descriptions, `0x2035` process-image settings, `0x2036` diagnostics, `0x2040` software reset (write `0x55AA`/`0xAA55`), `0x2043` factory settings.
|
||
|
||
### 3.4 Watchdog (§11.2.5.2/3)
|
||
|
||
The Modbus watchdog shall be simulated:
|
||
|
||
- `0x1000` timeout value ×100 ms (default `0x0064`), writable only when WD inactive.
|
||
- `0x1001`/`0x1002` coding masks (bit *n−1* of the mask = FC *n*; default `0xFFFF`).
|
||
- `0x1003` trigger (write nonzero, alternating values), `0x1005` stop (`0xAAAA` then `0x5555`), `0x1008` simple stop (`0xAA55`/`0x55AA`), `0x1007` restart (write `1`).
|
||
- `0x1006` status: `0` inactive / `1` active / `2` expired.
|
||
- `0x1009` close-socket-on-timeout, `0x100A` type Standard/Alternative, `0x100B` save WD params (remanent).
|
||
- On timeout: **all analog outputs → 0, all digital outputs → OFF**; in Standard mode further process-data access is answered with exception `0x04`; WD registers remain accessible; `0x1009 = 1` additionally closes TCP connections.
|
||
- Watchdog events shall be visible to the scenario engine (assertions: "WD triggered at t seconds").
|
||
|
||
### 3.5 Configuration & diagnostic registers
|
||
|
||
- `0x1022`–`0x1025`: image sizes (analog in/out bits, digital in/out bits) computed from the configured module list.
|
||
- `0x1029` (9 words) Modbus TCP statistics: SlaveDeviceFailure, BadProtocol, BadLength, BadFunction, BadAddress, BadData, TooManyRegisters, TooManyBits, ModTcpMessageCounter. Reset by writing `0xAA55`/`0x55AA`.
|
||
- `0x102A` active TCP connections.
|
||
- `0x1030` Modbus TCP idle timeout (×100 ms).
|
||
- `0x1031` MAC-ID (3 words, configurable to emulate a specific device).
|
||
- `0x1037` response delay (ms) — the simulator shall actually delay responses, to emulate link latency.
|
||
- `0x1038` TOS/DSCP (stored, echoed).
|
||
- `0x1050` module diagnosis (3 words): module, channel, diagnosis bits (underrange … collective failure) — scriptable fault injection.
|
||
- `0x1020`/`0x1021` LED error code/argument — scenario-settable.
|
||
- `0x102B` KBUS reset (write): re-reads the module configuration; process image rebuilt.
|
||
|
||
### 3.6 Fault injection
|
||
|
||
FR-3.6.1: The scenario engine shall be able to simulate fieldbus faults:
|
||
|
||
- KBUS/local-bus failure → `SlaveDeviceFailure` statistic increments and process-data reads may return exception `0x04`.
|
||
- Response-delay injection via `0x1037`.
|
||
- Connection refusal beyond 15 concurrent connections.
|
||
- Illegal-address probing (read/write the exception areas) → correct `0x02` responses (this validates master error handling).
|
||
|
||
### 3.7 Process data semantics
|
||
|
||
- Word mapping of digital channels follows the **Intel format** (§7.3.3): channel 1 = bit 0 of the low byte (`D0`), high byte `D1` on top.
|
||
- Digital word mapping: `Bit15 … Bit0` = `Byte D1 D0`, bit 0 = lowest channel number.
|
||
- Analog modules contribute 16-bit data words (control/status bytes not accessible via Modbus).
|
||
- A module list defines the image; the framework computes addresses and exposes them in the log and via module-description registers (`0x2030+`).
|
||
|
||
---
|
||
|
||
## 4. Functional Requirements — Scenario & Macro Engine
|
||
|
||
### 4.1 Concept
|
||
|
||
A **scenario** is a scripted, time-ordered description of the *world around the PLC*: sensor inputs change over time, actuators respond, and the script asserts on PLC outputs. This is what makes FAT autonomous — no operator needs to toggle signals.
|
||
|
||
- **State-based**: rules keyed on input/output conditions (`when input 5 turns ON …`).
|
||
- **Time-based**: scheduled events (`after 30 s …`).
|
||
- **Output actions**: `set`, `reset`, `pulse`, `toggle`, `ramp`, `hold_value` on simulated field outputs (what the PLC sees as its inputs).
|
||
- **Input observations**: the engine reads the process output image (what the PLC wrote) with edge detection (rising/falling).
|
||
- **Assertions**: expected PLC behavior (output X ON within 2 s after input Y) → FAT verdicts.
|
||
|
||
### 4.2 Scenario DSL (YAML)
|
||
|
||
```yaml
|
||
name: pump_station_fat
|
||
description: "Full FAT round for pump station PLC program"
|
||
node: config/pump_node.yaml # module configuration
|
||
timing: { tick: 10ms } # simulation tick
|
||
|
||
inputs: # named aliases → digital/analog inputs
|
||
start_button: { type: di, module: 0, channel: 0 } # DI0.0
|
||
level_sensor: { type: ai, module: 4, channel: 0, scale: 0.1 } # AI4.0, mm → counts
|
||
em_stop_ok: { type: di, module: 0, channel: 3 } # DI0.3
|
||
|
||
outputs: # named aliases → PLC outputs (read-only for script)
|
||
pump_relay: { type: do, module: 2, channel: 0 } # DO2.0
|
||
valve_open: { type: do, module: 2, channel: 1 } # DO2.1
|
||
speed_setpoint: { type: ao, module: 5, channel: 0, scale: 0.1 } # AO5.0
|
||
|
||
macros:
|
||
pulse_start: # reusable macro
|
||
- set: start_button = ON
|
||
- wait: 250ms
|
||
- set: start_button = OFF
|
||
|
||
states: # optional state machine
|
||
idle:
|
||
on: { em_stop_ok: ON }
|
||
goto: running
|
||
running:
|
||
entry: [ ramp: level_sensor, to: 8500, over: 30s ]
|
||
on: { pump_relay: RISING }
|
||
assert: { valve_open: ON, within: 2s }
|
||
goto: full
|
||
...
|
||
|
||
sequence: # time-ordered script (or fully state-driven)
|
||
- macro: pulse_start
|
||
- assert: { pump_relay: ON, within: 2s }
|
||
- wait: 5s
|
||
- set: level_sensor = 4200
|
||
- assert: { speed_setpoint: ">= 1000", within: 3s }
|
||
- report: PASS
|
||
```
|
||
|
||
### 4.3 Requirements
|
||
|
||
- FR-4.3.1: Scenarios shall be defined in YAML (no programming required for standard FAT sequences).
|
||
- FR-4.3.2: The engine shall support **state machines** (states, entry actions, transitions on input/output edges or conditions, guards).
|
||
- FR-4.3.3: The engine shall support **macros** (reusable action groups, parameterizable).
|
||
- FR-4.3.4: Actions on simulated inputs: `set`, `reset`, `pulse`, `toggle`, `ramp` (linear over time), `noise` (± band), `sin`, `hold`.
|
||
- FR-4.3.5: Assertions on PLC outputs with timeout → **PASS/FAIL/ERROR verdict per check**, with timestamps; final scenario verdict.
|
||
- FR-4.3.6: Assertions may compare against expected values, ranges, edges, and statistics registers (e.g., "watchdog expired").
|
||
- FR-4.3.7: All scenario events and all Modbus transactions shall be logged (CSV/JSONL) for the FAT report.
|
||
- FR-4.3.8: Python **action plugins** shall be supported for actions beyond the built-in set (registered callables), for complex plant-model physics.
|
||
- FR-4.3.9: The engine shall run **deterministically**: simulation time decoupled from wall-clock; results reproducible.
|
||
- FR-4.3.10: A scenario may load multiple nodes (multi-coupler FAT benches).
|
||
- FR-4.3.11: **Module-based channel addressing** — aliases may be addressed
|
||
as *module type + module number + channel number*, all components
|
||
explicitly configurable: `{ type: di, module: 2, channel: 1 }` selects
|
||
digital input channel 1 (0-based) on DI-module 2 (0-based **number
|
||
among the modules of the same type**, i.e. the third DI module in the
|
||
node), i.e. `DI2.1`. Channel numbers are 0-based. The channel kind must
|
||
match the module (`DO` on a DI-only module is a validation error).
|
||
This is an alternative to addressing by physical slot; both pool
|
||
addressing (`{ type: di, channel: 3 }` = 3rd DI channel across the
|
||
whole node) and module addressing are supported and may be mixed,
|
||
but an alias uses exactly one. **Implemented** in v1.1
|
||
(`channel_by_module`, `Alias.module`, `alias_location`).
|
||
**Changed in v1.5 (2026-10-04, breaking)**: module numbers previously
|
||
counted physical slots (all modules in one sequence); they now number
|
||
**within the module's type** — `DI0`/`DO0`/`AI0`/`AO0` start each kind's
|
||
sequence (e.g. a node `[DI_8, DI_16, DO_8, DO_16, AI_2, AO_2]` numbers
|
||
its DI modules `DI0`, `DI1` regardless of the DI/DO interleaving).
|
||
This matches how channels are named on real WAGO benches and makes
|
||
rearranging modules less disruptive to existing configs. Scenarios,
|
||
channel names, and analog ranges use the per-type codes.
|
||
|
||
### 4.4 Example FAT flow
|
||
|
||
1. PLC program boots, connects to simulator (Modbus TCP client), reads input image.
|
||
2. Scenario `pump_station_fat` starts in `idle` state; `em_stop_ok` turns ON.
|
||
3. Scenario pulses `start_button`; PLC sees input, starts pump relay (writes coil).
|
||
4. Engine detects rising edge on `pump_relay`, asserts `valve_open` within 2 s.
|
||
5. Scenario ramps `level_sensor` to 8500 counts; PLC adjusts `speed_setpoint`; assertions verify.
|
||
6. Scenario injects fault (KBUS error / watchdog timeout); PLC must react (e.g., switch to safe state) — asserted.
|
||
7. Report generated: verdicts, timing, Modbus trace.
|
||
|
||
---
|
||
|
||
## 5. Architecture
|
||
|
||
### 5.1 Package layout
|
||
|
||
```
|
||
wago-modbustcp-simulator-py/
|
||
├── docs/ # WAGO manuals + this spec
|
||
│ └── spec/
|
||
│ ├── software-spec.md # this document
|
||
│ └── register-map.md # extracted WAGO tables
|
||
├── src/
|
||
│ └── wago_sim/
|
||
│ ├── __init__.py
|
||
│ ├── mbap.py # MBAP frame codec
|
||
│ ├── pdu.py # PDU codec (request/response/exception)
|
||
│ ├── server.py # asyncio Modbus TCP server
|
||
│ ├── datastore.py # WAGO word/coil datastores
|
||
│ │ # (holds process images + register bank)
|
||
│ ├── registers.py # config/diagnostic/constant/firmware regs
|
||
│ ├── modules.py # virtual WAGO I/O modules (module catalog)
|
||
│ ├── node.py # node = modules → process image mapping
|
||
│ ├── watchdog.py # Modbus watchdog simulation
|
||
│ ├── clock.py # deterministic clock + scheduler
|
||
│ ├── scenario/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── engine.py # interpreter for scenario DSL
|
||
│ │ ├── dsl.py # YAML schema + validation
|
||
│ │ │ ├── actions.py # set/pulse/ramp/... + plugin registry
|
||
│ │ │ ├── assertions.py # condition matcher
|
||
│ │ │ ├── statemachine.py
|
||
│ │ └── report.py # verdicts, CSV/JSONL log
|
||
│ ├── stats.py # Modbus TCP statistics counters
|
||
│ ├── logging_setup.py
|
||
│ └── cli.py # entry point (argparse)
|
||
├── config/
|
||
│ └── pump_node.yaml # example node configuration
|
||
├── scenarios/
|
||
│ └── pump_station_fat.yaml # example scenario
|
||
├── tests/
|
||
│ ├── test_mbap.py
|
||
│ ├── test_pdu.py
|
||
│ ├── test_server.py
|
||
│ ├── test_datastore.py
|
||
│ ├── test_registers.py
|
||
│ � ├── test_watchdog.py
|
||
│ ├── test_scenario_engine.py
|
||
│ └── conftest.py
|
||
├── pyproject.toml
|
||
└── README.md
|
||
```
|
||
|
||
### 5.2 Design decisions
|
||
|
||
| Decision | Rationale |
|
||
|---|---|
|
||
| Pure-Python, asyncio TCP server | No external Modbus library needed; full control over WAGO-specific behavior (register-map exceptions, statistics, watchdog); testable |
|
||
| Deterministic clock | Reproducible FAT runs; scenario times independent of host speed |
|
||
| YAML DSL + plugin boundary | Test engineers write scenarios, not code; complex physics via Python plugins |
|
||
| Datastore split: process image vs. register bank | Mirrors real device: process image vs. internal registers; the readback-mirror rule (+0x0200) is dataflow, not copy-on-read |
|
||
| Statistics as first-class objects | FAT assertions can check communication health (`0x1029`) |
|
||
|
||
### 5.3 Threading & concurrency model
|
||
|
||
- asyncio event loop owns the Modbus server and the scheduler tick.
|
||
- Scenario engine runs in the same loop (coroutines), so image updates and Modbus reads see consistent data without locks.
|
||
- Response delay (`0x1037`) implemented as asyncio sleep before response send.
|
||
|
||
### 5.4 Data model
|
||
|
||
```mermaid
|
||
erDiagram
|
||
NodeConfig ||--|{ ModuleConfig : modules
|
||
NodeConfig ||--|| ProcessImage : builds
|
||
ModuleConfig ||--|{ Channel : channels
|
||
ProcessImage ||--|| InputImage : inputs
|
||
ProcessImage ||--|| OutputImage : outputs
|
||
OutputImage ||--|| InputMirror : "read-back at +0x200/+0x1000"
|
||
Scenario ||--|{ State : states
|
||
Scenario ||--|{ SequenceStep : sequence
|
||
State ||--|{ Action : entry
|
||
State ||--|{ Transition : on
|
||
SequenceStep ||--|{ Action : actions
|
||
Scenario ||--|{ Alias : "inputs/outputs aliases"
|
||
Scenario ||--|{ Macro : macros
|
||
Macro ||--|{ Action : steps
|
||
```
|
||
|
||
### 5.5 State chart — server session
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> Listening
|
||
Listening --> Connected: TCP accept (≤15)
|
||
Connected --> RequestParsing: MBAP complete
|
||
RequestParsing --> Dispatching: PDU decoded
|
||
Dispatching --> Responding: result
|
||
Dispatching --> ExceptionResponse: FC/addr/val error
|
||
Responding --> Connected
|
||
ExceptionResponse --> Connected
|
||
RequestParsing --> Connected: malformed (count BadLength)
|
||
Connected --> Closed: idle timeout / WD 0x1009=1 / client close
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Non-Functional Requirements
|
||
|
||
- NFR-6.1 Performance: sustain ≥ 500 requests/s with ≤ 10 ms added latency (typical FAT bench load).
|
||
- NFR-6.2 Portability: Python 3.11+ on Windows/Linux.
|
||
- NFR-6.3 Determinism: two runs of a scenario with identical inputs and timing produce identical verdict logs (except wall-clock stamps).
|
||
- NFR-6.4 Observability: every Modbus transaction is observable in
|
||
verbose mode (`-v` / `WAGO_SIM_VERBOSE`) as plain text — decoded
|
||
`REQ`/`RSP`/`EXC` lines with transaction id, unit, function name,
|
||
values, and the WAGO 750-362 register-map meaning of each address
|
||
(process areas with %IW/%QW mapping per Tables 119/120, read-back
|
||
mirrors, configuration/constant registers, illegal areas); malformed
|
||
frames are traced as `RAW` lines with PDU hex. *Implemented*
|
||
(`src/wago_sim/trace.py`).
|
||
- NFR-6.5 Robustness: malformed frames never crash the server; counted in
|
||
statistics. Invalid operator input — unknown commands or options,
|
||
non-numeric or out-of-range port numbers (CLI or
|
||
`WAGO_SIM_PORT`), missing/unparsable node or scenario files,
|
||
unwritable report paths — yields a one-line
|
||
`wago-sim: error: …` on stderr and exit code 2, never a Python
|
||
traceback; missing report directories are auto-created; unexpected
|
||
internal errors keep the traceback (logged) for debugging.
|
||
*CLI part implemented* (tested in `tests/test_cli.py`).
|
||
- NFR-6.6 Configurability: port, bind address, node config, firmware identity registers, MAC-ID configurable without code changes.
|
||
|
||
---
|
||
|
||
## 7. Acceptance Criteria (framework phase)
|
||
|
||
1. A Modbus TCP master (e.g., `pymodbus` client in tests) can connect, read constants `0x2000–0x2008`, and gets exactly `0x0000, 0xFFFF, 0x1234, 0xAAAA, 0x5555, 0x0166, ...` per spec — byte order verified.
|
||
2. Reads/writes in all mapped/unmapped areas produce values/exceptions per §3.3 tables.
|
||
3. Digital channel addressing: a configured 16-channel DI module + 8-channel DO module yields channel-number bit addressing (DI 47 → bit address 46).
|
||
4. Output readback mirrors at +`0x0200` (word) and +`0x0200`/+`0x1000` (bit/word area 2) work.
|
||
5. A YAML scenario can set a digital input, detect a PLC output rising edge, and produce a PASS/FAIL report file.
|
||
6. Watchdog: configure `0x1000`, trigger via `0x1003`, let it expire, outputs clear, `0x1006` = 2, process access → exception `0x04`, restart via `0x1007`.
|
||
7. Statistics counters increment correctly on bad frames/addresses; reset via `0x1029` write `0xAA55`.
|
||
|
||
---
|
||
|
||
## 8. Feature Request — Long-Duration (Continuous) Simulation
|
||
|
||
> **Status: implemented 2026-10-04** (roadmap phase 3 complete). 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.
|
||
**With the plant environment (FR-8.2.14)**, the decision source sees this
|
||
pipeline as one atomic `env` tick: `observe()` reads the state as of the
|
||
start of the tick, `apply()` queues the decided outputs, and the engine
|
||
then advances the pipeline once — outputs written by `apply()` land in
|
||
the output image before the next tick's `observe()`, so every state the
|
||
decision source sees is pipeline-consistent.
|
||
- 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.
|
||
- FR-8.2.14: **Plant environment abstraction** (requested 2026-10-04,
|
||
reworked same day to environment-first): `PlantEnv` is the **primary
|
||
interface** for continuous plant simulation, standalone-first — it
|
||
requires only a virtual node + datastore (no scenario), with the §8.3/8.4
|
||
machinery (degradation, invariants) as optional attachments. Per
|
||
deterministic tick (the engine or the runner advances the tick clock,
|
||
never wall-clock arithmetic): **`state = env.observe()`** returns the
|
||
state of ALL modules — DI/DO/AI/AO channels, engineering units, per-type
|
||
I/O codes, configured names, analog display ranges (FR-10.2.14),
|
||
plus active aliases and parameters — →
|
||
`env.model(state, dt)` (the caller-supplied plant physics) → **`apply()`
|
||
writes the new field state**. The environment owns no policy; plant
|
||
models are supplied by subclassing (FR-8.2.17).
|
||
- FR-8.2.15: **Action semantics**: `apply()` primarily writes the **input
|
||
side (DI/AI)** — the simulated field: what the sensors show, driven by
|
||
plant physics reacting to the PLC's output commands. Writing outputs
|
||
(DO/AO) is allowed as explicit **forcing** via a `force=True` keyword or
|
||
`env.force(...)` — for headless runs without a PLC and for bench
|
||
fault setup; forced outputs go through the datastore's internal write
|
||
paths so mirrors/statistics/watchdog stay consistent (FR-9.2.6).
|
||
Exactly one decision source may own an alias per tick (ownership
|
||
conflicts are validation errors, extending FR-8.2.2).
|
||
- FR-8.2.16: **Environment API surface** (concrete):
|
||
`env.observe() -> PlantState` (all channels with names and analog
|
||
display ranges, aliases, parameters, sim-time); `env.apply(values, force=False)` (keys: I/O code or alias
|
||
name; DI/AI default, DO/AO require force; unknown keys are errors);
|
||
`env.model(state, dt_s)` — override point for plant physics, returning
|
||
new field values; `env.is_done(state) -> bool` — optional end
|
||
criterion, default False (runs until manually stopped);
|
||
`env.reset()` — deterministic fresh episode; `env.tick_ms` /
|
||
`env.sim_time_ms` (read-only clock); `env.parameters` /
|
||
`env.invariants` attachments. `PlantState.channel(key)` returns
|
||
the full channel record by I/O code or by configured application
|
||
name (FR-10.2.11); `by_code()` returns values by I/O code;
|
||
`channel_range(key)` returns an analog channel's display range
|
||
(min, max) — always defined for AI/AO (dense store, FR-10.2.14).
|
||
- FR-8.2.17: **Environment authoring & runners**: environments are easy
|
||
to create and call — a subclass of `PlantEnv` overriding `model()`
|
||
(and optionally `is_done()`) in any importable module
|
||
(`--env myenvs.pump_station.PumpStationEnv`), or a module-level
|
||
factory function. Three runners: (a) `wago-sim serve --env ...` ticks
|
||
the environment on a **wall-clock-paced** periodic task (10 ms tick
|
||
alongside the Modbus server, real-time behavior for a live PLC) and
|
||
runs **until manually stopped** (SIGINT/SIGTERM — the existing
|
||
graceful-shutdown path); the ticker itself is owned by an
|
||
`EnvRunner` on the app so the web UI can stop/pause/resume/reset/
|
||
restart it at runtime (FR-10.2.15); (b) `wago-sim sim --node ...
|
||
--env ...` runs headless unpaced (as fast as determinism allows)
|
||
until `is_done()` or SIGINT. The scenario-continuous mode
|
||
(FR-8.2.1-13) remains a scripted layer on top of the same engine
|
||
ticks.
|
||
|
||
### 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 via `rate:`, or per start cycle with
|
||
`per: cycles`), `exponential`, `staircase` (`staircase:` level table).
|
||
A model is configured with `observes:` naming the alias whose wear
|
||
counts feed it (NB: the key is `observes` — a YAML `on` key would be
|
||
parsed as boolean True by YAML 1.1).
|
||
- 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_capacity:
|
||
observes: pump_relay # wear counts from this alias
|
||
model: linear
|
||
rate: 0.0004 # -0.04 %/h runtime → ~10 % after ~10.4 sim-days
|
||
from: 1.0
|
||
maintenance_at: 0.90 # threshold crossing injects diagnosis
|
||
diagnosis: { module: 4, channel: 1, value: 0x0001 }
|
||
# the PLC raises maintenance_alarm; the scenario asserts it:
|
||
|
||
sequence: # scheduled events (sim-time)
|
||
- at: 12d
|
||
maintenance_reset: true # service visit: capacity restored
|
||
- at: 12d
|
||
assert: { label: "maintenance alarm raised",
|
||
conditions: { maintenance_alarm: ON }, within: 60s }
|
||
|
||
plugins: [plugins.pump_plant.pump_plant] # loop: d(level)/dt = inflow − pump_flow(speed, capacity)
|
||
|
||
invariants:
|
||
- { alias: level_sensor, condition: '>= 200', label: "tank level floor" }
|
||
- { alias: level_sensor, condition: '<= 9000', label: "tank level brim" }
|
||
|
||
rate_limits:
|
||
- { alias: speed_setpoint, max_per_hour: 3000 } # ramp-rate bound
|
||
|
||
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
|
||
├── plantenv.py # PlantEnv: gym-style observe/apply/reset cycle over the
|
||
│ # process images — the decision-source interface
|
||
│ # (FR-8.2.14–16); plugins become thin adapters
|
||
└── engine.py # mode: continuous, parameter() accessor, snapshot
|
||
# scheduler; fixed tick pipeline per FR-8.2.12:
|
||
# datasets -> edges -> plugins/env -> degradation ->
|
||
# invariants -> scheduled steps -> snapshots
|
||
```
|
||
|
||
```mermaid
|
||
erDiagram
|
||
ContinuousScenario ||--o{ Dataset : "datasets (optional)"
|
||
ContinuousScenario ||--|| PlantEnv : "environment (FR-8.2.14)"
|
||
PlantEnv ||--o{ DecisionSource : "plugins / external agents"
|
||
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"
|
||
PlantEnv }o--|| ProcessImage : "observe/apply via datastore"
|
||
```
|
||
|
||
### 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).
|
||
12. **PlantEnv cycle** (FR-8.2.14): `observe()` returns every input channel
|
||
(values, names, codes, ranges, sim-time); `apply()` lands the decided
|
||
outputs in the output image before the next `observe()` — verified by a
|
||
deterministic two-tick test where the decision source copies an input
|
||
to an output and the second observation contains it.
|
||
13. **Plugin parity** (FR-8.2.15): the same plant logic runs unchanged as
|
||
a classic `plugins:` entry and as an `env.observe/apply` decision
|
||
source — identical outputs over a 1000-tick headless run (regression
|
||
gate for the adapter).
|
||
14. **External agent**: a decision source stepping the env over a
|
||
socket (non-Python agent) produces identical results to the in-process
|
||
variant on the same scenario (FR-8.2.15).
|
||
|
||
---
|
||
|
||
## 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: in implementation (2026-10-03).** The implementation
|
||
> decisions below are binding; milestones M1–M4 are implemented and
|
||
> verified (M5 configuration and M6 scenario view remain). A browser
|
||
> front-end overlooking and manipulating the simulated node's I/O
|
||
> values and states at runtime.
|
||
|
||
### 10.1 Motivation
|
||
|
||
Terminal monitors (§9) suffice for single engineers, but FAT benches are
|
||
collaborative: commissioning engineers, PLC programmers, and test
|
||
witnesses need a **shared, visual** view of the virtual I/O — the
|
||
equivalent of watching the LEDs on a real WAGO node, plus the ability to
|
||
force signals the way WAGO's own WBM does on real devices. A web UI keeps
|
||
the simulator usable from any machine on the bench network without
|
||
installing tools.
|
||
|
||
### 10.2 Requirements
|
||
|
||
- FR-10.2.1: The simulator shall serve a web UI (default port 8080,
|
||
configurable) alongside the Modbus TCP server, with no external backend
|
||
dependencies beyond Python (FastAPI/Starlette + static files, or an
|
||
equivalent embedded approach — implementation choice, to be decided).
|
||
- FR-10.2.2: The UI shall present the **node overview**: module list with
|
||
article numbers, per-module channel tables (type, module, channel,
|
||
addressing `DI0.1` per FR-4.3.11, Modbus address, current value/state),
|
||
derived from the same process image the Modbus server serves. On the
|
||
node graphic (requested 2026-10-04, refined same day): each module
|
||
shows its kind+slot designation (`DI0`, `DO2`, `AI4`, …) in the
|
||
module's **lower left corner, below the channel indicators**, and
|
||
the channel indicators carry only the 0-based channel number: below
|
||
each DI/DO LED, and on analog modules outside the indicator stack,
|
||
above the overflow and below the underflow LED (FR-10.2.14) so the
|
||
limit LEDs stay adjacent to the bar. DI/DO channels lay out in
|
||
**vertical columns of eight** — channels 0–7 in the left column,
|
||
8–15 in the next — and modules are drawn as **thin, tall bezels**
|
||
mirroring the proportions of real WAGO DIN-rail housings (narrow
|
||
width, vertically stacked LEDs).
|
||
- FR-10.2.3: Values shall update live (WebSocket push or short-polling),
|
||
including changes made by the PLC/master over Modbus, by scenarios, by
|
||
the console monitor, or by other UI clients — one shared source of truth.
|
||
- FR-10.2.4: **Manipulation**: clicking a DI/DO toggles it; an AI/AO gets a
|
||
numeric input (engineering units, using alias scale/offset when
|
||
defined); `pulse` (with duration) and `ramp` (target + duration) actions
|
||
on DI/DO/AI. All writes go through the datastore's Modbus-equivalent
|
||
paths (FR-9.2.6) so mirrors, watchdog, and statistics stay consistent.
|
||
Clicking an analog **bar indicator** (requested 2026-10-04) sets the
|
||
value proportionally to the click position within the channel's
|
||
display range (FR-10.2.14) — AI = field side, AO = master-side force;
|
||
exact values remain enterable via the channel list.
|
||
- FR-10.2.5: **Diagnostics view**: watchdog status (0x1006), statistics
|
||
block (0x1029, with reset button), module diagnosis (0x1050), LED error
|
||
registers (0x1020/0x1021) — mirroring WBM pages of the real device —
|
||
with controls to inject/clear faults for FAT fault cases.
|
||
- FR-10.2.6: A scenario/run view: which scenario (if any) is loaded,
|
||
current step, verdicts so far, and basic controls (start/abort). Headless
|
||
result reports (JSONL/CSV) remain downloadable via the CLI.
|
||
- FR-10.2.7: Multi-client consistency: several simultaneous browser
|
||
clients see the same state; forced values propagate to all clients and
|
||
to the Modbus side within one refresh interval.
|
||
- FR-10.2.8: Access control for shared benches: optional read-only mode
|
||
and (v2) token-based login, so witnesses can observe without accident
|
||
rights.
|
||
- FR-10.2.9: The UI must not disturb deterministic scenario runs: when a
|
||
scenario owns an alias, UI writes to it are rejected with a clear
|
||
message (alias-ownership rules of §8 FR-8.2.2 apply).
|
||
- FR-10.2.10: The UI's **configuration** pages (Ethernet/port settings,
|
||
Modbus watchdog settings, I/O Config with SAVE CONFIG, Modbus Mapping
|
||
overview, address-selection DIP switch) are specified in §11; §10 covers
|
||
value/state oversight and manipulation.
|
||
- FR-10.2.11: **Channel names** (requested 2026-10-03): every module
|
||
channel can carry an application-specific name (e.g. `start_button`,
|
||
`tank_level`). Names live in the node configuration — not in the
|
||
module catalog (catalog articles are shared per type) — persisted
|
||
alongside the module list, and are re-applied after process-image
|
||
rebuilds (§11). Unnamed channels fall back to the generic channel
|
||
label (e.g. `DI 1`). Names surface everywhere the UI lists a channel:
|
||
node overview, the channel list, the configuration editor, and
|
||
tooltips.
|
||
- FR-10.2.12: **Import/export of module configuration** (requested
|
||
2026-10-03): the complete module configuration — module list with
|
||
slot order, per-channel names, and coupler settings — can be
|
||
downloaded as a single JSON file and imported back. Import validates
|
||
the payload (known articles, ≤ 64 modules, name shapes) and applies
|
||
it atomically: module rebuild + channel names + coupler settings in
|
||
one step, with the same persistence behavior as SAVE CONFIG (§11).
|
||
Export/import makes FAT benches shareable and reproducible between
|
||
engineers and benches.
|
||
- FR-10.2.13: **Split overview** (requested 2026-10-03): the overview
|
||
page is divided in two — the realistic node graphic (DIN rail with
|
||
LEDs, FR-10.2.2) on top, and below it a scrollable **channel list**:
|
||
one row per channel with the I/O code (`DI0.0`), the configured
|
||
channel name (if any), and a control for the value — a switch for
|
||
DI/DO and a numeric field for AI/AO — which both displays the live
|
||
value and manipulates it (same semantics as FR-10.2.4: DI = field
|
||
side, DO/AO = master-side force through internal paths). The list
|
||
updates live like the rail (FR-10.2.3) and is the primary working
|
||
surface for benches with many channels.
|
||
- FR-10.2.14: **Analog display ranges** (requested 2026-10-04): the
|
||
analog bar indicators (rail + list) scale a channel's raw value
|
||
against a display range. The default range is 0–4095 counts
|
||
(100 % at 4095, matching the WAGO 750 analog modules' 12-bit
|
||
scaling); per-channel `min`/`max` can be configured in the node
|
||
configuration — used by both AI (generated) and AO (used) values.
|
||
Values within the range fill the bar 0–100 % from the bottom.
|
||
Underflow/overflow are indicated: the bar fills in the limit color
|
||
(red) and a small red LED above the bar lights for overflow, below
|
||
the bar for underflow, clearing when the value returns to range.
|
||
**Signed values** (2026-10-04): range bounds may be negative —
|
||
`-65534 <= min < max <= 65535` (the raw 16-bit image holds
|
||
0–65535; a negative display bound lets signed plant quantities map
|
||
onto the bar). Simulated AI values and AO forces accept negative
|
||
numbers within the same window; values are stored in the image
|
||
as their 16-bit two's-complement representation.
|
||
**Dense runtime store** (2026-10-04): `node.channel_ranges` is
|
||
always complete — every analog channel of the current layout has
|
||
an entry (unconfigured channels carry the materialized default),
|
||
codes that no longer match an analog channel are pruned on
|
||
rebuild, so envs and API consumers can read ranges without
|
||
default-fallback guesswork. Persistence (node YAML, config
|
||
export) stays **lean**: only explicit non-default entries are
|
||
written, so defaults survive a future DEFAULT_RANGE change.
|
||
- FR-10.2.15: **Plant environment control** (requested 2026-10-04):
|
||
the web UI can load/unload environments at runtime — the CLI's
|
||
`--env` becomes the startup default, not the only option. An
|
||
**environment card** shows the loaded ref, its state (running /
|
||
paused / done / none), sim-time, and tick ms; controls: **start**
|
||
(load + tick), **stop** (cancel the ticker, field freezes),
|
||
**pause/resume** (ticker cancelled or rescheduled, instance
|
||
kept — pause is a deliberate FAT fault: "plant stuck"),
|
||
**reset** (`env.reset()`, deterministic fresh episode,
|
||
NFR-8.7.3), **restart** (stop + start of the same ref, fresh
|
||
instance). Rules: exactly one environment at a time (one decision
|
||
source, FR-8.2.15); `is_done()` ends the ticker (state `done`, no
|
||
auto-restart); a module-list rebuild (SAVE CONFIG, FR-11.2.1)
|
||
**discards** the env — its bindings (channel bindings in the
|
||
model, sim state) belong to the old layout, exactly like values
|
||
are lost on a KBUS reset; starting a new env is refused while
|
||
one is loaded (stop first — explicit, no silent replacement);
|
||
readonly mode (FR-10.2.8) disables every mutating control.
|
||
- FR-10.2.16: **Environment file authoring** (requested 2026-10-04,
|
||
planned): create/modify/delete environment source files (e.g.
|
||
`envs/*.py`) through the web UI — an editor pane with the plain
|
||
PlantEnv subclass pattern (FR-8.2.17), plus upload/download.
|
||
Security note: the UI writing importable Python is remote code
|
||
execution **by design**; it requires the same trust boundary as
|
||
the bench network (auth, FR-10.2.8) and SHALL be disabled in
|
||
readonly mode; delete is refused while the env is loaded; the
|
||
on-disk directory must be explicit in configuration (today
|
||
`envs/` resolves from the working directory, not the installed
|
||
package). Status: planned, roadmap phase 5.
|
||
- FR-10.2.17: **Hot reload of environment code** (requested
|
||
2026-10-04, implemented same day as part of FR-10.2.15): Python
|
||
caches modules in `sys.modules`; a restart of an env whose
|
||
source changed on disk would silently reuse the old class.
|
||
`load_env(ref, reload=True)` — used by the UI's start/restart —
|
||
invalidates the cached module before import so a fresh instance
|
||
always picks up the edited code. The CLI's startup `--env` and
|
||
the headless `sim` runner do not reload (the process is new
|
||
anyway).
|
||
|
||
### 10.3 Implementation decisions (binding, 2026-10-03)
|
||
|
||
- **Framework**: FastAPI (Starlette) + Uvicorn, embedded **in-process on
|
||
the same asyncio event loop** as the Modbus TCP server — a UI write and
|
||
a Modbus request never race on the datastore.
|
||
- **Front-end**: single-page vanilla HTML/CSS/JS (no build step) under
|
||
`src/wago_sim/webui/static/`; realistic node graphics (DIN-rail strip,
|
||
module bezels, glowing per-channel LEDs for DI/DO, bar/numeric
|
||
indicators for AI/AO) drawn with inline SVG/CSS.
|
||
- **Authentication** (user request 2026-10-03): session-cookie login with
|
||
in-memory token store; default credentials `admin` / `simu`,
|
||
overridable via env (`WAGO_SIM_WEB_USER` / `WAGO_SIM_WEB_PASSWORD`);
|
||
optional read-only mode via `WAGO_SIM_WEB_READONLY`. No third-party
|
||
auth/session dependencies (`hashlib.scrypt` hashing; sessions need not
|
||
survive restarts on a bench).
|
||
- **Backend data access** (user request 2026-10-03): the API **never uses
|
||
Modbus TCP**. It reads via the internal memory handler
|
||
(`WagoDatastore` getters, `VirtualNode` channel model,
|
||
`ConfigRegisterBank`, `ModbusWatchdog`, `ModbusStatistics`) and writes
|
||
via internal paths chosen by role: simulated-field writes go through
|
||
`set_digital_input_channel()` / `set_input_word()` (as the scenario
|
||
engine does); master-side forcing goes through the datastore's
|
||
internal `write_bits()` / `write_words()` dispatch (mirrors, watchdog
|
||
kicks, and statistics stay consistent, FR-9.2.6 semantics) — but
|
||
always as direct in-process calls, never via TCP.
|
||
- **Live updates**: WebSocket push (`/api/ws`) — datastore
|
||
`add_listener` for output changes plus a 10 Hz snapshot-difference
|
||
sweep for input-side changes.
|
||
- **Wiring**: `serve` starts the web UI by default (`--no-web` to
|
||
disable; `--web-port`, env `WAGO_SIM_WEB_PORT`; default port 8080).
|
||
|
||
### 10.4 Architecture sketch (as-built)
|
||
|
||
```
|
||
src/wago_sim/
|
||
├── webui/
|
||
│ ├── __init__.py
|
||
│ ├── app.py # build_webui(app, ...) -> FastAPI; API + static
|
||
│ ├── auth.py # login/logout, scrypt hash, session dependency
|
||
│ ├── state.py # snapshot builder + change detection
|
||
│ ├── api.py # REST + WebSocket routes
|
||
│ └── static/ # single-page front-end (no build step)
|
||
```
|
||
|
||
### 10.5 API surface (as-built)
|
||
|
||
| Method/Route | Purpose | FR |
|
||
|---|---|---|
|
||
| `POST /api/auth/login`, `POST /api/auth/logout` | session cookie (`HttpOnly`) | auth |
|
||
| `GET /api/node` | module list, channel tables with `DI0.1`-style and Modbus addresses | 10.2.2 |
|
||
| `GET /api/values` | full channel-value snapshot | 10.2.3 |
|
||
| `WS /api/ws` | live value/state deltas | 10.2.3, 10.2.7 |
|
||
| `POST /api/channels/{kind}/{module}/{channel}` | set/toggle/pulse/ramp a channel (field side) | 10.2.4 |
|
||
| `POST /api/force` | master-side force at a Modbus address (internal dispatch) | 10.2.4 |
|
||
| `GET /api/diagnostics` | watchdog status, statistics, module diagnosis, LED errors | 10.2.5 |
|
||
| `POST /api/diagnostics/reset-stats` | statistics reset (0x1029 semantics) | 10.2.5 |
|
||
| `POST /api/diagnostics/inject-fault`, `POST /api/diagnostics/clear-fault` | LED / module-diagnosis injection | 10.2.5 |
|
||
| `GET/PUT /api/config/coupler` | watchdog settings, response delay, idle timeout, MAC-ID | §11 subset |
|
||
| `GET/PUT /api/config/modules` | module list CRUD + process-image rebuild | §11 FR-11.2.1–11.2.4 |
|
||
| `GET /api/config/export` · `POST /api/config/import` | full configuration (modules + channel names + coupler) as JSON | 10.2.12 |
|
||
| `GET/PUT /api/config/channel-names` | read/write per-channel names | 10.2.11 |
|
||
| `GET/PUT /api/config/channel-ranges` | read/write analog display ranges | 10.2.14 |
|
||
| `GET /api/env` | environment status: ref, state, sim-time, tick ms | 10.2.15 |
|
||
| `POST /api/env/start` | load + start an env by ref (refuses while one is loaded) | 10.2.15 |
|
||
| `POST /api/env/stop` · `pause` · `resume` · `reset` · `restart` | ticker lifecycle; reset = deterministic fresh episode | 10.2.15 |
|
||
|
||
### 10.6 Milestones
|
||
|
||
| # | Deliverable |
|
||
|---|---|
|
||
| M1 | WebUI skeleton, in-process FastAPI, CLI/env wiring, authentication (`admin`/`simu`), `GET /api/node` |
|
||
| M2 | Node overview page — DIN-rail graphics, LEDs, analog indicators, live values (WS) |
|
||
| M3 | Manipulation — toggle/pulse/ramp, AI engineering units, master-side force |
|
||
| M4 | Diagnostics — watchdog/statistics/diagnosis view, reset, fault injection |
|
||
| M5 | Configuration — coupler forms; module CRUD with image rebuild and persistence |
|
||
| M5a | Channel names (FR-10.2.11) + configuration import/export (FR-10.2.12) + split overview with channel list (FR-10.2.13) |
|
||
| M5b | Analog display ranges (FR-10.2.14) |
|
||
| M6 | (deferred) Scenario run view (FR-10.2.6) + alias-ownership rejection (FR-10.2.9) |
|
||
| M7 | Plant environment control (FR-10.2.15 + FR-10.2.17): runtime load/stop/pause/resume/reset/restart + hot reload |
|
||
| M8 | (planned) Environment file authoring (FR-10.2.16) |
|
||
|
||
### 10.7 Acceptance criteria
|
||
|
||
1. Opening the UI shows the node's module list and channel table exactly
|
||
as configured, with `DI0.1`-style addresses and Modbus addresses.
|
||
2. Toggling a DO in the browser is immediately visible to a connected
|
||
Modbus master (and vice versa: a master write appears in the UI within
|
||
one refresh interval).
|
||
3. Forcing an AI value uses engineering units and the configured scale.
|
||
4. The diagnostics page shows live watchdog status and statistics; the
|
||
reset button clears the counters (register 0x1029 semantics).
|
||
5. With a running scenario, writes to scenario-owned aliases are rejected
|
||
with a visible message; the scenario continues unaffected.
|
||
6. Two simultaneous browsers stay consistent with each other and with
|
||
the Modbus image.
|
||
7. **Environment control** (FR-10.2.15): an env started from the UI ticks
|
||
paced (its field changes visible to a Modbus master and the overview);
|
||
pause freezes the field; reset returns to the initial state; a
|
||
module-list SAVE CONFIG discards the loaded env (status none); an
|
||
edit to the env source on disk + restart runs the new code (hot
|
||
reload); starting while loaded is refused; readonly blocks all
|
||
env mutations.
|
||
|
||
---
|
||
|
||
## 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.
|
||
- FR-11.2.6: **Module reordering** (requested 2026-10-04, implemented
|
||
same day): the web UI's I/O configuration pane supports rearranging
|
||
modules two ways — drag-and-drop of module rows (HTML5 DnD) and
|
||
move up/down arrow controls per slot; both are one gesture each and
|
||
trigger the same process-image rebuild + persistence as any module
|
||
change (FR-11.2.1–11.2.3). Because I/O codes number modules per type
|
||
(FR-4.3.11 v1.5), moving a module past a module of a different kind
|
||
does not renumber the other kind's channels — only the relative
|
||
order within the same type affects addressing.
|
||
|
||
### 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. Deployment — Service / Containerized Operation
|
||
|
||
> **Status: implemented 2026-10-03 (v1.3).** The simulator can run as a
|
||
> long-lived service: docker/compose container, or a systemd unit on bare
|
||
> metal/VM. Verified: image builds, container answers Modbus requests,
|
||
> and `docker stop` (SIGTERM) shuts down gracefully in < 1 s.
|
||
|
||
### 12.1 Motivation
|
||
|
||
A FAT bench simulator is infrastructure: it must start on boot, survive
|
||
restarts, log to the platform's log pipeline, and stop cleanly so a
|
||
connected PLC sees a controlled fieldbus-node failure rather than a
|
||
> hung socket — the exact condition watchdog/FAT fault cases test.
|
||
|
||
### 12.2 Requirements (all implemented)
|
||
|
||
- FR-12.2.1: **Environment-variable configuration**: `WAGO_SIM_NODE`,
|
||
`WAGO_SIM_HOST`, `WAGO_SIM_PORT` configure `serve` without CLI arguments
|
||
(container `CMD ["wago-sim", "serve"]` needs zero arguments).
|
||
Explicit CLI arguments override environment values.
|
||
- FR-12.2.2: **Graceful shutdown**: on SIGINT/SIGTERM (docker stop,
|
||
systemctl stop) the server closes all client connections and the
|
||
listening socket via `server.stop()` before exiting — no forced kill,
|
||
no lingering sockets. Verified: `docker stop` completes in 0.34 s.
|
||
- FR-12.2.3: **Container image** (`Dockerfile`): `python:3.12-slim`,
|
||
non-root `wago-sim` user, unbuffered stdout logging, baked-in example
|
||
config for out-of-the-box startup, TCP health check, ~197 MB image.
|
||
- FR-12.2.4: **Compose service** (`docker-compose.yml`): port mapping
|
||
(5020→502 default; 502 needs privileges on some hosts), read-only
|
||
mounts for config/scenarios/plugins, writable `out/` for reports,
|
||
resource limits, log rotation, `restart: unless-stopped`.
|
||
- FR-12.2.5: **systemd unit** (`deploy/wago-sim.service`): dedicated
|
||
service user, env-var configuration, `Restart=on-failure`, hardened
|
||
(NoNewPrivileges, ProtectSystem, CAP_NET_BIND_SERVICE for port 502
|
||
without root).
|
||
- FR-12.2.6: Logs to stdout/stderr only (docker json-file / journald
|
||
collect them); single-line format, no ANSI colors.
|
||
|
||
### 12.3 Acceptance criteria (all verified)
|
||
|
||
1. `docker build -t wago-sim .` succeeds; image ~197 MB.
|
||
2. `docker run -p <host>:502 -e WAGO_SIM_NODE=... wago-sim` serves
|
||
Modbus (constants `0x2000–0x2008` read correctly from the container).
|
||
3. `docker stop` completes in < 1 s with clean shutdown logs.
|
||
4. `docker compose up -d` starts a healthy service; `down` tears it down.
|
||
5. CLI tests cover env-var configuration and override precedence.
|
||
|
||
---
|
||
|
||
## 13. Feature Request — Multi-Vendor Device Profiles
|
||
|
||
> **Status: requested 2026-10-03, not yet planned into a roadmap phase.**
|
||
> Simulate additional Modbus TCP bus couplers — other WAGO 750 couplers
|
||
> and other brands' Modbus TCP I/O heads — reusing the existing protocol
|
||
> stack. This section captures the requirements; it does not commit to
|
||
> a schedule.
|
||
|
||
### 13.1 Motivation
|
||
|
||
FAT benches test more than one controller and fieldbus family. Today
|
||
the simulator is single-device: the 750-362 register map, limits,
|
||
watchdog, and statistics are hard-coded across the protocol stack
|
||
(`pdu.py` limits, `datastore.py` address dispatch, `server.py`
|
||
connection limit and process-area predicate, `registers.py`,
|
||
`watchdog.py`, `stats.py`, `trace.py` annotations). The transport layer
|
||
(`mbap.py`), PDU codecs, server skeleton, scenario engine, DSL,
|
||
plugins, and reporting are protocol-generic and directly reusable.
|
||
|
||
A second Modbus TCP device multiplies the value of the bench without a
|
||
protocol rewrite: the deltas between Modbus TCP devices are — address
|
||
maps, configuration areas, watchdog presence/semantics, statistics
|
||
blocks, connection limits, and module catalogs with image-layout
|
||
rules. These are exactly the facts the device manual provides.
|
||
|
||
### 13.2 Requirements
|
||
|
||
- FR-13.2.1: **Device profile abstraction**: extract the per-device
|
||
facts into a profile concept covering: supported function codes and
|
||
per-FC limits, max concurrent connections, idle-timeout behavior,
|
||
process-image address map (reads and writes), read-back mirror rules,
|
||
configuration/diagnostic register areas and semantics, watchdog
|
||
(presence, register layout, modes), statistics block layout, and
|
||
identity/description registers. The 750-362 becomes the first
|
||
profile; its behavior must be unchanged after the refactor (the
|
||
existing test suite is the regression gate).
|
||
- FR-13.2.2: **Scenario-engine independence**: the scenario engine,
|
||
DSL, plugins, and reports must remain device-agnostic — they interact
|
||
with a node only through the channel model (`ModuleType` /
|
||
`Channel` / `ChannelLocation`) and the datastore's set/read surface.
|
||
Scenarios must not require changes when a profile is added.
|
||
- FR-13.2.3: **Per-vendor module catalogs**: catalogs keyed by brand;
|
||
each entry defines word/bit widths and channels. A node config
|
||
selects brand + module list; validation must reject cross-brand
|
||
module mixes within one node (e.g., a WAGO 750-430 in an ABB S800
|
||
station). New catalogs are data-only additions (no engine changes).
|
||
- FR-13.2.4: **Process-image layout rules per brand**: layout becomes
|
||
a profile parameter (word-first + byte-packed bit modules is the
|
||
WAGO §7.3.2.1 rule; other brands may pack differently — e.g.,
|
||
byte-per-channel images). The channel model and alias addressing
|
||
(FR-4.3.11 module-based and pool-based schemes) must keep working on
|
||
top of any layout.
|
||
- FR-13.2.5: **Config registers / watchdog / statistics as pluggable
|
||
blocks**: implementations following the existing
|
||
`registers.py`/`watchdog.py`/`stats.py` pattern, one set per
|
||
profile. Devices lacking a Modbus watchdog or statistics block
|
||
expose nothing (or an illegal-address exception) there — that is a
|
||
device fact, not a simulator limitation.
|
||
- FR-13.2.6: **Trace annotations per profile**: `trace.py` address
|
||
annotations (currently 750-362 Tables 119–122) must come from the
|
||
active profile so verbose tracing stays meaningful for every device.
|
||
- FR-13.2.7: **Profile selection**: node config gains a `device:` key
|
||
(e.g. `device: wago-750-362`, the default for backward
|
||
compatibility); CLI/`WAGO_SIM_NODE` paths unchanged. `wago-sim
|
||
validate` must report unknown devices and cross-brand module mixes
|
||
as clean errors.
|
||
|
||
### 13.3 Candidate devices (informative, non-binding)
|
||
|
||
- WAGO 750 Modbus TCP family beyond the 362 (352/363/860…): same
|
||
register-map family — profile effort is mainly constants and
|
||
identity registers.
|
||
- Other vendors' Modbus TCP I/O heads / controllers (e.g., Phoenix
|
||
Contact Inline, Beckhoff BK/BC series): a profile written against
|
||
the vendor manual; effort dominated by the address map and
|
||
configuration-register semantics.
|
||
|
||
### 13.4 Acceptance criteria (draft)
|
||
|
||
1. The 750-362 profile passes the full existing test suite unchanged
|
||
after the profile extraction.
|
||
2. A second profile (from §13.3) serves a PLC master that reads/writes
|
||
its documented process image; identity/description registers match
|
||
the vendor manual; the same scenario DSL drives its channels without
|
||
modification.
|
||
3. Scenarios written for one device run unchanged against another
|
||
device with equivalent channel configuration.
|
||
4. `wago-sim validate` reports unknown device keys and cross-brand
|
||
module mixes with clean one-line errors.
|
||
|
||
---
|
||
|
||
## 14. Feature Request — Additional Fieldbus Protocols (PROFINET, COMLI)
|
||
|
||
> **Status: requested 2026-10-03, not yet planned into a roadmap phase.**
|
||
> Extend the simulator beyond Modbus TCP, motivated by ABB AC800M/800xA
|
||
> systems and their distributed I/O: PROFINET IO (AC800M PN controllers
|
||
> with S800 I/O via CI840-class interface modules) and COMLI (ABB's
|
||
> protocol between AC800M controllers and S800/S900 I/O via CI51x/CI522
|
||
> communication interfaces, TCP-encapsulated).
|
||
|
||
### 14.1 Motivation
|
||
|
||
An AC800M FAT bench talks to its distributed I/O over the protocol the
|
||
controller is actually configured for. Simulating the I/O side of
|
||
those protocols lets the same scenario engine, datasets (§8), bench
|
||
mode (§9), and reporting drive FAT benches for ABB controllers, not
|
||
only Modbus TCP masters.
|
||
|
||
The architecture separates cleanly along the same seams the
|
||
multi-vendor profiles (§13) expose: the scenario engine couples to a
|
||
node only through the channel model and the datastore's set/read
|
||
surface; a new protocol is a new transport + protocol layer binding to
|
||
the same process-image core.
|
||
|
||
### 14.2 Requirements
|
||
|
||
- FR-14.2.1: **Protocol abstraction**: a protocol binding defines how
|
||
the field-side process image is exposed to a master: wire protocol,
|
||
address/identifier model, cyclic vs. transactional access, and the
|
||
device's diagnostics/alarm model. The simulator core (process image,
|
||
scenario engine, plugins, reports) stays protocol-independent.
|
||
- FR-14.2.2: **PROFINET IO device simulation** (candidate; scope to be
|
||
defined against AC800M PN-controller configuration): simulate the
|
||
device side of PROFINET IO — DCP identify/boot, cyclic RT I/O data
|
||
exchange for a configured module list, and enough record/identification
|
||
data for the IO controller to bring the device into data exchange.
|
||
Scope note: a full PROFINET stack is a major undertaking; a
|
||
realistic first target is an Ethernet-level RT simulation of one
|
||
S800-style station profile with a fixed module list, driven by the
|
||
existing deterministic clock and scenario engine.
|
||
- FR-14.2.3: **COMLI device simulation** (candidate; scope to be
|
||
defined against AC800M/CI51x/CI522 documentation): simulate the
|
||
I/O-side endpoint of ABB COMLI over TCP as used between AC800M and
|
||
S800/S900 I/O through CI communication interfaces — the
|
||
register/point addressing model, cyclic polling, and the diagnostics
|
||
responses the CI interface expects. COMLI's request/response
|
||
register semantics map naturally onto the datastore's set/read
|
||
surface.
|
||
- FR-14.2.4: **Shared scenario engine**: scenarios, aliases, macros,
|
||
assertions, plugins, and datasets (§8) work unchanged against
|
||
protocol bindings; only the channel/identifier model of the target
|
||
I/O system is expressed in the node config (module list with
|
||
brand-specific article numbers). A scenario must not name wire-level
|
||
addresses; alias resolution stays per-device via FR-4.3.11/FR-13.2.4.
|
||
- FR-14.2.5: **Trace and diagnostics parity**: per-protocol verbose
|
||
tracing equivalent to the Modbus plain-text trace (NFR-6.4):
|
||
PROFINET RT frames and COMLI requests decoded in readable form with
|
||
device-level annotations; protocol error counters analogous to the
|
||
Modbus statistics block.
|
||
- FR-14.2.6: **Fault-injection parity** (per protocol): for PROFINET —
|
||
frame loss/corruption, device pull/plug, subslot mismatch,
|
||
communication/watchdog timeouts (the PN equivalent of the Modbus
|
||
watchdog §3.4); for COMLI — request timeouts, malformed messages,
|
||
CI-interface failure simulation. These must be scriptable from
|
||
scenarios like the Modbus fault paths (§3.6).
|
||
- FR-14.2.7: **Deployment parity**: protocol servers run under the
|
||
same service/container model (§12): env-var configuration, graceful
|
||
shutdown; one container may host multiple protocol bindings for a
|
||
node (a station reachable via different protocols in different bench
|
||
setups).
|
||
|
||
### 14.3 Scoping questions (answer before planning)
|
||
|
||
1. PROFINET: full device stack vs. targeted RT simulation of one
|
||
station profile? Which controller-side tooling (e.g., ABB Compact
|
||
Builder) must accept the simulated device?
|
||
2. COMLI: exact CI51x/CI522 variants in scope; is TCP encapsulation
|
||
sufficient, or is the serial CI variant also needed?
|
||
3. Are both protocols needed, or is one the realistic FAT target?
|
||
(COMLI is far smaller in scope; PROFINET conformance is the larger
|
||
effort by an order of magnitude.)
|
||
4. Which I/O module families per protocol (S800 modules via
|
||
PROFINET/COMLI interfaces; WAGO 750 modules over Modbus as today)?
|
||
|
||
### 14.4 Acceptance criteria (draft)
|
||
|
||
1. An AC800M controller (or a protocol-level test master representing
|
||
one) establishes data exchange with the simulated I/O station and
|
||
reads/writes the documented process data.
|
||
2. The existing scenario DSL drives input changes and observes output
|
||
reactions through the new protocol binding, unchanged.
|
||
3. Verbose tracing shows per-protocol decoded traffic with device
|
||
annotations; fault-injection steps behave like the Modbus
|
||
equivalents.
|
||
4. The 750-362 Modbus behavior is unaffected (regression suite green).
|
||
|
||
---
|
||
|
||
## 15. Roadmap (post-framework)
|
||
|
||
| Phase | Feature |
|
||
|---|---|
|
||
| 2 | Module catalog expansion (analog, counter, SSI, DALI/DSI, serial modules), mailbox parameters; **bench mode tooling (§9)**: `--set` seeding, console monitor, runtime manipulation commands |
|
||
| 3 | **Long-duration simulation (§8)**: CSV dataset playback, degradation models, invariants, soak-test reporting |
|
||
| 4 | FAT report generator (HTML/PDF), scenario comparison, template library per project type |
|
||
| 5 | Multi-node orchestration (several couplers in one FAT bench), UDP variant, WBM simulation; **web UI (§10)**; **runtime device configuration (§11)**: module hot-configuration with image rebuild, DIP-switch emulation, WBM-equivalent pages, device-state persistence |
|
||
| 6 | CI integration: run scenarios headless, export JUnit XML |
|
||
| ? | **Multi-vendor device profiles (§13)** and **additional fieldbus protocols PROFINET/COMLI (§14)** — not yet scheduled; phase assignment after §13.3/§14.3 scoping |
|
||
|
||
---
|
||
|
||
## 16. References
|
||
|
||
- WAGO 750-362 manual `m07500362_xxxxxxxx_0en.pdf`, sections 7.2–7.3 (process image), 11.2 (Modbus), 12.2 (module data architecture)
|
||
- WAGO I/O System 750 system description `m075xxxxx_xxxxxxxx_0en.pdf`
|
||
- MODBUS Application Protocol Specification V1.1b3 / MODBUS TCP specification (modbus.org)
|
||
- ABB CI51x/CI522 communication interface and S800/S900 I/O documentation (COMLI)
|
||
- PROFINET specification (PI, profibus.com) — if PROFINET scope is confirmed |