Status and the write gate
The engine knows whether it runs: taktwerk.status, the heartbeat, stale inputs. It does not
know whether a model’s outputs make sense. A package says so in a status output, and the
project’s write gate holds the outputs a PLC or a plant data
system would otherwise take.
A status output
One integer scalar output, conventionally status: 0 when every output is valid, else one
bit per reason. An integer output of any width works for a gate; u32 maps to an OPC UA
UInt32 and a PLC’s UDINT. The engine interprets none of the bits; the project picks the
ones that hold writes.
The recommended layout keeps the bits that make outputs unusable apart from the ones that only inform:
| bits | value | meaning | outputs |
|---|---|---|---|
| 0 | 1 | not ready: initialising, too few measurements so far | not valid |
| 1 | 2 | an input not plausible: out of the model’s range, not finite | not valid |
| 2 | 4 | the computation failed this step (no convergence, singular data); outputs held | not valid |
| 3 to 7 | 8 to 128 | further reasons of the package’s own why outputs are not valid | not valid |
| 8 | 256 | an output limited to its bounds | valid |
| 9 | 512 | degraded: running on a fallback, a reduced model, fewer measurements | valid |
| 10 to 15 | 1024 to 32768 | further warnings of the package’s own | valid |
So status & 255 is zero when the outputs may be written, and a gate closes on mask = 255;
a project that also refuses degraded results adds 512. Document every bit the package uses in
its variable’s description or the package’s own documentation.
A step that sets a bit returns success. A non-success return is a fault: it stops the engine,
or with on_error = "hold" or "safe" takes the instance down and holds the heartbeat
(Faults), which is right for a bug and wrong for a bad measurement.
The gate
[[connector]]
id = "plc"
kind = "opcua-client"
endpoint = "opc.tcp://10.0.0.10:4840"
writes = "on"
[connector.gate]
open_after = 5 # good ticks in a row before writes resume
conditions = [{ signal = "est.status", mask = 255 }]
[connector.map]
"est.x_hat" = { node = "ns=4;s=Line.x_hat", min = 0.0, max = 50.0 }
"est.status" = { node = "ns=4;s=Line.est_status", gated = false }
"taktwerk.heartbeat" = "ns=4;s=Line.heartbeat"
"taktwerk.status" = "ns=4;s=Line.taktwerk_status"
- The gate is closed while the engine is not running or the heartbeat did not advance, while a
condition holds, or while a gated output is not finite or outside its mapping’s
minandmax. It opens afteropen_aftergood ticks in a row. - A closed gate writes none of the gated outputs, so the target keeps their last values. The
heartbeat and
taktwerk.statusare always written; map the status output withgated = falsetoo, so the PLC sees why. - Every change is a
gateevent (open,closed: <reason>) inevents.csv, and<prefix>.connector.<id>.gate_open,gate_opens,gate_closesandgate_heldcount it. - The
restconnector takes the samegatetable.
The PLC still owns the safe state: it watches the heartbeat and taktwerk.status, and decides
what a held value means for the plant (see
the safety contract).
Checks from outside
taktwerk engine status --expect 'est.status & 255 == 0' passes only while the outputs are
valid, so a checklist or a monitoring job can test the same bits
(Operating a running engine).