Projeto 05 — Campainha inteligente com notificação no celular

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

O que vamos construir

Uma campainha que, quando alguém pressiona o botão na porta, toca um buzzer e envia uma notificação instantânea para o celular via Bluetooth — mesmo que você esteja em outro cômodo sem ouvir o som. O celular pode ainda responder comandos de volta: silenciar o buzzer, verificar quantas vezes tocaram, ou resetar o contador.

🔔
Botão pressionado
Buzzer toca + celular recebe notificação
📱
cmd "S"
Silencia o buzzer remotamente
🔢
cmd "C"
Consulta quantas vezes tocou
🔕
cmd "M"
Modo mudo — desativa o buzzer

O padrão novo deste projeto: evento físico → notificação remota. Nos projetos anteriores o celular enviava comandos para o Arduino. Aqui a direção principal se inverte — o Arduino detecta um evento (botão pressionado) e empurra uma notificação para o celular sem que o usuário pedisse. É o padrão base de sistemas de alarme, monitoramento de presença e IoT em geral.

🔊 Componente novo: buzzer

O buzzer (piezoelétrico) converte sinais elétricos em som. Existem dois tipos:

Tipo Funcionamento Como controlar Uso neste projeto
Buzzer ativo Tem oscilador interno — emite som fixo quando recebe 5V digitalWrite(pino, HIGH) ✅ Mais simples — liga e desliga
Buzzer passivo Sem oscilador — precisa de sinal PWM com frequência tone(pino, freq) / noTone(pino) ✅ Permite melodias e diferentes tons

Este projeto usa o buzzer ativo. Para adicionar uma melodia de campainha, basta trocar por buzzer passivo e usar tone() — a lógica do projeto não muda.

Como identificar: buzzers ativos geralmente têm uma etiqueta adesiva cobrindo a abertura e são polarizados (perna longa = positivo). Buzzers passivos costumam ter a abertura visível. Na dúvida, aplique 5V direto — o ativo toca imediatamente, o passivo fica silencioso.

Corrente do buzzer: buzzers ativos consomem tipicamente 30–40 mA — próximo do limite de 40 mA por pino do Arduino. Para uso prolongado em hardware físico, intercale um transistor NPN (BC547) entre o pino e o buzzer. Para testes e simulações no Wokwi, a ligação direta funciona.

🖥️ Simulação no Wokwi

Tudo simulável exceto o Bluetooth. O Wokwi tem buzzer e botão como componentes nativos — você vai ouvir o som do buzzer durante a simulação (ou ver o indicador visual). Use o Monitor Serial no lugar do celular para enviar comandos e ver as notificações.

Roteiro de simulação:

  1. Adicione um Push Button ao diagrama — conecte uma perna ao pino 2 e a outra ao GND
  2. Adicione um Buzzer — positivo ao pino 9, negativo ao GND
  3. Adicione um LED + resistor 220Ω ao pino 8 (indicação visual)
  4. Use a versão Wokwi do código (Seção 7)
  5. Clique em ▶ Play e pressione o botão — veja RING:#1 aparecer no Monitor Serial e o buzzer soar
  6. Envie "S" para silenciar; "M" para ativar o modo mudo
  7. Com modo mudo ativo, pressione o botão novamente — a notificação chega, mas o buzzer fica silencioso

Hardware completo

Componente Conexão Observação
Botão da campainha Pino 2 → botão → GND INPUT_PULLUP — LOW quando pressionado
Buzzer ativo (+) Pino 9 HIGH = toca, LOW = silencia
LED notificação + 220Ω Pino 8 → R → LED → GND Pisca ao detectar toque
HC-05 TX → pino 10 Direto Recebe comandos do celular
HC-05 RX ← pino 11 Via divisor R1=1kΩ, R2=2kΩ Envia notificações ao celular

Protocolo de eventos e comandos

Notificações enviadas pelo Arduino (sem pedido)

Evento Mensagem enviada ao celular
Botão pressionado RING:#n — n = número do toque
Timer do buzzer expirou BUZZER:OFF
Sistema iniciado ONLINE:campainha

Comandos aceitos

Comando Ação Resposta
S Silencia o buzzer imediatamente BUZZER:OFF
M Toggle modo mudo MUDO:ON ou MUDO:OFF
C Consulta o contador de toques TOQUES:n
Z Zera o contador TOQUES:0
T5 Define duração do buzzer (1–60s) DURACAO:5
? Ajuda Lista de comandos

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

/*
 * =====================================================
 * Projeto 05 — Campainha inteligente
 * Botão + Buzzer + Bluetooth HC-05
 * =====================================================
 * Evento: botão → buzzer toca + notificação ao celular
 * Comandos: S M C Z T1-60 ?
 * =====================================================
 */

#include <SoftwareSerial.h>

// ── Bluetooth ──────────────────────────────────────
SoftwareSerial bluetooth(10, 11);

// ── Pinos ──────────────────────────────────────────
const int PINO_BOTAO = 2;
const int PINO_LED   = 8;
const int PINO_BUZ   = 9;

// ── Estado do buzzer ───────────────────────────────
bool          buzzerAtivo    = false;
bool          modoMudo       = false;
unsigned long buzzerFim      = 0;
unsigned long duracaoBuzzer  = 5000;   // 5s padrão

// ── Contador e debounce ────────────────────────────
unsigned int  contadorToques = 0;
int           estadoAnterior = HIGH;
unsigned long ultimoBounce   = 0;
const unsigned long DEBOUNCE = 50;

// ── LED piscante não bloqueante ────────────────────
unsigned long ultimoPiscada  = 0;
int           piscarRestante = 0;


// ════════════════════════════════════════════════════
// Buzzer
// ════════════════════════════════════════════════════
void tocarBuzzer() {
    if (modoMudo) return;
    buzzerAtivo = true;
    buzzerFim   = millis() + duracaoBuzzer;
    digitalWrite(PINO_BUZ, HIGH);
}

void silenciarBuzzer() {
    buzzerAtivo = false;
    digitalWrite(PINO_BUZ, LOW);
    bluetooth.println("BUZZER:OFF");
    Serial.println("BUZZER:OFF");
}

void verificarBuzzer() {
    if (buzzerAtivo && millis() >= buzzerFim) silenciarBuzzer();
}

// ════════════════════════════════════════════════════
// LED pisca n vezes sem bloquear o loop()
// ════════════════════════════════════════════════════
void iniciarPiscar(int n) {
    piscarRestante = n * 2;    // cada piscada = ligar + apagar
    ultimoPiscada  = millis();
}

void processarPiscar() {
    if (piscarRestante <= 0) { digitalWrite(PINO_LED, LOW); return; }
    if (millis() - ultimoPiscada >= 150) {
        ultimoPiscada = millis();
        digitalWrite(PINO_LED, piscarRestante % 2 == 0 ? HIGH : LOW);
        piscarRestante--;
    }
}

// ════════════════════════════════════════════════════
// Botão com debounce — detecta borda de descida
// ════════════════════════════════════════════════════
void verificarBotao() {
    int leitura = digitalRead(PINO_BOTAO);

    if (leitura != estadoAnterior) ultimoBounce = millis();

    if ((millis() - ultimoBounce) > DEBOUNCE) {
        // Borda de descida: botão acabou de ser pressionado
        if (leitura == LOW && estadoAnterior == HIGH) {
            contadorToques++;
            bluetooth.print("RING:#"); bluetooth.println(contadorToques);
            Serial.print("RING:#");    Serial.println(contadorToques);
            tocarBuzzer();
            iniciarPiscar(3);
        }
        estadoAnterior = leitura;
    }

    estadoAnterior = leitura;
}

// ════════════════════════════════════════════════════
// Comandos do celular
// ════════════════════════════════════════════════════
void processarMensagem(String msg) {
    if (msg.length() == 0) return;
    char cmd = msg[0];

    switch (cmd) {
        case 'S': case 's':
            silenciarBuzzer();
            break;

        case 'M': case 'm':
            modoMudo = !modoMudo;
            if (modoMudo) silenciarBuzzer();
            bluetooth.print("MUDO:"); bluetooth.println(modoMudo ? "ON" : "OFF");
            Serial.print("MUDO:");    Serial.println(modoMudo ? "ON" : "OFF");
            break;

        case 'C': case 'c':
            bluetooth.print("TOQUES:"); bluetooth.println(contadorToques);
            Serial.print("TOQUES:");   Serial.println(contadorToques);
            break;

        case 'Z': case 'z':
            contadorToques = 0;
            bluetooth.println("TOQUES:0");
            Serial.println("TOQUES:0");
            break;

        case 'T': case 't':
            if (msg.length() > 1) {
                int seg = constrain(msg.substring(1).toInt(), 1, 60);
                duracaoBuzzer = (unsigned long)seg * 1000UL;
                bluetooth.print("DURACAO:"); bluetooth.println(seg);
                Serial.print("DURACAO:"); Serial.println(seg);
            }
            break;

        case '?':
            bluetooth.println("Cmds: S M C Z T1-60 ?");
            break;

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

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


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

void setup() {
    pinMode(PINO_BOTAO, INPUT_PULLUP);
    pinMode(PINO_LED,   OUTPUT);
    pinMode(PINO_BUZ,   OUTPUT);
    digitalWrite(PINO_BUZ, LOW);

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

    Serial.println("=== Campainha Inteligente ===");
    bluetooth.println("ONLINE:campainha");   // handshake ao conectar
}

void loop() {
    verificarBotao();
    verificarBuzzer();
    processarPiscar();

    if (bluetooth.available() > 0) {
        String msg = bluetooth.readStringUntil('\n');
        msg.trim();
        processarMensagem(msg);
    }

    if (Serial.available() > 0) bluetooth.write(Serial.read());
}

=== Campainha Inteligente ===

[botão pressionado] RING:#1

[5s depois] BUZZER:OFF

[botão pressionado] RING:#2

[celular envia "S"] BUZZER:OFF

[celular envia "M"] MUDO:ON

[botão pressionado] RING:#3 (buzzer silencioso)

[celular envia "C"] TOQUES:3

[celular envia "Z"] TOQUES:0

Código adaptado para simulação no Wokwi

// ── Versão WOKWI ───────────────────────────────────
// Remove SoftwareSerial — usa Serial direto
// Pressione o botão no diagrama; envie comandos pelo Monitor Serial

const int PINO_BOTAO = 2;
const int PINO_LED   = 8;
const int PINO_BUZ   = 9;

bool          buzzerAtivo    = false;
bool          modoMudo       = false;
unsigned long buzzerFim      = 0;
unsigned long duracaoBuzzer  = 5000;
unsigned int  contadorToques = 0;
int           estadoAnterior = HIGH;
unsigned long ultimoBounce   = 0;
unsigned long ultimoPiscada  = 0;
int           piscarRestante = 0;
const unsigned long DEBOUNCE = 50;

void tocarBuzzer() {
    if (modoMudo) return;
    buzzerAtivo = true;
    buzzerFim   = millis() + duracaoBuzzer;
    digitalWrite(PINO_BUZ, HIGH);
}

void silenciarBuzzer() {
    buzzerAtivo = false;
    digitalWrite(PINO_BUZ, LOW);
    Serial.println("BUZZER:OFF");
}

void verificarBuzzer() {
    if (buzzerAtivo && millis() >= buzzerFim) silenciarBuzzer();
}

void iniciarPiscar(int n) {
    piscarRestante = n * 2;
    ultimoPiscada  = millis();
}

void processarPiscar() {
    if (piscarRestante <= 0) { digitalWrite(PINO_LED, LOW); return; }
    if (millis() - ultimoPiscada >= 150) {
        ultimoPiscada = millis();
        digitalWrite(PINO_LED, piscarRestante % 2 == 0 ? HIGH : LOW);
        piscarRestante--;
    }
}

void verificarBotao() {
    int leitura = digitalRead(PINO_BOTAO);
    if (leitura != estadoAnterior) ultimoBounce = millis();
    if ((millis() - ultimoBounce) > DEBOUNCE) {
        if (leitura == LOW && estadoAnterior == HIGH) {
            contadorToques++;
            Serial.print("RING:#"); Serial.println(contadorToques);
            tocarBuzzer();
            iniciarPiscar(3);
        }
        estadoAnterior = leitura;
    }
    estadoAnterior = leitura;
}

void processarMensagem(String msg) {
    if (msg.length() == 0) return;
    char cmd = msg[0];
    switch (cmd) {
        case 'S': case 's': silenciarBuzzer(); break;
        case 'M': case 'm':
            modoMudo = !modoMudo;
            if (modoMudo) silenciarBuzzer();
            Serial.print("MUDO:"); Serial.println(modoMudo ? "ON" : "OFF"); break;
        case 'C': case 'c': Serial.print("TOQUES:"); Serial.println(contadorToques); break;
        case 'Z': case 'z': contadorToques = 0; Serial.println("TOQUES:0"); break;
        case 'T': case 't':
            if (msg.length() > 1) {
                int seg = constrain(msg.substring(1).toInt(), 1, 60);
                duracaoBuzzer = (unsigned long)seg * 1000UL;
                Serial.print("DURACAO:"); Serial.println(seg);
            }
            break;
        case '?': Serial.println("Cmds: S M C Z T1-60 ?"); break;
        case '\n': case '\r': return;
        default: Serial.print("ERR:"); Serial.println(cmd);
    }
}

void setup() {
    pinMode(PINO_BOTAO, INPUT_PULLUP);
    pinMode(PINO_LED,   OUTPUT);
    pinMode(PINO_BUZ,   OUTPUT);
    digitalWrite(PINO_BUZ, LOW);
    Serial.begin(9600);
    Serial.println("=== Campainha Wokwi — pressione o botão ===");
}

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

Testando o modo mudo no Wokwi: pressione o botão e confirme que RING:#1 aparece e o buzzer soa. Envie "M" e pressione o botão novamente — RING:#2 deve aparecer, mas o buzzer deve ficar silencioso. Esse teste valida a separação entre notificação (sempre ativa) e som (controlável).

Decisões de projeto

Por que o LED pisca de forma não bloqueante?

A função processarPiscar() usa um contador piscarRestante e millis() para gerenciar as piscadas sem congelar o loop(). Se usasse delay(), o botão seria ignorado durante as piscadas — um segundo toque simultâneo se perderia. Com o padrão não bloqueante, o Arduino pisca o LED, checa o botão e lê o Bluetooth ao mesmo tempo.

Por que há duas linhas estadoAnterior = leitura em verificarBotao()?

A segunda atribuição (fora do if de debounce) garante que o estado anterior é sempre atualizado, mesmo quando o debounce ainda está bloqueando. Sem ela, se o botão for pressionado e solto rapidamente dentro da janela de debounce, estadoAnterior ficaria desatualizado e a próxima detecção falharia.

Por que bluetooth.println("ONLINE:campainha") no setup?

Quando o celular se reconectar ao Arduino (após desligamento ou perda de sinal), receberá imediatamente a mensagem "ONLINE:campainha". O app usa isso para confirmar que está conectado ao dispositivo correto e atualizar sua interface. É uma boa prática em sistemas que ficam ligados continuamente.

Padrão evento → notificação: verificarBotao() detecta o evento, tocarBuzzer() age localmente, e bluetooth.print("RING:#n") notifica remotamente — tudo no mesmo bloco. Esse padrão se repete em todos os sistemas de alerta: PIR → alerta, fumaça → alerta, porta aberta → alerta. A estrutura é sempre a mesma.

Configurando macros no app

Botão Rótulo Macro
M1 🔕 Silenciar S
M2 🔇 Modo mudo M
M3 🔢 Ver toques C
M4 🔄 Zerar Z
M5 ⏱️ 3s T3
M6 ⏱️ 10s T10

Notificação sonora no celular: configure o Serial Bluetooth Terminal para emitir um som ou vibrar quando receber mensagem contendo "RING:". Nas configurações do app, procure por Receive Sound ou Notification — assim você é alertado mesmo com o celular na bolsa.

Conceitos do curso aplicados neste projeto

Conceito Aula de origem Onde aparece neste projeto
Debounce com millis() Aula 42 verificarBotao() — janela de 50ms
Detecção de borda de descida Aula 42 leitura == LOW && estadoAnterior == HIGH
Temporizador com millis() Projeto 01 buzzerFim + verificarBuzzer()
Padrão não bloqueante Aula 41b processarPiscar() — LED pisca sem travar o loop
Evento → notificação remota Conceito novo (padrão IoT) bluetooth.print("RING:#n") ao detectar toque
INPUT_PULLUP Aula 42 Botão sem resistor externo — LOW = pressionado
Bluetooth HC-05 + protocolo Projetos 02–04 Canal bidirecional — notificações push + comandos pull

Quiz — Projeto 05

Teste sua compreensão da campainha inteligente:

1. Qual é o padrão novo introduzido neste projeto em relação aos projetos anteriores?

2. Por que processarPiscar() usa millis() e um contador em vez de um loop com delay()?

3. A condição de detecção do toque é leitura == LOW && estadoAnterior == HIGH. Por que não simplesmente leitura == LOW?

4. O que acontece quando o celular envia "M" enquanto o buzzer está tocando?

5. Por que bluetooth.println("ONLINE:campainha") é enviado no setup()?

6. Qual é a diferença entre um buzzer ativo e um buzzer passivo?

7. O padrão "evento físico → notificação remota" usado na campainha serve de base para quais outros sistemas?

8. Se o loop() roda ~10.000 vezes por segundo e o botão fica pressionado por 1 segundo, sem o código de borda de descida quantas vezes "RING:#n" seria enviado?