Automação Residencial

Sistema de Monitoramento de Qualidade do Ar com ESP32 e Aplicativo Flutter

Eletrogate 7 de agosto de 2025

Introdução

Neste post, vamos desenvolver um sistema completo de monitoramento da qualidade do ar que combina hardware e software. Utilizaremos o sensor MQ-7, capaz de detectar a concentração de monóxido de carbono no ambiente, conectado a um microcontrolador ESP32, que possui capacidades de conectividade WiFi e Bluetooth. Os dados coletados serão enviados para a internet através do protocolo MQTT (Message Queuing Telemetry Transport), um protocolo leve e eficiente para comunicação entre dispositivos IoT (Internet das Coisas).

Para visualizar esses dados, criaremos um aplicativo móvel utilizando o framework Flutter, desenvolvendo um APK para dispositivos Android. Esse aplicativo se conectará ao mesmo broker MQTT para receber e exibir os dados em tempo real.

Veja a estrutura do projeto a baixo:

Fonte: Autor


Circuito

Componentes Necessários

Para realizar este projeto, você precisará dos seguintes componentes:

Esquemático de Montagem

O MQ-7 é um sensor analógico que pode ser facilmente conectado ao ESP32. Vamos utilizar as seguintes conexões:

Fonte: Autor

Observe que o MQ-7 funciona geralmente em 5 V por isso criamos um divisor de tensão com dois resistores para baixar a tensão de entrada, permitindo uma leitura correta pelo ESP 32 considerando que ele opera em 3.3 V.


Conhecendo as tecnologias usadas

Funcionamento do Sensor MQ-7

O MQ-7 é um sensor eletroquímico que contém um elemento sensível composto de dióxido de estanho (SnO2), cuja resistência varia quando exposto ao monóxido de carbono (CO). O sensor possui um aquecedor interno que opera em dois ciclos de temperatura: um ciclo de alta temperatura (em torno de 450°C) para a limpeza do sensor e um ciclo de baixa temperatura (cerca de 50°C) para a detecção do CO.

Quando o monóxido de carbono entra em contato com o elemento sensível, ocorre uma reação química que altera sua resistência elétrica. Essa variação é convertida em um sinal analógico que pode ser lido pelo ESP32.

O MQ-7 precisa de um período de aquecimento (geralmente 24-48 horas para calibração completa, mas 20-30 minutos para uso básico) para estabilizar suas leituras. Durante o desenvolvimento, é recomendável deixar o sensor ligado por pelo menos 20 minutos antes de confiar nas medições.

Como faremos a comunicação

Usaremos o MQTT (Message Queuing Telemetry Transport) que é um protocolo de comunicação projetado para dispositivos IoT, conhecido por sua leveza e eficiência no uso de rede. Para entender como ele funciona, é importante conhecer seus principais componentes:


1. Broker (Servidor Central)

O broker é o coração da comunicação MQTT. Ele atua como um intermediário entre os dispositivos, gerenciando o fluxo de mensagens.

Principais responsabilidades do broker:

  • Receber mensagens de dispositivos que publicam dados (publicadores).
  • Distribuir essas mensagens para dispositivos que solicitaram recebê-las (assinantes).
  • Gerenciar tópicos, garantindo que apenas os dispositivos corretos recebam as mensagens.
  • Aplicar regras de autenticação e segurança.

Exemplos de brokers populares: EMQX, Mosquitto, HiveMQ, RabbitMQ.


2. Cliente MQTT (Publicador e Assinante)

Os clientes MQTT são os dispositivos conectados ao broker e podem atuar de duas formas:

Publicador (Publisher):

  • Envia mensagens para um tópico específico no broker.
  • Exemplo: Um sensor de temperatura publica valores para o tópico sensor/temperatura.

Assinante (Subscriber):

  • Se inscreve em um ou mais tópicos para receber mensagens.
  • Exemplo: Um aplicativo móvel assina o tópico sensor/temperatura para monitorar os valores enviados pelo sensor.

Importante: Um mesmo cliente pode ser publicador e assinante ao mesmo tempo!


3. Tópicos e Mensagens

O MQTT organiza a comunicação em tópicos, que funcionam como “endereços” para as mensagens.

Exemplo de estrutura de tópicos:

casa/sala/temperatura
casa/quarto/umidade
fabrica/motor1/status

Os assinantes podem ouvir mensagens de um tópico específico ou de um grupo deles usando curingas.


4. Qualidade de Serviço (QoS – Quality of Service)

O MQTT permite configurar o nível de garantia na entrega de mensagens:

QoS 0 (Entrega pelo melhor esforço – “At Most Once”)

  • A mensagem é enviada sem garantia de entrega.
  • Se houver falha na transmissão, a mensagem pode ser perdida.

QoS 1 (Entrega garantida – “At Least Once”)

  • O broker confirma o recebimento da mensagem.
  • O publicador pode enviar a mesma mensagem mais de uma vez caso não receba a confirmação.

QoS 2 (Entrega única – “Exactly Once”)

  • A mensagem é entregue exatamente uma vez.
  • Método mais seguro, mas com maior consumo de processamento e rede.

5. Retained Messages e Last Will

Retained Message:

  • O broker armazena a última mensagem publicada em um tópico e entrega para novos assinantes.
  • Útil para dados como estado de um dispositivo ou última leitura de um sensor.

Last Will and Testament (LWT):

  • Se um dispositivo se desconectar inesperadamente, o broker pode enviar uma mensagem predefinida para avisar outros clientes.
  • Exemplo: Se um monitor de temperatura cair, o broker pode enviar uma mensagem no tópico alertas/sensores dizendo: “Sensor desconectado”.

E qual broker usaremos?

Para testar e desenvolver aplicações com MQTT, utilizaremos o broker.emqx.io, um servidor gratuito oferecido pela EMQX. Esse broker é amplamente utilizado por desenvolvedores para prototipagem e testes, pois:

  • É um serviço gratuito e público, facilitando testes sem precisar configurar uma infraestrutura própria.
  • Suporta conexões seguras (TLS), aumentando a segurança da comunicação.
  • Tem alta compatibilidade com diferentes clientes MQTT, incluindo ESP32, Raspberry Pi, aplicações web e mobile.
  • É mantido por uma comunidade ativa e uma empresa especializada, garantindo estabilidade e atualizações constantes.

Alternativas para um sistema escalável

Embora o broker.emqx.io seja excelente para testes, seu uso em produção pode ser limitado, pois é um serviço público e pode sofrer instabilidades devido ao grande volume de acessos. Para sistemas que exigem maior controle, escalabilidade e segurança, algumas alternativas são:

  1. Hospedar seu próprio broker MQTT
    • Softwares como EMQX, Mosquitto e HiveMQ permitem que você configure seu próprio servidor MQTT em um servidor dedicado ou na nuvem.
    • Oferece total controle sobre segurança, desempenho e configuração de qualidade de serviço (QoS).
  2. Usar brokers comerciais na nuvem
    • Serviços como AWS IoT Core, Azure IoT Hub e Google Cloud IoT oferecem MQTT como parte de suas soluções, integrando-se com outras ferramentas de análise de dados, machine learning e monitoramento.
    • São altamente escaláveis e fornecem suporte a milhões de dispositivos conectados simultaneamente.
  3. Brokers MQTT privados gerenciados
    • Plataformas como EMQX Cloud e HiveMQ Cloud oferecem brokers gerenciados na nuvem, combinando facilidade de uso com alta disponibilidade.
    • Permitem configurar segurança avançada, autenticação e autorização para proteger os dados transmitidos.


Código para o ESP 32

Agora que entendemos mais sobre o que usaremos para nos comunicar vamos programar o ESP32 para ler os dados do sensor e enviá-los para um broker MQTT. Utilizaremos o Arduino IDE para programar o ESP32, então certifique-se de ter instalado a placa ESP32 em sua IDE e duas bibliotecas inportantes: PubSubClient – biblioteca de comunicação para o protocolo MQTT MQUnifiedsensor – Biblioteca de interpretação dos sensores MQ

Para instalar as bibliotecas na IDE do Arduino pesquise pelo nome dela no menu de bibliotecas e faça sua instalação.

 

Fonte: Autor

 

Fonte: Autor

Código

#include <WiFi.h>
#include <PubSubClient.h>
#include <MQUnifiedsensor.h>

// Configurações de WiFi e MQTT
const char* WIFI_SSID = "REDE"; //Coloque o nome de sua rede wifi aqui
const char* WIFI_PASSWORD = "SENHA"; //Coloque sua senha wifi aqui
const char* MQTT_TOPIC = "esp32/air_quality"; //Coloque aqui o tópico
const char* MQTT_CLIENT_ID = "esp32_air_monitor"; //Coloque aqui seu ID
const char* MQTT_BROKER = "broker.emqx.io";
const int MQTT_PORT = 1883;

// Configurações do Sensor MQ-7
#define SENSOR_PLACA "ESP-32"
#define SENSOR_VOLTAGE_RESOLUTION 3.3
#define SENSOR_ANALOG_PIN 32
#define SENSOR_TYPE "MQ-7"
#define SENSOR_ADC_RESOLUTION 12
#define SENSOR_CLEAN_AIR_RATIO 27.5
#define SENSOR_PWM_PIN 5

// Intervalos de tempo para ciclos de aquecimento e leitura
const unsigned long HEATING_HIGH_DURATION = 60 * 1000;  // 60 segundos em 5V
const unsigned long HEATING_LOW_DURATION = 90 * 1000;   // 90 segundos em 1.4V
const unsigned long PUBLISH_INTERVAL = 5000;            // 5 segundos entre publicações

// Objetos globais
MQUnifiedsensor MQ7(SENSOR_PLACA, SENSOR_VOLTAGE_RESOLUTION, SENSOR_ADC_RESOLUTION, SENSOR_ANALOG_PIN, SENSOR_TYPE);
WiFiClient espClient;
PubSubClient mqttClient(espClient);

unsigned long lastPublishTime = 0;
unsigned long heatingStartTime = 0;
bool isHighVoltageHeating = true;

void setupWiFi() {
  Serial.println("\nConectando ao WiFi...");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("\nWiFi conectado!");
  Serial.print("Endereço IP: ");
  Serial.println(WiFi.localIP());
}

void reconnectMQTT() {
  while (!mqttClient.connected()) {
    Serial.print("Tentando conectar ao broker MQTT... ");
    
    if (mqttClient.connect(MQTT_CLIENT_ID)) {
      Serial.println("Conectado!");
    } else {
      Serial.print("Falha. Código: ");
      Serial.println(mqttClient.state());
      delay(5000);
    }
  }
}

void calibrateSensor() {
  Serial.print("Calibrando sensor MQ-7... ");
  
  float calcR0 = 0;
  for(int i = 1; i <= 10; i++) {
    MQ7.update();
    calcR0 += MQ7.calibrate(SENSOR_CLEAN_AIR_RATIO);
    Serial.print(".");
  }
  
  MQ7.setR0(calcR0 / 10);
  Serial.println(" Concluído!");

  // Verificações de erro na calibração
  if (isinf(calcR0)) {
    Serial.println("Erro: Circuito aberto! Verifique o cabeamento.");
    while(1);
  }
  
  if (calcR0 == 0) {
    Serial.println("Erro: Curto-circuito no pino analógico!");
    while(1);
  }
}

void setupSensor() {
  // Configuração do modelo matemático de PPM
  MQ7.setRegressionMethod(1); // _PPM = a * ratio^b
  MQ7.setA(99.042);
  MQ7.setB(-1.518);

  // Inicialização do sensor
  MQ7.init();
  pinMode(SENSOR_PWM_PIN, OUTPUT);

  // Calibração
  calibrateSensor();
}

void setup() {
  Serial.begin(115200);
  
  // Configuração WiFi e MQTT
  setupWiFi();
  mqttClient.setServer(MQTT_BROKER, MQTT_PORT);

  // Configuração do sensor
  setupSensor();

  // Marca o início do aquecimento
  heatingStartTime = millis();
}

void loop() {
  // Garante conexão MQTT
  if (!mqttClient.connected()) {
    reconnectMQTT();
  }
  mqttClient.loop();

  unsigned long currentTime = millis();

  // Ciclo de aquecimento e leitura do sensor MQ-7
  if (isHighVoltageHeating) {
    analogWrite(SENSOR_PWM_PIN, 255); // 5V
    
    if (currentTime - heatingStartTime >= HEATING_HIGH_DURATION) {
      isHighVoltageHeating = false;
      heatingStartTime = currentTime;
    }
  } else {
    analogWrite(SENSOR_PWM_PIN, 20); // 1.4V
    
    if (currentTime - heatingStartTime >= HEATING_LOW_DURATION) {
      isHighVoltageHeating = true;
      heatingStartTime = currentTime;
    }
  }

  // Leitura e publicação a cada 5 segundos
  if (currentTime - lastPublishTime >= PUBLISH_INTERVAL) {
    lastPublishTime = currentTime;

    MQ7.update();
    float ppm = MQ7.readSensor();

    // Criar JSON com dados
    String jsonPayload = "{\"sensor\":\"MQ-7\",\"ppm\":" + String(ppm) + 
                         ",\"heating\":" + String(isHighVoltageHeating ? 5 : 1.4) + "}";

    Serial.print("Publicando: ");
    Serial.println(jsonPayload);

    // Publicar no tópico MQTT
    mqttClient.publish(MQTT_TOPIC, jsonPayload.c_str());
  }
}

Nosso código passa pelos seguintes processos:

  1. Conecta-se à rede WiFi configurada
  2. Conecta-se ao broker MQTT público (broker.emqx.io)
  3. Lê o valor analógico do sensor MQ-7
  4. Converte o valor lido para uma estimativa de PPM (partes por milhão)
  5. Cria uma mensagem JSON com os dados coletados
  6. Publica a mensagem no tópico MQTT definido
  7. Repete o processo a cada 5 segundos

Se atente para inserir no código a sua rede wifi, senha, nome desejado para o tópico no broker e ID escolhido.

Ao fazer o upload do código no ESP32 e abrir o monitor serial será exibido as informações de conexão com a internet, com o broker e a publicação dos dados no tópico, use essas informações para saber se o sistema esta rodando como planejado.

Aqui já usamos algumas outras tecnologias como o protocolo de rede Wifi para fazer a conexão via internet dos serviços, o arquivo JSON que é um formato comum de representação de dados para comunicação de programas e agora vamos continuar expandindo as tecnologias para a parte de aplicativos moveis.


Criando o app de monitoramento com Flutter

Preparando o Ambiente de Desenvolvimento

Passo 1: Instalando o Android Studio

O ambiente em que será desenvolvido o app é o Android Studio, existem outros com diversas configurações diferentes, mas para introduzir o desenvolvimento de apps esse é um bom ambiente.

  1. Acesse o site oficial do Android Studio: https://developer.android.com/studio
  2. Faça o download da última versão para seu sistema operacional
  3. Execute o instalador:
    • Aqui usaremos Windows: Execute o arquivo .exe
Configurações Iniciais do Android Studio
  • Durante a instalação, selecione “Standard” setup e basta aceitar a instalação padrão e os termos.

Passo 2: Instalando o Flutter

O Flutter é um framework de desenvolvimento multiplataforma criado pela Google, utilizado para criar aplicativos móveis, web e desktop a partir de uma única base de código para usar ele seguiremos os passos:

  1. Baixe o Flutter SDK: https://docs.flutter.dev/get-started/install/windows/mobile
  2. Extraia o arquivo zip para um diretório sem espaços (Ex: C:\\flutter)
  3. Adicione o Flutter ao PATH do sistema:
    • Abra “Variáveis de Ambiente”
    • Em “Variáveis do Sistema”, edite a variável PATH
    • Adicione o caminho: C:\\flutter\\bin

Passo 3: Verificando a Instalação

No terminal, execute: flutter doctor

Este comando verificará sua instalação e mostrará quaisquer dependências faltantes.

Geralmente nesta etapa as dependências necessárias para este projeto já estão instaladas.

Criando o Projeto Flutter

Configurando o Projeto

Na pasta onde você desenvolverá o projeto rode  no terminal:

flutter create nome_do_seu_projeto
cd nome_do_seu_projeto

Agora temos um projeto flutter criado, vamos abrir ele no Android Studio.

  • Abra o Android Studio
  • Selecione “Open” ou Abrir e busque a pasta que foi feita no passo anterior

Fonte: Autor

Configurando Dependências

Acesse a pasta do projeto flutter e busque o arquivo

pubspec.yaml neste arquivo temos as seguintes dependências que são importantes para rodar o projeto, copie esse código e substitua.

name: air_quality_monitor
description: "A new Flutter Eletrogate app."
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: ^3.7.0

dependencies:
  flutter:
    sdk: flutter

  
  flutter_hooks: ^0.20.5

  # Ícones no estilo iOS
  cupertino_icons: ^1.0.8

  # Cliente MQTT para comunicação com dispositivos IoT
  mqtt_client: ^9.8.1

  # Gerenciamento de estado simplificado
  provider: ^6.1.2

  # Biblioteca para lidar com conversão de JSON
  json_serializable: ^6.7.1

  # Gerenciamento de logs na aplicação
  logger: ^2.2.0

  # Para requisições HTTP e comunicação com APIs
  http: ^1.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  # Lints recomendadas para boas práticas de código
  flutter_lints: ^5.0.0

  # Ferramentas para geração de código a partir de anotações
  build_runner: ^2.4.7

flutter:
  uses-material-design: true

Agora vamos instalar as dependências necessárias, abra o terminal clicando no ícone inferior:

Fonte: Autor

Instale as dependências rodando o seguinte comando no terminal: 
flutter pub get

 


Desenvolvendo o Aplicativo

Dentro da pasta do app tem uma pasta chamada lib, dentro desta pasta tem um arquivo chamado main.dart acesse ele, apague o código existente e cole este código.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'dart:convert';
import 'package:provider/provider.dart';
import 'package:logger/logger.dart';

// Inicialização do logger para registro de eventos no aplicativo
final logger = Logger(
  printer: PrettyPrinter(methodCount: 0, lineLength: 80),
);

void main() {
  // Ponto de entrada do aplicativo Flutter
  runApp(
    // Utilizando ChangeNotifierProvider para gerenciar o estado global do app
    ChangeNotifierProvider(
      create: (_) => AirQualityModel(),
      child: const MyApp(),
    ),
  );
}

// Modelo para gerenciar os dados do sensor de qualidade do ar
class AirQualityModel extends ChangeNotifier {
  Map<String, dynamic> _sensorData = {"ppm": 0.0};
  bool _isConnected = false;
  String _connectionStatus = "Desconectado";

  Map<String, dynamic> get sensorData => _sensorData;
  bool get isConnected => _isConnected;
  String get connectionStatus => _connectionStatus;

  void updateSensorData(Map<String, dynamic> data) {
    _sensorData = data;
    notifyListeners();
  }

  void setConnectionStatus(bool connected, String status) {
    _isConnected = connected;
    _connectionStatus = status;
    notifyListeners();
  }

  // Método para interpretar a qualidade do ar com base no valor de PPM
  String getAirQualityStatus() {
    final ppm = _sensorData["ppm"] as double;
    if (ppm < 9) return "Excelente";
    if (ppm < 50) return "Boa";
    if (ppm < 100) return "Moderada";
    if (ppm < 300) return "Ruim";
    return "Perigosa";
  }

  // Método para determinar a cor que representa a qualidade do ar
  Color getStatusColor() {
    final ppm = _sensorData["ppm"] as double;
    if (ppm < 9) return Colors.green;
    if (ppm < 50) return Colors.lightGreen;
    if (ppm < 100) return Colors.yellow;
    if (ppm < 300) return Colors.orange;
    return Colors.red;
  }
}

// Widget principal do aplicativo
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false, // Remove o banner de debug
      title: 'Monitor de Qualidade do Ar',
      theme: ThemeData(
        // Definição do tema do aplicativo
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.light,
        ),
        useMaterial3: true, // Usando Material Design 3
        fontFamily: 'Roboto',
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.transparent,
          elevation: 0,
          centerTitle: true,
          foregroundColor: Colors.blue,
        ),
        cardTheme: CardTheme(
          elevation: 8,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
        ),
      ),
      darkTheme: ThemeData(
        // Tema escuro do aplicativo
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
        fontFamily: 'Roboto',
        cardTheme: CardTheme(
          elevation: 8,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
        ),
      ),
      themeMode: ThemeMode.system, // Segue o tema do sistema
      home: const SensorScreen(),
    );
  }
}

// Tela principal do sensor utilizando Hooks para gerenciamento de estado local
class SensorScreen extends HookWidget {
  const SensorScreen({super.key});

  // Configurações do broker MQTT
  final String broker = "broker.emqx.io"; // Servidor MQTT público para teste
  final int port = 1883; // Porta padrão para MQTT sem TLS
  final String clientId = "flutter_air_monitor"; // Identificador único do cliente
  final String topic = "esp32/air_quality"; // Tópico para inscrição

  @override
  Widget build(BuildContext context) {
    // Usando hooks para memorizar o cliente MQTT e evitar recriação
    final client = useMemoized(() => MqttServerClient(broker, clientId), []);
    
    // Efeito que executa na montagem do widget
    useEffect(() {
      // Conecta ao broker MQTT quando o widget é inicializado
      _connectMQTT(client, context);
      
      // Função de limpeza executada na desmontagem do widget
      return () {
        logger.i("Desconectando do broker MQTT");
        client.disconnect();
      };
    }, []);

    // Obtém o modelo de dados do Provider
    final model = Provider.of<AirQualityModel>(context);
    
    return Scaffold(
      // AppBar com design moderno e transparente
      appBar: AppBar(
        title: const Text(
          "Qualidade do Ar",
          style: TextStyle(fontWeight: FontWeight.bold),
        ),
        actions: [
          // Indicador de status de conexão
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              children: [
                Icon(
                  model.isConnected ? Icons.wifi : Icons.wifi_off,
                  color: model.isConnected ? Colors.green : Colors.red,
                ),
                const SizedBox(width: 8),
                Text(model.connectionStatus, 
                    style: TextStyle(
                      color: model.isConnected ? Colors.green : Colors.red,
                      fontSize: 12,
                    )),
              ],
            ),
          ),
        ],
      ),
      // Corpo principal com layout responsivo
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(20),
            child: Column(
              children: [
                // Card principal com informações do sensor
                _buildMainSensorCard(context, model),
                
                const SizedBox(height: 24),
                
                // Informações adicionais sobre o dispositivo
                _buildDeviceInfoCard(context),
                
                const SizedBox(height: 24),
                
                // Card com informações sobre qualidade do ar
                _buildAirQualityInfoCard(context),
              ],
            ),
          ),
        ),
      ),
    );
  }

  // Card principal com a leitura do sensor
  Widget _buildMainSensorCard(BuildContext context, AirQualityModel model) {
    final ppm = model.sensorData["ppm"] as double;
    final quality = model.getAirQualityStatus();
    final statusColor = model.getStatusColor();
    
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // Ícone animado que muda de cor conforme a qualidade do ar
            TweenAnimationBuilder<double>(
              tween: Tween<double>(begin: 0, end: ppm),
              duration: const Duration(seconds: 1),
              builder: (context, value, child) {
                return Column(
                  children: [
                    Icon(
                      CupertinoIcons.wind, 
                      size: 80, 
                      color: statusColor,
                    ),
                    const SizedBox(height: 16),
                    Text(
                      "Monóxido de Carbono (CO)",
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 24),
                    Text(
                      "${value.toStringAsFixed(1)} ppm",
                      style: Theme.of(context).textTheme.headlineLarge?.copyWith(
                        fontWeight: FontWeight.bold,
                        color: statusColor,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Container(
                      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                      decoration: BoxDecoration(
                        color: statusColor.withOpacity(0.2),
                        borderRadius: BorderRadius.circular(20),
                      ),
                      child: Text(
                        "Qualidade: $quality",
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          color: statusColor,
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }

  // Card com informações sobre o dispositivo
  Widget _buildDeviceInfoCard(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              "Informações do Dispositivo",
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            _buildInfoRow(context, "Sensor", "MQ-7 (Monóxido de Carbono)"),
            _buildInfoRow(context, "Dispositivo", "ESP32"),
            _buildInfoRow(context, "Localização", "Sala Principal"),
            _buildInfoRow(context, "Última Atualização", "Agora"),
          ],
        ),
      ),
    );
  }

  // Linha de informação para os cards
  Widget _buildInfoRow(BuildContext context, String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            label,
            style: TextStyle(
              color: Theme.of(context).colorScheme.secondary,
              fontWeight: FontWeight.w500,
            ),
          ),
          Text(
            value,
            style: const TextStyle(fontWeight: FontWeight.bold),
          ),
        ],
      ),
    );
  }

  // Card com informações sobre qualidade do ar
  Widget _buildAirQualityInfoCard(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              "Níveis de CO e Significado",
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            _buildQualityRow(context, "0 - 9 ppm", "Excelente", Colors.green),
            _buildQualityRow(context, "9 - 50 ppm", "Boa", Colors.lightGreen),
            _buildQualityRow(context, "50 - 100 ppm", "Moderada", Colors.yellow),
            _buildQualityRow(context, "100 - 300 ppm", "Ruim", Colors.orange),
            _buildQualityRow(context, "> 300 ppm", "Perigosa", Colors.red),
          ],
        ),
      ),
    );
  }

  // Linha de informação para o card de qualidade do ar
  Widget _buildQualityRow(BuildContext context, String range, String quality, Color color) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Container(
            width: 16,
            height: 16,
            decoration: BoxDecoration(
              color: color,
              shape: BoxShape.circle,
            ),
          ),
          const SizedBox(width: 12),
          Text(
            range,
            style: const TextStyle(fontWeight: FontWeight.w500),
          ),
          const Spacer(),
          Text(
            quality,
            style: TextStyle(
              fontWeight: FontWeight.bold,
              color: color,
            ),
          ),
        ],
      ),
    );
  }

  // Método para conectar ao broker MQTT
  Future<void> _connectMQTT(
      MqttServerClient client, BuildContext context) async {
    final model = Provider.of<AirQualityModel>(context, listen: false);
    
    // Configuração do cliente MQTT
    client.logging(on: true); // Ativa logs para debug
    client.port = port;
    client.keepAlivePeriod = 60; // Período para manter conexão ativa
    
    // Callbacks para eventos de conexão
    client.onConnected = () {
      logger.i("MQTT Conectado ao broker $broker");
      model.setConnectionStatus(true, "Conectado");
    };
    
    client.onDisconnected = () {
      logger.w("MQTT Desconectado");
      model.setConnectionStatus(false, "Desconectado");
    };
    
    client.onSubscribed = (String topic) {
      logger.i("Inscrito no tópico: $topic");
    };

    // Mensagem de conexão MQTT
    final connMessage = MqttConnectMessage()
        .withClientIdentifier(clientId) // ID único do cliente
        .startClean() // Inicia uma sessão limpa
        .withWillQos(MqttQos.atMostOnce); // Qualidade de serviço
    client.connectionMessage = connMessage;

    try {
      // Tentativa de conexão ao broker MQTT
      logger.i("Conectando ao broker MQTT: $broker:$port");
      await client.connect();
    } catch (e) {
      logger.e("Erro na conexão MQTT: $e");
      model.setConnectionStatus(false, "Erro: $e");
      client.disconnect();
      return;
    }

    // Verifica se o cliente está conectado
    if (client.connectionStatus!.state == MqttConnectionState.connected) {
      logger.i("Cliente conectado");
      
      // Inscreve-se no tópico para receber dados
      client.subscribe(topic, MqttQos.atMostOnce);

      // Configura listener para mensagens recebidas
      client.updates!.listen((List<MqttReceivedMessage<MqttMessage?>>? messages) {
        if (messages == null) return;
        
        final MqttPublishMessage recMessage = messages[0].payload as MqttPublishMessage;
        
        // Converte bytes da mensagem para string
        final String payload = MqttPublishPayload.bytesToStringAsString(
          recMessage.payload.message
        );

        logger.d("Mensagem recebida: $payload");

        try {
          // Decodifica o JSON recebido
          final data = jsonDecode(payload);
          model.updateSensorData(data);
        } catch (e) {
          logger.e("Erro ao decodificar JSON: $e");
        }
      });
    } else {
      logger.e("Cliente desconectado");
      model.setConnectionStatus(false, "Falha na conexão");
      client.disconnect();
    }
  }
}

Vamos entender melhor o que o código do app faz:

1. Importações:

  • flutter/cupertino.dart: Widgets do estilo Cupertino (iOS).
  • flutter/material.dart: Widgets do Material Design (Android).
  • mqtt_client/mqtt_client.dart e mqtt_client/mqtt_server_client.dart: Biblioteca para comunicação MQTT.
  • flutter_hooks/flutter_hooks.dart: Biblioteca para utilizar hooks no Flutter (simplifica o gerenciamento de estado).
  • dart:convert: Funções para codificar e decodificar dados JSON.
  • provider/provider.dart: Biblioteca para gerenciamento de estado.
  • logger/logger.dart: Biblioteca para registro de logs na aplicação.

2. Funções main() e MyApp:

  • A função main() inicia o aplicativo e configura o ChangeNotifierProvider para gerenciar o estado global.
  • MyApp é um widget StatelessWidget que configura o aplicativo:
    • Define o título do aplicativo como “Monitor de Qualidade do Ar”.
    • Configura o tema claro e escuro com Material Design 3.
    • Remove o banner de debug.
    • Define SensorScreen como a tela inicial.

3. AirQualityModel (Modelo de dados):

  • Classe que estende ChangeNotifier para gerenciar os dados do sensor.
  • Armazena dados do sensor, status de conexão e métodos para interpretação da qualidade do ar.
  • Fornece métodos para atualizar dados e determinar cores baseadas nos níveis de CO.

4. SensorScreen (HookWidget):

  • SensorScreen é um widget HookWidget, permitindo o uso de hooks.
  • Variáveis:
    • broker, port, clientId, topic: Configurações para a conexão MQTT.
    • client: Instância do MqttServerClient (conexão com o broker MQTT).
  • useMemoized Hook:
    • Cria uma instância do MqttServerClient usando as configurações do broker e cliente.
  • useEffect Hook:
    • Chama a função _connectMQTT para estabelecer a conexão MQTT quando o widget é construído.
    • Retorna uma função de limpeza que desconecta o cliente MQTT quando o widget é desmontado.
  • Interface do Usuário (UI):
    • Scaffold com um AppBar exibindo o título “Qualidade do Ar” e indicador de status de conexão.
    • Três cards principais:
      1. Card principal com a leitura do sensor e status visual da qualidade do ar
      2. Card com informações do dispositivo
      3. Card com explicação sobre os níveis de CO e seus significados

5. Widgets auxiliares:

  • _buildMainSensorCard: Cria o card principal com animação e indicador visual de qualidade.
  • _buildDeviceInfoCard: Exibe informações sobre o dispositivo sensor.
  • _buildAirQualityInfoCard: Mostra uma tabela com os diferentes níveis de CO e seus significados.
  • _buildInfoRow e _buildQualityRow: Componentes reutilizáveis para as linhas nos cards.

6. _connectMQTT (Função de Conexão MQTT):

  • Recebe o MqttServerClient e o BuildContext como parâmetros.
  • Configuração do Cliente:
    • Ativa o logging para depuração.
    • Define a porta e o tempo de keep-alive.
    • Configura callbacks para eventos de conexão, desconexão e inscrição.
  • Conexão:
    • Tenta conectar o cliente ao broker com mensagem de conexão configurada.
    • Atualiza o status de conexão no modelo de dados.
  • Inscrição no Tópico:
    • Inscreve o cliente no tópico esp32/air_quality com QoS atMostOnce.
  • Recebimento de Mensagens:
    • Ouve as mensagens recebidas no tópico.
    • Extrai a carga útil (payload) da mensagem como uma string JSON.
    • Decodifica o JSON e atualiza o modelo de dados.
    • Registra logs para depuração e tratamento de erros.

7. Protocolo MQTT:

  • MQTT é um protocolo leve de mensagens para comunicação entre dispositivos IoT.
  • O aplicativo se inscreve no tópico esp32/air_quality para receber dados do sensor.
  • O ESP32 publica as leituras do sensor MQ-7 (monóxido de carbono) nesse tópico.
  • Os dados são transmitidos em formato JSON e atualizados em tempo real.
  • O QoS atMostOnce é usado para entrega eficiente das mensagens.


Testando o Aplicativo

Agora vamos emular um dispositivo móvel para ver como o app ficará, você pode alterar seu layout, cores, funções e etc usando esse emulador como guia do que esta sendo feito.

No Android Studio criaremos um emulador seguindo os passos e baixando os recursos necessários.

Fonte: Autor

 

Fonte: Autor

 

Fonte: Autor

 

Fonte: Autor

 

Agora já escolhemos um aparelho que iremos emular e a sua versão de android.

Testando o App

  • Garanta que o ESP32 está publicando no broker MQTT, podemos ver isso pelo serial monitor.

No Andriod Studio:

Fonte: Autor

Aguardamos até o dispositivo ser emulado.

Fonte: Autor

Não se preocupe a primeira vez que é emulado o dispositivo demora bastante pois deve baixar algumas configurações para funcionar bem.

Depois que o dispositivo aparecer na tela rode no terminal, na pasta do app, no nosso caso air_quality_monitor:  flutter run

Se tudo der certo terá a visão da tela que construímos.

Fonte: Autor

 


Gerando APK para Distribuição

Agora vamos fazer um apk dessa aplicação para rodar em celulares android

Rode no terminal do Android Studio na pasta do projeto:flutter build apk --release

 

Depois que o processo terminar o apk estará em: build/app/outputs/flutter-apk/app-release.apk

Com o arquivo do apk basta passar ele para o dispositivo android e instalar ele, talvez seja necessário permitir a instalação de apks em seu dispositivo, mas é um processo simples.
Este é o app rodando em tempo real:


Considerações Finais

Dicas

  • Programação mobile é uma área complexa, então não se preocupe em não compreender muito sobre as estruturas desenvolvidas, mas caso surja o interesse se aprofunde nos temas para um desenvolvimento mais seguro e completo.
  • Use brokers MQTT públicos com cautela, preferindo-os apenas para testes e configurações.
  • Ao desenvolver um sistema real, implemente autenticação e autorização para garantir a segurança dos usuários.
  • No desenvolvimento de aplicações com múltiplos processos, erros podem surgir devido a diferentes abordagens. Isso é comum em programação. Sempre consulte documentações, tutoriais, vídeos e utilize IA ou blogs para expandir seu conhecimento.

Próximos Passos

Este post marcou o início da integração entre programação Android e IoT, mas há diversas melhorias e evoluções possíveis:

  • Implementar campos no aplicativo para configurar a conexão com um broker, permitindo que o usuário defina o nome do broker, o tópico e o ID. Isso tornaria o app capaz de gerenciar sensores de diferentes brokers.
  • Desenvolver um broker próprio ou utilizar serviços de hospedagem em nuvem para criar uma solução mais segura e personalizável.
  • Aprimorar a interface do aplicativo, adicionando ícones, personalizando cores, incorporando gráficos e ajustando os elementos visuais conforme as necessidades do projeto. Use sua criatividade e curiosidade para construir o app e testar ele de diferentes formas.


Referências

HANWEI ELECTRONICS. MQ-7 Semiconductor Sensor for Carbon Monoxide. Zhengzhou, China, 2014. Disponível em: https://www.sparkfun.com/datasheets/Sensors/Biometric/MQ-7.pdf. Acesso em: 7 mar. 2025.

MQTT. MQTT: The Standard for IoT Messaging. 2024. Disponível em: https://mqtt.org/. Acesso em: 7 mar. 2025.

HIVEMQ. MQTT Essentials – A Gentle Introduction to the MQTT Protocol. 2024. Disponível em: https://www.hivemq.com/mqtt-essentials/. Acesso em: 7 mar. 2025.

EMQX. EMQX – The World’s Most Scalable MQTT Broker. 2024. Disponível em: https://www.emqx.io/. Acesso em: 7 mar. 2025.


Sobre o Autor


Abraão da Silva

Estudante de Engenharia da Computação, especializado em curiosidades aparentemente aleatórias e desenvolvimento de software. Se eu não estiver pedalando agora, estou estudando ou tentando aproveitar a energia dos raios.


Eletrogate

7 de agosto de 2025

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!

Eletrogate Robô

Assine nossa newsletter e
receba  10% OFF  na sua
primeira compra!