Aula 47 — Projeto Integrador: painel de controle doméstico

Robótica com C — Bloco 2

O projeto: painel de controle com três modos

Esta aula não introduz nenhum componente ou função nova. Ela consolida tudo que o Bloco 2 ensinou num único projeto funcional — o mesmo tipo de sistema que os projetos do Bloco 3 vão usar, mas sem Bluetooth ainda.

O sistema simula um painel de controle doméstico com três modos de operação, dois dispositivos (LED de iluminação e LED de alarme), um sensor de luminosidade (LDR) e um display LCD que mostra o estado em tempo real:

Modo Comportamento Como ativar
AUTO A iluminação acende e apaga automaticamente conforme a luminosidade do LDR Comando "A" via Serial
MANUAL Iluminação controlada diretamente por comandos Serial ("L" liga, "D" desliga) Comando "M" via Serial
EMERGENCIA Iluminação pisca rapidamente, alarme ligado, ignora todos os outros comandos exceto "R" para resetar Comando "E" via Serial ou LDR abaixo do limiar crítico

Por que este projeto é o divisor de águas do curso: os projetos do Bloco 3 têm exatamente esta estrutura — modos de operação + LCD + controle remoto. A diferença será apenas o canal (Bluetooth em vez de USB) e os dispositivos (relay, servo, motor em vez de LEDs). Dominar este projeto significa estar pronto para o Bloco 3.

Hardware necessário

Componente Pino Função no projeto
LED amarelo + resistor 220Ω 9 (PWM) Iluminação — controlada pelo modo
LED vermelho + resistor 220Ω 8 Alarme — ativo no modo EMERGENCIA
LDR + resistor 10kΩ A0 Sensor de luminosidade para o modo AUTO
LCD I2C 16×2 A4 (SDA), A5 (SCL) Exibe modo atual, status e luminosidade
// Montagem no Wokwi:
// Pino 9  → resistor 220Ω → LED amarelo (ânodo) → GND
// Pino 8  → resistor 220Ω → LED vermelho (ânodo) → GND
// 5V → resistor 10kΩ → A0 → LDR → GND   (divisor de tensão)
// LCD I2C: GND→GND, VCC→5V, SDA→A4, SCL→A5

Arquitetura: planejar antes de codificar

Projetos com múltiplos subsistemas (estado + sensor + display + Serial) ficam difíceis de depurar quando tudo está misturado no loop(). A solução é a mesma que você usou em C desde a Aula 13: dividir em funções com responsabilidade única.

O projeto é dividido em quatro camadas:

Camada Funções Responsabilidade
Sensores lerLuminosidade() Leitura de hardware — retorna dados brutos
Estado mudarModo(), processarAuto(), processarEmergencia() Lógica de transição e comportamento de cada modo
Saída física atualizarLEDs() Traduz o estado em sinais elétricos
Interface atualizarLCD(), processarSerial() Comunicação com o usuário

O loop() apenas orquestra essas camadas — não contém lógica de negócio:

// Como o loop() vai ficar — lê-se como um roteiro:
void loop() {
    processarSerial();       // 1. há comandos do usuário?
    int lum = lerLuminosidade(); // 2. qual a luminosidade agora?

    switch (modoAtual) {
        case AUTO:       processarAuto(lum);       break;
        case EMERGENCIA: processarEmergencia();    break;
        case MANUAL:     /* aguarda comandos */   break;
    }

    atualizarLEDs();         // 3. atualiza os pinos físicos
    atualizarLCD(lum);       // 4. atualiza o display
}

Escreva o loop() primeiro, em pseudocódigo. Quando o roteiro de alto nível está claro, as funções individuais ficam triviais de implementar. Esse é o mesmo princípio top-down que você usou no Projeto Integrador da Aula 40.

Código completo

O código está organizado em blocos comentados, seguindo exatamente a arquitetura planejada acima:

/*
 * =====================================================
 * Painel de Controle Doméstico — Aula 47
 * Bloco 2: Projeto Integrador
 * =====================================================
 * Hardware: LED iluminação (p9), LED alarme (p8),
 *           LDR (A0), LCD I2C 16×2 (A4/A5)
 * Comandos: A=Auto | M=Manual | E=Emergência
 *           L=Ligar | D=Desligar | R=Reset
 * =====================================================
 */

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

// ── Pinos ──────────────────────────────────────────
const int PINO_LUZ   = 9;    // LED iluminação (PWM)
const int PINO_ALARM = 8;    // LED alarme
const int PINO_LDR   = A0;   // sensor de luminosidade

// ── LCD ────────────────────────────────────────────
LiquidCrystal_I2C lcd(0x27, 16, 2);

// ── Máquina de estados ─────────────────────────────
typedef enum { AUTO, MANUAL, EMERGENCIA } Modo;
Modo modoAtual = AUTO;   // inicia em AUTO

// ── Estado dos dispositivos ────────────────────────
bool luzLigada   = false;
bool alarmeLigado = false;

// ── Limiares ───────────────────────────────────────
const int LIMIAR_AUTO      = 400;  // abaixo: liga luz no AUTO
const int LIMIAR_CRITICO   = 100;  // abaixo: ativa EMERGENCIA

// ── Temporização ───────────────────────────────────
unsigned long ultimaTelaAtual = 0;
unsigned long ultimoPiscada   = 0;
bool          estadoPiscada   = false;


// ════════════════════════════════════════════════════
// CAMADA 1 — SENSORES
// ════════════════════════════════════════════════════

int lerLuminosidade() {
    return analogRead(PINO_LDR);   // 0–1023
}


// ════════════════════════════════════════════════════
// CAMADA 2 — ESTADO
// ════════════════════════════════════════════════════

void mudarModo(Modo novoModo) {
    if (modoAtual == novoModo) return;   // já está neste modo

    modoAtual    = novoModo;
    luzLigada    = false;
    alarmeLigado = false;

    Serial.print(">> Modo: ");
    switch (novoModo) {
        case AUTO:       Serial.println("AUTO");       break;
        case MANUAL:     Serial.println("MANUAL");     break;
        case EMERGENCIA: Serial.println("EMERGENCIA"); break;
    }
}

// Comportamento do modo AUTO
void processarAuto(int luminosidade) {
    // Luminosidade crítica → aciona EMERGENCIA automaticamente
    if (luminosidade < LIMIAR_CRITICO) {
        mudarModo(EMERGENCIA);
        return;
    }
    // Escuro → liga luz; claro → apaga
    luzLigada = (luminosidade < LIMIAR_AUTO);
}

// Comportamento do modo EMERGENCIA — pisca a iluminação
void processarEmergencia() {
    alarmeLigado = true;

    if (millis() - ultimoPiscada >= 300) {   // pisca a cada 300ms
        ultimoPiscada = millis();
        estadoPiscada = !estadoPiscada;
        luzLigada     = estadoPiscada;
    }
}


// ════════════════════════════════════════════════════
// CAMADA 3 — SAÍDA FÍSICA
// ════════════════════════════════════════════════════

void atualizarLEDs() {
    digitalWrite(PINO_LUZ,   luzLigada    ? HIGH : LOW);
    digitalWrite(PINO_ALARM, alarmeLigado ? HIGH : LOW);
}


// ════════════════════════════════════════════════════
// CAMADA 4 — INTERFACE
// ════════════════════════════════════════════════════

void atualizarLCD(int lum) {
    // Atualiza no máximo a cada 500ms para evitar pisca
    if (millis() - ultimaTelaAtual < 500) return;
    ultimaTelaAtual = millis();

    // Linha 0: modo atual
    lcd.setCursor(0, 0);
    switch (modoAtual) {
        case AUTO:       lcd.print("MODO: AUTO      "); break;
        case MANUAL:     lcd.print("MODO: MANUAL    "); break;
        case EMERGENCIA: lcd.print("!! EMERGENCIA !!"); break;
    }

    // Linha 1: status dos dispositivos e luminosidade
    lcd.setCursor(0, 1);
    lcd.print("Luz:");
    lcd.print(luzLigada ? "ON " : "OFF");
    lcd.print(" Lum:");
    lcd.print(map(lum, 0, 1023, 0, 100));
    lcd.print("%  ");
}

void processarSerial() {
    if (!Serial.available()) return;

    String msg = Serial.readStringUntil('\n');
    msg.trim();
    if (msg.length() == 0) return;

    char cmd = msg[0];

    // No modo EMERGENCIA só "R" funciona
    if (modoAtual == EMERGENCIA && cmd != 'R' && cmd != 'r') {
        Serial.println("EMERGENCIA ativa — apenas R para resetar");
        return;
    }

    switch (cmd) {
        case 'A': case 'a': mudarModo(AUTO);       break;
        case 'M': case 'm': mudarModo(MANUAL);     break;
        case 'E': case 'e': mudarModo(EMERGENCIA); break;

        case 'L': case 'l':
            if (modoAtual == MANUAL) {
                luzLigada = true;
                Serial.println("Luz LIGADA");
            } else {
                Serial.println("Comando L só funciona no modo MANUAL");
            }
            break;

        case 'D': case 'd':
            if (modoAtual == MANUAL) {
                luzLigada = false;
                Serial.println("Luz DESLIGADA");
            } else {
                Serial.println("Comando D só funciona no modo MANUAL");
            }
            break;

        case 'R': case 'r':
            mudarModo(AUTO);
            Serial.println("Sistema resetado → AUTO");
            break;

        case '?':
            Serial.println("Comandos: A M E L D R");
            break;

        case '\n': case '\r': break;

        default:
            Serial.print("Desconhecido: "); Serial.println(cmd);
    }
}


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

void setup() {
    pinMode(PINO_LUZ,   OUTPUT);
    pinMode(PINO_ALARM, OUTPUT);

    lcd.init();
    lcd.backlight();

    Serial.begin(9600);
    Serial.println("=== Painel de Controle ===");
    Serial.println("Comandos: A=Auto M=Manual E=Emerg L=Luz D=Deslig R=Reset ?=Ajuda");

    lcd.setCursor(0, 0);
    lcd.print("Iniciando...");
    delay(1000);
}

void loop() {
    processarSerial();

    int lum = lerLuminosidade();

    switch (modoAtual) {
        case AUTO:       processarAuto(lum);     break;
        case EMERGENCIA: processarEmergencia();  break;
        case MANUAL:     /* aguarda comandos */ break;
    }

    atualizarLEDs();
    atualizarLCD(lum);
}

=== Painel de Controle ===

Comandos: A=Auto M=Manual E=Emerg L=Luz D=Deslig R=Reset ?=Ajuda

[digitou M] >> Modo: MANUAL

[digitou L] Luz LIGADA

[digitou D] Luz DESLIGADA

[digitou E] >> Modo: EMERGENCIA

[digitou L] EMERGENCIA ativa — apenas R para resetar

[digitou R] Sistema resetado → AUTO

Dissecando o loop() — por que ele é tão curto

Com a arquitetura em camadas, o loop() ficou com apenas 7 linhas de conteúdo. Cada linha é uma pergunta:

Linha Pergunta respondida Camada
processarSerial() O usuário enviou algum comando? Interface
int lum = lerLuminosidade() Qual o estado do ambiente agora? Sensores
switch(modoAtual) O que o modo atual deve fazer com esses dados? Estado
atualizarLEDs() Os pinos físicos refletem o estado interno? Saída física
atualizarLCD(lum) O display mostra informação atualizada? Interface

Separação entre estado lógico e estado físico: note que processarAuto() e processarEmergencia() só modificam as variáveis luzLigada e alarmeLigado — elas não chamam digitalWrite() diretamente. Quem faz isso é atualizarLEDs(), sempre no final do loop. Essa separação evita que o estado físico fique inconsistente quando múltiplas funções tentam controlar o mesmo pino.

Pontos de extensão — como este projeto vira o Bloco 3

Este projeto tem quatro substituições diretas que o transformam nos projetos reais do Bloco 3:

Nesta aula Nos projetos do Bloco 3 Mudança no código
Serial USB (Monitor Serial) Bluetooth HC-05 (app no celular) Troca Serial por bluetooth em processarSerial()
LED de iluminação Módulo relay → lâmpada real Pino do relay no lugar do pino do LED — digitalWrite() idêntico
LED de alarme Buzzer ou segundo relay Mesmo pino, mesmo digitalWrite()
LDR como sensor PIR, DHT11, ultrassônico Troca a leitura em lerLuminosidade()

A arquitetura em camadas isola as mudanças. Quando você trocar o LED pelo relay, só muda o número do pino em uma linha. Quando trocar o Serial pelo Bluetooth, só muda o objeto em processarSerial(). O restante do código — máquina de estados, LCD, lógica de cada modo — não toca. Isso é exatamente o que torna o código escalável.

Diagrama de transições do sistema

O mapa completo de estados e transições — cada seta é uma condição que muda o modoAtual:

                    ┌─────────────────────────────────┐
                    │                                 │
             cmd "A"│                                 │cmd "A"
                    ▼                                 │
            ┌──────────────┐    cmd "M"    ┌──────────────┐
            │              │──────────────▶│              │
            │     AUTO     │               │    MANUAL    │
            │              │◀──────────────│              │
            └──────────────┘    cmd "A"    └──────────────┘
                    │                              │
       lum < 100   │ cmd "E"       cmd "E"        │
       (crítico)    │◀─────────────────────────────┘
                    │
                    ▼
            ┌──────────────┐
            │              │
            │  EMERGENCIA  │
            │  (pisca+     │
            │   alarme)    │
            └──────────────┘
                    │
              cmd "R"│
                    ▼
             volta para AUTO

Regra de ouro das transições: no modo EMERGENCIA, apenas "R" funciona — todos os outros comandos são bloqueados em processarSerial(). Isso representa um princípio de segurança: estados críticos devem ser explicitamente resetados, não silenciosamente sobrescritos por um comando mal enviado.

O que o Bloco 2 ensinou — visão consolidada

Antes de avançar para o Bloco 3, vale mapear como cada aula do bloco contribuiu para este projeto integrador:

Aula Conceito introduzido Onde aparece neste projeto
44 enum de estados, switch, millis() para transições typedef enum { AUTO, MANUAL, EMERGENCIA }, switch(modoAtual), pisca em EMERGENCIA
45 LCD I2C, atualização com setCursor(), frequência controlada atualizarLCD() com controle de 500ms e sobrescrita com espaços
46 Protocolo Serial, padrão available()/read(), feedback estruturado processarSerial() com readStringUntil(), bloqueio no modo EMERGENCIA
41b–43 LDR analógico, map(), arquitetura não-bloqueante lerLuminosidade(), conversão para %, nenhum delay() no código final

O Bloco 3 começa aqui. Você agora tem todos os padrões necessários:

O Projeto 01 do Bloco 3 vai pegar este projeto, trocar o LED pelo relay e o Monitor Serial pelo app do celular. Você vai reconhecer cada linha.

Quiz — Aula 47

Teste sua compreensão do projeto integrador e da arquitetura em camadas:

1. Por que processarAuto() e processarEmergencia() modificam luzLigada mas não chamam digitalWrite() diretamente?

2. Por que o modo EMERGENCIA bloqueia todos os comandos exceto "R"?

3. O que acontece na transição automática de AUTO para EMERGENCIA quando lum < LIMIAR_CRITICO?

4. Se você quisesse substituir o LED de iluminação por um módulo relay, o que mudaria no código?

5. Por que atualizarLCD() verifica millis() - ultimaTelaAtual < 500 logo no início?

6. O pisca-pisca do modo EMERGENCIA usa millis() com intervalo de 300ms. O que aconteceria se usasse delay(300) no lugar?

7. Qual aula do Bloco 2 ensinou o conceito diretamente aplicado em atualizarLCD(lum) com controle de frequência de 500ms?

8. No Projeto 01 do Bloco 3 (luz automática com relay + Bluetooth), qual linha de código desta aula muda e qual permanece idêntica?