App Android/iOS (feito em Flutter) que controla um robô com ESP32 por Bluetooth Low Energy (BLE). Você escaneia e conecta no robô direto (sem pareamento prévio) e usa uma cruz direcional na tela para mandar ele andar.
| Plataforma alvo | Android e iOS |
| Flutter | 3.47.0 (canal stable) |
| Comunicação | BLE (GATT), serviço no padrão Nordic UART Service (NUS) |
Até uma versão anterior deste app, a comunicação era por Bluetooth Classic (SPP) — o que só funcionava no Android. A migração para BLE foi feita justamente para permitir rodar no iOS; veja
robot_connection.darte o firmware emesp32_ble_bridge.inono repositórioorquestrador.
- Como o app funciona, em 30 segundos
- Rodando o projeto
- Atualização automática (Shorebird)
- Como fazer as alterações mais comuns
- Detalhes de configuração
┌─────────────────┐ você toca no robô ┌─────────────────┐
│ ConnectScreen │ ────────────────────► │ ControlScreen │
│ │ │ │
│ escaneia e │ ◄──────────────────── │ cruz direcional │
│ lista por perto │ botão desconectar │ + status │
└─────────────────┘ └─────────────────┘
│ │
└──────────────┬───────────────────────────┘
▼
┌───────────────────┐
│ RobotConnection │ ← o único que fala com o Bluetooth
└───────────────────┘
│
▼
🤖 ESP32
Cada toque num botão envia uma linha de texto pelo Bluetooth:
{"cmd":"F"}As letras são F (frente), B (ré), L (esquerda), R (direita) e
S (parar). O firmware do ESP32 precisa entender exatamente esse formato.
Se você mudar o formato aqui, tem que mudar lá também.
O robô anda enquanto o dedo está pressionando o botão. Ao soltar, o app
manda S automaticamente — inclusive se o dedo escorregar para fora do botão.
Para o mapa detalhado dos arquivos, veja ARQUITETURA.md.
Você precisa do Flutter 3.47+. Se ainda não tem, siga o guia oficial.
git clone <url-do-seu-repositorio>
cd app
flutter pub get # baixa as dependências listadas no pubspec.yaml
flutter doctor # confere o que falta no ambiente, por plataforma alvoVocê não precisa criar
android/local.propertiesna mão. Ele guarda os caminhos do SDK da sua máquina, por isso não vai para o Git — o próprioflutter buildgera ele no primeiro uso.
Se sua máquina Linux tem um adaptador Bluetooth (rfkill list bluetooth
mostra hci0 e o serviço bluetooth está ativo), o flutter_blue_plus
fala BLE de verdade por esse rádio via BlueZ — não é só uma prévia visual:
flutter run -d linuxIsso conecta de fato no ESP32 físico, sem precisar de celular nenhum. É o
jeito mais rápido de testar a lógica de conexão durante o desenvolvimento.
Único cuidado: permission_handler não tem implementação para desktop —
connect_screen.dart já pula o pedido de permissão fora de Android/iOS de
propósito, não "conserte" isso adicionando a chamada de volta.
Precisa do Android SDK (via Android Studio). Com um celular Android conectado (ou emulador rodando):
flutter run # hot reload direto no aparelho
flutter build apk --debug # ou gera um APK para instalar manualmenteO APK sai em build/app/outputs/flutter-apk/app-debug.apk. Copie para o
celular (cabo, Google Drive, etc.) e abra para instalar — o Android vai
pedir para autorizar "instalar apps de fontes desconhecidas" na primeira vez.
Para a versão final, menor e mais rápida: flutter build apk --release.
Este APK sai assinado com a chave do projeto (
android/robot-release.jks) e sem o Shorebird dentro — ou seja, ele não recebe atualização automática. Para gerar o APK que se atualiza sozinho, useshorebird releaseconforme a seção "Atualização automática" abaixo.
Não é possível compilar para iOS sem macOS. O Xcode — obrigatório para
compilar, assinar e instalar em um iPhone — só roda em macOS; não existe
workaround via Linux/Windows para essa etapa específica. Com acesso a um
Mac (próprio, emprestado, ou um serviço de CI como Codemagic/GitHub Actions
com runner macos):
open ios/Runner.xcworkspace # no Mac, dentro da pasta do projetoNo Xcode: selecione seu iPhone como destino, configure o signing com sua
Apple ID (aba "Signing & Capabilities" do target Runner) e rode. A
característica BLE já pede a permissão certa no iOS
(NSBluetoothAlwaysUsageDescription em ios/Runner/Info.plist).
flutter analyze # procura erros e código suspeito
flutter test # roda os testes automatizados (test/*.dart)Os dois precisam passar limpos antes de você subir alterações.
O app se atualiza sozinho. Quem tem o APK instalado não precisa reinstalar nada a cada mudança: o Shorebird entrega o código Dart novo pela internet, o app baixa em segundo plano e aplica no próximo abrir.
Você mexe no código Dart, commita, dá git push na main. Só isso. O
workflow .github/workflows/deploy.yml publica um patch e os celulares
pegam a atualização sozinhos.
Patch só troca código Dart. Ele não consegue trocar:
- código Kotlin ou o
AndroidManifest.xml - dependência nova no
pubspec.yamlque tenha parte nativa - versão do Flutter
- ícone e outros recursos Android
Nesses casos, bumpe a version: do pubspec.yaml (1.0.0+1 → 1.0.1+2,
lembrando que o número depois do + precisa subir) e dê push. O workflow
detecta a versão nova, compila um APK novo assinado e publica numa
GitHub Release. Esse APK precisa ser instalado à mão — é a
única hora em que isso acontece.
Se você mexer em código nativo e esquecer de bumpar a versão, o passo de patch falha de propósito, com a mensagem do Shorebird dizendo que detectou diferença nativa. É o comportamento desejado: melhor o CI falhar do que os celulares receberem um patch que trava o app.
export PATH="$HOME/.shorebird/bin:$PATH"
shorebird patch --platforms=android --release-version=1.0.1+2 # atualização OTA
shorebird release --platforms=android --artifact=apk # APK novoA esteira cobre só o Android. O Shorebird suporta iOS, mas compilar para
iOS exige um runner macos no GitHub Actions (mais caro que o ubuntu) e uma
conta paga no Apple Developer Program para assinar. Enquanto o iOS for
instalado à mão pelo Xcode, ele não recebe as atualizações automáticas — cada
mudança exige recompilar e reinstalar pelo Mac.
Ficam em Settings → Secrets and variables → Actions do repositório:
| Segredo | O que é |
|---|---|
SHOREBIRD_TOKEN |
API key criada em console.shorebird.dev |
ANDROID_KEYSTORE_BASE64 |
o android/robot-release.jks em base64 |
ANDROID_KEYSTORE_PASSWORD |
a senha do keystore |
ANDROID_KEY_ALIAS |
o alias da chave (robot) |
O keystore não está no Git (é segredo). Guarde uma cópia dele fora do projeto: perdê-lo significa não conseguir mais atualizar o app instalado nos celulares — só reinstalando do zero.
| Quero mudar... | Mexa em... |
|---|---|
| as cores do app | lib/app/theme.dart |
| o texto de um botão ou aviso | a tela correspondente em lib/screens/ |
| adicionar um comando novo (buzina, luz) | lib/models/robot_command.dart |
| o formato do que vai pelo Bluetooth | lib/services/robot_connection.dart, método send |
| os UUIDs do serviço BLE | RobotBleIds em robot_connection.dart e esp32_ble_bridge.ino (os dois lados) |
| o nome do app no celular | android/app/src/main/AndroidManifest.xml, atributo android:label |
| o ícone do app | android/app/src/main/res/mipmap-*/ |
Android: Bluetooth e Localização — a Localização parece estranha, mas o
Android exige ela para operações de Bluetooth em versões mais antigas; sem
ela a lista de dispositivos volta vazia e sem erro nenhum, difícil de
depurar. iOS: NSBluetoothAlwaysUsageDescription no Info.plist — sem
ela o app crasha ao tentar usar Bluetooth.
É com.setrem.robot_controller. Era com.example.robot_controller (o valor de
exemplo que o Flutter gera, que a Play Store recusa) e foi trocado antes de o
app começar a ser distribuído — de propósito, porque trocar depois é caro:
para o Android, um applicationId diferente é outro app, então a atualização
não instala por cima e todo mundo precisa desinstalar antes.
Se algum dia precisar mudar de novo, são dois lugares:
android/app/build.gradle.kts→applicationIdenamespace- a pasta
android/app/src/main/kotlin/...e opackagedoMainActivity.kt
Todo APK precisa ser assinado. Sem nenhuma configuração, o projeto assina o
release com a chave de debug — que é gerada por máquina. O APK instala e
funciona, mas dois APKs assinados por chaves diferentes não se atualizam: o
Android trata a troca de assinatura como app estranho e recusa a instalação
por cima. Por isso o projeto tem uma chave própria (android/robot-release.jks),
usada tanto aqui quanto pelo CI.
Para usar uma chave sua, o build.gradle.kts já está preparado: basta criar
os dois arquivos abaixo e o build passa a usá-los sozinho.
-
Gere o keystore (guarde bem a senha, ela não tem como ser recuperada):
keytool -genkey -v -keystore ~/robot-controller.jks \ -keyalg RSA -keysize 2048 -validity 10000 -alias robot -
Crie
android/key.propertiesa partir do modelo versionado:cp android/key.properties.example android/key.properties # abra e preencha as senhas e o caminho do .jks -
flutter build apk --release— pronto, sai assinado com a sua chave.
🔒 Nunca suba
key.propertiesnem o.jkspara o GitHub. Os dois já estão no.gitignore. Se a chave vazar, outra pessoa consegue publicar atualizações falsas em nome do seu app — e trocar a chave depois de publicado é um processo doloroso.
android/local.properties— caminhos do SDK da sua máquina, gerado automaticamente.build/e.dart_tool/— resultado da compilação, regeráveis.*.jksekey.properties— chaves de assinatura, são segredo.