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:
| placeholder | meaning | example |
|---|---|---|
<host> | the controller’s address | 192.168.1.10 |
<user> | your login on the controller | admin |
<dir> | a writable directory on the controller for everything here | /opt/custom/taktwerk |
<port> | a free TCP port for taktwerk’s OPC UA server | 4841 |
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
variantbecome inputs; a fixed selection is aconstmember 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 anoutput. - Sizes. Give every array its shape in dimension names or literal lengths. A size the
library reports at init is a
reported = truedimension member; give that dimension amax, 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 leftNULLafter 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 incheck, 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
endpointtoopc.tcp://<host>:<port>so it listens on the controller’s network address instead of only on127.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 Runningand a risingheartbeat;overrunsshould 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
| message | cause and fix |
|---|---|
GLIBC_2.xx not found | the binary or library was built against a newer glibc than the controller’s: step 3 |
load: .../lib/<arch>: No such file | no library for this machine’s architecture in the package |
abi.confirmed is false | review 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 |