Tutoriais de IoT

Tutorial do Cliente Paho MQTT C para Linux Embarcado

Um tutorial prático de Paho MQTT C para Linux embarcado: compile a biblioteca com OpenSSL, escreva um publicador e um assinante TLS, faça cross-compile para ARM e rode tudo de forma confiável sob o systemd.

Cliente Paho MQTT C rodando em uma placa Linux embarcada e publicando dados de sensores via TLS para um broker MQTT na nuvem
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

A biblioteca Paho MQTT C é a escolha padrão quando você precisa de um cliente MQTT pequeno e com poucas dependências no Linux embarcado. Neste tutorial você vai compilar o Paho MQTT C com OpenSSL, escrever um publicador TLS e um assinante, fazer cross-compile para uma placa ARM e rodar o resultado como um serviço systemd resiliente, conectado a um broker na nuvem.

Por que Paho MQTT C para Linux embarcado

O Eclipse Paho C é C99 puro, com uma única dependência opcional, o OpenSSL, para TLS. Ele roda em tudo, de um Raspberry Pi a um gateway industrial com poucos megabytes de RAM, suporta MQTT 3.1, 3.1.1 e 5, e está empacotado na maioria das distribuições e camadas Yocto. Se você está escolhendo entre linguagens e bibliotecas, a visão geral de bibliotecas cliente MQTT coloca a biblioteca em contexto; se preferir C++, o Paho também oferece um wrapper C++ sobre esta biblioteca.

A biblioteca vem em quatro variantes. Escolha a certa na hora do link:

Biblioteca Estilo de API TLS Uso típico
paho-mqtt3c Síncrona MQTTClient_* Não Ferramentas simples em redes confiáveis
paho-mqtt3cs Síncrona MQTTClient_* Sim A maioria dos publicadores e assinantes embarcados
paho-mqtt3a Assíncrona MQTTAsync_* Não Apps orientados a eventos
paho-mqtt3as Assíncrona MQTTAsync_* Sim Apps orientados a eventos com TLS

Este tutorial usa paho-mqtt3cs: a API síncrona com TLS. "Síncrona" significa que as chamadas de conexão e publicação bloqueiam até terminar; as mensagens recebidas continuam chegando em uma thread em segundo plano, por meio de callbacks.

Instalando ou compilando a biblioteca

No Debian, Ubuntu ou Raspberry Pi OS, a versão empacotada geralmente basta:

sudo apt update
sudo apt install -y build-essential libpaho-mqtt-dev libssl-dev ca-certificates

Para obter a versão mais recente, ou para fazer cross-compile, compile a partir do código-fonte:

git clone https://github.com/eclipse-paho/paho.mqtt.c.git
cd paho.mqtt.c
cmake -B build -DPAHO_WITH_SSL=TRUE -DPAHO_BUILD_SAMPLES=FALSE -DPAHO_BUILD_STATIC=FALSE -DCMAKE_BUILD_TYPE=Release
cmake --build build -j"$(nproc)"
sudo cmake --install build
sudo ldconfig

A documentação oficial do Paho C descreve cada estrutura e função usada a seguir.

Um publicador Paho MQTT C via TLS

Este programa se conecta a broker.connectmq.com na porta 8883, verifica o certificado do servidor contra o bundle de CAs do sistema e publica uma leitura a cada cinco segundos. Ele funciona de forma anônima no servidor MQTT público (apenas para testes) e lê as credenciais de variáveis de ambiente quando você migra para um broker privado.

publisher.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include "MQTTClient.h"

#define ADDRESS   "ssl://broker.connectmq.com:8883"
#define CA_BUNDLE "/etc/ssl/certs/ca-certificates.crt"
#define QOS       1
#define TIMEOUT   10000L

static volatile sig_atomic_t running = 1;
static void on_signal(int sig) { (void)sig; running = 0; }

static void connection_lost(void *ctx, char *cause) {
    (void)ctx;
    fprintf(stderr, "connection lost: %s\n", cause ? cause : "unknown");
}

static int message_arrived(void *ctx, char *topic, int len, MQTTClient_message *msg) {
    (void)ctx; (void)len;
    MQTTClient_freeMessage(&msg);
    MQTTClient_free(topic);
    return 1;
}

static int connect_broker(MQTTClient client) {
    MQTTClient_connectOptions opts = MQTTClient_connectOptions_initializer;
    MQTTClient_SSLOptions ssl = MQTTClient_SSLOptions_initializer;

    ssl.trustStore = CA_BUNDLE;
    ssl.enableServerCertAuth = 1;
    ssl.verify = 1;                       /* verifica também o hostname */
    ssl.sslVersion = MQTT_SSL_VERSION_TLS_1_2;

    opts.keepAliveInterval = 30;
    opts.cleansession = 1;
    opts.ssl = &ssl;
    opts.username = getenv("MQTT_USERNAME");   /* NULL = anônimo */
    opts.password = getenv("MQTT_PASSWORD");

    int rc = MQTTClient_connect(client, &opts);
    if (rc != MQTTCLIENT_SUCCESS)
        fprintf(stderr, "connect failed, rc=%d\n", rc);
    return rc;
}

int main(void) {
    const char *client_id = getenv("MQTT_CLIENT_ID");
    const char *prefix = getenv("MQTT_TOPIC_PREFIX");
    char id_buf[64], topic[256], payload[128];

    if (!client_id) {
        snprintf(id_buf, sizeof id_buf, "paho-c-%d", (int)getpid());
        client_id = id_buf;
    }
    if (!prefix) prefix = "connectmq/demo/paho-c-4821";
    snprintf(topic, sizeof topic, "%s/gateway1/temperature", prefix);

    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    MQTTClient client;
    int rc = MQTTClient_create(&client, ADDRESS, client_id,
                               MQTTCLIENT_PERSISTENCE_NONE, NULL);
    if (rc != MQTTCLIENT_SUCCESS) {
        fprintf(stderr, "create failed, rc=%d\n", rc);
        return EXIT_FAILURE;
    }
    MQTTClient_setCallbacks(client, NULL, connection_lost, message_arrived, NULL);

    int backoff = 1;
    while (running && connect_broker(client) != MQTTCLIENT_SUCCESS) {
        sleep(backoff);
        if (backoff < 60) backoff *= 2;
    }
    printf("connected as %s, publishing to %s\n", client_id, topic);

    srand((unsigned)time(NULL));
    while (running) {
        if (!MQTTClient_isConnected(client)) {
            if (connect_broker(client) != MQTTCLIENT_SUCCESS) { sleep(5); continue; }
        }

        double temp = 20.0 + (rand() % 100) / 10.0;
        int n = snprintf(payload, sizeof payload,
                         "{\"ts\":%ld,\"temperature\":%.1f}", (long)time(NULL), temp);

        MQTTClient_message msg = MQTTClient_message_initializer;
        MQTTClient_deliveryToken token;
        msg.payload = payload;
        msg.payloadlen = n;
        msg.qos = QOS;
        msg.retained = 0;

        rc = MQTTClient_publishMessage(client, topic, &msg, &token);
        if (rc == MQTTCLIENT_SUCCESS)
            rc = MQTTClient_waitForCompletion(client, token, TIMEOUT);
        if (rc != MQTTCLIENT_SUCCESS)
            fprintf(stderr, "publish failed, rc=%d\n", rc);
        else
            printf("sent %s\n", payload);

        sleep(5);
    }

    MQTTClient_disconnect(client, TIMEOUT);
    MQTTClient_destroy(&client);
    return EXIT_SUCCESS;
}

Compile e execute:

gcc -Wall -O2 -o publisher publisher.c -lpaho-mqtt3cs
./publisher

Algumas linhas merecem explicação. ssl.verify = 1 ativa a verificação do hostname, que vem desligada por padrão; sem ela, qualquer certificado válido para qualquer host seria aceito. Chamar MQTTClient_setCallbacks antes de conectar coloca o cliente em modo multithread, de modo que a biblioteca executa o keep-alive e o processamento de recebimento na própria thread, e connection_lost avisa quando o link cai. O loop de publicação verifica MQTTClient_isConnected e reconecta com uma tentativa própria, porque a API síncrona não reconecta automaticamente.

Um assinante com callbacks

O assinante reutiliza a mesma lógica de conexão e faz o seu trabalho em message_arrived. Substitua o main por este (mantendo as funções auxiliares acima):

static int on_message(void *ctx, char *topic, int len, MQTTClient_message *msg) {
    (void)ctx; (void)len;
    printf("%s: %.*s\n", topic, msg->payloadlen, (char *)msg->payload);
    MQTTClient_freeMessage(&msg);
    MQTTClient_free(topic);
    return 1;   /* 1 = mensagem tratada; 0 pede à biblioteca que reentregue */
}

int main(void) {
    const char *prefix = getenv("MQTT_TOPIC_PREFIX");
    char filter[256];
    if (!prefix) prefix = "connectmq/demo/paho-c-4821";
    snprintf(filter, sizeof filter, "%s/+/temperature", prefix);

    signal(SIGINT, on_signal);
    signal(SIGTERM, on_signal);

    MQTTClient client;
    MQTTClient_create(&client, ADDRESS, "paho-c-sub-4821",
                      MQTTCLIENT_PERSISTENCE_NONE, NULL);
    MQTTClient_setCallbacks(client, NULL, connection_lost, on_message, NULL);

    while (running) {
        if (!MQTTClient_isConnected(client)) {
            if (connect_broker(client) == MQTTCLIENT_SUCCESS) {
                MQTTClient_subscribe(client, filter, QOS);
                printf("subscribed to %s\n", filter);
            } else {
                sleep(5);
                continue;
            }
        }
        sleep(1);
    }
    MQTTClient_disconnect(client, TIMEOUT);
    MQTTClient_destroy(&client);
    return 0;
}

Liberar tanto a mensagem quanto a string do topic é obrigatório; esquecer qualquer um dos dois é o vazamento de memória mais comum em programas Paho C. Mantenha o callback curto. Se o processamento for lento (gravar em flash, chamar uma API HTTP), copie o payload para uma fila e retorne, porque o callback roda na thread de recebimento da biblioteca.

Você também pode acompanhar o publicador sem escrever código nenhum:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-4821 -t "connectmq/demo/paho-c-4821/#" -v

Cross-compile para uma placa ARM

Para imagens Yocto ou Buildroot, o Paho C tem receitas prontas. Para um cross build manual e rápido, aponte o CMake para o seu toolchain e para um OpenSSL compilado de forma cruzada:

cmake -B build-arm \
  -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
  -DCMAKE_SYSTEM_NAME=Linux \
  -DPAHO_WITH_SSL=TRUE \
  -DOPENSSL_ROOT_DIR=/opt/sysroot/usr \
  -DPAHO_BUILD_SHARED=TRUE -DPAHO_BUILD_SAMPLES=FALSE \
  -DCMAKE_INSTALL_PREFIX=/opt/paho-arm -DCMAKE_INSTALL_LIBDIR=lib
cmake --build build-arm -j"$(nproc)"
cmake --install build-arm

aarch64-linux-gnu-gcc -O2 -o publisher publisher.c \
  -I/opt/paho-arm/include -L/opt/paho-arm/lib -lpaho-mqtt3cs -lssl -lcrypto -lpthread

# Copie o binário e a biblioteca compartilhada para a placa
scp publisher /opt/paho-arm/lib/libpaho-mqtt3cs.so* root@gateway:/usr/local/lib/

Na placa, mova publisher para /usr/local/bin e execute ldconfig. O Paho também pode ser compilado como biblioteca estática (-DPAHO_BUILD_STATIC=TRUE) para evitar distribuir um .so separado, mas aí as atualizações do OpenSSL desse binário ficam por sua conta. Verifique se a imagem de destino contém um bundle de CAs; imagens mínimas muitas vezes não têm, e o TLS falha com um erro de verificação de certificado mesmo com o código correto. Se o mesmo gateway também controla uma tela touch local, o tutorial de cliente MQTT com Qt mostra como construir o lado da IHM em C++, e como testar clientes MQTT explica como colocar este código sob CI.

QoS, persistência e buffer offline

Gateways perdem conectividade, então decida desde o início o que acontece com os dados durante uma queda. O cliente acima usa MQTTCLIENT_PERSISTENCE_NONE, que mantém as mensagens QoS 1 em trânsito apenas na memória; se o processo reiniciar no meio da entrega, essas mensagens se perdem. Em vez disso, o Paho pode persistir as mensagens em trânsito em disco: passe MQTTCLIENT_PERSISTENCE_DEFAULT e um caminho de diretório como os dois últimos argumentos de MQTTClient_create, conecte com cleansession = 0, e a biblioteca reenvia tudo o que ainda não foi confirmado após uma reconexão. Em armazenamento flash, pese isso contra o desgaste por escrita, porque persistir cada mensagem em um cartão SD barato encurta a vida útil dele.

A persistência só cobre mensagens já entregues à biblioteca. Leituras feitas enquanto o link está fora do ar nunca chegam a MQTTClient_publishMessage no exemplo, porque o loop pula a publicação até reconectar. Se você precisa de todas as leituras, grave-as em um buffer circular local (um arquivo ou um banco SQLite) e esvazie-o depois de reconectar, das mais antigas para as mais novas, com o timestamp original no payload para que o back end posicione cada valor corretamente.

A escolha do QoS acompanha os dados. Telemetria periódica que é substituída a cada poucos segundos funciona bem com QoS 0 ou 1; comandos e alarmes merecem QoS 1. O QoS 2 acrescenta uma ida e volta extra e raramente é necessário em gateways. O plano gratuito ConnectMQ Serverless suporta QoS até 1, e os planos pagos acrescentam o QoS 2. O guia dos níveis de QoS do MQTT explica as garantias de entrega em detalhes.

Rodando como serviço systemd

/etc/systemd/system/mqtt-publisher.service:

[Unit]
Description=Paho MQTT C publisher
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/publisher
Environment=MQTT_CLIENT_ID=YOUR_CLIENT_ID
Environment=MQTT_USERNAME=YOUR_USERNAME
Environment=MQTT_PASSWORD=YOUR_PASSWORD
Environment=MQTT_TOPIC_PREFIX=acme/plant1/gateway1
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-publisher
journalctl -u mqtt-publisher -f

As credenciais acima vêm do seu broker privado. Com um broker MQTT gerenciado como o ConnectMQ, você cria o broker no portal e recebe usuário, senha e client ID gerados; os client IDs ficam vinculados a essas credenciais, então dê a cada gateway o seu próprio conjunto. Para qualquer coisa além de um punhado de dispositivos, mova os segredos para um EnvironmentFile legível apenas pelo root.

Códigos de retorno e solução de problemas

Código de retorno / sintoma Significado O que verificar
-1 (MQTTCLIENT_FAILURE) ao conectar com TLS Falha no handshake ou no certificado Caminho do bundle de CAs, relógio do sistema (o TLS falha se a data estiver errada), hostname
4 Usuário ou senha inválidos Credenciais, quebra de linha no final do arquivo de ambiente
5 Não autorizado O client ID não corresponde às credenciais emitidas
2 Identificador rejeitado Client ID vazio ou duplicado
A conexão trava na 8883 Firewall bloqueia a porta Teste com openssl s_client -connect broker.connectmq.com:8883
A memória cresce ao longo de dias Mensagens ou topics não liberados MQTTClient_freeMessage e MQTTClient_free em todos os caminhos do callback

Vale insistir na questão do relógio no Linux embarcado: placas sem relógio de tempo real inicializam em 1970, e todo certificado parece "ainda não válido" até o NTP sincronizar. Ordene o seu serviço depois de time-sync.target se isso for um problema. Mais padrões de falha de TLS estão em MQTT TLS handshake failed.

Comece a usar o ConnectMQ

O seu gateway em C está pronto para um broker real. Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito, coloque as credenciais geradas na unit do systemd e veja as mensagens chegando no explorador de topics ao vivo do portal. Quando a frota crescer, o mesmo binário se conecta a brokers pagos, dedicados ou BYOC sem mudanças no código.

Perguntas frequentes

Faça o link com paho-mqtt3cs para a API síncrona com TLS, ou com paho-mqtt3as para a API assíncrona com TLS. As variantes sem o s no final não têm suporte a SSL.

O cliente Paho MQTT C reconecta automaticamente?

A API síncrona MQTTClient não reconecta, então verifique MQTTClient_isConnected e reconecte você mesmo. A API assíncrona MQTTAsync oferece a opção automaticReconnect.

O Paho C suporta MQTT 5?

Sim. Crie o cliente com MQTTClient_createWithOptions e MQTTVERSION_5 e use a família de funções MQTTClient_connect5 e MQTTClient_publish5; o ConnectMQ aceita tanto MQTT 3.1.1 quanto 5.0.

Quanta memória o Paho MQTT C precisa?

Ele é pequeno o suficiente para gateways Linux embarcados típicos, mas o consumo exato depende das opções de build e do OpenSSL. Meça no seu hardware de destino com a variante que você realmente usa no link.

Artigos relacionados

Melhores Ferramentas CLI MQTT para Testes, Scripts e Depuração

Uma comparação prática entre mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, a CLI do MQTT.js, curl e mqttui, com o mesmo teste rodando no broker.connectmq.com, para você escolher a ferramenta certa para scripts e depuração.

9 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.