/
/
Construindo um Sistema Li-Fi com ESP32: Transmissão de Dados Usando Luz

Construindo um Sistema Li-Fi com ESP32: Transmissão de Dados Usando Luz

Sumário

Baixe agora a apostila Arduino Iniciante e comece seus projetos

Compartilhe seu conhecimento e receba em dinheiro ou produtos.

Introdução

A comunicação sem fio normalmente é associada ao Wi-Fi, Bluetooth e outras tecnologias baseadas em radiofrequência. Entretanto, existe uma área bastante interessante da telecomunicação chamada Li-Fi (Light Fidelity), onde a transmissão de dados acontece através da luz. Nesse tipo de sistema, LEDs podem ser utilizados para transmitir informações por meio de pulsos luminosos extremamente rápidos, enquanto sensores ópticos detectam essas variações e convertem novamente os sinais em dados digitais.Embora o Li-Fi comercial ainda esteja em pleno desenvolvimento em muitos setores, os princípios básicos dessa tecnologia podem ser reproduzidos utilizando componentes simples e acessíveis. Neste projeto construiremos um sistema Li-Fi didático utilizando dois módulos ESP32, um LED de alto brilho e um LDR (sensor dependente de luz). A comunicação será realizada por meio de Código Morse, permitindo transmitir mensagens digitadas pelo computador ou até mesmo pelo celular através de um aplicativo Android.O projeto é excelente para o aprendizado de:

  • Comunicação óptica
  • Transmissão digital de dados
  • Sensores analógicos com ESP32
  • Processamento de sinais em sistemas embarcados
 

O que é Li-Fi?

Li-Fi significa Light Fidelity, uma tecnologia de comunicação baseada em luz visível. Diferente do Wi-Fi, que utiliza ondas de rádio, o Li-Fi transmite dados modulando a intensidade luminosa de LEDs de forma extremamente rápida e imperceptível ao olho humano.Em aplicações reais, sistemas Li-Fi são estudados para uso em:

  • Ambientes hospitalares, onde interferências eletromagnéticas são indesejadas
  • Aviação, para comunicação dentro de aeronaves sem impacto em sistemas de navegação
  • Indústrias sensíveis à RF, como refinarias e plantas químicas
  • IoT indoor, aproveitando a infraestrutura de iluminação existente
  • Comunicação segura, pois a luz não atravessa paredes, limitando fisicamente o alcance da rede
As velocidades teóricas do Li-Fi podem superar 10 Gbps em laboratório, o que o torna um complemento promissor ao Wi-Fi convencional. Neste projeto, utilizaremos uma versão completamente didática e simplificada desse conceito para demonstrar os fundamentos.

Como Funciona Este Projeto de Comunicação Li-Fi

O sistema é dividido em duas partes independentes, cada uma rodando em um ESP32 diferente:

Transmissor

O ESP32 transmissor será responsável por:
  1. Receber mensagens digitadas pela Serial (Monitor Serial da Arduino IDE ou PlatformIO)
  2. Converter o texto para sequências de pulsos luminosos
  3. Acionar o LED de acordo com a temporização definida
  4. Respeitar os intervalos entre símbolos, letras e palavras

Receptor

O ESP32 receptor será responsável por:

  1. Monitorar continuamente a leitura analógica do LDR
  2. Detectar transições entre luz e sombra (bordas de subida e descida)
  3. Medir a duração de cada pulso e de cada intervalo de silêncio
  4. Reconstruir as letras e palavras recebidas, exibindo na Serial
A comunicação entre os dois ESP32 é completamente óptica, ou seja, nenhum fio os conecta. A informação viaja exclusivamente como luz do LED ao LDR.

Componentes Utilizados no Projeto

Todos os componentes estão disponíveis na loja da Eletrogate.

Parte Transmissora

Parte Receptora

Circuitos do Transmissor e Receptor

Montagem do Transmissor

ComponenteESP32
LED AnodoGPIO 26
LED CatodoResistor 220Ω → GND
O resistor de 220Ω limita a corrente pelo LED, protegendo tanto o componente quanto o GPIO do ESP32, que suporta no máximo 40mA por pino. Com 3,3V e 220Ω, a corrente fica em torno de 15mA, suficiente para garantir alta luminosidade sem sobrecarga.Por que usar um LED de alto brilho? A intensidade luminosa influencia diretamente na qualidade e no alcance da transmissão. LEDs de alto brilho permitem:
  • Maior alcance entre transmissor e receptor
  • Melhor detecção pelo LDR, mesmo com alguma luz ambiente
  • Menor sensibilidade a vibrações e pequenos desalinhamentos

Montagem do Receptor

ComponenteESP32
LDR perna 13.3V
LDR perna 2GPIO 4
Resistor 10kGPIO 4 → GND
O circuito forma um divisor de tensão no GPIO 4. Quando o LED incide sobre o LDR, sua resistência cai drasticamente, elevando a tensão lida no pino analógico. Com o resistor de 10kΩ no GND, a variação é bem pronunciada, facilitando a distinção entre "luz" e "sem luz".

Importância do Alinhamento Óptico

Em sistemas Li-Fi, o alinhamento entre transmissor e receptor é um fator crítico de desempenho. Diferente do Wi-Fi, onde as ondas de rádio se propagam em todas as direções e atravessam paredes, a comunicação óptica depende diretamente da incidência da luz sobre o sensor.Pequenos desalinhamentos reduzem a intensidade luminosa recebida pelo LDR, aumentando o risco de leituras incorretas dos pulsos. Durante os testes, recomenda-se:

  • Posicionar o LED apontado diretamente para o LDR
  • Manter uma distância inicial curta (15 a 30cm) para calibrar o limiar
  • Reduzir iluminação ambiente intensa que possa interferir na leitura
  • Ajustar o valor da constante THRESHOLD no firmware receptor conforme o ambiente
Esses cuidados representam na prática um dos desafios reais dos sistemas de comunicação óptica: a dependência da linha de visada. Nas figuras abaixo é mostrado o alinhamento entre o LED e o LDR na demonstração do projeto.

Firmwares

Ambos os firmwares foram desenvolvidos para PlatformIO com o framework Arduino, mas funcionam igualmente na Arduino IDE.

Firmware do ESP32 Transmissor

O transmissor aguarda texto digitado no Monitor Serial. Assim que o usuário pressiona Enter, a mensagem é processada caractere por caractere e o LED é acionado com os pulsos correspondentes.As temporizações usadas são:
SímboloDuração
Ponto (pulso curto)80ms
Traço (pulso longo)240ms
Intervalo entre símbolos80ms
Intervalo entre letras240ms
Intervalo entre palavras560ms
 Destaques do código:
  • A função pulse() acende e apaga o LED com precisão de 1ms via delay(), garantindo consistência nos pulsos.
  • A função sendChar() itera sobre os símbolos da letra e insere os intervalos corretos entre eles.
  • A função sendString() cuida dos espaços entre letras e palavras, respeitando a hierarquia de intervalos.

O buffer buf[] armazena a mensagem completa antes de transmitir, evitando transmissões parciais caso o usuário pause ao digitar.

/**
 * @file transmitter.cpp
 * @author Saulo Aislan ([email protected])
 * @brief Firmware para o transmissor de sinal Li-Fi.
 * @version 0.1
 * @date 2026-05-21
 *
 * @copyright Copyright (c) 2026
 *
 */

#include <Arduino.h>
#include <string.h>

#define LED_PIN  26

// Timings (ms) — devem ser identicos ao receiver.cpp
#define T_DOT     80
#define T_DASH    240
#define T_ELEM    80    // entre pontos/tracos na mesma letra
#define T_LETTER  240   // entre letras
#define T_WORD    560   // entre palavras

// Tabela Morse ITU
struct MorseEntry { char ch; const char* code; };
const MorseEntry MORSE_TABLE[] = {
    {'A',".-"},   {'B',"-..."}, {'C',"-.-."}, {'D',"-.."}, {'E',"."},
    {'F',"..-."}, {'G',"--."}, {'H',"...."}, {'I',".."},  {'J',".---"},
    {'K',"-.-"},  {'L',".-.."}, {'M',"--"},  {'N',"-."},  {'O',"---"},
    {'P',".--."}, {'Q',"--.-"}, {'R',".-."}, {'S',"..."}, {'T',"-"},
    {'U',"..-"},  {'V',"...-"}, {'W',".--"}, {'X',"-..-"},{'Y',"-.--"},
    {'Z',"--.."},
    {'1',".----"},{'2',"..---"},{'3',"...--"},{'4',"....-"},{'5',"....."},
    {'6',"-...."},{'7',"--..."},{'8',"---.."},{'9',"----."},{'0',"-----"},
};

/*
 * @brief Retorna o código Morse para um caractere.
 * @param c Caractere a ser convertido.
 * @return Ponteiro para a string do código Morse ou nullptr se não encontrado.
 */
const char* getMorse(char c) {
    c = toupper(c);
    for (const auto& e : MORSE_TABLE)
        if (e.ch == c) return e.code;
    return nullptr;
}

/**
 * @brief Envia um pulso de luz por um determinado tempo.
 * @param ms Tempo em milissegundos.
 */
void pulse(uint32_t ms) {
    digitalWrite(LED_PIN, HIGH);
    delay(ms);
    digitalWrite(LED_PIN, LOW);
}

/**
 * @brief Envia um caractere como código Morse.
 * @param c Caractere a ser enviado.
 */
void sendChar(char c) {
    const char* code = getMorse(c);
    if (!code) return;
    Serial.printf("%c", c);
    for (int i = 0; code[i]; i++) {
        if (i > 0) {
            delay(T_ELEM);
        }
        if (code[i] == '.') { pulse(T_DOT); }
        else                 { pulse(T_DASH); }
    }
}

/**
 * @brief Envia uma string como código Morse.
 * @param str String a ser enviada.
 */
void sendString(const char* str) {
    Serial.println("Transmitindo:");
    bool primeiraLetra = true;
    for (int i = 0; str[i]; i++) {
        char c = str[i];
        if (c == ' ') {
            delay(T_WORD);
            // Espaço entre palavras
            Serial.print(" ");
            primeiraLetra = true;
        } else {
            if (!primeiraLetra) delay(T_LETTER);
            sendChar(c);
            primeiraLetra = false;
        }
    }
    Serial.println("\n--- Fim ---\n");
}

char buf[128];
int  bufLen = 0;

/**
 * @brief Configura o transmissor e aguarda mensagens para enviar.
 */
void setup() {
    Serial.begin(115200);
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);

    Serial.println("=== Li-Fi Transmissor Morse ===");
    Serial.printf("LED: GPIO%d\n", LED_PIN);
    Serial.printf("Ponto:%dms | Traco:%dms | Elem:%dms | Letra:%dms | Palavra:%dms\n\n",
                  T_DOT, T_DASH, T_ELEM, T_LETTER, T_WORD);
    Serial.println("Digite a mensagem e pressione Enter:");
}

/**
 * @brief Loop principal para ler mensagens do console e transmiti-las.
 */
void loop() {
    while (Serial.available()) {
        char c = Serial.read();
        if (c == '\n' || c == '\r') {
            if (bufLen > 0) {
                buf[bufLen] = '\0';
                bufLen = 0;
                Serial.printf("\nMensagem: \"%s\"\n", buf);
                sendString(buf);
                Serial.println("Digite outra mensagem:");
            }
        } else if (bufLen < 127 && c >= 32) {
            buf[bufLen++] = c;
            Serial.print(c);  // echo
        }
    }
}

Firmware do ESP32 Receptor

O receptor monitora o GPIO 4 continuamente e classifica cada evento, pulso de luz ou intervalo de silêncio, com base na sua duração. A decodificação ocorre em tempo real, sem buffering de mensagem completa: cada letra é exibida assim que identificada.Destaques do código:
  • O receptor trabalha inteiramente por detecção de bordas (transições de estado), sem polling de tempo fixo, o que o torna responsivo e preciso.
  • Os limiares de classificação são calculados como ponto médio entre os valores adjacentes da tabela de temporização. Por exemplo, LIM_DOT_DASH = 160ms é a média entre 80ms (ponto) e 240ms (traço).
  • A constante THRESHOLD = 2500 define o nível da leitura ADC acima do qual se considera "luz presente". Esse valor pode precisar de ajuste fino conforme a iluminação do ambiente e a distância entre LED e LDR.
  • flush por timeout (LIM_FIM = 1120ms) garante que a última letra de uma transmissão seja sempre exibida, mesmo sem intervalo subsequente de palavra.
/**
 * @file receiver.cpp
 * @author Saulo Aislan ([email protected])
 * @brief Firmware para o receptor de sinal Li-Fi.
 * @version 0.1
 * @date 2026-05-21
 *
 * @copyright Copyright (c) 2026
 *
 */

#include <Arduino.h>
#include <string.h>

#define LDR_PIN   4
#define THRESHOLD 2500

// Timings (ms) — holecek.pavel.MorseCode
#define T_DOT     80
#define T_DASH    240
#define T_ELEM    80
#define T_LETTER  240
#define T_WORD    560

// Limiares de classificacao (ponto medio entre os valores adjacentes)
#define LIM_DOT_DASH    160   // pulso ON  < 160ms = ponto  | >= 160ms = traco   (80+240)/2
#define LIM_ELEM_LETTER 160   // gap  OFF  < 160ms = elem   | >= 160ms = nova letra (80+240)/2
#define LIM_LETTER_WORD 400   // gap  OFF  < 400ms = letra  | >= 400ms = palavra (240+560)/2
#define LIM_FIM        1120   // gap  OFF >= 1120ms = fim de transmissao (560*2)

// Tabela Morse ITU
struct MorseEntry { char ch; const char* code; };
const MorseEntry MORSE_TABLE[] = {
    {'A',".-"},   {'B',"-..."}, {'C',"-.-."}, {'D',"-.."}, {'E',"."},
    {'F',"..-."}, {'G',"--."}, {'H',"...."}, {'I',".."},  {'J',".---"},
    {'K',"-.-"},  {'L',".-.."}, {'M',"--"},  {'N',"-."},  {'O',"---"},
    {'P',".--."}, {'Q',"--.-"}, {'R',".-."}, {'S',"..."}, {'T',"-"},
    {'U',"..-"},  {'V',"...-"}, {'W',".--"}, {'X',"-..-"},{'Y',"-.--"},
    {'Z',"--.."},
    {'1',".----"},{'2',"..---"},{'3',"...--"},{'4',"....-"},{'5',"....."},
    {'6',"-...."},{'7',"--..."},{'8',"---.."},{'9',"----."},{'0',"-----"},
};

/**
 * @brief Retorna o caractere correspondente a um código Morse.
 * @param code String do código Morse a ser decodificada.
 * @return Caractere correspondente ou '?' se não encontrado.
 */
char decodeMorse(const char* code) {
    for (const auto& e : MORSE_TABLE)
        if (strcmp(code, e.code) == 0) return e.ch;
    return '?';
}

inline bool lightOn() { return analogRead(LDR_PIN) > THRESHOLD; }

bool     prevLight = false;
uint32_t edgeTime  = 0;
char     symBuf[8] = {};
int      symCount  = 0;

// Imprime a letra decodificada e limpa o buffer de simbolos.
// Retorna true se havia algum simbolo acumulado.
bool flushLetter() {
    if (symCount == 0) return false;
    symBuf[symCount] = '\0';
    Serial.printf("%c", decodeMorse(symBuf));
    symCount = 0;
    return true;
}

/**
 * @brief Adiciona um simbolo (ponto ou traço) ao buffer atual da letra.
 * @param s Simbolo a ser adicionado ('.' ou '-').
 */
void addSymbol(char s) {
    if (symCount < 7) {
        symBuf[symCount++] = s;
        //Serial.printf("%c ", s);
    }
}

void setup() {
    Serial.begin(115200);
    Serial.println("=== Li-Fi Receptor Morse ===");
    Serial.printf("LDR: GPIO%d | Limiar: %d\n", LDR_PIN, THRESHOLD);
    Serial.printf("Ponto:%dms | Traco:%dms | Elem:%dms | Letra:%dms | Palavra:%dms\n",
                  T_DOT, T_DASH, T_ELEM, T_LETTER, T_WORD);
    delay(100);
    int v = analogRead(LDR_PIN);
    Serial.printf("LDR: %d  (%s)\n\n", v,
                  v > THRESHOLD ? "CLARO - OK" : "ESCURO - ajuste THRESHOLD");
    Serial.println("Aguardando transmissao...\n");

    prevLight = lightOn();
    edgeTime  = millis();
}

void loop() {
    bool     cur     = lightOn();
    uint32_t now     = millis();
    uint32_t elapsed = now - edgeTime;

    if (cur != prevLight) {
        if (cur) {
            // Borda de subida (LED acendeu): classifica o gap que acabou (era OFF)
            if (elapsed >= LIM_LETTER_WORD) {
                bool hadLetter = flushLetter();
                if (hadLetter) Serial.print(" ");  // linha em branco entre palavras
            } else if (elapsed >= LIM_ELEM_LETTER) {
                flushLetter();                     // fim de letra, proxima comeca
            }
        } else {
            // Borda de descida (LED apagou): classifica o pulso que acabou (era ON)
            addSymbol(elapsed >= LIM_DOT_DASH ? '-' : '.');
        }

        prevLight = cur;
        edgeTime  = now;
    }

    // Flush por timeout: imprime ultima letra se o silencio durar >= LIM_FIM
    if (!cur && symCount > 0 && elapsed >= LIM_FIM) {
        flushLetter();
        Serial.println("\n--- Fim de transmissao ---\n");
        edgeTime = now;  // evita flush repetido
    }
}

Calibrando o Sistema

Antes de transmitir mensagens completas, é importante calibrar o limiar do receptor. Ao inicializar, o firmware exibe a leitura atual do LDR:LDR: 3021  (CLARO - OK)Se a mensagem exibida for ESCURO, significa que o LED não está incidindo sobre o LDR com intensidade suficiente, ou que o valor de THRESHOLD precisa ser reduzido. Uma forma prática de calibrar:

  1. Aponte o LED ligado diretamente no LDR
  2. Anote o valor exibido na Serial (por exemplo, 3200)
  3. Apague o LED e anote o novo valor (por exemplo, 800)
  4. Defina THRESHOLD como o valor médio entre os dois (neste caso, 2000)
Com esse procedimento, o receptor distingue com segurança os estados "luz acesa" e "luz apagada", independente de variações de iluminação ambiente.

Teste com Celular Android como Transmissor

Uma das formas mais interessantes de validar o receptor é usar o celular como transmissor, eliminando completamente o ESP32 transmissor do processo. Para isso, utilizamos o aplicativo Morse Code Torch (desenvolvido por perryOnCrack), disponível gratuitamente na Play Store.O aplicativo converte qualquer texto digitado em pulsos da lanterna do celular, seguindo as temporizações do Código Morse. Como a lanterna do celular é extremamente brilhante, ela se torna um transmissor Li-Fi bastante potente, capaz de acionar o LDR mesmo a distâncias maiores do que o LED do circuito transmissor.

Como Realizar o Teste

1. Instale o aplicativoBaixe o Morse Code Torch na Play Store. O app é gratuito, sem anúncios e não exige cadastro.2. Prepare o receptorLigue o ESP32 receptor ao computador, abra o Monitor Serial em 115200 baud e aguarde a mensagem de inicialização com a leitura do LDR:
=== Li-Fi Receptor Morse ===
LDR: GPIO4 | Limiar: 2500
LDR: 3150  (CLARO - OK)
Aguardando transmissao...
3. Configure o aplicativoAbra o Morse Code Torch, digite a mensagem desejada no campo de texto. Antes de transmitir, verifique a velocidade de transmissão configurada no app, a velocidade padrão do receptor é 80, coloque o valor de 80 no app.4. Alinhe a lanterna com o LDRPosicione a câmera traseira do celular (onde fica a lanterna) apontada diretamente para o LDR, a uma distância de 10 a 20 cm. O alinhamento preciso é importante: a lanterna do celular tem um ângulo de abertura relativamente estreito, então pequenos desvios já podem reduzir a leitura.5. Inicie a transmissãoPressione o botão de transmissão no aplicativo. A lanterna começará a piscar e o Monitor Serial do receptor exibirá as letras sendo decodificadas em tempo real.

Ajuste de Temporização

O ponto de atenção mais importante neste teste é a compatibilidade entre a velocidade do app e os limiares do firmware receptor. O firmware utiliza as seguintes constantes para classificar os pulsos:
LimiarValorSignificado
LIM_DOT_DASH160msPulsos abaixo = ponto / acima = traço
LIM_ELEM_LETTER160msSilêncio abaixo = entre símbolos / acima = nova letra
LIM_LETTER_WORD400msSilêncio acima = nova palavra
LIM_FIM1120msSilêncio acima = fim de transmissão
Esses valores foram calibrados para a velocidade do firmware transmissor (ponto de 80ms, traço de 240ms).Se as letras saírem erradas ou aparecerem como ? no Monitor Serial, edite os limiares no receiver.cpp para valores maiores ou modifique a velocidade no aplicativo para valores menores que 80. Ajuste os valores até obter decodificação correta.Abaixo o vídeo demonstrando a comunicação:

 

Por Que Isso É Interessante?

Este teste demonstra um ponto importante dos sistemas de comunicação óptica: transmissor e receptor precisam estar sincronizados em velocidade, exatamente como acontece em protocolos seriais (UART, SPI) ou em qualquer enlace de telecomunicação real. Ao ajustar os limiares para o celular, você está essencialmente configurando o "baud rate" óptico do receptor — um conceito diretamente análogo ao que acontece em modems e transceivers profissionais.Além disso, o fato de o celular funcionar como transmissor sem nenhuma modificação de hardware mostra a versatilidade do receptor: qualquer fonte de luz controlável pode ser usada para alimentar o canal Li-Fi.

Resultado

Após montar os dois circuitos e gravar os firmwares, o fluxo de operação é o seguinte:

  1. Abra o Monitor Serial do transmissor (115200 baud)
  2. Digite uma mensagem e pressione Enter
  3. O LED começa a piscar transmitindo os pulsos
  4. No Monitor Serial do receptor, as letras aparecem sendo decodificadas em tempo real:
O sistema funciona de forma estável para distâncias de até 15cm em ambientes com boa iluminação. Com um LED de alto brilho e em ambientes com pouca luz ambiente, é possível alcançar distâncias maiores. Veja no vídeo abaixo:

Conclusão

Este projeto demonstra de forma prática e didática como é possível implementar comunicação óptica sem fio utilizando componentes acessíveis e dois módulos ESP32. Ao construir o sistema Li-Fi, exploramos conceitos fundamentais de telecomunicações como modulação, temporização, detecção de bordas e decodificação de sinai, todos aplicáveis a sistemas reais de comunicação por luz.O projeto serve como ponto de partida para evoluções futuras, como:

  • Aumentar a velocidade de transmissão com temporizações menores e fotodetectores mais sensíveis
  • Adicionar verificação de erros (checksum ou CRC) para maior confiabilidade
  • Utilizar um fotodetector de alta velocidade no lugar do LDR para comunicações mais rápidas
  • Explorar modulação de amplitude (variação de brilho) ao invés de on/off simples
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!

Sobre o Autor


Saulo Aislan

Graduando em Tecnologia em Telemática pelo IFPB – Campus de Campina Grande - PB. Tenho experiência com os microcontroladores da família Arduino, ESP8266, ESP32, STM32 e microprocessador Raspberry Pi. Tenho projetos na áreas de IoTs voltada para a indústria 4.0, agroindústria e indústria aeroespacial civil utilizando LoRa, Bluetooth, ZigBee e Wi-Fi. Atualmente estudando e desenvolvendo em FreeRTOS para sistemas em tempo real com ESP32 e LoRaWan para Smart City e compartilhando alguns projetos no blog da Eletrogate.

Nesse sistema, utilizaremos LEDs para transmitir informações por meio de pulsos luminosos extremamente rápidos, enquanto sensores ópticos detectam essas variações e convertem novamente os sinais em dados digitais.

Precisa dos componentes para este projeto?

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