---
title: "Patterns"
space: "OpenPLC"
url: "https://docs.modbee.org/openplc/mqtt-and-sparkplug-b-blocks/08-patterns"
updated: "2026-10-25"
---

# Patterns

Worked shapes, and the habits that make them work.

## The two-task split

The one thing worth doing on every project.

```
NET_TASK  T#50ms  prio 20   PROGRAM netrun  ->  net( ... ); and every block given net
APP_TASK  T#20ms  prio 0    PROGRAM main    ->  the machine, no communication blocks
```

The two trade values through globals, each written by one task. Page 10 has the
numbers; the short version is that the node only notices things when it is
called, so its interval is the floor on how quickly anything happens - and a
block given the node from the machine's task refuses with `MQTT_WRONG_TASK`.
On the Runtime the network task can run at `T#1ms`; on a board, not faster than
`T#50ms`.

## A device that reports itself

The shape most PLCs want. Five blocks.

```
net     ( TRANSPORT := MQTT_RUNTIME_TCP, HOST := BROKER,
          CLIENT_ID := 'plc-intake' );
presence( CLIENT := net, TOPIC := 'plant/intake/status',
          RETAINED := TRUE, DELAY := T#30s );

report  ( CLIENT := net, TOPIC := 'plant/intake/level',
          VALUE := rLevel, DEADBAND := 0.05, PERIOD := T#1m,
          RETAINED := TRUE );
report  ( CLIENT := net, TOPIC := 'plant/intake/flow',
          VALUE := rFlow,  DEADBAND := 0.5,  PERIOD := T#1m,
          RETAINED := TRUE );

status  ( CLIENT := net );
```

One `report` instance, called twice. Each call keeps its own last value and
its own deadband.

## Commanding, and refusing to act on stale data

The commanding end owns the topic and publishes it. The commanded end
subscribes — and checks the quality before doing anything.

```
(* on scada *)
cmd( CLIENT := net, REQ := bSendSetpoint,
     TOPIC := 'plant/intake/cmd/level-setpoint',
     VALUE := rOperatorSetpoint, QOS := 2, RETAINED := TRUE );

(* on plc-intake *)
sp( CLIENT := net, TOPIC := 'plant/intake/cmd/level-setpoint',
    VALUE := rSetpoint, QOS := 2 );

IF sp.QUALITY >= MQTT_QUALITY_UNCERTAIN THEN
  rWorkingSetpoint := rSetpoint;
ELSE
  rWorkingSetpoint := rSafeDefault;
END_IF
```

`RETAINED := TRUE` on the command is what lets the intake PLC pick up the
current setpoint immediately after a restart, rather than running on a default
until the operator happens to change something.

QoS 2 because a setpoint applied twice is usually harmless but a setpoint lost
is not, and the cost is four packets on a value that changes rarely.

## An alarm that is not a retained ghost

```
alarm( CLIENT := net, REQ := bAlarmRising,
       TOPIC := 'plant/intake/alarm',
       VALUE := iAlarmCode, QOS := 2, RETAINED := FALSE );
```

`RETAINED := FALSE` on an event. A retained alarm keeps announcing itself to
everything that connects, for ever, long after the cause is gone.

If you want a dashboard to show the *current* alarm state, publish that as a
separate retained topic holding the state — `plant/intake/alarm-active` as a
`BOOL` — and keep the event topic for events.

## A site that survives its uplink

The PLC is the broker; a bridge carries a subset upstream.

```
broker( ENABLE := TRUE, PORT := 1883, MAX_CLIENTS := 32 );

bridge( ENABLE := TRUE,
        LOCAL_HOST := '127.0.0.1',
        REMOTE_HOST := 'mqtt.example.com',
        REMOTE_SECURITY := MQTT_TLS_VERIFY,
        REMOTE_USER := 'site7', REMOTE_PASSWORD := sPassword,
        BRIDGE_NAME := 'site7',
        TOPIC := 'plant/#', DIRECTION := MQTT_OUT, QOS := 1,
        REMOTE_PREFIX := 'sites/site7/' );

bridge( ENABLE := TRUE, TOPIC := 'cmd/#', DIRECTION := MQTT_IN, QOS := 2,
        REMOTE_PREFIX := 'sites/site7/' );
```

Twenty sensors connect to the PLC, not to head office. The site keeps working
when the uplink does not, and catches up when it returns. One instance of
`bridge`, called twice, is two rules on one pair of connections.

## Drawing these blocks in ladder

Everything on these pages is written in ST because it is shorter to read. In
ladder the same call is a contact, the block, and a variable box on every pin —
and one detail decides how the rung is drawn.

**Twelve of the fifteen blocks take rung power on `EN` and hand it back on
`ENO`.** Every pin is then named, including `ENABLE`:

```
sub( EN := always, CLIENT := net, TOPIC := 'plant/pump-01/cmd/SP',
     VALUE := setpoint, ENABLE := always );
```

**Three do not**, because their first input and their first output are both
`BOOL`, which is how the editor draws a `TON`:

| block | power in | power out |
| --- | --- | --- |
| `MQTT_BROKER` | `ENABLE` | `LISTENING` |
| `MQTT_PUBLISH` | `REQ` | `BUSY` |
| `MQTT_REQUEST` | `REQ` | `BUSY` |

For those three the contact **is** the pin, and the pin on the way out carries
on to the next element:

```
srv( ENABLE := always, LISTEN_ON := '', PORT := 1883, MAX_CLIENTS := 16 );
listening := srv.LISTENING;
```

which is the rung `[always]───[MQTT_BROKER]───(listening)`. It reads the way a
start/stop rung reads, and it is the reason `REQ` is the first pin on
`MQTT_PUBLISH`: a publish request belongs on the rung's own power.

**Name every other pin.** A library block writes only the pins the rung names —
one left out is not defaulted, it is never written at all, so a `QOS` you meant
to set to 1 stays 0 and nothing says so.

## Habits worth keeping

**Give every device its own `CLIENT_ID`.** Two sharing one will knock each
other off for ever, each looking healthy on its own. `MQTT_SESSION_TAKEN_OVER`
is what that looks like from the inside.

**Decide a topic scheme before the second device.** Nothing enforces one, and
retrofitting is a site visit. `site/area/device/measurement` works.

**Always set `PERIOD` on a report.** Without it a value that never
changes is indistinguishable from a device that has died.

**Retain state, never events.**

**Check `QUALITY` before acting on a subscribed number**, and decide what
happens when it is not good. "Hold the last value" and "fail to a safe
default" are both reasonable; "act on it anyway" is not.

**Put the node in its own fast task.**

**Turn `AUTO_RECONNECT` off on cellular**, and reconnect from your own logic
where you can afford the stall.

## What bites people

**A retained value that is older than the device.** A broker will serve a
retained message from something that died last Tuesday and nothing marks it as
old. Pair every publisher with `MQTT_PRESENCE` and subscribe to the status
topic, or you are trusting a ghost.

**`DEADBAND` without `PERIOD`.** Covered above, and the most common
one.

**Publishing every scan.** MQTT has no rate limit and a broker will accept
everything you send it until it falls over. Use `MQTT_REPORT`, not
`MQTT_PUBLISH` on a timer.

**Wildcards in a publish topic.** `+` and `#` are for subscribing. The block
refuses them with `MQTT_INVALID_TOPIC`.

**Expecting request-and-response.** MQTT is a noticeboard. `MQTT_REQUEST`
exists and works at 5.0, but if most of your traffic is questions and answers
you have picked the wrong protocol — use Modbus.

**Assuming QoS 2 means fast.** It means exactly once. It is four packets and
two round trips, and on a cellular link that is noticeable.

**Next:** [09 Troubleshooting](/openplc/mqtt-and-sparkplug-b-blocks/09-troubleshooting)
