O sistema proposto realiza o monitoramento ambiental no cenário da Internet das Coisas (IoT, do inglês, Internet of Things), no qual implementa o protocolo de troca de mensagens Message Queue Telemetry Transport (MQTT). É divido em 3 módulos: SBC, Interface remota e Interface Local.
O protótipo é capaz de tratar e controlar medidas de temperatura, umidade, pressão atmosférica e luminosidade através de leitura de sensores. Esse módulo é chamado de Single Board Computer (SBC). Além disso, o protótipo possui duas Interface Homem-Máquina (IHM) que apresenta em tempo real as medições de cada sensor, histórico com as 10 últimas medições de cada sensor, além de permitir o ajuste do intervalo de tempo entre as medições. A IHM local utiliza o display LCD e a IHM remota é uma interface desktop em JAVA.
Para o desenvolvimento do SBC utilizou-se o conceito de threads para que fosse possível realizar a leitura de forma simultânea a exibição das informações no display LCD, o envio dos dados medidos para o broker, bem como a alteração do intervalo de tempo na interface. Optou-se por utilizar threads pela facilidade de implementação além de ser viável para a solução deste problema.
Para o desenvolvimento deste protótipo foram utilizados a Raspberry PI 0, o sensor DHT11, dois potenciômetros de 10k Ohm, display LCD 16x2, três botões, a linguagem de programação C e JAVA.
O SBC é o módulo composto pela leitura de umidade e temperatura através do sensor digital DHT11, simulação dos sensores analógicos de luminosidade (BH1750) e pressão atmosférica (BMP180), além de ser responsável pelo gerenciamento desses dados e envio para a interface local (display LCD) e a interface remota.
O SBC possui o fluxo de execução principal (main) e três threads: uma thread para a leitura dos dados dos sensores, uma thread para o envio dos dados para a IHM remota, e outra thread para a exibição no display LCD, dos dados lidos. Na main são criados os clientes MQTT para o envio e recebimento dos dados via o protocolo, configuração da biblioteca WiringPi, inicialização da biblioteca Mosquitto e criação das threads. Para a utilização de threads utilizou-se a biblioteca pthread.h
O sensor de umidade e temperatura, assim como no nosso problema anterior, foi feito com o DHT11. Foi utilizado um código em C em conjunto com a biblioteca WiringPI para conseguirmos receber seus dados como estabelecido no seu protocolo.
O pino selecionado para a comunicação com o sensor foi o GPIO 17 (WiringPi 0) e é nele que recebemos e enviamos todos os sinais relativos ao sensor.
Abaixo mostraremos parte do código que faz o processo inicial do protocolo de recebimento dos dados do DHT11
wiringPiSetup(); // Inicializa a biblioteca
pinMode( DHTPIN, OUTPUT ); // Seleciona um pino e o coloca como Saida
digitalWrite( DHTPIN, LOW ); // Coloca o sinal desse pino para baixo
delay( 20 ); // espera 20 ms
digitalWrite( DHTPIN, HIGH ); // Coloca o sinal desse pino para alto
delayMicroseconds( 50 ); // espera 50 us
pinMode( DHTPIN, INPUT ); // Altera o pino selecionado para Entrada
O código do DHT11 foi retirado do https://github.com/nkundu/wiringpi-examples/blob/master/dht11.c. Poucas alterações substanciais foram feitas no código.
A função de leitura do sensor retorna um vetor de inteiros de tamanho 4, contendo os digitos antes e depois da virgula da temperatura e umidade.
printf( "Umidade = %d.%d %% Temperatura = %d.%d° C\n",dht11_dat[0], dht11_dat[1], dht11_dat[2], dht11_dat[3]);
O sensor de luminosidade e pressão atmosférica foi simulado por dois potenciômetros, que estão no canal 0 e 3 respectivamente da Raspberry Pi utilizada. Para o sensor de luminosidade simulou-se o BH1750 que tem faixa de medição de 1 até 65535 LUX. Já para o sensor de pressão atmosférica, simulou-se o BMP180 que possui faixa de medição de 300 a 1100 hPa.
O potenciômetro gera sinal analógico que é convertido para sinal digital, utilizando o ADS1115 que é um conversor de sinais analógicos I2C. Para essa conversão utilizou-se uma biblioteca criada pelo Embarcados (2019).
Na biblioteca foram utilizados 2 registradores de memória do ADS1115, que são acessados através do Address Pointer Register: Conversion Register que armazena os valores da conversão no formato de 16 bits e oConfig Register para configuração de parâmetros.
A biblioteca configura os parâmetros MUX, PGA e MODE do registrador Config Register, antes de realizar a leitura dos dados. O parâmetro MUX configura o multiplexador para a leitura dos canais. O PGA(Programmable Gain Amplifier) configura a faixa de medição do conversor. O parâmetro MODE corresponde ao bit 8 e configura a forma de operação: conversões continuamente ou uma conversão e aguardo pela leitura. Foi configurado nesse projeto a segunda forma, inserindo o valor 1 no bit 8.
A interface l2C é inicializada com o endereço do barramento I2C /dev/i2c-1, que é passado como parâmetro da função:
int openI2CBus(char *bus);
A configuração do endereço do ADS1115 é através do pino ADDR. Nesse problema, utilizou-se o pino ADDR conectado ao GND, no qual o endereço correspondente é 0x48 em hexadecimal. A função que realiza essa configuração é:
int setI2CSlave(unsigned char endereco);
Por fim, para realizar a leitura e conversão do dado em sinal analógico para digital é utilizado a função readVoltage, que tem como parâmetro o canal do potenciômetro. A conversão do sinal lido é feito da seguinte forma:
voltagem = (float)valorAnalogico*4.096/32767.0;
Para transformar o sinal digital na medida equivalente dos sensores simulados, foi realizado o mapeamento com regra de três composta. A função têm como parâmetros o valor lido e convertido do potenciômetro, valor mínimo e máximo de tensão do potenciômetro e a faixa mínima e máxima de valor do sensor simulado.
A conta realizada para o mapeamento resulta um valor equivalente de uma medida do sensor e é a seguinte:
float fmap(float valorLido, float minPotenciometro, float maxPotenciometro, float minSensor, float maxSensor);
Foi implementado o protocolo MQTT, que é um protocolo de envio e recebimento de mensagens que utiliza um esquema Publisher/Subscriber. Cada subscriber se increve em um "Tópico" e aguarda recebimentos de mensagens, enquanto isso, o publisher envia essas mensagens para os tópicos específicos. O local onde os tópicos se encontram é chamado de Broker, é nele em que nossos Publishers e Subscribers se conectam e fazem a comunicação necessária.
Esse sistema possui 5 tópicos, os quais são exibidos na tabela abaixo. O SBC é editor (publisher) de todos os tópicos e ouvinte (subscriber) apenas do tópico de tempo.
Tópico | Funcionalidade |
monitoramentoAmbiental/temperatura | Valor da temperatura atual |
monitoramentoAmbiental/umidade | Valor da umidade atual |
monitoramentoAmbiental/luminosidade | Valor da luminosidade atual |
monitoramentoAmbiental/pressao | Valor da pressão atmosférica atual |
monitoramentoAmbiental/tempo | Intervalo de tempo entre as medições |
Utilizamos a ferramenta Mosquitto para nos auxiliar e, por isso, toda nossa comunicação de dados de sensores com o broker é feita com o Mosquitto. A biblioteca disponibilizada pelo Mosquitto, a Mosquitto.h, foi usada para implementarmos funções necessárias assim como suas modificação para acomodar as especificidades do nosso problema.
mosquitto_lib_init(); // Inicializa o Mosquitto
mosquitto_connect(); // Se conecta ao Host indicado
mosquitto_publish(); // Publica uma menssagem em um Host indicado para o tópico indicado
mosquitto_subscribe(); // Inscreve seu client em um tópico, assim, receberá atualizações sempre que algo for publicado no tópico
mosquitto_username_pw_set(); // Coloca nome de usuário e senha. Necessário caso o broker requisite essas informações
Abaixo mostraremos exemplos de como funciona nosso código para um Subscriber
mosquitto_lib_init(); // Inicializador da biblioteca Mosquitto
struct mosquitto *mosq; // Criamos uma struct mosquito, ela servirá como base de todas nossas funções Mosquitto
mosq = mosquitto_new("Nome do client mosquitto",true,NULL); // indicamos o ID da nossa sessão mosquitto,
// se ela vai começar com sem dados (True ou False)
// Algum callback específico (não utilizaremos agora)
mosquitto_connect_callback_set(mosq, on_connect); // Opcional. Se quisermos fazer alguma ação quando tentarmos
// nos conectar ao broker, deve ser especificada
// numa função. Essa função é então chamada dentro do connect_callback_set.
// portanto, nesse exemplo, temos uma função chamada on_connect em que ela
// indica o comportamento ao tentarmos nos conectar ao broker.
mosquitto_message_callback_set(mosq, on_message); // Opcional. Se quisermos fazer alguma ação quando recebermos
// alguma mensagem no tópico. A função tem que ser específicada
// assim como no exemplo acima.
mosquitto_username_pw_set(mosq,"aluno","aluno*123"); // Define usuario e senha da sessão MQTT. Importante caso o seu
// broker necessite de autenticação.
mosquitto_connect(mosq,Host,1883,10); // Definimos nossa sessão, o IP do Host, nesse caso o IP do Broker.
// O endereço da porta a se conectar. Utilizamos o padrão do Mosquitto.
// O tempo em segundos que ele tentará se conectar ao broker.
mosquitto_subscribe(mosq,NULL,Topico,1); // Definimos o tópico em que nos inscreveremos
// Assim como o QOS, neste caso, 1
mosquitto_loop_start(mosq); // Iniciamos um loop com a função do mosquitto
getchar(); // Utilizamos o getchar pra caso quisermos sair do loop
// precisamos apenas pressionar alguma tecla
mosquitto_loop_stop(mosq,true); // Encerramos o loop
mosquitto_disconnect(mosq); // Disconectamos a sessão do broker
mosquitto_destroy(mosq); // Destruirmos a sessão mosquitto
mosquitto_lib_cleanup(); // Utilizamos a função da biblioteca para limparmos quaisquer lixo deixados
Abaixo mostraremos exemplo de código para um Publisher
struct mosquitto *mosq;
mosquitto_lib_init();
mosq = mosquitto_new(Nome,true,NULL);
mosquitto_username_pw_set(mosq,"aluno","aluno*123"); //Define usuario e senha
mosquitto_connect(mosq,Host,1883,60);
printf("%s conectou-se ao broker.\n",pub.Nome);
mosquitto_publish(mosq,NULL,Topico,strlen(Msg),Msg,1,true); // A diferença primordial para com o do subscriber
// é essa função. Nela indicamos a nossa sessão mosquitto
// indicamos qual tópico iremos publicar a mensagem
// indicamos o tamanho da mensagem e a própria mensagem
// assim como o QOS, neste caso, 1
mosquitto_disconnect(mosq);
mosquitto_destroy(mosq);
mosquitto_lib_cleanup();
Abaixo mostraremos exemplo de código para o On_Message, que utilizamos para capturar os valores recebidos para o tópicos
void on_message(struct mosquitto *mosq, void *obj, const struct mosquitto_message *msg)
{
intervaloTempo = atof((char *) msg->payload); // Utilizamos o Payload com um cast para Char, assim conseguimos pegar a mensagem enviada nos tópicos.
return;
}
Para facilitar as implementações de publisher e subscribers, criamos structs onde já passamos inicialmente os valores necessários. Abstraindo assim, a complexidade para conseguir se conectar ao Broker, Enviar uma mensagem para um tópico, e receber mensagens de um tópico.
typedef struct Cliente{
char Nome[255];
char Host[255];
char Topico[255];
}Cliente;
//Struct para publisher mqtt
typedef struct Publisher{
char Nome[255];
char Host[255];
char Topico[255];
char Msg[300];
}Publisher;
void publicar(Publisher pub);
void create_client(Cliente client);
Para garantir uma visualização do perfil das medidas do sensoriamento foi estabelecido um historico de dados que armazena as dez ultimas medições realizadas sendo visivel na interface local e remota além da SBC gerar um arquivo de texto com esse historico para garantir a persistencia dos dados
O Historico foi implementado como uma lista estática, usando um vetor de tamanho MAX definido no cabeçalho SBC.h e pelos ponteiros de indice L( ultima medida feita) e O( medida mais velha), foram definidas duas funções para o controle da estrutura do historico são elas ADD e getOrdenada.
ADD insere um novo elemento no vetor na próxima posição para o ponteiro L, a posição do ponteiro L é baseada na ocupação do vetor, ao atingir o tamanho máximo o ponteiro passa para a posição da medida mais antiga assim sempre a medida mais antiga é sobrescrita e o ponteiro O é incrementado para uma próxima posição.
A função getOrdenada criar um vetor do tipo Dados com os valores para o historico mas de forma ordenada da medição mais recente para mais antiga, essa função é usada para fornecer ao display um vetor ordenado para exibir o historico no display de modo sequencial sem nescessidade de ler o historico real na ordem de exibição.
A interface Desktop foi desenvolvida usando a linguagem Java e a biblioteca Paho para a implementação do cliente MQTT.
A interface Desktop comunica-se com o SBC através do protocolo MQTT. No qual é um ouvinte (subscriber) do broker, nos tópicos referentes a temperatura, umidade, luminosidade, pressão, tempo e nos tópicos referentes ao histórico das medições dos sensores. Além disso, a interface é um editor (publisher) no tópico de tempo, para que seja possível determinar o intervalo de tempo dentre as medições dos sensores.
Para armazenar os dados de cada sensor com consistência, foi criado uma classe DadoSensores no qual implementa o padrão singleton, possuindo assim uma única instância da classe. A classe possui os atributos do tipo String para as medidas do sensor, para o intervalo de tempo e armazena o histórico de cada sensor em ArrayList.
Quando uma mensagem é publicada nos tópicos, o ouvinte MQTT no método messageArrived verifica em qual tópico a mensagem foi publicada e salva o dado recebido no atributo correspondente na classe DadoSensores. Essa verificação é realizada utilizando a estrutura condicional Switch case.
Por exemplo, se a mensagem foi publicada no tópico "monitoramentoAmbiental/luminosidade", o dado recebido será salvo alterando o valor do atributo luminosidade da classe DadoSensores.
O histórico é armazenado em um arrayList e há um array para cada medição. Após um dado ser recebido e salvo, verifica-se se o arrayList do histórico de medição já tem 10 itens, se sim, remove o último elemento. Sempre que um novo dado é recebido, sempre é adicionado no início do arrayList.
Já a publicação no tópico "monitoramentoAmbiental/tempo" acontece sempre. Na interface há um campo para que o usuário insira o intervalo de tempo que deseja. Esse valor inserido na interface é convertido para String e em seguida para byte, e passado como parâmetro no método que realiza a publicação no tópico. É interessante ressaltar que o método publicar da biblioteca Paho tem como parâmetro o tópico, o dado a ser publicado no tipo Byte e a qualidade de serviço(0,1 ou 2).
public void publicar(String topico, byte[] informacao, int qos);
Como a interface implementa o padrão MVC, há um controlador que é responsável pela troca de informações entre a model e a view. Sendo assim, é nele que são criados os clientes MQTT editor e ouvintes de cada tópico. Os clientes MQTT conectam-se ao broker 10.0.0.101 com o usuário aluno e senha aluno*123. Esse broker foi criado em uma rede local, mas caso deseja-se é possível alterar o endereço para um broker disponível na internet, assim como o usuário e senha.
A interface possui duas telas, uma para a exibição dos dados dos sensores e outra para a exibição do histórico das medições de cada sensor. As telas implementam o Runnable, e ficam sempre atualizando os valores do campos exibidos, além de ficar sempre enviando o valor do campo de alterar o intervalo de tempo entre as medições.
Ao executar o programa, será exibida uma tela de monitoramento de todos os sensores: temperatura, umidade, luminosidade e pressão atmosférica. Os dados exibidos correspondem aos mais recentes enviados pelo SBC. Além disso, exibe a informação do intervalo de tempo em que as medições estão sendo realizadas.
Para alterar o tempo das medições, o usuário deve inserir o valor no campo abaixo dos dados dos sensores. Nesta mesma tela há um botão “Histórico de medições”, que ao ser clicado direciona para a tela contendo os históricos de cada sensor.
Na tela de histórico, são exibidos o histórico de cada sensor, bem como a data e hora em que cada medida foi realizada. Para voltar para a tela de monitoramento, é necessário que o usuário clique o botão “Voltar”, que está no canto inferior esquerdo.
Para o acesso e visualização dos dados dos sensores no SBC, foi construída uma interface homem máquina local que viabiliza o usuário interagir com o SBC.
A interface foi construída usando um display LCD 16x2, e push buttons. Para o controle do display LCD foi utilizada a biblioteca LCD.h e para uso dos botões e GPIO da Raspberry foi utilizada a biblioteca wiringPi. As bibliotecas definem as funções para a comunicação com o dispositivo
A interface local exibe os dados de temperatura, umidade, pressão atmosférica e luminosidade, com o histórico das dez últimas medições de cada sensor, além do ajuste do tempo de medição.
A exibição dos dados do display foi organizada em menus, no qual cada medida possui um menu e um submenu. No menu é exibida a última medição realizada e um opção para acessar o histórico ao pressionar o botão 2. Dentro do submenu do histórico são exibidos dois dados do histórico, um em cada linha, ao pressionar o botão 1 os dados são alternados indo dos dois primeiros aos dois últimos e ao pressionar o botão 2 o display volta ao menu anterior que estava. Para alternar entre os menus, o botão 1 deve ser pressionado permitindo que sejam exibidos 5 menus diferentes. O último menu é o de alteração do tempo de sensoriamento, que será discutido abaixo.
O menu de alterar o tempo de sensoriamento exibe o tempo atual do intervalo de medição e a opção para alterá-lo ao pressionar o botão 2. No submenu de alterar o tempo de medição, são exibidos 7 dígitos que podem ser incrementados de 0-9 para definir o novo intervalo. Os valores para cada dígito vão de 0 até 9 de modo circular. Os dois primeiros dígitos correspondem ao valor de minutos, o terceiro e quarto dígitos correspondem ao tempo em segundos e os três últimos ao tempo em milisegundos. Para alternar entre os dígitos, o botão 1 é usado, para incrementar o digito o botão 2 é pressionado. Para sair do submenu e alterar o tempo de medição o botão 3 deve ser pressionado.
A lógica de controle do display foi implementada como uma máquina de estados. Para cada estado listado, uma configuração diferente é exibida no display, com essa abordagem pode-se garantir uma progressão lógica na exibição da interface e a facilidade de inserir novos estados se fossem necessários.
A variável estado guarda qual o estado atual do display, e estes estados estão definidos no arquivo SBC.h. A implementação da máquina de estados seguiu a abordagem com o uso do switch-case, assim, em cada case do switch as instruções para o display são definidas. A lógica da mudança de estado é feita com base no estado atual e com os botões. Nos menus e submenus as transições ocorrem quando os botões B1 e B2 são pressionados, apenas com a exceção do submenu de alterar tempo em que só ocorre uma transição de estado quando o botão 3 é pressionado.
O programa da interface é rodado numa thread em que possui um loop que executa infinitamente o switch-case.
Ao final do loop é verificado se um botão foi pressionado. Se algum botão é pressionado o display é limpo para escrita do novo estado.
if(!b0 || !b1 || !b2){ //Limpa o display se algum botão foi pressionado
lcdClear(lcd);
}
O switch verifica qual o case para o estado atual e então imprime no display os dados para a configuração atual (funções lcdPrint e lcdPosition).
No fim de cada case há um trecho que verifica a lógica da mudança do próximo estado, baseado no estado atual e em qual botão foi pressionado, e desta forma a variável estado é atualizada para o próximo estado.
Durante o desenvolvimento, para testar a interface remota utilizou-se um cliente mqtt para enviar dados simulados a fim de verificar se o valor enviado realmente era o exibido na interface, bem como, para verificar se o valor enviado era separado na medição atual e o na medição contendo data e hora que seria exibida no histórico. Verificou-se que os dados recebidos correspondiam aos que estavam sendo enviados.
Para a configuração do intervalo de tempo através da interface remota, inseriu-se valores decimais e inteiros e foi conferindo na interface local se o valor recebido era o enviado. Para o caso em que o usuário insere 0, realizamos o tratamento para que não seja enviado para o broker. Em relação a inserção de valores decimais, a nossa interface aceita valores com os separadores decimais "," e ".".
Para simular os sensores de pressão atmosferica e luminosidade foram utilizados dois potenciomêtros fornecendo uma leitura analogica que é convertida em dado digital pelo conversor analogico digital ads1115 os testes para esse módulo foram realizados individualmente, foi criada uma rotina c que realizava a leitura dos canais 0 e 3 do ads1115 e os valores exibidos no terminal, analogamente as medias de tensão do potenciometrô foram tomadas com auxilio de um multimetro, em resumo a faixa de leitura dos potenciometros foram mapeadas por três vezes.
A interface local (display LCD e botões) foi testada por repetição de ações simulando casos de uso:acessar menu de temperatura, acessar menu de umidade, acessar historico de temperatura,altenar historico, alternar data e hora no historico entre outros. os casos de uso foram repetidos com uma pessoa simulando um usuario interagindo com os botões e menus a fim de indentificar os possíveis erros.
Caso de Testes DHT11:
Sensor Ausente - Com o nosso código executando, nós removemos o sensor DHT11.
Resultado Esperado: O Código aguarda o sensor ser encontrado novamente. Enquanto espera, o código não faz nenhuma leitura de nenhum outro sensor.
Dado incorreto pelo sensor - O sensor envia dados incorretos (Sabemos pela verificação do checksum)
Resultado Esperado: O sensor repete a leitura do sensor até que ele retorne um resultado válido.
Sensor em perfeito estado - O sensor está presente e funciona corretamente.
Resultado Esperado: O sensor ler os dados de temperatura e umidade corretamente.
Exemplos utilizando WiringPi. Disponível em: https://github.com/nkundu/wiringpi-examples, acesso em 25/05/2022.
Biblioteca exemplo do ADS1115. Disponível em: https://www.embarcados.com.br/raspberry-pi-e-ads1115/, acesso em 25/05/2022.
Para executar a interface desktop é necessário ter o Java instalado na máquina. Dentro da pasta MonitoramentoAmbiental->dist abra o terminal e execute o comando:
java -jar MonitoramentoAmbiental.jar
Os arquivos da pasta SBC devem ser salvos na Raspberry. Dentro da pasta contendo os arquivos baixados, execute no terminal os comandos:
make
sudo ./sbc