Multi Driver¶
Core rule¶
Use exactly one of these forms in io.toml:
- single driver:
io.driver+io.params - composed drivers:
io.drivers = [...]
Do not mix them in one file.
When multi-driver makes sense¶
- one runtime needs both device-style and broker-style exchange
- you are bridging multiple protocol planes into one process image
- a gradual migration requires two transports during commissioning
Example¶
Communication Example: Multi-Driver Composition (io.drivers)¶
This example shows how to run multiple I/O transports in one runtime via
io.drivers = [...].
What you learn¶
- how composed drivers are declared and validated
- why production projects often need mixed protocols
- why
io.driverandio.driversare mutually exclusive
Files in this folder¶
src/main.st: minimal%IX -> %QXlogic (DO0 := DI0)src/config.st: task binding plusVAR_CONFIGmapping (P1.DI0/P1.DO0)io.toml: composed Modbus/TCP + MQTT profileruntime.toml: runtime profile defaultstrust-lsp.toml: project profile
Step 1: Build PLC logic¶
Why: isolate source correctness from integration concerns.
cd examples/communication/multi_driver
trust-runtime build --project . --sources src
Step 2: Inspect composed driver config¶
Why: each transport has its own failure modes; composition should be explicit.
[io]
drivers = [
{ name = "modbus-tcp", params = { address = "127.0.0.1:1502", unit_id = 1, input_start = 0, output_start = 0, timeout_ms = 500, on_error = "fault" } },
{ name = "mqtt", params = { broker = "127.0.0.1:1883", topic_in = "trust/examples/multi/in", topic_out = "trust/examples/multi/out", reconnect_ms = 500, keep_alive_s = 5, allow_insecure_remote = false } }
]
Why this pattern:
- Modbus handles deterministic register exchange.
- MQTT handles broker/event style exchange.
- One runtime can compose both process-image contributors.
Step 3: Validate composed form¶
Why: schema catches malformed driver entries and invalid mutually-exclusive configuration.
trust-runtime validate --project .
Step 4: Enforce mutual-exclusion rule¶
Why: mixed forms are ambiguous and rejected.
Do not mix these in one file:
[io]
driver = "simulated"
drivers = [{ name = "mqtt", params = {} }]
Use exactly one form:
- single driver:
io.driver+io.params - multi-driver:
io.drivers = [...]
Step 5: Commission transport-by-transport¶
Why: debugging all transports simultaneously obscures root cause.
Recommended sequence:
- bring up Modbus endpoint and verify first,
- bring up MQTT broker and verify second,
- run both together and verify combined cycle behavior.
Common mistakes¶
- composing drivers before validating each one independently
- inconsistent timeout/reconnect settings across transports
- forgetting safe-state policy while expanding transport scope
- mixing
io.driverandio.drivers
Full tutorial¶
Tutorial 17: I/O Backends and Multi-Driver Configuration¶
This tutorial teaches how to configure and validate all major I/O backend forms:
loopbacksimulatedgpiomodbus-tcpmqtt- composed
io.drivers = [...]
Why this tutorial exists¶
Many users only run simulated in early testing, then hit avoidable failures
when moving to hardware or multi-protocol integration. This guide explains each
form and what it is for.
What you will learn¶
- when to use each backend
- how to keep safe-state output policy across driver changes
- how to validate
io.tomlbefore runtime start - why
io.driverandio.driverscannot be mixed
Prerequisites¶
- complete Tutorial 13 first
- optional hardware/network brokers for full runtime checks
Step 1: Prepare a sandbox project¶
Why: swapping backends repeatedly is easier in a disposable copy.
rm -rf /tmp/trust-io-backends
cp -R /tmp/trust-tutorial-13 /tmp/trust-io-backends
cd /tmp/trust-io-backends
Build once:
trust-runtime build --project . --sources src
Step 2: Start with loopback (local functional sanity)¶
Why: loopback is the fastest no-hardware path to confirm %Q writes can feed
%I reads for local testing.
cat > io.toml <<'TOML'
[io]
driver = "loopback"
params = {}
[[io.safe_state]]
address = "%QX0.0"
value = "FALSE"
TOML
trust-runtime validate --project .
Expected result: - validation passes
Step 3: Switch to simulated (deterministic software I/O)¶
Why: simulated is useful when you want fake process behavior without physical
pins or remote protocols.
cat > io.toml <<'TOML'
[io]
driver = "simulated"
params = {}
[[io.safe_state]]
address = "%QX0.0"
value = "FALSE"
TOML
trust-runtime validate --project .
Expected result: - validation passes
Step 4: Configure GPIO profile (hardware edge I/O)¶
Why: GPIO needs explicit IEC-to-pin mapping and debounce/initial state choices.
cat > io.toml <<'TOML'
[io]
driver = "gpio"
[io.params]
backend = "sysfs"
inputs = [
{ address = "%IX0.0", line = 17, debounce_ms = 5 }
]
outputs = [
{ address = "%QX0.0", line = 27, initial = false }
]
[[io.safe_state]]
address = "%QX0.0"
value = "FALSE"
TOML
trust-runtime validate --project .
Expected result: - config is schema-valid - runtime may still require platform permissions/hardware at run time
Step 5: Configure Modbus/TCP profile¶
Why: Modbus introduces transport, unit-id, and timeout policy decisions.
cat > io.toml <<'TOML'
[io]
driver = "modbus-tcp"
[io.params]
address = "192.168.0.10:502"
unit_id = 1
input_start = 0
output_start = 0
timeout_ms = 500
on_error = "fault"
[[io.safe_state]]
address = "%QX0.0"
value = "FALSE"
TOML
trust-runtime validate --project .
Expected result: - config validates - actual runtime connectivity depends on reachable Modbus server
Step 6: Configure MQTT profile¶
Why: MQTT requires explicit topic boundaries and reconnect behavior.
cat > io.toml <<'TOML'
[io]
driver = "mqtt"
[io.params]
broker = "127.0.0.1:1883"
topic_in = "line/in"
topic_out = "line/out"
reconnect_ms = 500
keep_alive_s = 5
allow_insecure_remote = false
[[io.safe_state]]
address = "%QX0.0"
value = "FALSE"
TOML
trust-runtime validate --project .
Expected result: - config validates - runtime connectivity depends on broker availability and ACLs
Step 7: Use multi-driver composition (io.drivers)¶
Why: production systems may need multiple protocol drivers in one runtime.
cat > io.toml <<'TOML'
[io]
drivers = [
{ name = "modbus-tcp", params = { address = "192.168.0.10:502", unit_id = 1, input_start = 0, output_start = 0, timeout_ms = 500, on_error = "fault" } },
{ name = "mqtt", params = { broker = "127.0.0.1:1883", topic_in = "line/in", topic_out = "line/out", reconnect_ms = 500, keep_alive_s = 5, allow_insecure_remote = false } }
]
[[io.safe_state]]
address = "%QX0.0"
value = "FALSE"
TOML
trust-runtime validate --project .
Expected result: - configuration validates as composed-driver form
Step 8: Understand the mutual-exclusion rule¶
Why: mixed driver forms are ambiguous and rejected.
Do not do this in the same file:
[io]
driver = "simulated"
drivers = [{ name = "mqtt", params = {} }]
Use exactly one form:
- io.driver + io.params
- or io.drivers = [...]
Step 9: Final verification command set¶
Why: standard validation loop catches most integration mistakes early.
trust-runtime build --project . --sources src
trust-runtime validate --project .
trust-runtime ctl --project . io-read
Common mistakes¶
- forgetting
io.safe_statefor critical outputs - setting
on_error = "ignore"during commissioning - validating config but not verifying real network reachability
- mixing
io.driverandio.drivers
Completion checklist¶
- [ ] each backend form authored and validated
- [ ] multi-driver form validated
- [ ] safe-state policy preserved across changes
- [ ] mutual-exclusion rule understood and enforced