Smart devices surround us every day and not only in everyday life: sensors, household appliances, light bulbs, sockets and other equipment. Every day we encounter newer and smarter devices controlled via the Internet or Wi-Fi.
IoT (Internet of Things) means the Internet of smart things. It is a concept that unites physical devices into one network for data transfer and management. And it turns out that the Internet of Things is not a limitation! You can manage devices in the network using the lightweight MQTT protocol.
Hello, Habr! My name is Alexander Cherednikov and I am CTO at QTIM, a company that does custom development. In this article, based on my talk at PHP Russia, I will tell you how to communicate with smart devices using PHP.
MQTT and some of its features
MQTT is a lightweight communication protocol designed specifically for devices with limited capabilities and low bandwidth. Let’s look at some of its features.
MQTT Broker
An MQTT broker is a central node through which clients can exchange messages.
MQTT Brokers:
- Eclipse Mosquitto;
- EMQ;
- HiveMQ;
- NanoMQ;
- VerneMQ;
- RabbitMQ with MQTT plugin.
In practice, I had to work with Eclipse Mosquitto. We chose it for several reasons: it is lightweight, easy to configure, very fast and allows you to transmit messages instantly. You can read about the pros and cons of the others in the documentation and choose one for your needs.
Messages
Messages in MQTT are transmitted in binary form, which reduces memory and decreases the time it takes to transmit a message.
There are different types of messages from smart devices:
- JSON;
- Protobuf;
- XML;
- CBOR;
- Text.
Fortunately, PHP can handle all of this.
Types of packages
When transmitting messages through a broker to smart devices, different types of packets are used in the message:
- CONNECT — establishes a connection with the broker.
- CONNACK — connection confirmation.
- PUBLISH — sending a message to a specific topic.
- PUBACK, PUBREC, PUBREL, PUBCOMP — delivery confirmations for QoS 1 and QoS 2.
- SUBSCRIBE — a request to subscribe to a topic.
- SUBACK — subscription confirmation.
- UNSUBSCRIBE - unsubscribe.
- UNSUBACK - confirmation of subscription cancellation.
- PINGREQ and PINGRESP - connection check.
- DISCONNECT — connection termination.
The packet type is indicated by the first byte at the beginning of the message. Based on it, the broker determines which message to work with. In versions MQTT 3.3.1, MQTT 5.0, these packet types may differ. You need to look at the specific packet type for a specific specification and devices.
QoS
QoS is an important parameter for guaranteeing message delivery when exchanging messages between smart devices and the server.
Types of QoS :
- QoS 0 - does not guarantee message delivery to the subscriber. This means that the publisher sends the message only once and does not wait for confirmation that the message has been received.
- QoS 1 is a more reliable delivery method. QoS 1 guarantees that the message will be delivered to the subscriber at least once. But it does not guarantee that the message will not arrive again.
- QoS 2 is the most reliable delivery method. QoS 2 ensures that the publisher sending the message will send it only once, without duplication.
Topics
When exchanging messages, topics are used - routes that serve to organize and filter data when transmitting a message.
home/temperature | home/humidity | office/temperature |
Topics are similar to routes from the HTTP protocol, but they do not have query parameters for filtering.
There are some features of topics:
- Consists of segments separated by slashes: /rooms/{sensor}/action/{action}
For example, if we want to select certain readings from certain sensors in rooms, we can use wildcards.
- Have wildcards:
Single level (+) - /rooms/+/action/get
A single-level wildcard means that we will only collect information at one level. For example, we want to collect data from all temperature sensors in all rooms, without using information from other devices.
Multilevel (#) - /rooms/#
A multi-level symbol is used when it is necessary to collect information from all devices and sensors in all rooms.
The broker also has system topics, which are designated $SYS. They are intended for diagnosing the broker itself:
$SYS/broker/uptime— broker working hours.$SYS/broker/clients/connected— number of connected clients.$SYS/broker/clients/disconnected— the number of disconnected clients.$SYS/broker/clients/total— total number of clients.$SYS/broker/load— current load on the broker.$SYS/broker/messages/sent— the number of messages sent.$SYS/broker/messages/stored— the number of messages in the broker.
With their help, we can track the load, the number of clients connected to the broker, its operating time, etc. This information may be needed when diagnosing problems and when compiling metrics and graphs.
It is important that system topics do not relate to smart devices in any way, but only to message brokers. It is impossible to obtain any information from the devices themselves.
Publishers
A publisher is a publisher that sends a message about a specific topic to a broker.
Subscribers
Subscribers read these messages according to a specific topic.
For example, a smart temperature sensor sends its data to a broker; then subscribers who monitor the corresponding topic read this data. As a result, we either display the temperature to the end user, or store the data and draw conclusions, build graphs.
The image below shows how the exchange with the MQTT broker occurs.
How PHP Interacts with MQTT
Let’s look at how PHP can interact with MQTT.
Many have heard that PHP is not able to communicate with smart devices. But the MQTT broker can do this, it will receive and coordinate messages. And PHP will help us communicate with MQTT.
The simplest PHP worker looks like this:
//...
$sock = fsockopen($broker, $port, $errno, $errstr, 10);
//...
while (true) {
$response = fread($sock, 2048);
if (ord($response[0]) >> 4 === 3) {
$remainingLength = ord($response[1]);
$topicLength = ord($response[2]) << 8 | ord($response[3]);
$msgTopic = substr($response, 4, $topicLength);
$message = substr($response, 4 + $topicLength, $remainingLength - $topicLength - 2);
echo "Получено сообщение в топике '$msgTopic': $message" . PHP_EOL;
}
usleep (500000);
}
Basically, it’s a long-running loop that runs in a process and reads information from a TCP socket. This information is then parsed into message packets, the message itself, and the topic. And we can work with this information.
Libraries for working with MQTT
There are special libraries for communicating with MQTT:
https://github.com/php-mqtt/client — lightweight, easy to configure and support. I will show examples further with it. It has one feature — it does not support MQTT 5.0. If you encounter this protocol, keep this in mind.
https://github.com/simps/mqtt — built on the Swoole library.
https://github.com/aws/aws-sdk-php/tree/master/src/Iot — MQTT client is provided by the AWS library in its SDK.
How to read messages from a smart device
To read messages, we create a client and connect to it. On this client, we call the subscribe method. We specify as the first parameter which topic we want to receive the message from. In the callback, we accept the topic itself, the message and process it as needed - either save any processing logic to the database, or show it to the client. The third parameter in the subscription is QoS. This must be done so that the library, when sending a message, understands which type of packet to send to the broker for correct interaction. Then a long-lived cycle in the form of loop is launched, and the subscription will be valid until we interrupt it.
If we interrupt the subscription or an exception occurs, we will disconnect from this client to avoid creating unnecessary connections and terminate the long-running process.
try {
$client = new MqttClient(MQTT_BROCKER_HOST, MQTT_BROCKER_PORT);
$client->connect();
$client->subscribe('rooms/+/temp', function (string $topic, string $message) {
// Логика обработки сообщения
}, MqttClient: :Q0S_AT_LEAST_ONCE);
$client->loop();
$client->disconnect();
} catch (MqttClientException) {
// Логика обработки сообщения
}
You can also manage devices if you need to send a command to them. To send a command to a device, you need to publish a message that it receives on a specific topic to the topic to which this device is subscribed. We will also create a client and connect to it.
A simple example of renting a scooter:
$client = new MqttClient(MQTT_BROKER_HOST, MQTT_BROKER_PORT);
$client->connect();
$client->publish(
topic: 'scooter/1234/rent',
message: json_encode(['rent_number' => '4321']),
qualityOfService: MqttClient: :QOS_AT_MOST_ONCE,
);
$client->disconnect();
The topic contains the “Scooter” segment, its number, and the rent command. In the body, we pass the rental number so that the scooter understands what rental it was turned on under. We also pass QOS as the third parameter. Here, we do not start any long-lived cycles, but simply publish a message to the topic and disconnect from the client.
Sometimes you may need to publish a message to a topic. At this point, you need to get a response from the device. Then we can complete the long-lived process so as not to keep constant subscriptions.
Sclient = new NqttClient(MQTT_BROKER HOST, MQTT_BROKER_PORT);
$client->registerLoopEventHandler(function (MqttClient $client, float $elapsedTime) {
if ($elapsedTime >= 30) {
$client->interrupt();
}
});
$client->subscribe('/device/1111/update’, function (string $topic, string $message) use ($client) {
if (substr($message, 1, 2) === 'QG") {
// Тут логика обработки сообщения
$client->interrupt();
}
});
// Публикация сообщения на которой ожидаем ответ
$client->publish('/device/1111/get', json_encode(['data’ => *test']));
$client->loop();
$client->disconnect();
We create a client, subscribe to a certain topic. By the way, this logic is from a real device, which received the device message type in the body of the message itself. We understand the type of this message and make logic on this basis. After processing, we interrupt the message and exit the long-lived cycle, which starts just below.
Next, we publish the message. An important point is that the publication will occur after we have subscribed. It happens that we have published a message, it has time to arrive at the broker. The device has time to read this message, give a response, but the connection has not happened. At this point, we lose the message and do not know what happened at the moment of publication to the device. In the code registered by event handlers immediately after creating a client, you can perform all the same actions with subscriptions and publication. In the example above, I showed how to determine the timeout of the message response from the smartest device.
For example, the Wi-Fi or GSM connection is lost. If you have implemented a request via a button on your website or in a mobile application, you must interrupt the cycle at some point so as not to hang it for a long time. We have 30 seconds for this. Usually, devices respond somewhere around 2-3 seconds, which is not critical. If the timeout is much longer, then a queue system is implemented to process these messages.
Important:
- Do not allow multiple clients to connect with the same ID, because the MQTT broker will return an error that the ID is not unique.
- Close the connection in a timely manner to avoid unnecessary load on the broker.
- Don’t forget about long-lived processes and don’t allow them to leak memory when creating a subscription to a specific topic in the worker.
Logger implementation
The implementation of a simple logger looks like this:
$client = new MqttClient(MQTT_BROKER_HOST, MQTT_BROKER_PORT);
$client->subscribe('#', function (string $topic, string $message) {
$this->logger->info('Сообщение от устройства.', [
'topic' => $topic,
'message' => $message,
]);
});
$client->loop();
$client->disconnect();
We subscribe to all topics sent by devices. This means that we want to see the chronology with timestamps of events sent to the broker. For example, to diagnose why a message was not delivered. The topic itself and the message itself are logged. A long-lived process in the form of a loop is also launched. If this process is interrupted, we disconnect from this client.
The log from a real IoT device looks like this:
In Moscow, Kazan and St. Petersburg there is a service for sharing chargers PowerApp. For stations storing power banks, I wrote software from scratch to communicate with them. This is an example from one of the stations.
Here, update means that the message came from the device. Get means that the message was sent by the server. The CN command turns on the station. We respond that we have received the message, and then the device sends its internal information. The device contains slots where the power banks are located, and we need to understand which power bank is currently inserted, what its charge level is, and see the power bank error codes. Based on this information, we can decide whether to continue renting out the power bank.
The device sends a message in the form of a heartbeat. This happens, depending on the settings, once a minute or three times. The heartbeat shows how the power banks are charging, their current charge, and information about whether the device is online.
A message in the form of Protobuf is also from a real device like the “Take Charge” station, where the exchange takes place in exactly this form:
syntax = "proto3";
package messages. setUpVoice;
message ServerSend {
uint32 rl_index = 1;
uint32 rl_ivl = 2;
uint32 rl_seq = 3;
}
message CabinetReply {
uint32 rl_result = 1;
uint32 rl_code = 2;
uint32 rl_seq = 3;
}
The example above shows setting the volume in the device itself: a message sends the volume level value to the device. Then a message comes back from the device with a response code: successful or not. Everything works fine and is processed.
Problems with physical devices
There are always problems. Most of the equipment is produced on the Chinese market. When communicating directly with Chinese representatives, translation difficulties often arise. We do not always understand each other. We have to adapt.
Some critical device issues I have encountered:
- There is no guarantee of a response to the request. A device on the network has received a message, but does not send anything back. We need to take this into account in the code and re-request additional information in this case.
- The message that needs to be processed is interrupted. We send a command, or the device sends a message in a torn form (half a line, a quarter of a line). Perhaps this is due to the limitation of physical devices, but it happens.
- GSM/WI-FI network loss. If the station or any device is offline, we need to consider this when sending requests to them.
- Rebooting devices during a request. This happens, for example, when renting power banks at stations. We send a rental request, the user comes up, scans the QR code, the bank leaves and uses it, and the station at that moment sends a request to turn on, as if it had just gone online. The Chinese wrote: “Send logs from a real station.” This meant that we had to go to a restaurant, connect, catch this moment and take all the logs from the station to send them later. This did not suit us, so we solved it using logic in the code.
- Not all devices can receive more than one message at a time. This is probably due to the fact that the devices are limited in their technical implementation. For example, two people approach the charger, scan the QR code, and two commands fly into the device. It confuses these commands or does not process them at all, or processes the wrong ones. You need to know how to work with this. For example, keep in mind that you should not send more than one command until the device responds.
MQTT as a way of communication in PHP
PHP can also exchange messages without smart devices, acting as both a publisher and a subscriber. This adds another way to choose how to exchange messages between microservices or devices.
But you will need an additional broker through which you will exchange. There are a number of nuances: messages are limited in query parameters and headers, so it will be inconvenient to communicate between services. But there is also a plus - messages are compressed into binary form and are transmitted very quickly.
Scaling
To sort out all the messages coming from devices, you can use queues. There are currently over a thousand stations in Russia that issue power banks.
Each station sends messages when the heart beats. Commands are also sent to it, and it sends messages in response. It turns out up to 3-3.5 thousand messages per second. Even if the logic for processing these messages is very large, we can easily analyze them all using Rabbit.
Horizontal scaling can be anything you want, with Docker and Kubernetes.
Everything scales perfectly. There are no practical limitations - PHP workers, PHP applications, MQTT brokers themselves scale.
Benefits of PHP in IoT
Let me summarize the advantages of PHP when communicating with smart devices:
- Reducing development costs. We do not involve other teams, do not spend money on creating an additional service, we do everything ourselves, in our native language.
- Quick control of smart devices via applications. We send a command via any application (web, mobile), and send a request to a smart device via the backend. After the device responds, we can immediately update the information on the action in the application.
- Integration with any type of messages. Fortunately, the language allows you to process any messages sent by smart devices.
- Easy to scale using standard tools.