MQTT¶
Good fit vs bad fit¶
| Good fit | Bad fit |
|---|---|
| brokered fan-out and pub/sub | strict register-oriented device control |
| remote telemetry and event distribution | hard real-time same-host coordination |
| systems with existing broker ACL/TLS policy | cases where a direct device protocol already exists |
First things to decide¶
- what broker boundary is authoritative?
- what topic names represent input and output clearly?
- are messages raw process-image bytes or typed scalar point topics?
- what reconnect/keep-alive policy is acceptable?
- what TLS/auth requirements apply in this network?
Success means the broker boundary, topic directions, payload format, reconnect policy, and TLS/auth requirements are explicit before messages are trusted as plant state.
The default profile is a raw byte bridge: topic_in payload bytes are copied
into %I, and %Q bytes are published to topic_out. Use input_points and
output_points when MQTT topics carry typed scalar values instead. Each point
names the topic, process-image byte offset, optional bit for bool, data type
(bool, u16, i16, u32, i32, or f32), payload format (text, json,
binary_le, or binary_be), and optional scale/offset. Scaling converts
input raw values as engineering = raw * scale + offset; output writes invert
that formula before publishing.
This typed scalar map is intentionally separate from Sparkplug B. For Sparkplug
B outbound node metrics, set [io.params.sparkplug] with enabled = true,
namespace = "spBv1.0", spec_version = "3.0.0", group_id, and
edge_node_id, then define typed output_points with stable metric_name
values. The runtime publishes NBIRTH on connect, configures NDEATH as the MQTT
last will, and publishes NDATA payloads from typed output points using the Tahu
scalar protobuf wire shape.
Sparkplug support is intentionally bounded today: outbound node metrics only. Commands, device-level DBIRTH/DDATA topics, aliases, templates, and store-and-forward are not part of this profile.
At runtime, MQTT exchange is worker-backed. Scan-cycle reads copy the latest
fresh worker snapshot or return the configured on_error policy result, and
scan-cycle writes hand off the latest desired output without waiting for broker
readiness or publish latency. The MQTT worker owns broker connect/poll/publish,
reconnect backoff, Sparkplug birth/data publishing, and stale/degraded health
reporting through the normal driver status surface.
Example and commissioning guide¶
This example shows how to use io.driver = "mqtt" for broker-based I/O exchange.
What you learn¶
- topic design for PLC input/output channels
- reconnect and keep-alive tuning basics
- why secure broker boundary decisions must be explicit
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: MQTT backend profileruntime.toml: runtime defaultstrust-lsp.toml: project settings
Step 1: Build first¶
Why: separate compiler issues from transport issues.
cd examples/communication/mqtt
trust-runtime build --project . --sources src
Step 2: Review io.toml¶
Why: topic and reconnect policy directly shape reliability and security.
[io]
driver = "mqtt"
[io.params]
broker = "127.0.0.1:1883"
topic_in = "trust/examples/mqtt/in"
topic_out = "trust/examples/mqtt/out"
reconnect_ms = 500
keep_alive_s = 5
allow_insecure_remote = false
Field intent:
broker: MQTT endpoint.topic_in: messages consumed into%Iimage.topic_out: messages published from%Qimage.reconnect_ms: backoff cadence for broken sessions.keep_alive_s: session liveness interval.allow_insecure_remote: blocks unsafe remote configuration.
By default, MQTT payloads are raw process-image bytes. If the broker uses typed scalar topics instead, add explicit point maps:
[[io.params.input_points]]
topic = "trust/examples/mqtt/in/di0"
image_offset = 0
image_bit = 0
data_type = "bool"
payload_format = "json"
[[io.params.output_points]]
topic = "trust/examples/mqtt/out/do0"
image_offset = 0
image_bit = 0
data_type = "bool"
payload_format = "json"
Point maps also support u16, i16, u32, i32, and f32 values with
text, json, binary_le, or binary_be payloads and optional
scale/offset.
To map MQTT topics directly to named program-instance variables, use
mappings. The direction is relative to the PLC, so write publishes a PLC
value and read subscribes and writes the received value into the PLC:
[[io.params.mappings]]
tag = "MainInstance.Green"
topic = "traffic/north/green"
direction = "write"
Mappings are resolved when the runtime starts. An unknown tag, unsupported
non-scalar type, invalid direction, or unknown MQTT parameter rejects startup
before the MQTT worker starts. See ../mqtt_traffic_light for a complete
project based on the traffic-light tutorial.
For Sparkplug B outbound node metrics, add a Sparkplug profile and stable metric names to typed output points:
[io.params.sparkplug]
enabled = true
namespace = "spBv1.0"
spec_version = "3.0.0"
group_id = "trust-examples"
edge_node_id = "mqtt-runtime"
[[io.params.output_points]]
topic = "trust/examples/mqtt/out/do0"
metric_name = "do0"
image_offset = 0
image_bit = 0
data_type = "bool"
payload_format = "json"
This publishes NBIRTH on connect, NDEATH as the MQTT last will, and NDATA for typed output metrics. It does not subscribe to Sparkplug commands or publish device-level DBIRTH/DDATA topics.
Step 3: Validate config¶
Why: detect missing mandatory fields and invalid values early.
trust-runtime validate --project .
Step 4: Run against a broker¶
Why: topic contract validation must happen with a real broker path.
- Start broker on
127.0.0.1:1883. - Run runtime:
trust-runtime run --project .
- In another terminal, inspect I/O:
trust-runtime ctl --project . io-read
Step 5: Production hardening checklist¶
Why: MQTT deployments often fail at boundary assumptions, not syntax.
- enforce broker auth/TLS according to site policy
- keep input/output topics separate and explicit
- define ACLs for publish/subscribe directions
- confirm reconnect behavior under broker restart tests
Common mistakes¶
- reusing same topic for both
topic_inandtopic_out - leaving broker ACLs open during rollout
- treating successful
validateas proof of runtime connectivity - dropping
safe_statedefinitions for output-critical projects
Common MQTT gotchas¶
- using the same topic for
topic_inandtopic_out - unclear ACL direction between publish and subscribe
- mixing raw byte topics and typed point topics without documenting which profile owns the process image
- enabling Sparkplug without stable
group_id,edge_node_id, andmetric_namevalues - using binary MQTT payload endianness as if it changed
%IW/%IDprocess-image byte order - testing only against a local open broker and skipping production auth/TLS
- forgetting safe-state behavior for output-critical projects