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
Success means each driver has a separate purpose, no config mixes single-driver and multi-driver forms, and one process-image address is not silently owned by two paths.
Use the single-driver form first unless commissioning or migration really needs two planes at once.
The examples show when the extra complexity is justified and how to keep the config readable.
Example¶
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¶
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