Manual

do

Maker

.

com

QT MQTT e QML - parte 3

QT MQTT e QML - parte 3

Com esse artigo, introduziremos alguns novos conceitos importantes para integrar recursos do C++ como backend para se comunicar com a interface em QML, agora usando também o QT MQTT para fazer a comunicação, como prometido no artigo anterior. Se não está acompanhando a série, sugiro que comece desse artigo.

Como acessar propriedades do QML a partir do C++

No artigo anterior vimos como interagir entre arquivos QML, através de alias e import. Agora vamos manipular as propriedades dos componentes a partir do C++.

Para relembrar um ponto importante, abaixo disponho do código que insere uma imagem de fundo à janela que será exibida no display:

Image {
        anchors.fill: parent
        x: 0
        y: 0
        id: rpi
        source: "raspberry.jpeg"
        opacity: 0.2
    }

Repare que, como anteriormente explicado, o identificador do componente é o id. Ele é utilizado para acessar propriedades de um componente a partir de outra parte do código QML. Porém, para acessar uma propriedade do componente a partir do QML, alguns procedimentos iniciais serão necessários.

objectName

Assim como id é utilizado para identificar um componente no código QML, para acessar o mesmo componente a partir do C++ devemos utilizar outro tipo de identificador; o objectName. Dessa vez, o nome do identificador deve ser protegido por aspas, tal como o exemplo acima o faz para identificar a imagem a ser utilizada para o background do display, especificado na propriedade source.

Não há problema algum em ter ambos os identificadores na declaração do componente. Para o componente Dial eu utilizei a seguinte porção de código:

Dial {
        objectName: "dialTemp"
        id: dialTemp
        x: 19
        y: 48
        width: 53
        height: 53
        value: 0.374

        background: Rectangle {
            x: dialTemp.width / 2 - width / 2
            y: dialTemp.height / 2 - height / 2
            width: Math.max(64, Math.min(dialTemp.width, dialTemp.height))
            height: width
            color: "transparent"
            radius: width / 2
            border.color: dialTemp.pressed ? "#17a81a" : "#21be2b"
            opacity: dialTemp.enabled ? 1 : 0.3
        }

        handle: Rectangle {
            id: handleItemTemp
            x: dialTemp.background.x + dialTemp.background.width / 2 - width / 2
            y: dialTemp.background.y + dialTemp.background.height / 2 - height / 2
            width: 6
            height: 3
            color: dialTemp.pressed ? "#17a81a" : "#21be2b"
            radius: 8
            antialiasing: true
            //opacity: control.enabled ? 1 : 0.3
            transform: [
                Translate {
                    y: -Math.min(
                           dialTemp.background.width,
                           dialTemp.background.height) * 0.4 + handleItemTemp.height / 2
                },
                Rotation {
                    angle: dialTemp.angle
                    origin.x: handleItemTemp.width / 2
                    origin.y: handleItemTemp.height / 2
                }
            ]
        }

Apenas as primeiras 3 linhas são importantes nesse momento. O restante do código é personalização do componente, também descrito no artigo anterior. A personalização não é obrigatória, caso ache código demais, apenas quis modificar a aparência do componente para harmonizar com a janela.

Repare que em ambos os identificadores utilizei o mesmo nome. Não é mandatório e sendo iguais ou diferentes, funcionarão do mesmo modo, desde que respeitada a proteção de objectName por aspas.

Nenhum dos identificadores pode se repetir em outro componente. Por exemplo, se for definido:

Dial {
    objectName: "dial"
    ...
}

Outros componentes não poderão ter o nome "dial" como idenficador.

QQuickItem

Já no código C++, precisamos criar um QObject para acessar os dados do QML, que será recebido de um QQuickItem. Por essa razão, o primeiro passo no código C++ é incluir a biblioteca QQuickItem:

#include <QQuickItem>

Depois, criamos um QObject, trazendo o objeto raiz com todos seus componentes:

QObject* mainPage      = engine.rootObjects().first();

Depois disso, já podemos começar a acessar as propriedades dos componentes do nosso arquivo QML:

mainPage->findChild<QQuickItem*>("labelTempRPi")->setProperty("text", "42.2 C");

No exemplo acima, o objectName a ser tratado é o "labelTempRPi", definido previamente no arquivo QML. A propriedade a ser modificada é o texto, cujo identificador é text, que modificamos através do método setProperty, passando a propriedade e o valor.

Não se preocupe com o código, ele estará disponível para download mais ao final do artigo, agora tente apenas entender os conceitos, sem compromisso com desenvolvimento.

Onde manipular os componentes QML?

No projeto originado a partir do exemplo swipe, disponível na própria IDE QtCreator, temos apenas os arquivos ui.qml.qml e o main.cpp.

Particularmente, não gosto de escrever código de rotina dentro do arquivo main.cpp. Para testar o funcionamento dos recursos a serem utilizados, tudo bem, mas a implementação fica mais "limpa" se for feita em uma classe à parte. Para isso criei uma classe que interage com os componentes QML e de quebra recebe as atualizações das informações advindas de sensores por MQTT, como é o caso dos componentes dial.

A temperatura do Raspberry é pega do próprio sistema, utilizando outro recurso do Qt, que discorro mais adiante.

Criar uma classe C++ no Qt

Tendo o projeto aberto, basta ir ao menu File > New File or Project > C++ Class.

cpp_class-300x138.webp

Na tela seguinte, dê um nome à classe (eu escolhi MyComm, mas pode ser o que quiser). Em Base Class escolha QObject e marque a caixa de seleção Include QObject. No vídeo mostrarei a criação de uma classe base, mas é bastante simples. Uma estrutura inicial será criada e só precisamos implementar o código sobre ela.

Nessa classe, criei alguns slots e alguns métodos, nada complexo. No arquivo .h definimos os construtores e implementamos no arquivo .cpp. Mas antes disso, será necessário adicionar o suporte ao QT MQTT.

Como instalar o QT MQTT

Clone ou baixe o arquivo zip do meu repositório. No notebook, utilizei o qmake do Qt baixado diretamente do site. Instalei também outra versão, mas essa não utilizei ainda. Se quiser, aproveite e baixe-a também.

Exceto pelo nome do diretório, o processo restante é o mesmo:

cd qmqtt
qmake -r
make
sudo make install

Se tudo correr bem, siga adiante, senão, resolva o problema primeiro.

Modificar o arquivo .pro para adicionar o QT MQTT

No Qt temos o arquivo de projeto, que é o nome do projeto seguido pela extensão .pro. Nela, devemos adicionar outros recusos do Qt que não o quick, já presente.

QT += quick network core qmqtt

Implementação do QT MQTT

Agora vá ao arquivo MyComm.h (ou no arquivo de header com o nome que você definiu) e inclua, dentre outras, o qmqtt:

#include <QObject>
#include <QQuickItem>
#include <QDebug>
#include <QTimer>
#include <QProcess>
#include "qmqtt.h"

Explico previamente o que usaremos de cada uma delas.

O QObject já estará incluso, desde a criação da classe. O QQuickItem é o que utilizaremos para pegar os componentes do arquivo QML. O QDebug é como o cout, o printf ou o Serial.println do Arduino, apenas para exibir mensagens, que é mais rápido do que usar o debugger para analisar a implementação. O debugger uso normalmente quando há realmente alguma anomalia que precisa ser analisada. Devo mostrar isso em algum artigo posterior relacionado a Qt.

A biblioteca QTimer está sendo utilizada para temporizar a captura de temperatura do Raspberry, cujo processo é feito a partir de um slot, descrito mais adiante.

A biblioteca QProcess é muito próximo de uma chamada system, mas ela realmente consegue interagir com um binário de sistema, de forma a ser uma excelente escolha para criar front-ends. É muito comum em Linux a utilização de front-ends, de forma que o binário com função específica se torna compatível com utilização por terminal ou por GUI.

Signals e slots

A implementação é curta e os feedbacks são emitidos por sinais, chamados no Qt de signals. Para interagir com os signals, utilizamos os slots.

Os sinais são como as interrupções na MCU e os slots são como as ISR. Para relacionar um sinal a um slot, utilizamos a chamada connect do QObject.

Para implementar a conexão, precisamos de 6 linhas de código, mas para ter uma implementação mais robusta é fundamental implementar alguns desses sinais. Vou manter a minha senha de teste e o IP do meu broker no código para fidelizar o que está funcional. Criei o método start() com todo o código necessário para a conexão acontecer:

this->client  = new QMQTT::Client(QHostAddress("192.168.1.104"), 1883);

    client->setClientId("teste2");
    client->setUsername("dobitaobyte");
    client->setPassword("fsjmr112");

    connect(client,SIGNAL(connected()),this,SLOT(onConnected()));
    connect(client,SIGNAL(disconnected()),this,SLOT(onDisconnected()));
    connect(client,SIGNAL(received(const QMQTT::Message&)),this,SLOT(onReceived(const QMQTT::Message&)));

    client->connectToHost();

Repare que, tirando o connect, temos 5 linhas. A sexta linha é a subescrição ao tópico MQTT. Já explico onde está.

Aqui temos um objeto client criado a partir da classe QMQTT::Client do QT MQTT. Ao instanciar o objeto, passamos o endereço e a porta. O endereço deve ser um QHostAddress, por isso o IP está dentro de outro método.

Devemos configurar um client ID. No caso, usei teste2 porque fiz uma conexão pelo smartphone utilizando o aplicativo MQTT Dashpara Android.

Configurei um usernamepassword para acessar o broker MQTT, sem permitir acesso anônimo. A senha de laboratório uso para WiFi, MQTT e qualquer outro artigo que necessite um usuário e senha. Se quiser reproduzir na íntegra, use-os em seu ambiente de teste.

Agora o que mais me apaixona no Qt; os signals e slots.

Quando o programa se conecta ao broker, ele emite um sinal connected(). Quando perde a conexão com o broker, emite um sinal disconnected(). Quando chega dados, ele recebe um sinal received() que recebe como argumento um objeto QMQTT::Message. Para receber esses sinais, precisamos criar nossos slots, com o nome desejado e quando necessário for, criar com o respectivo argumento de função.

Para finalizar, iniciamos a conexão, com client->connectToHost(), da própria biblioteca.

Nos slots de conexão e desconexão, podemos inserir qualquer tratamento que acharmos devido, conforme a situação. Mesmo sem essas implementações, podemos executar o programa sem problemas. Já para a recepção da mensagem, não tenha dúvidas quanto à necessidade de implementá-la.

void MyComm::onReceived(const QMQTT::Message &message)
{
    qDebug() << "Message inbox..." << endl;

    QString topic = message.topic();
    QString payload = QString(message.payload());
    double valueDouble = payload.toDouble() / 100;

    qDebug() << topic;
    if (topic.contains("temperature")){
        this->mainPage->findChild<QQuickItem*>("labelTempExt")->setProperty("text", payload);
        this->mainPage->findChild<QQuickItem*>("dialTemp")->setProperty("value", valueDouble);
    }
    else if (topic.contains("humidity")){
        this->mainPage->findChild<QQuickItem*>("labelHumidity")->setProperty("text", payload);
        this->mainPage->findChild<QQuickItem*>("dialHumidity")->setProperty("value", valueDouble);
    }
}

No QMQTT::Message temos duas informações importantes utilizadas nessa implementação; o topic e o payload. Conforme o tópico, adicionamos um tratamento específico, assim criamos apenas um método para tratar todas as mensagens. O payload contém a informação que iremos exibir nos componentes da interface.

O tipo utilizado nos componentes label é um QString e para os valores de ponto flutuante, utilize double. Os valores double que fazem a variação da posição dos componentes dialprogressBar vão de 0.0 à 1.0. Nesse caso, foi necessário dividir o valor de leitura por 100, considerando que o valor máximo de temperatura seja esse. De outro modo, seria necessário fazer um mapeamento dos valores.

Após essas implementações, ainda precisaremos utilizar o programa com o driver do display ST7789 para carregar a imagem no display.

O código já foi descrito em outro artigo. A melhor implementação de tempo para evitar sobrecarga do sistema foi essa porção de código (mas o código completo estará disponível para download, não se preocupe):

for i in range(10000):
    #os.system("raspistill -t 1 --width 240 --height 240 -o /dev/shm/teste.jpg")
    try:
        image4 = Image.open('/dev/shm/merda.png')
        image4.thumbnail((240, 240), Image.ANTIALIAS)
        image4 = expand2square(image4, (0,0,0))
        disp.display(image4)
        sleep(0.300)
    except:
        pass

Nela, estou fazendo um range de 10.000 interações, mas para manter em loop infinito, basta trocar a primeira linha por:

while True:

O intervalo de tempo é bastante baixo, como pode ser visto; 3 frames por segundo, aproximadamente. A linha comentada do raspistill foi utilizada para fazer streaming da câmera do Raspberry, mostrada nesse outro artigo.

Compilar o programa para exibir no display

Desenvolvi tudo no notebook, depois copiei o diretório do projeto para o Raspberry e então compilei nativamente. Basicamente:

cd 240x240
qmake
make
./240x240 -platform vnc

Desse modo, pude também depurar o funcionamento mesmo antes de enviar para o display, utilizando o vncviewer apontando para o IP do Raspberry.

Cada frame capturado será gravado em /dev/shm, que é um sistema de arquivos em memória, de modo que não haverá gargalo nem escrita no cartão micro SD.

Mantendo o programa em Qt rodando em um terminal, executei em outro terminal o programa clock_NEW.py, modificado a partir do exemplo contido no diretório da biblioteca do display ST7789.

Configurar o broker MQTT

Rodei um broker no próprio Raspberry. Se ainda não fez nenhuma implementação do broker, recomendo. Não leva mais que 5 minutos, basta seguir esse artigo.

Código para gerar valores randômicos

Já configurei diversos tipos de sensores de temperatura e umidade, como você pode ter acompanhado no blog. Mas não mantenho os sensores conectados e como o objetivo era testar o programa de visualização e não o sensor, preferi escrever um código curto para enviar dados randômicos ao broker MQTT. Um mero shell script, com o seguinte código:

#!/bin/bash

for i in `seq 1 100`; do
    VALUE_ONE=`for i in {1..100}; do echo .$(( ( RANDOM % 100 )  + 1 )); done | awk '{ a += $1 } END { print a }'`

    mosquitto_pub -h 192.168.1.104 -p 1883 -i djames -u dobitaobyte -P fsjmr112 -t /qml/temperature -m "$VALUE_ONE"

    sleep 1
    
    VALUE_TWO=`for i in {1..100}; do echo .$(( ( RANDOM % 100 )  + 1 )); done | awk '{ a += $1 } END { print a }'`

    mosquitto_pub -h 192.168.1.104 -p 1883 -i djames -u dobitaobyte -P fsjmr112 -t /qml/humidity -m "$VALUE_TWO"

    echo $VALUE_ONE
    echo $VALUE_TWO
done

Salve o conteúdo acima em um arquivo.she mude as permissões para torná-lo executável:

chmod 700 arquivo.sh

Para executá-lo:

./arquivo.sh

Temperatura do Raspberry

A temperatura do Raspberry é obtida através do comandovcgencmd measure_temp. Esse valor que aparece na imagem de destaque para a temperatura do Raspberry Pi 3B+ é real. A galera fica em polvorosa por causa da temperatura do Raspberry Pi 4, mas particularmente não vejo problema algum. Dissipe o calor! Hoje é necessário uma fonte de 5V@3A para atender a alimentação do Raspberry, as coisas mudam.

Vou colocar dissipador em ambas, a versão 3B+ e a 4B, depois mostro como farei, não gaste nenhum centavo com dissipadores por enquanto!

Onde comprar o display ST7789?

Esse display pode ser encontrado facilmente na UsinaInfo, através desse link.

Onde comprar o Raspberry Pi 3B?

Para quem procura essa versão, ainda pode recorrer à CurtoCircuito, através desse link.

Onde comprar a Raspberry Pi 3B+?

Um bom lugar para comprar, inclusive com loja física na Santa Efigênia - São Paulo - SP, é a Saravati, através desse link.

Onde comprar a Raspberry Pi 4 4G?

Já chegou um novo lote essa semana e pelo visto está para acabar. As últimas 3 peças estão disponíveis nesse link.

Vídeo com QT MQTT, interface etc

Gravarei à noite para reduzir o ruído ambiente, pretendo mostrar o código e explicar um pouco de todos os recursos utilizados, mas mostrando cada ponto, para tentar deixar mais claro ainda. Se não é inscrito no canal, inscreva em DobitaobyteBrasil no Youtube e clique no sininho para receber notificações.

Download do programa

O programa está disponível para download em meu repositório no github. Acesse-o através desse link.

  Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.