Skip to content

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.driver and io.drivers are mutually exclusive

Files in this folder

  • src/main.st: minimal %IX -> %QX logic (DO0 := DI0)
  • src/config.st: task binding plus VAR_CONFIG mapping (P1.DI0/P1.DO0)
  • io.toml: composed Modbus/TCP + MQTT profile
  • runtime.toml: runtime profile defaults
  • trust-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:

  1. bring up Modbus endpoint and verify first,
  2. bring up MQTT broker and verify second,
  3. 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.driver and io.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:

  • loopback
  • simulated
  • gpio
  • modbus-tcp
  • mqtt
  • 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.toml before runtime start
  • why io.driver and io.drivers cannot 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_state for critical outputs
  • setting on_error = "ignore" during commissioning
  • validating config but not verifying real network reachability
  • mixing io.driver and io.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