📝 Update spec and README

This commit is contained in:
Jonas Bengtsson
2026-10-04 14:15:26 +02:00
parent 59679b96ee
commit c30b444ef1
2 changed files with 84 additions and 10 deletions
+12 -1
View File
@@ -325,7 +325,7 @@ requirement ID for each item in parentheses.
statistics table (0x1029 words) with reset, LED-error statistics table (0x1029 words) with reset, LED-error
(0x1020/0x1021) and module-diagnosis (0x1050) fault injection (0x1020/0x1021) and module-diagnosis (0x1050) fault injection
and clear; SF LED on the coupler reflects injected LED errors. and clear; SF LED on the coupler reflects injected LED errors.
- [x] M5: configuration — coupler settings forms (watchdog time/masks/type/close-on-timeout via 0x1000–0x100B, response delay 0x1037, idle timeout 0x1030) with the device's own validation surfaced (writes refused while the watchdog runs → field error); module list editor over the 13-article catalog with SAVE CONFIG → live process-image rebuild (`rebuild_node`: datastore survives, values clear like a KBUS reset, 0x1022–0x1025 and 0x2030+ updated) + persistence to the node-config YAML via a comment-preserving custom emitter. - [x] M5: configuration — coupler settings forms (watchdog time/masks/type/close-on-timeout via 0x1000–0x100B, response delay 0x1037, idle timeout 0x1030) with the device's own validation surfaced (writes refused while the watchdog runs → field error); module list editor over the 13-article catalog with SAVE CONFIG → live process-image rebuild (`rebuild_node`: datastore survives, values clear like a KBUS reset, 0x1022–0x1025 and 0x2030+ updated) + persistence to the node-config YAML via a comment-preserving custom emitter. Modules re-orderable by drag-and-drop or ▲/▼ arrows (FR-11.2.6); with per-type I/O codes (FR-4.3.11 v1.5) cross-kind moves don't renumber other kinds' channels.
- [x] M5a: channel names for every module channel (FR-10.2.11) — - [x] M5a: channel names for every module channel (FR-10.2.11) —
node-config persisted (`channel_names:` mapping, I/O code → node-config persisted (`channel_names:` mapping, I/O code →
name), survive module saves and rebuilds, shown in the overview name), survive module saves and rebuilds, shown in the overview
@@ -400,6 +400,17 @@ not yet implemented. Datasets are **optional**: signal logic
(plugins, scheduled steps, degradation) runs in parallel with CSV (plugins, scheduled steps, degradation) runs in parallel with CSV
streaming, or entirely without it. streaming, or entirely without it.
- [ ] Plant environment `scenario/plantenv.py` (FR-8.2.14–16, requested
2026-10-04): gym-style `PlantEnv` — deterministic engine-owned
tick clock (never wall-clock); per tick:
`observe()` returns the WAGO input image in engineering units
(channel values, names, per-type I/O codes, ranges, sim-time);
the decision source decides; `apply()` writes the decided
outputs through the datastore's internal paths (mirrors/
statistics/watchdog stay consistent). Existing `plugins:`
become thin adapters; external (non-Python) decision sources
step the env over a socket; one decision source per output
alias per tick (ownership conflicts are errors).
- [ ] `scenario/datasets.py` — streaming CSV playback for designated - [ ] `scenario/datasets.py` — streaming CSV playback for designated
inputs (OPTIONAL driver): header-driven time column, inputs (OPTIONAL driver): header-driven time column,
`interpolation: linear|hold`, `repeat` with seamless seams, `interpolation: linear|hold`, `repeat` with seamless seams,
+72 -9
View File
@@ -567,6 +567,12 @@ unchanged whether or not CSV streaming is used.
(6) scheduled steps due at this tick execute, (6) scheduled steps due at this tick execute,
(7) snapshots are taken if due. (7) snapshots are taken if due.
This order is part of the specification and shall be covered by tests. 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 - FR-8.2.13: **Logic-only continuous runs**: with no `datasets:` block at
all, continuous mode shall work unchanged — demand profiles and other all, continuous mode shall work unchanged — demand profiles and other
exogenous signals may be generated *inside* plant-model plugins (e.g., a exogenous signals may be generated *inside* plant-model plugins (e.g., a
@@ -574,6 +580,44 @@ unchanged whether or not CSV streaming is used.
top-level `until:` bounding the run. All §8.3–8.4 machinery (degradation, top-level `until:` bounding the run. All §8.3–8.4 machinery (degradation,
invariants, rate limits, snapshots) applies identically, whether or not invariants, rate limits, snapshots) applies identically, whether or not
any CSV streaming is configured. any CSV streaming is configured.
- FR-8.2.14: **Plant environment abstraction** (requested 2026-10-04): the
continuous engine exposes a gymnasium-style **environment object** —
`PlantEnv` — as the single programmatic interface to the simulated plant.
Per deterministic tick (the engine's fixed tick clock, FR-4.3.9 — never
wall-clock), the cycle is:
**`state = env.observe()`** → the caller (plant logic, an external
agent, or the scenario) decides → **`env.apply(actions)`** writes the
decided output values → the engine advances exactly one tick
(edges, datasets, degradation, invariants — FR-8.2.12 order) → repeat.
`observe()` returns the state of the **generic inputs** — the WAGO
input process image in engineering units (all DI/DO/AI/AO channel
values, per-type module/channel addressing, configurable channel
names, plus the active aliases) — and `apply(actions)` maps decided
values onto the **output process image** through the datastore's
internal Modbus write paths, so mirrors, statistics, and watchdog
behavior stay consistent (FR-9.2.6 semantics). The environment owns
no policy: the plant model (physics, closed-loop control) is supplied
by the caller, exactly like `gym.Env` vs. its agents.
- FR-8.2.15: **Environment integration**: `PlantEnv` is the interface the
per-tick plugins (FR-4.3.8) are migrated onto: a plugin becomes
`env.observe()` → decide → `env.apply()` per tick, and a scenario may
alternatively register an external `step(env)` agent — including a
non-Python one (e.g., a trained RL policy) communicating over a
socket/pipe — without touching the engine loop. The existing
`plugins:` mechanism keeps working unchanged (thin adapter); the
engine always owns the tick clock, and exactly one decision source
may drive an output alias per tick (ownership conflicts are validation
errors, extending FR-8.2.2).
- FR-8.2.16: **Environment API surface** (concrete):
`env.observe() -> PlantState` (channel values, names, I/O codes,
ranges, sim-time, wear/parameters); `env.apply(actions) -> None`
(actions keyed by I/O code or alias name; unknown keys are errors);
`env.reset(config) -> PlantState` (fresh episode, deterministic
seed); `env.tick_ms`, `env.sim_time_ms` (read-only clock); and the
full §8.3/§8.4 machinery (parameters, degradation, invariants,
snapshots) reachable as `env.parameters`, `env.invariants`. The same
object is served over the scenario-run HTTP API in server mode (§9),
so external agents can run against a live PLC-connected bench.
### 8.3 Requirements — degradation models (plant logic over time) ### 8.3 Requirements — degradation models (plant logic over time)
@@ -744,15 +788,20 @@ src/wago_sim/scenario/
│ # time_factor — OPTIONAL driver in continuous mode │ # time_factor — OPTIONAL driver in continuous mode
├── degradation.py # WearCounters, DegradationModel registry, maintenance actions ├── degradation.py # WearCounters, DegradationModel registry, maintenance actions
├── invariants.py # invariant & rate-limit evaluation, episode aggregation ├── 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 └── engine.py # mode: continuous, parameter() accessor, snapshot
# scheduler; fixed tick pipeline per FR-8.2.12: # scheduler; fixed tick pipeline per FR-8.2.12:
# datasets -> edges -> plugins -> degradation -> # datasets -> edges -> plugins/env -> degradation ->
# invariants -> scheduled steps -> snapshots # invariants -> scheduled steps -> snapshots
``` ```
```mermaid ```mermaid
erDiagram erDiagram
ContinuousScenario ||--o{ Dataset : "datasets (optional)" ContinuousScenario ||--o{ Dataset : "datasets (optional)"
ContinuousScenario ||--|| PlantEnv : "environment (FR-8.2.14)"
PlantEnv ||--o{ DecisionSource : "plugins / external agents"
Dataset ||--|{ ColumnMapping : "alias columns" Dataset ||--|{ ColumnMapping : "alias columns"
ContinuousScenario ||--|{ PlantPlugin : "signal logic (always runs)" ContinuousScenario ||--|{ PlantPlugin : "signal logic (always runs)"
ContinuousScenario ||--|{ DegradationModel : degradation ContinuousScenario ||--|{ DegradationModel : degradation
@@ -764,6 +813,7 @@ erDiagram
ContinuousScenario ||--o{ Snapshot : "periodic dumps" ContinuousScenario ||--o{ Snapshot : "periodic dumps"
PlantPlugin }o--|| DegradationModel : "reads parameters" PlantPlugin }o--|| DegradationModel : "reads parameters"
PlantPlugin }o--o{ Alias : "reads dataset-driven + computes logic-driven" PlantPlugin }o--o{ Alias : "reads dataset-driven + computes logic-driven"
PlantEnv }o--|| ProcessImage : "observe/apply via datastore"
``` ```
### 8.7 Non-functional requirements (long-duration) ### 8.7 Non-functional requirements (long-duration)
@@ -809,6 +859,18 @@ erDiagram
tick-order test. tick-order test.
11. A continuous scenario with no datasets, no scheduled steps, and no 11. A continuous scenario with no datasets, no scheduled steps, and no
`until:` is rejected at validation time (FR-8.2.1). `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).
--- ---
@@ -1135,14 +1197,15 @@ This feature request closes that gap.
- FR-11.2.5: A reconfiguration event is logged (sim-time, change - 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 description, new image sizes) and visible in the web UI's I/O Config
page, mirroring the WBM SAVE CONFIG flow. page, mirroring the WBM SAVE CONFIG flow.
- FR-11.2.6: **Module reordering** (requested 2026-10-04): the web UI's - FR-11.2.6: **Module reordering** (requested 2026-10-04, implemented
I/O configuration pane supports rearranging modules — move up/down same day): the web UI's I/O configuration pane supports rearranging
controls per module slot. Reordering triggers the same process-image modules two ways — drag-and-drop of module rows (HTML5 DnD) and
rebuild as any module change (FR-11.2.1–11.2.3) and persists the new move up/down arrow controls per slot; both are one gesture each and
order. Because I/O codes number modules per type (FR-4.3.11 v1.5), trigger the same process-image rebuild + persistence as any module
moving a module past a module of a different kind does not renumber change (FR-11.2.1–11.2.3). Because I/O codes number modules per type
the other kind's channels — only the relative order within the same (FR-4.3.11 v1.5), moving a module past a module of a different kind
type affects addressing. 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 ### 11.3 Requirements — address-selection DIP switch emulation