Projeto 03 — Lâmpada controlada pelo celular

Robótica com C — Bloco 3: Projetos Práticos

O que vamos construir

Uma lâmpada (ou qualquer carga elétrica de 110V/220V) que você liga e desliga pelo celular, de qualquer lugar dentro do alcance do Bluetooth. Este projeto combina os dois componentes introduzidos nos projetos anteriores — o relay do Projeto 01 e o módulo Bluetooth HC-05 do Projeto 02 — num sistema de automação residencial completo.

📱 → 💡
Celular envia "L"
Bluetooth HC-05 recebe → Arduino aciona relay → lâmpada liga
📱 → 🌑
Celular envia "D"
Bluetooth HC-05 recebe → Arduino desaciona relay → lâmpada apaga

O que este projeto acrescenta: nos Projetos 01 e 02 você aprendeu relay e Bluetooth separadamente. Aqui eles se integram — o celular é o controle remoto do relay. Além disso, o projeto adiciona feedback de estado (o Arduino responde ao celular confirmando o que fez) e um modo timer (desliga automaticamente após X minutos).

🖥️ Simulação no Wokwi

Situação semelhante ao Projeto 02: o Wokwi possui o componente relay nativamente (você pode arrastar e ver o circuito completo), mas não simula o Bluetooth HC-05. O rádio e o pareamento com o celular não têm equivalente no simulador.

A estratégia é a mesma do Projeto 02: use o Monitor Serial como substituto do celular para validar toda a lógica, e reserve o HC-05 físico para a etapa de hardware.

O que você consegue simular no Wokwi:

Para simular, use a versão com Serial no lugar de bluetooth — idêntica ao que você fez no Projeto 02. A versão com HC-05 físico está na Seção 5.

Relay no Wokwi: adicione o componente Relay Module no diagrama. Conecte o pino IN ao Arduino. O Wokwi mostra visualmente o contato NO abrindo e fechando ao chamar digitalWrite() — ótimo para entender a lógica invertida antes de usar o hardware físico.

Hardware necessário

Componente Conexão Observação
HC-05 VCC/GND → 5V / GND —
HC-05 TX → pino 10 (RX software) Direto — HC-05 envia 3,3V, OK para o Arduino
HC-05 RX → divisor R1/R2 → pino 11 (TX software) R1=1kΩ, R2=2kΩ — obrigatório!
Relay IN → pino 7 Lógica invertida: LOW = relay aciona
Relay VCC/GND → 5V / GND —
LED status + 220Ω pino 8 → R → LED → GND Indica visualmente se a lâmpada está ligada
Lâmpada (ou LED de teste) Relay COM + NO → carga → neutro Para testes iniciais: substitua por LED + resistor

⚠️ Segurança com 110V/220V: para validar o código, use um LED de 5V no lugar da lâmpada real. Só conecte a rede elétrica quando o código estiver totalmente testado, em bancada adequada e com o circuito desenergizado durante a fiação. Nunca mexa nas conexões com tensão aplicada.

Protocolo de comandos

Este projeto expande o protocolo do Projeto 02 com novos comandos — incluindo o timer e consulta de estado:

Comando Ação Resposta do Arduino
L Liga a lâmpada LAMP:1
D Desliga a lâmpada LAMP:0
T Toggle — inverte o estado atual LAMP:1 ou LAMP:0
S Status — consulta o estado atual sem mudar LAMP:1 ou LAMP:0
Tnnn Timer — liga por nnn minutos e desliga TIMER:nnn
X Cancela o timer ativo TIMER:OFF
? Lista de comandos Texto com os comandos

Por que prefixo chave:valor nas respostas? "LAMP:1" é parseável pelo app — você pode configurar o Serial Bluetooth Terminal para mudar a cor de um botão quando receber "LAMP:1". Texto livre como "Lâmpada ligada!" é legível para humanos mas difícil de processar programaticamente. Adote sempre o formato estruturado em projetos reais.

Código completo (versão com HC-05 físico)

/*
 * =====================================================
 * Projeto 03 — Lâmpada controlada pelo celular
 * Relay + Bluetooth HC-05
 * =====================================================
 * Comandos: L=liga D=desliga T=toggle S=status
 *           T001-T999=timer (minutos) X=cancela timer
 *           ?=ajuda
 * Respostas: LAMP:0/1 | TIMER:nnn | TIMER:OFF
 * =====================================================
 */

#include <SoftwareSerial.h>

// ── Bluetooth ──────────────────────────────────────
SoftwareSerial bluetooth(10, 11);  // RX=10, TX=11

// ── Pinos ──────────────────────────────────────────
const int PINO_RELAY  = 7;
const int PINO_STATUS = 8;

// ── Estado ─────────────────────────────────────────
bool lampLigada = false;

// ── Timer ──────────────────────────────────────────
bool          timerAtivo    = false;
unsigned long timerFim      = 0;    // millis() em que o timer expira
unsigned int  timerMinutos  = 0;


// ════════════════════════════════════════════════════
// Controla relay + LED de status
// Relay: LOW = aciona (lógica invertida)
// ════════════════════════════════════════════════════
void setLampada(bool ligar) {
    lampLigada = ligar;
    digitalWrite(PINO_RELAY,  ligar ? LOW  : HIGH);
    digitalWrite(PINO_STATUS, ligar ? HIGH : LOW);
    bluetooth.print("LAMP:");
    bluetooth.println(ligar ? "1" : "0");
    Serial.print("LAMP:"); Serial.println(ligar ? "1" : "0");
}

// ════════════════════════════════════════════════════
// Ativa o timer: liga a lâmpada por X minutos
// ════════════════════════════════════════════════════
void ativarTimer(unsigned int minutos) {
    timerMinutos = minutos;
    timerAtivo   = true;
    timerFim     = millis() + ((unsigned long)minutos * 60000UL);
    setLampada(true);
    bluetooth.print("TIMER:"); bluetooth.println(minutos);
    Serial.print("TIMER:"); Serial.println(minutos);
}

void cancelarTimer() {
    timerAtivo = false;
    bluetooth.println("TIMER:OFF");
    Serial.println("TIMER:OFF");
}

// ════════════════════════════════════════════════════
// Verifica se o timer expirou
// ════════════════════════════════════════════════════
void verificarTimer() {
    if (!timerAtivo) return;

    if (millis() >= timerFim) {
        timerAtivo = false;
        setLampada(false);
        bluetooth.println("TIMER:EXPIROU");
        Serial.println("TIMER:EXPIROU");
    }
}

// ════════════════════════════════════════════════════
// Processa comandos recebidos pelo Bluetooth
// ════════════════════════════════════════════════════
void processarMensagem(String msg) {
    if (msg.length() == 0) return;

    char cmd = msg[0];

    switch (cmd) {
        case 'L': case 'l':
            cancelarTimer();           // liga manual cancela timer
            setLampada(true);
            break;

        case 'D': case 'd':
            cancelarTimer();
            setLampada(false);
            break;

        case 'T': case 't':
            if (msg.length() > 1) {
                // "T005" → timer de 5 minutos
                int minutos = constrain(msg.substring(1).toInt(), 1, 999);
                ativarTimer(minutos);
            } else {
                // "T" sozinho → toggle
                cancelarTimer();
                setLampada(!lampLigada);
            }
            break;

        case 'S': case 's':
            bluetooth.print("LAMP:"); bluetooth.println(lampLigada ? "1" : "0");
            if (timerAtivo) {
                unsigned long restante = (timerFim - millis()) / 60000UL;
                bluetooth.print("TIMER:"); bluetooth.println(restante + 1);
            }
            break;

        case 'X': case 'x':
            cancelarTimer();
            break;

        case '?':
            bluetooth.println("Cmds: L D T S T001-T999 X ?");
            break;

        case '\n': case '\r': return;

        default:
            bluetooth.print("ERR:"); bluetooth.println(cmd);
    }
}


// ════════════════════════════════════════════════════
// SETUP e LOOP
// ════════════════════════════════════════════════════

void setup() {
    pinMode(PINO_RELAY,  OUTPUT);
    pinMode(PINO_STATUS, OUTPUT);
    digitalWrite(PINO_RELAY, HIGH);   // garante relay desligado no boot
    digitalWrite(PINO_STATUS, LOW);

    Serial.begin(9600);
    bluetooth.begin(9600);

    Serial.println("=== Lampada Bluetooth ===");
    Serial.println("Aguardando conexao...");
}

void loop() {
    // Verifica timer antes de processar comandos
    verificarTimer();

    // Lê comandos do Bluetooth
    if (bluetooth.available() > 0) {
        String msg = bluetooth.readStringUntil('\n');
        msg.trim();
        processarMensagem(msg);
    }

    // Espelho para debug pelo Monitor Serial
    if (Serial.available() > 0) {
        bluetooth.write(Serial.read());
    }
}

=== Lampada Bluetooth ===

Aguardando conexao...

[celular envia "L"] LAMP:1

[celular envia "S"] LAMP:1

[celular envia "T10"] LAMP:1 / TIMER:10

[celular envia "S"] LAMP:1 / TIMER:10

... 10 minutos depois ...

TIMER:EXPIROU

LAMP:0

[celular envia "D"] LAMP:0

Código adaptado para simulação no Wokwi

Substitua bluetooth por Serial e envie os comandos pelo Monitor Serial. A lógica de relay, timer e estados é 100% idêntica:

// ── Versão WOKWI ───────────────────────────────────
// Remove SoftwareSerial — usa Serial direto
// Envie pelo Monitor Serial: L D T T005 S X ?
// O relay e o LED de status funcionam normalmente

const int PINO_RELAY  = 7;
const int PINO_STATUS = 8;

bool          lampLigada  = false;
bool          timerAtivo  = false;
unsigned long timerFim    = 0;

void setLampada(bool ligar) {
    lampLigada = ligar;
    digitalWrite(PINO_RELAY,  ligar ? LOW  : HIGH);
    digitalWrite(PINO_STATUS, ligar ? HIGH : LOW);
    Serial.print("LAMP:"); Serial.println(ligar ? "1" : "0");
}

void ativarTimer(unsigned int minutos) {
    timerAtivo = true;
    timerFim   = millis() + ((unsigned long)minutos * 60000UL);
    setLampada(true);
    Serial.print("TIMER:"); Serial.println(minutos);
}

void cancelarTimer() {
    timerAtivo = false;
    Serial.println("TIMER:OFF");
}

void verificarTimer() {
    if (timerAtivo && millis() >= timerFim) {
        timerAtivo = false;
        setLampada(false);
        Serial.println("TIMER:EXPIROU");
    }
}

void processarMensagem(String msg) {
    if (msg.length() == 0) return;
    char cmd = msg[0];
    switch (cmd) {
        case 'L': case 'l': cancelarTimer(); setLampada(true);          break;
        case 'D': case 'd': cancelarTimer(); setLampada(false);         break;
        case 'T': case 't':
            if (msg.length() > 1) ativarTimer(constrain(msg.substring(1).toInt(), 1, 999));
            else                  { cancelarTimer(); setLampada(!lampLigada); }
            break;
        case 'S': case 's': Serial.print("LAMP:"); Serial.println(lampLigada ? "1" : "0"); break;
        case 'X': case 'x': cancelarTimer(); break;
        case '?': Serial.println("Cmds: L D T S T001-T999 X ?"); break;
        case '\n': case '\r': return;
        default: Serial.print("ERR:"); Serial.println(cmd);
    }
}

void setup() {
    pinMode(PINO_RELAY,  OUTPUT);
    pinMode(PINO_STATUS, OUTPUT);
    digitalWrite(PINO_RELAY, HIGH);
    Serial.begin(9600);
    Serial.println("=== Lampada Wokwi — envie: L D T T005 S X ?");
}

void loop() {
    verificarTimer();
    if (Serial.available() > 0) {
        String msg = Serial.readStringUntil('\n');
        msg.trim();
        processarMensagem(msg);
    }
}

Teste o timer no Wokwi assim: envie T1 (1 minuto). O Wokwi roda em tempo real, então espere 60 segundos — ou reduza temporariamente 60000UL para 10000UL (10 segundos) durante o teste e restaure antes de gravar no hardware físico.

Decisões de projeto

Por que cancelarTimer() é chamado antes de L e D?

Se o timer está ativo e o usuário envia L ou D, o comando manual tem prioridade. Sem cancelar o timer, a lâmpada seria apagada pelo timer mesmo depois de o usuário tê-la ligado manualmente — comportamento confuso e indesejado.

Por que unsigned long e o sufixo UL no timer?

O cálculo minutos * 60000 pode ultrapassar 32767 (limite de int de 2 bytes no Uno) para valores acima de ~32 minutos. O sufixo UL força o literal para unsigned long (4 bytes, até ~49 dias), evitando overflow silencioso — o mesmo problema de tipos que você viu na Aula 02.

Por que readStringUntil('\n') em vez de read()?

O comando de timer tem múltiplos caracteres ("T010"). Com read() você leria um byte por vez e precisaria montar a string manualmente. readStringUntil('\n') coleta tudo até o Enter e entrega a string completa — mais simples e sem risco de ler a metade de um comando.

Evolução do protocolo: o Projeto 02 usava read() com um byte por comando. O Projeto 03 usa readStringUntil() com strings. Ambos os padrões têm uso — read() é mais rápido e consome menos memória, readStringUntil() é mais flexível para comandos com parâmetros. Você vai escolher o mais adequado caso a caso nos projetos seguintes.

Configurando botões no app

Configure os botões de macro no Serial Bluetooth Terminal para controle com um toque:

Botão Rótulo Macro enviada
M1 💡 Ligar L
M2 🌑 Desligar D
M3 🔄 Toggle T
M4 ⏱️ 30 min T030
M5 ⏱️ 60 min T060
M6 ❌ Cancel timer X
M7 📊 Status S

Conceitos do curso aplicados neste projeto

Conceito Aula de origem Onde aparece neste projeto
Relay com lógica invertida Projeto 01 setLampada() — LOW aciona, HIGH desaciona
Bluetooth HC-05 + SoftwareSerial Projeto 02 Canal de comunicação — substituível por Serial no Wokwi
Protocolo com prefixo Aula 46 Comando "T010" → timer de 10 minutos
Temporizador com millis() Aula 41b / Projeto 01 timerFim + verificarTimer()
Overflow de int / unsigned long Aula 02 Sufixo UL em 60000UL para evitar overflow
Feedback estruturado (chave:valor) Aula 46 "LAMP:1", "TIMER:10", "ERR:cmd"
Arquitetura em camadas Aula 47 setLampada(), verificarTimer(), processarMensagem()

Quiz — Projeto 03

Teste sua compreensão do projeto de lâmpada com timer:

1. Por que cancelarTimer() é chamado dentro dos casos 'L' e 'D' antes de setLampada()?

2. Por que o cálculo do timer usa 60000UL em vez de 60000?

3. O comando T sozinho faz toggle, e T010 ativa o timer por 10 minutos. Como o código distingue os dois casos?

4. Por que este projeto usa readStringUntil('\n') em vez de read() byte a byte, como fazia o Projeto 02?

5. O que acontece se o timer expirar enquanto o usuário estiver com o app fechado no celular?

6. Por que o relay recebe digitalWrite(PINO_RELAY, HIGH) logo no setup()?

7. Na simulação no Wokwi, você envia T1 pelo Monitor Serial. Quanto tempo precisa esperar para ver "TIMER:EXPIROU"?

8. O que muda no código ao migrar da versão Wokwi (Serial) para a versão física (HC-05)?