Publishing
Two blocks. MQTT_REPORT for a value that changes, MQTT_PUBLISH for an
event that happens.
A value that changes
level( CLIENT := net,
TOPIC := 'plant/intake/level',
VALUE := rTankLevel,
DEADBAND := 0.05,
PERIOD := T#1m,
RETAINED := TRUE );
Standing — no REQ, no edge to arrange. It watches the variable and publishes
when it is worth publishing.
The two timing pins are the point of the block, and they work together.
DEADBAND is how far the value must move before that is worth a message. A
level transmitter that jitters by 0.02 m is not telling you anything, and
publishing that four times a second for a year is thirty million messages
nobody reads. Set it to the smallest change an operator would act on.
PERIOD sends it anyway after that long, unchanged. Leaving this
at 0 is the mistake worth avoiding. A value that never moves publishes
nothing, and a consumer cannot tell that from a device that has died — it just
sees silence. Set it to a minute or two and the silence becomes meaningful,
which is exactly what lets the subscribing end run a quality on it (page 05).
Deadband is measured on the number, so it has no meaning for a STRING: there
any change at all is a change, and PERIOD still applies.
An event that happens
alarm( CLIENT := net,
REQ := bHighLevelAlarm,
TOPIC := 'plant/intake/alarm',
VALUE := iAlarmCode,
QOS := 2 );
Acted on the rising edge of REQ. Holding it true does not send
repeatedly.
BUSY |
a level: out and not yet concluded |
DONE |
one scan, when the broker has it |
ERROR |
one scan, when it did not |
DONE means different things at different QoS, and that difference is the
whole point of the pin. At QoS 0 it means the bytes left this device. At QoS 1
the broker acknowledged them. At QoS 2 the broker acknowledged them exactly
once and the four-packet handshake completed. Interlock on DONE at QoS 1 or
2 and you have a delivery receipt; at QoS 0 you have a despatch note.
Latch DONE if a slower task has to see it.
One value pin, of your own type
There is no type selector and no per-type pin. VALUE : ANY takes the
variable itself and its declared type is what travels:
| ST type | on the wire |
|---|---|
BOOL |
true / false |
INT, DINT, SINT, LINT |
42 |
UINT, WORD, BYTE, DWORD |
42 |
REAL |
23.5 — seven significant figures, which is what a float carries |
LREAL |
23.5 — fifteen |
STRING |
its text |
Text, not raw bytes, and deliberately. A dashboard, a Node-RED flow, a
Python script and mosquitto_sub all read 23.5; none of them agree on what
four raw bytes mean and half would get the byte order wrong.
The text of a STRING is published, never its IEC representation. Worth
naming because a competing platform gets this wrong and documents it: passing
a string as a raw payload there sends the four header bytes every IEC 61131
string carries, plus a trailing NUL. Anybody who has debugged that once does
not want to meet it again.
A type the wire cannot carry — a structure, an array, a TIME — is refused
with MQTT_INVALID_ARGUMENT rather than guessed at. A wrong number on a SCADA
screen is worse than no number.
Retain, and when not to
RETAINED := TRUE keeps the message on the broker, so anything subscribing
later is told immediately instead of waiting for the next change.
The pin is RETAINED rather than MQTT's own word RETAIN for one reason:
RETAIN is a reserved keyword in Structured Text — VAR RETAIN is how a
variable survives a power cycle — so a pin of that name cannot be declared.
Retain what describes a state. A level, a setpoint, a mode, an
online/offline flag. A SCADA screen opened at three in the morning shows the
plant instead of blank fields.
Do not retain an event. A retained "pump started" keeps announcing
something that happened last Tuesday to everything that connects, for ever.
Alarms, commands and counters are events.
To withdraw a retained value, publish an empty message to the same topic with
retain on. That is MQTT's delete.
Choosing a QoS
| when | |
|---|---|
| 0 | a reading published often. The next one is along shortly |
| 1 | something you would rather have twice than lose — a totaliser |
| 2 | something where twice and never are both wrong — a command |
QoS costs round trips, and QoS 2 costs four packets per message. On a cellular
link that is real money. Publish readings at 0 and commands at 2.
A whole STRUCT, one block
pubAll( CLIENT := mqtt, BASE := 'plant/intake', DATA := stIntake,
MODE := MQTT_ON_CHANGE, PERIOD := T#1m, RETAINED := TRUE );
Every member of stIntake becomes its own topic under BASE, published only
when it moves:
TYPE S_INTAKE : STRUCT plant/intake/LEVEL
LEVEL : REAL; plant/intake/FLOW
FLOW : REAL; plant/intake/RUNNING
RUNNING : BOOL; plant/intake/TAG
TAG : STRING(16);
END_STRUCT END_TYPE
Fifty setpoints become one block. In ST you could loop; in ladder there is
no FOR, so the alternative is fifty rungs.
The member name IS the topic, so the STRUCT declaration is the topic
scheme. Rename a member and you rename a topic — worth knowing before the
third device is commissioned.
A member the wire cannot carry is refused, not skipped quietly. An array, a
nested STRUCT, a TIME: counted on SKIPPED and named on STATUS. A member
silently missing from a payload is the kind of fault that survives
commissioning and turns up months later in an argument about whose data is
wrong.
There is no DEADBAND here, deliberately. One number covering fifty
members of mixed types is a trap: a BOOL counts as 1.0, so a deadband above
that would freeze every bit in the struct — including a FAULT — until the
interval rescued it, and a counter moving by one would go the same way. Where a
member needs a deadband it gets an MQTT_REPORT of its own, which is one point,
one unit and one declared type, all visible on the rung. The two blocks coexist
happily on one client.
MODE is the pin that matters here, and it is worth matching to the data:
MQTT_ON_CHANGE |
setpoints, commands, states — things that move when somebody moves them. PERIOD is then the heartbeat that keeps the far end's QUALITY good while nothing happens |
MQTT_ON_INTERVAL |
analogues that never stop moving. A wet well level changes every scan, so on-change would publish at the task rate — send the set on a clock instead |
MQTT_ON_REQUEST |
a recipe download. REQ sends the whole set, changed or not, so the receiving end gets something complete to act on rather than the three values that happened to differ |
MQTT_ON_INTERVAL with PERIOD := T#0s is refused with
MQTT_INVALID_ARGUMENT rather than turned into "every scan" — fifty topics at
the task rate is how a broker falls over.
One instance, many topics
A block holds no slot index. Its state is keyed by the topic it was given, so
one instance called several times in a scan is several publishes:
report( CLIENT := net, TOPIC := 'plant/intake/level', VALUE := rLevel );
report( CLIENT := net, TOPIC := 'plant/intake/flow', VALUE := rFlow );
report( CLIENT := net, TOPIC := 'plant/intake/pressure', VALUE := rPressure );
Three reports, each with its own last value and its own deadband, on one
block's memory. Called more than once, the block's outputs describe the last
call.
Next: 05 Subscribing