1783 lines
52 KiB
Markdown
1783 lines
52 KiB
Markdown
# BLE + ESP32: Masterclass Teórico-Práctico
|
|
|
|
## De Cero a Experto en Bluetooth Low Energy y Desarrollo Embedded
|
|
|
|
---
|
|
|
|
## TABLA DE CONTENIDOS
|
|
|
|
1. [Módulo 0: Fundamentos de BLE](#módulo-0-fundamentos-de-ble)
|
|
2. [Módulo 1: Advertising & Discovery](#módulo-1-advertising--discovery)
|
|
3. [Módulo 2: Connections & GATT](#módulo-2-connections--gatt)
|
|
4. [Módulo 3: Security & Advanced Topics](#módulo-3-security--advanced-topics)
|
|
5. [Debugging Masterclass](#debugging-masterclass)
|
|
6. [Ejercicios Prácticos Progresivos](#ejercicios-prácticos-progresivos)
|
|
7. [Referencias & Recursos](#referencias--recursos)
|
|
|
|
---
|
|
|
|
# MÓDULO 0: FUNDAMENTOS DE BLE
|
|
|
|
## 0.1 ¿Qué es Bluetooth Low Energy (BLE)?
|
|
|
|
BLE es una tecnología inalámbrica diseñada para **baja potencia, corto alcance y bajo consumo de energía**. Se diferencia del **Classic Bluetooth** (BR/EDR) en:
|
|
|
|
| Característica | Classic BT | BLE |
|
|
|---|---|---|
|
|
| **Potencia** | ~100 mW | ~10 mW promedio |
|
|
| **Alcance** | ~100 m | ~100 m (BLE 5.x) |
|
|
| **Latencia** | 100 ms | <10 ms |
|
|
| **Ancho de banda** | 720 kbps | 1-2 Mbps |
|
|
| **Caso de uso** | Audio, streaming | Sensores, IoT, wearables |
|
|
| **Stack** | Complejo (Bluedroid+BR/EDR) | Simplificado (NimBLE BLE-only) |
|
|
|
|
**¿Por qué BLE importa en IoT/Edge AI?**
|
|
- Actualizaciones de modelos ML en dispositivos wearables sin descargar batería.
|
|
- Comunicación entre edge devices (sensores + nodos) con bajo overhead.
|
|
- Integración con blockchain: coordinación de múltiples IoT nodes.
|
|
|
|
---
|
|
|
|
## 0.2 Arquitectura del Stack BLE
|
|
|
|
El stack BLE se organiza en **capas**, de forma similar a OSI:
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ APLICACIÓN (Application) │ ← Tu código
|
|
├─────────────────────────────────────┤
|
|
│ GATT (Generic Attribute Profile) │ ← Servicios y características
|
|
├─────────────────────────────────────┤
|
|
│ GAP (Generic Access Profile) │ ← Advertising, conexiones
|
|
├─────────────────────────────────────┤
|
|
│ ATT (Attribute Protocol) │ ← Lectura/escritura de atributos
|
|
├─────────────────────────────────────┤
|
|
│ L2CAP (Logical Link Control) │ ← Multiplexación de canales
|
|
├─────────────────────────────────────┤
|
|
│ Link Layer (Enlace de datos) │ ← Control de conexión, hop freq
|
|
├─────────────────────────────────────┤
|
|
│ PHY (Física) │ ← Radio 2.4 GHz, modulación
|
|
└─────────────────────────────────────┘
|
|
│
|
|
├─── CONTROLLER ───────────────────── (Firmware en el chip)
|
|
│ (HCI: Host Controller Interface)
|
|
│
|
|
└─── HOST ─────────────────────────── (Software en tu aplicación)
|
|
(Bluedroid o NimBLE en ESP32)
|
|
```
|
|
|
|
### Conceptos Clave:
|
|
|
|
**1. Controller vs Host**
|
|
- **Controller**: Firmware bajo nivel en el chip (RF, Link Layer). No lo tocas en ESP32; está pre-compilado.
|
|
- **Host**: Software que interpreta comandos del aplicación y genera HCI commands para el controller.
|
|
- **HCI (Host Controller Interface)**: Protocolo de comunicación entre host y controller. En ESP32 usa transporte RAM (no UART).
|
|
|
|
**2. Roles BLE**
|
|
- **Peripheral (Esclavo)**: Anuncia datos, acepta conexiones. Ej: reloj inteligente.
|
|
- **Central (Maestro)**: Escanea, inicia conexiones. Ej: smartphone.
|
|
|
|
**3. Estados GAP**
|
|
- **Standby**: Inactivo.
|
|
- **Advertising**: Transmite paquetes de publicidad.
|
|
- **Scanning**: Escucha paquetes de publicidad.
|
|
- **Connected**: Vinculado con otra dispositivo.
|
|
|
|
---
|
|
|
|
## 0.3 Physical Layer (PHY): La Radio
|
|
|
|
### Banda de Frecuencia
|
|
|
|
BLE opera en la banda ISM (Industrial, Scientific, Medical) de **2.4 GHz**, compartida con WiFi, Zigbee, etc.
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ 2.4 GHz ISM Band (2400 - 2483.5 MHz) │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ BLE: 37 canales de datos + 3 canales de advertising │
|
|
│ Espaciados cada 2 MHz │
|
|
│ │
|
|
│ Ch 37: 2402 MHz (Adv) │
|
|
│ Ch 38: 2426 MHz (Adv) │
|
|
│ Ch 39: 2480 MHz (Adv) │
|
|
│ │
|
|
│ Ch 0-36: 2404 - 2480 MHz (Datos + algunas adv ext) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Modulación y Velocidades (PHYs)
|
|
|
|
| PHY | Velocidad | Modulación | Uso |
|
|
|---|---|---|---|
|
|
| **LE 1M** (default) | 1 Mbps | GFSK | Compatibilidad, bajo costo |
|
|
| **LE 2M** (BLE 5.0+) | 2 Mbps | GFSK | Mayor throughput, menos batería en tx |
|
|
| **LE Coded** (BLE 5.0+) | 125/500 kbps | GFSK + FEC | Largo alcance, menor SNR |
|
|
|
|
**Nota**: Todos los chips ESP32 modernos soportan LE 1M. LE 2M y LE Coded dependen de la variante (C3, S3, etc.).
|
|
|
|
### Formato de Paquete BLE (Uncoded PHY)
|
|
|
|
```
|
|
┌───────┬──────────────┬───────────┬─────┐
|
|
│Preamble│Access Address│ PDU │ CRC │
|
|
│ 1 byte │ 4 bytes │ 2-257B │3 byte│
|
|
└───────┴──────────────┴───────────┴─────┘
|
|
└────────────────────────────────┘
|
|
Link Layer Packet (LL PDU)
|
|
```
|
|
|
|
- **Preamble** (0xAA/0x55): Sincronización de frecuencia.
|
|
- **Access Address** (4B): ID del dispositivo o conexión.
|
|
- **PDU**: Datos; puede ser advertising PDU o data channel PDU.
|
|
- **CRC** (3B): Cyclic Redundancy Check para detectar errores.
|
|
|
|
---
|
|
|
|
## 0.4 Link Layer: La Inteligencia
|
|
|
|
### Frequency Hopping (AFH)
|
|
|
|
BLE evita colisiones e interferencias usando **Adaptive Frequency Hopping (AFH)**:
|
|
|
|
1. **Secuencia pseudo-aleatoria**: Los dispositivos emparejados comparten un `Access Address` y un `hop increment` (derivado de direcciones MAC).
|
|
2. **Salto por conexión**: Salta entre los 37 canales de datos cada `connection interval` (7.5 ms - 4 s).
|
|
3. **Channel map**: Si ciertos canales tienen mucho ruido, se excluyen dinámicamente.
|
|
|
|
**Fórmula**: Nuevo canal = (Anterior + hop_increment) % 37
|
|
|
|
```python
|
|
# Pseudocódigo del hop
|
|
def next_channel(current_ch, hop_inc, channel_map):
|
|
"""Calcula el próximo canal en el hop."""
|
|
unmapped_channels = channel_map # Máscara de 37 bits
|
|
|
|
next_unmapped = (current_ch + hop_inc) % 37
|
|
# Si está excluido, reintenta con el siguiente
|
|
while not test_bit(unmapped_channels, next_unmapped):
|
|
next_unmapped = (next_unmapped + 1) % 37
|
|
|
|
return next_unmapped
|
|
```
|
|
|
|
### Paquetes Advertising PDU
|
|
|
|
Formato de un paquete de publicidad:
|
|
|
|
```
|
|
┌──────────────────┬─────────────────────────┐
|
|
│ Advertising Hdr │ Advertising Payload │
|
|
├──────────────────┼─────────────────────────┤
|
|
│ Tipo | Tx/Rx Adr │ MAC + Datos Payload │
|
|
│ 1B │ 1B │ ≤ 255 bytes │
|
|
└──────────────────┴─────────────────────────┘
|
|
```
|
|
|
|
**Tipos de Advertising PDU:**
|
|
- `ADV_IND` (0x00): Connectable undirected (periférico abierto).
|
|
- `ADV_DIRECT_IND`: Connectable directed (a un central específico).
|
|
- `ADV_NONCONN_IND`: Non-connectable (beacon).
|
|
- `ADV_SCAN_IND`: Scannable undirected (periférico + scan response).
|
|
- `SCAN_RSP`: Respuesta a scan request.
|
|
|
|
---
|
|
|
|
## 0.5 ATT & GATT: Estructura de Datos
|
|
|
|
### ATT (Attribute Protocol)
|
|
|
|
ATT es el protocolo **crudo** de lectura/escritura de **atributos** (bloques de datos con UUID).
|
|
|
|
**Operaciones ATT:**
|
|
```
|
|
Cliente (Central) Servidor (Peripheral)
|
|
│ │
|
|
├─ Read Request ────────────────> │
|
|
│ Read Response │
|
|
│ <────────────────────────────────┤
|
|
│ │
|
|
├─ Write Request ───────────────> │
|
|
│ Write Response │
|
|
│ <────────────────────────────────┤
|
|
│ │
|
|
├─ Notify (unsolicited) <───────── │
|
|
│ (sin confirmación) │
|
|
│ │
|
|
├─ Indicate (unsolicited) <────── │
|
|
│ (con confirmación) │
|
|
│ ─── Confirmation ───────────> │
|
|
```
|
|
|
|
### GATT (Generic Attribute Profile)
|
|
|
|
GATT es una **capa de semántica** sobre ATT que organiza atributos en:
|
|
|
|
1. **Servicios**: Colecciones de funcionalidad. Ej: Heart Rate Service (HRS).
|
|
2. **Características**: Propiedades dentro de un servicio. Ej: Heart Rate Measurement.
|
|
3. **Descriptores**: Metadatos de una característica. Ej: CCCD (Client Characteristic Configuration).
|
|
|
|
**Estructura GATT:**
|
|
```
|
|
Service (UUID 0x180D = Heart Rate Service)
|
|
├─ Characteristic 1 (UUID 0x2A37 = Heart Rate Measurement)
|
|
│ ├─ Value: [BPM value]
|
|
│ ├─ Properties: Read, Notify
|
|
│ └─ Descriptor (CCCD): [0x01, 0x00] = Notificaciones enabled
|
|
│
|
|
└─ Characteristic 2 (UUID 0x2A38 = Body Sensor Location)
|
|
├─ Value: [0x01 = Chest]
|
|
└─ Properties: Read
|
|
```
|
|
|
|
**CCCD (Client Characteristic Configuration Descriptor)**:
|
|
- Bitmask que el cliente escribe para activar/desactivar notificaciones.
|
|
- `0x0001`: Notificaciones.
|
|
- `0x0002`: Indicaciones.
|
|
|
|
---
|
|
|
|
## 0.6 Bluedroid vs NimBLE: Comparativa Técnica
|
|
|
|
### ESP-Bluedroid
|
|
|
|
**Arquitectura:**
|
|
```
|
|
┌─────────────────┐
|
|
│ esp_* APIs │ ← Tu código
|
|
├─────────────────┤
|
|
│ BTC (Bluetooth │ ← Profile manager, HCI handlers
|
|
│ Transport Ctrl) │
|
|
├─────────────────┤
|
|
│ BTU (Bluetooth │ ← L2CAP, GATT, SMP, ATT
|
|
│ Upper Layer) │
|
|
├─────────────────┤
|
|
│ HCI Interface │ ← Comandos al controller
|
|
└─────────────────┘
|
|
```
|
|
|
|
**Características:**
|
|
- Soporta Classic BT (BR/EDR) + BLE.
|
|
- Memora RAM: **~200 KB heap** típicamente.
|
|
- Velocidad init: ~631 ms (ver benchmark).
|
|
- Mayor footprint de código.
|
|
|
|
**Cuándo usar Bluedroid:**
|
|
- Necesitas Classic BT (audio Bluetooth clásico).
|
|
- Compatibilidad máxima con código Android.
|
|
|
|
### Apache NimBLE
|
|
|
|
**Arquitectura:**
|
|
```
|
|
┌─────────────────┐
|
|
│ ble_* APIs │ ← Tu código
|
|
├─────────────────┤
|
|
│ NimBLE Host │ ← Task separado en FreeRTOS
|
|
│ (event-driven) │
|
|
├─────────────────┤
|
|
│ HCI Interface │ ← Comandos al controller
|
|
└─────────────────┘
|
|
```
|
|
|
|
**Características:**
|
|
- **BLE-only** (no Classic BT).
|
|
- RAM: **~70-80 KB heap**, mucho más eficiente.
|
|
- Velocidad init: **~209 ms** (3x más rápido).
|
|
- Open-source Apache license.
|
|
- Mejor para deep sleep / battery-powered devices.
|
|
|
|
**Cuándo usar NimBLE:**
|
|
- Solo necesitas BLE (99% de IoT moderno).
|
|
- Quieres optimizar potencia y memoria.
|
|
- Prefieres open-source.
|
|
|
|
### Recomendación
|
|
|
|
**🎯 Para tu roadmap (Edge AI + IoT + Blockchain):**
|
|
|
|
Usa **NimBLE** porque:
|
|
1. Menor footprint → más espacio para ML models.
|
|
2. Mejor para federated learning (múltiples nodos IoT).
|
|
3. Comunidad open-source muy activa (Apache).
|
|
4. Por defecto moderno en nuevos proyectos ESP32.
|
|
|
|
---
|
|
|
|
## 0.7 Seguridad BLE: Introducción Conceptual
|
|
|
|
### Legacy Pairing (BLE 4.1 y anteriores)
|
|
|
|
1. **Intercambio de claves Temporary Link Key (TK)** basado en IO capabilities.
|
|
2. **Generación STK** (Short Term Key) via algoritmo simple (vulnerable).
|
|
3. **Encriptación** de la conexión con STK.
|
|
|
|
**Vulnerabilidades conocidas:**
|
|
- KNOB attack (Key Negotiation Of Bluetooth).
|
|
- Downgrade attacks.
|
|
|
|
### LE Secure Connections (LESC, BLE 4.2+)
|
|
|
|
1. **Generación de pares ECDH** (Elliptic Curve Diffie-Hellman) en cada dispositivo.
|
|
2. **Intercambio de claves públicas** (seguro matemáticamente).
|
|
3. **Generación LTK** (Long Term Key) determinista y fuerte.
|
|
4. **Variantes de autenticación:**
|
|
- **Numeric Comparison**: Ambos muestran 6 dígitos; usuario confirma si coinciden.
|
|
- **Passkey Entry**: Uno muestra código, otro lo digita.
|
|
- **Just Works**: Sin confirmación (sin MITM protection).
|
|
- **Out Of Band (OOB)**: Intercambio de claves por canal alternativo (NFC, etc.).
|
|
|
|
**Matemática simplificada:**
|
|
```
|
|
Dispositivo A: d_a = random(); Q_a = d_a * G (ECDH público)
|
|
Dispositivo B: d_b = random(); Q_b = d_b * G
|
|
|
|
Intercambio: Q_a <-> Q_b
|
|
|
|
Shared secret: DHKey = d_a * Q_b = d_b * Q_a = (d_a * d_b) * G
|
|
|
|
LTK = PRES(DHKey, other_params) [derivado del shared secret]
|
|
```
|
|
|
|
**En ESP32:**
|
|
- LESC es el default en NimBLE.
|
|
- Generación ECDH es **blocking ~160 ms** (limitación ROM).
|
|
- Requiere supervision_timeout >= 165 ms durante pairing.
|
|
|
|
---
|
|
|
|
## 0.8 Consumo de Potencia y Parámetros Críticos
|
|
|
|
### Ecuación General
|
|
|
|
```
|
|
Consumo promedio = (T_on * I_active + T_sleep * I_sleep) / T_total
|
|
```
|
|
|
|
### Parámetros de Conexión
|
|
|
|
| Parámetro | Rango | Impacto |
|
|
|---|---|---|
|
|
| **Connection Interval** | 7.5 ms - 4 s | Menor = mayor latencia pero más potencia |
|
|
| **Peripheral Latency** | 0 - 499 | Nodo puede saltear N intervalos (reduce despertares) |
|
|
| **Supervision Timeout** | 100 ms - 32 s | Si no recibe dato en este tiempo, desconecta |
|
|
| **MTU (Max Transfer Unit)** | 23 - 251 bytes | Mayor MTU = menos fragmentación, menos paquetes |
|
|
|
|
**Ejemplo optimizado para batería:**
|
|
```c
|
|
// Connection interval 1 segundo, permite saltar 199 de cada 200 eventos
|
|
ble_gap_upd_params conn_params = {
|
|
.itvl_min = 800, // 1000 ms
|
|
.itvl_max = 800,
|
|
.latency = 199, // Puede saltear hasta 199 intervalos
|
|
.supervision_timeout = 320, // 3.2 segundos
|
|
.min_ce_len = 0x00,
|
|
.max_ce_len = 0x00
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
# MÓDULO 1: ADVERTISING & DISCOVERY
|
|
|
|
## 1.1 Ciclo de Vida del Advertising
|
|
|
|
### Estados del Periférico
|
|
|
|
```
|
|
┌─────────────┐
|
|
│ STANDBY │
|
|
│ (inactivo) │
|
|
└──────┬──────┘
|
|
│ esp_ble_gap_start_advertising()
|
|
▼
|
|
┌─────────────┐
|
|
│ ADVERTISING │◄──────────────────────┐
|
|
│ (cada ~100 │ │
|
|
│ eventos, │ │
|
|
│ hopping │ │
|
|
│ canales) │ │
|
|
└──────┬──────┘ │
|
|
│ (remoto) Connection request │
|
|
▼ │
|
|
┌─────────────┐ │
|
|
│ CONNECTED │────► Disconnect ──────┘
|
|
│ │
|
|
└─────────────┘
|
|
```
|
|
|
|
### Advertising Event
|
|
|
|
**Cada N milisegundos:**
|
|
|
|
1. ESP32 transmite paquete de publicidad en **Ch 37**.
|
|
2. Espera ~120 μs por **SCAN_REQ** (si scannable).
|
|
3. Si recibe SCAN_REQ, transmite **SCAN_RSP** en Ch 37.
|
|
4. Salta a **Ch 38**, repite.
|
|
5. Salta a **Ch 39**, repite.
|
|
6. Espera hasta el próximo `advertising interval` (ej: 100 ms).
|
|
|
|
---
|
|
|
|
## 1.2 Estructura de Advertising Data
|
|
|
|
Los datos de publicidad se empaquetan en **TLV (Type-Length-Value)** blocks:
|
|
|
|
```
|
|
┌──────┬────────┬─────────────────┐
|
|
│ Type │ Length │ Value │
|
|
│ 1B │ 1B │ Length bytes │
|
|
└──────┴────────┴─────────────────┘
|
|
^ ^ ^
|
|
│ │ └─ Contenido real (flags, UUID, etc.)
|
|
│ └─ Tamaño en bytes
|
|
└─ Tipo de dato (0x01 = Flags, 0x08 = Name, etc.)
|
|
```
|
|
|
|
### Tipos Comunes (AD Types)
|
|
|
|
| AD Type | Valor | Significado |
|
|
|---------|-------|-----------|
|
|
| `0x01` | Flags | Propiedades del dispositivo |
|
|
| `0x08` | Shortened Local Name | Nombre corto |
|
|
| `0x09` | Complete Local Name | Nombre completo |
|
|
| `0x0A` | TX Power Level | Potencia de transmisión |
|
|
| `0x16` | Service Data (16-bit UUID) | Datos asociados a servicio |
|
|
| `0x20` | Service Data (32-bit UUID) | Idem pero UUID 32-bit |
|
|
| `0x21` | Service Data (128-bit UUID) | Idem pero UUID 128-bit |
|
|
|
|
### Flags Comunes
|
|
|
|
```c
|
|
#define ESP_BLE_ADV_FLAG_LIMIT_DISC 0x01 // Limited Discoverable Mode
|
|
#define ESP_BLE_ADV_FLAG_GEN_DISC 0x02 // General Discoverable Mode
|
|
#define ESP_BLE_ADV_FLAG_BREDR_NOT_SPT 0x04 // BR/EDR Not Supported
|
|
#define ESP_BLE_ADV_FLAG_DMR_CONTROLLER 0x08 // Dual Mode Controller
|
|
#define ESP_BLE_ADV_FLAG_DMR_HOST 0x10 // Dual Mode Host
|
|
```
|
|
|
|
**Ejemplo de payload de advertising completo:**
|
|
|
|
```
|
|
01 01 06 → Flags: 0x06 (General Discoverable, BR/EDR Not Supported)
|
|
09 09 45 53 50 33 32 5F 42 4C 45 → Name: "ESP32_BLE" (9 bytes)
|
|
02 0A F4 → TX Power: -12 dBm (0xF4 = -12 en two's complement)
|
|
|
|
Total: 15 bytes de 31 disponibles
|
|
```
|
|
|
|
---
|
|
|
|
## 1.3 Beacons: Tipos y Formatos
|
|
|
|
Los **beacons** son dispositivos que no necesitan conexión; solo transmiten datos periódicamente.
|
|
|
|
### iBeacon (Apple)
|
|
|
|
**Formato:**
|
|
```
|
|
Company ID (Apple 0x004C) + Type + Major + Minor + TX Power
|
|
2B + 1B + 2B + 2B + 1B
|
|
```
|
|
|
|
**Ejemplo en Service Data:**
|
|
```c
|
|
uint8_t adv_data[] = {
|
|
0x02, 0x01, 0x06, // Flags
|
|
0x1A, 0xFF, // Service Data type
|
|
0x4C, 0x00, // Company ID: 0x004C (Apple)
|
|
0x02, 0x15, // iBeacon type (0x02) + version (0x15)
|
|
// UUID: 12345678-1234-5678-1234-56789ABCDEF0
|
|
0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78,
|
|
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0,
|
|
0x12, 0x34, // Major: 0x1234
|
|
0x56, 0x78, // Minor: 0x5678
|
|
0xC3 // TX Power: -61 dBm
|
|
};
|
|
```
|
|
|
|
### Eddystone (Google/open)
|
|
|
|
Más flexible que iBeacon. Formatos:
|
|
|
|
- **Eddystone-URL**: URL corta (prefijo comprimido).
|
|
- **Eddystone-UID**: UUID 16B + instancia (sensores).
|
|
- **Eddystone-TLM**: Telemetría (batería, temp).
|
|
- **Eddystone-EID**: Ephemeral ID (privacidad).
|
|
|
|
---
|
|
|
|
## 1.4 API: Bluedroid Beacon
|
|
|
|
### Código Ejemplo (Conceptual)
|
|
|
|
```c
|
|
#include "esp_gatt_defs.h"
|
|
#include "esp_gap_ble_api.h"
|
|
#include "esp_bt.h"
|
|
|
|
// Advertising data: Name + TX Power
|
|
static uint8_t adv_data[] = {
|
|
0x02, 0x01, 0x06, // Flags
|
|
0x09, 0x09, 'B', 'L', 'U', 'E', 'D', 'R', 'O', 'I', 'D', // Name
|
|
0x02, 0x0A, 0xF4 // TX Power: -12 dBm
|
|
};
|
|
static uint8_t scan_rsp_data[] = {
|
|
0x02, 0x0A, 0xF4, // TX Power
|
|
0x0B, 0x09, 'h', 't', 't', 'p', 's', ':', '/', '/', 'e', 's', 'p'
|
|
};
|
|
|
|
// GAP Callback
|
|
static void esp_gap_cb(esp_gap_ble_cb_event_t event,
|
|
esp_ble_gap_cb_param_t *param) {
|
|
switch (event) {
|
|
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
|
|
ESP_LOGI("GAP", "Advertising data set");
|
|
esp_ble_gap_start_advertising(&adv_params);
|
|
break;
|
|
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
|
if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
|
|
ESP_LOGI("GAP", "Advertising started");
|
|
}
|
|
break;
|
|
// ... más eventos
|
|
}
|
|
}
|
|
|
|
void app_main(void) {
|
|
// Inicializar NVS
|
|
esp_err_t ret = nvs_flash_init();
|
|
if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
|
|
nvs_flash_erase();
|
|
nvs_flash_init();
|
|
}
|
|
|
|
// Liberar memoria de Classic BT
|
|
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
|
|
|
|
// Init Bluetooth
|
|
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
|
esp_bt_controller_init(&bt_cfg);
|
|
esp_bt_controller_enable(ESP_BT_MODE_BLE);
|
|
|
|
// Init Bluedroid
|
|
esp_bluedroid_init();
|
|
esp_bluedroid_enable();
|
|
|
|
// Registrar callback GAP
|
|
esp_ble_gap_register_callback(esp_gap_cb);
|
|
|
|
// Configurar parámetros de advertising
|
|
esp_ble_adv_params_t adv_params = {
|
|
.adv_int_min = 0x0020, // 20 ms
|
|
.adv_int_max = 0x0040, // 40 ms
|
|
.adv_type = ADV_TYPE_NONCONN_IND, // Non-connectable
|
|
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
|
|
.channel_map = ADV_CHNL_ALL,
|
|
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
|
|
};
|
|
|
|
// Establecer datos de advertising
|
|
esp_ble_gap_config_adv_data_raw(adv_data, sizeof(adv_data));
|
|
esp_ble_gap_config_scan_rsp_data_raw(scan_rsp_data, sizeof(scan_rsp_data));
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 1.5 API: NimBLE Beacon
|
|
|
|
### Código Ejemplo (Conceptual)
|
|
|
|
```c
|
|
#include "host/ble_hs.h"
|
|
#include "host/util/util.h"
|
|
#include "nimble/nimble_port.h"
|
|
|
|
static uint8_t own_addr_type;
|
|
|
|
// Inicializar GAP
|
|
static int adv_init(void) {
|
|
struct ble_svc_gap_params gap_params;
|
|
struct ble_hs_cfg cfg;
|
|
struct ble_gap_adv_params adv_params;
|
|
struct ble_gap_adv_set_fields adv_fields;
|
|
|
|
// Asegurar dirección válida
|
|
ble_hs_util_ensure_addr(0);
|
|
ble_hs_id_infer_auto(0, &own_addr_type);
|
|
|
|
// Formato de datos de advertising
|
|
memset(&adv_fields, 0, sizeof(adv_fields));
|
|
|
|
// Flags
|
|
adv_fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
|
|
|
// Nombre
|
|
adv_fields.name = (uint8_t *)"NimBLE_Beacon";
|
|
adv_fields.name_len = strlen("NimBLE_Beacon");
|
|
adv_fields.name_is_complete = 1;
|
|
|
|
// TX Power
|
|
adv_fields.tx_pwr_lvl_is_present = 1;
|
|
adv_fields.tx_pwr_lvl = -12; // dBm
|
|
|
|
rc = ble_gap_adv_set_fields(&adv_fields);
|
|
if (rc != 0) {
|
|
ESP_LOGE("ADV", "ble_gap_adv_set_fields failed: %d", rc);
|
|
return rc;
|
|
}
|
|
|
|
// Configurar parámetros de advertising
|
|
memset(&adv_params, 0, sizeof(adv_params));
|
|
adv_params.conn_mode = BLE_GAP_CONN_MODE_NON; // Non-connectable
|
|
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; // General discoverable
|
|
adv_params.itvl_min = BLE_GAP_ADV_ITVL_MS(20); // 20 ms
|
|
adv_params.itvl_max = BLE_GAP_ADV_ITVL_MS(40); // 40 ms
|
|
|
|
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
|
|
NULL, NULL);
|
|
if (rc != 0) {
|
|
ESP_LOGE("ADV", "ble_gap_adv_start failed: %d", rc);
|
|
return rc;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Sync callback (cuando NimBLE está listo)
|
|
static void ble_app_on_sync(void) {
|
|
int rc = adv_init();
|
|
assert(rc == 0);
|
|
}
|
|
|
|
void app_main(void) {
|
|
// Inicializar NVS
|
|
esp_err_t ret = nvs_flash_init();
|
|
if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
|
|
nvs_flash_erase();
|
|
nvs_flash_init();
|
|
}
|
|
|
|
// Init Bluetooth
|
|
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
|
esp_bt_controller_init(&bt_cfg);
|
|
esp_bt_controller_enable(ESP_BT_MODE_BLE);
|
|
|
|
// Init NimBLE
|
|
nimble_port_init();
|
|
|
|
// Registrar callback sync
|
|
ble_hs_cfg.sync_cb = ble_app_on_sync;
|
|
|
|
// Iniciar stack NimBLE
|
|
nimble_port_freertos_init(BLE_HS_TASK_PRIORITY);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 1.6 Ejercicio Práctico 1: Beacon Custom
|
|
|
|
**Objetivo**: Crear un beacon que transmita datos custom (temperatura, estado sensor).
|
|
|
|
**Requisitos:**
|
|
1. Usa NimBLE.
|
|
2. Implementa Service Data con UUID 16-bit custom.
|
|
3. Transmite 2 bytes (temperatura + humedad mocked).
|
|
4. Advertising interval: 100 ms.
|
|
5. TX Power: -10 dBm.
|
|
|
|
**Pasos:**
|
|
1. Crea un proyecto nuevo en `examples/bluetooth/ble_beacon_custom/`.
|
|
2. Incluye lectura ADC simulada (temperatura mocked).
|
|
3. Actualiza datos cada 500 ms (sin reiniciar advertising).
|
|
4. Verifica en nRF Connect que los datos cambian.
|
|
|
|
---
|
|
|
|
# MÓDULO 2: CONNECTIONS & GATT
|
|
|
|
## 2.1 Ciclo de Vida de Conexión
|
|
|
|
### Central (Smartphone, Tuya App, etc.)
|
|
|
|
```
|
|
SCAN ──> DISCOVER ──> CONNECT ──> GATT ──> DISCONNECT
|
|
│ │ │ │
|
|
│ Busca │ Envía │ Espera │ Lee/
|
|
│ beacons │ LE_CONN │ CONNECT │ Escribe/
|
|
│ que se │ _REQ en │ _IND │ Notif
|
|
│ anuncian│ Ch 37/38/ │ │
|
|
│ │ 39 │ │
|
|
```
|
|
|
|
### Periférico (ESP32)
|
|
|
|
```
|
|
ADVERTISING ──> CONNECT_IND ──> CONNECTED ──> [GATT] ──> DISCONNECT
|
|
│ │ │
|
|
│ Transmite │ Central envia │ Intercambia
|
|
│ en 37/38/39 │ LE_CONN_REQ en │ atributos y
|
|
│ │ cualquier canal │ datos
|
|
│ │ (parámetros) │
|
|
```
|
|
|
|
---
|
|
|
|
## 2.2 Connection Request: Parámetros Clave
|
|
|
|
Cuando un central inicia conexión, negocia:
|
|
|
|
```c
|
|
struct ble_gap_conn_params {
|
|
uint16_t scan_itvl; // Scan interval (central)
|
|
uint16_t scan_window; // Scan window (central)
|
|
uint16_t itvl_min; // Min connection interval (periférico)
|
|
uint16_t itvl_max; // Max connection interval
|
|
uint16_t latency; // Peripheral latency
|
|
uint16_t supervision_timeout; // Supervision timeout
|
|
uint16_t min_ce_len; // Min connection event length
|
|
uint16_t max_ce_len; // Max connection event length
|
|
};
|
|
```
|
|
|
|
**Connection Interval**: Tiempo entre dos eventos de conexión.
|
|
- Rango: 7.5 ms - 4 s (en múltiplos de 1.25 ms).
|
|
- **Valores típicos:**
|
|
- Low latency: 7.5 - 30 ms (gaming, streaming).
|
|
- Normal: 30 - 100 ms (sensores, actuadores).
|
|
- Battery: 1 - 4 s (wearables, medidores).
|
|
|
|
**Peripheral Latency**: Nodo puede ignorar N connection events.
|
|
- Ej: latency=199 en 1s interval = nodo responde cada 200s (5 minutos).
|
|
- **Trade-off**: Menor consumo vs. menor responsividad.
|
|
|
|
**Supervision Timeout**: Máximo tiempo sin recibir datos antes de desconexión.
|
|
- Debe ser > (1 + latency) * interval_max * 2.
|
|
- Ej: interval=100ms, latency=199 → timeout >= 40 segundos.
|
|
|
|
---
|
|
|
|
## 2.3 MTU Negotiation
|
|
|
|
**MTU (Maximum Transmission Unit)**: Máximo tamaño de datos útil en ATT.
|
|
|
|
```
|
|
┌──────────────┬────────────┬─────────────────┐
|
|
│ Link Layer │ L2CAP HDR │ ATT Data │
|
|
│ (4 bytes) │ (4 bytes) │ (= MTU - 3) │
|
|
└──────────────┴────────────┴─────────────────┘
|
|
│
|
|
Total ≤ 251 bytes (BLE packet limit)
|
|
```
|
|
|
|
- **Default MTU**: 23 bytes → 20 bytes de datos útil.
|
|
- **Max MTU**: 251 bytes → 248 bytes de datos útil.
|
|
|
|
**Negociación:**
|
|
```
|
|
Client → Server: MTU Exchange Request (MTU_desired = 128)
|
|
Server → Client: MTU Exchange Response (MTU_server = 251)
|
|
Negociado: MTU = min(128, 251) = 128
|
|
```
|
|
|
|
**En NimBLE:**
|
|
```c
|
|
// Configurar MTU esperado durante init
|
|
ble_hs_cfg.att_max_mtu = 251;
|
|
|
|
// En runtime, solicitar MTU desde central
|
|
ble_gattc_exchange_mtu(conn_handle);
|
|
```
|
|
|
|
---
|
|
|
|
## 2.4 GATT Services y Characteristics
|
|
|
|
### Estructura Jerárquica
|
|
|
|
```
|
|
┌────────────────────────────┐
|
|
│ SERVICE (HRS 0x180D) │
|
|
│ ├─ Start Handle: 1 │
|
|
│ └─ End Handle: 10 │
|
|
├────────────────────────────┤
|
|
│ CHARACTERISTIC 1 (0x2A37) │
|
|
│ ├─ Handle: 2 │
|
|
│ ├─ Value Handle: 3 │
|
|
│ ├─ Properties: R,N │
|
|
│ │ │
|
|
│ └─ DESCRIPTOR CCCD │
|
|
│ ├─ Handle: 4 │
|
|
│ └─ Value: [0x00, 0x00] │
|
|
├────────────────────────────┤
|
|
│ CHARACTERISTIC 2 (0x2A38) │
|
|
│ ├─ Handle: 5 │
|
|
│ ├─ Value Handle: 6 │
|
|
│ └─ Properties: R │
|
|
└────────────────────────────┘
|
|
```
|
|
|
|
### Declaración de Servicio (Bluedroid)
|
|
|
|
```c
|
|
// Tabla de atributos
|
|
static const esp_gatts_attr_db_t gatt_db[GATTS_CHAR_IDX_NB] = {
|
|
// Declaración de servicio
|
|
[0] = {
|
|
{ESP_GATT_AUTO_RSP},
|
|
{
|
|
ESP_UUID_LEN_16,
|
|
(uint8_t *)&primary_service_uuid, // 0x2800
|
|
ESP_GATT_PERM_READ,
|
|
2, // 16-bit UUID de servicio
|
|
(uint8_t *)&service_uuid // 0x180D (HRS)
|
|
}
|
|
},
|
|
// Declaración de característica
|
|
[1] = {
|
|
{ESP_GATT_AUTO_RSP},
|
|
{
|
|
ESP_UUID_LEN_16,
|
|
(uint8_t *)&character_declaration_uuid, // 0x2803
|
|
ESP_GATT_PERM_READ,
|
|
1, // Properties
|
|
(uint8_t *)&char_prop_read_notify // 0x12
|
|
}
|
|
},
|
|
// Valor de característica
|
|
[2] = {
|
|
{ESP_GATT_AUTO_RSP},
|
|
{
|
|
ESP_UUID_LEN_16,
|
|
(uint8_t *)&heart_rate_char_uuid, // 0x2A37
|
|
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
|
|
8,
|
|
(uint8_t *)&heart_rate_value // [BPM]
|
|
}
|
|
},
|
|
// CCCD Descriptor
|
|
[3] = {
|
|
{ESP_GATT_AUTO_RSP},
|
|
{
|
|
ESP_UUID_LEN_16,
|
|
(uint8_t *)&character_client_config_uuid, // 0x2902
|
|
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
|
|
2,
|
|
(uint8_t *)&cccd_value // [0x00, 0x00]
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### Declaración de Servicio (NimBLE)
|
|
|
|
```c
|
|
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
|
|
{
|
|
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
|
.uuid = BLE_UUID16_DECLARE(0x180D), // Heart Rate Service
|
|
.characteristics = (struct ble_gatt_chr_def[]) {
|
|
{
|
|
.uuid = BLE_UUID16_DECLARE(0x2A37), // HRM
|
|
.access_cb = gatt_svr_chr_access,
|
|
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
|
|
.descriptors = (struct ble_gatt_dsc_def[]) {
|
|
{
|
|
.uuid = BLE_UUID16_DECLARE(BLE_GATT_DSC_CLT_CFG_UUID16),
|
|
.access_cb = gatt_svr_dsc_access,
|
|
.flags = BLE_GATT_DSC_F_WRITE,
|
|
},
|
|
{0}
|
|
},
|
|
},
|
|
{0}
|
|
},
|
|
},
|
|
{0}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 2.5 Notificaciones vs Indicaciones
|
|
|
|
| Propiedad | Notificación | Indicación |
|
|
|-----------|--------------|-----------|
|
|
| **Confirmación** | No | Sí |
|
|
| **Velocidad** | Rápida | Más lenta |
|
|
| **Fiabilidad** | Sin garantía | Garantizada |
|
|
| **Caso de uso** | Streaming datos | Eventos críticos |
|
|
| **Evento NimBLE** | `BLE_GAP_EVENT_NOTIFY_TX` | `BLE_GAP_EVENT_CONN_UPDATE` |
|
|
|
|
**Flujo de Indicación:**
|
|
```
|
|
Peripheral → INDICATE → Central
|
|
│
|
|
▼ Procesa dato
|
|
│
|
|
▼
|
|
Peripheral ← CONFIRM ← Central
|
|
↓
|
|
Evento: BLE_GAP_EVENT_INDICATE_TX
|
|
```
|
|
|
|
**Flujo de Notificación:**
|
|
```
|
|
Peripheral → NOTIFY → Central
|
|
(sin confirmación)
|
|
```
|
|
|
|
---
|
|
|
|
## 2.6 Ejercicio Práctico 2: GATT Server + LED Control
|
|
|
|
**Objetivo**: Crear un periférico que expone dos características:
|
|
1. **LED Control** (write): Escribe 0x00 = OFF, 0x01 = ON.
|
|
2. **Button Status** (read + notify): Lee estado de botón cada 1 segundo.
|
|
|
|
**Requisitos:**
|
|
1. Usa NimBLE.
|
|
2. Servicios:
|
|
- Automation IO service (custom UUID 128-bit).
|
|
- LED write characteristic.
|
|
- Button read characteristic (con CCCD).
|
|
3. Connection parameters: interval 30 ms, latency 0, timeout 3 s.
|
|
4. MTU: 128 bytes.
|
|
|
|
**Pasos:**
|
|
1. Configura GPIO (LED en Pin 2, Button en Pin 0).
|
|
2. Implementa `gatt_svr_chr_access` para manejar reads/writes.
|
|
3. Crea task que envíe notificaciones cada 1 segundo.
|
|
4. Verifica en nRF Connect.
|
|
|
|
---
|
|
|
|
# MÓDULO 3: SECURITY & ADVANCED TOPICS
|
|
|
|
## 3.1 Flujo de Pairing (LESC)
|
|
|
|
### Fase 1: Exchange Public Keys
|
|
|
|
```
|
|
Central (Smartphone) Peripheral (ESP32)
|
|
│ │
|
|
├─ Pairing Request ────────> │
|
|
│ (IO cap, auth req, etc.) │
|
|
│ │
|
|
│ <──── Pairing Response ────┤
|
|
│ (IO cap, bonding flag) │
|
|
│ │
|
|
├─ Public Key (ECDH) ──────> │ [~160 ms blocking]
|
|
│ │
|
|
│ <── Public Key (ECDH) ─────┤
|
|
```
|
|
|
|
### Fase 2: Authentication
|
|
|
|
Depende de IO capabilities:
|
|
|
|
```
|
|
Numeric Comparison:
|
|
Ambos generan random: 0-999999
|
|
Ambos muestran 6 dígitos
|
|
User confirma: "¿Coinciden?" → Yes/No
|
|
|
|
Passkey Entry:
|
|
Central muestra: 123456
|
|
Periférico: Entrada en UART → 123456
|
|
|
|
Just Works:
|
|
Sin confirmación (sin MITM)
|
|
```
|
|
|
|
### Fase 3: Key Generation
|
|
|
|
```
|
|
Shared Secret (DHKey) = d_central * Public_Key_periférico
|
|
= d_periférico * Public_Key_central
|
|
|
|
LTK (Long Term Key):
|
|
= PRES(DHKey, 256-bit salt, identifiers)
|
|
[Derivación de 128 bits]
|
|
|
|
CSRK (Connection Signature Resolving Key):
|
|
= Generado para firmar datos [opcionalidad]
|
|
|
|
IRK (Identity Resolving Key):
|
|
= Permite resolver direcciones privadas
|
|
```
|
|
|
|
---
|
|
|
|
## 3.2 NVS: Almacenamiento de Bonding
|
|
|
|
**NVS (Non-Volatile Storage)**: Partición en flash donde persisten pares.
|
|
|
|
**Estructura en ESP32:**
|
|
```
|
|
Flash
|
|
├─ Partition: nvs
|
|
│ ├─ Namespace: peer_0
|
|
│ │ ├─ ltk (16 bytes)
|
|
│ │ ├─ ediv (2 bytes)
|
|
│ │ ├─ rand (8 bytes)
|
|
│ │ ├─ csrk (16 bytes)
|
|
│ │ ├─ irk (16 bytes)
|
|
│ │ └─ identity_address (6 bytes)
|
|
│ │
|
|
│ ├─ Namespace: peer_1
|
|
│ └─ ...
|
|
```
|
|
|
|
**API NimBLE:**
|
|
```c
|
|
// Almacenar automáticamente al parear
|
|
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
|
|
|
// Restaurar bonded devices al reconnectar
|
|
static void ble_app_on_sync(void) {
|
|
// NimBLE automáticamente busca pares en NVS
|
|
// Si encontrada identidad, hace "reconnect with encryption"
|
|
}
|
|
|
|
// Limpiar bonding
|
|
ble_store_util_delete_all();
|
|
|
|
// Listar pares
|
|
ble_store_util_status_rr(); // Imprime en logs
|
|
```
|
|
|
|
---
|
|
|
|
## 3.3 LE Secure Connections vs Legacy
|
|
|
|
### Legacy Pairing (BLE 4.0-4.1)
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Vulnerabilidad: TK débil, algoritmo simple│
|
|
│ → KNOB attack: downgrade a 3-DES débil │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### LE Secure Connections (BLE 4.2+)
|
|
|
|
```
|
|
┌──────────────────────────────────────────────┐
|
|
│ ✓ ECDH robusta (P-256 curve) │
|
|
│ ✓ Immune a KNOB (key length siempre 128 bit)│
|
|
│ ✓ Numeric Comparison sin vulnerabilidades │
|
|
│ ✗ Bloquea ~160 ms durante pairing (ROM) │
|
|
└──────────────────────────────────────────────┘
|
|
```
|
|
|
|
**En ESP32, LESC es default. No la deshabilites a menos que requieras legacy.**
|
|
|
|
---
|
|
|
|
## 3.4 Privacy: Dirección Privada Resolvible (RPA)
|
|
|
|
### Problema
|
|
|
|
Un central puede **trackear** un periférico por su MAC fijo.
|
|
|
|
### Solución: RPA (Resolvable Private Address)
|
|
|
|
Dirección de 48 bits generada dinámicamente:
|
|
```
|
|
┌─────────────┬──────────────────────┐
|
|
│ Hash (16 b) │ Random (32 b) │
|
|
│ 2 MSB = 01 │ (lower 2 bits = 01) │
|
|
└─────────────┴──────────────────────┘
|
|
```
|
|
|
|
**Hash = AES(IRK, Random_part)[0:15]**
|
|
|
|
- Cada conexión genera RPA nueva.
|
|
- Central con IRK bonded puede resolver dirección.
|
|
- Sin IRK, imposible trackear.
|
|
|
|
**En NimBLE:**
|
|
```c
|
|
// Forzar RPA (privacidad máxima)
|
|
ble_hs_cfg.rpa_timeout = 60; // Regenerar cada 60 s
|
|
ble_hs_cfg.sm_use_rpa = 1; // Use RPA en lugar de dirección fija
|
|
|
|
// En periférico
|
|
ble_gap_set_addr_type(BLE_ADDR_TYPE_RPA_RANDOM_DEFAULT);
|
|
```
|
|
|
|
---
|
|
|
|
## 3.5 Herramientas de Debugging de Seguridad
|
|
|
|
### Extraer LTK de iOS (post-pairing)
|
|
|
|
1. **Xcode Bluetooth Packet Logger** (Mac):
|
|
```bash
|
|
# Busca: "LE Start Encryption"
|
|
# En la expansión, observa: LTK = [16 bytes]
|
|
```
|
|
|
|
2. **Android HCI Snoop** (developer options):
|
|
```bash
|
|
adb pull /sdcard/btsnoop_hci.log
|
|
# Abre en Wireshark, filtra por "Start Encryption"
|
|
```
|
|
|
|
### Monitoreo en Tiempo Real (ESP32)
|
|
|
|
```c
|
|
// Log pairing state
|
|
static void gatt_svr_subscribe_cb(struct ble_gap_event *event, void *arg) {
|
|
if (event->type == BLE_GAP_EVENT_SUBSCRIBE) {
|
|
ESP_LOGI("SECURITY", "Subscribe flags: 0x%04x",
|
|
event->subscribe.cur_notify);
|
|
}
|
|
}
|
|
|
|
// Log encryption state
|
|
case BLE_GAP_EVENT_ENC_CHANGE:
|
|
ESP_LOGI("SECURITY", "Encrypted: %d",
|
|
event->enc_change.encrypted);
|
|
break;
|
|
```
|
|
|
|
---
|
|
|
|
## 3.6 Ejercicio Práctico 3: Periférico Seguro con Bonding
|
|
|
|
**Objetivo**: Crear un dispositivo que requiere LESC + Passkey, almacena bonding y re-encripta automáticamente.
|
|
|
|
**Requisitos:**
|
|
1. NimBLE + LESC activado (default).
|
|
2. Passkey Entry (periférico muestra código).
|
|
3. Almacenamiento bonding en NVS.
|
|
4. Característica protegida: requiere encriptación.
|
|
5. Desconectar si intenta acceso sin encripción.
|
|
|
|
**Pasos:**
|
|
1. Configura Security Manager:
|
|
```c
|
|
ble_hs_cfg.sm_mitm = 1;
|
|
ble_hs_cfg.sm_bonding = 1;
|
|
ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_DISP_ONLY;
|
|
```
|
|
|
|
2. Implementa callback `BLE_GAP_EVENT_PASSKEY_ACTION`.
|
|
|
|
3. En `gatt_svr_chr_access`, valida `event->access.op` y rechaza si no cifrado.
|
|
|
|
4. Test: Parear, desconectar, reconectar sin re-introducir código.
|
|
|
|
---
|
|
|
|
## 3.7 Coexistencia WiFi-BLE y PSRAM
|
|
|
|
### El Problema
|
|
|
|
ESP32 tiene un **controller compartido** para WiFi y BLE. Ambos usan la banda 2.4 GHz.
|
|
|
|
**Collision mitigation:**
|
|
- Algoritmo de frequency hopping separa canales.
|
|
- BLE tiene prioridad sobre WiFi en ciertos eventos críticos.
|
|
- Aumenta latencia WiFi durante transferencias BLE.
|
|
|
|
### Estrategia de Optimización
|
|
|
|
```c
|
|
// Config: Asignar más tiempo a BLE si es crítico
|
|
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
|
bt_cfg.ble_ll_resolv_list_size = 0; // Liberar RAM si no usas privacidad
|
|
esp_bt_controller_init(&bt_cfg);
|
|
|
|
// O usar PSRAM para buffers del stack
|
|
ble_hs_cfg.ble_l2cap_mtu = 251; // Reduce MTU default si hay presión RAM
|
|
```
|
|
|
|
### PSRAM (Pseudo SRAM)
|
|
|
|
ESP32 puede tener hasta 4 MB de PSRAM externo. **Acceso es lento (2-5x que IRAM)**, pero útil para:
|
|
- Buffers grandes.
|
|
- Modelos ML pequeños.
|
|
|
|
**En contexto de BLE:**
|
|
- No recomendable para buffers de paquetes (latencia).
|
|
- Sí para almacenamiento de bonding LUTs si muchos pares.
|
|
|
|
---
|
|
|
|
## 3.8 ESP32 Memory Deep Dive
|
|
|
|
### Layout de Memoria
|
|
|
|
```
|
|
IRAM (Instruction RAM): 192 KB total
|
|
├─ 64 KB: PRO/APP cache MMU
|
|
├─ 128 KB: Ejecutable + stack tasks + heap crítico
|
|
│ ├─ NimBLE task: ~8 KB stack
|
|
│ ├─ Application task: ~4 KB
|
|
│ └─ Heap: Resto
|
|
|
|
DRAM (Data RAM): 320 KB total
|
|
├─ 160 KB: Estáticos (.data + .bss)
|
|
├─ 160 KB: Heap runtime (rest)
|
|
|
|
FLASH (External): 4 MB típico
|
|
├─ Partition: app (1.2 MB código)
|
|
├─ Partition: nvs (64 KB bonding)
|
|
└─ Partition: rest (available)
|
|
```
|
|
|
|
### Impacto en BLE
|
|
|
|
| Stack | IRAM | DRAM Estático | Heap Init |
|
|
|-------|------|--------------|-----------|
|
|
| **Bluedroid** | ~60 KB | ~40 KB | ~200 KB |
|
|
| **NimBLE** | ~40 KB | ~20 KB | ~70 KB |
|
|
|
|
**Conclusión**: NimBLE deja 130 KB más para ML models. Crítico para Edge AI.
|
|
|
|
---
|
|
|
|
# DEBUGGING MASTERCLASS
|
|
|
|
## Debugging 1: Logging Básico (Siempre Disponible)
|
|
|
|
### ESP-IDF Monitor
|
|
|
|
```bash
|
|
idf.py -p /dev/ttyUSB0 monitor
|
|
```
|
|
|
|
**Dentro del monitor:**
|
|
```
|
|
# Cambiar nivel de log
|
|
Ctrl+T Ctrl+L → Choose log level → ble (default DEBUG)
|
|
|
|
# Reset
|
|
Ctrl+T Ctrl+R
|
|
|
|
# Salir
|
|
Ctrl+]
|
|
```
|
|
|
|
### Filtrar Logs por Módulo
|
|
|
|
```c
|
|
// En código
|
|
esp_log_level_set("BLE_GATT", ESP_LOG_DEBUG); // Solo GATT debug
|
|
esp_log_level_set("*", ESP_LOG_INFO); // Todo a INFO
|
|
|
|
// En menuconfig
|
|
idf.py menuconfig
|
|
→ Component config → Log output → Default log level → DEBUG
|
|
→ Bluetooth → Log level → Debug
|
|
```
|
|
|
|
### Ejemplo de Logging Custom
|
|
|
|
```c
|
|
#include "esp_log.h"
|
|
|
|
static const char *TAG = "MY_BLE_APP";
|
|
|
|
void my_function() {
|
|
ESP_LOGD(TAG, "Debug: valor=%d", 42);
|
|
ESP_LOGI(TAG, "Info: evento=%s", "connected");
|
|
ESP_LOGW(TAG, "Warning: mtu bajo");
|
|
ESP_LOGE(TAG, "Error: code=%d", error_code);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging 2: GDB + OpenOCD (JTAG Hardware Required)
|
|
|
|
### Hardware Setup
|
|
|
|
**Requerimientos:**
|
|
- **JTAG Adapter**: ESP-PROG, Jlink, ST-Link, o Raspberry Pi.
|
|
- **5 Cables**: TDI, TDO, TCK, TMS, GND (+ SRST opcional).
|
|
|
|
**ESP32 JTAG Pins:**
|
|
```
|
|
GPIO12 → TDI
|
|
GPIO13 → TCK
|
|
GPIO14 → TDO
|
|
GPIO15 → TMS
|
|
GND → GND
|
|
```
|
|
|
|
### Instalación OpenOCD
|
|
|
|
```bash
|
|
# Linux (Debian/Ubuntu)
|
|
sudo apt-get install openocd
|
|
|
|
# macOS
|
|
brew install openocd
|
|
|
|
# Verify
|
|
openocd --version
|
|
```
|
|
|
|
### Iniciar OpenOCD
|
|
|
|
```bash
|
|
# Para ESP-PROG en Linux
|
|
openocd -f interface/ftdi/esp32_devkitj_v1.cfg \
|
|
-f target/esp32.cfg
|
|
|
|
# Para ST-Link en Windows
|
|
openocd -f interface/stlink.cfg \
|
|
-f target/esp32.cfg
|
|
|
|
# Verás algo como:
|
|
# Info : Listening on port 3333 for gdb connections
|
|
# Info : Listening on port 6666 for telnet connections
|
|
```
|
|
|
|
### Conectar GDB
|
|
|
|
**Terminal 1 (OpenOCD ejecutándose):**
|
|
```bash
|
|
openocd -f interface/esp32_prog.cfg -f target/esp32.cfg
|
|
```
|
|
|
|
**Terminal 2 (GDB):**
|
|
```bash
|
|
cd your_project_dir
|
|
|
|
# Opción 1: Automático (desde ESP-IDF)
|
|
idf.py gdb
|
|
|
|
# Opción 2: Manual
|
|
xtensa-esp32-elf-gdb build/your_app.elf
|
|
|
|
# En GDB console:
|
|
(gdb) target remote localhost:3333
|
|
(gdb) mon reset halt # Halt on reset
|
|
(gdb) thb app_main # Breakpoint temporal en app_main
|
|
(gdb) c # Continue
|
|
```
|
|
|
|
### Comandos GDB Útiles
|
|
|
|
```bash
|
|
# Breakpoints
|
|
(gdb) b function_name
|
|
(gdb) b file.c:123
|
|
(gdb) info breakpoints
|
|
(gdb) delete 1
|
|
|
|
# Stepping
|
|
(gdb) n # Next (step over)
|
|
(gdb) s # Step into function
|
|
(gdb) c # Continue
|
|
(gdb) finish # Run until return
|
|
|
|
# Inspection
|
|
(gdb) p variable # Print value
|
|
(gdb) p /x 0x1234 # Hex format
|
|
(gdb) x/32x 0x3ffb0000 # Examine memory (32 words hex)
|
|
(gdb) info registers # All registers
|
|
(gdb) bt # Backtrace (stack)
|
|
|
|
# FreeRTOS-aware (custom GDB macros)
|
|
(gdb) info tasks # List FreeRTOS tasks
|
|
(gdb) task 2 # Switch to task 2
|
|
```
|
|
|
|
### Debugging BLE Specific Issues
|
|
|
|
**Breakpoints en callbacks:**
|
|
```bash
|
|
(gdb) b esp_gap_cb # Breakpoint GAP callback
|
|
(gdb) b gatt_svr_chr_access # Breakpoint GATT access
|
|
|
|
# Cuando se para, inspecciona evento
|
|
(gdb) p event->type # Tipo de evento
|
|
(gdb) p event->connect.conn_handle # Handle conexión
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging 3: Análisis de Paquetes (Wireshark + HCI Sniffing)
|
|
|
|
### Opción A: Wireshark en Linux (sin hardware sniffer)
|
|
|
|
**Limitación**: Captura solo HCI local (advertiser), no conexiones activas.
|
|
|
|
```bash
|
|
# Instalar Wireshark + plugin Bluetooth
|
|
sudo apt-get install wireshark wireshark-plugin-btsnoop
|
|
|
|
# Ejecutar como root
|
|
sudo wireshark
|
|
|
|
# Capturar:
|
|
# 1. Capture → Interfaces
|
|
# 2. Selecciona "Bluetooth"
|
|
# 3. Start capturing
|
|
```
|
|
|
|
**Filtros útiles:**
|
|
```
|
|
btatt # Filter: Attribute Protocol
|
|
btle # Filter: BLE link layer
|
|
btatt.handle == 0x0003 # Characteristic handle
|
|
btatt.opcode == 0x0b # Write command
|
|
```
|
|
|
|
### Opción B: HCI Sniffing en Android
|
|
|
|
```bash
|
|
# Activar developer options
|
|
# → Bluetooth HCI snoop log: ON
|
|
|
|
# Ejecutar comando
|
|
adb pull /sdcard/btsnoop_hci.log
|
|
|
|
# Abrir en Wireshark
|
|
wireshark btsnoop_hci.log
|
|
```
|
|
|
|
### Opción C: Captura de Logs de Sincronización (ESP32 Built-in)
|
|
|
|
```bash
|
|
# Compilar con debug habilitado
|
|
idf.py menuconfig
|
|
# → Component config → Bluetooth → Enable debug logs
|
|
# → Log Level → Debug
|
|
|
|
# En monitor, buscar:
|
|
# [HCI] LE Create Connection Complete
|
|
# [GATT] Write characteristic
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging 4: Profiling y Análisis de Performance
|
|
|
|
### Task CPU Usage
|
|
|
|
```c
|
|
#include "freertos/task.h"
|
|
|
|
void print_task_stats(void) {
|
|
char* buffer = malloc(1024);
|
|
vTaskList(buffer);
|
|
printf("%s\n", buffer);
|
|
free(buffer);
|
|
}
|
|
|
|
// Output:
|
|
// Task Name State Priority Stack Num
|
|
// nimble_host_task X 100 4096 123
|
|
// IDLE R 0 1024 0
|
|
```
|
|
|
|
### Heap Usage
|
|
|
|
```c
|
|
#include "esp_heap_caps.h"
|
|
|
|
void print_heap_stats(void) {
|
|
printf("Internal DRAM: %d bytes free\n",
|
|
heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
|
|
|
|
printf("Internal IRAM: %d bytes free\n",
|
|
heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_32BIT));
|
|
}
|
|
```
|
|
|
|
### Timing Analysis
|
|
|
|
```c
|
|
#include "esp_timer.h"
|
|
|
|
void measure_operation(void) {
|
|
int64_t start = esp_timer_get_time(); // microseconds
|
|
|
|
// [Operation]
|
|
|
|
int64_t elapsed_us = esp_timer_get_time() - start;
|
|
ESP_LOGI(TAG, "Operation took %.2f ms", elapsed_us / 1000.0);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging 5: Clang + AddressSanitizer (ASAN)
|
|
|
|
### Enable AddressSanitizer
|
|
|
|
```bash
|
|
idf.py menuconfig
|
|
# → Component config → Compiler options → Enable AddressSanitizer
|
|
```
|
|
|
|
### Build & Run
|
|
|
|
```bash
|
|
idf.py build flash monitor
|
|
```
|
|
|
|
**Output de memory error:**
|
|
```
|
|
=================================================================
|
|
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on unknown address
|
|
Read of size 4 at 0x... Thread T1
|
|
#0 in some_function ...
|
|
```
|
|
|
|
### Instrumentación Manual
|
|
|
|
```c
|
|
#include "sanitizer/asan_interface.h"
|
|
|
|
void unsafe_access(void) {
|
|
uint8_t buffer[10];
|
|
|
|
// Marcar rango válido
|
|
ASAN_POISON_MEMORY_REGION(buffer + 10, 1);
|
|
|
|
// Esto triggea ASAN error si accede
|
|
buffer[15] = 0xFF; // Out of bounds!
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
# EJERCICIOS PRÁCTICOS PROGRESIVOS
|
|
|
|
## Ejercicio 1: Beacon Personalizado (Nivel: Básico)
|
|
|
|
**Objetivo**: Dominar advertising y formatos de datos.
|
|
|
|
**Requisitos:**
|
|
- Transmitir temperatura simulada cada 100 ms.
|
|
- Usar Service Data con UUID 16-bit personalizado (0xAABB).
|
|
- Formato: `[temp_byte] [humidity_byte]`.
|
|
- Verificable en nRF Connect.
|
|
|
|
**Template:**
|
|
```c
|
|
static uint8_t service_data[] = {
|
|
0x02, 0x0A, 0xF4, // TX Power
|
|
0x07, 0x16, // Service Data 16-bit
|
|
0xBB, 0xAA, // UUID 0xAABB
|
|
25, // Temperature
|
|
60 // Humidity
|
|
};
|
|
|
|
// Update cada 100ms en timer
|
|
void update_adv_data(void) {
|
|
service_data[4] = read_temp();
|
|
service_data[5] = read_humidity();
|
|
ble_gap_adv_set_fields(&adv_fields);
|
|
}
|
|
```
|
|
|
|
**Verificación:**
|
|
1. Conecta a nRF Connect.
|
|
2. Abre Raw Data en advertising.
|
|
3. Confirma que hex cambia cada 100 ms.
|
|
|
|
---
|
|
|
|
## Ejercicio 2: GATT Server Interactivo (Nivel: Intermedio)
|
|
|
|
**Objetivo**: Implementar lectura/escritura de características + notificaciones.
|
|
|
|
**Requisitos:**
|
|
- Servicio custom (UUID 128-bit).
|
|
- 3 características:
|
|
1. **LED Control** (write): 0x00=OFF, 0x01=ON.
|
|
2. **Button State** (read+notify): 0x00/0x01.
|
|
3. **BPM Mock** (read+notify): Valor 60-100 BPM.
|
|
- BPM actualiza cada 2 segundos.
|
|
- Button emula presión cada 5 segundos.
|
|
|
|
**Pasos:**
|
|
1. Define UUID 128-bit de servicio:
|
|
```c
|
|
#define CUSTOM_SERVICE_UUID \
|
|
{0x12, 0x34, 0x56, 0x78, ...} // 16 bytes
|
|
```
|
|
|
|
2. Implementa task que actualiza BPM y Button.
|
|
|
|
3. Maneja `GATT_WRITE_EVT` para LED control.
|
|
|
|
4. En nRF Connect, verifica:
|
|
- Write LED → GPIO on/off.
|
|
- Read BPM → valor entre 60-100.
|
|
- Suscribirse a BPM → recibe notificaciones cada 2s.
|
|
|
|
---
|
|
|
|
## Ejercicio 3: Bonding + Security (Nivel: Avanzado)
|
|
|
|
**Objetivo**: Dominar seguridad LESC con persistencia.
|
|
|
|
**Requisitos:**
|
|
- LESC + Numeric Comparison.
|
|
- Almacenamiento bonding en NVS.
|
|
- Reinicio sin re-pairing.
|
|
- Rechazo de acceso no encriptado.
|
|
|
|
**Pasos:**
|
|
1. Configura SM:
|
|
```c
|
|
ble_hs_cfg.sm_mitm = 1;
|
|
ble_hs_cfg.sm_bonding = 1;
|
|
ble_hs_cfg.sm_sc = 1; // Secure Connections
|
|
```
|
|
|
|
2. Implementa passkey callback:
|
|
```c
|
|
case BLE_GAP_EVENT_PASSKEY_ACTION:
|
|
ESP_LOGI("SEC", "Passkey: %06d",
|
|
event->passkey.params.action_type);
|
|
break;
|
|
```
|
|
|
|
3. Valida encriptación en GATT access:
|
|
```c
|
|
if (!ble_gap_conn_find(conn_handle, &desc)) {
|
|
if (!desc.sec_state.encrypted) {
|
|
return BLE_ATT_ERR_INSUFFICIENT_AUTHEN;
|
|
}
|
|
}
|
|
```
|
|
|
|
4. Test:
|
|
- Parear, confirmar passkey.
|
|
- Reboot, reconectar → sin passkey.
|
|
- Borrar bonding en ESP32 (`ble_store_util_delete_all()`).
|
|
- Reconectar → pide passkey nuevamente.
|
|
|
|
---
|
|
|
|
## Ejercicio 4: IoT Sensor Sync (Nivel: Avanzado+)
|
|
|
|
**Objetivo**: Integrar múltiples sensores + WiFi + BLE.
|
|
|
|
**Requisitos:**
|
|
- 1x Beacon BLE (temperatura).
|
|
- 1x GATT Server (BLE) con:
|
|
- Lectura ADC (temperatura real).
|
|
- Lectura RSSI de WiFi.
|
|
- Reset button.
|
|
- WiFi conectado a broker MQTT.
|
|
- Publicar datos por MQTT cada 10s.
|
|
- Recibir comandos por GATT + aplicar.
|
|
|
|
**Pasos:**
|
|
1. Setup ADC + WiFi + MQTT.
|
|
2. Crea beacon con temp actualizada.
|
|
3. Crea GATT server con:
|
|
- Service: Environmental Sensing (0x181A).
|
|
- Characteristic: Temperature (0x2A6E).
|
|
- Characteristic: Humidity (mock, 0x2A6F).
|
|
|
|
4. Escribe datos en NVS bajo etiqueta `iot_config`.
|
|
|
|
5. Implementa: Write characteristic para actualizar MQTT topic.
|
|
|
|
6. Test:
|
|
- BLE beacon muestra temp en tiempo real.
|
|
- GATT server lee sensores.
|
|
- MQTT publica cada 10s.
|
|
- Comando BLE actualiza MQTT topic.
|
|
|
|
---
|
|
|
|
# REFERENCIAS & RECURSOS
|
|
|
|
## Especificaciones Oficiales
|
|
|
|
- **Bluetooth Core Specification Vol 4 (BLE)**
|
|
- Part A: Architecture
|
|
- Part B: Link Layer
|
|
- Part D: ATT/GATT
|
|
- Part E: HCI
|
|
- Part H: Security
|
|
|
|
- **ESP32 Technical Reference Manual**
|
|
- Wireless, Memory, Timers
|
|
|
|
## Documentación
|
|
|
|
- **ESP-IDF Official Docs**: https://docs.espressif.com
|
|
- Bluetooth Low Energy API
|
|
- NimBLE Host
|
|
- JTAG Debugging
|
|
|
|
- **Apache NimBLE Project**: https://mynewt.apache.org
|
|
- API Reference
|
|
- Architecture Guide
|
|
|
|
## Herramientas de Desarrollo
|
|
|
|
- **nRF Connect** (mobile app): Escanear, conectar, debug BLE
|
|
- **Wireshark**: Análisis de paquetes BLE/HCI
|
|
- **OpenOCD**: JTAG debugger
|
|
- **GDB**: GNU Debugger
|
|
- **Ubertooth One**: Hardware BLE sniffer (avanzado)
|
|
|
|
## Repositorios Open-Source
|
|
|
|
- **esp-idf/examples/bluetooth**: Ejemplos oficiales
|
|
- **apache/mynewt-nimble**: Stack NimBLE
|
|
- **espressif/esp-idf**: Full ESP-IDF
|
|
- **h2zero/NimBLE-Arduino**: NimBLE para Arduino IDE
|
|
|
|
## Artículos Técnicos Recomendados
|
|
|
|
1. **"The Bluetooth Low Energy Protocol Stack: Understanding the Fundamentals"** - NoveBits
|
|
2. **"A Practical Guide to Debugging BLE Communication on iOS"** - NoveBits
|
|
3. **"Understanding the Architecture of the Bluetooth Low Energy Protocol"** - Analog Devices
|
|
4. **"BLE Frequency Hopping Algorithm Analysis"** - arxiv.org (si te interesa sniffing avanzado)
|
|
|
|
## Comunidades
|
|
|
|
- **ESP32 Community Forum**: esp32.com
|
|
- **Apache NimBLE Mailing List**: dev@mynewt.apache.org
|
|
- **Bluetooth SIG**: bluetooth.com (especificaciones)
|
|
|
|
---
|
|
|
|
## ROADMAP: Próximos Pasos para Edge AI + IoT + Blockchain
|
|
|
|
Con BLE + ESP32 dominados, el siguiente paso en tu roadmap:
|
|
|
|
1. **Quantización de Modelos ML**
|
|
- TensorFlow Lite Micro (< 1 MB, ~10 ms inferencia).
|
|
- Desplegar en ESP32 con BLE para actualizaciones federadas.
|
|
|
|
2. **Federated Learning sobre BLE**
|
|
- Múltiples ESP32 (sensores) entrenando modelo compartido.
|
|
- Coordinación por NimBLE + Bluetooth Mesh.
|
|
|
|
3. **Blockchain + BLE**
|
|
- Verificación de datos IoT en cadena (Substrate).
|
|
- Provenance de sensor data.
|
|
|
|
4. **Edge Inference Optimization**
|
|
- ONNX Runtime en ESP32.
|
|
- Comparación: nanoGPT vs DistilBERT en hardware limitado.
|
|
|
|
5. **Rust + CUDA para kernels críticos**
|
|
- Compilar NimBLE components en Rust (safety).
|
|
- CUDA kernels para training federated en nodos GPU (coordinadores).
|
|
|
|
---
|
|
|
|
**¡Felicidades por completar esta masterclass! Ahora eres experto en BLE + ESP32.** 🎓
|
|
|
|
Para profundizar, revisa los repos sugeridos e implementa los mini-proyectos. Cada iteración refuerza internalizacion de conceptos.
|