MCU.TEC Algoritimos Projeto Embarcatech – Etapa 2, Atividade 3 do Capítulo 2

Projeto Embarcatech – Etapa 2, Atividade 3 do Capítulo 2

Este artigo descreve o desenvolvimento de uma atividade prática realizada no contexto da Fase 2 da Residência Profissional Embarcatech. O objetivo foi implementar a comunicação entre dois dispositivos embarcados utilizando o protocolo MQTT (Message Queuing Telemetry Transport).

Foram desenvolvidos dois firmwares distintos:

  1. Firmware Emissor – Responsável por monitorar o estado de um botão físico e publicar seu valor em um tópico MQTT.
  2. Firmware Receptor – Subscrito ao mesmo tópico, interpreta a mensagem recebida e altera o estado de um LED de acordo com o valor enviado.

Broker MQTT

Para a troca de mensagens entre os dispositivos, foi utilizado um broker MQTT próprio, hospedado no endereço:

mqtt://mqtt.rapport.tec.br

Esse broker foi configurado utilizando o Mosquitto, uma implementação leve e eficiente do protocolo MQTT, ideal para ambientes embarcados. Para fins de autenticação e rastreabilidade, foi criada uma conta específica para o monitor responsável pela atividade:

  • Usuário: usuario007
  • Senha: vestidaparamatar

⚠️ Nota de segurança: em um ambiente real de produção, credenciais como essas nunca devem ser expostas em texto aberto. Aqui estão incluídas apenas para fins acadêmicos e reprodutibilidade da atividade.


Repositório do Projeto

O código-fonte completo do projeto está disponível no repositório público no GitHub:

git@github.com:carlosdelfino/embarcatech_etapa_2_cap_2_Tarefa-Pr-tica-IOT-3.git

Este repositório contém os arquivos dos dois firmwares mencionados, bem como instruções adicionais para compilação e execução.


Publisher (Servidor de Telemetria)

O Publisher é o componente responsável pela geração e envio dos dados de telemetria do dispositivo embarcado. Ele coleta as informações diretamente do hardware — como o estado de sensores ou botões — e publica esses dados em tópicos específicos no servidor MQTT Broker.

No contexto deste projeto, o Publisher é utilizado para monitorar o estado de um botão físico. Sempre que ocorre uma mudança nesse estado, o valor correspondente é publicado em um tópico MQTT dedicado, por exemplo:

embarcatech/tarefa_pratica/iot/3/botao

Esse modelo permite que múltiplos dispositivos ou serviços subscritos a esse tópico sejam notificados em tempo real sobre a alteração, promovendo uma comunicação desacoplada e eficiente.

Embora sua função principal seja a de enviar mensagens, um Publisher também pode atuar simultaneamente como Subscriber, reagindo a comandos ou atualizações enviadas por outros dispositivos. Isso é especialmente útil em sistemas interativos ou distribuídos, como no caso deste projeto, onde o mesmo dispositivo pode tanto publicar o estado de um botão quanto reagir a comandos remotos.

Na próxima seção, será apresentado o código do firmware que implementa esse comportamento, com explicações detalhadas sobre cada parte da lógica.


Estrutura Inicial do Código – Bibliotecas, Macros e Variáveis Globais

A seguir mostro a carga das bibliotecas, as constantes definidas via macros e as variáveis globais utilizadas na aplicação Publisher desenvolvida para o microcontrolador RP2040 com conectividade Wi-Fi (via módulo CYW43).

Bibliotecas e Headers

As primeiras linhas do código realizam a inclusão das bibliotecas essenciais ao funcionamento da aplicação:

  • stdio.h e string.h — bibliotecas padrão da linguagem C para entrada/saída e manipulação de strings.
  • "pico/stdlib.h" — biblioteca da SDK do Raspberry Pi Pico com funções para GPIO, delay, UART, entre outras.
  • "hardware/gpio.h" — utilizada para manipulação dos pinos GPIO diretamente.
  • "hardware/adc.h" — necessária caso o projeto também envolva leitura de sinais analógicos.
  • "pico/cyw43_arch.h" — habilita a pilha de Wi-Fi via chip CYW43 da Infineon, usado no Raspberry Pi Pico W.
  • Bibliotecas da lwIP:
    • "lwip/apps/mqtt.h" — implementação cliente do protocolo MQTT.
    • "lwip/ip_addr.h" — manipulação de endereços IP.
    • "lwip/dns.h" — resolução de nomes de domínio.

Esses headers juntos estabelecem a base para que o dispositivo se conecte à internet, resolva domínios e se comunique com o broker MQTT.

Macros Definidas

#define MQTT_BROKER_PORT 1883
#define BUTTON_STATE 5
  • MQTT_BROKER_PORT: define a porta padrão para comunicação MQTT sem criptografia (TCP), amplamente adotada por brokers como Mosquitto.
  • BUTTON_STATE: define o número do pino GPIO utilizado para ler o estado do botão físico. Aqui, o botão está ligado ao GPIO 5.

Variáveis Globais

static mqtt_client_t *mqtt_client;
static ip_addr_t broker_ip;
static char mqtt_button_topic[50];
static bool mqtt_connected = false;
static bool last_button_state = false;

Estas variáveis mantêm o estado do sistema:

  • mqtt_client: ponteiro para a estrutura do cliente MQTT usada pela lwIP.
  • broker_ip: estrutura que armazena o IP do broker MQTT (após resolução DNS, se aplicável).
  • mqtt_button_topic: string que conterá o nome do tópico utilizado para publicação dos dados do botão.
  • mqtt_connected: flag booleana para indicar se a conexão MQTT foi estabelecida com sucesso.
  • last_button_state: guarda o último estado lido do botão, útil para detectar mudanças e evitar reenvios desnecessários.

Protótipos das Funções e Callbacks

Em seguida à definição das variáveis globais, o código apresenta os protótipos das funções principais que organizam a lógica de operação e comunicação do dispositivo. Definir as assinaturas antecipadamente melhora a clareza e a manutenibilidade do código, especialmente em projetos embarcados mais estruturados.

// Protótipos de Funções
static void mqtt_connection_callback(mqtt_client_t *client, void *arg, mqtt_connection_status_t status);
void publish_button_state(bool pressed);
void dns_check_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg);

Funções Declaradas:

  • mqtt_connection_callback
    Esta função será chamada automaticamente pela pilha MQTT da lwIP sempre que houver uma mudança no estado da conexão com o broker. Ela permite reagir a eventos como conexão bem-sucedida, falha de autenticação, ou perda de conexão.
  • publish_button_state(bool pressed)
    Esta função será responsável por publicar o estado atual do botão no tópico MQTT apropriado. O argumento pressed representa o estado lógico do botão (pressionado ou não).
  • dns_check_callback
    Callback invocado após a resolução de nome via DNS do endereço do broker MQTT. Recebe como parâmetro o nome original requisitado, o IP resolvido e o argumento de contexto. Essa função garante que o dispositivo possa operar com um nome de domínio (FQDN) em vez de um IP fixo, facilitando a portabilidade e flexibilidade do sistema.

Estas declarações permitem organizar o código de maneira modular, separando claramente os eventos de rede, as publicações MQTT e as reações aos estados do sistema. Esse tipo de arquitetura é essencial em aplicações IoT mais robustas e escaláveis.


Callback de Conexão com o Broker MQTT

Após a tentativa de conexão com o broker, a pilha lwIP chama automaticamente a função mqtt_connection_callback, cuja responsabilidade é indicar o status da conexão e, em caso de sucesso, prosseguir com a publicação inicial ou outras ações.

static void mqtt_connection_callback(mqtt_client_t *client, void *arg, mqtt_connection_status_t status)
{
    if (status == MQTT_CONNECT_ACCEPTED) {
        printf("[MQTT] Conectado com sucesso ao broker.\n");
        mqtt_connected = true;
        // Aqui poderia ser feita uma publicação inicial ou inscrição em tópicos
    } else {
        printf("[MQTT] Falha na conexão. Código: %d\n", status);
        mqtt_connected = false;
    }
}

Detalhes do Comportamento:

  • Parâmetros da função:
    • mqtt_client_t *client: ponteiro para a estrutura do cliente MQTT.
    • void *arg: argumento genérico (não utilizado neste exemplo, mas útil para contexto em projetos maiores).
    • mqtt_connection_status_t status: enumeração que representa o resultado da tentativa de conexão (ex: aceito, recusado por autenticação, broker indisponível etc).
  • Tratamento de sucesso:
    • Se o status retornado for MQTT_CONNECT_ACCEPTED, o dispositivo considera a conexão estabelecida e define a variável global mqtt_connected como true.
  • Tratamento de falha:
    • Caso contrário, é emitida uma mensagem com o código de erro e o sistema pode ser configurado para tentar reconectar automaticamente (não implementado neste exemplo, mas recomendável em produção).

Essa função é um ponto-chave na arquitetura MQTT, pois determina como o dispositivo irá reagir a flutuações na rede, reinícios do broker ou credenciais inválidas.


Inicialização do Wi-Fi, GPIO e Cliente MQTT

A função main() marca o ponto de entrada do firmware e é responsável por inicializar todos os recursos necessários para o funcionamento da aplicação. Esta etapa envolve três componentes principais:

  1. Subsistema de Wi-Fi
  2. Configuração do GPIO para o botão
  3. Inicialização do cliente MQTT
int main()
{
    stdio_init_all();
    sleep_ms(2000);
    printf("\n=== Iniciando MQTT Button Monitor ===\n");

A função stdio_init_all() habilita o console serial para que mensagens printf() possam ser exibidas, essencial para debug. Um pequeno delay de 2 segundos (sleep_ms) garante tempo para inicialização do sistema antes da conexão Wi-Fi.


Inicialização do Wi-Fi

    // Inicializa Wi-Fi
    if (cyw43_arch_init()) {
        printf("Erro na inicialização do Wi-Fi\n");
        return -1;
    }
    cyw43_arch_enable_sta_mode();

A função cyw43_arch_init() prepara o módulo CYW43 (integrado no Pico W) para uso. Caso a inicialização falhe, o programa é abortado. Em seguida, o modo Station (STA) é ativado, permitindo que o dispositivo se conecte a uma rede existente.

    printf("[Wi-Fi] Conectando...\n");
    if (cyw43_arch_wifi_connect_timeout_ms(ssid: WIFI_SSID, pw: WIFI_PASSWORD, auth: CYW43_AUTH_WPA2_AES_PSK, timeout: 30000)) {
        printf("[Wi-Fi] Falha na conexão Wi-Fi\n");
        return -1;
    } else {
        printf("[Wi-Fi] Conectado com sucesso!\n");
    }

A função cyw43_arch_wifi_connect_timeout_ms() realiza a conexão com a rede informada usando SSID e senha definidos em outro trecho do código. Um timeout de 30 segundos evita bloqueios indefinidos. O resultado é exibido no terminal serial para acompanhamento.


Configuração do GPIO (Botão)

    // Configura GPIO do botão
    gpio_init(BUTTON_STATE);
    gpio_set_dir(BUTTON_STATE, GPIO_IN);
    gpio_pull_up(BUTTON_STATE);  // <<< ATENÇÃO: pull-up ativado

Aqui, o pino definido como BUTTON_STATE (GPIO 5) é inicializado como entrada (GPIO_IN) e configurado com pull-up interno ativado. Isso significa que o botão será interpretado como:

  • Alto (1) quando não pressionado
  • Baixo (0) quando pressionado (conectado ao GND)

O uso do pull-up elimina a necessidade de resistor externo em muitos casos práticos.


Inicialização do Cliente MQTT

    // Inicializa Cliente MQTT
    mqtt_client = mqtt_client_new();

Por fim, a função mqtt_client_new() aloca e inicializa a estrutura que representará o cliente MQTT, utilizando a pilha lwIP. Essa estrutura será posteriormente usada para configurar a conexão e realizar publicações.


Essa organização sequencial na main() garante que todas as camadas estejam corretamente configuradas antes que o dispositivo comece a interagir com o broker MQTT.


Resolução do Nome de Domínio do Broker MQTT

Antes que o cliente MQTT possa se conectar ao broker, é necessário resolver o nome de domínio (FQDN) do servidor em um endereço IP numérico. Essa tarefa é realizada pela pilha de rede lwIP, que fornece uma API assíncrona para resolução DNS.

// Resolve DNS do broker MQTT
err_t err = dns_gethostbyname(hostname: MQTT_BROKER, addr: &broker_ip, found: dns_check_callback);

A função dns_gethostbyname() inicia a resolução do nome de domínio definido na constante MQTT_BROKER. O IP resolvido será armazenado na variável broker_ip, e o callback dns_check_callback() será chamado quando o endereço estiver disponível.


Tratamento de Respostas DNS

if (err == ERR_OK) {
    dns_check_callback(name: MQTT_BROKER, ipaddr: &broker_ip, callback_arg: NULL);
} else if (err == ERR_INPROGRESS) {
    printf("[DNS] Resolvendo...\n");
} else {
    printf("[DNS] Erro ao resolver DNS: %d\n", err);
    return -1;
}

A verificação do valor de retorno permite três cenários distintos:

  • ERR_OK: o endereço IP já está em cache e disponível. O callback dns_check_callback é chamado manualmente para manter a lógica uniforme.
  • ERR_INPROGRESS: a resolução está sendo feita de forma assíncrona; o callback será executado automaticamente quando finalizada.
  • Outro valor (erro): houve falha na resolução, e o programa exibe uma mensagem no terminal e encerra a execução com erro.

Esse mecanismo garante que o firmware possa trabalhar com nomes de domínio dinâmicos, permitindo maior flexibilidade e integração com infraestrutura em nuvem ou redes corporativas, sem depender de IPs fixos.


Loop Principal (Super Loop)

O coração do firmware embarcado é seu loop principal, conhecido como super loop. Nele, são executadas periodicamente todas as tarefas críticas para o funcionamento do sistema, em especial aquelas que não exigem interrupções ou multitarefa (como no caso de um RTOS).

while (true) {
    // Atualiza tarefas de rede
    cyw43_arch_poll();

A chamada cyw43_arch_poll() processa eventos pendentes da pilha de rede do módulo Wi-Fi. Isso é necessário para manter a conexão com o Access Point e garantir o funcionamento contínuo do cliente MQTT.


Leitura e Monitoramento do Botão

    // Lê o estado do botão
    bool button_state = !gpio_get(BUTTON_STATE); // <<< Inverte porque é pull-up

O estado do botão é lido diretamente do GPIO definido por BUTTON_STATE. A inversão lógica (!gpio_get(...)) é necessária porque o botão está configurado com pull-up interno, ou seja:

  • GPIO em nível alto (1) → botão não pressionado
  • GPIO em nível baixo (0) → botão pressionado

Detecção de Mudança de Estado

    if (button_state != last_button_state) {
        printf("[BOTÃO] Estado mudou para: %s\n", button_state ? "ON" : "OFF");
        publish_button_state(button_state);
        last_button_state = button_state;
    }

Este trecho verifica se o botão foi pressionado ou solto em relação à última leitura registrada:

  • Se houver mudança de estado, imprime-se o novo valor no console serial.
  • A função publish_button_state() é chamada para enviar o novo estado ao broker MQTT, permitindo que outros dispositivos ou sistemas reajam à interação do usuário.
  • A variável last_button_state é atualizada para manter referência para a próxima iteração.

Controle da Frequência de Leitura

    sleep_ms(1000); // Ajuste conforme desejado

Um delay de 1 segundo limita a frequência de leitura e publicação, evitando sobrecarga no broker e garantindo que mudanças de estado sejam perceptíveis. Em sistemas mais sofisticados, essa lógica pode ser substituída por debounce por software ou uso de interrupções por borda para maior responsividade.


Esse loop simples, porém eficiente, permite implementar telemetria e controle remoto com confiabilidade usando apenas a estrutura bare-metal e a pilha MQTT da lwIP.


Função de Publicação do Estado do Botão

A função publish_button_state(bool pressed) é responsável por construir e publicar uma mensagem no broker MQTT, informando o estado lógico do botão lido no hardware. Essa função encapsula toda a lógica de transmissão, promovendo organização e reutilização de código.

void publish_button_state(bool pressed) {
    if (!mqtt_connected) {
        printf("[MQTT] Não conectado, não publicando estado da porta\n");
        return;
    }

Validação de Conexão

Antes de qualquer tentativa de publicação, o firmware verifica se a conexão com o broker foi previamente estabelecida. Essa validação evita falhas e mensagens de erro desnecessárias caso a rede tenha sido interrompida.


Construção do Tópico e da Mensagem

    char topic_button_state[50];
    snprintf(topic_button_state, sizeof(topic_button_state), "%s/button", mqtt_button_topic);

    const char *message = pressed ? "ON" : "OFF";
  • O tópico MQTT é montado dinamicamente com base na string mqtt_button_topic (definida em outro trecho do código, normalmente com o nome do dispositivo ou usuário) e o sufixo /button, resultando, por exemplo, em: iot/embarcatech/wellingson/button
  • A mensagem a ser enviada será "ON" se o botão estiver pressionado (pressed == true) ou "OFF" caso contrário.

Publicação MQTT

    printf("[MQTT] Publicando: tópico='%s', mensagem='%s'\n", topic_button_state, message);

    err_t err = mqtt_publish(
        client: mqtt_client,
        topic: topic_button_state,
        payload: message,
        payload_length: strlen(message),
        qos: 0,
        retain: 0,
        callback: NULL,
        callback_arg: NULL
    );
  • A função mqtt_publish() da biblioteca lwIP envia a mensagem para o broker.
  • São usados:
    • QoS 0 (Quality of Service) — entrega não garantida, mas rápida e leve, ideal para aplicações como esta.
    • Retain = 0 — o broker não irá armazenar a última mensagem publicada para novos inscritos no tópico.

Verificação da Publicação

    if (err == ERR_OK) {
        printf("[MQTT] Publicação enviada com sucesso\n");
    } else {
        printf("[MQTT] Erro ao publicar: %d\n", err);
    }
}

A função imprime o resultado da operação, permitindo depuração durante testes e garantindo que falhas sejam visíveis ao desenvolvedor.


Comentário

Essa função demonstra um padrão bastante comum em aplicações embarcadas IoT: encapsular a lógica de comunicação em uma função dedicada, com verificação de estado e formatação dinâmica de tópicos. Isso permite escalabilidade, já que o mesmo modelo pode ser usado para sensores, atuadores, comandos ou respostas.


Callback de Resolução de Nome de Domínio (DNS)

A função dns_check_callback() é chamada automaticamente pela pilha lwIP assim que a resolução do nome de domínio do broker MQTT é concluída, seja com sucesso ou falha. Este é um ponto crítico da inicialização, pois o cliente MQTT precisa de um endereço IP válido para se conectar ao broker.

void dns_check_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg) {
    if (ipaddr != NULL) {
        broker_ip = *ipaddr;
        printf("[DNS] Resolvido: %s -> %s\n", name, ipaddr_ntoa(ipaddr));
  • O endereço IP resolvido é copiado para a variável global broker_ip.
  • A função ipaddr_ntoa() converte o IP em uma string para exibição no terminal (ex: mqtt.rapport.tec.br -> 192.168.1.100).
  • A verificação if (ipaddr != NULL) garante que a resolução foi bem-sucedida antes de tentar conexão.

Estrutura de Conexão MQTT

        struct mqtt_connect_client_info_t ci = {
            .client_id = "pico-client",
            .keep_alive = 60,
            .client_user = NULL,
            .client_pass = NULL,
            .will_topic = NULL,
            .will_msg = NULL,
            .will_qos = 0,
            .will_retain = 0
        };

Esta estrutura mqtt_connect_client_info_t define os parâmetros para o cliente MQTT. Os campos principais são:

  • .client_id: identifica unicamente o dispositivo conectado. Aqui usamos "pico-client", mas em aplicações reais é recomendável usar um identificador exclusivo por dispositivo.
  • .keep_alive: tempo máximo em segundos entre mensagens de controle (pings).
  • .client_user / .client_pass: credenciais de autenticação (opcional; aqui não utilizadas).
  • Campos de LWT (Last Will and Testament): permitem ao broker publicar uma mensagem automaticamente se o cliente se desconectar abruptamente — útil em aplicações críticas, mas não habilitado aqui.

Tentativa de Conexão ao Broker

        printf("[MQTT] Conectando ao broker...\n");
        mqtt_client_connect(mqtt_client, &broker_ip, MQTT_BROKER_PORT, mqtt_connection_callback, NULL, &ci);
    } else {
        printf("[DNS] Falha ao resolver DNS para %s\n", name);
    }
  • A função mqtt_client_connect() é chamada com o IP do broker, a porta MQTT (1883), o callback de conexão (mqtt_connection_callback) e a estrutura de conexão ci.
  • Em caso de falha, o nome do domínio é impresso como erro.

Comentário

Esta função ilustra claramente o modelo event-driven típico de sistemas embarcados conectados, onde tarefas como DNS e conexão MQTT não são síncronas. Ao dividir essas etapas em callbacks bem definidos, o código se mantém responsivo e eficiente mesmo em ambientes com poucos recursos, como o RP2040.


Tratando a Conexão com o Broker MQTT

A função mqtt_connection_callback() é um callback de evento de conexão invocado automaticamente pela pilha MQTT (lwIP) após a tentativa de conexão ao broker. Ela define o comportamento do firmware de acordo com o status da conexão.

static void mqtt_connection_callback(mqtt_client_t *client, void *arg, mqtt_connection_status_t status) {
    if (status == MQTT_CONNECT_ACCEPTED) {
        printf("[MQTT] Conectado ao broker!\n");
        mqtt_connected = true;
    } else {
        printf("[MQTT] Falha na conexão MQTT. Código: %d\n", status);
        mqtt_connected = false;
    }
}

Parâmetros:

  • mqtt_client_t *client: ponteiro para a instância do cliente MQTT.
  • void *arg: ponteiro de argumento auxiliar (não utilizado aqui).
  • mqtt_connection_status_t status: enumeração com o resultado da tentativa de conexão.

Lógica de Tratamento:

  • Se a conexão for bem-sucedida (MQTT_CONNECT_ACCEPTED):
    • O firmware exibe uma mensagem indicando sucesso.
    • A flag global mqtt_connected é definida como true, permitindo publicações subsequentes.
  • Caso contrário:
    • Um aviso de erro é impresso com o código de status retornado pelo broker (por exemplo, erros de autenticação, cliente duplicado, broker indisponível, etc).
    • A flag mqtt_connected é definida como false, bloqueando tentativas de publicação até que uma nova conexão seja bem-sucedida.

Comentário Técnico

Essa abordagem é fundamental em sistemas IoT baseados em redes instáveis, como Wi-Fi. Separar o tratamento da conexão MQTT em um callback permite que o firmware permaneça não bloqueante, reativo e com maior tolerância a falhas. Além disso, ao utilizar uma flag como mqtt_connected, o código que publica mensagens pode validar previamente a conectividade de forma simples e eficaz.


Firmware Subscriber (Cliente)

O Subscriber é o componente do sistema responsável por monitorar o broker MQTT em busca de atualizações em um ou mais tópicos de interesse. A cada nova mensagem recebida no tópico subscrito, o firmware reage executando uma ação definida — no caso deste projeto, alterar o estado de um LED, funcionando como indicador do sinal enviado por outro dispositivo.

Embora sua principal função seja ouvir o broker, o Subscriber pode, assim como o Publisher, assumir ambos os papéis em sistemas mais completos, publicando informações de resposta, status ou diagnóstico. Essa bidirecionalidade é comum em projetos IoT robustos, onde sensores e atuadores precisam interagir em tempo real.


Estratégia de Desenvolvimento

Durante a construção deste firmware, adotei uma abordagem diferente da utilizada no Publisher, com o objetivo de explorar e compreender novas estruturas de lógica, funções alternativas da biblioteca MQTT da lwIP, e práticas recomendadas no ecossistema embarcado.

Esse método comparativo — alterando levemente a abordagem em cada implementação — favorece o aprendizado aprofundado, permitindo identificar vantagens, limitações e comportamentos específicos de cada modelo adotado.


Excelente escolha, Carlos. O uso de uma estrutura struct para consolidar o estado e os parâmetros da conexão MQTT é uma prática muito recomendada em sistemas embarcados, especialmente em aplicações mais escaláveis e modulares.

Aqui está a versão aprimorada da explicação para essa parte do código, com ênfase técnica e clareza didática para publicação no MCU.TEC.BR:


Estrutura de Estado do Cliente MQTT

Uma das primeiras diferenças no firmware Subscriber em relação ao Publisher é a adoção de uma estrutura de dados agregada (struct) para representar o estado completo do cliente MQTT. Isso torna o código mais organizado, encapsulado e extensível, especialmente útil em projetos que evoluem para múltiplas conexões ou funcionalidades adicionais.

typedef struct {
    mqtt_client_t* mqtt_client_inst;
    struct mqtt_connect_client_info_t mqtt_client_info;
    char data[MQTT_OUTPUT_RINGBUF_SIZE];
    char topic[MQTT_TOPIC_LEN];
    uint32_t len;
    ip_addr_t mqtt_broker_address;
    bool connect_done;
    int subscribe_count;
    bool stop_client;
} MQTT_CLIENT_DATA_T;

Campos da estrutura explicados:

  • mqtt_client_inst: ponteiro para a instância do cliente MQTT da lwIP, usado para chamadas de publicação, inscrição, etc.
  • mqtt_client_info: estrutura que guarda os parâmetros da conexão, como client_id, keep-alive, usuário, senha e mensagens LWT.
  • data[]: buffer onde a mensagem recebida do broker será armazenada.
  • topic[]: buffer que armazena o nome do tópico ao qual o cliente está atualmente subscrito.
  • len: comprimento da mensagem recebida.
  • mqtt_broker_address: IP resolvido do broker MQTT (resultado de resolução DNS ou atribuição estática).
  • connect_done: flag que indica se a conexão com o broker foi concluída com sucesso.
  • subscribe_count: contador de quantos tópicos o cliente está subscrito — útil para diagnosticar e controlar múltiplas inscrições.
  • stop_client: flag de controle para interromper ou encerrar o cliente MQTT, útil em aplicações com comandos externos ou automação.

Comentário Técnico

A centralização dessas variáveis em uma única estrutura:

  • Facilita a passagem de contexto entre funções e callbacks;
  • Permite multiplexação (vários clientes simultâneos no futuro);
  • Melhora a legibilidade do código;
  • Permite expansão fácil com novos campos como estatísticas, tempo de última mensagem, status de erro etc.

Além disso, o uso de subscribe_count reforça que este cliente, embora com função principal de Subscriber, pode também se comportar como Publisher, tornando o firmware flexível e alinhado aos princípios da IoT bidirecional.


Definições de Constantes para o Cliente MQTT (Subscriber)

Nesta etapa do firmware Subscriber, são definidas constantes fundamentais que configuram o comportamento do cliente MQTT. Esses parâmetros determinam desde o tempo de keep-alive até tópicos especiais como o de presença (Last Will and Testament), além de indicar se a aplicação utiliza tópicos únicos por dispositivo.

#define MQTT_KEEP_ALIVE_S 60

Define o intervalo de keep-alive em segundos. Isso determina o tempo máximo que o cliente pode ficar sem enviar qualquer mensagem antes que o broker o considere desconectado. No caso, foi adotado um valor padrão de 60 segundos, comum em aplicações embarcadas com conexões Wi-Fi.


Níveis de Qualidade de Serviço (QoS)

#define MQTT_SUBSCRIBE_QOS 1
#define MQTT_PUBLISH_QOS 1
#define MQTT_PUBLISH_RETAIN 0
  • QoS 1 (At least once): Garante que a mensagem será recebida ao menos uma vez, podendo ocorrer duplicações. É o meio-termo entre confiabilidade e eficiência, ideal para comandos como ligar/desligar um LED.
  • Retain 0: Indica que o broker não irá reter a última mensagem publicada nesse tópico — útil quando não se deseja que novos subscribers recebam dados antigos.

Tópicos de Presença (Last Will and Testament)

#define MQTT_WILL_TOPIC "/online"
#define MQTT_WILL_MSG "0"
#define MQTT_WILL_QOS 1
  • Tópico /online: será publicado automaticamente pelo broker se o cliente se desconectar de forma inesperada (por exemplo, queda de energia).
  • Mensagem "0": usada para sinalizar que o dispositivo está offline.
  • QoS 1: garante que a mensagem de desconexão seja entregue ao menos uma vez.

📌 Essa funcionalidade é importante para aplicações críticas que exigem supervisão contínua dos dispositivos conectados.


Base do Tópico e Unicidade

#ifndef MQTT_BASE_TOPIC
#define MQTT_BASE_TOPIC "rack_inteligente"
#endif

#ifndef MQTT_UNIQUE_TOPIC
#define MQTT_UNIQUE_TOPIC 1
#endif
  • MQTT_BASE_TOPIC: define o prefixo principal dos tópicos MQTT usados por este firmware. Aqui, o tópico base foi nomeado como "rack_inteligente", provavelmente indicando o dispositivo ou aplicação (ex: automação de racks de servidores, iluminação ou controle térmico).
  • MQTT_UNIQUE_TOPIC = 1: quando habilitado, faz com que o firmware adicione o nome do cliente ao tópico final, garantindo que múltiplos dispositivos usando o mesmo broker não se sobrescrevam nos tópicos uns dos outros.

Comentário Técnico

A separação desses parâmetros em diretivas #define torna o firmware altamente portável. Para criar um novo dispositivo, basta alterar essas constantes sem precisar modificar a lógica interna do código.

Além disso, o uso de #ifndef permite que os parâmetros sejam sobrescritos externamente (ex: via makefile ou -D no compilador), prática comum em sistemas embarcados configuráveis via linha de comando.


Funções Auxiliares do Cliente MQTT

Antes da função main(), foram definidas duas funções auxiliares fundamentais para o funcionamento do firmware Subscriber:

  1. Construção dinâmica de tópicos MQTT
  2. Controle físico do LED com base em mensagens recebidas

Esse tipo de encapsulamento de lógica melhora a modularidade do código, evita repetição e facilita manutenção ou expansão.


Construção do Tópico Completo: full_topic(...)

static const char *full_topic(MQTT_CLIENT_DATA_T *state, const char *name) {
#if MQTT_UNIQUE_TOPIC
    static char full_topic[MQTT_TOPIC_LEN];
    snprintf(full_topic, sizeof(full_topic), "%s%s", state->mqtt_client_info.client_id, name);
    return full_topic;
#else
    return name;
#endif
}

Essa função retorna a string completa do tópico MQTT utilizado em publicações e subscrições. Ela faz parte de uma estratégia para gerar nomes únicos de tópico, fundamentais quando vários dispositivos compartilham o mesmo broker MQTT.

  • Quando MQTT_UNIQUE_TOPIC está ativado (#define MQTT_UNIQUE_TOPIC 1), o nome do cliente (client_id) é concatenado ao nome base do tópico. Exemplo: pico-client/state
  • Quando desativado, retorna apenas o nome original (name), permitindo tópicos genéricos para todos os dispositivos.

🔧 Essa flexibilidade é essencial em ambientes com múltiplos nós, como redes de sensores ou ambientes de automação residencial compartilhada.


Controle de LED: control_led(bool on)

static void control_led(bool on) {
    const char* message = on ? "ON" : "OFF";
    printf("Control LED: %s\n", message);

    if (on) {
        gpio_put(LED_RED_PIN, 1);
        gpio_put(LED_GREEN_PIN, 0);
    } else {
        gpio_put(LED_RED_PIN, 0);
        gpio_put(LED_GREEN_PIN, 1);
    }
}

Esta função implementa a ação física correspondente à mensagem recebida via MQTT.

  • Quando on == true, o LED vermelho é aceso e o verde apagado.
  • Quando on == false, ocorre o contrário: LED verde aceso e vermelho apagado.

Esse comportamento binário de LEDs é uma estratégia visual simples e eficiente para depuração e sinalização de estado, muito comum em prototipagem embarcada.

💡 Você pode evoluir essa função para trabalhar com PWM (brilho), RGB ou até sequências visuais (piscar, alerta, etc.), criando uma camada de abstração para atuadores visuais.


Callbacks para Publicação e Subscrição MQTT

O protocolo MQTT operando sobre a pilha lwIP adota um modelo assíncrono orientado a eventos, no qual as operações de publicação, inscrição e cancelamento de inscrição não são bloqueantes. Assim, o retorno da operação é tratado por funções de callback, que executam ações específicas após a conclusão de cada requisição.

Neste firmware Subscriber, são definidas três funções de callback:


📤 pub_request_cb(...) — Callback de Publicação

static void pub_request_cb(__unused void *arg, err_t err) {
    if (err != 0) {
        printf("pub_request_cb failed %d", err);
    }
}

Esta função é chamada após a tentativa de publicação de uma mensagem (caso o Subscriber também atue como Publisher).

  • Se ocorrer algum erro (err != 0), uma mensagem de falha é exibida no console.
  • Neste firmware, não há lógica de retry, o que é aceitável em aplicações simples ou tolerantes a perda.
  • O uso do modificador __unused sinaliza que o parâmetro arg não será utilizado, evitando warnings.

📥 sub_request_cb(...) — Callback de Subscrição

static void sub_request_cb(void *arg, err_t err) {
    MQTT_CLIENT_DATA_T *state = (MQTT_CLIENT_DATA_T *)arg;
    if (err != 0) {
        panic("subscribe request failed %d", err);
    }
    state->subscribe_count++;
}

Esta função é chamada após o envio de uma requisição de inscrição a um tópico.

  • Caso haja erro, é invocada a macro panic(), encerrando o firmware com uma mensagem crítica — o que é válido em sistemas onde a inscrição é obrigatória para funcionamento.
  • Se a inscrição for bem-sucedida, o campo subscribe_count é incrementado, permitindo rastrear quantas inscrições estão ativas.

🧠 O uso de um contador de inscrições é útil para gerenciar cancelamentos de forma segura e condicional.


📤 unsub_request_cb(...) — Callback de Cancelamento de Subscrição

static void unsub_request_cb(void *arg, err_t err) {
    MQTT_CLIENT_DATA_T *state = (MQTT_CLIENT_DATA_T *)arg;
    if (err != 0) {
        panic("unsubscribe request failed %d", err);
    }
    state->subscribe_count--;
    assert(state->subscribe_count >= 0);

    if (state->subscribe_count <= 0 && state->stop_client) {
        mqtt_disconnect(state->mqtt_client_inst);
    }
}

Esta função é executada após a solicitação de cancelamento de uma subscrição.

  • Em caso de erro, o sistema é encerrado com panic(), reforçando o caráter crítico da operação.
  • O campo subscribe_count é decrementado. O uso de assert() impede que esse valor fique negativo, o que indicaria um bug no controle de inscrições.
  • Por fim, se todas as inscrições foram canceladas (subscribe_count <= 0) e a flag stop_client estiver ativada, o cliente MQTT é desconectado do broker automaticamente.

🔧 Esse padrão permite que o sistema “se auto-desligue” de forma limpa quando sua função estiver concluída ou quando receber um comando externo de desligamento.


Considerações Técnicas

  • Esses callbacks mostram claramente o poder e a importância do modelo reativo nos sistemas MQTT embarcados.
  • A contagem de inscrições ativas (subscribe_count) é uma excelente prática para manter o controle do ciclo de vida do cliente MQTT.
  • Em sistemas mais robustos, as funções de callback também podem registrar logs, notificar via LED ou até reinicializar o sistema conforme a criticidade da falha.

Função Auxiliar para Subscrição e Cancelamento de Tópicos MQTT

A função sub_unsub_topics(...) representa um padrão elegante e reutilizável para gerenciamento de múltiplos tópicos MQTT — tanto para subscrição (subscribe) quanto para remoção (unsubscribe).

static void sub_unsub_topics(MQTT_CLIENT_DATA_T* state, bool sub) {
    mqtt_request_cb_t cb = sub ? sub_request_cb : unsub_request_cb;

    mqtt_sub_unsub(state->mqtt_client_inst, full_topic(state, "/exit"),  MQTT_SUBSCRIBE_QOS, cb, state);
    mqtt_sub_unsub(state->mqtt_client_inst, full_topic(state, "/door"),  MQTT_SUBSCRIBE_QOS, cb, state);
}

🧠 Comportamento Inteligente:

  • A função recebe:
    • state: estrutura com o estado do cliente MQTT.
    • sub: flag booleana que indica a operação:
      • true: subscreve aos tópicos.
      • false: cancela a subscrição.
  • Baseado em sub, é escolhido o callback apropriado:
    • sub_request_cb para requisições de inscrição.
    • unsub_request_cb para cancelamento.
  • Os tópicos utilizados são:
    • /exit: pode representar um comando para desligar ou resetar.
    • /door: controle remoto de uma porta ou dispositivo lógico.

Ambos os tópicos são convertidos em caminhos completos com a função full_topic(...), que insere o client_id se MQTT_UNIQUE_TOPIC estiver ativo, garantindo que cada dispositivo use seus próprios tópicos mesmo que o broker seja compartilhado.


Vantagens Arquiteturais

  • Redução de Código Repetido: a função evita duplicação das chamadas mqtt_sub_unsub, que são idênticas exceto pelo tipo de operação.
  • Escalável: novos tópicos podem ser adicionados com facilidade.
  • Modularidade: as funções de inscrição e desinscrição ficam desacopladas da lógica da main() ou do handler de conexão.
  • Segurança: callbacks distintos permitem rastrear corretamente o resultado de cada operação e agir de forma segura.

Sugestão de Evolução

Você pode generalizar essa função ainda mais, usando uma lista de tópicos armazenada em um array de const char*, o que permitiria varrer todos os tópicos de forma iterativa, especialmente útil se o firmware tiver muitos sensores ou comandos MQTT.



Callback de Recepção de Mensagens MQTT

A função mqtt_incoming_data_cb() é o callback associado aos dados recebidos nas inscrições MQTT. Sempre que uma nova mensagem é publicada em um tópico ao qual o cliente está inscrito, essa função é chamada automaticamente pela pilha lwIP.

static void mqtt_incoming_data_cb(void *arg, const u8_t *data, u16_t len, u8_t flags) {
    MQTT_CLIENT_DATA_T *state = (MQTT_CLIENT_DATA_T *)arg;

Parâmetros:

  • arg: ponteiro de contexto (utilizado aqui para acessar a estrutura state).
  • data: ponteiro para os bytes recebidos na mensagem.
  • len: tamanho da mensagem.
  • flags: informações adicionais, como fragmentação (não utilizado neste exemplo).

Determinação do Tópico Básico

#if MQTT_UNIQUE_TOPIC
    const char *basic_topic = state->topic + strlen(state->mqtt_client_info.client_id);
#else
    const char *basic_topic = state->topic;
#endif

Esse trecho ajusta o ponteiro basic_topic para obter somente o sufixo do tópico (por exemplo, "/door" ou "/exit"), descartando o prefixo do client_id, se MQTT_UNIQUE_TOPIC estiver ativado.

Essa estratégia permite que a lógica de tratamento seja aplicada independentemente de qual dispositivo publicou a mensagem, mantendo a flexibilidade da arquitetura.


Armazenamento e Impressão dos Dados

strncpy(state->data, (const char *)data, len);
state->len = len;
state->data[len] = '
strncpy(state->data, (const char *)data, len);
state->len = len;
state->data[len] = '\0';

printf("Topic: %s, Basic Topic: %s, Message: %s\n", state->topic, basic_topic, state->data);
';
printf("Topic: %s, Basic Topic: %s, Message: %s\n", state->topic, basic_topic, state->data);
  • A mensagem recebida é copiada para o buffer state->data, e um caractere nulo ('\0') é adicionado para garantir que ela seja uma string C válida.
  • Em seguida, a mensagem e o tópico são impressos no console para fins de debug e rastreamento.

Processamento da Mensagem

if (strcmp(basic_topic, "/door") == 0) {
    printf("Door topic: %s\n", state->data);
    if (lwip_stricmp((const char *)state->data, "ON") == 0 ||
        strcmp((const char *)state->data, "1") == 0)
    {
        control_led(true);
    } else if (lwip_stricmp((const char *)state->data, "OFF") == 0 ||
               strcmp((const char *)state->data, "0") == 0)
    {
        control_led(false);
    }
  • Se o tópico for "/door", a mensagem controla o estado do LED:
    • "ON" ou "1" → liga o LED vermelho.
    • "OFF" ou "0" → liga o LED verde (LED desligado).
  • A comparação usa lwip_stricmp() para garantir insensibilidade a maiúsculas/minúsculas, o que aumenta a robustez do firmware.

Finalizando a Conexão com "/exit"

} else if (strcmp(basic_topic, "/exit") == 0) {
    state->stop_client = true;  // stop the client when ALL subscriptions are stopped
    sub_unsub_topics(state, false);  // unsubscribe
}
  • Se a mensagem for publicada no tópico "/exit", o cliente marca-se para desligamento (stop_client = true) e executa a função sub_unsub_topics com false, cancelando suas inscrições.
  • Como vimos anteriormente, isso dispara a lógica que desconecta o cliente automaticamente quando todas as inscrições forem removidas.

Comentário Técnico

Esse callback é o elo entre o mundo físico e o mundo lógico: é nele que a mensagem MQTT se transforma em uma ação real no hardware (como acionar LEDs). Seu design modular e uso inteligente de tópicos tornam o firmware:

  • Adaptável a novos comandos (basta adicionar mais if com novos tópicos);
  • Extensível para múltiplos dispositivos, sem colidir nos tópicos;
  • Seguro e robusto, com validação de dados e controle de estado.

Callback de Conexão MQTT: mqtt_connection_cb

A função mqtt_connection_cb(...) é o ponto de entrada para a lógica de inicialização do cliente após a tentativa de conexão com o broker MQTT. Essa função é executada automaticamente pela pilha lwIP assim que o broker responde com o status da conexão.

static void mqtt_connection_cb(mqtt_client_t *client, void *arg, mqtt_connection_status_t status) {
    MQTT_CLIENT_DATA_T *state = (MQTT_CLIENT_DATA_T *)arg;

Parâmetros:

  • client: ponteiro para o cliente MQTT ativo.
  • arg: contexto passado à função, aqui sendo convertido para o tipo MQTT_CLIENT_DATA_T.
  • status: enum que indica o resultado da conexão (sucesso, desconexão ou erro inesperado).

Conexão Bem-Sucedida: MQTT_CONNECT_ACCEPTED

if (status == MQTT_CONNECT_ACCEPTED) {
    state->connect_done = true;
    sub_unsub_topics(state, true);  // subscribe aos tópicos
  • Define que a conexão foi concluída com sucesso (connect_done = true).
  • Executa a inscrição automática nos tópicos definidos, através da função sub_unsub_topics(state, true).

Publicação do Estado Online

    if (state->mqtt_client_info.will_topic) {
        mqtt_publish(state->mqtt_client_inst, state->mqtt_client_info.will_topic, "1", ...);
    }
  • Se foi configurado um tópico de presença (will_topic), o cliente publica "1" manualmente indicando que está online.
  • Isso complementa a funcionalidade do LWT (Last Will and Testament), que publica "0" automaticamente em caso de falha.
  • Dessa forma, forma-se um par lógico de presença: Evento Tópico Mensagem Cliente online /online "1" Cliente falha /online "0"

Conexão Não Estabelecida

} else if (status == MQTT_CONNECT_DISCONNECTED) {
    if (!state->connect_done) {
        panic("Failed to connect to mqtt server");
    }
}
  • Caso o cliente tenha sido desconectado antes de completar a conexão, a função panic() é chamada. Isso indica que nenhuma inscrição nem publicação será possível, e o sistema deve encerrar ou reiniciar.

Tratamento de Erros Inesperados

else {
    panic("Unexpected status");
}
  • Captura situações não previstas (ex: estados inválidos ou corrompidos), garantindo que o sistema não prossiga de forma inconsistente.

Comentário Técnico

Esse callback mostra claramente boas práticas de controle de fluxo:

  • Uso de flags de estado (connect_done) para distinguir falha inicial de desconexão normal.
  • Inicialização assíncrona das subscrições após a confirmação da conexão.
  • Publicação proativa no will_topic, complementando a detecção de presença.
  • Estrutura robusta com tratamento de exceções explícito.

Função Auxiliar de Inicialização da Conexão MQTT: start_client(...)

A função start_client(MQTT_CLIENT_DATA_T *state) encapsula o processo de criação do cliente MQTT, conexão com o broker e registro dos callbacks. Esta organização modular simplifica a reutilização do código e facilita o controle de erros durante o processo de inicialização.

static void start_client(MQTT_CLIENT_DATA_T *state) {

Suporte a TLS (Transport Layer Security)

#if LWIP_ALTCP && LWIP_ALTCP_TLS
    const int port = MQTT_TLS_PORT;
    printf("Using TLS\n");
#else
    const int port = MQTT_PORT;
    printf("Warning: Not using TLS\n");
#endif
  • A função detecta se o suporte a TLS está habilitado via flags de compilação LWIP_ALTCP_TLS.
  • Caso afirmativo, a porta segura (geralmente 8883) é usada.
  • Caso contrário, assume-se a porta padrão (1883), com um aviso explícito.
  • Essa estratégia permite compilar o mesmo firmware tanto com quanto sem criptografia de transporte, aumentando a flexibilidade do projeto.

Criação da Instância do Cliente MQTT

state->mqtt_client_inst = mqtt_client_new();
if (!state->mqtt_client_inst) {
    panic("MQTT client instance creation error");
}
  • A estrutura mqtt_client_t é criada dinamicamente.
  • Se houver falha, o firmware entra em estado de pânico com uma mensagem de erro — fundamental para evitar operações com ponteiros nulos.

Diagnóstico e Inicialização da Pilha Wi-Fi

printf("IP address of this device %s\n", ipaddr_ntoa(&netif_list->ip_addr));
printf("Connecting to mqtt server at %s\n", ipaddr_ntoa(&state->mqtt_broker_address));
cyw43_arch_lwip_begin();
  • Imprime-se o IP atual do dispositivo e o IP do broker MQTT (resolvido previamente).
  • A chamada cyw43_arch_lwip_begin() prepara a pilha lwIP e o módulo CYW43 (Wi-Fi).

Conexão ao Broker

if (mqtt_client_connect(state->mqtt_client_inst, &state->mqtt_broker_address, port, mqtt_connection_cb, state, &state->mqtt_client_info)) {
    panic("MQTT broker connection error");
}
printf("Connected to mqtt server\n");
  • Tenta a conexão com o broker usando os dados preparados na estrutura mqtt_client_info.
  • O callback mqtt_connection_cb será chamado com o resultado da conexão.

Suporte a SNI (Server Name Indication)

#if LWIP_ALTCP && LWIP_ALTCP_TLS
    mbedtls_ssl_set_hostname(altcp_tls_context(state->mqtt_client_inst->conn), MQTT_BROKER);
#endif
  • Se TLS estiver habilitado, a função mbedtls_ssl_set_hostname define o hostname para o contexto TLS — necessário para validação do certificado em servidores com SNI habilitado.

Registro do Callback de Recepção

mqtt_set_inpub_callback(state->mqtt_client_inst, mqtt_incoming_publish_cb, mqtt_incoming_data_cb, state);
  • Registra os dois callbacks que tratam:
    • mqtt_incoming_publish_cb: identifica o tópico recebido.
    • mqtt_incoming_data_cb: processa os dados (já explicado anteriormente).

Finaliza a Seção da Pilha lwIP

printf("Subscribed to topics\n");
cyw43_arch_lwip_end();
}
  • Fecha a sessão iniciada com cyw43_arch_lwip_begin(), liberando a pilha para outras tarefas.

Comentário Técnico

Essa função representa um modelo robusto e portátil para inicialização MQTT, com suporte condicional a TLS e diagnóstico de conexão. Os principais pontos positivos incluem:

  • Separação entre configuração e execução.
  • Tratamento seguro de ponteiros.
  • Modularidade com suporte a múltiplas plataformas e modos de operação.
  • Compatibilidade com brokers profissionais (TLS + SNI).

Callback de Resolução DNS: dns_found(...)

A função dns_found() é o callback executado automaticamente pela pilha lwIP após a tentativa de resolução de nome de domínio do broker MQTT (FQDN → IP).

static void dns_found(const char *hostname, const ip_addr_t *ipaddr, void *arg) {
    MQTT_CLIENT_DATA_T *state = (MQTT_CLIENT_DATA_T *)arg;

Parâmetros:

  • hostname: nome do broker solicitado (ex: "mqtt.rapport.tec.br").
  • ipaddr: ponteiro para o endereço IP resolvido.
  • arg: ponteiro de contexto (state) passado na requisição DNS.

Lógica de Tratamento

    if (ipaddr) {
        state->mqtt_broker_address = *ipaddr;
        start_client(state);
    } else {
        panic("dns request failed");
    }
  • Se o ponteiro ipaddr for válido (não nulo), significa que o DNS foi resolvido com sucesso:
    • O endereço IP é salvo na estrutura do cliente MQTT.
    • A função start_client() é chamada, dando início ao processo de conexão com o broker.
  • Caso a resolução falhe (ipaddr == NULL):
    • O firmware entra em estado de erro com panic("dns request failed"), impedindo conexões inválidas e ajudando no diagnóstico do sistema.

Comentário Técnico

Esse tipo de callback é padrão em arquiteturas event-driven (orientadas a eventos), como as utilizadas na lwIP. Ele permite que a resolução DNS não bloqueie o sistema e que a conexão MQTT só seja iniciada quando o IP do broker for conhecido.

🔧 O uso do ponteiro arg para carregar a estrutura de estado do cliente é uma técnica comum e poderosa para manter o contexto entre chamadas assíncronas.

Essa função complementa a robustez do firmware, adicionando resiliência contra falhas de rede, como DNS inválido ou indisponível.


Função main() – Inicialização do Firmware Subscriber (Parte 1)

A função main() é o ponto de entrada da aplicação embarcada, onde são configurados os periféricos essenciais e iniciada a conexão com o broker MQTT. A estrutura foi organizada para garantir que tudo esteja devidamente inicializado antes do início das operações de rede.

int main(void) {
    stdio_init_all();
  • Inicializa a infraestrutura de entrada e saída padrão, permitindo o uso de printf() para depuração via porta USB.

Aguardar Conexão com a Serial

    while (!stdio_usb_connected()) {
        sleep_ms(1000);
    }
  • Esse laço garante que o programa aguarde até que o terminal serial (como PuTTY ou minicom) esteja conectado ao USB.
  • Isso é especialmente útil para garantir que o usuário veja todas as mensagens de depuração desde o início da execução, evitando perda de logs importantes.

Inicialização dos GPIOs dos LEDs

    gpio_init(LED_RED_PIN);
    gpio_init(LED_GREEN_PIN);
    gpio_set_dir(LED_RED_PIN, GPIO_OUT);
    gpio_set_dir(LED_GREEN_PIN, GPIO_OUT);
  • Os pinos definidos como LED_RED_PIN e LED_GREEN_PIN são inicializados e configurados como saídas digitais, responsáveis pelo controle físico dos LEDs.
  • Essa configuração é essencial para que o firmware possa exibir visualmente o estado do sistema conforme os comandos MQTT recebidos.

Mensagem de Inicialização

    printf("mqtt client starting\n");
  • Imprime uma mensagem no console indicando o início do processo de inicialização do cliente MQTT.

Estrutura de Estado do Cliente

    static MQTT_CLIENT_DATA_T state;
  • Declara e aloca a estrutura que armazenará todas as informações e estados do cliente MQTT, incluindo IP do broker, contador de tópicos, buffers, etc.
  • O uso de static garante que os dados sejam persistentes e alocados fora da pilha, essencial em sistemas bare-metal com pouco RAM.

Inicialização do Módulo Wi-Fi CYW43

    if (cyw43_arch_init()) {
        panic("Failed to initialize CYW43");
    }
  • Esta chamada configura o módulo Wi-Fi integrado (CYW43) do Raspberry Pi Pico W.
  • Em caso de falha, o sistema entra em pânico com uma mensagem descritiva.

Comentário Técnico

Essa primeira parte da main() mostra uma inicialização sequencial, segura e eficiente:

  • Garante que a interface de usuário (serial e LEDs) esteja pronta.
  • Prepara os recursos de hardware necessários (Wi-Fi, GPIOs).
  • Centraliza o estado do cliente em uma única estrutura, facilitando modularidade.

💡 Esse padrão de inicialização é altamente recomendado para firmwares MQTT embarcados, pois separa claramente as responsabilidades e facilita testes unitários ou substituições por mocks em simulações.


Inicialização dos Parâmetros de Conexão MQTT – Identidade e Segurança

Nesta etapa do main(), são configuradas todas as informações que compõem a estrutura de conexão do cliente MQTT (mqtt_connect_client_info_t). Essa estrutura será passada ao mqtt_client_connect() para estabelecer a sessão com o broker.


Definindo o Identificador do Cliente

char client_id_buf[sizeof(MQTT_BASE_TOPIC)];
memcpy(&client_id_buf[0], MQTT_BASE_TOPIC, sizeof(MQTT_BASE_TOPIC) - 1);
client_id_buf[sizeof(client_id_buf) - 1] = 0;
  • Gera-se o identificador do cliente (client_id) com base na constante MQTT_BASE_TOPIC (por exemplo, "rack_inteligente").
  • Este ID será utilizado:
    • Para distinguir o cliente no broker.
    • Para gerar tópicos únicos quando MQTT_UNIQUE_TOPIC estiver ativado.
state.mqtt_client_info.client_id = client_id_buf;
state.mqtt_client_info.keep_alive = MQTT_KEEP_ALIVE_S;
  • O tempo de keep-alive é configurado, determinando o intervalo entre pacotes ping para manter a sessão ativa.

Autenticação com Usuário e Senha (Opcional)

#if defined(MQTT_USERNAME) && defined(MQTT_PASSWORD)
    state.mqtt_client_info.client_user = MQTT_USERNAME;
    state.mqtt_client_info.client_pass = MQTT_PASSWORD;
#else
    state.mqtt_client_info.client_user = NULL;
    state.mqtt_client_info.client_pass = NULL;
#endif
  • Se definido, o firmware usa autenticação via usuário e senha, compatível com brokers que exigem autenticação básica (como Mosquitto configurado com ACL).
  • Caso contrário, os campos são nulos, indicando sessão anônima.

Configuração do Tópico de Presença (Last Will and Testament – LWT)

static char will_topic[MQTT_TOPIC_LEN];
strncpy(will_topic, full_topic(&state, MQTT_WILL_TOPIC), sizeof(will_topic));
state.mqtt_client_info.will_topic = will_topic;
state.mqtt_client_info.will_msg = MQTT_WILL_MSG;
state.mqtt_client_info.will_qos = MQTT_WILL_QOS;
state.mqtt_client_info.will_retain = true;
  • O cliente define um tópico de “última vontade”, utilizado pelo broker caso o dispositivo se desconecte de forma inesperada.
  • Neste exemplo:
    • Tópico: "rack_inteligente/online" (ou variante com client_id)
    • Mensagem: "0"
    • QoS: 1 (entrega garantida)
    • Retain: true (mensagem persistida)

💡 Este recurso é crucial para aplicações críticas que precisam saber se um dispositivo caiu da rede.


Suporte a TLS/SSL

#if LWIP_ALTCP && LWIP_ALTCP_TLS

Com Certificados (2-Way Authentication)

#ifdef MQTT_CERT_INC
static const uint8_t ca_cert[] = TLS_ROOT_CERT;
static const uint8_t client_key[] = TLS_CLIENT_KEY;
static const uint8_t client_cert[] = TLS_CLIENT_CERT;

state.mqtt_client_info.tls_config =
    altcp_tls_create_config_client_2wayauth(ca_cert, sizeof(ca_cert),
                                            client_key, sizeof(client_key),
                                            client_cert, sizeof(client_cert));
  • Usa certificados embarcados para autenticação mútua (cliente e servidor), ideal para ambientes corporativos e industriais com alto grau de segurança.
  • Exige configuração adequada dos certificados e chaves compiladas no binário.

Sem Certificado (Cliente apenas valida o servidor)

#else
state->client_info.tls_config = altcp_tls_create_config_client(NULL, 0);
WARN_printf("Warning: tls without a certificate is insecure\n");
#endif
  • Caso os certificados não estejam incluídos, o cliente utiliza TLS com verificação limitada — ainda criptografado, mas com validação mais frágil.

Comentário Técnico

Esse trecho demonstra um firmware MQTT:

  • Flexível (funciona com ou sem autenticação, com ou sem TLS).
  • Seguro (suporte completo a LWT, TLS, verificação de hostname).
  • Portável (uso de constantes e macros permite adaptação por #define).

Esse padrão de configuração modular é altamente recomendado em projetos profissionais que envolvam conectividade segura e confiável com brokers MQTT.


Conexão Wi-Fi e Resolução DNS do Broker MQTT (Fechamento da main())

A última parte da função main() conclui o processo de inicialização do firmware com duas etapas essenciais:

  1. Estabelecimento da conexão Wi-Fi
  2. Resolução do endereço IP do broker MQTT via DNS

Habilitação do Modo Station (STA) e Conexão à Rede Wi-Fi

cyw43_arch_enable_sta_mode();
if (cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000)) {
    panic("Failed to connect");
}
printf("\nConnected to Wifi\n");
  • Ativa o modo station (STA), permitindo que o dispositivo embarcado se conecte a uma rede Wi-Fi existente.
  • A conexão é tentada com o SSID e senha definidos nas macros WIFI_SSID e WIFI_PASSWORD, usando criptografia WPA2.
  • Um timeout de 30 segundos evita travamentos indefinidos.
  • Em caso de falha, o sistema aborta com panic(), indicando problema de rede.

Resolução DNS do Broker MQTT

cyw43_arch_lwip_begin();
int err = dns_gethostbyname(MQTT_BROKER, &state.mqtt_broker_address, dns_found, &state);
cyw43_arch_lwip_end();
  • A função dns_gethostbyname() tenta resolver o hostname do broker (MQTT_BROKER) para um endereço IP.
  • Se o IP estiver em cache, a função retorna ERR_OK e o endereço é imediatamente acessado.
  • Caso contrário, ela retorna ERR_INPROGRESS, e o callback dns_found() será invocado assim que o IP for resolvido.

O uso de cyw43_arch_lwip_begin() e cyw43_arch_lwip_end() garante que o acesso à pilha lwIP esteja sincronizado — uma prática essencial ao lidar com recursos de rede em firmware bare-metal.


Decisão Pós-Resolução

if (err == ERR_OK) {
    start_client(&state);
} else if (err != ERR_INPROGRESS) {
    panic("dns request failed");
}
  • Se o IP foi resolvido imediatamente, a função start_client() é chamada para conectar-se ao broker e iniciar a comunicação MQTT.
  • Se o processo falhar (erro diferente de ERR_INPROGRESS), o sistema exibe um erro fatal.

💡 Com isso, o firmware está pronto para operar de forma assíncrona: o resto do funcionamento será tratado pelos callbacks definidos ao longo do código.


Conclusão da main()

Com essa lógica, a função main() garante que o firmware:

  • Se conecte corretamente à rede Wi-Fi
  • Resolva dinamicamente o endereço do broker MQTT
  • Ative a comunicação segura (com ou sem TLS)
  • Inicie o cliente MQTT apenas quando o sistema estiver totalmente funcional

🧩 Super Loop MQTT + Wi-Fi Polling

while (!state.connect_done || mqtt_client_is_connected(state.mqtt_client_inst)) {
    cyw43_arch_poll();
    cyw43_arch_wait_for_work_until(make_timeout_time_ms(10000));
}

🔍 Explicação passo a passo:

🧠 Condição do while

(!state.connect_done || mqtt_client_is_connected(...))

Esse loop se mantém ativo enquanto a conexão MQTT não estiver estabelecida (state.connect_done == false) ou estiver conectada (mqtt_client_is_connected(...) == true).

  • Assim, o loop cobre tanto a fase de tentativa de conexão, quanto a fase de operação contínua.
  • Ele finaliza somente após uma desconexão voluntária (como após o tópico "exit" ser recebido e stop_client = true ser acionado).

🔁 cyw43_arch_poll();

  • Essa chamada é essencial para manter o driver da pilha Wi-Fi (CYW43) responsivo:
    • Trata interrupções
    • Gerencia o handshake de pacotes TCP/IP
    • Mantém conexões TLS ativas

Sem isso, o Wi-Fi congela em ambientes bare-metal.


⏱️ cyw43_arch_wait_for_work_until(...)

cyw43_arch_wait_for_work_until(make_timeout_time_ms(10000));
  • Entra em modo de espera eficiente, sem consumo ativo de CPU, até:
    • Um evento de rede ou hardware ocorrer
    • Ou passar o tempo máximo especificado (aqui, 10.000 ms)

Importância:

  • Ideal para reduzir o consumo de energia (útil em sistemas IoT alimentados por bateria)
  • Mantém o firmware bloqueado em uma espera supervisionada

💡 Comparando com mqtt_client_yield()

Anteriormente você havia mencionado mqtt_client_yield(), mas aqui foi usado um loop integrado com o driver CYW43 — o que é mais adequado quando se usa Pico W com lwIP.

  • mqtt_client_yield() pode ser mais comum em pilhas como a do Paho ou lwESP
  • Neste caso, o mqtt_client está embutido na estrutura lwIP e requer interação com o scheduler do driver da Wi-Fi

🔐 Segurança e Robustez

Esse super loop também evita falhas por:

  • Time-outs MQTT
  • Perda de keep-alive
  • Requisições de unsubscribe
  • Reinicialização forçada se for desconectado do broker

✅ Conclusão

Esse trecho representa o coração reativo do firmware MQTT, mantendo:

  • A conectividade com o broker
  • A recepção de mensagens
  • O tratamento adequado do hardware Wi-Fi

Configurações do Projeto: CMakeLists.txt

O processo de construção de um firmware embarcado moderno depende de uma configuração adequada do sistema de build. No ecossistema C/C++ para embarcados, o CMake se tornou uma ferramenta amplamente adotada por sua capacidade de gerar arquivos de construção específicos para diferentes plataformas e IDEs, como GNU Make, Ninja ou mesmo Visual Studio Code. No contexto deste projeto, o arquivo CMakeLists.txt é o ponto central que orquestra a organização dos arquivos-fonte, define as dependências, configura opções de compilação, integra bibliotecas como lwIP, CYW43, MQTT, e estabelece os parâmetros que garantirão a correta vinculação do firmware com o hardware-alvo.

Nesta seção, vamos nos aprofundar na estrutura e nos elementos críticos presentes no arquivo CMakeLists.txt, destacando como ele influencia a geração do firmware, o uso de TLS (quando habilitado), o suporte à pilha de rede via lwIP, e a integração com o cliente MQTT. Também abordaremos como esse arquivo está organizado para facilitar a portabilidade, manutenção e expansão do projeto.

A seguir, analisaremos uma das versões mais completas do CMakeLists.txt utilizadas neste projeto.


Configurações do Ambiente

O trecho inicial do arquivo CMakeLists.txt tem como principal função configurar o ambiente de desenvolvimento, garantindo que a compilação ocorra de forma adequada tanto no Windows quanto em sistemas baseados em Unix (Linux/macOS). Esse bloco realiza, em primeiro lugar, a detecção do sistema operacional e define o caminho da variável USERHOME, que será utilizada posteriormente para localizar arquivos auxiliares da extensão do VS Code para Raspberry Pi Pico.

Logo após, são definidas três variáveis cruciais para o build do projeto:

  • sdkVersion: versão do SDK do Raspberry Pi Pico a ser utilizada;
  • toolchainVersion: versão do conjunto de ferramentas de compilação (toolchain) especificada;
  • picotoolVersion: versão do utilitário picotool, usado para interagir com o dispositivo via USB.

Outro ponto importante neste bloco é a verificação da existência do arquivo pico-vscode.cmake, responsável por integrar o ambiente do Visual Studio Code à infraestrutura de build do Pico SDK. Caso o arquivo seja encontrado no diretório correto, ele é incluído automaticamente no build através da instrução include.

Além disso, é feita a definição da placa-alvo com a diretiva:

set(PICO_BOARD pico_w CACHE STRING "Board type")

Isso garante que o build será direcionado à variante pico_w, compatível com conectividade Wi-Fi e usada neste projeto.

Finalizando este trecho, são definidos os padrões das linguagens C e C++ que o projeto irá seguir (C11 e C++17, respectivamente), além da ativação da exportação dos comandos de compilação, útil para ferramentas de análise estática ou assistentes de IDE.


Parametrizando a Conexão

Nesta seção do CMakeLists.txt, são definidos os parâmetros que controlam a conexão Wi-Fi e o uso do protocolo MQTT. Para tornar o projeto flexível e facilmente configurável, utiliza-se o mecanismo de variáveis de ambiente via .env, permitindo adaptar o firmware a diferentes redes ou brokers sem alterar diretamente o código-fonte.

O trecho inicia com verificações para garantir que as variáveis esperadas estejam definidas. Caso não estejam, valores padrão são atribuídos, acompanhados de mensagens de advertência para o usuário. As variáveis consideradas são:

  • WIFI_SSID: o nome da rede Wi-Fi à qual o dispositivo deve se conectar (padrão: "ArvoreDosSaberes").
  • WIFI_PASSWORD: a senha da rede Wi-Fi (padrão: "Arduino2022").
  • MQTT_BROKER: endereço do servidor MQTT (padrão: "mqtt.rapport.tec.br").
  • MQTT_BASE_TOPIC: prefixo base para os tópicos MQTT utilizados (padrão: "rack_inteligente").
  • MQTT_RACK_NUMBER: número da rack ou identificador do dispositivo. Essa variável é obrigatória, e sua ausência gera erro fatal, interrompendo o processo de configuração.

Após essa checagem e atribuição condicional, os valores são transferidos para variáveis internas do CMake por meio de set(), assegurando que todo o sistema de build tenha acesso uniforme a essas informações.

Esse mecanismo de fallback é crucial para ambientes de desenvolvimento colaborativo ou para automações CI/CD (Integração Contínua e Entrega Contínua), permitindo consistência e segurança ao preparar o firmware para diferentes contextos operacionais.


Parametrização Final e Integração com o Build

A última etapa do CMakeLists.txt consolida a preparação do projeto, integrando o firmware MQTT com o SDK do Raspberry Pi Pico, as bibliotecas necessárias e as definições finais para a geração do executável.

A diretiva include(pico_sdk_import.cmake) importa o SDK oficial do RP2040 e deve ser chamada antes da definição do projeto para garantir que todas as ferramentas estejam disponíveis. Em seguida, o comando project(...) define o nome do projeto (firmware_client_mqtt) e especifica que ele será compilado com suporte às linguagens C, C++ e Assembly — recurso importante caso haja funções de baixo nível otimizadas diretamente em ASM.

A função pico_sdk_init() inicializa o ambiente do SDK e é fundamental para configurar os drivers e a estrutura básica de compilação.

O add_executable(...) define qual será o nome do executável gerado e quais arquivos-fonte compõem a aplicação. Aqui, é apenas um: firmware_client_mqtt.c.

Os comandos subsequentes, como pico_set_program_name e pico_set_program_version, são auxiliares para identificação do binário e podem ser úteis em processos automatizados ou logs no dispositivo.

A serial UART é desabilitada com pico_enable_stdio_uart(...) e a USB é ativada com pico_enable_stdio_usb(...), sinalizando que a comunicação serial será feita exclusivamente via USB, prática comum em firmwares modernos baseados em Pico W.

A seção target_link_libraries(...) estabelece as dependências do projeto com bibliotecas essenciais, como:

  • pico_stdlib: biblioteca padrão do RP2040;
  • pico_cyw43_arch_lwip_threadsafe_background: driver do Wi-Fi com pilha LWIP em modo cooperativo seguro;
  • pico_lwip_mqtt: cliente MQTT baseado em LWIP.

Com target_include_directories(...), o diretório atual é incluído no path de headers do compilador, permitindo que arquivos de cabeçalho locais sejam encontrados com facilidade.

Por fim, target_compile_definitions(...) injeta todas as variáveis de ambiente processadas anteriormente (WIFI_SSID, MQTT_BROKER, etc.) como macros pré-processadas diretamente no código C/C++. Isso garante que o firmware seja compilado com os parâmetros corretos sem a necessidade de edição direta nos arquivos-fonte.

Essa abordagem modular e robusta é ideal para projetos profissionais e colaborativos, promovendo reusabilidade, portabilidade e controle de configuração de forma elegante e segura.


Conclusão Final

Ao longo deste artigo, analisamos em profundidade o desenvolvimento de um firmware para microcontrolador Raspberry Pi Pico W com conectividade Wi-Fi e suporte ao protocolo MQTT, utilizando uma pilha leve baseada em LWIP. Passamos por todas as etapas essenciais de estruturação do projeto, desde a configuração do ambiente de compilação com CMake, passando pela inicialização do hardware, até a implementação dos principais callbacks e do laço principal de execução (super loop).

Um dos pontos fortes desse projeto é sua modularidade. O uso de um arquivo .env.cmake para parametrização torna o código-fonte mais limpo, reutilizável e portável. As mensagens de WARNING e FATAL_ERROR no CMakeLists.txt ajudam o desenvolvedor a identificar e corrigir falhas de configuração ainda na fase de build, evitando surpresas em tempo de execução. Essa abordagem é típica de projetos bem organizados, principalmente quando se deseja escalar a solução para múltiplos dispositivos ou contextos de uso distintos.

A organização do código C, com funções bem segmentadas como start_client, dns_found e o mqtt_connection_cb, demonstra atenção às boas práticas de desenvolvimento de firmware embarcado. A separação de responsabilidades, o uso de callbacks assíncronos e a sincronização por meio do cyw43_arch_lwip_begin() e end() indicam um domínio prático da arquitetura da pilha LWIP e do modelo de operação do chip CYW43xx — fundamental para garantir estabilidade em ambientes concorrentes.

Por outro lado, vale destacar que o projeto ainda pode ser expandido com estratégias mais avançadas, como:

  • Implementação de reconexão automática com backoff exponencial em caso de falhas MQTT ou Wi-Fi;
  • Inclusão de mensagens de keep-alive customizadas e heartbeat para o broker;
  • Suporte a certificados dinâmicos e provisionamento seguro em campo;
  • Logging persistente via SPIFFS ou flash externa, útil para diagnósticos offline.

Além disso, o super loop adotado, embora eficiente para aplicações simples, pode ser futuramente substituído por um agendador cooperativo ou preemptivo, como um kernel RTOS leve (ex: FreeRTOS). Isso traria maior controle sobre tarefas críticas e aumentaria a previsibilidade temporal — algo relevante em aplicações com múltiplas interfaces (I2C, SPI, sensores) ou requisitos de tempo real.

Em termos didáticos, este projeto serve como base sólida para quem deseja compreender não apenas o funcionamento de um cliente MQTT embarcado, mas também como organizar um projeto moderno em C/C++ com suporte a rede segura (TLS), abstrações de build bem definidas, e comunicação baseada em tópicos. Cada trecho do código apresentado pode ser reaproveitado ou ampliado conforme o domínio de aplicação: automação residencial, sistemas IoT industriais, controle de racks inteligentes, entre outros.


Como este Artigo foi Escrito

Este artigo foi escrito com base no trabalho acadêmico feito para o Projeto Embarcatech, após o trabalho pronto entreguei ao chatgpt as imagens e códigos, além do texto escrito para o trabalho, para que ele ajustasse, corrigisse e reestruturasse o trabalho.

0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários

Related Post

0
Adoraria saber sua opinião, comente.x