Simulation¶
Deterministic control over a scenario-backed simulated target: step it cycle by cycle, run it in
real time, pause it, and reset it back to its declared start state. Requires feature flag
Simulation enabled — off by default.
Development and CI only. These endpoints drive machine state, and are deliberately not
exposed on a shared network by default. They are also structurally unable to reach real
hardware: every route requires the target to have a Scenario configured (see
Configuration), and startup fails outright if a
Scenario is ever declared on a target that is not Mode: Simulated — so there is no
configuration under which sim/* can touch a real PLC. That is a guardrail, not a licence to put
this behind a public endpoint.
A target only has these routes at all once it has a Scenario and ScenarioHostedService has
loaded it at startup. A plcId that is not configured at all, and a plcId that is configured
but has no Scenario, are different problems and are reported differently — see
Error Codes below.
Get State¶
Returns the simulated target’s state, cycle count and elapsed time. Requires role = admin, scope |
curl http://localhost:5000/api/plcs/demo/sim/state
{
"data": {
"state": "Stopped",
"cycleCount": 0,
"elapsedMs": 0,
"scanMs": 100,
"error": null
},
"error": null
}
state is one of Stopped (seeded but not ticking — the initial state), Running (the
real-time pump is ticking), Paused (stopped, can be resumed or stepped) or Faulted (a cycle
threw; terminal until reset). error carries the failure message once Faulted, surfaced here
rather than only logged — a scenario author, or an agent correcting one, needs it without shelling
into the host’s logs. cycleCount and elapsedMs count from load or the last sim/reset, not
from process start.
Step¶
Runs exactly |
Runs exactly cycles scan cycles inline and returns once they are all done — the deterministic
entry point a CI assertion drives the machine with. cycles must be between 1 and 100000
(see MaxStepCycles below); an out-of-range value is rejected before anything
runs.
curl -X POST "http://localhost:5000/api/plcs/demo/sim/step?cycles=20"
{
"data": { "state": "Stopped", "cycleCount": 20, "elapsedMs": 2000, "scanMs": 100, "error": null },
"error": null
}
Stepping a simulation that is currently Running is rejected — pause it first. A pumped
simulation’s cycle boundaries are wall-clock timed, so interleaving a step with the pump cannot
produce a trace anything could reproduce.
Run¶
Starts the real-time pump, one scan cycle every |
Starts the real-time pump: one scan cycle every scanMs (the scenario’s declared scan period),
on its own background loop. Idempotent — calling run on an already-Running simulation is a
no-op, not an error.
curl -X POST http://localhost:5000/api/plcs/demo/sim/run
Pause¶
Stops the real-time pump, leaving cycle count, elapsed time and symbol values where they were. |
Stops the real-time pump, leaving cycleCount, elapsedMs and every symbol’s current value
exactly where they were. A paused simulation can be resumed with sim/run or driven deterministically
with sim/step.
curl -X POST http://localhost:5000/api/plcs/demo/sim/pause
Reset¶
Stops the pump, re-seeds every symbol from the scenario’s declared start state and clears any fault. |
Stops the pump if it is running, re-seeds every symbol from machine.yaml’s declared start state,
and clears any fault — the only way back to a usable target once state is Faulted. Also the
way to replay a scenario that has settled and stopped moving (see
Scenario Authoring below).
curl -X POST http://localhost:5000/api/plcs/demo/sim/reset
sim/reset re-seeds only the scenario’s half of the target’s symbols, not the
InitialValues a PlcTargets entry may also declare — the two are independent seed sources
layered on top of each other at startup (see
Configuration), and only the scenario half
is re-applied on reset.
The nine stRev.* revision counters are never reset. A reset silently rewrites every symbol
in the store back to its seed value without notifying any subscriber (seeding never fires
subscription callbacks, by design), so if the counters reset to zero too, the very next bump
could republish a value some subscriber had already observed before the reset — indistinguishable
from no change at all. They keep counting from wherever they were instead, so every value a
subscriber has ever seen for the life of the host stays behind the next one.
Error Codes¶
In addition to the common error codes:
Code |
HTTP Status |
Description |
|---|---|---|
|
404 |
The target exists and is |
|
409 |
The request conflicts with the simulation’s current state in a way the caller can resolve: stepping while |
A scan-cycle failure that is not one of those two conflicts — a symbol write failing partway
through a flush, for instance — is a server error, not a conflict: it answers 500
INTERNAL_ERROR like any other unexpected failure, and separately faults the simulation (visible
via sim/state).
MaxStepCycles¶
cycles on sim/step is bounded at 100,000 per request. At a 100 ms scan that is close to
three simulated hours — far more than any test needs in one call — while protecting the host from
a single request that never returns. Request more cycles across multiple calls if you genuinely
need them; each call is still bounded individually.
Rate Limiting¶
sim/* carries no per-endpoint rate-limit policy, unlike every other write-ish slice in this
API: the feature flag is off by default, the surface only exists for targets with a Scenario,
and being driven hard by a CI harness stepping thousands of cycles is the point, not a risk. The
one thing worth guarding against a single request for — an enormous cycles value — is
MaxStepCycles’s job, not a per-endpoint policy’s.
The global per-user limiter still applies. adsify installs one RateLimiting:GlobalPerUser
limiter ahead of every request regardless of endpoint — 200 requests/second by default, and
RateLimiting:Enabled defaults to true — and sim/* is not exempt from it, only from the
per-endpoint policies every other write-ish slice carries. A harness polling sim/state in a
tight loop, or issuing many small sim/step calls instead of fewer larger ones, will hit 429
RATE_LIMITED from that global budget well before MaxStepCycles becomes relevant. See
Rate Limiting for the full configuration and
how to raise or disable it.