php-mqtt/client

Cannot subscribe to a topic

Anubarak opened this issue · 4 comments

Is there anything special to consider when I try to listen to a certain topic?
I've tried it with this code

$topic = 'my/topic';
$client->subscribe($topic, function ($topic, $message){
    echo sprintf("Received message on topic [%s]: %s\n", $topic, $message);
}, 0);
$random = random_int(10, 240);
$client->publish('topic/that/should/trigger/subscribe', 'on');


$i = 0;
while ($i < 5){
    $i++;
    sleep(1);
}

$client->close();

But there is no result. Note: when I try it

$mqtt->loop(true);

I create infinite loops and need to manually kill the PHP Process. The above works fine when I do the same in JS (the topics are totally correct)

Edit: For whatever reason it works when I do it via Console Requests via SSH connection.

Well, let me start by telling you that your code doesn't make much sense in its current state. Generally speaking, a client normally either subscribes to a topic or publishes messages, but there are only very few scenarios where you would want a client to do both at the same time. The primary use case for both at the same time would be to respond with a published message after receiving a message on a subscribed topic (to implement something like RPC).

In your case, I'm not sure what you are trying to go for. Since you were writing mostly about subscriptions, I guess that is what you really want. So the way to go is running $mqtt->loop(true) after adding your subscriptions. Yes, this will start an infinite loop. Which is fine though, since a subscription basically tells the broker: please send me all messages matching this topic pattern, until I tell you I don't want to receive such messages anymore (or until I disconnect). The former can be achieved using $mqtt->unsubscribe($topicPattern).

If you need to subscribe to a topic and want to stop receiving messages as soon as you received a message, you will still need to run $mqtt->loop() but you can interrupt the loop within the subscription callback. Multiple examples for this use case can be found in the php-mqtt/client-examples repository. Especially the subscription examples (section 2) should be helpful. If it's about a clean way to interrupt the loop, the Interrupting the Loop section (5) is probably most helpful.


As a side note, I want to mention that if you are trying to use MQTT subscriptions within a controller (i.e. as part of a request), which is what your description sounds to me like, you are doing something wrong.

Thanks for your detailed answer.
Indeed I need to publish something in order to get the current state of the topic. Maybe it's worth mentioning that I'm not responsible for the MQTT API (not sure if it's actually called that way).

I need to publish a command like foo/bar with the value ? in order to receive the current value of that topic. (Not my design pattern, I just use it)
In my special use case I need to grab the temperature of a room via MQTT (they don't want to create a REST interface for that) and in order to receive that value I need to send an ? to room/5/temperature. That will emit room/5/temperature and I can grab the value.

And yes - I want to do that in a Controller action (not sure where else I should do it, maybe you can help me more with that?) I'm totally new to MQTT and have not done it before. My actual task is to store the current temperature of multiple rooms in our DB via Cronjob. I would prefer a REST API for that but they don't want to create one.

My final goal is actually:

Subscribe to 12 topics (there are 12 rooms)
publish the "send me your temperatures" command in order to receive their temperatures
give up after X seconds if you don't receive something in order to prevent an infinite loop if the devices don't answer or something.

Maybe it's easier for you to understand me when I tell you the entire scenario.
I created a booking system where you can book private saunas for several hours.
Before the booking starts the sauna temperature must be 60° so I need to create a Cron, check for upcoming bookings and maybe increase the temperature depending on the current temperature.

Alright, I understand. Not sure if you use a framework like Laravel or Symfony, but if you do this via cronjob, it would actually be easier to run a console command. You could even keep the MQTT client running and perform the cronjob action entirely in there (using an event loop handler which executes its own logic only after a given amount of seconds has passed since the last iteration). But to keep things simple, I would expect you to use a console command and not an HTTP request.

I'll ignore exception handling in the following example, just to give you an idea how it could be done:

// We have a list of rooms with an initial temperature. This gives us an idea which rooms we need to fetch data for.
$rooms = [
    1 => -1,
    2 => -1,
    3 => -1
];

$client = new MqttClient($host, $port, $clientId);
$client->connect(null, true);

// Here we register a timeout using a loop event handler. The event handler is passed the elapsed time
// the loop runs already in seconds, which is handy in our case here.
$client->registerLoopEventHandler(function function (MqttClient $client, float $elapsedTime) {
    // After 10 seconds, we quit the loop.
    if ($elapsedTime > 10) {
        $client->interrupt();
    }
});

// We use the wildcard `+` to match messages for all rooms.
$client->subscribe('room/+/temperature', function (string $topic, string $message) use ($client, &$rooms) {
    $room = preg_match('/^room\/(\d+)\/temperature$/', $topic, $matches, PREG_OFFSET_CAPTURE, 0);

    // Assign the value to our room.
    $rooms[$matches[1]] = (int) $message;

    // If we received a temperature for all rooms, we can safely return before the timeout.
    if (count(array_filter($rooms, fn ($r) => $r === -1)) === 0) {
        $client->interrupt();
    }
}, MqttClient::QOS_AT_MOST_ONCE);

// After subscribing, we send a request for each room.
foreach ($rooms as $room => $temperature) {
    $client->publish("room/${room}/temperature", '?', MqttClient::QOS_AT_MOST_ONCE);
}

// We then wait for a response to our requests. The loop will be interrupted when all temperature values
// have been received or when the timeout kicks in.
$client->loop(true);

// Gracefully terminate the connection to the broker.
$client->disconnect();

Thank you very much. Yeah this is done via Console Request (using Yii2) The Cron is executed as Console request, not HTTP request on the same server.

You are really awesome, thanks for your effort and for your library again.