Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
/
/
Leitor de Sinais Infravermelhos com ESP32 e Display LCD: decodificando controles remotos

Leitor de Sinais Infravermelhos com ESP32 e Display LCD: decodificando controles remotos

Sumário

Baixe agora a apostila Arduino Iniciante e comece seus projetos

Compartilhe seu conhecimento e receba em dinheiro ou produtos.

Introdução

Aprenda a capturar comandos de controles remotos, identificar protocolos conhecidos e analisar sinais infravermelhos desconhecidos com ESP32. Controles remotos infravermelhos estão presentes em uma enorme quantidade de equipamentos eletrônicos. Televisores, aparelhos de som, ar-condicionado, ventiladores, luminárias, fitas LED, climatizadores e diversos outros dispositivos utilizam sinais infravermelhos para receber comandos.

À primeira vista, parece que pressionar um botão simplesmente faz o controle enviar um código. Porém, por trás dessa ação existe uma sequência de pulsos cuidadosamente temporizados. Cada botão do controle remoto gera uma determinada sequência de MARKs e SPACEs, e essa sequência segue regras definidas pelo protocolo utilizado pelo fabricante.

Neste projeto, vamos utilizar um ESP32, um receptor infravermelho e um display LCD 16x2 com interface I2C para construir um analisador capaz de capturar esses sinais.

A grande diferença em relação a um simples projeto de leitura de controle remoto é que nosso sistema não trabalha somente com protocolos conhecidos. Quando a biblioteca consegue reconhecer o sinal, o ESP32 apresenta informações como:

  • Protocolo;
  • Endereço;
  • Comando;
  • Quadro completo;
  • Quantidade de bits.

Quando o protocolo não é reconhecido, o ESP332 não descarta o sinal. Ele apresenta o sinal como DESCONHECIDO, calcula um identificador em forma de hash e, principalmente, mostra os tempos RAW de MARK e SPACE.

Isso permite estudar controles remotos proprietários e sinais que não possuem um protocolo diretamente reconhecido pela biblioteca. Esse recurso é particularmente útil para investigar controles de equipamentos como fitas LED, humidificadores, climatizadores e outros dispositivos que utilizam protocolos proprietários ou formatos que não são identificados automaticamente pela biblioteca.

O que vamos construir?

Uma simples analisador de sinais IR que utiliza um ESP32 como microcontrolador e um display LCD. O LCD funciona como a interface principal do equipamento. O monitor serial pode ser utilizado principalmente durante o desenvolvimento e a análise dos sinais, nele é apresentado informações adicionais que não cabem no display.

No firmware utilizado neste projeto, os dados relevantes apresentados no LCD são exibidos em hexadecimal, justamente para facilitar a comparação com tabelas de códigos, documentação técnica e informações obtidas durante a análise.

O que é infravermelho?

A luz infravermelha, ou IR (Infrared), é uma forma de radiação eletromagnética que não pode ser percebida pelo olho humano. Nos controles remotos, um LED infravermelho é utilizado para transmitir informações para o equipamento controlado. O LED não fica simplesmente ligado durante toda a transmissão. Ele é acionado e desligado em intervalos extremamente rápidos.

Um receptor IR demodulador detecta essa transmissão e transforma a informação em um sinal digital que pode ser interpretado pelo ESP32. É importante entender que o receptor não entrega diretamente algo como:

BOTÃO = POWER

O que chega ao microcontrolador é uma sequência temporal. Podemos representar simplificadamente:

A duração de cada MARK e SPACE é importante.

O que são MARK e SPACE?

O MARK representa o período em que existe transmissão. No projeto, consideramos o MARK como o período em que o LED IR está ativo, enquanto o SPACE representa o intervalo sem emissão. Um comando completo é formado por várias dessas regiões. Por exemplo:

O protocolo determina quanto tempo cada uma dessas regiões deve durar.
  • MARK: Um período específico de envio de pulsos
  • SPACE: Um período específico de não enviar nada

Dependendo dessas durações, o sistema pode interpretar a sequência como um determinado protocolo e, posteriormente, como um endereço e um comando. Para mais informações e exemplos dobre Controle e Receptor IR veja o artigo: Guia Completo do Controle Remoto IR + Receptor IR para Arduino veja também esse outro artigo: Entendendo os fundamentos das comunicações no infravermelho.

O que é um protocolo infravermelho?

Um protocolo IR é um conjunto de regras utilizado para representar informações através dos pulsos infravermelhos. Existem diversos protocolos utilizados por fabricantes diferentes. Entre eles estão:

  • NEC;
  • Sony SIRC;
  • RC5;
  • RC6;
  • Samsung;
  • LG;
  • JVC;
  • Denon;
  • Panasonic;
  • Kaseikyo;
  • Pulse Distance;
  • Pulse Width;
  • entre outros.

No nosso firmware, diversos desses decodificadores são habilitados explicitamente. O código utiliza DECODE_NEC, DECODE_SONY, DECODE_RC5, DECODE_RC6, DECODE_SAMSUNG, DECODE_LG, DECODE_JVC, DECODE_DENON, DECODE_PANASONIC, DECODE_KASEIKYO, além do decodificador genérico DECODE_DISTANCE_WIDTH e do mecanismo de hash. Isso permite que o analisador reconheça uma grande variedade de controles.

Conhecido não significa igual

Um ponto importante é que dois controles podem utilizar o mesmo protocolo e ainda assim transmitir comandos diferentes. Por exemplo, dois equipamentos podem utilizar NEC, mas possuir diferentes endereços e diferentes comandos. Podemos imaginar:

CONTROLE A
Protocolo: NEC
Endereço: 0x10
Comando:  0x45

e:

CONTROLE B
Protocolo: NEC
Endereço: 0x20
Comando:  0x45

O protocolo é o mesmo, mas o endereço permite diferenciar os dispositivos. Por isso, o analisador apresenta separadamente:

  • Protocolo;
  • Endereço;
  • Comando;
  • Quadro completo;
  • Quantidade de bits.

E quando o protocolo é desconhecido?

Imagine que você encontre um controle remoto que não seja reconhecido pela biblioteca. Em vez de simplesmente mostrar:

UNKNOWN

e parar por aí, nosso firmware captura informações adicionais. O resultado pode ser:

PROTOCOLO: DESCONHECIDO
HASH DO SINAL:
0x12345678
TRANSICOES:
0x0032

Além disso, o monitor serial pode apresentar os tempos brutos da transmissão. Essa informação é muito mais valiosa do que simplesmente saber que o protocolo é desconhecido.

Por que analisar controles desconhecidos?

Existem situações em que queremos automatizar um equipamento, mas não temos acesso ao protocolo utilizado pelo controle remoto. Isso pode acontecer com:

  • Fitas LED;
  • Luminárias;
  • Ventiladores;
  • Humidificadores;
  • Climatizadores;
  • Aparelhos de ar-condicionado;
  • Equipamentos antigos;
  • Equipamentos com protocolos proprietários.

Um exemplo prático é um controle remoto utilizado por determinado equipamento. Não devemos assumir que todo produto de um fabricante utiliza exatamente o mesmo formato de sinal. O procedimento mais seguro é capturar o sinal diretamente do controle utilizado no equipamento.

Se a biblioteca reconhecer o protocolo, teremos o endereço e o comando. Se não reconhecer, ainda teremos os dados RAW para análise.

Materiais necessários

Para montar o projeto, você precisará de:

Para desenvolver o projeto apresentado neste artigo serão utilizados alguns componentes, todos facilmente encontrados na loja da Eletrogate.

Montagem do circuito

A ligação utilizada pelo firmware é:

ComponenteESP32
Receptor IR OUTGPIO 17
Receptor IR VCC3V3
Receptor IR GNDGND
LCD SDAGPIO 21
LCD SCLGPIO 22
LCD VCCAlimentação do módulo
LCD GNDGND

Firmware

/*******************************************************************************
 * ANALISADOR DE SINAIS INFRAVERMELHOS (IR) — ESP32 + LCD 16x2 I2C
 * ============================================================================
 * Artigo para o blog Eletrogate.
 * ============================================================================
 * @author Saulo Aislan ([email protected])
 * @version 0.1
 * @date 2026-09-19
 * @copyright Copyright (c) 2026
*/

#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

#define RAW_BUFFER_LENGTH 750

#define DECODE_NEC              // NEC / Apple / Onkyo (o mais comum no Brasil)
#define DECODE_SONY             // Sony SIRC
#define DECODE_RC5              // Philips RC5
#define DECODE_RC6              // Philips RC6
#define DECODE_SAMSUNG          // Samsung
#define DECODE_LG               // LG
#define DECODE_JVC              // JVC
#define DECODE_DENON            // Denon / Sharp
#define DECODE_PANASONIC        // Panasonic
#define DECODE_KASEIKYO         // Familia Kaseikyo
#define DECODE_DISTANCE_WIDTH   // Decodificador generico Pulse Distance/Width
#define DECODE_HASH             // Ultimo recurso: gera um hash do sinal

#include <IRremote.hpp>

/* =============================================================================
 * PINOS E PARAMETROS
 * ===========================================================================*/

#define IR_RECEIVE_PIN   17     // Pino de dados do receptor IR
#define FEEDBACK_LED_PIN  27     // LED onboard: pisca a cada recepcao

#define I2C_SDA_PIN      21
#define I2C_SCL_PIN      22

#define LCD_ADDRESS    0x3F     // Use 0x27 se o scanner I2C indicar esse valor
#define LCD_COLUMNS      16
#define LCD_ROWS          2

#define LCD_PAGE_TIME  2500     // Tempo (ms) de cada pagina no display
#define LCD_MAX_PAGES     3

#define SHOW_RAW_TIMINGS true   // false = nao imprime a lista de tempos brutos

/* =============================================================================
 * OBJETOS E ESTADO GLOBAL
 * ===========================================================================*/

LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLUMNS, LCD_ROWS);

uint32_t signalCount = 0;       // Total de sinais novos capturados

// Conteudo atualmente escrito no display (usado para espelhar no serial)
char lcdBuffer[LCD_ROWS][LCD_COLUMNS + 1];

char     lcdPages[LCD_MAX_PAGES][LCD_ROWS][LCD_COLUMNS + 1];
uint8_t  lcdPageCount = 0;
uint8_t  lcdPageIndex = 0;
uint32_t lcdPageTimer = 0;

/* =============================================================================
 *  CAMADA DE EXIBICAO DO LCD
 *  -----------------------------------------------------------------------------
 *  Escrevemos sempre 16 caracteres por linha (completando com espacos) em vez
 *  de usar lcd.clear(). Isso evita o piscar da tela e apaga automaticamente os
 *  restos da mensagem anterior.
 * ===========================================================================*/

void lcdLine(uint8_t row, const char *text)
{
    char padded[LCD_COLUMNS + 1];

    memset(padded, ' ', LCD_COLUMNS);
    padded[LCD_COLUMNS] = '\0';

    size_t length = strlen(text);

    if (length > LCD_COLUMNS)
    {
        length = LCD_COLUMNS;    // trunca o que nao cabe
    }

    memcpy(padded, text, length);

    lcd.setCursor(0, row);
    lcd.print(padded);

    strcpy(lcdBuffer[row], padded);
}

// Grava o conteudo de uma pagina (ainda nao exibe)
void lcdSetPage(uint8_t page, const char *line1, const char *line2)
{
    if (page >= LCD_MAX_PAGES)
    {
        return;
    }

    snprintf(lcdPages[page][0], LCD_COLUMNS + 1, "%s", line1);
    snprintf(lcdPages[page][1], LCD_COLUMNS + 1, "%s", line2);
}

void lcdRenderPage()
{
    lcdLine(0, lcdPages[lcdPageIndex][0]);
    lcdLine(1, lcdPages[lcdPageIndex][1]);
}

// Ativa o conjunto de paginas recem-preenchido, comecando pela primeira
void lcdStartPages(uint8_t pageCount)
{
    lcdPageCount = pageCount;
    lcdPageIndex = 0;
    lcdPageTimer = millis();

    lcdRenderPage();
}

// Troca de pagina automaticamente, sem bloquear a recepcao
void lcdUpdate()
{
    if (lcdPageCount > 1 && (millis() - lcdPageTimer) >= LCD_PAGE_TIME)
    {
        lcdPageIndex = (lcdPageIndex + 1) % lcdPageCount;
        lcdPageTimer = millis();

        lcdRenderPage();
    }
}

// Tela de repouso, mostrada apenas ate a primeira leitura
void lcdIdle()
{
    lcdSetPage(0, "ANALISADOR IR", "Aguardando...");
    lcdStartPages(1);
}

/* =============================================================================
 *  CAMADA DE LOG NO MONITOR SERIAL
 *  -----------------------------------------------------------------------------
 *  Serial.printf() esta disponivel no core do ESP32 e deixa o log alinhado com
 *  muito menos codigo que varios Serial.print() em sequencia.
 * ===========================================================================*/

void logRule(char character)
{
    for (uint8_t i = 0; i < 52; i++){
        Serial.print(character);
    }
    Serial.println();
}

void logTitle(const char *title)
{
    Serial.println();
    logRule('=');
    Serial.printf("  %s\n", title);
    logRule('=');
}

// Imprime uma linha no formato "  Rotulo . . . . . : valor   (observacao)"
void logField(const char *label, const char *value, const char *note = nullptr)
{
    char dotted[21];

    memset(dotted, ' ', sizeof(dotted) - 1);
    dotted[sizeof(dotted) - 1] = '\0';

    size_t length = strlen(label);

    if (length > sizeof(dotted) - 1) {
        length = sizeof(dotted) - 1;
    }

    memcpy(dotted, label, length);

    // Pontilhado que guia o olho do rotulo ate o valor
    for (size_t i = length + 1; i < sizeof(dotted) - 1; i += 2) {
        dotted[i] = '.';
    }

    if (note == nullptr) {
        Serial.printf("  %s : %s\n", dotted, value);
    }
    else {
        Serial.printf("  %s : %-14s %s\n", dotted, value, note);
    }
}

/*
 * Desenha no serial exatamente o que esta escrito no LCD.
 * E o recurso mais didatico do log: o leitor compara, lado a lado, o dado
 * bruto e a interface que o usuario final enxerga.
 */
void logLcdMirror()
{
    Serial.println();
    Serial.println("  Tela vista pelo usuario:");
    Serial.println("    +----------------+");
    Serial.printf("    |%s|\n", lcdBuffer[0]);
    Serial.printf("    |%s|\n", lcdBuffer[1]);
    Serial.println("    +----------------+");
}

/* =============================================================================
 *  FERRAMENTA DE DIAGNOSTICO: SCANNER I2C
 *  -----------------------------------------------------------------------------
 *  Modulos de LCD saem de fabrica no endereco 0x27 ou 0x3F. Trocar um pelo
 *  outro e a causa numero 1 de "o display nao acende".
 * ===========================================================================*/

void scanI2C()
{
    Serial.println("  Procurando dispositivos no barramento I2C...");

    uint8_t found = 0;

    for (uint8_t address = 1; address < 127; address++) {
        Wire.beginTransmission(address);

        if (Wire.endTransmission() == 0) {
            Serial.printf("    -> encontrado em 0x%02X\n", address);
            found++;
        }
    }

    if (found == 0) {
        Serial.println("    -> nenhum dispositivo encontrado.");
        Serial.println("       Verifique SDA, SCL, GND e alimentacao.");
    }
}

/* =============================================================================
 * TRATAMENTO DE CADA TIPO DE RESULTADO
 * ===========================================================================*/

/*
 * Caso 1: estouro de buffer.
 * O sinal tem mais transicoes do que RAW_BUFFER_LENGTH comporta.
 */
void handleOverflow()
{
    logTitle("ERRO: ESTOURO DE BUFFER");

    Serial.println("  O sinal recebido e maior que o buffer de captura.");
    Serial.printf("  RAW_BUFFER_LENGTH atual: %d transicoes\n",
                  RAW_BUFFER_LENGTH);
    Serial.println("  Aumente esse valor e grave o firmware novamente.");
    Serial.println("  Controles de ar-condicionado podem exigir 1000 ou mais.");

    lcdSetPage(0, "ERRO: OVERFLOW", "Sinal > buffer");
    lcdSetPage(1, "Aumente o valor", "RAW_BUFFER_LEN");
    lcdStartPages(2);

    logLcdMirror();
    logRule('=');
}

/*
 * Caso 2: repeticao.
 * Segurando a tecla, o controle envia quadros curtos de "repeat" em vez de
 * reenviar o comando. O LCD NAO e alterado: o usuario continua lendo o codigo
 * da tecla que pressionou.
 */
void handleRepeat()
{
    Serial.println("  [repeticao] tecla mantida pressionada");
}

/*
 * Caso 3: protocolo desconhecido.
 * Nenhum decodificador reconheceu o formato, mas os tempos brutos descrevem o
 * sinal por completo e permitem reproduzi-lo com sendRaw().
 */
void handleUnknownProtocol()
{
    const uint32_t hash   = IrReceiver.decodedIRData.decodedRawData;
    const uint16_t rawLen = IrReceiver.decodedIRData.rawDataPtr->rawlen;

    char text[24];

    logField("Protocolo", "DESCONHECIDO");

    snprintf(text, sizeof(text), "0x%08lX", (unsigned long) hash);
    logField("Hash do sinal", text, "(identifica a tecla)");

    snprintf(text, sizeof(text), "0x%04X", rawLen);
    logField("Transicoes", text, "(pares MARK/SPACE)");

    // ---- LCD: o hash e o unico identificador disponivel neste caso ---------
    char line[LCD_COLUMNS + 1];

    lcdSetPage(0, "PROTOCOLO:", "DESCONHECIDO");

    snprintf(line, sizeof(line), "0x%08lX", (unsigned long) hash);
    lcdSetPage(1, "HASH DO SINAL:", line);

    snprintf(line, sizeof(line), "0x%04X", rawLen);
    lcdSetPage(2, "TRANSICOES:", line);

    lcdStartPages(3);

    logLcdMirror();

    if (SHOW_RAW_TIMINGS) {
        Serial.println();
        logRule('-');
        Serial.println("  TEMPOS BRUTOS (microssegundos)");
        Serial.println("    MARK  = LED infravermelho ligado");
        Serial.println("    SPACE = intervalo sem emissao");
        logRule('-');

        IrReceiver.printIRResultRawFormatted(&Serial, true);

        Serial.println();
        Serial.println("  Array pronto para IrSender.sendRaw():");

        IrReceiver.compensateAndPrintIRResultAsCArray(&Serial, true);
    }

    logRule('=');
}

/*
 * Caso 4: protocolo reconhecido.
 * Endereco, comando, quadro completo e numero de bits — todos em hexadecimal.
 */
void handleKnownProtocol()
{
    const uint16_t address = IrReceiver.decodedIRData.address;
    const uint16_t command = IrReceiver.decodedIRData.command;
    const uint16_t bits    = IrReceiver.decodedIRData.numberOfBits;
    const uint32_t rawData = IrReceiver.decodedIRData.decodedRawData;

    // No core do ESP32 (nao AVR) getProtocolString() retorna const char*
    const char *protocol = getProtocolString(IrReceiver.decodedIRData.protocol);

    char text[24];

    // ---- Log serial ---------------------------------------------------------
    logField("Protocolo", protocol);

    snprintf(text, sizeof(text), "0x%04X", address);
    logField("Endereco", text, "(aparelho / fabricante)");

    snprintf(text, sizeof(text), "0x%02X", command);
    logField("Comando", text, "(tecla pressionada)");

    snprintf(text, sizeof(text), "0x%08lX", (unsigned long) rawData);
    logField("Quadro completo", text, "(endereco + comando)");

    snprintf(text, sizeof(text), "0x%02X", bits);
    logField("Bits", text, "(tamanho do quadro)");

    // ---- LCD: tudo em hexadecimal ------------------------------------------
    char line0[LCD_COLUMNS + 1];
    char line1[LCD_COLUMNS + 1];

    // Pagina 1: protocolo e quadro completo
    snprintf(line0, sizeof(line0), "%s", protocol);
    snprintf(line1, sizeof(line1), "0x%08lX", (unsigned long) rawData);
    lcdSetPage(0, line0, line1);

    // Pagina 2: endereco e comando separados
    snprintf(line0, sizeof(line0), "ADDR: 0x%04X", address);
    snprintf(line1, sizeof(line1), "CMD:  0x%02X", command);
    lcdSetPage(1, line0, line1);

    // Pagina 3: tamanho do quadro
    snprintf(line0, sizeof(line0), "BITS: 0x%02X", bits);
    snprintf(line1, sizeof(line1), "      %u bits", bits);
    lcdSetPage(2, line0, line1);

    lcdStartPages(3);

    logLcdMirror();

    // ---- Como retransmitir --------------------------------------------------
    Serial.println();
    Serial.println("  Para retransmitir este sinal:");
    Serial.print("  ");

    IrReceiver.printIRSendUsage(&Serial);

    logRule('=');
}

/* =============================================================================
 * SETUP
 * ===========================================================================*/

void setup()
{
    Serial.begin(115200);
    delay(500);

    // --- Barramento I2C e display --------------------------------------------
    Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
    pinMode(FEEDBACK_LED_PIN, OUTPUT);

    lcd.init();
    lcd.backlight();
    lcdSetPage(0, "ANALISADOR IR", "Inicializando...");
    lcdStartPages(1);

    // --- Apresentacao --------------------------------------------------------
    logTitle("ANALISADOR DE SINAIS INFRAVERMELHOS");

    scanI2C();
    Serial.println();

    IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK, FEEDBACK_LED_PIN);

    char text[24];

    snprintf(text, sizeof(text), "GPIO %d", IR_RECEIVE_PIN);
    logField("Receptor IR", text);

    snprintf(text, sizeof(text), "%d", RAW_BUFFER_LENGTH);
    logField("Buffer de captura", text, "(transicoes)");

    snprintf(text, sizeof(text), "0x%02X", LCD_ADDRESS);
    logField("Endereco do LCD", text);

    Serial.print("  Protocolos ativos    : ");
    printActiveIRProtocols(&Serial);
    Serial.println();

    logRule('-');
    Serial.println("  Aponte o controle para o receptor e pressione uma tecla.");
    Serial.println("  Todos os valores sao apresentados em HEXADECIMAL.");
    logRule('-');

    lcdIdle();
}

/* =============================================================================
 * LOOP
 * ===========================================================================*/

void loop()
{
    if (IrReceiver.decode()) {
        const bool wasOverflow =
            IrReceiver.decodedIRData.flags & IRDATA_FLAGS_WAS_OVERFLOW;

        const bool isRepeat =
            IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT;

        if (wasOverflow) {
            handleOverflow();
        }
        else if (isRepeat) {
            handleRepeat();
        }
        else {
            signalCount++;

            const uint32_t now = millis();

            char title[48];

            snprintf(title, sizeof(title), "SINAL #%lu   |   t = %lu.%lu s",
                     (unsigned long) signalCount,
                     (unsigned long) (now / 1000),
                     (unsigned long) ((now % 1000) / 100));

            logTitle(title);

            if (IrReceiver.decodedIRData.protocol == UNKNOWN) {
                handleUnknownProtocol();
            }
            else {
                handleKnownProtocol();
            }
        }
        IrReceiver.resume();
    }
    lcdUpdate();
}
 

Algumas funcionalidades do firmware

Endereço I2C do LCD

Um problema bastante comum em projetos com LCD I2C é utilizar o endereço errado. Existem módulos configurados em:

0x27

e outros em:

0x3F

Por isso, o firmware foi desenvolvido com um pequeno scanner I2C. Durante a inicialização, o ESP32 percorre os endereços do barramento e informa no monitor serial quais dispositivos foram encontrados. No firmware atual, o endereço configurado é:

#define LCD_ADDRESS 0x3F

e existe inclusive um comentário indicando que 0x27 deve ser utilizado caso esse seja o endereço identificado pelo scanner. Isso facilita bastante a montagem.

O scanner percorre os endereços possíveis do barramento:

for (uint8_t address = 1; address < 127; address++)

Quando encontra um dispositivo, informa:

-> encontrado em 0x3F

Caso nenhum dispositivo seja encontrado, o programa informa:

-> nenhum dispositivo encontrado.
Verifique SDA, SCL, GND e alimentacao.

Essa pequena ferramenta evita uma das causas mais comuns de problemas em projetos com LCD I2C. Para mais informações sobre o display LCD e a conexão I2C veja esses nosso artigos: Guia Completo do Display LCD – Arduino e Simplificando a montagem de sensores no Esp e Arduíno para aulas Makers com I2C.

Funcionamento do LCD

O LCD não é utilizado apenas para mostrar uma mensagem fixa. O firmware possui uma pequena camada de gerenciamento de páginas. Isso permite mostrar diferentes informações automaticamente. Por exemplo, para um protocolo conhecido, podemos ter:

NEC
0xBA45FF00

Depois:

ADDR: 0x0000
CMD:  0x45

E finalmente:

BITS: 0x20
      32 bits

Cada página permanece visível durante um determinado período. No firmware atual:

#define LCD_PAGE_TIME 2500
#define LCD_MAX_PAGES 3

Ou seja, cada página é apresentada durante aproximadamente 2,5 segundos.

Por que não utilizar lcd.clear()?

Um detalhe interessante do código é que ele não utiliza lcd.clear() a cada atualização. Em vez disso, a função lcdLine() preenche toda a linha com espaços antes de escrever o novo conteúdo. Isso evita que restos de mensagens anteriores permaneçam no display e também reduz o efeito de "piscar" da tela. O código mantém inclusive um buffer com o conteúdo atualmente exibido para poder reproduzi-lo no monitor serial.

O monitor serial como espelho do LCD

O LCD é a interface principal do usuário. Porém, o monitor serial fornece muito mais espaço para informações. Por isso, o firmware cria um recurso interessante: ele desenha no monitor serial uma representação da tela do LCD. O resultado aparece aproximadamente assim:

Tela vista pelo usuario:
+----------------+
|NEC |
|0xD52AC2EA |
+----------------+ 

Isso torna a depuração mais fácil. É possível acompanhar no computador exatamente o que o usuário está vendo no display.

Capturando e interpretando controles remotos

Com o circuito montado e o programa gravado no ESP32, abra o Monitor Serial em 115200 baud. Durante a inicialização, o projeto verifica o barramento I2C, inicializa o display LCD e prepara o receptor infravermelho para começar as capturas. Após a inicialização, o display apresenta a mensagem:

Agora basta apontar um controle remoto para o receptor infravermelho e pressionar uma tecla. O ESP32 captura o sinal e tenta identificar automaticamente o protocolo utilizado. Para isso, o firmware utiliza diversos decodificadores da biblioteca IRremote (podemos baixar no link IRremote), incluindo protocolos como NEC, Sony, RC5, RC6, Samsung, LG, JVC, Denon, Panasonic e Kaseikyo. Além desses protocolos, o projeto também possui recursos para analisar sinais que não sejam reconhecidos automaticamente, utilizando decodificação genérica e captura dos tempos RAW. Isso permite utilizar o mesmo analisador com controles remotos de diferentes fabricantes e equipamentos.

Quando o protocolo é reconhecido

Quando a biblioteca consegue identificar o protocolo, o ESP32 apresenta no Monitor Serial as principais informações do comando recebido. Uma captura pode apresentar, por exemplo:

Protocolo       : NEC
Endereco        : 0xC2EA
Comando         : 0x2A
Quadro completo : 0xD52AC2EA
Bits            : 0x20

No LCD, essas informações são distribuídas em três telas. Como o display possui apenas duas linhas, o firmware alterna automaticamente entre as páginas. A primeira tela apresenta o protocolo e o quadro completo:

A segunda apresenta o endereço e o comando:E a terceira mostra a quantidade de bits do quadro:

Cada tela permanece no display durante aproximadamente 2,5 segundos antes de passar para a próxima. Dessa forma, mesmo utilizando um LCD 16x2, conseguimos visualizar as principais informações do comando capturado.

O que significam esses valores?

O protocolo informa qual padrão de comunicação foi identificado. Já o endereço e o comando são informações extraídas do quadro recebido. De forma geral, o endereço está relacionado à identificação do dispositivo, enquanto o comando representa a informação associada à tecla pressionada. Assim, diferentes teclas de um mesmo controle podem gerar comandos diferentes:

POWER → 0x45
VOL+  → 0x46
VOL-  → 0x47

Os valores acima são apenas exemplos. Os códigos reais dependem do controle remoto utilizado. O programa também apresenta o quadro completo, que pode ser utilizado para registrar e comparar os comandos capturados.

E quando o controle não é reconhecido?

Nem todos os controles remotos utilizam um protocolo que pode ser identificado diretamente pela biblioteca. Isso pode acontecer com equipamentos que utilizam formatos próprios, como determinados controles de fitas LED, humidificadores, climatizadores e outros aparelhos. Quando nenhum dos decodificadores reconhece o sinal, o ESP32 não simplesmente descarta a captura. O firmware passa a tratar o sinal como desconhecido e apresenta informações que permitem continuar a análise.

No display será apresentada a indicação:Em seguida, o ESP32 apresenta um hash associado ao sinal capturado:Por fim, é apresentada a quantidade de transições encontradas:

Essas informações permitem identificar e comparar diferentes sinais mesmo quando o protocolo não foi reconhecido.

Analisando um sinal desconhecido

O hash apresentado pelo display não deve ser confundido com o código que será enviado ao equipamento. Ele funciona como uma forma de identificar a sequência capturada. Isso permite, por exemplo, comparar diferentes teclas de um mesmo controle:

POWER
Hash: 0x12345678
LIGHT
Hash: 0x87654321

Se os valores forem diferentes, sabemos que as capturas possuem diferenças. Entretanto, o hash não contém todas as informações necessárias para reproduzir o sinal. Para isso, precisamos analisar os tempos de cada pulso infravermelho. É aí que entram os dados RAW.

Visualizando os tempos RAW

Quando o protocolo é desconhecido, o firmware imprime no Monitor Serial os tempos brutos da transmissão. Uma captura pode apresentar uma sequência semelhante a:

+9000 -4500
+560 -560
+560 -1690
+560 -560
...

Esses valores representam a duração dos períodos de MARK e SPACE, em microssegundos. O MARK corresponde ao período em que o LED infravermelho está emitindo, enquanto o SPACE representa o intervalo sem emissão. O projeto apresenta essa explicação no Monitor Serial:

TEMPOS BRUTOS (microssegundos)

MARK  = LED IR ligado
SPACE = LED IR desligado
A captura dos tempos RAW é habilitada no firmware através da configuração:
#define SHOW_RAW_TIMINGS true

Com essa opção habilitada, os dados brutos ficam disponíveis para análise e podem posteriormente ser utilizados para desenvolver uma rotina de retransmissão.

Por que os tempos RAW são importantes?

Imagine que você tenha um controle remoto cujo protocolo não seja reconhecido pela biblioteca. O ESP32 poderá informar:

PROTOCOLO:
DESCONHECIDO
Mesmo assim, o receptor conseguiu capturar a sequência temporal do sinal:
+9000 -4500
+560 -560
+560 -1690
+560 -560
...
Essa sequência descreve como o controle transmite aquela tecla. Podemos então capturar vários botões e comparar os resultados:
POWER
→ sequência A

LIGHT
→ sequência B

MODE
→ sequência C

Com essas informações, podemos observar quais partes do sinal permanecem iguais e quais são alteradas de acordo com o comando. Essa característica é especialmente interessante para investigar controles que utilizam formatos proprietários ou que não são identificados diretamente pelos protocolos disponíveis na biblioteca IRremote.

Um exemplo prático com um controle desconhecido

Vamos imaginar um controle remoto de um humidificador ou climatizador. Pressionamos o botão POWER e o analisador informa que o protocolo é desconhecido. Em vez de descartar a captura, registramos o hash, o número de transições e os tempos RAW. Depois fazemos o mesmo com outros comandos:

  • POWER
  • MODE
  • FAN
  • TIMER
  • LIGHT
Os resultados podem ser organizados em uma tabela:
TeclaProtocoloHashTransições
POWERDesconhecido0xD52AC2EA0x0002
MODEDesconhecido0xA134F1330x0002
FANDesconhecido0x123456780x0002
TIMERDesconhecido0xAAAAAAAA0x0002
LIGHTDesconhecido0xDADA324D0x0002

Além dessas informações, cada captura possui sua própria sequência RAW. Dessa maneira, o analisador pode ser utilizado como uma ferramenta para catalogar os comandos de um controle remoto, inclusive quando seu protocolo não é identificado automaticamente.

Gerando o código para retransmissão

Uma das características mais interessantes da análise RAW é a possibilidade de utilizar os dados capturados posteriormente em um transmissor infravermelho. Para facilitar esse processo, o firmware utiliza a função:

IrReceiver.compensateAndPrintIRResultAsCArray(&Serial, true);
Essa função gera automaticamente uma representação da captura em formato de array C. No Monitor Serial será indicado:
Array pronto para IrSender.sendRaw():
O resultado pode ser semelhante a:
uint16_t rawData[] = {
    9000, 4500,
    560, 560,
    560, 1690,
    560, 560
};

Esse array pode posteriormente ser utilizado com a função IrSender.sendRaw() da biblioteca IRremote para desenvolver um transmissor infravermelho. Dessa forma, mesmo quando o protocolo aparece como DESCONHECIDO, os dados capturados podem servir como base para reproduzir o comando.

Repetições ao manter uma tecla pressionada

Alguns controles remotos enviam um quadro especial quando uma tecla permanece pressionada. Em vez de transmitir novamente o comando completo, o controle pode enviar um quadro de repetição. O firmware identifica esse tipo de transmissão separadamente. Nesse caso, o Monitor Serial informa:

[repeticao] tecla mantida pressionada

O LCD não é alterado nesse caso. Dessa maneira, o usuário continua visualizando o comando original em vez de receber uma nova informação a cada quadro de repetição. Esse comportamento é especialmente útil durante os testes, pois evita que uma tecla mantida pressionada seja interpretada como várias teclas diferentes.

Quando ocorre um OVERFLOW?

Os sinais infravermelhos podem possuir diferentes quantidades de transições. Alguns controles enviam quadros relativamente curtos, enquanto outros podem transmitir uma quantidade muito maior de informações. Para armazenar essas transições, o projeto utiliza inicialmente um buffer de 750 posições:

#define RAW_BUFFER_LENGTH 750

Se o sinal recebido possuir mais transições do que o buffer consegue armazenar, o firmware identifica a situação e apresenta uma mensagem de erro. No LCD será mostrado:

ERRO: OVERFLOW
Sinal > buffer
Na página seguinte, o display orienta o usuário:
Aumente o valor
RAW_BUFFER_LEN

O Monitor Serial também informa que o sinal recebido é maior que o buffer disponível e mostra o valor configurado atualmente. Em controles que transmitem quadros muito longos, como determinados controles de ar-condicionado, pode ser necessário aumentar o valor de RAW_BUFFER_LENGTH.

Resultado

Vídeo demonstrando a captura dos dados lidos pelo sensor IR.

Conclusão

O projeto apresentado neste artigo vai além de simplesmente mostrar o código hexadecimal de um controle remoto. Com o ESP32, um receptor IR e um LCD 16x2, criamos uma ferramenta capaz de identificar protocolos conhecidos e investigar sinais que não são reconhecidos automaticamente pela biblioteca IRremote.

Nos protocolos conhecidos, temos acesso ao protocolo, endereço, comando, quadro completo e quantidade de bits. Já nos sinais desconhecidos, o projeto fornece o hash, o número de transições e, principalmente, os timings RAW de MARK e SPACE. A partir dessas informações, é possível estudar o funcionamento do controle e gerar um array em C para utilização posterior com sendRaw(). Isso abre possibilidades interessantes para quem deseja trabalhar com controles de fitas LED, humidificadores, climatizadores e outros equipamentos que utilizam sinais proprietários ou não identificados pelos decodificadores tradicionais.

O projeto também conta com recursos importantes para facilitar os testes, como scanner I2C, indicação visual de recepção através do LED de feedback, tratamento de comandos repetidos e detecção de sinais que ultrapassam o tamanho do buffer. A partir daqui, o próximo passo é transformar os sinais capturados em comandos utilizáveis por um transmissor IR, permitindo integrar equipamentos originalmente controlados apenas por infravermelho a projetos de automação e IoT com ESP32.

Para mais materiais como esse, continue acompanhando as postagens semanais do blog e não deixe de visitar nossa loja. Lá você encontra todos os componentes necessários para desenvolver esse e muitos outros projetos!

Que a força esteja com você! Até mais!

Aprenda a capturar comandos de controles remotos, identificar protocolos conhecidos e analisar sinais infravermelhos desconhecidos com ESP32.

Precisa dos componentes para este projeto?

Encontre tudo na Loja Eletrogate com frete grátis para compras acima de R$ 200