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

Tutorial: a single-entry library on a controller

You have a model compiled as a C shared library that exports one function for init and step, and you want it running on a Linux controller next to a PLC, stepping at a fixed period and reachable over OPC UA. This tutorial takes it there with taktwerk, from your workstation over ssh.

The library looks like this; names differ, the shape is what matters:

typedef struct { int32_t *init_flag; int32_t variant; double *k; double *u; char *title; } my_in;
typedef struct { double *y; char *label; int32_t *n_u; int32_t *n_y; } my_out;
int my_entry(int id, char *in, char *out);   /* 0 on success */

One function, an integer id, and two opaque pointers the library casts to its input and output structs. A flag in the input struct tells init from step, and the library may report its own sizes at init. examples/raw-single-entry is a complete library of this kind to practise on.

Placeholders used below:

placeholdermeaningexample
<host>the controller’s address192.168.1.10
<user>your login on the controlleradmin
<dir>a writable directory on the controller for everything here/opt/custom/taktwerk
<port>a free TCP port for taktwerk’s OPC UA server4841

1. Look at the controller

ssh <user>@<host> 'uname -m; ldd --version | head -1; ss -ltn'

Note three things: the architecture (x86_64 or aarch64), the glibc version, and which ports are taken. A controller with its own OPC UA server usually holds 4840; pick another <port>.

2. Install taktwerk on your workstation

On a Linux workstation with Rust 1.88 or newer:

cargo install --locked taktwerk
taktwerk --version

This gives you import-header, new, inspect, check and tui locally.

3. Build taktwerk for the controller

If the workstation has the controller’s architecture and a glibc no newer than the controller’s, the binary you just installed runs there too: use ~/.cargo/bin/taktwerk.

Otherwise cross-build it from the published crate with cargo-zigbuild, which links against an older glibc than the workstation’s. Name a glibc version at or below the controller’s:

cargo install --locked cargo-zigbuild        # also needs `zig` on the PATH
rustup target add x86_64-unknown-linux-gnu   # or aarch64-unknown-linux-gnu, armv7-unknown-linux-gnueabihf

curl -sSfL https://static.crates.io/crates/taktwerk/taktwerk-0.5.2.crate | tar xz
cd taktwerk-0.5.2
cargo zigbuild --release --locked --target x86_64-unknown-linux-gnu.2.35
cd ..
cp taktwerk-0.5.2/target/x86_64-unknown-linux-gnu/release/taktwerk ./taktwerk-target

Your library has the same constraint: build it for the controller’s architecture and against a glibc at or below the controller’s (zig cc -target x86_64-linux-gnu.2.35 -shared -fPIC ... works for C).

4. Make a model package

A package is a directory with the library under lib/<arch>/ and a descriptor that tells taktwerk how to call it. The header produces the descriptor:

mkdir -p mymodel/lib/x86_64
cp libmymodel.so mymodel/lib/x86_64/
taktwerk import-header my_model.h --entry my_entry \
  --arg-struct in=my_in --arg-struct out=my_out -o mymodel/taktwerk-model.toml

--entry names the one function; each --arg-struct says which struct an opaque parameter carries, by the parameter’s name in the header. The result is a proposal: it starts with notes, one per guess, and abi.confirmed = false. Go through the notes and fix the descriptor:

  • The id. The leading integer argument is a const; set the value the library expects.
  • The init flag. A flag-like member is guessed as phase = { init = 1, step = 0 }; check the values.
  • By-value members such as variant become inputs; a fixed selection is a const member instead.
  • Roles. Pointer members become inputs or outputs by their struct. A value you want to change while running without it being an input is a tunable; a member of the input struct the library writes back is an output.
  • Sizes. Give every array its shape in dimension names or literal lengths. A size the library reports at init is a reported = true dimension member; give that dimension a max, because buffers are allocated at it.
  • Output pointers the library sets. If the library allocates its own output buffers at init and stores their addresses into the output struct’s members, the header cannot show it: add owner = "library" to those members, or the outputs stay zero. A pointer left NULL after init then fails init with a message naming the member.
  • Text. A char * member is text: give its variable a literal capacity with the NUL included, shape = [32]. Text shows as a string in check, on OPC UA and in the TUI.
  • Matrices need two shape entries and their layout.

Then set abi.confirmed = true. The descriptor has every key; the example’s taktwerk-model.toml shows a confirmed one and the command that proposed it.

5. Write the project

The project file says which models run, at which period, with which sizes, and where the OPC UA server listens:

taktwerk new mymodel --kind raw --port <port> -o project.toml --write

Open project.toml and:

  • bind every dimension under [instance.dims];
  • set start values under [instance.parameters] (text as a TOML string: title = "tank 3");
  • set the server’s endpoint to opc.tcp://<host>:<port> so it listens on the controller’s network address instead of only on 127.0.0.1.

inspect, new and check load the library of the machine they run on (lib/<uname -m>/). On a workstation of another architecture, run them on the controller in step 6 instead.

taktwerk inspect mymodel --kind raw   # the interface as taktwerk reads it
taktwerk check project.toml           # load, resolve, bind; prints the signal table

6. Ship to the controller

ssh <user>@<host> mkdir -p <dir>
scp -r taktwerk-target <user>@<host>:<dir>/taktwerk
scp -r mymodel project.toml <user>@<host>:<dir>/

The model path in project.toml is relative to the project file, so keep the two side by side as they are on the workstation.

7. Run and test

On the controller:

ssh <user>@<host>
cd <dir>
./taktwerk check project.toml
./taktwerk run project.toml

check loads the library and verifies the plan without calling the model. run calls the entry once with the init phase, checks the sizes the library reported against the project, then calls it once per tick with the step phase. A non-zero return stops the engine with the code in the log.

From the workstation, in a second terminal:

taktwerk tui opc.tcp://<host>:<port>
  • The header shows status Running and a rising heartbeat; overruns should stay 0.
  • Select an input or a tunable and set a value; the outputs follow from the next tick.
  • Text signals show as strings and are edited as strings.

Any OPC UA client works the same way: every signal is a node under the namespace urn:taktwerk.

Stop the engine with Ctrl+C. It prints a summary:

cycles 5999  published 5999  overruns 0  stale 0  cycle mean 0.059 ms  max 0.073 ms

max against the tick is your margin. A process started from an ssh session usually ends when that session closes; for anything longer than a test, run it as a service.

8. Keep it running

Running in production has the systemd unit, the real-time settings (SCHED_FIFO, a CPU, locked memory) and what each needs. On a controller that also runs a PLC runtime, keep the cycle thread’s priority below the runtime’s own tasks.

To connect the model to the PLC’s variables instead of driving it by hand, add an opcua-client connector that maps signals to the PLC’s OPC UA nodes; taktwerk new writes a commented one into the project. See Connectors.

When something fails

messagecause and fix
GLIBC_2.xx not foundthe binary or library was built against a newer glibc than the controller’s: step 3
load: .../lib/<arch>: No such fileno library for this machine’s architecture in the package
abi.confirmed is falsereview the proposal and set abi.confirmed = true
the library reports … but dimension … is bound to …the project’s dims do not match what the library reports at init
Address already in use<port> is taken: pick another in the server’s endpoint