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.
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 Dash para Android.
Configurei um username e password 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 dial e progressBar 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.sh e 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 comando vcgencmd 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.
1 comentário
Comments are closed, but trackbacks and pingbacks are open.