Projeto 01 — Luz automática com PIR e relay

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

O que vamos construir

Uma luz que acende sozinha quando detecta movimento e apaga automaticamente após alguns segundos sem atividade. O mesmo sistema usado em corredores, banheiros, garagens e áreas de segurança.

É o primeiro projeto do Bloco 3 e introduz o componente mais importante da automação residencial: o módulo relay — que permite ao Arduino controlar cargas de 110V/220V com segurança.

🏃 Movimento detectado
PIR aciona o relay → lâmpada acende → temporizador reinicia
⏱️ Sem movimento por X segundos
Temporizador expira → relay desliga → lâmpada apaga

Conexão com o Bloco 2: este projeto usa exatamente os padrões que você dominou nas aulas 44–47: millis() para o temporizador, enum para os estados do sistema (AGUARDANDO, ATIVO, APAGANDO), e a arquitetura em camadas com sensores, estado e saída física separados.

🔌 Componente novo: módulo relay

O relay (relé) é um interruptor eletromecânico controlado eletricamente. Por dentro, um eletroímã é energizado por um sinal de 5V do Arduino, o que atrai uma alavanca metálica e fecha (ou abre) um contato de alta tensão. O circuito do Arduino e o circuito da lâmpada são completamente isolados — sem risco de 220V chegar ao microcontrolador.

Lado de baixa tensão (Arduino)
  • VCC → 5V do Arduino
  • GND → GND do Arduino
  • IN → pino digital do Arduino
Lado de alta tensão (carga)
  • COM — terminal comum
  • NO — normalmente aberto (fecha ao acionar)
  • NC — normalmente fechado (abre ao acionar)
Aspecto Detalhe
Tensão de controle 5V (compatível direto com Arduino)
Lógica de acionamento Ativo em LOW — digitalWrite(RELAY, LOW) liga a carga
Capacidade da carga Até 10A / 250VAC ou 10A / 30VDC (verificar no módulo)
Isolamento Optoacoplador — Arduino completamente isolado da rede elétrica
Corrente de controle ~15–20 mA — dentro do limite seguro de um pino digital
Tempo de resposta ~10 ms — adequado para automação residencial

⚠️ Lógica invertida — muito importante: a maioria dos módulos relay de 1 canal para Arduino usa lógica ativa em LOW. Isso significa:

Isso parece contraintuitivo, mas é assim por projeto — garante que a carga fique desligada se o Arduino reiniciar ou perder energia. Verifique sempre o datasheet do seu módulo específico.

⚠️ Segurança com 110V/220V: nos projetos em simulação (Wokwi) e em testes iniciais, substitua a lâmpada real por um LED de 5V para validar o código. Só trabalhe com tensão de rede em bancada adequada, com isolamento e nunca com o circuito energizado ao fazer conexões físicas. Em caso de dúvida, simule no Wokwi e peça auxílio de alguém com experiência em elétrica.

Circuito no Wokwi

Para simulação no Wokwi, a "lâmpada" será representada por um LED comum com resistor — o relay é controlado exatamente da mesma forma que controlaria uma lâmpada real:

Componente Pino / Conexão Função
Sensor PIR OUT → pino 2 Detecta movimento — HIGH quando ativo
Módulo Relay IN → pino 7 Controla a lâmpada — lógica invertida
LED (simula lâmpada) Saída NO do relay → LED → GND Representa a carga controlada
LED de status pino 8 → LED vermelho → GND Indica que o sistema está no modo ATIVO
Potenciômetro (opcional) cursor → A0 Ajusta o tempo de espera em tempo real
// Ligação resumida:
// PIR:   VCC→5V, GND→GND, OUT→pino 2
// Relay: VCC→5V, GND→GND, IN→pino 7
// LED lâmpada: relay NO → resistor 220Ω → LED → GND
// LED status:  pino 8 → resistor 220Ω → LED vermelho → GND
// Potenc:  terminal esq→5V, terminal dir→GND, cursor→A0

Código completo

O projeto usa três estados: o sistema aguarda movimento, está ativo (lâmpada acesa, temporizador rodando) ou em transição de apagamento. O tempo de espera é ajustável via potenciômetro:

/*
 * =====================================================
 * Projeto 01 — Luz automática com PIR e Relay
 * =====================================================
 * AGUARDANDO → movimento detectado → ATIVO
 * ATIVO       → sem movimento por T segundos → AGUARDANDO
 * T (tempo de espera) ajustável via potenciômetro
 * =====================================================
 */

// ── Pinos ──────────────────────────────────────────
const int PINO_PIR    = 2;   // saída do sensor PIR
const int PINO_RELAY  = 7;   // controle do relay (lógica invertida)
const int PINO_STATUS = 8;   // LED de status do sistema
const int PINO_POT    = A0;  // potenciômetro de tempo

// ── Configuração ───────────────────────────────────
const unsigned long T_MIN =  5000;  // 5s mínimo
const unsigned long T_MAX = 30000;  // 30s máximo

// ── Máquina de estados ─────────────────────────────
typedef enum { AGUARDANDO, ATIVO } EstadoLuz;
EstadoLuz estado = AGUARDANDO;

// ── Temporização ───────────────────────────────────
unsigned long ultimoMovimento = 0;  // quando foi o último movimento


// ════════════════════════════════════════════════════
// Lê e converte o tempo de espera do potenciômetro
// ════════════════════════════════════════════════════
unsigned long lerTempoEspera() {
    int pot = analogRead(PINO_POT);                    // 0–1023
    return map(pot, 0, 1023, T_MIN, T_MAX);            // 5s–30s
}

// ════════════════════════════════════════════════════
// Controla relay e LED de status
// Relay: LOW = aciona (lógica invertida)
// ════════════════════════════════════════════════════
void setLuz(bool ligar) {
    digitalWrite(PINO_RELAY,  ligar ? LOW  : HIGH);   // invertido!
    digitalWrite(PINO_STATUS, ligar ? HIGH : LOW);
}

// ════════════════════════════════════════════════════
// Processa o sensor PIR e o temporizador
// ════════════════════════════════════════════════════
void processarPIR() {
    bool movimento = (digitalRead(PINO_PIR) == HIGH);
    unsigned long agora = millis();
    unsigned long tempoEspera = lerTempoEspera();

    switch (estado) {

        case AGUARDANDO:
            if (movimento) {
                estado          = ATIVO;
                ultimoMovimento = agora;
                setLuz(true);
                Serial.print("Movimento! Luz ON — espera: ");
                Serial.print(tempoEspera / 1000);
                Serial.println("s");
            }
            break;

        case ATIVO:
            if (movimento) {
                // Reinicia o temporizador enquanto há movimento
                ultimoMovimento = agora;
            }

            // Verifica se o tempo de espera expirou
            if (agora - ultimoMovimento >= tempoEspera) {
                estado = AGUARDANDO;
                setLuz(false);
                Serial.println("Sem movimento — Luz OFF");
            }
            break;
    }
}


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

void setup() {
    pinMode(PINO_PIR,    INPUT);
    pinMode(PINO_RELAY,  OUTPUT);
    pinMode(PINO_STATUS, OUTPUT);

    // Garante que o relay começa desligado (HIGH = desligado na lógica invertida)
    digitalWrite(PINO_RELAY, HIGH);
    digitalWrite(PINO_STATUS, LOW);

    Serial.begin(9600);
    Serial.println("=== Luz Automática com PIR ===");
    Serial.println("Aguardando movimento...");
    Serial.println("(Ajuste o potenciômetro para mudar o tempo de espera)");
}

void loop() {
    processarPIR();

    // Imprime o tempo de espera configurado a cada 5s (debug)
    static unsigned long ultimoDebug = 0;
    if (millis() - ultimoDebug >= 5000) {
        ultimoDebug = millis();
        if (estado == AGUARDANDO) {
            Serial.print("Aguardando... (tempo configurado: ");
            Serial.print(lerTempoEspera() / 1000);
            Serial.println("s)");
        }
    }
}

=== Luz Automática com PIR ===

Aguardando movimento...

(Ajuste o potenciômetro para mudar o tempo de espera)

Aguardando... (tempo configurado: 15s)

Movimento! Luz ON — espera: 15s

Movimento! Luz ON — espera: 15s

Sem movimento — Luz OFF

Aguardando... (tempo configurado: 8s)

Decisões de projeto — por que cada escolha foi feita

Por que reiniciar o temporizador enquanto há movimento?

No estado ATIVO, cada leitura de movimento chama ultimoMovimento = agora. Se o PIR continua detectando movimento, a condição agora - ultimoMovimento >= tempoEspera nunca será verdadeira — a luz permanece acesa pelo tempo necessário. Sem isso, a luz apagaria no meio de uma conversa ou passagem lenta.

Por que digitalWrite(PINO_RELAY, HIGH) no setup()?

Com lógica invertida, HIGH = relay desligado. Inicializar explicitamente garante que a lâmpada não ficará ligada durante o boot do Arduino (que pode demorar centenas de milissegundos). É uma medida de segurança — o estado inicial nunca deve ser ambíguo.

Por que o tempo de espera é lido a cada iteração e não uma vez no setup?

Porque queremos que o potenciômetro ajuste o tempo em tempo real. Se você girar o potenciômetro enquanto a luz está acesa, o novo tempo será aplicado ao próximo ciclo. lerTempoEspera() é chamada dentro do switch, então cada verificação usa o valor mais recente.

Padrão "temporizador reiniciável": guardar ultimoMovimento e comparar com millis() é um padrão que aparece em todos os projetos de automação com timeout — portão que fecha automaticamente, ar condicionado que desliga por inatividade, alarme que espera antes de acionar. Memorize esse padrão.

Variações e extensões

Variação 1: Modo noturno com LDR

A luz só deve acionar de noite. Adicione um LDR e uma condição extra em processarPIR():

// Adicione no topo:
const int PINO_LDR       = A1;
const int LIMIAR_NOTURNO = 300;   // abaixo = escuro = noite

// Dentro de case AGUARDANDO, modifique a condição:
bool eNoite  = (analogRead(PINO_LDR) < LIMIAR_NOTURNO);

if (movimento && eNoite) {
    // Aciona apenas se for noite E houver movimento
    estado          = ATIVO;
    ultimoMovimento = agora;
    setLuz(true);
    Serial.println("Noite + movimento → Luz ON");
}

Variação 2: LCD mostrando status

Integre o LCD I2C da Aula 45 para exibir o estado e o tempo restante — exatamente como no semáforo:

// Após incluir Wire.h e LiquidCrystal_I2C.h:
void atualizarLCD() {
    static unsigned long ultimaTela = 0;
    if (millis() - ultimaTela < 500) return;
    ultimaTela = millis();

    lcd.setCursor(0, 0);
    lcd.print(estado == ATIVO ? "LUZ: LIGADA     " : "LUZ: AGUARDANDO ");

    lcd.setCursor(0, 1);
    if (estado == ATIVO) {
        unsigned long restante = lerTempoEspera() - (millis() - ultimoMovimento);
        lcd.print("Desliga em: ");
        lcd.print(restante / 1000);
        lcd.print("s  ");
    } else {
        lcd.print("Tempo: ");
        lcd.print(lerTempoEspera() / 1000);
        lcd.print("s      ");
    }
}

Variação 3: Preparando para Bluetooth

Adicionar controle remoto por app é a transição natural para o Projeto 03. Basta incluir um processarSerial() no loop():

// Comandos para adicionar:
// "L" → força ligar manualmente
// "D" → força desligar manualmente
// "A" → volta ao modo automático (PIR)

typedef enum { AUTO_PIR, MANUAL_ON, MANUAL_OFF } ModoControle;
ModoControle modo = AUTO_PIR;

// No loop(): se modo == AUTO_PIR, chama processarPIR()
// Caso contrário, o estado foi forçado manualmente

Construa incrementalmente. Comece com o código base, teste no Wokwi, depois adicione o LDR, depois o LCD, depois o Bluetooth. Cada passo é uma camada. Se algo quebrar, você sabe exatamente onde procurar.

Lista de materiais para hardware físico

Componente Especificação Onde encontrar (BR) Preço estimado
Arduino Uno ou Nano ATmega328P Mercado Livre, Robocore, FilipeFlop R$ 25–60
Sensor PIR HC-SR501 Alcance ~7m, ângulo 120° Mercado Livre, lojas de eletrônica R$ 10–20
Módulo Relay 1 canal 5V, 10A/250VAC, com optoacoplador Mercado Livre, FilipeFlop R$ 8–15
Potenciômetro 10kΩ Linear Qualquer loja de eletrônica R$ 2–5
LED + resistor 220Ω Para teste sem rede elétrica Qualquer loja de eletrônica R$ 1–2
Protoboard + jumpers 400 pontos ou mais Mercado Livre, Robocore R$ 15–25

Kit recomendado para todos os projetos do Bloco 3: um kit iniciante de Arduino com protoboard, jumpers, LEDs, resistores, potenciômetro e sensores básicos cobre os primeiros 6 projetos. Procure por "kit Arduino starter" — custam entre R$80–150 e incluem a maioria dos componentes necessários.

Conceitos do curso aplicados neste projeto

Conceito Aula de origem Onde aparece neste projeto
enum de estados Aula 44 typedef enum { AGUARDANDO, ATIVO }
Temporizador com millis() Aula 41b / 44 ultimoMovimento + comparação com tempoEspera
Sensor PIR digital Aula 41c digitalRead(PINO_PIR) == HIGH
Leitura analógica + map() Aula 43 Potenciômetro → lerTempoEspera()
Arquitetura em camadas Aula 47 processarPIR(), setLuz(), lerTempoEspera()
Inicialização segura de pinos Aula 42 digitalWrite(PINO_RELAY, HIGH) no setup()
Debug com variável static Aula 45 Print periódico a cada 5s no loop()

Quiz — Projeto 01

Teste sua compreensão do relay e do projeto de luz automática:

1. Por que o relay permite ao Arduino controlar uma lâmpada de 220V com segurança?

2. No módulo relay com lógica invertida, o que acontece quando você chama digitalWrite(PINO_RELAY, HIGH)?

3. Por que ultimoMovimento = agora é executado mesmo quando já estamos no estado ATIVO e há movimento?

4. Por que digitalWrite(PINO_RELAY, HIGH) é chamado no setup() logo após o pinMode()?

5. O que faz map(pot, 0, 1023, T_MIN, T_MAX) na função lerTempoEspera()?

6. Na variação com LDR, a condição if (movimento && eNoite) usa qual operador lógico do C que você aprendeu na Aula 05?

7. Qual é o terminal do relay que deve ser usado para acionar uma carga que normalmente fica desligada?

8. O padrão ultimoEvento = millis() + if (agora - ultimoEvento >= timeout) é chamado "temporizador reiniciável". Qual outro projeto do Bloco 3 vai usar esse mesmo padrão?