last updated: 2023-10-23
Song of this chapter: Rogue Wave > Descended Like Vultures > Publish My Love
MQTT
(http://mqtt.org/) is a lightweight publish-subscribe messaging protocol designed to be used with IoT devices (wiki).
MQTT
stands for Message Queue Telemetry Transport and one of the main development objectives was to communicate from machine-to-machine (M2M
) but also connect M2M
to clouds (Big Data).
As HTTP, MQTT runs on top of Transmission Control Protocol / Internet Protocol (TCP/IP) stack. The most recent implementation is MQTT v5.0 (official OASIS standard), based on the earlier v3.1.1 standard. Our software sill uses the v3.1.1 standard, so we stick to v3.1.1.
Like e.g. ROS
(Robot Operating System) MQTT
uses a publish-subscribe pattern (pub-sub pattern). This is a messaging pattern where messages are not sent directly from transmitter to receiver (point to point), but are distributed by an MQTT server
(formerly called MQTT broker).
The MQTT
server is the centrepiece of an pub-sub architecture. It can be implemented very simply on an single board computer like the Raspberry Pi or a NAS
but naturally also on mainframes or internet server. As the server distributes messages, the server has to be a publisher, but is never a subscriber!
Clients can publish messages (sender), subscribe messages (receiver) or both. A client (also called node) is an intelligent device like a microcontroller, or computer with a TCP/IP stack and software implementing the MQTT
protocol.
A publishing client (called publisher) is only loosely coupled to a subscribing client. It does not even know if there are other clients. Same for a receiving client (subscriber) that is interested in a specific types of messages and will subscribe to these messages.
The messages are published under a topic allowing filtering. Topics are hierarchical divided UTF-8 strings. The different topic levels are separated by a slash /
.
Let's take a look at the following setup. The photovoltaic power plant is a publisher. The main topic level is "PV"
. The plant publishes two sub-levels "sunshine"
and "data"
.
"PV/sunshine"
is a boolean value (yes/no, could also be 1/0) and is needed by the charging station to know if the electric vehicle should be loaded (only when the sun shines :)). The charging station (EVSE
) is a subscriber and subscribes to "PV/sunshine"
to get the information from the server.
"PV/data"
on the other hand transports the momentary power generated by the plant in kW and that topic could e.g. be subscribed by computers or tablets to generate diagrams of the delivered power over a day.
MQTT.fx
is a very helpful Java software implementing an MQTT client and written by Jens Deters. It helps to understand the MQTT protocol and mainly test MQTT setups. Install MQTT.fx. We will use a free server from hivemq.com
. This MQTT server from hivemq.com (broker.hivemq.com
, standard port for MQTT server: 1883) is in the Internet and can be used freely by everyone. Send the message "yes"
under the topic "PV/sunshine"
and receive the same message. Document with two screenshots.MQTT
software is MQTT-Explorer
. It has the supplementary features to plot numeric topics (time diagrams) or to retain a history of each topic. Use the school MQTT-Server (192.168.128.82) and send 4 numeric values and plot them. Document with a screenshot.The topic is are hierarchical divided UTF-8 string. The different topic levels are separated by a slash /
, the topic level separator. There is no need to inform the broker over the topics, every syntactically right string is valid. Some examples of topics:
myhome/firstFloor/Kitchen/temperature
myhome/firstFloor/Kitchen/humidity
Luxembourg/Luxembourg/Jofferchersgaass/4/heating/onOff
6133/49609/btsiot/sc4
The first topic has 4 levels. Topics are case-sensitive, so firstfloor is not the same as firstFloor. Empty spaces are permitted but not very useful as such strings are prone to errors. Each topic must contain at least 1 character, but the forward slash alone is a valid topic.
Topics should not begin with $
. The $-symbol topics are reserved for internal statistics of the MQTT broker, even if there is no official standardization. Commonly $SYS/
is used for all the following information as explained in the MQTT GitHub wiki.
It is important to maintain consistency in topic names and we should be aware that we can subscribe to multiple topics by using topic filters. Therefore, it is very important to choose the right topic names for a project.
To filter topics when subscribing, two wildcards can be used, the single-level wildcard: +
or the multi-level wildcard: #
.
+
For all temperatures on the first floor of myhome we could use:
myhome/firstFloor/+/temperature
For all temperatures in myhome we could:
myhome/+/+/temperature
#
The the multi-level wildcard must be placed as the last character in the topic and preceded by a forward slash.
For all sensors placed in the Kitchen (first floor) of myhome we could use:
myhome/firstFloor/Kitchen/#
And for all sensors from the first floor:
myhome/firstFloor/#
The quality of service is an important feature of MQTT. As we use TCP/IP the connections are already secured at a certain level. But in wireless networks interruptions and disturbances are frequent and MQTT helps here to avoid the loss of information with its quality of service levels. These levels are used when publishing. If a client publishes to the MQTT server, the client will be the sender and the MQTT server the receiver. When the MQTT server publishes messages the client, the server is the sender and the client is the receiver.
This is the normal quality of TCP/IP. There is no acknowledgement from the server and the message is not stored.
QoS 1 promises that the message will be delivered at least once to the subscriber. The message gets a message Id. The sender stores the message until it receives an acknowledgement (PUBACK
) from the subscriber. The first time the sender sets the duplicate flag to 0
(DUP = 0
). QoS 1 tells the destination device (receiver) that a confirmation requirement is necessary. If the message was received correctly the receiver sends a PUBACK
(acknowledgement) with the right message Id. However if a certain time is exceeded without confirmation, the message is sent again, this time with the duplicate flag set to 1
(DUP = 1
). This can happen more than once to the same destination device (the message is delivered multiple times). This destination device must have the necessary logic to detect duplicates and react likewise, e.g if the receiver is an MQTT server, it sends the message to all clients subscribing the topic and then replies with PUBACK
.
With QoS 2 we have a warranty that the message is delivered only once to the destination. For this the message with its unique message Id is stored twice, first from the sender and later by the receiver. QoS level 2 has the highest overhead in the network because two flows between the sender and the receiver are needed. After the first sending (DUP = 0
) the sender repeats the sending ((DUP = 1
) until it receives a PUBREC
that tells, that the message was stored by the receiver. With the second transaction the sender tells the receiver to stop the transmission with a PUBREL
(release), to clear the buffer used for the storage and send a PUBCOMP
(complete). A published QoS 2 message is successfully delivered after the sender is sure that it has been successfully received once by the destination device.
More infos can be found here: https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels/
Using free server in the Internet is simple but also a security risk that is not worth to take if we want to use MQTT e.g. locally in buildings. We will activate a server on a Raspberry Pi (Raspi) single board computer.
We burn an image with RaspiOS on an SD card with the Raspberry Pi Imager
. After starting the OS we enable ssh and VNC , change the password and do an upgrade. Finally we give our Raspi a static IP address, so we can reach it. All this was perhaps done in the previous chapter, if the chapters are worked through in order. For more info look here: Using the Raspberry Pi (Raspi).
We will use an an open source (EPL/EDL licensed) message server from Eclipse. The server is called Mosquitto (https://mosquitto.org/) and implements the MQTT protocol versions 5.0, 3.1.1 and 3.1. Mosquitto is lightweight and so can be used on all devices from low power single board computers to full servers.
The installation on a Raspi is done with one command:
sudo apt install mosquitto
For security reasons the server is not accessible for every one. We have to change some settings. The standard file to do this is the mosquitto.conf
file, but we will use our own file in /etc/mosquitto/conf.d
. Create the file with:
cd /etc/mosquitto/conf.d
sudo nano mymosqui.conf
Add the following two lines to mymosqui.conf
:
listener 1883
allow_anonymous true
Restart the service with:
sudo service mosquitto restart
Test the status of your server and look if the mosquitto server is listening on the default port 1883 with the following commands. Document and comment the outputs:
sudo service mosquitto status
netstat -an | grep 1883
Add a new profile to MQTT.fx with your own server. Send the message "no"
under the topic "PV/sunshine"
and receive the same message. Document the profiles screen (gear) and the subscribe screen (MQTT.fx) with two screenshots .
We want to publish messages and subscribe to messages on the same Raspi where the server runs. For this we install a client program on the Raspi:
sudo apt install mosquitto-clients
Start the subscribe client mosquitto_sub
with:
mosquitto_sub -V mqttv311 -t PV/sunshine -d
The Raspi mosquitto server and MQTT.fx use MQTT v3.1.1. So we use the -V
option to specify the version. The -t
option is needed to specify the topic to which we subscribe, and the -d
option stands for debug an gives an verbose output with debug informations.
MQTT.fx
and the second client is the Raspi
(mosquitto_sub). Document the two outputs.Start the publishing client mosquitto_pub
with:
mosquitto_pub -V mqttv311 -t PV/sunshine -m "yes" -d
The new -m
option is needed to specify the message.
The MQTT protocol is flexible, trustworthy and efficient. MQTT messages combine a fixed header (all packets), with variable headers (some packets) and payload (some packets).
The fixed header has 2 byte. The first byte contains the message type and 3 flags (DUP
, QoS
and RETAIN
). The flags are only used in the PUBLISH
control packet (for MQTT 3.1.1). The second byte contains the message length (0-127). This is the length of the variable headers and the payload. With none of these the length byte is 0.
If bit 7 of this byte is set, one to three more bytes can be used to store the message length, witch gives a maximum length of 268435455 byte (0xFF, 0xFF, 0xFF, 0x7F). So the overall header can have from 2 to 5 bytes.
message type (name) | value | description | direction |
---|---|---|---|
reserved | 0 | reserved | -- |
CONNECT |
1 | client request to connect to server | client ⟶ server |
CONNACK |
2 | connect acknowledgment | client ⟵ server |
PUBLISH |
3 | publish message | client ⟷ server |
PUBACK |
4 | publish acknowledge | client ⟷ server |
PUBREC |
5 | publish received (assured delivery part 1) | client ⟷ server |
PUBREL |
6 | publish released (assured delivery part 2) | client ⟷ server |
PUBCOMP |
7 | publish complete (assured delivery part 3) | client ⟷ server |
SUBSCRIBE |
8 | client request to subscribe to topic | client ⟶ server |
SUBACK |
9 | subscribe acknowledgment | client ⟵ server |
UNSUBSCRIBE |
10 | client request to unsubscribe from topic | client ⟶ server |
UNSUBACK |
11 | unsubscribe acknowledgment | client ⟵ server |
PINGREQ |
12 | client requests PING | client ⟶ server |
PINGRESP |
13 | PING response | client ⟵ server |
DISCONNECT |
14 | client is disconnecting | client ⟶ server |
reserved | 15 | reserved | -- |
With a variable content in the message (topics, client IDs, last will and testament ...) we need a variable header to transport this information. With e.g. 2 topics, we need 2 variable headers. The 2 first bytes contain the length, the following bytes the information. If QoS 1 or QoS 2 are used a packet identifier (consecutive number), is appended.
The payload length is the message length minus the variable headers length. As the protocol uses a binary transmission, the payload can transport any data not only ASCII.
With 14 messages we have the all possibilities to login, logout, publish, subscribe and surveillance. Often the client initiates the communication. The server answers with acknowledge messages.
task | client | server |
---|---|---|
login/logout | CONNECT, DISCONNECT | CONNACK |
publish | PUBLISH, PUBACK, PUBREC, PUBREL, PUBCOMP | PUBLISH, PUBACK, PUBREC, PUBREL, PUBCOMP |
subscribe | SUBSCRIBE, UNSUBSCRIBE | SUBACK, UNSUBACK |
surveillance | PINGREQ | PINGRESP |
The MQTT server is responsible for all connections. It has to authenticate and authorize the MQTT clients. If a client wants to establish a connection with the MQTT server, it must send a CONNECT
control packet with a payload that includes all the necessary information to the server. The MQTT server will check the packet. After performing authentication and authorization it sends a CONNACK
control packet to the client to signalise the successful connection.
If the client sends an invalid CONNECT
control packet, the server automatically closes the connection. The server will keep a successful connection open until the client sends a DISCONNECT
control packet to the server or the connection is lost.
The CONNECT
control packet must include the following fields or flags in the payload
:
ClientId
:
The client identifier, is a unique string that identifies the MQTT client. If ClientId has an empty value, the MQTT server generates a unique ClientId
.
Tip: Always change your ClienID in your projects! A simple way is to append something that changes, like a timestamp.
CleanSession
:
This boolean flag specifies what happens if the client reconnects. If CleanSession
is true (1), the session will only last as long as the network connection is alive. After this all information about the session is discarded. A new reconnect will be a new clean session and the MQTT server will not use any data from the previous session. If CleanSession
is false (0) the session is persistent, meaning all subscriptions are stored and available after a reconnect, so the client will receive all the messages that were not transmitted after the loss of the connection.
KeepAlive
:
Is a time interval in seconds. If the time interval is not zero the MQTT client sends control packets to the server within the time specified for KeepAlive
. If the client has no control packets, it must send a PINGREQ
control packet to the server, to tell the server that the client connection is alive. The MQTT server responds with a PINGRESP
response to the MQTT client, to confirm that the connection is still alive. If there are no control packets the connection closes.
The following fields are optional in the CONNECT control packet:
UserName
:
If authentication authorization is requested the "UserName flag" must be set to 1 and the "UserName field" must contain a value.
Password
:
For authentication and authorization we need also to set the "Password flag" (1) and specify a value for the "Password field".
ProtocolLevel
:
This value indicates the MQTT protocol version (we use MQTT 3.1.1).
Will
, WillTopic
, WillMessage
, WillQoS
and WillRetain
:
MQTT has a last will and testament feature. If the "Will flag" is set to 1 it tells the server to store a last will message associated with the session. The client must specify the values for the "WillTopic field" and "WillMessage field". The "WillQoS flag" defines the desired quality of service for the last will message and the "WillRetain flag" indicates if this message must be retained.
After a valid CONNECT
control packet the server responds with a CONNACK
control packet.
The CONNACK
packet has a header with the following fields and flags:
SessionPresent
:
This flag tells the client if there is still a session present. It mirrors and confirms the the requests of the "CleanSession flag".
ReturnCode:
The "ReturnCode" is 0 if the connection was accepted. Otherwise the "ReturnCode" indicates that the connection is refused and tells us why:
1
The MQTT server doesn't support the requested MQTT protocol version.
2
ClientId
has been rejected.
3
Network connection established but MQTT service not available.
4
User name or password values are
malformed.
5
Authorization failed.
The used packets depend on the Quality of Service (QoS) levels (see above).
The PUBLISH
packet has a header with the following fields and flags:
PacketId
:
With QoS level > 0 the packet identifier has a number value. It is needed to identify the packet and make it possible to identify the
responses related to this packet.
DUP
:
The DUP
licate flag shows that the message has been send a second time because of QoS 1 or QoS 2 (0 fir QoS 0). The receiver can use this flag to avoid duplicates.
QoS
:
Two bits are needed for 3 Quality of Service (QoS) levels (0b00 = QoS 0, 0b01 = QoS 1, 0b10 = QoS 2, 0b11 = reserved).
Retain
:
If the value of this flag is true (1), the MQTT server stores
the message with its QoS level. When a new client subscribes to a topic of the retained message, the last stored message for this topic will be sent to the new subscriber.
TopicName
:
A string with the topic name.
The payload contains the actual message. As binary transmission is used, the payload can transport any data, and not only strings.
A single SUBSCRIBE
packet can request to subscribe to many topics. The SUBSCRIBE
packet includes at least one topic filter and QoS (list of topic + QoS). For QoS 1 and 2 we also get a PacketId field.
The SUBSCRIBE
packet has the following fields:
PacketId
:
With QoS level > 0 the packet identifier has a number value. It is needed to identify the packet and make it possible to identify the
responses related to this packet.
Topic + QoS
:
Here we get a list of pairs. Each pair contains a topic and the corresponding QoS.
If the server gets a valid SUBSCRIBE
packet it will respond with a SUBACK
packet that confirms the receipt and processing of the SUBSCRIBE
packet.
The SUBACK
packet has the following fields:
PacketId
:SUBACK
will include the same PacketId in the header that was received in the SUBSCRIBE
packet. With QoS level > 0 the packet identifier has a number value.
ReturnCode:
SUBACK
will include one return code for each pair of topic + QoS. The possible values for these return codes are:
0
Successful subscription with QoS 0.
1
Successful subscription with QoS 1.
2
Successful subscription with QoS 2.
128
Subscription failed.
The UNSUBSCRIBE
packet contains a PacketId
in the header and one or more topics in the payload. It is not necessary to include the QoS levels. A single UNSUBSCRIBE
packet can request the server to unsubscribe a client from many topics. The server responds with an UNSUBACK
packet. It confirms the receipt and processing of the UNSUBSCRIBE
packet. The UNSUBACK
packet contains the same PacketId
in the header that was received with the UNSUBSCRIBE
packet.
Install wireshark. Start wireshark and select your Ethernet interface. In the textbox (apply a display filter) add the following filter line with the IP address of your Raspberry Pi:ip.addr == 192.168.131.100
.
Open MQTT.fx and open the connection profile for your server (Raspberry Pi). Define a username and a password under User Credentials
. Now connect MQTT.fx
with the server. Document the CONNECT
command with wireshark (screen). Analyse the command in detail. Mark the different informations with different colours in the screenshot.
Catch two more screenshots for a PUBLISH
and a SUBSCRIBE
command.
Let's get practical. We want to create an IoT device, capable of publishing and subscribing. An App on our mobile phone is used to get the information and to give us the possibility to act.
The device should have a light sensor and should be able to switch on an LED lamp and even dim it.
For our device we need a microcontroller or sb-computer that has a TCP/IP stack. And we need an MQTT library for this device. Fortunately there are MQTT client libraries available for the most popular programming languages and platforms. Some of the libraries might not implement all the features of MQTT, so we must look if a library is suitable or not for our needs.
Possible solutions for IoT devices acting as MQTT client (subscriber, publisher or both) are Arduino or ESP boards (with WiFi, Ethernet or both), a Raspberry Pi board, a mobile phone, a laptop, tablet , computer , NAS, server etc..
We choose an ESP32
board and use a photo-resistor as light sensor and a stripe of white LED's working with 12 V (4.2 W) as a lamp. Measure the resistance of your photo-resistor when it's dark, and when you have full light (outside) with an ohmmeter. Design a voltage divider to get the maximum span to read the luminosity with the ADC of the ESP32
. Document the circuit and your calculations of the output voltages of the voltage divider with the chosen resistance. Measure the min/max output voltages with a voltmeter.
Write an Arduino program to measure the light. Map the min/max values with the map
-function to get a range from about 0 to 100% and output this mapped value on the serial monitor. Document the software and the output screen(min + max). Adjust your series resistor if needed.
Connect your 12V LED stripe with a BUZ11
(alt.: P36NF06
) transistor to your ESP32
. We will use PWM to change the power and thus the luminosity of the stripe. Document the transistors main features (GS voltage, DS voltage, max. current, rDS(ON)), comment them and and design a circuit to get the job done. Test the stripe with different PWM values. The ESP32 uses the functions ledcAttachPin(pin, led_channel)
and ledcWrite(led_channel, value)
instead of analogWrite()
. More infos can be found here an in the net.
Measure the maximum voltage the stripe gets. The BUZ11
needs a Gate-Source voltage above 3 V to act as a switch and minimise it's Drain-Source-voltage to 40 mΩ. We need a second transistor (2N7000
) to fully switch the BUZ11
to "ON". The BUZ11
needs some 2-3 mA, so dimension your 2N7000
transistor working resistor accordingly. Draw and built the circuit. What happens with the logic? Why did we need the BUZ11
and couldn't use only one 2N7000
?
Write and document the Arduino code to change the luminosity from dark to bright (0-100%) with the help of a PWM
. More infos about PWM
pins on ESP32
can be found here. Than use the values from the photo resistor to dim the LED stripe. Document your doings.
Install the PubSubClient
library from Nick O'Leary in Arduino (Tools > Manage Libraries...)
Next we need an MQTT library for Arduino. One of the oldest and stablest libraries is the library of Nick O'Leary called PubSubClient
.
To understand how the library works we will look at first at an Arduino sketch that only publishes a message:
/* mqtt_basic_pub.ino
* weigu.lu
* Basic ESP32 MQTT example to publish a message*/
#include <WiFi.h>
#include <PubSubClient.h>
// WiFi and network settings
const char *WIFI_SSID = "myssid"; // SSID
const char *WIFI_PASSWORD = "mypass"; // password
// MQTT settings
const char *MQTT_SERVER = "192.168.131.100";
const char *MQTT_CLIENT_ID = "bussy_mqtt_pub_1";
const char *MQTT_OUT_TOPIC = "bussy_mqtt_pub";
const short MQTT_PORT = 1883; // TLS=8883
WiFiClient ESP32_Client;
PubSubClient MQTT_Client(ESP32_Client);
const unsigned long PUB_DELAY = 3000; // publish every in ms
String mqtt_message;
int message_counter = 0;
long prev_millis = 0;
void setup() {
Serial.begin(115200);
init_wifi();
MQTT_Client.setServer(MQTT_SERVER, MQTT_PORT);
}
void loop() {
if (!MQTT_Client.connected()) {
mqtt_reconnect();
}
MQTT_Client.loop();
if (millis() - prev_millis > PUB_DELAY) {
prev_millis = millis();
++message_counter;
mqtt_message = "Hello world#" + String(message_counter);
Serial.println("Publish MQTT message: " + mqtt_message);
MQTT_Client.publish(MQTT_OUT_TOPIC, mqtt_message.c_str());
}
}
void init_wifi() {
Serial.println("Connecting to " + String(WIFI_SSID));
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
randomSeed(micros());
Serial.print("\nWiFi connected\nIP address: ");
Serial.println(WiFi.localIP());
}
void mqtt_reconnect() { // Loop until reconnected
while (!MQTT_Client.connected()) {
Serial.print("Attempting MQTT connection...");
if (MQTT_Client.connect(MQTT_CLIENT_ID)) { // Attempt to connect
Serial.println("connected");
MQTT_Client.publish(MQTT_OUT_TOPIC, "connected");
}
else {
Serial.println("failed, rc=" + String(MQTT_Client.state()) +
" try again in 5 seconds");
delay(5000); // Wait 5 seconds before retrying
}
}
}
First we need to include the WiFi library (included in the core package) and the SubPubClient library. Next we initialise some constants as the SSID and the password for our WiFi connection and the IP of our MQTT server. We will use two functions, one to initialise and connect to WiFi (init_wifi()
) a and a second to reconnect to the MQTT server if the connection is lost (mqtt_reconnect()
). In our main loop we use the function millis()
to get a delay of 3 s without blocking the loop.
The reconnect function is a blocking function meaning if a reconnect is not possible the other tasks of the sketch will not be done until a reconnection is possible.
For the message itself we use a String object instead of C-strings (ESP32 has no memory problems). This makes it easier to compose messages. To convert the String object to a C-String we use the method c_str()
(the publish
method of PubSubClient
needs a C-string).
Test the publishing sketch with the help of MQTT.fx
(screenshot).
Change the sketch to a non-blocking program. Eliminate the while loop in the mqtt_reconnect()
function. The reconnect function should return a boolean value using return client.connected();
. Rename the function to mqtt_reconnect_once()
. Use millis()
in the main loop where the reconnect function is called and the reconnect should be tried every 5 seconds. For help look at the nonblocking example of the library (File > examples > PubSubClient > mqtt_reconnect_nonblocking
). Document the commented sketch.
Change the sketch from above, so that it publishes the values from the photo resistor (previous exercise) instead of a "Hello World". The output should be formatted as JSON string as shown in the picture below. You can use normal String functions or the Arduino_JSON library by Arduino. Document the commented sketch and the output.
/* mqtt_basic_sub.ino
* weigu.lu
* Basic ESP32 MQTT example to subscribe to a topic*/
#include <WiFi.h>
#include <PubSubClient.h>
// WiFi and network settings
const char *WIFI_SSID = "myssid"; // SSID
const char *WIFI_PASSWORD = "mypass"; // password
// MQTT settings
const char *MQTT_SERVER = "192.168.131.100";
const char *MQTT_CLIENT_ID = "bussy_mqtt_sub_1";
const char *MQTT_OUT_TOPIC = "bussy_mqtt_sub";
const char *MQTT_IN_TOPIC = "bussy_mqtt_pub";
const short MQTT_PORT = 1883; // TLS=8883
WiFiClient ESP32_Client;
PubSubClient MQTT_Client(ESP32_Client);
void setup() {
pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED
Serial.begin(115200);
init_wifi();
MQTT_Client.setServer(MQTT_SERVER, MQTT_PORT);
MQTT_Client.setCallback(mqtt_callback);
}
void loop() {
if (!MQTT_Client.connected()) {
mqtt_reconnect();
}
MQTT_Client.loop();
}
void init_wifi() {
Serial.println("Connecting to " + String(WIFI_SSID));
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
randomSeed(micros());
Serial.print("\nWiFi connected\nIP address: ");
Serial.println(WiFi.localIP());
}
void mqtt_reconnect() { // Loop until reconnected
while (!MQTT_Client.connected()) {
Serial.print("Attempting MQTT connection...");
if (MQTT_Client.connect(MQTT_CLIENT_ID)) { // Attempt to connect
Serial.println("connected");
MQTT_Client.publish(MQTT_OUT_TOPIC, "connected");
MQTT_Client.subscribe(MQTT_IN_TOPIC); // ... and resubscribe
}
else {
Serial.println("failed, rc=" + String(MQTT_Client.state()) +
" try again in 5 seconds");
delay(5000); // Wait 5 seconds before retrying
}
}
}
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Topic: " + String(topic) + "\nMQTT message: ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
if ((char)payload[0] == '1') { // Switch LED on if first character is '1'
digitalWrite(BUILTIN_LED, HIGH); // LED on
} else {
digitalWrite(BUILTIN_LED, LOW); // LED off
}
}
In this example we get have 2 topics, one to publish and one to subscribe.
We need the same init_wifi()
function, but the mqtt_reconnect()
function changes. It must reconnect and subscribe to the in topic. New is the mqtt_callback()
function.
The following JSON string will be used to control the luminosity of our LED stripe:{"control":"PWM","value":50,"unit":"%"}
.
To parse (divide into parts and identify the parts) the JSON string, we will use the simple Arduino_JSON library by Arduino. Install the library with the Arduino library manager. Write a little test program to extract the value from the JSON string. Look at the JSONObject
example of the library. Document your sketch.
Change the subscribe program, so it is using the value from the JSON string to control the LED stripe. The JSON lib needs an array of char, so convert the byte array with a cast by using the following lines. The PWM
value needs to be of type double
:
char input[128];
for (int i = 0; i < length; i++) {
input[i] = (char)payload[i];
}
Security is for the IoT an extremely important topic! Sometimes we we use MQTT to publish values that are neither confidential nor critical for other
applications, but often it is important, that only the authorized persons can access a device e.g if we control a drone via MQTT.
Another aspect is that our device perhaps can be used to gain control over the network behind our device. If you search the web you find many of these exploits, like the hack of the Cherokee jeep or the HVAC hack at target.
The novel "Blackout" from author Marc Elsberg published in 2012 shows impressively how hacked smartmeters could be used to trigger a collapse of electrical grids across Europe. The book made clear that we didn't focus enough on security and helped to to draw attention to the problem. It was even noticed by Germany politics.
Security generates always an additional overhead. So it is important to keep a balance to avoid overheads that can make our project unusable. Also some cryptographic algorithms are not suitable for microcontroller with little processing power in our IoT boards. If security is needed we better choose a more powerful hardware for our IoT board. An ESP32 microcontroller for example has a separate hardware accelerator for cryptographic algorithm implementations, that neither an Arduino or ESP8266 can provide.
Many security levels also may require too many maintenance tasks (e.g. generate and distribute a certificate for each device), so the project gets unmaintainable and extremely complex.
For security we need need Authentication to check who (person or device) wants to publish or subscribe (login with name and password) and we need Authorisation to allow the access to all the data or parts of it or to deny the access.
MQTT has some own security features, but uses also other standardized solutions like SSL/TLS. As MQTT resides in the top layers of the on top of the OSI model we can implement security in different layers:
Network-layer:
If our client connects via WLAN we get already a good security by using WPA2/3. To secure the data from our client to the server over wire we can use a Virtual Private Network (VPN
).
Transport-layer:
MQTT uses the Transmission Control Protocol (TCP
) as the transport protocol. With TCP the communication is not encrypted. Encoding can be done with a symmetric key cryptographic protocol like Pre-Shared Key (TLS/PSK
) or an unsymmetrical cryptographic protocol like Transport Layer Security(TLS
or TLS/SSL
because Secure Socket Layers (SSL) is the predecessor).
Application-layer:
Here we use the features of MQTT. Authentication is done with username and password. Additionally we can use the ClientId (Client Identifier) to identify each client and combine it with username and password authentication. We can also provide a unique prefix to the ClientId, only known by the server to increase security. Naturally we also can encrypt the message payload and add integrity checks to ensure data integrity. However, the topic and password would still be unencrypted. Only TLS or PSK make sure that everything is encrypted. With plug-ins it is possible to provide more complex authentication and authorization mechanisms to e.g. grant or deny permissions to different topics.
First let's create a folder named mosquitto
in our home folder and change the directory.
mkdir mosquitto
cd mosquitto
To create a password file we have the utility mosquitto_passwd
. The switch
-c
is used to create a new password file (overwrites an existing file!) and the switch -b
is convenient to run in batch mode and allow passing passwords on the command line. Here an example:
The file is named mosqui_passfile
. Two logins with the usernames btsiot1
and btsiot2
are created. The passwords are btsiotpass1
and btsiotpass2
.
After creating the file we find in in our new folder: /home/pi/mosquitto
. We see that the username is cleartext, but the passwords are hashed and not readable!
Now we need to tell the mosquitto-server to use the password file. This is done in the configuration file.
The default configuration file is in /etc/mosquitto/mosquitto.conf
. Here a link to a complete default configuration file: mosquitto.conf
. If we open this file we read at the beginning that we should place our own file into the folder conf.d
:
We also see that a log file is already specified in the default file.
Let's use our own configuration file with the name mymosqui.conf
in the /etc/mosquitto/conf.d
folder:
# My personal config file for mosquitto
# Port to listen (1883 is also used with password!)
listener 1883
### Security ###
# Allow anonymous
allow_anonymous false
# Client Prefix:
#clientid_prefixes btsiot-
# Path to password file
password_file /home/pi/mosquitto/mosqui_passfile
# Logging (don't define "log_dest file" because already defined in default!)
log_dest stdout
log_type error
log_type warning
log_type notice
log_type information
connection_messages true
log_timestamp true
Now you can use the following commands for two different clients. The -i switch is here optional, but let's us add the client-id if we want to (pay attention, it must be unique):
mosquitto_sub -V mqttv311 -p 1883 -i mycl1 -u btsiot1 -P btsiotpass1 -t PV/sunshine -d
mosquitto_pub -V mqttv311 -p 1883 -i mycl2 -u btsiot2 -P btsiotpass2 -t PV/sunshine -m Greetings -d
mosquitto
in your home folder. Test the new configuration with in your Raspi console by subscribing and publishing with password (2 screenshots). Tip: the simplest way to reload the new config file is a sudo service mosquitto restart
and than a sudo service mosquitto status
to check if there is no error in the config file. You can also look in the log file under /var/logs/mosquitto
.We can increase security with a unique prefix for the client Id.
clientid_prefixes btsiot-
and test again with a prefix by adding the prefix to your client-id (screenshots)./var/log/mosquitto/mosquitto.log
to view the mosquitto log messages.Even if we use passwords and prefix, the password, client ID and the topics are only encrypted if WiFi is used. In a wired environment it is easy to get these information with a sniffer as wireshark (as seen in the just do it exercise).
The only secure solution is encryption. We have two possibilities. We can use TLS-PSK
Transport Layer Security cipher suites with a pre-shared key or TLS/SSL
encryption.
The pre-shared keys are symmetric keys (same key on client and server) shared in advance among the clients and server to establish a TLS connection. Pre shared keys are used e.g. in WiFi routers where all the clients need to know the WiFi key in advance. Clearly it is difficult to exchange secret keys with unknown clients over the Internet (e.g. to secure Internet shopping). This is the disadvantage of TLS-PSK, and for such applications TLS/SSL with asymmetric keys (private and public keys) is used.
So what are the advantages of TLS-PSK?
A first advantage of TLS-PSK (depending on the cipher suite) is that there are no public key operations (no key server) needed. So less performant microcontroller can be used. A second advantage is that it is also easier in closed environments (manually configuration in advance) to configure a PSK than to use certificates.
If we send a message of 72 Byte we get 1172 byte with TLS-PSK and 3742 byte with TLS/SSL. So clearly we get less overhead with TLS-PSK.
In MQTT the PSK keys are defined in a PSK file. The name of the file is arbitrary. PSK identities and keys are separated with a colon. The key is in hex and should have more than 20 digits. Let's try this with a file named mosqui_pskfile
with the following content:
btsiot1:0123456789abcdef0123
Now we change the listener port and add two lines to switch on psk-support and provide the path to the psk-file to /etc/mosquitto/conf.d/mymosqui.conf
:
# Port to use for the default listener.
listener 8883
# Pre-shared-key based SSL/TLS support (text "btsiot" is arbitrary)
psk_hint btsiot
# Path to pre-shared-key key file
psk_file /home/pi/mosquitto/mosqui_pskfile
The default port for encrypted MQTT communication is 8883. The psk_hint
line switches psk support on. The text is needed for authentication by the broker but is arbitrary.
Now we can publish and subscribe with the following lines. Because of our unique prefix the -i
switch (ClientId) is not optional.
mosquitto_sub -V mqttv311 -p 8883 -i btsiot-client1 --psk-identity btsiot1 --psk 0123456789abcdef0123 \
-u btsiot1 -P btsiotpass1 -t PV/sunshine -d
mosquitto_pub -V mqttv311 -p 8883 -i btsiot-client2 --psk-identity btsiot1 --psk 0123456789abcdef0123 \
-u btsiot1 -P btsiotpass1 -t PV/sunshine -m "Hallo_BTS-IoT_:)" -d
Test the two commands with PSK (2 screenshots).
On the Raspberry Pi we can use tshark
to capture the communication. Install tshark with sudo apt install tshark
.
Create a file with the name mqtt_psk.txt
in /home/pi
and change the permissions, so that other can read and write the file.
Now use the following command to capture data: sudo tshark -i any -f "port 8883" -w mqtt_psk.txt
. Copy the file to your desktop PC and analyse the data with wireshark.
To use TLS-PSK we include the WiFiClientSecure
header file and use an object of type WiFiClientSecure
instead of WiFiClient
. In setup()
we need to add the setPreSharedKey()
method to submit the PSK identity and the key. Here is the code to publish a message:
/* mqtt_basic_pub_psk.ino
* weigu.lu
* Basic ESP32 MQTT example to publish an encrypted message with TLS-PSK0*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
const char *WIFI_SSID = "myssid"; // SSID
const char *WIFI_PASSWORD = "mypass"; // password
// MQTT settings
const char *MQTT_SERVER = "192.168.131.100";
const char *MQTT_CLIENT_ID = "btsiot-client2";
const char *MQTT_PSK_IDENTITY = "btsiot1";
const char *MQTT_PSK_KEY = "0123456789abcdef0123"; // hex string without 0x
const char *MQTT_OUT_TOPIC = "PV/sunshine";
const short MQTT_PORT = 8883; // TLS=8883
const char *MQTT_USERNAME = "btsiot1";
const char *MQTT_PASSWORD = "btsiotpass1";
WiFiClientSecure ESP32_SEC_Client;
PubSubClient MQTT_Client(ESP32_SEC_Client);
const unsigned long PUB_DELAY = 3000; // publish every in ms
String mqtt_message;
int message_counter = 0;
long prev_millis = 0;
void setup() {
Serial.begin(115200);
init_wifi();
MQTT_Client.setServer(MQTT_SERVER, MQTT_PORT);
ESP32_SEC_Client.setPreSharedKey(MQTT_PSK_IDENTITY, MQTT_PSK_KEY);
}
void loop() {
if (!MQTT_Client.connected()) {
mqtt_reconnect();
}
MQTT_Client.loop();
if (millis() - prev_millis > PUB_DELAY) {
prev_millis = millis();
++message_counter;
mqtt_message = "Hello world#" + String(message_counter);
Serial.println("Publish MQTT message: " + mqtt_message);
MQTT_Client.publish(MQTT_OUT_TOPIC, mqtt_message.c_str());
}
}
void init_wifi() {
Serial.println("Connecting to " + String(WIFI_SSID));
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
randomSeed(micros());
Serial.print("\nWiFi connected\nIP address: ");
Serial.println(WiFi.localIP());
}
void mqtt_reconnect() { // Loop until reconnected
while (!MQTT_Client.connected()) {
Serial.print("Attempting MQTT connection...");
if (MQTT_Client.connect(MQTT_CLIENT_ID,MQTT_USERNAME,MQTT_PASSWORD)) { // Attempt to connect
Serial.println("connected");
MQTT_Client.publish(MQTT_OUT_TOPIC, "connected");
}
else {
Serial.println("failed, rc=" + String(MQTT_Client.state()) +
" try again in 5 seconds");
delay(5000); // Wait 5 seconds before retrying
}
}
}
Usually, Transport Layer Security (TLS
) uses public key certificates. We find many sites on the internet that explain how to use TLS/SSL
on the Raspberry Pi with mosquitto or with Arduino. As this topic is already covered in other modules and can be reviewed in the net, we will not go into it further.