App Inventor Arduino Bluetooth: crea tu app Android con HC-05
Controla Arduino por Bluetooth con App Inventor: conexión del HC-05, ListPicker, bloques de botones y deslizadores, LED, relé, servo, sensores, APK, Android 12.
Controlar Arduino por Bluetooth con App Inventor es la forma más rápida de crear tu propia app Android sin escribir ni una línea de Java o Kotlin. En MIT App Inventor, la herramienta gratuita del MIT que funciona en el navegador, arrastras botones, deslizadores y etiquetas de texto a la pantalla y defines qué hacen con bloques que encajan como piezas de un puzle. El móvil se conecta a un módulo Bluetooth HC-05 unido al Arduino; cada toque en la app viaja hasta el Arduino como un comando de texto corto, y el Arduino devuelve sus lecturas de sensores al móvil por el mismo camino.
Al terminar esta guía tendrás una app de control remoto que funciona de verdad: un ListPicker que se conecta al HC-05 desde una lista, botones que encienden y apagan un LED y un relé, dos deslizadores (Slider) para el brillo del LED y el ángulo del servo, y etiquetas que muestran en directo la temperatura y la humedad de un DHT11. Verás paso a paso el código de Arduino, cada bloque de App Inventor, cómo generar el APK y la solución al problema de permisos Bluetooth que llegó con Android 12. Si nunca has usado el HC-05, echa antes un vistazo a la guía del módulo Bluetooth HC-05.

¿Qué es App Inventor y cómo se comunica con Arduino?
MIT App Inventor es un entorno gratuito de desarrollo de apps creado por el Instituto Tecnológico de Massachusetts (MIT); inicias sesión con tu cuenta de Google y trabajas en el navegador. Tiene dos vistas: en el Designer (diseñador) preparas el aspecto de la app arrastrando componentes, y en el editor de Blocks (bloques) escribes reglas como «cuando se pulse el botón, haz esto» con bloques de colores. Para probar la app sin instalarla se usa la app MIT AI2 Companion de Play Store; al terminar, un clic genera un archivo APK instalable.
En la parte Bluetooth, el trabajo lo hace el componente BluetoothClient. Usa el perfil SPP (Serial Port Profile, perfil de puerto serie) del Bluetooth clásico, que es justo el que habla el HC-05. El resultado es un cable serie invisible entre el móvil y el Arduino: cuando la app envía «L1» con el bloque SendText, el Arduino lee esos dos caracteres con bt.read(), como si las hubieras escrito en el Monitor Serie. Todo lo que el Arduino envía con bt.print() llega al bloque ReceiveText del móvil. En el Arduino no hace falta ninguna librería específica de Bluetooth.
| Capa | Función | En este proyecto |
|---|---|---|
| Interfaz (Designer) | Componentes visibles en pantalla | ListPicker, 5 Button, 2 Slider, Label |
| Lógica (Blocks) | Bloques que reaccionan a eventos | Eventos Click, PositionChanged y Timer |
| BluetoothClient | Conexión SPP, envío y recepción de datos | Connect, SendText, ReceiveText |
| HC-05 | Puente entre Bluetooth y UART | 9600 baudios, PIN 1234 |
| Arduino | Interpreta comandos, maneja salidas, lee el sensor | LED, relé, servo, DHT11 |
Materiales necesarios
| Material | Cantidad | Nota |
|---|---|---|
| Arduino Uno (o Nano) | 1 | El código y los pines son iguales en ambos |
| Módulo Bluetooth HC-05 o HC-06 | 1 | Bluetooth clásico; el HM-10 (BLE) no funciona de esta forma |
| Resistencias de 1 kΩ y 2 kΩ | 1 de cada | Divisor de tensión para el pin RXD del HC-05 (2,2 kΩ también sirve) |
| LED y resistencia de 220 Ω | 1 de cada | LED con brillo regulable (D5, PWM) |
| Módulo de relé de 5 V | 1 | Para encender cargas como una lámpara o un ventilador |
| Servomotor SG90 | 1 | Control de 0 a 180° con un deslizador |
| Sensor de temperatura y humedad DHT11 | 1 | Datos que se mostrarán en el móvil |
| Móvil Android | 1 | El HC-05 no funciona con iPhone |
| Protoboard y cables jumper | — | Una GND común es obligatoria |





Circuito: conexión del HC-05, LED, relé, servo y DHT11
La placa del HC-05 lleva un regulador, así que su pin VCC admite 5 V, pero los pines de datos trabajan con lógica de 3,3 V. El Arduino lee sin problemas la señal de 3,3 V que sale del pin TXD del módulo; en cambio, la señal TX de 5 V del Arduino debe llegar al pin RXD a través de un divisor de tensión: D11 → 1 kΩ → RXD, RXD → 2 kΩ → GND. Así llegan a RXD 5 × 2 / 3 ≈ 3,3 V. Conectamos el HC-05 a D10 y D11 con SoftwareSerial en lugar de a los pines serie por hardware del Uno (0 y 1); así puedes subir código con el módulo conectado y seguir usando el Monitor Serie para depurar. Puedes comprobar la resistencia del LED con la calculadora de resistencia para LED.
| Conexión | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| HC-05 VCC | 5V | No hace falta (Bluetooth integrado) | VBUS (5 V) |
| HC-05 GND | GND | — | GND |
| HC-05 TXD | D10 (RX de SoftwareSerial) | — | GP1 (UART0 RX) |
| HC-05 RXD | D11, con divisor 1 kΩ / 2 kΩ | — | GP0 (UART0 TX), sin divisor |
| LED (+ 220 Ω) | D5 (PWM) | GPIO 2 (LED de la placa) | GP15 |
| IN del módulo de relé | D7 | GPIO 26 | GP14 |
| Señal del servo (naranja) | D9 | — (se puede añadir con ESP32Servo) | GP13 |
| Datos del DHT11 | D2 | GPIO 4 | GP16 |
Protocolo de comunicación: ¿qué enviará el móvil?
Antes de escribir código hay que decidir qué «idioma» hablarán el móvil y el Arduino. En este proyecto cada comando es una letra, seguida de un número y, al final, de un carácter de salto de línea (\n, ASCII 10). El salto de línea es clave: los datos Bluetooth pueden llegar por partes, y si «S1» y «35» llegan por separado, el Arduino espera al salto de línea y procesa «S135» como un solo comando. Como los comandos son texto legible, también puedes probarlos a mano con cualquier app de terminal Bluetooth.
| Comando | Sentido | Significado |
|---|---|---|
| L1 / L0 | Móvil → Arduino | Encender el LED al máximo / apagarlo |
| P0 … P255 | Móvil → Arduino | Brillo del LED (valor de analogWrite) |
| R1 / R0 | Móvil → Arduino | Activar / desactivar el relé |
| S0 … S180 | Móvil → Arduino | Ángulo del servo (grados) |
| 24.5;41 | Arduino → móvil | Temperatura (°C) y humedad (%), separadas por punto y coma |
En lugar de texto, App Inventor también puede enviar un único byte (0–255) con el bloque Send1ByteNumber, que el Arduino lee directamente como número. Es corto y rápido, pero solo sirve para un único deslizador: si dos deslizadores y cuatro botones envían bytes, el Arduino no sabe si un 90 que llega es un ángulo del servo o un brillo. El formato letra + número + salto de línea ocupa unos bytes más, pero a 9600 baudios pasan unos 960 caracteres por segundo, así que no notarás la diferencia.
El código de Arduino
Las librerías Servo y SoftwareSerial vienen con el Arduino IDE. Para el DHT11, instala DHT sensor library (Adafruit) desde el Gestor de Librerías; cuando pregunte por las dependencias, elige Install all para añadir también Adafruit Unified Sensor. Como no usamos los pines 0 y 1, no hace falta desconectar el HC-05 al subir el código.
1#include <SoftwareSerial.h>
2#include <Servo.h>
3#include <DHT.h>
4
5SoftwareSerial bt(10, 11); // RX, TX → HC-05 TXD, RXD (bölücü üzerinden / via divider)
6Servo servo;
7DHT dht(2, DHT11); // DHT11 veri pini D2 / data pin D2
8
9const int LED_PIN = 5; // PWM destekli pin / PWM-capable pin
10const int ROLE_PIN = 7;
11const int SERVO_PIN = 9;
12const int ROLE_ACIK = LOW; // çoğu röle modülü LOW ile çeker / most relay modules are active LOW
13const int ROLE_KAPALI = HIGH;
14
15String komut = ""; // gelen satırın tamponu / buffer for the incoming line
16unsigned long sonGonderim = 0;
17
18void komutIsle(String k) {
19 k.trim(); // boşluk ve \r temizle / strip spaces and \r
20 if (k.length() == 0) return;
21 Serial.println(k); // hata ayıklama / debugging
22 char tur = k.charAt(0); // komut harfi / command letter
23 int deger = k.substring(1).toInt(); // harften sonraki sayı / number after the letter
24
25 switch (tur) {
26 case 'L': // L1 / L0 → LED aç, kapat / LED on, off
27 analogWrite(LED_PIN, deger == 1 ? 255 : 0);
28 break;
29 case 'P': // P0..P255 → parlaklık / brightness
30 analogWrite(LED_PIN, constrain(deger, 0, 255));
31 break;
32 case 'R': // R1 / R0 → röle / relay
33 digitalWrite(ROLE_PIN, deger == 1 ? ROLE_ACIK : ROLE_KAPALI);
34 break;
35 case 'S': // S0..S180 → servo açısı / servo angle
36 servo.write(constrain(deger, 0, 180));
37 break;
38 }
39}
40
41void setup() {
42 pinMode(LED_PIN, OUTPUT);
43 pinMode(ROLE_PIN, OUTPUT);
44 digitalWrite(ROLE_PIN, ROLE_KAPALI); // açılışta röle kapalı / relay off at start
45 servo.attach(SERVO_PIN);
46 servo.write(90); // orta konum / centre position
47 dht.begin();
48 Serial.begin(9600); // Seri Monitör / Serial Monitor
49 bt.begin(9600); // HC-05 veri modu hızı / HC-05 data mode baud rate
50}
51
52void loop() {
53 // 1) Telefondan gelen komutları satır satır oku / read commands from the phone line by line
54 while (bt.available()) {
55 char c = bt.read();
56 if (c == '\n') { // satır bitti / line complete
57 komutIsle(komut);
58 komut = "";
59 } else if (komut.length() < 16) { // aşırı uzun satırı kes / cap overlong lines
60 komut += c;
61 }
62 }
63
64 // 2) 2 saniyede bir sensör verisini gönder / send sensor data every 2 seconds
65 if (millis() - sonGonderim >= 2000) {
66 sonGonderim = millis();
67 float t = dht.readTemperature();
68 float h = dht.readHumidity();
69 if (!isnan(t) && !isnan(h)) {
70 bt.print(t, 1); // örnek / example: 24.5
71 bt.print(';');
72 bt.print(h, 0); // örnek / example: 41
73 bt.print('\n'); // println değil, \r gitmesin / not println, no \r
74 }
75 }
76}- Búfer de línea: loop() añade cada carácter recibido a la variable komut (comando); al llegar \n pasa la línea a komutIsle() y vacía el búfer. Lo que supere los 16 caracteres se descarta, así los datos basura no llenan la memoria.
- trim(): Algunas apps de terminal añaden también \r al final de la línea; trim() lo elimina junto con los espacios, de modo que «L1\r» se sigue reconociendo como «L1».
- Letra y número: charAt(0) da la letra del comando y substring(1).toInt() el número que la sigue. constrain() limita el valor a un rango seguro; aunque el móvil envíe 300 por error, el servo nunca se fuerza más allá de 180°.
- Envío sin bloqueos: El sensor se lee cada 2 segundos con millis(). Como no hay delay(), los comandos que llegan mientras tanto se procesan sin retraso; más detalles en millis() frente a delay().
- print('\n') en lugar de println: println() termina la línea con \r\n. App Inventor solo usa \n como delimitador, así que el \r quedaría pegado al final del valor de humedad; enviar solo \n mantiene limpio el análisis en el móvil.
- Relé activo a nivel bajo: La mayoría de los módulos de relé se activan cuando IN pasa a LOW. Si el tuyo se activa con HIGH, basta con intercambiar los valores de las constantes ROLE_ACIK (encendido) y ROLE_KAPALI (apagado).
Diseñar la interfaz en App Inventor (Designer)
- Entra en ai2.appinventor.mit.edu desde el navegador, inicia sesión con tu cuenta de Google y crea un proyecto llamado BlueGraysRemote con Projects > Start new project.
- Arrastra un HorizontalArrangement desde el grupo Layout de la Palette y coloca dentro un ListPicker (Text: Conectar), un Button (Text: Desconectar) y un Label (Text: No conectado).
- Añade debajo dos HorizontalArrangement más: en el primero, los botones Encender LED y Apagar LED; en el segundo, Encender relé y Apagar relé. Si pones Width en Fill parent, los botones son más fáciles de pulsar.
- Añade dos Slider: para el brillo MinValue 0, MaxValue 255, ThumbPosition 0; para el servo MinValue 0, MaxValue 180, ThumbPosition 90. Pon encima de cada uno un Label que diga para qué sirve.
- Añade otros dos Label para la temperatura y la humedad y sube su FontSize a unos 20.
- Desde el grupo Connectivity añade un BluetoothClient; en el panel Properties pon DelimiterByte a 10 y marca la casilla NoLocationNeeded.
- Desde el grupo Sensors añade un Clock; pon TimerInterval a 200 y desmarca TimerEnabled. Activaremos el temporizador con bloques cuando haya conexión.
| Componente | Nombre (Rename) | Ajuste importante |
|---|---|---|
| ListPicker | LP_Connect | Text: Conectar |
| Button × 5 | BTN_Disconnect, BTN_LedOn, BTN_LedOff, BTN_RelayOn, BTN_RelayOff | Width: Fill parent |
| Slider | SLD_Brightness | MinValue 0, MaxValue 255 |
| Slider | SLD_Servo | MinValue 0, MaxValue 180, ThumbPosition 90 |
| Label | LBL_Status, LBL_Temp, LBL_Hum | FontSize 20 (para los valores) |
| BluetoothClient | BluetoothClient1 | DelimiterByte 10, NoLocationNeeded marcado |
| Clock | Clock1 | TimerInterval 200, TimerEnabled desactivado |
Conectarse al HC-05 con un ListPicker
Primero empareja el HC-05 desde los ajustes Bluetooth del móvil (el PIN suele ser 1234; si no, 0000). Este paso es imprescindible: BluetoothClient1.AddressesAndNames solo muestra dispositivos emparejados y no busca otros nuevos cerca. Cada elemento de la lista es un texto con la dirección MAC primero y el nombre del dispositivo después, como «98:D3:31:F5:2A:10 HC-05». Puedes pasar el texto completo al bloque Connect; App Inventor descarta por sí solo lo que va después del primer espacio. El bloque devuelve true si la conexión se establece y false si falla.
- En el evento when LP_Connect.BeforePicking, coloca set LP_Connect.Elements to BluetoothClient1.AddressesAndNames. La lista se vuelve a llenar con los dispositivos emparejados cada vez que se abre.
- En el evento when LP_Connect.AfterPicking, añade un bloque if y encaja en su condición call BluetoothClient1.Connect address: LP_Connect.Selection.
- En la parte then del if, pon LBL_Status.Text a «Conectado» y empieza a leer datos con set Clock1.TimerEnabled to true; en la parte else, escribe «Error de conexión».
- En el evento when BTN_Disconnect.Click, primero set Clock1.TimerEnabled to false, luego call BluetoothClient1.Disconnect y, por último, LBL_Status.Text a «No conectado». Parar antes el temporizador evita una lectura y un mensaje de error después de cerrar la conexión.
Enviar datos: botones y deslizadores
Para enviar basta un solo bloque: call BluetoothClient1.SendText text. Cuando escribes «L1\n» en un bloque de texto, App Inventor convierte el \n final en un carácter de salto de línea. Enviar sin conexión provoca el error 515, así que es buena costumbre envolver cada envío en una comprobación if BluetoothClient1.IsConnected. En un deslizador, unes el número a la letra y al salto de línea con un bloque join: join("S", round(thumbPosition), "\n").
| Evento (event) | Texto enviado | Resultado en el Arduino |
|---|---|---|
| BTN_LedOn.Click | L1\n | El LED se enciende al máximo |
| BTN_LedOff.Click | L0\n | El LED se apaga |
| BTN_RelayOn.Click | R1\n | El relé se activa y la lámpara conectada se enciende |
| BTN_RelayOff.Click | R0\n | El relé se desactiva |
| SLD_Brightness.PositionChanged | join("P", round(thumbPosition), "\n") | analogWrite(5, valor) |
| SLD_Servo.PositionChanged | join("S", round(thumbPosition), "\n") | servo.write(ángulo) |
Mientras arrastras un deslizador con el dedo, el evento PositionChanged se dispara decenas de veces por segundo y thumbPosition devuelve valores con decimales como 87,4. Si envías en cada evento, el mismo ángulo sale una y otra vez, el búfer del HC-05 se llena y el servo se mueve a tirones y con retraso. La solución es sencilla: crea una variable global lastAngle; envía solo si el valor redondeado con round(thumbPosition) es distinto de ella y luego actualiza la variable. Así solo se envían datos cuando el ángulo cambia de verdad. Aplica el mismo truco al deslizador de brillo con global lastBrightness.
Recibir datos: mostrar los valores del sensor en el móvil
El Arduino envía una línea «24.5;41» cada 2 segundos. En App Inventor no hay ningún evento que se dispare solo cuando llegan datos, así que usamos el componente Clock para comprobar a intervalos regulares si hay datos nuevos. Como pusimos DelimiterByte a 10 en el Designer, al dar -1 como numberOfBytes al bloque ReceiveText, este lee hasta el salto de línea y devuelve la línea sin el \n. Un temporizador de 200 ms comprueba mucho más a menudo de lo que llegan los datos (cada 2000 ms), así que en el búfer no se acumula nada.
- Coloca una condición if BluetoothClient1.IsConnected dentro de when Clock1.Timer.
- Añade dentro una segunda condición: call BluetoothClient1.BytesAvailableToReceive > 0.
- Guarda la línea recibida en una variable local con initialize local line to call BluetoothClient1.ReceiveText numberOfBytes: -1.
- Convierte la línea en una lista con el bloque split text line at ";": «24.5;41» se convierte en la lista de dos elementos [24.5, 41]. Guarda el resultado en una variable local llamada parts.
- if length of list parts = 2, pon LBL_Temp.Text a join("Temperatura: ", select list item parts index 1, " °C") y LBL_Hum.Text a join("Humedad: ", select list item parts index 2, " %"). Comprobar el número de elementos evita errores con líneas recibidas a medias o dañadas.
El problema de permisos Bluetooth en Android 12 y posteriores
Con Android 12 (API 31) cambiaron los permisos de Bluetooth. El antiguo permiso BLUETOOTH, que se concedía automáticamente al instalar, se sustituyó por BLUETOOTH_CONNECT y BLUETOOTH_SCAN, que el usuario debe aprobar mientras la app está en marcha; en los ajustes del móvil aparecen como el permiso Dispositivos cercanos. Si no se ha concedido, App Inventor abre el diálogo de permiso en el primer intento, pero detiene el bloque que se estaba ejecutando. El resultado son síntomas conocidos: la lista se abre vacía la primera vez que pulsas Conectar, aparece el mensaje «Error 908: The permission ... has been denied» (con BLUETOOTH_CONNECT o BLUETOOTH_SCAN como nombre) o, si el usuario lo rechazó una vez, la app ya no vuelve a preguntar.
- En el evento when Screen1.Initialize, comprueba la versión de Android: si select list item (split Screen1.PlatformVersion at ".") index 1 ≥ 12, pasa a call Screen1.AskForPermission el valor BluetoothConnect de la lista Permission. En Android 11 y anteriores estos permisos no existen, y pedirlos allí también provoca el error 908, por eso importa la comprobación de versión.
- En el evento when Screen1.PermissionGranted, si el permiso concedido es BLUETOOTH_CONNECT (contains text: permissionName, piece: "CONNECT"), pide a continuación BluetoothScan. AskForPermission solo admite un permiso cada vez, así que hay que encadenarlos de esta forma.
- En el evento when Screen1.PermissionDenied, muestra con un Notifier un mensaje con la ruta «Ajustes > Aplicaciones > BlueGraysRemote > Permisos > Dispositivos cercanos > Permitir». Gestionar este evento muestra al usuario una explicación clara en lugar de una ventana de error en inglés.
- Deja marcada la casilla NoLocationNeeded del BluetoothClient en el Designer; indica a Android que la app no usa el Bluetooth para averiguar la ubicación.
- Prueba siempre también con el APK: los permisos del AI2 Companion pertenecen a la app Companion, y tu APK pide sus propios permisos después de instalarse.
| Error | Significado | Solución |
|---|---|---|
| Error 507: Unable to connect. Is the device turned on? | No se pudo conectar con el módulo | Comprueba la alimentación del HC-05, que no esté conectado a otro móvil y el emparejamiento; si sigue fallando, desmarca Secure en el BluetoothClient y vuelve a probar |
| Error 515: Not connected to a Bluetooth device. | Envío o lectura sin conexión | Envuelve los bloques de envío y del Clock en una comprobación IsConnected |
| Error 516: Unable to write: Broken pipe | La conexión se cortó a mitad (sin alimentación, fuera de alcance) | Para el Clock, llama a Disconnect y vuelve a conectar; alimenta aparte el servo y el relé |
| Error 908: The permission … has been denied | Falta el permiso Dispositivos cercanos en Android 12+ | Pídelo con bloques o concédelo a mano en Ajustes |
| La lista se abre vacía | Módulo sin emparejar o permiso recién solicitado | Empareja el HC-05 en los ajustes del móvil y, tras conceder el permiso, vuelve a pulsar Conectar |
Probar la app y generar el APK
Durante el desarrollo no hace falta instalar un APK tras cada cambio. Instala la app MIT AI2 Companion desde Play Store; con el ordenador y el móvil en la misma red Wi-Fi, elige Connect > AI Companion en App Inventor y escanea el código QR o escribe el código de 6 caracteres que aparece en pantalla. Cada cambio que hagas en el Designer o en los bloques aparece al instante en el móvil. Cuando la app esté terminada, genera el archivo instalable así:
- Selecciona Screen1 en el Designer y rellena AppName (el nombre que se ve en el móvil), Icon (un PNG cuadrado), VersionCode y VersionName.
- En el menú superior elige Build > Android App (.apk). La compilación suele tardar uno o dos minutos.
- Al terminar se abre una ventana con un código QR y un botón de descarga. Puedes escanear el QR con el móvil para descargar el archivo directamente, o guardar el APK en el ordenador y pasarlo al móvil por cable. El enlace del QR solo es válido durante poco tiempo (unas 2 horas).
- Al abrir el APK en el móvil, Android te pide que permitas al navegador o al gestor de archivos instalar aplicaciones desconocidas. Si Play Protect muestra un aviso, puedes seguir con la opción de instalar de todos modos en los detalles; este aviso es habitual en apps de fuera de Play Store con una firma desconocida.
- En el primer arranque de la app, acepta el permiso Dispositivos cercanos, pulsa Conectar y elige el HC-05 en la lista.
La misma app con un ESP32, sin HC-05

El ESP32 clásico integra tanto Bluetooth clásico como BLE. La librería BluetoothSerial presenta el ESP32 al móvil como un dispositivo SPP, así que puedes usar la app que acabas de crear sin cambiar nada. Empareja en los ajustes Bluetooth del móvil el dispositivo llamado «BlueGrays-ESP32», elígelo en la lista de la app y listo. Ojo: el ESP32-S3, el ESP32-C3 y el ESP32-C6 solo admiten BLE; no tienen BluetoothSerial y en App Inventor necesitas la extensión BluetoothLE. Para trabajar con BLE, consulta la guía ESP32 Bluetooth y BLE.
1#include "BluetoothSerial.h"
2#include <DHT.h>
3
4BluetoothSerial SerialBT;
5DHT dht(4, DHT11); // DHT11 veri pini GPIO 4 / data pin GPIO 4
6
7const int LED_PIN = 2; // çoğu DevKit'te kart üstü LED / onboard LED on most DevKits
8const int ROLE_PIN = 26;
9
10String komut = "";
11unsigned long sonGonderim = 0;
12
13void setup() {
14 pinMode(ROLE_PIN, OUTPUT);
15 digitalWrite(ROLE_PIN, HIGH); // aktif LOW röle kapalı / active-LOW relay off
16 dht.begin();
17 SerialBT.begin("BlueGrays-ESP32"); // telefonda görünecek ad / name shown on the phone
18}
19
20void loop() {
21 while (SerialBT.available()) {
22 char c = SerialBT.read();
23 if (c == '\n') {
24 komut.trim();
25 int deger = komut.substring(1).toInt();
26 if (komut.startsWith("L")) {
27 analogWrite(LED_PIN, deger == 1 ? 255 : 0); // LED aç, kapat / LED on, off
28 } else if (komut.startsWith("P")) {
29 analogWrite(LED_PIN, constrain(deger, 0, 255)); // parlaklık / brightness
30 } else if (komut.startsWith("R")) {
31 digitalWrite(ROLE_PIN, deger == 1 ? LOW : HIGH); // röle / relay
32 }
33 komut = "";
34 } else if (komut.length() < 16) {
35 komut += c;
36 }
37 }
38
39 if (millis() - sonGonderim >= 2000) { // Uno koduyla aynı biçim / same format as the Uno
40 sonGonderim = millis();
41 float t = dht.readTemperature();
42 float h = dht.readHumidity();
43 if (!isnan(t) && !isnan(h)) {
44 SerialBT.print(t, 1);
45 SerialBT.print(';');
46 SerialBT.print(h, 0);
47 SerialBT.print('\n');
48 }
49 }
50}El código entiende los mismos comandos que la versión para Uno y envía los datos con el mismo formato «temperatura;humedad»; ignora los comandos S porque no hay ningún servo conectado. La librería Servo de Arduino no funciona en el ESP32; para añadir un servo, instala la librería ESP32Servo y usa las mismas llamadas attach() y write(), por ejemplo en el GPIO 13. Alimenta el módulo de relé desde el pin VIN (5 V); si su entrada IN no conmuta de forma fiable con la señal de 3,3 V del ESP32, elige un módulo de relé compatible con 3,3 V. Más detalles en ¿Qué es un módulo de relé?
Con la Raspberry Pi Pico y MicroPython

La Raspberry Pi Pico estándar no tiene Bluetooth, así que conectas el HC-05 a los pines UART0 (GP0 y GP1). Como los pines de la Pico trabajan a 3,3 V, esta vez no hace falta divisor de tensión; alimenta el módulo desde el pin VBUS, que da los 5 V del USB. La Pico W y la Pico 2 W tienen Bluetooth integrado, pero MicroPython solo admite BLE en ellas; para hablar con el BluetoothClient clásico de App Inventor, el HC-05 sigue siendo el camino más sencillo. El código siguiente entiende los mismos comandos, mueve el servo con PWM de 50 Hz y envía los datos con el mismo formato; el módulo dht viene incluido en el firmware MicroPython de la Pico.
1from machine import UART, Pin, PWM
2import dht
3import time
4
5bt = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1)) # GP0 → HC-05 RXD, GP1 ← HC-05 TXD
6led = PWM(Pin(15))
7led.freq(1000)
8role = Pin(14, Pin.OUT, value=1) # aktif LOW röle kapalı / active-LOW relay off
9servo = PWM(Pin(13))
10servo.freq(50) # servo 50 Hz ister / servos need 50 Hz
11sensor = dht.DHT11(Pin(16))
12
13def komut_isle(k):
14 if not k:
15 return
16 tur, deger = k[0], int(k[1:] or 0) # harf + sayı / letter + number
17 if tur == 'L':
18 led.duty_u16(65535 if deger == 1 else 0)
19 elif tur == 'P':
20 led.duty_u16(min(max(deger, 0), 255) * 257) # 0-255 → 0-65535
21 elif tur == 'R':
22 role.value(0 if deger == 1 else 1)
23 elif tur == 'S':
24 aci = min(max(deger, 0), 180)
25 servo.duty_ns(544000 + aci * 1856000 // 180) # 544-2400 µs darbe / pulse
26
27tampon = b''
28son = time.ticks_ms()
29while True:
30 if bt.any():
31 tampon += bt.read()
32 while b'\n' in tampon: # tam satır geldi mi? / full line received?
33 satir, tampon = tampon.split(b'\n', 1)
34 try:
35 komut_isle(satir.decode().strip())
36 except ValueError:
37 pass # bozuk komutu atla / skip malformed command
38 if len(tampon) > 32:
39 tampon = b'' # satır sonu hiç gelmedi / no newline arrived
40 if time.ticks_diff(time.ticks_ms(), son) >= 2000:
41 son = time.ticks_ms()
42 try:
43 sensor.measure()
44 bt.write('{:.1f};{}\n'.format(sensor.temperature(), sensor.humidity()))
45 except OSError:
46 pass # sensör okunamadı / sensor read failed
47 time.sleep_ms(10)Ideas de proyectos con App Inventor y Arduino
- Coche controlado por Bluetooth: Asigna los botones de dirección a las letras F, B, L y R y mueve los motores con un L298N; tienes todos los pasos en la guía cómo hacer un coche robot con Arduino.
- Panel de casa inteligente: Controla una lámpara, un enchufe y un ventilador desde una sola pantalla con un módulo de relés de cuatro canales; para ideas, mira el proyecto de casa inteligente con Arduino.
- Lámpara con selector de color: Ajusta por separado el rojo, el verde y el azul de un LED RGB con tres deslizadores.
- Brazo robótico: Añade un deslizador por articulación y maneja los servomotores desde el móvil.
- Monitor de invernadero: Muestra en el móvil los datos del DHT11 y del sensor de humedad del suelo y añade un botón que active la bomba cuando la tierra esté seca.
- Control por voz: Con el componente SpeechRecognizer de App Inventor, crea una app que envíe L1 cuando digas «enciende la luz».
Errores frecuentes y cómo solucionarlos
- La app se conecta pero el LED no se enciende: Los cables TXD y RXD del HC-05 están intercambiados o los pines de SoftwareSerial del código no coinciden con el circuito. El TXD del módulo debe ir al RX del Arduino (D10).
- Los comandos no se procesan: Falta el \n en el bloque de texto; el Arduino espera el salto de línea. Comprueba si los comandos aparecen en el Monitor Serie.
- Caracteres sin sentido: La velocidad del HC-05 no coincide con la de bt.begin(). El módulo viene de fábrica a 9600 baudios; si se cambió con comandos AT, ajusta el código.
- El servo tiembla o se corta la conexión: El servo hunde la línea de 5 V del Arduino; aliméntalo con una fuente de 5 V aparte y une las GND. También puede haber un ligero temblor porque SoftwareSerial bloquea las interrupciones un instante mientras recibe; como solución definitiva, usa un Arduino Mega o un ESP32, que tienen puertos serie por hardware libres. De vez en cuando el servo también puede saltar un instante a un ángulo erróneo: la librería DHT desactiva las interrupciones unos 5 ms en cada lectura y un comando que llegue justo entonces puede corromperse; el siguiente comando del deslizador lo corrige.
- Los valores del móvil llegan con retraso: El intervalo del Clock es más largo que el de envío del Arduino y las líneas se acumulan. Mantén TimerInterval por debajo del intervalo de envío.
- La app se cuelga: DelimiterByte sigue en 0 o el Arduino no termina sus líneas con \n; ReceiveText(-1) espera un delimitador que nunca llega.
- No funciona en iPhone: El HC-05 usa Bluetooth clásico, que iOS no permite. Para un iPhone hace falta un módulo BLE (HM-10) o un ESP32.
Combinar App Inventor y Arduino es la forma más divertida de aprender a la vez desarrollo de apps y electrónica. Una vez montada la conexión y el protocolo, el mismo esqueleto sirve para el mando de un coche, de una casa inteligente o de un invernadero; lo único que cambia son las letras que envían los botones. En la app BlueGrays encontrarás paso a paso, con esquemas y código, los proyectos de encender un LED desde el móvil por Bluetooth y del coche controlado por Bluetooth con L298N.
Preguntas frecuentes
¿Una app de App Inventor con Bluetooth funciona en iPhone?
Con un HC-05 o un HC-06, no. Estos módulos usan Bluetooth clásico (SPP) y iOS no permite que las apps normales abran ese tipo de conexión serie. Para comunicarte con un iPhone tienes que pasar a un módulo BLE como el HM-10 o a un ESP32; en proyectos con HC-05, un móvil Android es la opción más sencilla.
¿Por qué la lista Bluetooth sale vacía en App Inventor?
AddressesAndNames solo muestra los dispositivos ya emparejados con el móvil, así que primero empareja el HC-05 en los ajustes Bluetooth con el PIN 1234. En Android 12 y posteriores, la lista también sale vacía la primera vez si no se ha concedido el permiso Dispositivos cercanos; concédelo y vuelve a pulsar Conectar. Asegúrate también de que el Bluetooth esté activado.
¿Cómo soluciono el error 507 de App Inventor?
El error 507 significa «Unable to connect. Is the device turned on?»: el móvil no ha podido abrir la conexión con el módulo. Comprueba que el HC-05 tiene alimentación, que su LED parpadea rápido y que no está conectado a otro móvil. Si el problema sigue, borra el emparejamiento y vuelve a emparejar, o prueba a desactivar la propiedad Secure del BluetoothClient.
¿Cómo se genera el APK en App Inventor?
Abre el proyecto y haz clic en Build > Android App (.apk) en el menú superior. Cuando termine la compilación, escanea el código QR con el móvil o descarga el APK en el ordenador y pásalo al móvil. Durante la instalación tendrás que permitir la instalación de aplicaciones desconocidas.
¿App Inventor se puede conectar a un ESP32 por Bluetooth?
Sí. Con la librería BluetoothSerial, el ESP32 clásico se comporta como un dispositivo SPP y el BluetoothClient de App Inventor se conecta a él igual que a un HC-05. El ESP32-S3 y el ESP32-C3 solo admiten BLE, así que en esas placas necesitas la extensión BluetoothLE.
¿Puedo publicar mi app de App Inventor en Google Play?
Sí. La opción Android App Bundle (.aab) del menú Build genera un archivo que puedes subir a Google Play; para ello necesitas una cuenta de desarrollador de Google Play. Las actualizaciones deben firmarse con la misma clave, así que guarda tu keystore con Projects > Export keystore.
Proyectos para hacer después de leer esto
Ver todos los proyectos
Parpadeo de LED
El primer hola de la electrónica: encender y apagar un solo LED. El Arduino enciende y apaga el LED a intervalos cortos, así que la luz parpadea rápido. Ves de la forma más sencilla cómo la placa cont

Temperatura y humedad con DHT11
¿Qué temperatura hace en tu habitación, y el aire está húmedo o seco? El sensor DHT11 mide la temperatura y la humedad, y el Arduino escribe los valores en una pantalla LCD de dos líneas, actualizándo

Controlar un LED desde el móvil por Bluetooth
Un pequeño ensayo para encender y apagar las luces de tu habitación desde el móvil. El módulo Bluetooth pasa al Arduino las letras que envías desde el teléfono; una letra enciende el LED rojo, otra en

Coche controlado por Bluetooth con L298N
Convierte tu móvil en un mando y conduce tu propio coche robot. El Arduino recibe por un módulo Bluetooth las órdenes de una app de control remoto en el móvil y mueve las ruedas con un controlador de

Uso del servomotor
El brazo de un pequeño servomotor gira por turnos a tres posiciones: al principio del todo, justo al centro y al final del todo, esperando dos segundos en cada parada. El Arduino le dice al motor en q

Uso de Led RGB
Dentro de un solo LED se esconden tres colores: el LED RGB se enciende en rojo, luego en verde y luego en azul, cada color medio segundo encendido y medio segundo apagado, y la secuencia vuelve a empe

Bombilla 220V con Pulsador (Push)
Mientras mantienes pulsado el botón, una bombilla real de 220 voltios se queda encendida, y se apaga en cuanto lo sueltas. La pequeña señal del Arduino no puede encender la bombilla directamente, así

Comunicación unidireccional entre 2 Arduino
Haces que dos Arduino hablen por un solo cable: pulsas el botón de uno y se enciende el LED del otro, vuelves a pulsar y se apaga. La placa que envía avisa del botón con un mensaje corto, y la que rec
Más artículos
App BlueGrays
Monta este circuito paso a paso en tu teléfono
Esquema, código y lista de piezas en una sola pantalla. Más de 160 proyectos de Arduino, ESP32 y Pico listos para montar.
Escanea con tu teléfono
Apunta la cámara al código y se abrirá la tienda de apps de tu teléfono.
o ábrela en la tienda

