# Tema L1 · Laboratorio · ESP32 emulado y Bluetooth

Programación Dirigida por Eventos · Grado en Ingeniería Informática · 2026/27

Del ESP32 emulado a tu app Android: un firmware que corre en esp-emu manda un tick cada 5 s y responde con eco por Bluetooth clásico (SPP) gracias a Bumble, y una Activity Kotlin lo recibe como eventos que llegan desde otro hilo.

## Montar la cadena: firmware, esp-emu, Bumble y emulador

_Qué tienes que conseguir, las cuatro piezas del laboratorio, por qué hace falta Bumble, la cadena SPP completa con su orden de arranque, los siete pasos con hitos verificables y cómo se trabaja el firmware._

### A dónde tienes que llegar · Teoría

En este laboratorio tu app Android **conversa con un ESP32 que no existe físicamente**. El firmware que compilas con PlatformIO corre en un **emulador de ESP32-C3** dentro de tu ordenador y se comunica con la app por **Bluetooth clásico (SPP)**, exactamente igual que lo haría con un módulo **HC-05** real.

El comportamiento es sencillo a propósito:

- **Cada 5 segundos** el firmware manda un `tick` con los segundos que lleva en marcha.
- Cuando le escribes algo desde la app, **contesta con un eco** numerado.

Esto es lo que verás en la app (emulador Pixel 8, conectada a `ESP32-EMU-SPP`):

```text
<< tick 370 s
<< tick 375 s
>> hola desde la app
<< eco #4: hola desde la app
```

Las flechas `<<` marcan lo que **llega** del ESP32 y `>>` lo que **envías** tú.

> [!ANALOGIA]
> Es como ensayar una obra de teatro con un actor de cartón: el decorado, el guion y el público (tu app) son reales, pero el actor que responde lo pone el ordenador. Si la obra funciona con el actor de cartón, funcionará con el de carne y hueso.

### ¿Por qué es interesante para esta asignatura?

Hasta ahora casi todos tus eventos venían **del dedo**: `onClick`, `onCheckedChanged`... Aquí aparecen dos fuentes nuevas:

<div class="cards">
  <div class="card"><h4>Evento temporal</h4><p>El tick cada 5 s: nadie lo pide, llega solo.</p></div>
  <div class="card"><h4>Evento externo</h4><p>Bytes que llegan por un socket cuando quieren, y en otro hilo.</p></div>
  <div class="card"><h4>Evento de usuario</h4><p>Tu mensaje: lo escribes y lo envías, y provoca un eco.</p></div>
</div>

**Preguntas**

**1.** Sin tocar la app, ¿qué deberías ver aparecer en ella cuando todo funciona?

   a) Nada: el ESP32 solo responde cuando le escribes
   b) Un `eco #N` cada 5 segundos
   c) Un `tick N s` cada 5 segundos
   d) Un mensaje cada vez que pulsas el botón BOOT

**2.** ¿Por qué tipo de enlace hablan la app y el ESP32 emulado?

   a) Wi-Fi, con peticiones HTTP
   b) Bluetooth clásico con el perfil SPP, como un HC-05
   c) Bluetooth Low Energy con GATT
   d) Cable USB emulado

### Cuatro piezas, cuatro herramientas · Teoría

La práctica se monta con cuatro piezas. **Solo esp-emu y Bumble son nuevas**; el resto ya lo tienes del laboratorio anterior.

<div class="cards">
  <div class="card"><h4>1 · Firmware</h4><p><b>VS Code + PlatformIO.</b> Proyecto <code>demo-esp32</code>, placa ESP32-C3, framework Arduino. Compila y genera la imagen de flash que carga el emulador.</p></div>
  <div class="card"><h4>2 · Chip</h4><p><b>esp-emu (Espressif).</b> Emulador oficial de ESP32-C3/C6/H2/P4. Ejecuta tu firmware y expone la UART1 por TCP. Es lo nuevo de esta práctica.</p></div>
  <div class="card"><h4>3 · Radio</h4><p><b>Bumble (Google).</b> Pila Bluetooth en Python. Hace de HC-05 virtual: convierte la UART del firmware en un servicio SPP visible desde Android.</p></div>
  <div class="card"><h4>4 · App</h4><p><b>Android Studio.</b> La plantilla <code>pde-plantilla-sdk36</code> con una Activity nueva, <code>BluetoothActivity</code>. El emulador Pixel 8 (API 36) aporta la radio virtual (<i>netsim</i>).</p></div>
</div>

> [!IMPORTANTE] Regla de oro
> **Tú escribes los dos extremos**: el firmware (C++/Arduino) y la Activity (Kotlin). Lo de en medio (esp-emu, Bumble y el emulador Android) **solo se arranca**: viene configurado en los scripts.

Esta regla te ahorra mucho tiempo: si algo falla en medio, casi nunca es «tu código», sino el orden de arranque o una dirección. Lo veremos en el diagnóstico.

| Pieza | Lenguaje / herramienta | ¿La programas tú? |
| --- | --- | --- |
| Firmware | C++ / Arduino en PlatformIO | **Sí** |
| esp-emu | Binario de Espressif | No, solo se arranca |
| Bumble | Paquete Python | No, solo se arranca |
| Emulador Android + netsim | Android Studio | No, solo se arranca |
| BluetoothActivity | Kotlin | **Sí** |

**Preguntas**

**3.** Según la regla de oro, ¿qué piezas **escribes tú**?

   a) El firmware `src/spp/main.cpp`
   b) El emulador esp-emu
   c) El puente Bumble
   d) La `BluetoothActivity` en Kotlin
   e) La radio virtual netsim

**4.** ¿Qué pieza aporta la radio Bluetooth virtual del lado del móvil?

   a) esp-emu
   b) Bumble
   c) PlatformIO
   d) netsim, que arranca con el emulador Android

### Por qué hace falta Bumble en medio · Teoría

Parece que sobra una pieza: si el emulador ejecuta un ESP32, ¿por qué no habla Bluetooth directamente? Tres razones:

1. **esp-emu solo emula chips RISC-V**: ESP32-C3, C6, H2 y P4. **Ninguno tiene Bluetooth clásico**: todos son *BLE only*. El ESP32 «clásico», que sí lo tiene, no se puede emular.
2. **La app usa SPP**, el perfil de Bluetooth clásico de los módulos HC-05/HC-06 de Arduino: un socket con un **flujo de bytes en cada sentido**.
3. **Bumble hace de HC-05.** El firmware habla por su UART1; Bumble publica el servicio SPP en la radio virtual y **reenvía cada byte** a esa UART. Tu código no cambia.

Compara las dos topologías:

![Dos filas alineadas. Con hardware real: ESP32 unido por UART al módulo HC-05, que habla por Bluetooth con el móvil. En esta práctica: esp-emu unido por un socket TCP a Bumble, el HC-05 virtual, que habla por Bluetooth con el emulador Android y su radio netsim. HC-05 y Bumble están en la misma columna, resaltados: cumplen el mismo papel.](/img/pde/laboratorio/hardware-vs-practica.svg "Hardware real frente a la práctica: Bumble ocupa el lugar del HC-05")

La línea recta es un cable (o su equivalente, un socket TCP) y la ondulada es radio Bluetooth. La **topología es la misma**: solo cambia quién pone la radio.

> [!ANALOGIA]
> El HC-05 es un traductor simultáneo: el ESP32 le habla por cable en «UART» y él lo repite por radio en «Bluetooth». Bumble es el mismo traductor, pero en software.

> [!NOTA] Ampliación
> `demo-esp32` trae también un modo **BLE** (NimBLE/GATT), que sí podría hablar el chip emulado. En esta práctica usamos SPP.

> [!EXAMEN]
> «¿Por qué no se conecta la app directamente al ESP32-C3 emulado por Bluetooth clásico?» Porque el C3 (como C6, H2 y P4) es **BLE only**: no tiene Bluetooth clásico. Bumble pone el SPP.

**Preguntas**

**5.** ¿Cuál es la razón principal de que haga falta Bumble?

   a) esp-emu es demasiado lento para gestionar la radio
   b) Los chips que emula esp-emu son BLE only y la app usa SPP, que es Bluetooth clásico
   c) Android no permite conectar con emuladores sin un intermediario Python
   d) PlatformIO no sabe compilar código Bluetooth

**6.** ¿Qué chips puede emular esp-emu?

   a) ESP32-C3
   b) ESP32 clásico
   c) ESP32-C6
   d) ESP32-H2
   e) ESP32-P4

### La cadena completa en modo SPP · Teoría

Esta es la cadena de extremo a extremo:

![Cadena vertical de cinco piezas. Firmware src/spp/main.cpp, que habla por Serial1 (UART1) como si hubiera un HC-05 cableado. esp-emu, que ejecuta merged_flash.bin y sirve esa UART en TCP 127.0.0.1:5556; una flecha naranja indica que es el puente quien se conecta a esp-emu. Puente Bumble (bumble-rfcomm-bridge), que publica el servicio SPP «ESP32-EMU-SPP» en el canal 1. netsim, la radio Bluetooth virtual del emulador Android. BluetoothActivity, que abre un socket RFCOMM al dispositivo emparejado. A la derecha, el firmware y la Activity se marcan como «tú escribes» y las tres piezas intermedias como «solo se arranca, con los scripts».](/img/pde/laboratorio/cadena-spp.svg "La cadena SPP de extremo a extremo: tú escribes los extremos, lo de en medio solo se arranca")

### Direcciones y sentido de la conexión

- La UART1 del firmware se sirve en **`127.0.0.1:5556`**. Escribe siempre la IP, **nunca «localhost»**: `localhost` puede resolver a la dirección IPv6 (`::1`) y entonces no encuentra a esp-emu.
- **El puente RFCOMM se conecta a esp-emu, no al revés.** esp-emu es el servidor TCP; Bumble es el cliente.

### El orden de arranque

Como el puente necesita encontrar a esp-emu escuchando, el orden importa:

<div class="flow">
  <div><b>1</b><small>Emulador Android</small></div>
  <div><b>2</b><small>esp-emu</small></div>
  <div><b>3</b><small>Puente Bumble</small></div>
  <div><b>4</b><small>Emparejar</small></div>
  <div><b>5</b><small>App</small></div>
</div>

Los pasos 2 y 3 los hace `./run.sh spp`, que ya arranca esp-emu primero y luego Bumble.

> [!ANALOGIA]
> Es una cadena de teléfonos: no puedes llamar a alguien que todavía no ha enchufado el suyo. esp-emu tiene que estar «descolgado» (escuchando en el 5556) antes de que Bumble marque.

**Preguntas**

**7.** Ordena el arranque de la cadena.

   Ordena: Abrir la app · Emparejar ESP32-EMU-SPP · Emulador Android · Puente Bumble · esp-emu

**8.** ¿En qué puerto TCP sirve esp-emu la UART1 del firmware?

**9.** ¿Por qué los scripts usan `127.0.0.1` y nunca `localhost`?

   a) Porque `localhost` puede resolver a IPv6 y no encontrar a esp-emu
   b) Porque `localhost` solo funciona en Windows
   c) Porque `127.0.0.1` es más rápido de escribir
   d) Porque Bumble no acepta nombres de máquina por licencia

### Lo que recibes y lo que instalas · Práctica

Recibes **dos ZIP** del profesor e instalas **dos herramientas nuevas**.

### ZIP `demo-esp32`

| Fichero | Para qué |
| --- | --- |
| `platformio.ini` | Dos entornos: `esp32c3` (BLE) y **`esp32c3-spp`** (el nuestro) |
| `src/spp/main.cpp` | El firmware de partida |
| `build.sh` y `run.sh` | Compilar y arrancar la cadena |
| `tools/hc05.json` | Identidad del HC-05 virtual |
| `tools/verify_spp.py` | Prueba de la cadena **sin app** |

### ZIP `pde-plantilla-sdk36`

La plantilla de la asignatura (Compose + vistas clásicas). Sus versiones están fijadas: **no toques `gradle/`**. Aquí añadirás `BluetoothActivity`, su layout y los permisos en el manifest. El emulador **Pixel 8 API 36** se crea desde *Device Manager*.

### Lo que instalas tú

- **esp-emu**: binario oficial de Espressif (instalador o *release* de GitHub). Hito: `esp-emu --help` responde.
- **Bumble**: paquete Python en un **entorno virtual**, con **Python 3.10 o superior**:

```bash
# desde la carpeta que CONTIENE demo-esp32 (los scripts esperan ../.venv)
python3 -m venv .venv
source .venv/bin/activate
pip install "bumble[all]"
```

Ya tienes del laboratorio anterior VS Code + PlatformIO y Android Studio.

> [!CUIDADO] Rutas que esperan los scripts
> `build.sh` y `run.sh` buscan el venv de Bumble en **`../.venv`** (una carpeta por encima del proyecto) y PlatformIO en **`~/.platformio`**. Si lo pones en otro sitio, ajusta los scripts.

Las comillas en `"bumble[all]"` no son decorativas: en zsh los corchetes se interpretan como patrón de ficheros y sin comillas el comando puede fallar.

**Preguntas**

**10.** Creas el venv de Bumble dentro de `demo-esp32/.venv` y `run.sh` no encuentra Bumble. ¿Por qué?

   a) Bumble necesita instalarse con permisos de administrador
   b) Los scripts esperan el venv en `../.venv`, una carpeta por encima del proyecto
   c) El venv debe llamarse `bumble`, no `.venv`
   d) Bumble solo funciona instalado en `~/.platformio`

**11.** ¿Qué versión mínima de Python necesitas para Bumble?

   a) Python 2.7
   b) Python 3.6
   c) Python 3.10
   d) Python 3.13

**12.** ¿Cómo se llama el entorno de `platformio.ini` que usamos en esta práctica (el de SPP)?

### Los siete pasos, con hitos · Práctica

Los pasos 1 a 3 se hacen **una sola vez**; del 4 al 7 son el **ciclo de trabajo** de la práctica.

| # | Qué | Con qué | Hito: sabes que está cuando… |
| --- | --- | --- | --- |
| 1 | Instalar esp-emu y Bumble | Instalador de esp-emu · venv + `pip install "bumble[all]"` | `esp-emu --help` responde |
| 2 | Compilar el firmware SPP | VS Code + PlatformIO · `./build.sh esp32c3-spp` | Existe `merged_flash.bin` |
| 3 | Arrancar el emulador Android | Android Studio · Pixel 8 API 36 | `netsimd` está corriendo |
| 4 | Arrancar esp-emu y el puente | `./run.sh spp` (esp-emu primero, luego Bumble) | Consola: «firmware SPP listo» |
| 5 | Emparejar (solo una vez) | Ajustes → Bluetooth → *Pair new device* | «ESP32-EMU-SPP» emparejado |
| 6 | Crear `BluetoothActivity` | Layout, permisos, socket RFCOMM, hilo lector | El Spinner lista el dispositivo |
| 7 | Cerrar el bucle | Cambiar firmware y app; recompilar, reinstalar | Ticks y ecos en la app |

> [!IMPORTANTE] Regla
> Cada fila es un **hito verificable**. Si no puedes marcar la última columna, **no pases a la siguiente fila**.

¿Por qué tanta insistencia? Porque en una cadena de cinco piezas, un fallo en la fila 2 se manifiesta como «la app no conecta» en la fila 7, y entonces no sabes dónde mirar. Avanzar hito a hito es **aislar el fallo antes de que se esconda**.

> [!TRUCO]
> El emparejamiento (paso 5) se hace **una vez**. Si reinicias el puente más adelante, no vuelvas a emparejar: el dispositivo sigue en la lista de emparejados. Lo que puede pasar es otra cosa (el «enlace zombi»), que verás en el diagnóstico.

**Preguntas**

**13.** Ordena los siete pasos del laboratorio.

   Ordena: Arrancar el emulador Android · Arrancar esp-emu y el puente · Cerrar el bucle · Compilar el firmware SPP · Crear BluetoothActivity · Emparejar ESP32-EMU-SPP · Instalar esp-emu y Bumble

**14.** ¿Cuál es el hito del paso 4 (arrancar esp-emu y el puente)?

   a) Existe `merged_flash.bin`
   b) `netsimd` está corriendo
   c) El Spinner lista el dispositivo
   d) La consola muestra «firmware SPP listo»

### El firmware: Serial, Serial1 y merged_flash.bin · Práctica

Proyecto `demo-esp32`, entorno `esp32c3-spp`, placa `esp32-c3-devkitm-1`, framework Arduino, **sin librerías extra**.

### Qué tocar: `src/spp/main.cpp`

El firmware usa **dos puertos serie** con papeles distintos:

| Puerto | Para qué | Dónde lo ves |
| --- | --- | --- |
| `Serial` | **Logs** de depuración | Consola de esp-emu |
| `Serial1` (UART1, pines 4 y 5) | **Hablar con la app** | Bumble → Bluetooth → app |

El firmware lee líneas de `Serial1`, responde con eco y manda un tick cada 5 s. Un boceto orientativo (la configuración exacta de `Serial1` es la de la plantilla):

```cpp
uint32_t ultimoTick = 0;
uint32_t ecos = 0;
String linea;

void setup() {
  Serial.begin(115200);                       // logs → consola de esp-emu
  // Serial1.begin(...): baudios y pines 4/5 como vienen en la plantilla
  Serial.println("[APP] firmware SPP listo");
}

void loop() {
  if (millis() - ultimoTick >= 5000) {        // evento temporal: tick
    ultimoTick = millis();
    Serial1.printf("tick %lu s\n", (unsigned long) (millis() / 1000));
  }
  while (Serial1.available()) {               // evento externo: bytes
    char c = Serial1.read();
    if (c == '\n') {
      Serial.printf("[UART1] recibido: %s\n", linea.c_str());
      Serial1.printf("eco #%lu: %s\n", (unsigned long) ++ecos, linea.c_str());
      linea = "";
    } else if (c != '\r') {
      linea += c;
    }
  }
}
```

### Cómo compilar

```bash
./build.sh esp32c3-spp
```

El script hace `pio run` y después `esptool merge_bin`. **esp-emu quiere la imagen unificada `merged_flash.bin`, no `firmware.bin`**: necesita toda la flash montada (no solo tu aplicación), como si fuera el chip real recién grabado.

> [!IMPORTANTE] Regla del ciclo
> Cada cambio en el firmware = **recompilar y relanzar esp-emu**. La app **no** hay que reinstalarla.

> [!EXAMEN]
> Diferencia `Serial` / `Serial1` y `firmware.bin` / `merged_flash.bin`: son las dos confusiones más típicas de esta práctica.

**Preguntas**

**15.** Escribes `Serial.println("hola")` esperando verlo en la app, pero solo aparece en la consola de esp-emu. ¿Qué ha pasado?

   a) Bumble no está arrancado
   b) `Serial` es el puerto de logs; para la app hay que escribir en `Serial1`
   c) Hay que reinstalar la app
   d) Falta el permiso `BLUETOOTH_CONNECT`

**16.** ¿Cómo se llama la imagen unificada que carga esp-emu?

### Ejercicio: ¿qué tengo que relanzar? · Ejercicio

En el ciclo de trabajo (pasos 4 a 7) vas a cambiar cosas a ambos lados constantemente. Saber **qué relanzar** según qué has tocado te ahorra muchos minutos.

| Has cambiado… | Tienes que… | No hace falta… |
| --- | --- | --- |
| El firmware (`main.cpp`) | `./build.sh esp32c3-spp` y relanzar esp-emu (`./run.sh spp`) | Reinstalar la app |
| La Activity o el layout | Recompilar y reinstalar la app desde Android Studio | Recompilar el firmware |
| Nada, solo reiniciaste el puente | Puede que Android crea que sigue el enlace anterior (ver diagnóstico) | Volver a emparejar |

Piensa en la cadena como piezas independientes que se hablan por «tuberías» (TCP, Bluetooth). Cambiar una pieza solo obliga a reiniciar **esa** pieza y, como mucho, las que dependen de ella para conectarse.

<details><summary>Caso resuelto</summary>
<p>Cambias el periodo del tick de 5 s a 2 s. Recompilas con <code>./build.sh esp32c3-spp</code> y relanzas <code>./run.sh spp</code>. Como esp-emu arranca de nuevo, el contador de segundos del tick vuelve a empezar. La app sigue instalada; si se queda «colgada» tras el reinicio del puente, mira el caso del enlace zombi.</p>
</details>

**Preguntas**

**17.** Cambias el texto del tick en el firmware. ¿Qué tienes que hacer?

   a) Ejecutar `./build.sh esp32c3-spp`
   b) Relanzar esp-emu con `./run.sh spp`
   c) Reinstalar la app
   d) Volver a emparejar ESP32-EMU-SPP

**18.** Añades un botón nuevo a la Activity que envía el comando `reset`. ¿Qué recompilas?

   a) Solo el firmware
   b) El firmware y la app
   c) Nada: Android recarga el layout solo
   d) Solo la app, y la reinstalas

### Repaso: la cadena en una página · Repaso

> [!RESUMEN] La cadena SPP
> **Firmware** (`Serial1`, UART1) → **esp-emu** (`merged_flash.bin`, UART1 en `127.0.0.1:5556`) → **Bumble** (HC-05 virtual, servidor SPP «ESP32-EMU-SPP», canal 1) → **netsim** (radio del emulador) → **app** (socket RFCOMM).

| Idea | Recuerda |
| --- | --- |
| Objetivo | Tick cada 5 s y eco a cada mensaje, por Bluetooth clásico (SPP) |
| Regla de oro | Escribes los extremos (firmware y Activity); lo de en medio solo se arranca |
| ¿Por qué Bumble? | esp-emu emula C3/C6/H2/P4, todos BLE only; Bumble pone el SPP |
| Orden | Emulador Android → esp-emu → puente Bumble → emparejar → app |
| Direcciones | `127.0.0.1`, nunca `localhost`; el puente se conecta a esp-emu |
| Firmware | `Serial` = logs; `Serial1` (pines 4 y 5) = app |
| Imagen | esp-emu carga `merged_flash.bin`, no `firmware.bin` |
| Ciclo | Cambio de firmware → recompilar y relanzar esp-emu; la app no se reinstala |
| Hitos | Si no marcas el hito de una fila, no pases a la siguiente |

**Preguntas**

**19.** En modo SPP, ¿quién inicia la conexión TCP entre esp-emu y Bumble?

   a) esp-emu se conecta a Bumble
   b) Las dos piezas se conectan a netsim y este las une
   c) El puente Bumble se conecta a esp-emu
   d) La app Android abre la conexión TCP

**20.** Marca las afirmaciones **correctas** sobre el laboratorio.

   a) El ESP32-C3 emulado no tiene Bluetooth clásico
   b) Hay que emparejar el dispositivo cada vez que se relanza el puente
   c) `tools/verify_spp.py` permite probar la cadena sin la app
   d) Los scripts `build.sh` y `run.sh` vienen en el ZIP `demo-esp32`
   e) esp-emu carga directamente `firmware.bin`

**21.** Escribe el comando que arranca esp-emu y el puente Bumble en modo SPP.

### Solucionario

1. c) Un `tick N s` cada 5 segundos  
   El firmware manda un `tick` cada 5 s por su cuenta. El `eco #N` solo aparece como respuesta a un mensaje tuyo.
2. b) Bluetooth clásico con el perfil SPP, como un HC-05  
   Se usa Bluetooth clásico (SPP), el perfil de los módulos HC-05/HC-06. El modo BLE existe solo como ampliación.
3. a) El firmware `src/spp/main.cpp`; d) La `BluetoothActivity` en Kotlin  
   Escribes los dos extremos: firmware y Activity. esp-emu, Bumble y el emulador Android (con netsim) solo se arrancan con los scripts.
4. d) netsim, que arranca con el emulador Android  
   netsim es la radio Bluetooth virtual del emulador Android. Bumble publica el servicio SPP en esa radio, y esp-emu solo expone la UART por TCP.
5. b) Los chips que emula esp-emu son BLE only y la app usa SPP, que es Bluetooth clásico  
   C3, C6, H2 y P4 no tienen Bluetooth clásico. Bumble actúa como un HC-05 virtual que convierte la UART en un servicio SPP.
6. a) ESP32-C3; c) ESP32-C6; d) ESP32-H2; e) ESP32-P4  
   esp-emu emula los RISC-V C3, C6, H2 y P4. El ESP32 «clásico» no se puede emular.
7. 1. Emulador Android → 2. esp-emu → 3. Puente Bumble → 4. Emparejar ESP32-EMU-SPP → 5. Abrir la app  
   Emulador Android → esp-emu → puente Bumble → emparejar → app. esp-emu va antes que el puente porque es el puente quien se conecta a esp-emu.
8. `5556`  
   La UART1 queda en `127.0.0.1:5556`.
9. a) Porque `localhost` puede resolver a IPv6 y no encontrar a esp-emu  
   `localhost` puede resolverse a `::1` (IPv6). Con la IPv4 explícita no hay ambigüedad.
10. b) Los scripts esperan el venv en `../.venv`, una carpeta por encima del proyecto  
   Los scripts esperan `../.venv`. O lo mueves ahí o ajustas `build.sh` y `run.sh`.
11. c) Python 3.10  
   El material pide Python 3.10 o superior.
12. `esp32c3-spp`  
   `esp32c3-spp`. El otro entorno, `esp32c3`, es el modo BLE de ampliación.
13. 1. Instalar esp-emu y Bumble → 2. Compilar el firmware SPP → 3. Arrancar el emulador Android → 4. Arrancar esp-emu y el puente → 5. Emparejar ESP32-EMU-SPP → 6. Crear BluetoothActivity → 7. Cerrar el bucle  
   Instalar → compilar → emulador Android → esp-emu + puente → emparejar → Activity → cerrar el bucle.
14. d) La consola muestra «firmware SPP listo»  
   Al arrancar la cadena, la consola de esp-emu debe mostrar el mensaje del firmware. `merged_flash.bin` es el hito del paso 2, `netsimd` del 3 y el Spinner del 6.
15. b) `Serial` es el puerto de logs; para la app hay que escribir en `Serial1`  
   `Serial` va a la consola de esp-emu. Lo que viaja a la app es lo que se escribe en `Serial1` (UART1), que Bumble reenvía por SPP.
16. `merged_flash.bin`  
   `merged_flash.bin`, generada por `build.sh` con `esptool merge_bin`. No vale `firmware.bin` a secas.
17. a) Ejecutar `./build.sh esp32c3-spp`; b) Relanzar esp-emu con `./run.sh spp`  
   Cambio de firmware = recompilar y relanzar esp-emu. La app no se reinstala y el emparejamiento se conserva.
18. d) Solo la app, y la reinstalas  
   Has tocado la Activity, así que recompilas y reinstalas la app. Si además quieres que el firmware reaccione de forma especial a `reset`, eso sería otro cambio, ya en el firmware.
19. c) El puente Bumble se conecta a esp-emu  
   El puente RFCOMM se conecta a esp-emu, no al revés. Por eso esp-emu debe arrancar antes.
20. a) El ESP32-C3 emulado no tiene Bluetooth clásico; c) `tools/verify_spp.py` permite probar la cadena sin la app; d) Los scripts `build.sh` y `run.sh` vienen en el ZIP `demo-esp32`  
   El emparejamiento se hace una sola vez y esp-emu carga `merged_flash.bin`. El resto es correcto.
21. `./run.sh spp`  
   `./run.sh spp` arranca esp-emu primero y luego el puente Bumble.

## La BluetoothActivity, eventos externos y diagnóstico

_Permisos, dispositivos emparejados, socket RFCOMM con el UUID SPP en un hilo aparte, hilo lector con runOnUiThread y su relación con Observer. Cierra con Windows/WSL2, los tres casos de «no conecta» y la lista de llegada._

### Manifest y permiso BLUETOOTH_CONNECT · Práctica

La Activity nueva se llama `BluetoothActivity` y su interfaz es **XML con vistas clásicas**, como la de `ContadorVistas`. La novedad es que algunos eventos ya no vienen de la pantalla, sino **del socket**.

### Solo un permiso

```xml
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

- **`BLUETOOTH_CONNECT`**: necesario para hablar con dispositivos ya emparejados. Es un permiso peligroso, así que además de declararlo **se pide en tiempo de ejecución**.
- **Sin `BLUETOOTH_SCAN`**: no buscamos dispositivos nuevos; usamos **los ya emparejados** (paso 5 del laboratorio).

```kotlin
private val REQ_BT = 1

private fun pedirPermiso() {
    val concedido = ContextCompat.checkSelfPermission(
        this, Manifest.permission.BLUETOOTH_CONNECT
    ) == PackageManager.PERMISSION_GRANTED
    if (concedido) cargarEmparejados()
    else ActivityCompat.requestPermissions(
        this, arrayOf(Manifest.permission.BLUETOOTH_CONNECT), REQ_BT
    )
}

override fun onRequestPermissionsResult(
    requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == REQ_BT && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
        cargarEmparejados()
    }
}
```

Fíjate en que la respuesta del usuario al diálogo de permisos **también es un evento**: `onRequestPermissionsResult` es un callback que el sistema invoca cuando el usuario decide.

> [!NOTA]
> Menos permisos = menos diálogos para el usuario y menos superficie de error. Si no escaneas, no pidas `BLUETOOTH_SCAN`.

**Preguntas**

**1.** ¿Qué permiso declaras en el manifest y pides en tiempo de ejecución para usar los dispositivos emparejados?

**2.** ¿Por qué la práctica **no** pide `BLUETOOTH_SCAN`?

   a) Porque en API 36 ya no existe
   b) Porque el emulador concede todos los permisos automáticamente
   c) Porque no se buscan dispositivos: se usan los ya emparejados
   d) Porque Bumble hace el escaneo por la app

### Adaptador y dispositivos emparejados · Práctica

El adaptador Bluetooth se obtiene **a través de `BluetoothManager`**. Si está apagado, se pide al usuario que lo encienda con `ACTION_REQUEST_ENABLE`. Después, `bondedDevices` (los dispositivos emparejados) rellena un **Spinner**.

```kotlin
private var emparejados: List<BluetoothDevice> = emptyList()

@SuppressLint("MissingPermission") // ya comprobado en pedirPermiso()
private fun cargarEmparejados() {
    val manager = getSystemService(BluetoothManager::class.java)
    val adapter = manager.adapter ?: return          // sin Bluetooth
    if (!adapter.isEnabled) {
        startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
        return
    }
    emparejados = adapter.bondedDevices.toList()
    spinner.adapter = ArrayAdapter(
        this,
        android.R.layout.simple_spinner_dropdown_item,
        emparejados.map { it.name ?: it.address }
    )
}
```

Hito del paso 6: **el Spinner lista `ESP32-EMU-SPP`**. Si no aparece, el problema está antes: ¿emparejaste en *Ajustes → Bluetooth → Pair new device*?

> [!CUIDADO] Emparejado no es lo mismo que disponible
> `bondedDevices` devuelve lo que Android **recuerda** haber emparejado, no lo que está encendido ahora. Que `ESP32-EMU-SPP` salga en el Spinner **no garantiza** que esp-emu y el puente estén corriendo. Esto es la base del primer caso del diagnóstico.

<div class="flow">
  <div><b>Permiso</b><small>BLUETOOTH_CONNECT</small></div>
  <div><b>Manager</b><small>BluetoothManager.adapter</small></div>
  <div><b>¿Encendido?</b><small>si no, ACTION_REQUEST_ENABLE</small></div>
  <div><b>bondedDevices</b><small>lista de emparejados</small></div>
  <div><b>Spinner</b><small>el usuario elige</small></div>
</div>

**Preguntas**

**3.** ¿Qué propiedad del adaptador devuelve los dispositivos ya emparejados?

**4.** `ESP32-EMU-SPP` aparece en el Spinner. ¿Qué puedes concluir con seguridad?

   a) Que esp-emu está ejecutando el firmware
   b) Que el puente Bumble está conectado a esp-emu
   c) Que el socket RFCOMM ya está abierto
   d) Que el dispositivo se emparejó en algún momento

### Conectar: socket RFCOMM en un Thread · Práctica

Para hablar SPP se abre un **socket RFCOMM** con el **UUID estándar del perfil SPP**, que empieza por `00001101-`:

```kotlin
private val SPP_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
private var socket: BluetoothSocket? = null

@SuppressLint("MissingPermission")
private fun conectar(device: BluetoothDevice) {
    estado = "Conectando con ${device.name}…"
    actualizarVista()
    Thread {
        try {
            val s = device.createRfcommSocketToServiceRecord(SPP_UUID)
            s.connect()                       // BLOQUEA hasta conectar o fallar
            socket = s
            runOnUiThread { estado = "Conectado"; actualizarVista() }
            arrancarLector(s)
        } catch (e: IOException) {
            runOnUiThread { estado = "Error de conexión: ${e.message}"; actualizarVista() }
        }
    }.start()
}
```

### Dos reglas

1. **`connect()` dentro de un `Thread`, nunca en el hilo principal.** Es una llamada bloqueante: puede tardar segundos o no volver hasta el *timeout*. En el hilo principal congelaría la interfaz (y Android acabaría mostrando «la app no responde»).
2. Las vistas solo se tocan desde el hilo principal: por eso el resultado vuelve con `runOnUiThread`.

> [!CUIDADO] Trampa: `cancelDiscovery()`
> Muchos ejemplos de Internet llaman a `adapter.cancelDiscovery()` antes de `connect()`. **No lo hagas aquí**: ese método exige `BLUETOOTH_SCAN`, que no hemos pedido, y la app **casca** con una excepción de seguridad.

> [!EXAMEN]
> «¿Por qué `connect()` va en otro hilo?» Porque bloquea. El hilo principal es el que procesa los eventos de la interfaz; si se queda esperando, no se atiende ningún clic ni se repinta nada.

**Preguntas**

**5.** Un compañero llama a `socket.connect()` directamente en el `onClick` del botón Conectar. ¿Qué ocurrirá?

   a) Nada grave: `connect()` es asíncrono y vuelve enseguida
   b) La interfaz se congela mientras `connect()` bloquea el hilo principal
   c) Android lanza la conexión en otro hilo automáticamente
   d) El emparejamiento se pierde

**6.** Añades `adapter.cancelDiscovery()` antes de `connect()` porque lo viste en un tutorial y la app se cierra al conectar. ¿Por qué?

   a) `cancelDiscovery()` exige `BLUETOOTH_SCAN`, que la app no tiene
   b) `cancelDiscovery()` desempareja el dispositivo
   c) `cancelDiscovery()` cierra el socket antes de abrirlo
   d) `cancelDiscovery()` solo funciona con BLE

**7.** ¿Por qué grupo de dígitos empieza el UUID estándar del perfil SPP?

### Hilo lector, runOnUiThread y enviar · Práctica

Una vez conectado, los bytes del ESP32 **llegan cuando quieren**: un tick cada 5 s, un eco tras cada mensaje. Para recibirlos hace falta un **hilo lector** que espere sobre `inputStream` y convierta **cada línea en un evento de interfaz**:

```kotlin
private val log = mutableListOf<String>()

private fun arrancarLector(s: BluetoothSocket) {
    Thread {
        try {
            val lector = s.inputStream.bufferedReader()
            while (true) {
                val linea = lector.readLine() ?: break   // bloquea hasta que llega una línea
                runOnUiThread {
                    log.add("<< $linea")
                    actualizarVista()
                }
            }
        } catch (e: IOException) {
            runOnUiThread { estado = "Desconectado"; actualizarVista() }
        }
    }.start()
}
```

**Enviar** es lo contrario: escribir en `outputStream`. Añadimos el salto de línea porque el firmware lee **líneas**:

```kotlin
private fun enviar(texto: String) {
    val s = socket ?: return
    Thread { s.outputStream.write("$texto\n".toByteArray()) }.start()
    log.add(">> $texto")
    actualizarVista()
}
```

### Una sola función que pinta

Todos los eventos acaban en el mismo sitio:

```kotlin
private fun actualizarVista() {
    tvEstado.text = estado
    tvLog.text = log.joinToString("\n")
}
```

> [!TRUCO]
> `readLine()` es bloqueante, igual que `connect()`. Por eso el lector vive en su propio hilo: el principal queda libre para atender los clics de «Enviar».

| Acción | Hilo | Mecanismo |
| --- | --- | --- |
| `connect()` | Secundario | `Thread { }` |
| Leer líneas | Secundario (lector) | `inputStream` + `readLine()` |
| Tocar vistas | Principal | `runOnUiThread { }` |
| Enviar | Escribe en `outputStream` | `"texto\n"` |

**Preguntas**

**8.** ¿Qué función usa el hilo lector para que la línea recibida se pinte en pantalla?

   a) `Thread.sleep()`
   b) `runOnUiThread { }`
   c) `socket.connect()`
   d) `setContentView()`

**9.** Envías `hola` sin salto de línea y el ESP32 nunca responde con el eco. ¿Por qué?

   a) Porque el firmware lee por líneas y espera el `\n` para procesar el mensaje
   b) Porque `outputStream` solo admite números
   c) Porque falta el permiso `BLUETOOTH_SCAN`
   d) Porque el eco solo se manda cada 5 segundos

### Eventos que no vienen del dedo · Teoría

Esto es lo que de verdad enseña la práctica: **eventos que llegan por un socket, en otro hilo**.

<div class="cards">
  <div class="card"><h4>Evento de usuario</h4><p><code>onClick</code>, <code>onCheckedChanged</code>. Llegan <b>ya en el hilo principal</b>, igual que en el contador de pulsaciones.</p></div>
  <div class="card"><h4>Evento externo</h4><p>Bytes que llegan cuando quieren, <b>en otro hilo</b>. Hay que traerlos al hilo de la interfaz con <code>runOnUiThread</code> para repintar.</p></div>
  <div class="card"><h4>Misma regla</h4><p>Un solo estado y una función que vuelca el estado en las vistas: <code>actualizarVista()</code>. Los dos tipos de evento la llaman.</p></div>
</div>

![Dos carriles. Arriba, el hilo lector (secundario): inputStream con los bytes del socket, readLine() que bloquea hasta tener una línea, y la línea «tick 375 s», que es un evento. Una flecha naranja, runOnUiThread, la baja al carril de abajo, el hilo principal (UI). Allí, el clic en «Enviar» ya llega directamente; ambos caminos pasan por log.add, el único estado, y terminan en actualizarVista(), que pinta tvEstado y tvLog.](/img/pde/laboratorio/hilos-activity.svg "El clic ya está en el hilo principal; la línea del socket necesita runOnUiThread para llegar")

### Enlace con la Unidad 2: Observer con un hilo en medio

<div class="flow">
  <div><b>Hilo lector</b><small>productor: genera un evento por línea</small></div>
  <div><b>runOnUiThread</b><small>puente al hilo principal</small></div>
  <div><b>Activity</b><small>suscriptor: actualiza el estado</small></div>
  <div><b>actualizarVista()</b><small>vuelca el estado en las vistas</small></div>
</div>

Es el **Observer de la Clase 3** con una diferencia: el sujeto (el hilo lector) no vive en el mismo hilo que el observador (la Activity). `runOnUiThread` hace de **puente**: no llama al observador directamente, sino que **encola** el aviso en el hilo principal, que lo procesará en su bucle de eventos cuando le toque.

> [!ANALOGIA]
> El hilo lector es un mensajero que trae cartas a cualquier hora. No puede entrar en el despacho (las vistas) mientras el jefe (hilo principal) trabaja: deja la carta en la bandeja de entrada (`runOnUiThread`) y el jefe la abre cuando termina lo que estaba haciendo.

> [!EXAMEN]
> Identifica los roles: **productor** = hilo lector, **suscriptor** = Activity, **puente** = `runOnUiThread`, **única función de pintado** = `actualizarVista()`. Y razona por qué un `onClick` no necesita puente y un byte del socket sí.

**Preguntas**

**10.** Ordena el viaje de un `tick` desde que llega por el socket hasta que lo ves.

   Ordena: El hilo lector completa la línea con `readLine()` · La Activity añade la línea al estado · Los bytes llegan al `inputStream` del socket · `actualizarVista()` vuelca el estado en las vistas · `runOnUiThread` encola el aviso en el hilo principal

**11.** ¿Por qué un `onClick` **no** necesita `runOnUiThread` y una línea del socket sí?

   a) Porque `onClick` es más rápido
   b) Porque el `onClick` llega ya en el hilo principal; la línea del socket llega en el hilo lector
   c) Porque los eventos de Bluetooth tienen más prioridad
   d) Porque `runOnUiThread` solo funciona con datos de red

**12.** En la analogía con Observer, marca las asociaciones **correctas**.

   a) Hilo lector → productor (sujeto)
   b) Activity → suscriptor (observador)
   c) `runOnUiThread` → suscriptor
   d) `runOnUiThread` → puente al hilo principal
   e) `actualizarVista()` → productor

### Si trabajas en Windows: WSL2 · Práctica

**esp-emu solo tiene binarios para Linux y macOS.** En Windows, el chip emulado corre en **WSL2** (Ubuntu). El resto se queda en Windows:

| Pieza | Dónde corre | Nota |
| --- | --- | --- |
| VS Code + PlatformIO (compilar) | Windows | Igual que en el lab anterior |
| esp-emu (ejecutar el firmware) | **WSL2 (Ubuntu)** | Instalador oficial dentro de WSL; el `.bin` se lee desde `/mnt/c/…` |
| Bumble (HC-05 virtual) | Windows (Python) | Llega a esp-emu por localhost: WSL2 reenvía los puertos |
| Android Studio + emulador | Windows | netsim arranca con el emulador, sin cambios |
| `build.sh` y `run.sh` | Git Bash o WSL | Son scripts bash; en PowerShell no funcionan tal cual |

`/mnt/c/` es cómo ve WSL2 la unidad `C:\` de Windows: así esp-emu, dentro de Linux, puede leer el `merged_flash.bin` que compilaste en Windows.

<div class="flow">
  <div><b>Windows</b><small>PlatformIO compila merged_flash.bin</small></div>
  <div><b>WSL2</b><small>esp-emu lo lee de /mnt/c/… y abre :5556</small></div>
  <div><b>Windows</b><small>Bumble llega a :5556 por el reenvío de puertos</small></div>
  <div><b>Windows</b><small>emulador Android + netsim</small></div>
</div>

> [!CUIDADO] Ruta sin validar
> Según el propio material, **esta ruta todavía no se ha probado en el laboratorio**: la cadena completa solo está verificada en **macOS**. Queda pendiente confirmar antes de la sesión si se usará WSL2 o una máquina virtual Linux completa. Si estás en Windows, avanza hito a hito y apunta dónde te atascas.

> [!TRUCO]
> Si `./build.sh` te da errores raros en Windows, comprueba que no lo estás lanzando desde PowerShell. Abre Git Bash (o WSL) y repite.

**Preguntas**

**13.** En Windows, ¿dónde se ejecuta esp-emu?

   a) En PowerShell, como un .exe
   b) Dentro del emulador Android
   c) En WSL2 (Ubuntu), porque solo hay binarios para Linux y macOS
   d) En Git Bash, que emula Linux

**14.** Lanzas `./run.sh spp` desde PowerShell y no funciona. ¿Qué haces?

   a) Reescribirlo en PowerShell
   b) Ejecutarlo como administrador
   c) Instalar esp-emu para Windows
   d) Lanzarlo desde Git Bash o WSL, porque es un script bash

### Ejercicio: cuando no conecta, tres casos · Ejercicio

Tres síntomas típicos y sus tres causas. Lee las causas y resuelve los casos de abajo emparejando síntoma y causa.

<div class="cards">
  <div class="card"><h4>A · No hay nadie al otro lado</h4><p>esp-emu o el puente no están corriendo. El dispositivo sigue emparejado y por eso aparece en el Spinner. Arreglo: lanza <code>./run.sh spp</code> y reintenta.</p></div>
  <div class="card"><h4>B · Enlace zombi</h4><p>Reiniciaste el puente y Android cree que sigue el enlace anterior. Arreglo: reinicia el Bluetooth del emulador <b>sin perder el emparejamiento</b>.</p></div>
  <div class="card"><h4>C · Orden o dirección equivocados</h4><p>En modo SPP esp-emu va antes que el puente, y siempre <code>127.0.0.1</code>, no <code>localhost</code> (que resuelve a IPv6).</p></div>
</div>

Para el enlace zombi:

```bash
adb shell cmd bluetooth_manager disable
adb shell cmd bluetooth_manager enable
```

> [!TRUCO] Cómo razonar
> - Si el error sale **en la app** al conectar → mira primero si la cadena está levantada.
> - Si la app se queda **esperando sin error** → sospecha de un estado antiguo en Android.
> - Si el error sale **al arrancar los scripts** → la app ni ha entrado en juego: es orden o dirección.

<details><summary>¿Por qué el caso A es tan engañoso?</summary>
<p>Porque el Spinner se rellena con <code>bondedDevices</code>, que es memoria de Android, no estado real. Ver «ESP32-EMU-SPP» en la lista da sensación de que todo está en marcha, pero puede que no haya nada escuchando.</p>
</details>

**Preguntas**

**15.** Pulsas Conectar y aparece `Error de conexión: read failed, socket might closed or timeout`. ¿Causa más probable?

   a) A · No hay nadie al otro lado: esp-emu o el puente no están corriendo
   b) B · Enlace zombi
   c) C · Orden o dirección equivocados
   d) Falta el permiso `BLUETOOTH_SCAN`

**16.** Acabas de reiniciar el puente. La app muestra «Conectando con ESP32-EMU-SPP…» y no pasa nada, sin error. ¿Qué haces?

   a) Desemparejar y volver a emparejar el dispositivo
   b) Reiniciar el Bluetooth del emulador con `adb shell cmd bluetooth_manager disable` / `enable`
   c) Cambiar `127.0.0.1` por `localhost`
   d) Añadir `cancelDiscovery()` antes de `connect()`

**17.** Al ejecutar los scripts sale `Connection refused` al arrancar esp-emu o el puente. ¿Causa?

   a) A · No hay nadie al otro lado
   b) B · Enlace zombi
   c) Falta el permiso `BLUETOOTH_CONNECT`
   d) C · Orden o dirección equivocados: el puente arrancó antes que esp-emu, o se usó `localhost`

### Cómo saber que has llegado · Práctica

Cuatro comprobaciones. Las dos primeras demuestran que la cadena funciona; **las dos últimas cierran el bucle**, porque demuestran que controlas los dos extremos.

- [ ] La consola de esp-emu muestra `[APP] firmware SPP listo` y, al enviar desde la app, `[UART1] recibido: …`.
- [ ] La app recibe los `tick N s` **sin tocar nada** y un `eco #N` por cada mensaje enviado.
- [ ] Has **cambiado el firmware** (el texto del tick, el periodo) y se ve en la app tras recompilar.
- [ ] Has **cambiado la Activity** (un botón nuevo, un comando) y el firmware reacciona.

### Prueba sin app: `verify_spp.py`

Si la app no conecta y no sabes si el problema está en tu Activity o en la cadena, usa el script del ZIP:

```bash
# desde demo-esp32, con el venv de Bumble activado
python tools/verify_spp.py
```

Recorre **el mismo camino** que la app (TCP → RFCOMM → puente → UART1) y debe terminar en **«OK»**.

| verify_spp.py | La app | Dónde está el problema |
| --- | --- | --- |
| OK | Conecta | En ningún sitio |
| OK | No conecta | En tu **Activity** (permisos, UUID, hilos…) |
| Falla | No conecta | En la **cadena** (esp-emu, puente, orden, dirección) |

> [!TRUCO]
> Esta tabla es la técnica de **dividir el problema por la mitad**: una prueba que cubre la mitad del camino te dice en qué mitad buscar.

**Preguntas**

**18.** `tools/verify_spp.py` termina en «OK», pero tu app no conecta. ¿Dónde buscas?

   a) En esp-emu: hay que reinstalarlo
   b) En el puente Bumble: hay que reiniciarlo
   c) En tu Activity: permisos, UUID, hilos…
   d) En el firmware: hay que recompilarlo

**19.** ¿Qué comprobaciones **cierran el bucle** según el material?

   a) Cambiar el firmware y ver el cambio en la app
   b) Ver `[APP] firmware SPP listo` en la consola
   c) Cambiar la Activity y ver que el firmware reacciona
   d) Ver los ticks en la app

### Repaso: la Activity y el diagnóstico · Repaso

> [!RESUMEN] BluetoothActivity en seis líneas
> 1. Manifest: `BLUETOOTH_CONNECT`, pedido en tiempo de ejecución; **sin** `BLUETOOTH_SCAN`.
> 2. `BluetoothManager` → adaptador; si está apagado, `ACTION_REQUEST_ENABLE`.
> 3. `bondedDevices` → Spinner.
> 4. `createRfcommSocketToServiceRecord` con el UUID SPP `00001101-…`; `connect()` en un `Thread`. **Nunca** `cancelDiscovery()`.
> 5. Hilo lector sobre `inputStream` → `runOnUiThread` → `actualizarVista()`. Enviar = escribir en `outputStream`.
> 6. Productor (hilo lector) → puente (`runOnUiThread`) → suscriptor (Activity): Observer con un hilo en medio.

| Síntoma | Causa | Arreglo |
| --- | --- | --- |
| `read failed, socket might closed or timeout` | Nadie al otro lado | `./run.sh spp` y reintentar |
| «Conectando…» y nada | Enlace zombi | `adb shell cmd bluetooth_manager disable` / `enable` |
| `Connection refused` al arrancar | Orden o dirección | esp-emu antes que el puente; `127.0.0.1` |

**Windows:** esp-emu en WSL2; el resto en Windows; scripts en Git Bash o WSL. Ruta aún sin validar.

**Preguntas**

**20.** ¿Qué operaciones deben hacerse **fuera** del hilo principal?

   a) `socket.connect()`
   b) `lector.readLine()` en bucle
   c) `tvLog.text = …`
   d) `spinner.adapter = …`

**21.** ¿Qué herramienta de línea de comandos usas para reiniciar el Bluetooth del emulador Android en el caso del enlace zombi?

**22.** Pregunta de examen: ¿qué patrón de la Unidad 2 reconoces en el hilo lector + `runOnUiThread` + Activity?

   a) Singleton
   b) Observer, con un hilo en medio
   c) Factory Method
   d) MVC sin controlador

### Solucionario

1. `BLUETOOTH_CONNECT`  
   `BLUETOOTH_CONNECT`. No hace falta `BLUETOOTH_SCAN` porque no buscamos dispositivos.
2. c) Porque no se buscan dispositivos: se usan los ya emparejados  
   La app solo lista `bondedDevices` (emparejados). Sin búsqueda no hace falta `BLUETOOTH_SCAN`.
3. `bondedDevices`  
   `adapter.bondedDevices` (en Java, `getBondedDevices()`) devuelve el conjunto de dispositivos emparejados.
4. d) Que el dispositivo se emparejó en algún momento  
   `bondedDevices` refleja el emparejamiento guardado, no el estado actual de la cadena.
5. b) La interfaz se congela mientras `connect()` bloquea el hilo principal  
   `connect()` es bloqueante. En el hilo principal impide procesar eventos de interfaz: la app se congela.
6. a) `cancelDiscovery()` exige `BLUETOOTH_SCAN`, que la app no tiene  
   Es la trampa señalada en el material: `cancelDiscovery()` requiere `BLUETOOTH_SCAN` y, sin él, la app casca.
7. `00001101`  
   El UUID SPP es `00001101-0000-1000-8000-00805F9B34FB`.
8. b) `runOnUiThread { }`  
   `runOnUiThread` encola el bloque en el hilo principal, el único que puede tocar las vistas.
9. a) Porque el firmware lee por líneas y espera el `\n` para procesar el mensaje  
   El firmware acumula caracteres hasta recibir un fin de línea; sin `\n` el mensaje nunca se da por completo.
10. 1. Los bytes llegan al `inputStream` del socket → 2. El hilo lector completa la línea con `readLine()` → 3. `runOnUiThread` encola el aviso en el hilo principal → 4. La Activity añade la línea al estado → 5. `actualizarVista()` vuelca el estado en las vistas  
   Socket → hilo lector (productor) → `runOnUiThread` (puente) → Activity (suscriptor) actualiza el estado → `actualizarVista()`.
11. b) Porque el `onClick` llega ya en el hilo principal; la línea del socket llega en el hilo lector  
   Los eventos de usuario se entregan en el hilo principal. Los del socket aparecen en otro hilo y hay que trasladarlos para tocar las vistas.
12. a) Hilo lector → productor (sujeto); b) Activity → suscriptor (observador); d) `runOnUiThread` → puente al hilo principal  
   Productor = hilo lector, suscriptor = Activity, puente = `runOnUiThread`. `actualizarVista()` es la función que vuelca el estado.
13. c) En WSL2 (Ubuntu), porque solo hay binarios para Linux y macOS  
   esp-emu no tiene binario para Windows: se instala en WSL2 y lee el `.bin` desde `/mnt/c/…`.
14. d) Lanzarlo desde Git Bash o WSL, porque es un script bash  
   `build.sh` y `run.sh` son scripts bash: funcionan en Git Bash o en WSL, no en PowerShell tal cual.
15. a) A · No hay nadie al otro lado: esp-emu o el puente no están corriendo  
   El dispositivo sale en el Spinner porque está emparejado, pero no hay nadie escuchando. Solución: `./run.sh spp` y reintentar.
16. b) Reiniciar el Bluetooth del emulador con `adb shell cmd bluetooth_manager disable` / `enable`  
   Es el enlace zombi: Android cree que sigue el enlace anterior. Se reinicia el Bluetooth del emulador sin perder el emparejamiento. `cancelDiscovery()` haría cascar la app.
17. d) C · Orden o dirección equivocados: el puente arrancó antes que esp-emu, o se usó `localhost`  
   El puente se conecta a esp-emu; si esp-emu no escucha aún, o la dirección resuelve a IPv6, la conexión se rechaza. Los permisos de Android no intervienen al arrancar los scripts.
18. c) En tu Activity: permisos, UUID, hilos…  
   Si el script llega hasta la UART1, la cadena está bien. Lo que falla es lo único que el script no usa: tu Activity.
19. a) Cambiar el firmware y ver el cambio en la app; c) Cambiar la Activity y ver que el firmware reacciona  
   Las dos últimas comprobaciones exigen modificar cada extremo y ver el efecto al otro lado.
20. a) `socket.connect()`; b) `lector.readLine()` en bucle  
   `connect()` y `readLine()` bloquean: van en hilos secundarios. Tocar vistas (`tvLog`, `spinner`) debe hacerse en el hilo principal.
21. `adb`  
   `adb shell cmd bluetooth_manager disable` y luego `enable`: reinicia el Bluetooth sin perder el emparejamiento.
22. b) Observer, con un hilo en medio  
   El hilo lector produce eventos, la Activity los observa y `runOnUiThread` hace de puente entre hilos.
