Aula 46 — Controle por Serial: base do Bluetooth

Robótica com C — Bloco 2

Por que aprender Serial antes do Bluetooth?

Nos projetos do Bloco 3, você vai controlar LEDs, relays e portões pelo celular via módulo Bluetooth HC-05. Por dentro, o HC-05 é transparente — ele recebe dados do celular e os entrega ao Arduino exatamente como se fossem dados da porta Serial. Letras, números, comandos: chegam pelo mesmo Serial.read() que você já conhece da Aula 41b.

Isso significa que tudo que você aprender aqui sobre controle por Serial funciona identicamente com Bluetooth, sem modificar uma linha de código. A única diferença é o canal físico — cabo USB vs rádio de 2,4 GHz.

A abstração que o HC-05 oferece: o módulo Bluetooth faz o papel de um "cabo USB sem fio". Do ponto de vista do código Arduino, não existe diferença entre receber "L" pelo Monitor Serial do computador e receber "L" de um app no celular via Bluetooth. O protocolo UART por baixo é o mesmo.

Aspecto Serial USB (esta aula) Bluetooth HC-05 (Bloco 3)
Função de leitura Serial.read() Serial.read() — idêntico
Verificar dados Serial.available() Serial.available() — idêntico
Enviar resposta Serial.print() Serial.print() — idêntico
Origem dos dados Monitor Serial do computador App no celular (ex: Serial Bluetooth Terminal)
Alcance Comprimento do cabo USB Até ~10 metros
Hardware extra Nenhum Módulo HC-05 (~R$20)

Revisão: Serial bidirecional

Você viu Serial.read() brevemente na Aula 41b. Aqui vai a estrutura completa que será usada em todos os projetos com controle remoto:

// Esqueleto universal de controle por Serial/Bluetooth

void setup() {
    Serial.begin(9600);
}

void loop() {
    // 1. Verificar se chegou algum dado
    if (Serial.available() > 0) {

        // 2. Ler um byte (caractere)
        char cmd = Serial.read();

        // 3. Processar o comando
        processarComando(cmd);
    }

    // 4. Resto do loop() continua livre
    // (ler sensores, atualizar display, etc.)
}

Por que verificar available() antes de read()? Se o buffer estiver vazio e você chamar Serial.read() direto, ele retorna -1 — e seu código vai processar um "comando" inválido a cada iteração do loop, causando comportamento errático. Sempre verifique antes de ler. É a mesma disciplina de verificar fscanf() em C.

Padrão 1 — Comandos de caractere único

O protocolo mais simples e direto: cada letra representa uma ação. Digitar "L" liga, "D" desliga, "R" reseta. Usado nos projetos de LED RGB, lâmpada e campainha do Bloco 3:

/*
 * Controle de 3 LEDs por comandos de caractere
 * R = vermelho, G = verde, B = azul
 * Maiúscula = liga, minúscula = desliga
 * X = apaga todos
 */

const int PINO_R = 9;
const int PINO_G = 10;
const int PINO_B = 11;

void setup() {
    pinMode(PINO_R, OUTPUT);
    pinMode(PINO_G, OUTPUT);
    pinMode(PINO_B, OUTPUT);
    Serial.begin(9600);
    Serial.println("Comandos: R/r G/g B/b X");
}

void processarComando(char cmd) {
    switch (cmd) {
        case 'R': digitalWrite(PINO_R, HIGH); Serial.println("Vermelho LIGADO");  break;
        case 'r': digitalWrite(PINO_R, LOW);  Serial.println("Vermelho DESLIGADO"); break;
        case 'G': digitalWrite(PINO_G, HIGH); Serial.println("Verde LIGADO");     break;
        case 'g': digitalWrite(PINO_G, LOW);  Serial.println("Verde DESLIGADO");  break;
        case 'B': digitalWrite(PINO_B, HIGH); Serial.println("Azul LIGADO");      break;
        case 'b': digitalWrite(PINO_B, LOW);  Serial.println("Azul DESLIGADO");   break;
        case 'X':
            digitalWrite(PINO_R, LOW);
            digitalWrite(PINO_G, LOW);
            digitalWrite(PINO_B, LOW);
            Serial.println("Todos DESLIGADOS");
            break;
        case '\n': case '\r': break;  // ignora quebras de linha
        default:
            Serial.print("Comando desconhecido: ");
            Serial.println(cmd);
    }
}

void loop() {
    if (Serial.available() > 0) {
        processarComando(Serial.read());
    }
}

Comandos: R/r G/g B/b X

[digitou R] Vermelho LIGADO

[digitou G] Verde LIGADO

[digitou r] Vermelho DESLIGADO

[digitou X] Todos DESLIGADOS

[digitou Z] Comando desconhecido: Z

Sempre trate o default: no ambiente real com Bluetooth, o app pode enviar caracteres inesperados — quebras de linha, bytes de controle, erros de digitação. O default com feedback evita que o Arduino fique em silêncio quando recebe algo inesperado, o que dificulta muito a depuração.

Padrão 2 — Comandos com valor numérico

Às vezes um caractere não é suficiente — você quer enviar um ângulo (0–180), um brilho (0–255) ou uma velocidade (0–100). O padrão é enviar o número como texto e converter com Serial.parseInt():

/*
 * Controle de brilho de LED por valor numérico
 * Digite um número de 0 a 255 e pressione Enter
 * Ex: "128" → LED em 50% de brilho
 */

const int PINO_LED = 9;   // pino PWM (~)

void setup() {
    pinMode(PINO_LED, OUTPUT);
    Serial.begin(9600);
    Serial.setTimeout(100);   // timeout para parseInt() — ms
    Serial.println("Digite 0-255 e pressione Enter:");
}

void loop() {
    if (Serial.available() > 0) {
        int brilho = Serial.parseInt();   // lê número inteiro do buffer

        // constrain() limita o valor ao intervalo válido
        brilho = constrain(brilho, 0, 255);

        analogWrite(PINO_LED, brilho);

        Serial.print("Brilho definido: ");
        Serial.print(brilho);
        Serial.print(" (");
        Serial.print(map(brilho, 0, 255, 0, 100));
        Serial.println("%)");
    }
}

Digite 0-255 e pressione Enter:

[digitou 128] Brilho definido: 128 (50%)

[digitou 255] Brilho definido: 255 (100%)

[digitou 0] Brilho definido: 0 (0%)

constrain(valor, min, max): função nativa do Arduino que limita um valor a um intervalo — equivalente a if (v < min) v = min; if (v > max) v = max; em uma linha. Indispensável para proteger componentes de valores fora do range esperado.

Padrão 3 — Comandos com prefixo (protocolo expandível)

Quando o projeto tem múltiplos dispositivos controláveis — como no Painel de Comandos do Bloco 3 — um único caractere não é suficiente para identificar o alvo e o valor. O padrão com prefixo resolve isso: cada mensagem começa com uma letra que identifica o dispositivo, seguida do valor.

// Protocolo: LETRA + NÚMERO + '\n'
// Exemplos:
//   "R128\n"  → LED vermelho com brilho 128
//   "S90\n"   → Servo no ângulo 90°
//   "F1\n"    → Ventilador ligado (1) ou desligado (0)

// Formato recebido como string: ex "S90"

#include <Servo.h>

const int PINO_LED = 9;
Servo servo;

void setup() {
    pinMode(PINO_LED, OUTPUT);
    servo.attach(6);
    Serial.begin(9600);
    Serial.println("Protocolo: R0-255 | S0-180 | F0/1");
}

void processarMensagem(String msg) {
    if (msg.length() < 2) return;   // mensagem inválida

    char   dispositivo = msg[0];           // primeira letra
    int    valor       = msg.substring(1).toInt();  // resto como número

    switch (dispositivo) {
        case 'R':
            valor = constrain(valor, 0, 255);
            analogWrite(PINO_LED, valor);
            Serial.print("LED vermelho: "); Serial.println(valor);
            break;

        case 'S':
            valor = constrain(valor, 0, 180);
            servo.write(valor);
            Serial.print("Servo: "); Serial.print(valor); Serial.println("°");
            break;

        case 'F':
            digitalWrite(PINO_LED, valor ? HIGH : LOW);
            Serial.println(valor ? "Ventilador LIGADO" : "Ventilador DESLIGADO");
            break;

        default:
            Serial.print("Dispositivo desconhecido: "); Serial.println(dispositivo);
    }
}

void loop() {
    if (Serial.available() > 0) {
        String msg = Serial.readStringUntil('\n');  // lê até Enter
        msg.trim();                                 // remove espaços e \r
        processarMensagem(msg);
    }
}

Protocolo: R0-255 | S0-180 | F0/1

[enviou "R200"] LED vermelho: 200

[enviou "S45"] Servo: 45°

[enviou "F1"] Ventilador LIGADO

[enviou "F0"] Ventilador DESLIGADO

String com S maiúsculo: neste padrão usamos a classe String do Arduino para facilitar o parsing com substring() e toInt(). Lembre do aviso da Aula 41a — em projetos longos, prefira char[] com atoi() para evitar fragmentação de heap. Para os projetos do Bloco 3, onde os comandos são processados pontualmente, String é aceitável.

Comunicação bidirecional — enviando dados ao celular

O Bluetooth não é só entrada — você pode enviar dados de sensores de volta ao celular. O app exibe temperatura, distância, status — qualquer dado que o Arduino gerar. Basta usar Serial.print() normalmente:

/*
 * Monitor bidirecional — recebe comandos E envia dados
 * "T\n" → Arduino responde com a temperatura atual
 * "L\n" → Arduino liga LED e confirma
 * "D\n" → Arduino desliga LED e confirma
 */

const int PINO_LED  = 8;
const int PINO_LM35 = A0;

void setup() {
    pinMode(PINO_LED, OUTPUT);
    Serial.begin(9600);
    Serial.println("Pronto. Comandos: T | L | D");
}

float lerTemperatura() {
    return analogRead(PINO_LM35) * (5.0 / 1023.0) * 100.0;
}

void loop() {
    // Envia temperatura automaticamente a cada 5 segundos
    static unsigned long ultimoEnvio = 0;
    if (millis() - ultimoEnvio >= 5000) {
        ultimoEnvio = millis();
        Serial.print("TEMP:");
        Serial.println(lerTemperatura(), 1);
    }

    // Responde a comandos recebidos
    if (Serial.available() > 0) {
        String cmd = Serial.readStringUntil('\n');
        cmd.trim();

        if (cmd == "T") {
            Serial.print("TEMP:");
            Serial.println(lerTemperatura(), 1);
        } else if (cmd == "L") {
            digitalWrite(PINO_LED, HIGH);
            Serial.println("LED:1");
        } else if (cmd == "D") {
            digitalWrite(PINO_LED, LOW);
            Serial.println("LED:0");
        } else {
            Serial.println("ERR:cmd_desconhecido");
        }
    }
}

Pronto. Comandos: T | L | D

TEMP:23.4

[enviou "L"] LED:1

[enviou "T"] TEMP:24.1

TEMP:24.1

[enviou "D"] LED:0

Protocolo chave:valor — prefixar as respostas com um identificador ("TEMP:", "LED:", "ERR:") é uma boa prática pois permite que o app parse as mensagens facilmente. Aplicativos como o Serial Bluetooth Terminal (Android) permitem configurar macros para enviar comandos com um toque e exibir respostas em tempo real.

Adaptando para o módulo Bluetooth HC-05

Quando o Bloco 3 chegar, a única mudança no código será a porta Serial usada. O Arduino Uno tem apenas uma UART de hardware (Serial), que é compartilhada com o USB. Para usar o HC-05 sem perder o debug pelo cabo, use SoftwareSerial — uma UART por software em pinos digitais quaisquer:

// Estrutura que você vai usar no Bloco 3:
// HC-05 TX → pino 10 (RX do Arduino)
// HC-05 RX → pino 11 (TX do Arduino) via divisor de tensão!

#include <SoftwareSerial.h>

SoftwareSerial bluetooth(10, 11);  // RX, TX

void setup() {
    Serial.begin(9600);       // debug no computador
    bluetooth.begin(9600);    // comunicação com o HC-05
}

void loop() {
    // Lê do Bluetooth e replica no Serial (debug)
    if (bluetooth.available()) {
        char c = bluetooth.read();
        Serial.print("BT recebeu: "); Serial.println(c);
        processarComando(c);       // mesma função dos exemplos anteriores!
    }

    // Lê do Serial e replica no Bluetooth (teste)
    if (Serial.available()) {
        bluetooth.write(Serial.read());
    }
}

Divisor de tensão no RX do HC-05: o Arduino envia 5V no TX, mas o HC-05 aceita apenas 3,3V no seu pino RX. Conectar direto pode danificar o módulo. Use um divisor de tensão simples: R1=1kΩ entre o TX do Arduino e o RX do HC-05, R2=2kΩ entre o RX do HC-05 e o GND. Isso reduz os 5V para ~3,3V. Essa ligação está detalhada na aula do Projeto 02 do Bloco 3.

O que muda do código desta aula para o Bloco 3: basicamente nada. Troque Serial.read() por bluetooth.read() e Serial.print() por bluetooth.print() — ou mantenha Serial se usar um Arduino Mega que tem múltiplas UARTs hardware. A lógica de processamento de comandos (switch, processarComando()) é copiada literalmente.

C vs Arduino — Controle por Serial

Conceito C puro (arquivo/stdin) Arduino (Serial/Bluetooth)
Ler caractere getchar() / fgetc(stdin) Serial.read()
Verificar disponibilidade feof() / select() Serial.available() > 0
Ler string até delimitador fgets(buf, n, stdin) Serial.readStringUntil('\n')
Converter string para int atoi(buf) Serial.parseInt() ou String.toInt()
Processar comandos switch(cmd) switch(cmd) — idêntico
Limitar valor a intervalo if/else manual constrain(v, min, max)
Bluetooth vs USB Não aplicável Troca Serial por bluetooth — mesma API

Quiz — Aula 46

Teste o que você aprendeu sobre controle por Serial e a base do Bluetooth:

1. Por que o código de controle por Serial desta aula funciona sem modificação com o módulo Bluetooth HC-05?

2. O que acontece se você chamar Serial.read() sem verificar Serial.available() primeiro?

3. Por que o switch no padrão de comandos por caractere inclui case '\n': case '\r': break;?

4. O que faz constrain(valor, 0, 255)?

5. No padrão 3 (protocolo com prefixo), a mensagem "S90" é processada assim: dispositivo = msg[0] e valor = msg.substring(1).toInt(). Qual o equivalente em C puro?

6. Por que é necessário um divisor de tensão entre o TX do Arduino e o RX do HC-05?

7. No exemplo de comunicação bidirecional, as respostas usam o formato "TEMP:23.4", "LED:1", "ERR:cmd". Por que usar esse formato em vez de texto livre?

8. O que muda no código ao migrar do Serial USB desta aula para o Bluetooth HC-05 via SoftwareSerial?