MQTT and Sparkplug B

MQTT and Sparkplug B

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

ModBee MQTT

ModBee MQTT

MQTT 5.0 and 3.1.1 in C++14: a client, a broker and a bridge. It runs on a
microcontroller and on a Linux server, and nothing in it ever waits on the
network.

It was written for industrial control. There, a network call that blocks is a
scan cycle that overran, and a message that vanishes quietly is a pump that did
not start. Those two constraints shaped everything else in it.

 library:  esp32 · esp8266 · mbed (Portenta, Opta, Nano 33) · rp2040 · stm32 · Linux · macOS
 server:   Linux on x86-64 and ARM64, as a Docker image

What is in it

The library: for a board, or for your own program

src/, included with #include <ModBeeMqtt.h>. It has no dependencies, and is
the same source on every platform.

MqttDevice The short form: one object, per-topic handlers, an online/offline status topic, a variable published when it changes, and a staleness check
Client QoS 0/1/2, sessions, retained messages, wills, keepalive, reconnect with backoff, credentials, topic aliases, flow control, and MQTT 5 properties both ways, including request/response
Broker Sessions, retained messages, wills, shared subscriptions, an authorisation hook, and TLS from a certificate it can generate for itself. It runs on a board as well as a host
Bridge Two brokers and a topic mapping, with loop prevention
Typed values Bind a variable, publish it on change, and read it back with a quality that goes stale on schedule
Sparkplug B An edge node and a host application (the SCADA end), conformant to Sparkplug 3.0 and tested with the Eclipse TCK
Transports TCP and TLS on a host. On a board, any Arduino Client: WiFi, Ethernet, or cellular through TinyGSM, with TLS on all three

Nothing is behind a build flag. A part your program never uses is code nothing
references, and the linker drops it; footprint.md measures
that.

TLS uses whatever is there: wolfSSL or OpenSSL on a host; on an ESP32 the
core's own mbedTLS over any channel (MqttChannelTlsTransport - the same checks
on Wi-Fi, Ethernet and a modem, with key pinning); and WiFiClientSecure or
SSLClient on other boards.

The server: the broker at scale

server/, cli/ and docker/. Linux only, and not part of the Arduino
library.

modbee serve The broker as a server: one broker per CPU core, behind one port. Configured with one modbee.yaml
Kafka pipe MQTT to Apache Kafka and back, with messages passed through untouched. A publisher's PUBACK comes back only once Kafka has the message
Device logins A login per device, added, reset and removed while it runs - or devices that register themselves with a provisioning key for their site - with per-device topic permissions (strict, or open to every topic for a login without rules: authentication.default-topics)
Retained messages that survive a restart Kept in memory as always, and in a file or a compacted Kafka topic as well, with one setting
modbee command Publish, subscribe, request/response and benchmark from a terminal
Docker image ghcr.io/mod-bee/modbee-mqtt: the server and the modbee command, for amd64 and arm64

Quick start

On a board

Put this repository in your Arduino libraries/ folder, or use Sketch →
Include Library → Add .ZIP Library
. This is a complete device: it announces
itself, registers a Will so its absence is noticed, and reconnects by itself.
It publishes the level only when it changes, and holds what you send while
the link is down.

#include <Ethernet.h>            // or <WiFi.h>, or <TinyGsmClient.h>
#include <ModBeeMqtt.h>

byte MAC[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x01 };
const uint8_t PUMP = 5;
float tankLevel = 0.0f;

EthernetClient net;
MqttDevice mqtt(net);

void onCommand(const char*, const MqttPayload& payload) {
    digitalWrite(PUMP, payload.toBool() ? HIGH : LOW);
}

void setup() {
    Ethernet.begin(MAC);

    mqtt.statusTopic("plant/tank-01/status");       // online / offline
    mqtt.onMessage("plant/tank-01/pump", &onCommand);

    // Publish the level when it moves 20 mm, and at least every 30 seconds.
    mqtt.report("plant/tank-01/level", &tankLevel, 0.02, 30000);

    mqtt.connect("192.168.1.10", "tank-01");        // returns immediately
}

void loop() {
    mqtt.poll();                                    // the whole contract
    tankLevel = analogRead(A0) * (10.0f / 1023.0f);
}

Only the network lines differ between links:

WiFiClient net;             MqttDevice mqtt(net);   // WiFi
EthernetClient net;         MqttDevice mqtt(net);   // wired
TinyGsmClient net(modem);   MqttDevice mqtt(net);   // cellular
MqttChannelTlsTransport<EthernetClient> tls(net, "broker", 8883);
                            MqttDevice mqtt(tls);   // TLS on any channel, ESP32
WiFiClientSecure net;       MqttDevice mqtt(net);   // TLS, ESP8266
SSLClient net(wired, TAs, TAs_NUM, A5);             // TLS where the chip has none

New to MQTT? Getting started explains brokers,
topics and QoS before any code.

In your own C++ program

add_subdirectory(modbee-mqtt)
target_link_libraries(my-app PRIVATE modbee-mqtt)

The same MqttDevice on a host needs no network object:

MqttDevice mqtt;
mqtt.secure("/etc/ssl/certs/ca-certificates.crt");
mqtt.connect("broker.example.com", "gateway-1");

A broker in your own program:

MqttBrokerConfig config;
config.maxConnections = 64;

MqttTcpListener plain;
plain.configure(nullptr, 1883, config.maxConnections);   // address, port, slots
if (!plain.begin()) { /* plain.lastError() says why */ }

MqttBroker broker;
broker.begin(config);                      // allocates everything, once
broker.addListener(plain);

while (running) broker.poll();             // never waits

As a server, in Docker

docker run -d --name modbee -p 1883:1883 --restart unless-stopped ghcr.io/mod-bee/modbee-mqtt

That is an MQTT broker on port 1883. For a server in production,
docker/examples/ has one ready-to-run folder per case,
each with a README:

Your server Without Kafka With Kafka
Your own network (LAN, VPN) broker broker-kafka
On the internet - Traefik gets the certificate traefik traefik-kafka
On the internet - Nginx Proxy Manager already runs npm npm-kafka

Each needs, at most, a domain name, an email address and passwords, all in one
.env file. You never handle a certificate file. To try the Kafka pipe on
this computer first: docker/compose.yml.

From a terminal

The modbee command is in the Docker image:

docker run --rm ghcr.io/mod-bee/modbee-mqtt pub -h broker.example.com -t test/hello -m hi -q 1
#1 test/hello: delivered to the broker

It is also built with this repository on Linux and macOS. It is not part of
the Arduino library. cli.md covers both.


How it behaves

  • Nothing blocks, including connect(). You call poll(), and outcomes
    arrive there. That is what lets it live inside a scan cycle or an Arduino
    loop(). The one call that can stall is the Arduino core's own
    Client::connect(), and transports.md says what to do
    about it.
  • Nothing is silently dropped. Every operation ends in exactly one stated
    outcome: sent, refused, superseded or failed. A publish that will not fit is
    refused with a reason before it is accepted. A broker whose subscribers are
    backed up answers 0x97 Quota Exceeded, not a PUBACK that claims success.
  • One core, every role. The client, the broker and the bridge share the
    same framing, codecs, sessions and flow control. A plant panel can run its
    own broker, keep working when the site link drops, and bridge the topics
    head office needs.
  • One source, every platform. The same files build for an ESP32 and for a
    Linux container. Nothing above the transport layer knows which platform it
    is on.
  • The source says why. Constants, lengths and reason codes cite the clause
    of the MQTT standard that requires them.

The codebase

src/ The library, and the only folder the Arduino IDE compiles. ModBeeMqtt.h is the single include
src/broker/, bridge/, sparkplug/, typed/ The broker, the bridge, Sparkplug B, and typed values
src/codec/, src/transport/ The MQTT 3.1.1 and 5.0 codecs, and TCP, TLS, the Arduino Client adapter and the listeners
examples/arduino/ Fifteen sketches, each a real application
examples/cpp/ Publish, subscribe, a gateway, a broker, a bridge, a broker serving TLS, and a Sparkplug node and host
server/ The server tier: one broker per core (ShardedBroker), modbee.yaml, the Kafka pipe and metrics. Linux
cli/ The modbee command
docker/ The image (docker/Dockerfile) and the compose files
docs/ Everything below

The library has five layers, and dependencies point downward only:

 L4  ABOVE     MqttDevice, typed values, Sparkplug B    conveniences over the raw API
 L3  ROLE      MqttClient, MqttBroker, MqttBridge       the only layers that know which end they are
 L2  CORE      MqttLink, MqttOperations, MqttSession    framing, the operation queue, what survives a connection
 L1  PROTOCOL  MqttWire, the codecs, MqttProperties     functions over buffers: no I/O, no clock, no allocation
 L0  LINK      MqttTransport, MqttListener              one ordered, reliable byte stream

Two rules are checked in CI: no layer below L3 knows which role it is playing,
and no layer above the platform layer knows which platform it is on.
ARCHITECTURE.md explains why each boundary is where it
is.


Building

cmake -S . -B build
cmake --build build -j
ctest --test-dir build

That builds the library, the examples and the tests, and, on a host, the
modbee command (build/cli/modbee) and the server. TLS is used if wolfSSL or
OpenSSL is installed. The Kafka pipe is built if the librdkafka development
package (librdkafka-dev) is installed. Neither is required.

When another CMake project adds this one with add_subdirectory(), it gets the
library alone.

C++14 is all the library needs, because the mbed-based Arduino cores and
OpenPLC's STruC++ compile at C++14. It builds clean with
-fno-exceptions -fno-rtti -Werror.


Examples

examples/arduino/:

First_Sketch The shortest thing that works
Tank_Level_Ethernet A level on a dashboard, over a wired link
Publish_On_Change A variable published when it moves, and not otherwise
Pump_Confirm_Stop A command that must be confirmed, read back from the contactor
Remote_Site_Cellular An outstation on LTE-M, reconnecting on its own terms
Alarm_On_Silence A panel that notices when a device stops speaking
Request_And_Reply Ask a device something, and get an answer back
Subscribe_With_Quality A reading that goes stale on schedule rather than going quiet
Version_Fallback MQTT 5.0 where it is offered, 3.1.1 where it is not
TLS_Client An encrypted connection from a board with a secure client
TLS_Over_Ethernet TLS on a board whose network chip has none
Panel_Broker The board is the broker, on port 1883
Secure_Broker The same, serving TLS on 8883 from a certificate it made itself
Sparkplug_Edge_Node A Sparkplug B edge node that SCADA finds by itself, with a setpoint SCADA can write
Provisioned_Device A device that registers itself with the broker and keeps its own login - nobody types a password into it

examples/cpp/: publish, subscribe, gateway, broker, bridge,
secure_broker, and sparkplug_node with sparkplug_host, which talk to each
other.

examples/kafka/nodejs/: a Node.js app on the Kafka pipe - it reads every MQTT
message from Kafka and sends a pump command back
(kafka-apps.md).


What it is not

  • Not MQTT-SN. That is a separate protocol for links without TCP; run an
    MQTT-SN gateway in front of a broker (versions.md).
  • Not a WiFi manager or an OTA updater. It does MQTT over the network
    connection you already have.
  • Not a multi-machine cluster. modbee serve uses every core of one
    machine. Kafka is how several servers share data.
  • No streaming publish. A payload is a buffer. A streamed write could block,
    and a QoS 1 message could not be resent after a reconnect
    (api.md).

Documentation

getting-started.md New to MQTT? Start here. Brokers, topics and QoS, then a working device
recipes.md Five real jobs, done properly
transports.md WiFi, Ethernet, cellular, and TLS on all three
api.md The reference: everything public
broker.md A broker in your own program
bridge.md Two brokers and a mapping
typed-values.md Bind a variable; read it back with a quality
sparkplug.md Sparkplug B: an edge node, a host application, and what each does for you
versions.md MQTT 3.1.1 and 5.0: what differs
security.md TLS, and what "it connected" does and does not prove
footprint.md Flash and RAM, measured
docker.md Running the server: one command, a stack with a certificate, or that stack with Kafka
kafka.md modbee.yaml and the Kafka pipe
logins.md Device logins: a login per device, devices that register themselves, permissions, backups
settings.md The server's settings, changed over MQTT and applied at a restart
events.md What clients do - connect, go, subscribe - as messages, and in Kafka
kafka-apps.md Your own app on Kafka - reading device data and sending commands, from Node.js, Python or anything else
cli.md The modbee command
ARCHITECTURE.md The five layers, and why
extending.md Building on it: a PLC wrapper, a new transport, and the design rules

Licence

GNU Affero General Public License, Version 3, with additional ModBee terms
(see LICENSE). Use is non-commercial; commercial use requires a
separate licence
, and the LICENSE says how to ask for one.

To report a security defect, see SECURITY.md.

Last updated 1 hour ago
Was this helpful?
Thanks!