Skip to content

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 -> %QX logic (DO0 := DI0)
  • src/config.st: task binding plus VAR_CONFIG mapping (P1.DI0/P1.DO0)
  • io.toml: MQTT backend profile
  • runtime.toml: runtime defaults
  • trust-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 %I image.
  • topic_out: messages published from %Q image.
  • 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.

  1. Start broker on 127.0.0.1:1883.
  2. Run runtime:
trust-runtime run --project .
  1. 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_in and topic_out
  • leaving broker ACLs open during rollout
  • treating successful validate as proof of runtime connectivity
  • dropping safe_state definitions for output-critical projects

Common MQTT gotchas

  • using the same topic for topic_in and topic_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, and metric_name values
  • using binary MQTT payload endianness as if it changed %IW/%ID process-image byte order
  • testing only against a local open broker and skipping production auth/TLS
  • forgetting safe-state behavior for output-critical projects