Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Project file

One TOML file per engine is the source of truth. Unknown keys are errors. Relative paths are resolved against the project file’s directory.

[engine]
tick_ms = 10.0
input_max_age_ms = 100.0

[engine.realtime]
policy = "fifo"
priority = 80
cpu = 3
lock_memory = true

[models.pid]
kind = "raw"            # or "fmi"
path = "models/pid"     # package directory, .fmu, or extracted FMU directory

[[instance]]            # instances run in declared order
id = "loop1"
model = "pid"
every = 2               # period = every × tick_ms
dims = { n = 3, m = "from-server" }
parameters = { kp = 1.5, ki = [0.1, 0.2, 0.3] }
inputs = { y = { signal = "plant.y", max_age_ms = 50.0 }, sp = "loop1.sp" }
outputs = { u = "plant.u" }
tunables = { kp = "tuning.kp" }

[[connector]]
id = "ua"
kind = "opcua-server"

[engine]

keytypedefaultmeaning
tick_msfloatrequiredbase tick, milliseconds, > 0; every period is a multiple of it
start_timefloat0.0model time of the first tick, seconds
input_max_age_msfloatnonedefault age limit of external inputs; none never goes stale
system_prefixstring"taktwerk"prefix of the engine’s own signals; non-empty
realtimetablenonereal-time settings of the cycle thread, below

[engine.realtime]

Applies to the cycle thread only; the I/O threads keep the default scheduling.

keytypedefaultmeaning
policystringrequired"fifo" (SCHED_FIFO) or "rr" (SCHED_RR)
priorityintegerrequired1 to 99
cpuintegernoneCPU the cycle thread is pinned to
lock_memoryboolfalsemlockall before the first tick

A refused setting (missing privilege, CPU out of range) stops the engine at start. See Running in production for the limits it needs.

[models.<id>]

keytypemeaning
kindstring"fmi" (FMI 2 or 3 co-simulation) or "raw" (C library package)
pathstringan .fmu, an extracted FMU directory, or a raw package directory

[[instance]]

keytypedefaultmeaning
idstringrequiredunique, non-empty, no .
modelstringrequireda key of [models]
everyinteger1period in ticks, ≥ 1
dimstable{}dimension → length or "from-server"
parameterstable{}parameter or tunable → start value
inputstable{}input → signal, { signal, max_age_ms }, name[i] for a scalar, or a list of element sources
outputstable{}output → signal
tunablestable{}tunable → signal

Every key in dims, parameters, inputs, outputs and tunables must name a dimension or a variable of that causality in the model.

dims

  • n = 3: a fixed length, checked against the dimension’s min and max.
  • n = "from-server": the length is discovered at init. The model needs a one-dimensional variable of shape [n]; the engine asks each connector for the length of the external item mapped to that variable’s signal. Today the OPC UA client answers for nodes in its map.
  • Unbound: the model’s default; without one, init fails.

parameters

Start values of parameters and tunables. Unset ones keep the model’s value: the FMU’s start value, or zero for a raw library.

  • A scalar takes a number or a boolean. Integers convert to floats; floats never convert to integers.
  • An array takes nested arrays, one level per dimension, first index outermost: A = [[1.0, 0.0], [0.0, 1.0]]. The engine stores it in the variable’s layout.
  • An array also takes one flat array of the full length, taken as already stored in the variable’s layout: A = [1.0, 0.0, 0.0, 1.0].

inputs, outputs, tunables

Each maps a variable to a signal name. Unmapped variables get <instance>.<variable>. An input binding is either a signal name or a table:

inputs = { y = "plant.y" }                                   # signal only
inputs = { y = { signal = "plant.y", max_age_ms = 50.0 } }   # with an age limit

max_age_ms overrides engine.input_max_age_ms for that input. It is refused on a wired input (one some instance outputs). When several instances consume one external signal, the explicit limits they set must agree; one explicit limit wins over the default. See Concepts for wiring rules.

A one-dimensional input may instead be composed element by element from a list with one source per element, in element order:

inputs = { y = ["plc.p", "est.mu", { signal = "plc.t", max_age_ms = 50.0 }, "est.v[1]"] }
  • name is a scalar signal of the input’s type: another instance’s scalar output (wired) or an external input, created when no one else uses it.
  • name[i] is element i, counted from 0, of another instance’s one-dimensional output of the input’s type.
  • { signal, max_age_ms } sets an age limit on one external element, as above.
  • The list’s length must equal the input’s bound length. Multi-dimensional and text inputs, outputs and tunables cannot be composed.
  • The composed value is the signal <instance>.<input>, read-only, stored each time the instance is about to step (and before its init), so a connector shows what the model received. taktwerk check lists every composed input with its element sources.

A scalar input takes one element the same way, without a list:

inputs = { n_st = "mpc.u[0]" }

It is a composed input of one element, with the same rules and the same read-only signal <instance>.<input>. A name with [ or ] that is not an element of another instance’s one-dimensional output is refused: no signal name contains brackets, so it never becomes an external input or a connector’s node mapping.

[[connector]]

keytypemeaning
idstringunique
kindstring"opcua-server" or "opcua-client"

All other keys belong to the connector kind; see Connectors. A project may have several connectors of one kind, for example one client per PLC.

Changes

Everything in the project file is read at start. Structural changes (models, dimensions, wiring, connectors) need a restart. While running, only tunables change, through a connector.